Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions codenib/wiki/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@
get_visual_evidence,
search_visual_context,
)
from .media_pipeline import build_multimodal_repository_knowledge
from .media_vlm import OpenAICompatibleVisualFactExtractor

__all__ = [
"WikiBuilder",
"OpenAICompatibleVisualFactExtractor",
"build_media_evidence_pack",
"build_multimodal_knowledge_view",
"build_multimodal_repository_knowledge",
"build_visual_facts_manifest",
"deterministic_visual_facts",
"discover_media_manifest",
Expand Down
72 changes: 72 additions & 0 deletions codenib/wiki/media_pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# SPDX-FileCopyrightText: 2025-2026 CodeNib Contributors
#
# SPDX-License-Identifier: Apache-2.0

"""End-to-end construction for multimodal repository knowledge."""

from __future__ import annotations

from pathlib import Path
from typing import Any, Mapping

from ..repository_source_selection import (
DEFAULT_REPOSITORY_SOURCE_SELECTION,
RepositorySourceSelection,
)
from .media_artifacts import discover_media_manifest
from .media_facts import VisualFactExtractor, build_visual_facts_manifest
from .media_grounding import (
discover_source_symbol_candidates,
ground_visual_facts_to_sources,
)
from .media_knowledge import build_multimodal_knowledge_view


def build_multimodal_repository_knowledge(
repo_path: str | Path,
*,
commit: str | None = None,
exclude_roots: tuple[str | Path, ...] = (),
selection: RepositorySourceSelection = DEFAULT_REPOSITORY_SOURCE_SELECTION,
extractor: VisualFactExtractor | None = None,
max_artifacts: int = 4096,
max_source_candidates: int = 8192,
) -> dict[str, Any]:
"""Build the deterministic multimodal repository knowledge bundle."""

media_manifest = discover_media_manifest(
repo_path,
commit=commit,
exclude_roots=exclude_roots,
selection=selection,
max_artifacts=max_artifacts,
)
facts_kwargs: dict[str, Any] = {}
if extractor is not None:
facts_kwargs["extractor"] = extractor
visual_facts_manifest = build_visual_facts_manifest(media_manifest, **facts_kwargs)
source_candidates = discover_source_symbol_candidates(
repo_path,
exclude_roots=exclude_roots,
selection=selection,
max_candidates=max_source_candidates,
)
grounding_manifest = ground_visual_facts_to_sources(
visual_facts_manifest,
source_candidates,
)
knowledge_view = build_multimodal_knowledge_view(
media_manifest,
visual_facts_manifest,
grounding_manifest,
)
return {
"media_manifest": media_manifest,
"visual_facts_manifest": visual_facts_manifest,
"source_candidate_count": len(source_candidates),
"grounding_manifest": grounding_manifest,
"knowledge_view": knowledge_view,
}


__all__ = ["build_multimodal_repository_knowledge"]
16 changes: 16 additions & 0 deletions docs/experiments/multimodal_repository_knowledge.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,22 @@ print(view["entry_count"])
This creates a deterministic local view. A VLM extractor can be added later by
passing a custom extractor into `build_visual_facts_manifest()`.

For callers that want the full deterministic pipeline in one step:

```python
from codenib.wiki import build_multimodal_repository_knowledge

bundle = build_multimodal_repository_knowledge(repo)
view = bundle["knowledge_view"]
```

The same deterministic bundle can be written from the command line:

```text
python scripts/build_multimodal_knowledge.py /path/to/repository \
--output /tmp/multimodal-knowledge.json
```

```python
from codenib.wiki import OpenAICompatibleVisualFactExtractor

Expand Down
72 changes: 72 additions & 0 deletions scripts/build_multimodal_knowledge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2025-2026 CodeNib Contributors
#
# SPDX-License-Identifier: Apache-2.0

"""Build a deterministic multimodal repository knowledge bundle."""

from __future__ import annotations

import argparse
import json
from pathlib import Path

from codenib.wiki import build_multimodal_repository_knowledge


def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("repo", help="Repository root to scan")
parser.add_argument(
"--output",
"-o",
required=True,
help="Path to write the multimodal knowledge bundle JSON",
)
parser.add_argument(
"--commit",
default=None,
help="Optional commit identity to record in the media manifest",
)
parser.add_argument(
"--max-artifacts",
type=int,
default=4096,
help="Maximum media artifacts to include",
)
parser.add_argument(
"--max-source-candidates",
type=int,
default=8192,
help="Maximum source-symbol candidates to consider for grounding",
)
return parser


def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
bundle = build_multimodal_repository_knowledge(
args.repo,
commit=args.commit,
max_artifacts=args.max_artifacts,
max_source_candidates=args.max_source_candidates,
)
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(
json.dumps(bundle, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
counts = {
"media_artifacts": bundle["media_manifest"]["artifact_count"],
"visual_fact_packs": bundle["visual_facts_manifest"]["fact_count"],
"source_candidates": bundle["source_candidate_count"],
"visual_code_bindings": bundle["grounding_manifest"]["binding_count"],
"knowledge_entries": bundle["knowledge_view"]["entry_count"],
}
print(json.dumps(counts, sort_keys=True))
return 0


if __name__ == "__main__":
raise SystemExit(main())
51 changes: 51 additions & 0 deletions test/scripts/test_build_multimodal_knowledge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: 2025-2026 CodeNib Contributors
#
# SPDX-License-Identifier: Apache-2.0

from __future__ import annotations

import json
import subprocess
import sys


def test_build_multimodal_knowledge_script_writes_bundle(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
(repo / "docs").mkdir()
(repo / "src").mkdir()
(repo / "docs" / "architecture.svg").write_text(
"<svg>WikiService</svg>",
encoding="utf-8",
)
(repo / "README.md").write_text(
"![WikiService architecture](docs/architecture.svg)",
encoding="utf-8",
)
(repo / "src" / "wiki.py").write_text(
"class WikiService: pass",
encoding="utf-8",
)
output = tmp_path / "bundle.json"

completed = subprocess.run(
[
sys.executable,
"scripts/build_multimodal_knowledge.py",
str(repo),
"--output",
str(output),
"--commit",
"abc123",
],
check=True,
stdout=subprocess.PIPE,
text=True,
)

counts = json.loads(completed.stdout)
bundle = json.loads(output.read_text(encoding="utf-8"))
assert counts["media_artifacts"] == 1
assert counts["knowledge_entries"] == 1
assert bundle["media_manifest"]["commit"] == "abc123"
assert bundle["knowledge_view"]["entry_count"] == 1
22 changes: 22 additions & 0 deletions test/wiki/test_media_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from codenib.wiki import (
build_multimodal_knowledge_view,
build_multimodal_repository_knowledge,
build_visual_facts_manifest,
discover_media_manifest,
discover_source_symbol_candidates,
Expand Down Expand Up @@ -57,3 +58,24 @@ def test_multimodal_repository_knowledge_pipeline(tmp_path):
assert view["entry_count"] == 1
assert search_visual_context(view, "IndexCompiler architecture")
assert find_visual_code_links(view, "src/compiler.py", symbol="IndexCompiler")


def test_build_multimodal_repository_knowledge_wraps_pipeline(tmp_path):
(tmp_path / "docs").mkdir()
(tmp_path / "src").mkdir()
(tmp_path / "docs" / "diagram.png").write_bytes(b"png")
(tmp_path / "README.md").write_text(
"![WikiService diagram](docs/diagram.png)",
encoding="utf-8",
)
(tmp_path / "src" / "wiki.py").write_text(
"class WikiService: pass",
encoding="utf-8",
)

bundle = build_multimodal_repository_knowledge(tmp_path, commit="abc123")

assert bundle["media_manifest"]["artifact_count"] == 1
assert bundle["visual_facts_manifest"]["fact_count"] == 1
assert bundle["source_candidate_count"] >= 1
assert bundle["knowledge_view"]["entry_count"] == 1
Loading