From 170bcb8973629447472578b85511b138cccdfc32 Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:07:23 +0300 Subject: [PATCH 01/12] test(eval): exclude changelog-style files from the corpus, not just from answers gen_queries documented that CHANGELOG_EXCLUDES was applied by the harness, but the harness never imported it: for a git-derived query set the changelog (and Flask-style CHANGES.rst, which the deny pattern also missed) stayed indexed and could outrank the implementation it paraphrases. Both lists now cover CHANGES*, HISTORY*, NEWS* and RELEASE_NOTES*, and build_corpus_index applies them. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- tests/eval/gen_queries.py | 12 ++++++++---- tests/eval/harness.py | 9 ++++++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/eval/gen_queries.py b/tests/eval/gen_queries.py index 1e0d0c8..286f16b 100644 --- a/tests/eval/gen_queries.py +++ b/tests/eval/gen_queries.py @@ -70,7 +70,7 @@ # Files that may be *changed* by a commit but are never a useful retrieval answer. _ANSWER_DENY_RE = re.compile( r"(?:^|/)(?:" - r"CHANGELOG[^/]*|HISTORY[^/]*|NEWS[^/]*|" + r"CHANGELOG[^/]*|CHANGES[^/]*|HISTORY[^/]*|NEWS[^/]*|RELEASE[_-]?NOTES[^/]*|" r"package-lock\.json|yarn\.lock|poetry\.lock|Cargo\.lock|requirements\.lock|" r"go\.sum|Gemfile\.lock|pnpm-lock\.yaml" r")$", @@ -90,9 +90,13 @@ # Benchmark scaffolding: a commit touching it must not become a benchmark query. _SCAFFOLD_RE = re.compile(r"(?:^|/)tests/(?:eval|benchmark_)", re.I) -# Corpus documents that paraphrase commit subjects. `harness` applies these on top -# of its own excludes whenever a git-derived query set is evaluated. -CHANGELOG_EXCLUDES = ("CHANGELOG*", "HISTORY*", "NEWS*", "**/CHANGELOG*") +# Corpus documents that paraphrase commit subjects. `harness.build_corpus_index` +# applies these on top of its own excludes for every corpus, so a changelog can +# never be the top lexical hit for a query that is literally its own line item. +CHANGELOG_EXCLUDES = ( + "CHANGELOG*", "CHANGES*", "HISTORY*", "NEWS*", "RELEASE_NOTES*", "RELEASE-NOTES*", + "**/CHANGELOG*", "**/CHANGES*", "**/HISTORY*", "**/NEWS*", +) _WORD_RE = re.compile(r"[A-Za-z][A-Za-z0-9_-]*") diff --git a/tests/eval/harness.py b/tests/eval/harness.py index 58b9e2c..7802301 100644 --- a/tests/eval/harness.py +++ b/tests/eval/harness.py @@ -33,6 +33,7 @@ from codebase_index.storage.db import Database from . import metrics +from .gen_queries import CHANGELOG_EXCLUDES QUERY_DIR = Path(__file__).parent / "queries" DEFAULT_BUDGET = 1500 @@ -147,13 +148,19 @@ def validate_queries(queries: Iterable[EvalQuery], root: Path) -> list[str]: # scaffolding is excluded from the corpus it grades. CORPUS_EXCLUDES = ("tests/eval/**", "tests/benchmark_*", "tests/fixtures/expected_answers.yml") +# Changelog-style files paraphrase commit subjects, so for a git-derived query set +# they are the answer key in prose. They are never accepted as *answers* +# (`gen_queries._ANSWER_DENY_RE`) and, since 1.9.1, never indexed as *corpus* +# either: a changelog that outranks the implementation it describes is a +# measurement artefact, not a ranking signal. + def build_corpus_index(root: Path, db_path: Path) -> Database: """Build a fresh index for `root` at `db_path` and return the open handle.""" cfg = Config() cfg.root = str(root) cfg.embeddings.enabled = False - cfg.extra_ignore = [*cfg.extra_ignore, *CORPUS_EXCLUDES] + cfg.extra_ignore = [*cfg.extra_ignore, *CORPUS_EXCLUDES, *CHANGELOG_EXCLUDES] db = Database(db_path).open() build_index(cfg, db, root=root) return db From b98bb5a20e8e8267f6bcbd9f041e2858b17b6bcc Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:07:24 +0300 Subject: [PATCH 02/12] fix(plugin): allow architecture, diff-impact, path and describe in bin/ wrappers The plugin wrappers whitelisted ten subcommands while the skill template's cbx allowed fourteen, so plugin users could not run the four commands SKILL.md routes to. A parity test now pins bin/cbx, bin/cbx.ps1 and both template wrappers to the same set. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- bin/cbx | 2 +- bin/cbx.ps1 | 2 +- tests/test_plugin_wrappers.py | 24 ++++++++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/bin/cbx b/bin/cbx index cfe60b7..d4fb7f6 100644 --- a/bin/cbx +++ b/bin/cbx @@ -3,7 +3,7 @@ # from the venv provisioned by scripts/bootstrap.sh (located via the .venv-path pointer). set -euo pipefail -ALLOWED="search explain symbol refs impact graph stats doctor update index" +ALLOWED="search explain architecture symbol refs impact diff-impact path describe graph stats doctor update index" sub="${1:-}" case " $ALLOWED " in *" ${sub} "*) : ;; diff --git a/bin/cbx.ps1 b/bin/cbx.ps1 index 85face7..d841080 100644 --- a/bin/cbx.ps1 +++ b/bin/cbx.ps1 @@ -5,7 +5,7 @@ param( [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Rest ) $ErrorActionPreference = "Stop" -$allowed = @("search", "explain", "symbol", "refs", "impact", "graph", "stats", "doctor", "update", "index") +$allowed = @("search", "explain", "architecture", "symbol", "refs", "impact", "diff-impact", "path", "describe", "graph", "stats", "doctor", "update", "index") if ($allowed -notcontains $Subcommand) { Write-Error "cbx: refusing subcommand '$Subcommand'. Allowed: $($allowed -join ', ')" exit 2 diff --git a/tests/test_plugin_wrappers.py b/tests/test_plugin_wrappers.py index 7a3bc97..b92d379 100644 --- a/tests/test_plugin_wrappers.py +++ b/tests/test_plugin_wrappers.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re import shutil import subprocess from pathlib import Path @@ -64,3 +65,26 @@ def test_cbx_execs_venv_cli_via_pointer(tmp_path): ) assert res.returncode == 0, res.stderr assert "CBXSTUB search foo" in res.stdout + + +def _whitelist_sh(path: Path) -> set[str]: + m = re.search(r'^ALLOWED="([^"]+)"', path.read_text(encoding="utf-8"), re.M) + assert m, f"no ALLOWED= line in {path}" + return set(m.group(1).split()) + + +def _whitelist_ps1(path: Path) -> set[str]: + m = re.search(r"\$allowed = @\(([^)]+)\)", path.read_text(encoding="utf-8"), re.S) + assert m, f"no $allowed line in {path}" + return {tok.strip().strip('"') for tok in m.group(1).split(",")} + + +def test_plugin_wrapper_whitelist_matches_skill_template(): + """The plugin `bin/` wrappers must allow exactly what the skill template's + `cbx` allows, or plugin users silently lose commands the SKILL.md advertises + (this drifted once: architecture/diff-impact/path/describe were missing).""" + template = ROOT / "src/codebase_index/skill_template/scripts" + expected = _whitelist_sh(template / "cbx") + assert _whitelist_ps1(template / "cbx.ps1") == expected + assert _whitelist_sh(ROOT / "bin/cbx") == expected + assert _whitelist_ps1(ROOT / "bin/cbx.ps1") == expected From 0d83adba2e48009c07d0736761454033dfbc5c3c Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:07:27 +0300 Subject: [PATCH 03/12] feat(retrieval): cap recommended_reads at max_read_lines (default 120) A symbol-aligned chunk can be a whole class (1,500 lines in Flask's app.py), and the read plan handed that span to the agent verbatim. On the public baseline benchmark that made the index's follow-through reads cost more than three 80-line grep windows. Reads are now capped at the definition head; capped entries carry additive truncated/line_end_full fields so the agent can read on deliberately. retrieval.max_read_lines=0 restores the old behaviour. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- .../references/response-contract.md | 5 +- .../references/response-contract.md | 5 +- .../references/response-contract.md | 5 +- docs/RETRIEVAL.md | 4 +- examples/config.example.json | 5 +- skill/references/response-contract.md | 5 +- .../references/response-contract.md | 5 +- src/codebase_index/config.py | 6 ++ src/codebase_index/retrieval/pipeline.py | 23 ++++++- src/codebase_index/service.py | 1 + .../references/response-contract.md | 5 +- tests/test_read_cap.py | 61 +++++++++++++++++++ 12 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 tests/test_read_cap.py diff --git a/.claude/skills/codebase-index/references/response-contract.md b/.claude/skills/codebase-index/references/response-contract.md index 059d87a..798f152 100644 --- a/.claude/skills/codebase-index/references/response-contract.md +++ b/.claude/skills/codebase-index/references/response-contract.md @@ -18,7 +18,10 @@ Each result can contain: - `elided_lines` `recommended_reads` is the read plan. Start with its first one to three entries -and use exact line ranges. +and use exact line ranges. An entry with `truncated: true` was capped at the +definition head (`max_read_lines`, default 120); `line_end_full` gives the real +extent. Read the capped range first and continue only when the head is not +enough. `pagination.has_more` and `pagination.next_offset` indicate additional results. Prefer a more specific command or a larger token budget before paging. diff --git a/.codex/skills/codebase-index/references/response-contract.md b/.codex/skills/codebase-index/references/response-contract.md index 059d87a..798f152 100644 --- a/.codex/skills/codebase-index/references/response-contract.md +++ b/.codex/skills/codebase-index/references/response-contract.md @@ -18,7 +18,10 @@ Each result can contain: - `elided_lines` `recommended_reads` is the read plan. Start with its first one to three entries -and use exact line ranges. +and use exact line ranges. An entry with `truncated: true` was capped at the +definition head (`max_read_lines`, default 120); `line_end_full` gives the real +extent. Read the capped range first and continue only when the head is not +enough. `pagination.has_more` and `pagination.next_offset` indicate additional results. Prefer a more specific command or a larger token budget before paging. diff --git a/.opencode/skills/codebase-index/references/response-contract.md b/.opencode/skills/codebase-index/references/response-contract.md index 059d87a..798f152 100644 --- a/.opencode/skills/codebase-index/references/response-contract.md +++ b/.opencode/skills/codebase-index/references/response-contract.md @@ -18,7 +18,10 @@ Each result can contain: - `elided_lines` `recommended_reads` is the read plan. Start with its first one to three entries -and use exact line ranges. +and use exact line ranges. An entry with `truncated: true` was capped at the +definition head (`max_read_lines`, default 120); `line_end_full` gives the real +extent. Read the capped range first and continue only when the head is not +enough. `pagination.has_more` and `pagination.next_offset` indicate additional results. Prefer a more specific command or a larger token budget before paging. diff --git a/docs/RETRIEVAL.md b/docs/RETRIEVAL.md index 6102baa..e375032 100644 --- a/docs/RETRIEVAL.md +++ b/docs/RETRIEVAL.md @@ -182,7 +182,9 @@ Results are trimmed to fit `--token-budget` (default per intent, e.g. 1500 token 2. Greedily attach snippets to the highest-ranked results until budget is hit. 3. Snippets are trimmed to the relevant line range (± a few context lines), not whole functions. 4. Lower-ranked results become **`recommended_reads`** (path + range, no snippet) so Claude can - choose to read them itself. + choose to read them itself. A read is capped at `retrieval.max_read_lines` (default 120): a + symbol-aligned chunk can be a whole class, and the read plan should bill the agent for the + definition head, not the body. Capped entries carry `truncated: true` and `line_end_full`. 5. Snippet text passes through secret redaction (see SECURITY.md) before emission. The point: Claude gets enough to decide, and a precise list of what to read next — never a dump. diff --git a/examples/config.example.json b/examples/config.example.json index f28b046..6d4711c 100644 --- a/examples/config.example.json +++ b/examples/config.example.json @@ -9,7 +9,10 @@ "default_mode": "hybrid", "rrf_k": 60, "token_budget": 1500, - "limit": 10 + "limit": 10, + "compact_snippets": true, + "compact_min_reduction": 0.25, + "max_read_lines": 120 }, "embeddings": { "backend": "noop", diff --git a/skill/references/response-contract.md b/skill/references/response-contract.md index 059d87a..798f152 100644 --- a/skill/references/response-contract.md +++ b/skill/references/response-contract.md @@ -18,7 +18,10 @@ Each result can contain: - `elided_lines` `recommended_reads` is the read plan. Start with its first one to three entries -and use exact line ranges. +and use exact line ranges. An entry with `truncated: true` was capped at the +definition head (`max_read_lines`, default 120); `line_end_full` gives the real +extent. Read the capped range first and continue only when the head is not +enough. `pagination.has_more` and `pagination.next_offset` indicate additional results. Prefer a more specific command or a larger token budget before paging. diff --git a/skills/codebase-index/references/response-contract.md b/skills/codebase-index/references/response-contract.md index 059d87a..798f152 100644 --- a/skills/codebase-index/references/response-contract.md +++ b/skills/codebase-index/references/response-contract.md @@ -18,7 +18,10 @@ Each result can contain: - `elided_lines` `recommended_reads` is the read plan. Start with its first one to three entries -and use exact line ranges. +and use exact line ranges. An entry with `truncated: true` was capped at the +definition head (`max_read_lines`, default 120); `line_end_full` gives the real +extent. Read the capped range first and continue only when the head is not +enough. `pagination.has_more` and `pagination.next_offset` indicate additional results. Prefer a more specific command or a larger token budget before paging. diff --git a/src/codebase_index/config.py b/src/codebase_index/config.py index 217cad7..eac4cb8 100644 --- a/src/codebase_index/config.py +++ b/src/codebase_index/config.py @@ -28,6 +28,12 @@ class RetrievalConfig(BaseModel): limit: int = 10 compact_snippets: bool = True compact_min_reduction: float = 0.25 + # Longest line span a single `recommended_reads` entry may ask the agent to + # open. A symbol-aligned chunk can be a whole 1,500-line class; the read plan + # points at its head instead and marks the entry `truncated` with the full + # `line_end_full`, so the agent pays for a bounded window and decides itself + # whether the rest is worth it. 0 disables the cap. + max_read_lines: int = 120 class EmbeddingsConfig(BaseModel): diff --git a/src/codebase_index/retrieval/pipeline.py b/src/codebase_index/retrieval/pipeline.py index 020c956..56c9e70 100644 --- a/src/codebase_index/retrieval/pipeline.py +++ b/src/codebase_index/retrieval/pipeline.py @@ -144,6 +144,26 @@ def _fallback_suggestions(query, ranked) -> dict: return {"ripgrep": rg} +def _bounded_read(entry: dict, max_lines: int) -> dict: + """Cap one read-plan entry to `max_lines`, keeping the original span visible. + + Symbol-aligned chunks can span an entire class. Handing the agent + ``line_start..line_end`` verbatim then bills it for the whole body when the + definition head is usually what it needs first; the capped entry points at + the head and records the full extent so the agent can read on deliberately. + Additive fields only (`truncated`, `line_end_full`): schema unchanged. + """ + span = entry["line_end"] - entry["line_start"] + 1 + if max_lines <= 0 or span <= max_lines: + return entry + return { + **entry, + "line_end": entry["line_start"] + max_lines - 1, + "line_end_full": entry["line_end"], + "truncated": True, + } + + def search( conn: sqlite3.Connection, query: str, @@ -159,6 +179,7 @@ def search( offset: int = 0, compact: bool = True, compact_min_reduction: float = 0.25, + max_read_lines: int = 120, ) -> dict: tuning = tuning or DEFAULT_TUNING plan = detect_intent(query) @@ -215,7 +236,7 @@ def search( paginated = all_results[offset:offset + limit] paginated_keys = {(r["path"], r["line_start"], r["line_end"]) for r in paginated} recommended = [ - r for r in all_recommended + _bounded_read(r, max_read_lines) for r in all_recommended if (r["path"], r["line_start"], r["line_end"]) in paginated_keys ] has_more = len(all_results) > offset + limit diff --git a/src/codebase_index/service.py b/src/codebase_index/service.py index 1857221..7dcfcb5 100644 --- a/src/codebase_index/service.py +++ b/src/codebase_index/service.py @@ -103,6 +103,7 @@ def search_payload( config=cfg, compact=compact, compact_min_reduction=cfg.retrieval.compact_min_reduction, + max_read_lines=cfg.retrieval.max_read_lines, ) diff --git a/src/codebase_index/skill_template/references/response-contract.md b/src/codebase_index/skill_template/references/response-contract.md index 059d87a..798f152 100644 --- a/src/codebase_index/skill_template/references/response-contract.md +++ b/src/codebase_index/skill_template/references/response-contract.md @@ -18,7 +18,10 @@ Each result can contain: - `elided_lines` `recommended_reads` is the read plan. Start with its first one to three entries -and use exact line ranges. +and use exact line ranges. An entry with `truncated: true` was capped at the +definition head (`max_read_lines`, default 120); `line_end_full` gives the real +extent. Read the capped range first and continue only when the head is not +enough. `pagination.has_more` and `pagination.next_offset` indicate additional results. Prefer a more specific command or a larger token budget before paging. diff --git a/tests/test_read_cap.py b/tests/test_read_cap.py new file mode 100644 index 0000000..176c426 --- /dev/null +++ b/tests/test_read_cap.py @@ -0,0 +1,61 @@ +"""`recommended_reads` entries are capped at `max_read_lines` (additive fields only).""" + +from __future__ import annotations + +from codebase_index.retrieval.pipeline import _bounded_read + + +def test_short_entry_is_untouched(): + entry = {"path": "a.py", "line_start": 10, "line_end": 40} + assert _bounded_read(entry, 120) is entry + + +def test_long_entry_is_capped_and_annotated(): + entry = {"path": "Big.java", "line_start": 1, "line_end": 1500} + out = _bounded_read(entry, 120) + assert out == { + "path": "Big.java", + "line_start": 1, + "line_end": 120, + "line_end_full": 1500, + "truncated": True, + } + assert entry["line_end"] == 1500, "input must not be mutated" + + +def test_zero_disables_the_cap(): + entry = {"path": "Big.java", "line_start": 1, "line_end": 1500} + assert _bounded_read(entry, 0) is entry + + +def test_search_applies_cap_through_config(tmp_path): + """End to end on a synthetic file whose only symbol spans 300 lines.""" + from pathlib import Path + + from codebase_index.config import Config + from codebase_index.indexer.pipeline import build_index + from codebase_index.retrieval.pipeline import search + from codebase_index.storage.db import Database + + root = tmp_path / "repo" + (root / "src").mkdir(parents=True) + body = "\n".join(f" x{i} = {i} # frobnicate widget" for i in range(298)) + (root / "src" / "widget.py").write_text( + f"class WidgetFrobnicator:\n '''Frobnicates widgets.'''\n{body}\n", encoding="utf-8" + ) + cfg = Config(root=str(root)) + cfg.embeddings.enabled = False + db = Database(tmp_path / "idx.sqlite").open() + try: + build_index(cfg, db, root=Path(root)) + # A budget of 1 token guarantees the hit lands in recommended_reads, not a snippet. + capped = search(db.conn, "WidgetFrobnicator", mode="hybrid", limit=5, + token_budget=1, no_fallback=True, max_read_lines=50) + full = search(db.conn, "WidgetFrobnicator", mode="hybrid", limit=5, + token_budget=1, no_fallback=True, max_read_lines=0) + finally: + db.close() + capped_reads = [r for r in capped["recommended_reads"] if r.get("truncated")] + assert capped_reads, capped["recommended_reads"] + assert all(r["line_end"] - r["line_start"] + 1 == 50 for r in capped_reads) + assert not any(r.get("truncated") for r in full["recommended_reads"]) From 53165f643933ef90449acf18d5550651f865448f Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:07:45 +0300 Subject: [PATCH 04/12] ci(release): version-consistency, link and packaging gates; changelog-driven release notes - scripts/check_versions.py: __version__ must match plugin.json, the requirements.lock tag, the three .skill_version stamps and a CHANGELOG heading. - scripts/check_links.py: relative Markdown links and image sources must resolve. - scripts/release_notes.py: the GitHub release body is the CHANGELOG section for the tag, and an empty section fails the release job. - CI lint job runs both checks; a new package job builds, twine-checks and runs the clean-venv install smoke on every PR instead of only at tag time. - release.yml refuses a tag whose name does not match __version__. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- .github/workflows/ci.yml | 21 +++ .github/workflows/release.yml | 14 ++ docs/RELEASE_CHECKLIST.md | 45 +++-- .../plans/2026-05-29-m9-release.md | 2 +- scripts/check_links.py | 92 ++++++++++ scripts/check_versions.py | 101 ++++++++++ scripts/release_notes.py | 113 ++++++++++++ tests/test_release_scripts.py | 172 ++++++++++++++++++ 8 files changed, 541 insertions(+), 19 deletions(-) create mode 100644 scripts/check_links.py create mode 100644 scripts/check_versions.py create mode 100644 scripts/release_notes.py create mode 100644 tests/test_release_scripts.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0d031a..c313134 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,27 @@ jobs: run: mypy src/codebase_index - name: Skill copies in sync run: python scripts/sync_skill_copies.py --check + - name: Version mirrors agree + run: python scripts/check_versions.py + - name: Markdown links resolve + run: python scripts/check_links.py + + package: + needs: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - run: python -m pip install --upgrade pip + - run: pip install -e ".[dev,build]" + - name: Build sdist + wheel + run: python -m build + - name: Twine check + run: twine check dist/* + - name: Clean-venv install smoke (init -> index -> search) + run: python scripts/release_smoke.py test: needs: lint diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5756979..9ebc840 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,13 @@ jobs: with: python-version: "3.12" - run: pip install -e ".[dev,build]" + - name: Version mirrors agree with the tag + run: | + python scripts/check_versions.py + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + ver=$(python -c "import re,pathlib;print(re.search(r'__version__ = \"([^\"]+)\"', pathlib.Path('src/codebase_index/__init__.py').read_text()).group(1))") + [ "${GITHUB_REF_NAME}" = "v${ver}" ] || { echo "tag ${GITHUB_REF_NAME} != package version v${ver}"; exit 1; } + fi - name: Test gate run: pytest - name: Build @@ -43,10 +50,17 @@ jobs: with: name: dist path: dist + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.12" + - name: Release notes from CHANGELOG.md + run: python scripts/release_notes.py --out release_notes.md - name: Create GitHub release uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: dist/* + body_path: release_notes.md + # Appends the auto-generated "What's Changed" PR list after the changelog body. generate_release_notes: true pypi-publish: diff --git a/docs/RELEASE_CHECKLIST.md b/docs/RELEASE_CHECKLIST.md index a159688..e5f3886 100644 --- a/docs/RELEASE_CHECKLIST.md +++ b/docs/RELEASE_CHECKLIST.md @@ -9,31 +9,30 @@ recreating a GitHub release (used to publish an already-tagged version). Work top to bottom. Do not tag until every required box is checked. -## 1. Version sync (single source + the two manual mirrors) +## 1. Version sync (single source, mirrors checked by script) The package version is single-sourced from `src/codebase_index/__init__.py` -(hatch dynamic version). Two files mirror it and are **not** auto-synced — bump -them by hand and verify: +(hatch dynamic version). `scripts/sync_skill_copies.py` rewrites every mirror; +`scripts/check_versions.py` proves they agree and runs in CI lint and at the top +of the release build, so a mismatched tag fails before anything is published. - [ ] `src/codebase_index/__init__.py` → `__version__` bumped (canonical). -- [ ] `.claude-plugin/plugin.json` → `"version"` matches. -- [ ] `.claude-plugin/marketplace.json` → version matches (if present). -- [ ] `requirements.lock` → the GitHub tarball tag matches the new tag - (`.../tags/vX.Y.Z.tar.gz`). The plugin bootstrap installs exactly this pin. -- [ ] README / QUICKSTART / INSTALLATION / FAQ / MCP install snippets reference - the new tag (`@vX.Y.Z`). -- [ ] Skill copies + `.skill_version` stamps regenerated and in sync: +- [ ] Mirrors regenerated and verified: ```bash - python scripts/sync_skill_copies.py # regenerate - python scripts/sync_skill_copies.py --check # CI gate: must pass clean + python scripts/sync_skill_copies.py # plugin.json, requirements.lock, skill copies + stamps + python scripts/sync_skill_copies.py --check # CI gate: no drift + python scripts/check_versions.py # CI gate: package == plugin.json == lock tag == stamps, CHANGELOG has the section ``` +- [ ] Docs that quote a pinned tag (`@vX.Y.Z`, `codebase-index==X.Y.Z`) updated; + `python scripts/check_links.py` passes (CI gate for relative links). + ## 2. Tests and lint - [ ] `pytest` green locally (coverage gate `--cov-fail-under=80` enforced). -- [ ] `ruff check src/ tests/` clean. -- [ ] `mypy src/codebase_index` (advisory) reviewed. +- [ ] `ruff check src tests scripts` clean (CI gate). +- [ ] `mypy src/codebase_index` clean (CI gate, not advisory). - [ ] Slow/perf tests considered: `pytest --runslow` for index/search latency. - [ ] CI matrix green (Ubuntu/macOS/Windows × py3.11–3.13) on the release branch. @@ -93,8 +92,15 @@ them by hand and verify: ## 8. Changelog and docs -- [ ] `CHANGELOG.md`: move `[Unreleased]` items under the new `vX.Y.Z` dated +- [ ] `CHANGELOG.md`: move `[Unreleased]` items under the new `## [X.Y.Z] - YYYY-MM-DD` heading; add the version-compare link at the bottom. +- [ ] Preview the GitHub release body — it is generated from that section, and an + empty or missing section fails the release job: + + ```bash + python scripts/release_notes.py # prints the notes for __version__ + ``` + - [ ] ROADMAP / docs reflect anything that shipped or moved. - [ ] `docs/PRODUCT_UPGRADE_PLAN.md` status column updated for shipped items. @@ -102,9 +108,12 @@ them by hand and verify: - [ ] Commit the version bump + changelog on the release branch; open/merge PR. - [ ] Tag: `git tag vX.Y.Z && git push origin vX.Y.Z`. -- [ ] `release.yml` build job green (test gate + `python -m build` + `twine check` - + `release_smoke.py`). -- [ ] GitHub release created with artifacts attached; release notes reviewed. +- [ ] `release.yml` build job green (`check_versions.py` + tag/version match + test + gate + `python -m build` + `twine check` + `release_smoke.py`). The same + build/twine/smoke steps already ran on the PR in CI's `package` job. +- [ ] GitHub release created with artifacts attached; body = CHANGELOG section + (`release_notes.py`) followed by the auto-generated PR list. +- [ ] PyPI shows the new version (`pip index versions codebase-index`). - [ ] Post-publish: re-run `pipx install "...@vX.Y.Z"` once to confirm the tag resolves. diff --git a/docs/superpowers/plans/2026-05-29-m9-release.md b/docs/superpowers/plans/2026-05-29-m9-release.md index 363352e..4c2573d 100644 --- a/docs/superpowers/plans/2026-05-29-m9-release.md +++ b/docs/superpowers/plans/2026-05-29-m9-release.md @@ -807,7 +807,7 @@ In `README.md`, replace the `## Status` section body (the "🚧 Blueprint / scaf ✅ **`0.1.0` released.** All milestones M0–M9 are implemented: discovery + storage, FTS5 lexical search, tree-sitter symbols/refs, hybrid ranking, graph impact, optional local embeddings, the packaged skill + freshness contract, hooks/watch, and a tested, `pipx`-installable release. See -[CHANGELOG.md](CHANGELOG.md) and [docs/ROADMAP.md](docs/ROADMAP.md). +[CHANGELOG.md](../../../CHANGELOG.md) and [docs/ROADMAP.md](../../ROADMAP.md). ``` Also add `[CHANGELOG.md](CHANGELOG.md)` to the Documentation list. diff --git a/scripts/check_links.py b/scripts/check_links.py new file mode 100644 index 0000000..c06be07 --- /dev/null +++ b/scripts/check_links.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Fail on broken relative links in Markdown files. + + python scripts/check_links.py # whole repository + python scripts/check_links.py docs README.md + +Checks `[text](target)` and `` / `` where the +target is a relative path. `#fragment` suffixes are stripped; http(s):, mailto:, +and fragment-only links are ignored. Prints `path:line: target` for each broken +link and exits 1 when any is found. Stdlib only; runs in CI lint. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "dist", "build", ".tmp-public-benchmark"} + +MD_LINK_RE = re.compile(r"(?]*?\b(?:src|href)=\"([^\"]+)\"", re.I) +FENCE_RE = re.compile(r"^\s*(```|~~~)") +INLINE_CODE_RE = re.compile(r"`[^`]*`") +SCHEME_RE = re.compile(r"^[a-zA-Z][a-zA-Z0-9+.-]*:") + + +def _is_external(target: str) -> bool: + return bool(SCHEME_RE.match(target)) or target.startswith(("#", "//", "${", "<")) + + +def find_broken(md_file: Path, repo: Path = REPO) -> list[tuple[int, str]]: + broken: list[tuple[int, str]] = [] + in_fence = False + for lineno, line in enumerate(md_file.read_text(encoding="utf-8", errors="replace").splitlines(), 1): + if FENCE_RE.match(line): + in_fence = not in_fence + continue + if in_fence: + continue + line = INLINE_CODE_RE.sub("", line) # `[x](y)` inside code spans is prose, not a link + targets = MD_LINK_RE.findall(line) + MD_IMAGE_RE.findall(line) + HTML_SRC_RE.findall(line) + for raw in targets: + target = raw.strip("<>") + if _is_external(target): + continue + target = target.split("#", 1)[0].split("?", 1)[0] + if not target: + continue + base = repo if target.startswith("/") else md_file.parent + resolved = (base / target.lstrip("/")).resolve() + if not resolved.exists(): + broken.append((lineno, raw)) + return broken + + +def iter_markdown(paths: list[Path], repo: Path = REPO): + for root in paths: + if root.is_file(): + yield root + continue + for p in sorted(root.rglob("*.md")): + if any(part in SKIP_DIRS for part in p.relative_to(root).parts): + continue + yield p + + +def main(argv: list[str] | None = None) -> int: + args = argv if argv is not None else sys.argv[1:] + roots = [Path(a).resolve() for a in args] or [REPO] + total_files = 0 + total_broken = 0 + for md in iter_markdown(roots, REPO): + total_files += 1 + for lineno, target in find_broken(md, REPO): + total_broken += 1 + try: + shown = md.relative_to(REPO).as_posix() + except ValueError: + shown = md.as_posix() + print(f"{shown}:{lineno}: {target}") + if total_broken: + print(f"\n{total_broken} broken link(s) in {total_files} markdown file(s)", file=sys.stderr) + return 1 + print(f"links OK: {total_files} markdown file(s), no broken relative links") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_versions.py b/scripts/check_versions.py new file mode 100644 index 0000000..8a47e4e --- /dev/null +++ b/scripts/check_versions.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Fail when the version is not the same everywhere it is recorded. + + python scripts/check_versions.py + +Canonical: src/codebase_index/__init__.py::__version__ (hatch dynamic version). +Mirrors that must agree: + .claude-plugin/plugin.json "version" + requirements.lock refs/tags/v.tar.gz + .claude|.codex|.opencode/skills/codebase-index/.skill_version + CHANGELOG.md a `## []` heading, OR the + version is newer than every released + heading and `## [Unreleased]` exists + (main between releases). +Stdlib only; runs in CI lint and before the release build. +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] + +VERSION_RE = re.compile(r'^__version__ = "([^"]+)"$', re.M) +LOCK_TAG_RE = re.compile(r"refs/tags/v([0-9][^/]*?)\.tar\.gz") +HEADING_RE = re.compile(r"^## \[([^\]]+)\]", re.M) +STAMPS = ( + Path(".claude/skills/codebase-index/.skill_version"), + Path(".codex/skills/codebase-index/.skill_version"), + Path(".opencode/skills/codebase-index/.skill_version"), +) + + +def _vtuple(version: str) -> tuple[int, ...]: + return tuple(int(p) for p in re.findall(r"\d+", version)) + + +def check(repo: Path = REPO) -> list[str]: + """Return a list of mismatch descriptions; empty means consistent.""" + problems: list[str] = [] + init = (repo / "src/codebase_index/__init__.py").read_text(encoding="utf-8") + match = VERSION_RE.search(init) + if not match: + return ["src/codebase_index/__init__.py: no __version__"] + version = match.group(1) + + plugin = json.loads((repo / ".claude-plugin/plugin.json").read_text(encoding="utf-8")) + if plugin.get("version") != version: + problems.append(f".claude-plugin/plugin.json: {plugin.get('version')!r} != {version!r}") + + lock = (repo / "requirements.lock").read_text(encoding="utf-8") + tag = LOCK_TAG_RE.search(lock) + if not tag: + problems.append("requirements.lock: no refs/tags/vX.Y.Z.tar.gz pin found") + elif tag.group(1) != version: + problems.append(f"requirements.lock: tag v{tag.group(1)} != v{version}") + + for rel in STAMPS: + path = repo / rel + if not path.is_file(): + problems.append(f"{rel.as_posix()}: missing") + continue + stamp = path.read_text(encoding="utf-8").strip() + if stamp != version: + problems.append(f"{rel.as_posix()}: {stamp!r} != {version!r}") + + changelog = (repo / "CHANGELOG.md").read_text(encoding="utf-8") + headings = HEADING_RE.findall(changelog) + released = [h for h in headings if h.lower() != "unreleased"] + if version in released: + pass + elif "Unreleased" in headings and all(_vtuple(version) > _vtuple(h) for h in released): + pass # main between releases: version bumped ahead, notes still under Unreleased + else: + problems.append( + f"CHANGELOG.md: no '## [{version}]' heading and version is not newer than " + f"the latest released heading ({released[0] if released else 'none'})" + ) + return problems + + +def main() -> int: + problems = check() + if problems: + print("version mismatch:", file=sys.stderr) + for p in problems: + print(f" - {p}", file=sys.stderr) + return 1 + version = VERSION_RE.search( + (REPO / "src/codebase_index/__init__.py").read_text(encoding="utf-8") + ).group(1) # type: ignore[union-attr] + print(f"versions consistent: {version} (package, plugin.json, requirements.lock, " + f"{len(STAMPS)} skill stamps, CHANGELOG)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/release_notes.py b/scripts/release_notes.py new file mode 100644 index 0000000..70aaccc --- /dev/null +++ b/scripts/release_notes.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Extract one version's section from CHANGELOG.md as GitHub release notes. + + python scripts/release_notes.py # current __version__, to stdout + python scripts/release_notes.py 1.9.0 # explicit version + python scripts/release_notes.py --out notes.md # write instead of print + +Exits non-zero when the section is missing or empty, so a tag with an unwritten +changelog fails the release job instead of shipping an empty release page. +Stdlib only; used by .github/workflows/release.yml. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +REPO = Path(__file__).resolve().parents[1] +REPO_URL = "https://github.com/denfry/codebase-index" + +VERSION_RE = re.compile(r'^__version__ = "([^"]+)"$', re.M) +HEADING_RE = re.compile(r"^## \[(?P[^\]]+)\](?:\s*-\s*(?P\S+))?\s*$", re.M) +LINK_DEF_RE = re.compile(r"^\[(?P[^\]]+)\]:\s*(?P\S+)\s*$", re.M) + + +def package_version(repo: Path = REPO) -> str: + text = (repo / "src/codebase_index/__init__.py").read_text(encoding="utf-8") + match = VERSION_RE.search(text) + if not match: + raise SystemExit("could not find __version__ in src/codebase_index/__init__.py") + return match.group(1) + + +def extract_section(changelog: str, version: str) -> tuple[str, str | None, str | None]: + """Return (body, date, previous_version) for `version`. + + `body` is the text between this version's heading and the next `## ` heading, + with trailing link definitions removed. Raises KeyError when the heading is + absent. + """ + headings = list(HEADING_RE.finditer(changelog)) + for idx, match in enumerate(headings): + if match.group("version") != version: + continue + end = headings[idx + 1].start() if idx + 1 < len(headings) else len(changelog) + body = changelog[match.end():end] + # Link definitions live at the bottom of the file; they are not notes. + body = LINK_DEF_RE.sub("", body).strip() + previous = headings[idx + 1].group("version") if idx + 1 < len(headings) else None + return body, match.group("date"), previous + raise KeyError(version) + + +def compare_url(changelog: str, version: str, previous: str | None) -> str | None: + for match in LINK_DEF_RE.finditer(changelog): + if match.group("version") == version: + return match.group("url") + if previous and previous.lower() != "unreleased": + return f"{REPO_URL}/compare/v{previous}...v{version}" + return None + + +def render(changelog: str, version: str) -> str: + body, _date, previous = extract_section(changelog, version) + if not body: + raise SystemExit(f"CHANGELOG.md section for {version} is empty") + footer = [ + "", + "---", + "", + "**Install**", + "", + "```bash", + f"pip install codebase-index=={version}", + f"pipx install codebase-index=={version}", + "```", + ] + url = compare_url(changelog, version, previous) + if url: + footer += ["", f"Full diff: {url}"] + return body + "\n" + "\n".join(footer) + "\n" + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("version", nargs="?", help="default: package __version__") + ap.add_argument("--changelog", default=str(REPO / "CHANGELOG.md")) + ap.add_argument("--out", help="write notes here instead of stdout") + args = ap.parse_args(argv) + + version = args.version or package_version() + changelog = Path(args.changelog).read_text(encoding="utf-8") + try: + notes = render(changelog, version) + except KeyError: + print(f"CHANGELOG.md has no '## [{version}]' section", file=sys.stderr) + return 1 + + if args.out: + Path(args.out).write_text(notes, encoding="utf-8") + print(f"wrote release notes for {version} to {args.out}") + else: + # The changelog uses arrows and dashes; a cp1251 console must not crash. + sys.stdout.buffer.write(notes.encode("utf-8")) + sys.stdout.flush() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_release_scripts.py b/tests/test_release_scripts.py new file mode 100644 index 0000000..0ae2a23 --- /dev/null +++ b/tests/test_release_scripts.py @@ -0,0 +1,172 @@ +"""Unit tests for the release/CI hygiene scripts under scripts/.""" + +from __future__ import annotations + +import importlib.util +import json +from pathlib import Path + +import pytest + +REPO = Path(__file__).resolve().parents[1] + + +def _load(name: str): + spec = importlib.util.spec_from_file_location(name, REPO / "scripts" / f"{name}.py") + mod = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(mod) + return mod + + +release_notes = _load("release_notes") +check_versions = _load("check_versions") +check_links = _load("check_links") + + +CHANGELOG = """# Changelog + +## [Unreleased] + +## [1.9.0] - 2026-09-02 + +### Added + +- Thing one → arrow. + +## [1.8.0] - 2026-09-02 + +### Changed + +- Older thing. + +## [1.7.0] - 2026-07-29 + +[Unreleased]: https://github.com/denfry/codebase-index/compare/v1.8.0...HEAD +[1.8.0]: https://github.com/denfry/codebase-index/compare/v1.7.0...v1.8.0 +""" + + +# --- release_notes ----------------------------------------------------------- +def test_release_notes_extracts_section_and_synthesizes_compare_link(): + out = release_notes.render(CHANGELOG, "1.9.0") + assert "Thing one" in out + assert "Older thing" not in out + assert "pip install codebase-index==1.9.0" in out + # No link definition for 1.9.0 -> derived from the previous heading. + assert "compare/v1.8.0...v1.9.0" in out + + +def test_release_notes_prefers_existing_link_definition(): + out = release_notes.render(CHANGELOG, "1.8.0") + assert "compare/v1.7.0...v1.8.0" in out + assert "[1.8.0]:" not in out # link defs are stripped from the body + + +def test_release_notes_fails_on_empty_or_missing_section(): + with pytest.raises(SystemExit): + release_notes.render(CHANGELOG, "1.7.0") # heading present, body empty + with pytest.raises(KeyError): + release_notes.render(CHANGELOG, "9.9.9") + + +def test_release_notes_cli_writes_file(tmp_path): + changelog = tmp_path / "CHANGELOG.md" + changelog.write_text(CHANGELOG, encoding="utf-8") + out = tmp_path / "notes.md" + rc = release_notes.main(["1.9.0", "--changelog", str(changelog), "--out", str(out)]) + assert rc == 0 + assert "Thing one" in out.read_text(encoding="utf-8") + assert release_notes.main(["9.9.9", "--changelog", str(changelog)]) == 1 + + +# --- check_versions ---------------------------------------------------------- +def _fake_repo(tmp_path: Path, version: str, *, plugin=None, lock=None, stamps=None, + changelog=CHANGELOG) -> Path: + tmp_path.mkdir(parents=True, exist_ok=True) + (tmp_path / "src/codebase_index").mkdir(parents=True) + (tmp_path / "src/codebase_index/__init__.py").write_text( + f'__version__ = "{version}"\n', encoding="utf-8" + ) + (tmp_path / ".claude-plugin").mkdir() + (tmp_path / ".claude-plugin/plugin.json").write_text( + json.dumps({"version": plugin or version}), encoding="utf-8" + ) + (tmp_path / "requirements.lock").write_text( + f"codebase-index @ https://github.com/denfry/codebase-index/archive/refs/tags/" + f"v{lock or version}.tar.gz\n", + encoding="utf-8", + ) + for rel in check_versions.STAMPS: + (tmp_path / rel).parent.mkdir(parents=True, exist_ok=True) + (tmp_path / rel).write_text(f"{stamps or version}\n", encoding="utf-8") + (tmp_path / "CHANGELOG.md").write_text(changelog, encoding="utf-8") + return tmp_path + + +def test_check_versions_consistent(tmp_path): + assert check_versions.check(_fake_repo(tmp_path, "1.9.0")) == [] + + +def test_check_versions_reports_every_mismatch(tmp_path): + repo = _fake_repo(tmp_path, "1.9.0", plugin="1.8.0", lock="1.8.0", stamps="1.8.0") + problems = check_versions.check(repo) + assert any("plugin.json" in p for p in problems) + assert any("requirements.lock" in p for p in problems) + assert sum(".skill_version" in p for p in problems) == 3 + + +def test_check_versions_allows_bumped_version_with_unreleased(tmp_path): + # main between releases: version ahead of the newest heading, notes under Unreleased. + assert check_versions.check(_fake_repo(tmp_path / "ahead", "1.10.0")) == [] + # but a version that is neither released nor newer is a mistake + problems = check_versions.check(_fake_repo(tmp_path / "behind", "1.8.5")) + assert any("CHANGELOG" in p for p in problems) + + +def test_check_versions_real_repo_is_consistent(): + assert check_versions.check(REPO) == [] + + +# --- check_links ------------------------------------------------------------- +def test_check_links_detects_broken_and_ignores_external(tmp_path, monkeypatch): + docs = tmp_path / "docs" + docs.mkdir() + (docs / "ok.md").write_text("target\n", encoding="utf-8") + (tmp_path / "img.png").write_bytes(b"\x89PNG") + md = tmp_path / "README.md" + md.write_text( + "\n".join([ + "[good](docs/ok.md#section)", + "[external](https://example.com/x.md)", + "[mail](mailto:a@b.c)", + "[anchor](#top)", + '', + "![shot](assets/missing.png)", + "[bad](docs/nope.md)", + "```", + "[fenced](docs/not-checked.md)", + "```", + ]), + encoding="utf-8", + ) + monkeypatch.setattr(check_links, "REPO", tmp_path) + broken = check_links.find_broken(md, tmp_path) + assert [t for _, t in broken] == ["assets/missing.png", "docs/nope.md"] + assert broken[0][0] == 6 and broken[1][0] == 7 + + +def test_check_links_cli_exit_codes(tmp_path, monkeypatch, capsys): + (tmp_path / "a.md").write_text("[x](b.md)\n", encoding="utf-8") + monkeypatch.setattr(check_links, "REPO", tmp_path) + assert check_links.main([str(tmp_path)]) == 1 + assert "a.md:1: b.md" in capsys.readouterr().out + (tmp_path / "b.md").write_text("ok\n", encoding="utf-8") + assert check_links.main([str(tmp_path)]) == 0 + + +def test_check_links_skips_venv(tmp_path, monkeypatch): + (tmp_path / ".venv").mkdir() + (tmp_path / ".venv/x.md").write_text("[x](missing.md)\n", encoding="utf-8") + monkeypatch.setattr(check_links, "REPO", tmp_path) + assert list(check_links.iter_markdown([tmp_path], tmp_path)) == [] From cbaba6147432e730143c3fa86cace18a33cfaf8a Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:07:47 +0300 Subject: [PATCH 05/12] docs(community): contributor onboarding, issue templates, support and security policy - docs/DEVELOPMENT.md: fifteen-minute path from clone to a merged PR, with verified commands and recipes for languages, graph edges, ranking signals, MCP tools and CLI commands. - CONTRIBUTING.md: contribution areas mapped to labels, evidence rules for retrieval-quality PRs, and the fork workflow; unverified uv/ruff-format instructions removed. - SECURITY.md: supported line is 1.9.x, private vulnerability reporting via GitHub advisories, explicit statement that checksums/SBOMs are not yet shipped. - Issue templates: concise bug/feature forms plus benchmark-report and language-support forms; blank issues off; SUPPORT.md; labels.yml with an apply script; placeholder FUNDING.yml removed. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- .github/FUNDING.yml | 8 - .github/ISSUE_TEMPLATE/benchmark_report.yml | 62 +++++ .github/ISSUE_TEMPLATE/bug_report.yml | 62 ++--- .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 58 +++-- .github/ISSUE_TEMPLATE/language_support.yml | 48 ++++ .github/PULL_REQUEST_TEMPLATE.md | 45 ++-- .github/SUPPORT.md | 25 ++ .github/labels.yml | 63 +++++ CONTRIBUTING.md | 244 ++++++++------------ SECURITY.md | 120 ++++++---- docs/DEVELOPMENT.md | 236 +++++++++++++++++++ scripts/apply_labels.sh | 19 ++ 13 files changed, 704 insertions(+), 294 deletions(-) delete mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/benchmark_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/language_support.yml create mode 100644 .github/SUPPORT.md create mode 100644 .github/labels.yml create mode 100644 docs/DEVELOPMENT.md create mode 100644 scripts/apply_labels.sh diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 365d548..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,8 +0,0 @@ -github: - patreon: # placeholder - ko_fi: # placeholder - tidelift: # placeholder - community_bridge: # placeholder - liberapay: # placeholder - issuehunt: # placeholder - custom: # placeholder diff --git a/.github/ISSUE_TEMPLATE/benchmark_report.yml b/.github/ISSUE_TEMPLATE/benchmark_report.yml new file mode 100644 index 0000000..f8d2669 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/benchmark_report.yml @@ -0,0 +1,62 @@ +name: Benchmark report +description: Share a reproduced, failed, or new benchmark run (good or bad numbers both welcome) +title: "[Benchmark]: " +labels: ["benchmark"] +body: + - type: markdown + attributes: + value: | + Independent runs are how this project keeps its numbers honest. Unflattering results are as useful as flattering ones. + + - type: dropdown + id: kind + attributes: + label: What did you run? + options: + - tests/eval/run_eval.py (retrieval eval, own corpus) + - tests/eval/run_eval.py (shipped query sets) + - tests/benchmark_public.py (synthetic suite) + - tests/benchmark_honest.py (index vs rg+window) + - Something else (describe below) + validations: + required: true + + - type: input + id: version + attributes: + label: codebase-index version / commit + placeholder: "1.9.0 or git SHA" + validations: + required: true + + - type: input + id: corpus + attributes: + label: Corpus + description: Public repository URL + commit SHA, or language / size if private. + placeholder: "https://github.com/pallets/flask @ d318b68 — Python, ~18k LOC" + validations: + required: true + + - type: textarea + id: command + attributes: + label: Exact commands + render: shell + validations: + required: true + + - type: textarea + id: results + attributes: + label: Results table / raw output + description: Paste the pooled table and significance rows as printed. Attach the JSON if large. + render: markdown + validations: + required: true + + - type: textarea + id: notes + attributes: + label: Observations + description: Anything surprising, queries that failed, or where the baseline beat the index. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 4eacf48..e7f3348 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,74 +1,74 @@ -name: Bug Report -description: Report a bug or unexpected behavior +name: Bug report +description: Something is wrong, crashes, or returns the wrong result title: "[Bug]: " labels: ["bug", "triage"] body: - type: markdown attributes: value: | - Thanks for taking the time to report a bug. Please fill in as much of the form below as possible. + Thanks. A reproducible report gets fixed fastest. Do not paste real secrets — the tool redacts snippets, your logs may not. - type: input id: version attributes: label: codebase-index version - description: What version of codebase-index are you running? - placeholder: "0.1.0" + description: "`pip show codebase-index` (there is no --version flag)" + placeholder: "1.9.0" validations: required: true - type: input - id: python-version + id: env attributes: - label: Python version - description: What Python version are you using? - placeholder: "3.12.3" + label: Python and OS + placeholder: "Python 3.12.3 · macOS 14.4 / Ubuntu 24.04 / Windows 11" validations: required: true - - type: input - id: os + - type: dropdown + id: surface attributes: - label: Operating system - description: What OS and version are you using? - placeholder: "macOS 14.4, Ubuntu 24.04, Windows 11" + label: Surface + options: + - CLI + - Claude Code skill / plugin + - Codex CLI + - OpenCode + - MCP server + - Installer scripts + - Other validations: required: true - type: textarea id: what-happened attributes: - label: What happened? - description: Describe the bug and what you expected to happen. + label: What happened, and what did you expect? placeholder: | - 1. Ran `codebase-index index` on my project - 2. Expected: index completes successfully - 3. Actual: got error "..." + Ran `codebase-index refs open_session --json` on . Expected the call in ctx.py; got an empty list with coverage.partial=false. validations: required: true - type: textarea - id: reproduction + id: repro attributes: label: Steps to reproduce - description: Provide minimal steps to reproduce the issue. + description: Ideally against a public repository or a tiny snippet we can index. placeholder: | - 1. `codebase-index init` - 2. `codebase-index index` - 3. `codebase-index search "query"` - 4. See error + 1. git clone https://github.com/... && cd ... + 2. codebase-index index + 3. codebase-index search "..." --json validations: required: true - type: textarea - id: logs + id: doctor attributes: - label: Relevant log output - description: Paste any relevant terminal output or error messages. + label: "`codebase-index doctor` output" render: shell - type: textarea - id: additional + id: logs attributes: - label: Additional context - description: Add any other context, screenshots, or configuration details. + label: Relevant output (`--json` where applicable) or traceback + render: shell diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..00e12b5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions and ideas + url: https://github.com/denfry/codebase-index/discussions + about: Ask how to use the tool, discuss design, or share what you built with it. + - name: Security vulnerability + url: https://github.com/denfry/codebase-index/security/advisories/new + about: Report privately. Please do not open a public issue (see SECURITY.md). diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index b3f039c..ab8c587 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,56 +1,50 @@ -name: Feature Request -description: Suggest an idea or improvement for codebase-index +name: Feature request +description: Propose an improvement or a new capability title: "[Feature]: " labels: ["enhancement", "triage"] body: - - type: markdown - attributes: - value: | - Thanks for suggesting a feature. Please describe your idea and its use case. - - type: textarea id: problem attributes: - label: Problem statement - description: Is your feature request related to a problem? Describe it. - placeholder: "I'm frustrated when..." + label: Problem + description: What are you trying to do with your agent that the tool makes hard today? + placeholder: "When I ask Claude Code about ..., codebase-index returns ..., so the agent ends up reading ..." validations: required: true - type: textarea - id: solution + id: proposal attributes: - label: Proposed solution - description: Describe what you want to happen. - placeholder: "It would be great if codebase-index could..." + label: Proposal + description: What should happen instead? A CLI example or a sketch of the JSON is ideal. validations: required: true - - type: textarea - id: alternatives - attributes: - label: Alternatives considered - description: Describe any alternative solutions or workarounds you've tried. - - type: dropdown - id: scope + id: area attributes: - label: Scope - description: Which area does this feature primarily affect? + label: Area options: - - Indexing (discovery, parsing, storage) - - Retrieval (search, ranking, fusion) - - CLI (commands, output format) - - Skill (SKILL.md, allowed-tools, workflow) - - Configuration (config.json, hooks) + - Retrieval quality (ranking, fusion, budget) + - Graph extraction (edges, impact, paths) + - Language support + - Indexing (discovery, parsing, freshness) + - CLI / output format + - MCP server + - Claude Code / Codex / OpenCode integration + - Benchmarks / evaluation - Documentation - - Testing / CI - Other validations: required: true - type: textarea - id: additional + id: evidence + attributes: + label: How would we know it worked? + description: Retrieval changes need a measurable effect (see tests/eval). A query and the file you expected is enough to start. + + - type: textarea + id: alternatives attributes: - label: Additional context - description: Add any other context, mockups, or examples. + label: Alternatives or workarounds you tried diff --git a/.github/ISSUE_TEMPLATE/language_support.yml b/.github/ISSUE_TEMPLATE/language_support.yml new file mode 100644 index 0000000..4b8485b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/language_support.yml @@ -0,0 +1,48 @@ +name: Language support +description: Request or offer symbol/graph extraction for a language +title: "[Language]: " +labels: ["language support"] +body: + - type: input + id: language + attributes: + label: Language and file extensions + placeholder: "Swift — .swift" + validations: + required: true + + - type: dropdown + id: current + attributes: + label: Current status (see docs/LANGUAGES.md) + options: + - Not detected at all + - Tier C (line chunks + FTS only) + - Tier B (generic Tree-sitter, no graph edges) + - Tier A but extraction is wrong or incomplete + validations: + required: true + + - type: input + id: grammar + attributes: + label: tree-sitter-language-pack grammar name + description: "`tree_sitter_language_pack.get_language('')` must succeed." + placeholder: "swift" + + - type: textarea + id: expectations + attributes: + label: What should be extracted? + description: Definition kinds (function/class/...), calls, imports, inheritance — with a 10–20 line sample file. + render: text + validations: + required: true + + - type: checkboxes + id: offer + attributes: + label: Contribution + options: + - label: I can open a PR adding the `LangSpec` and fixture (see docs/DEVELOPMENT.md → "Add a language") + - label: I can provide a public repository in this language for the retrieval eval diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b7d25b8..8b54ad6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,38 +1,21 @@ -## Description +## What and why -Describe your changes and their impact. + -Fixes #(issue) +## How I verified it -## Type of change +```bash +# commands you ran, e.g. +pytest tests/test_xxx.py -q --no-cov +python tests/eval/run_eval.py --ablate # required for any ranking change; paste the table below +``` -- [ ] Bug fix (non-breaking change that fixes an issue) -- [ ] New feature (non-breaking change that adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Documentation update -- [ ] Refactoring (no functional change) -- [ ] CI / build / tooling change + ## Checklist -- [ ] I have read the [CONTRIBUTING.md](../CONTRIBUTING.md) guide -- [ ] My code follows the project's style conventions (`ruff check`, `ruff format`) -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] All tests pass (`pytest`) -- [ ] Type checking passes (`mypy`, if applicable) -- [ ] I have updated the documentation accordingly -- [ ] I have updated `CHANGELOG.md` under `[Unreleased]` -- [ ] My commits follow [Conventional Commits](https://www.conventionalcommits.org/) - -## Testing - -Describe how you tested your changes: - -```bash -# Example: -pytest tests/test_my_new_feature.py -codebase-index index --root tests/fixtures/sample_repo -codebase-index search "test query" -``` - -## Screenshots (if applicable) +- [ ] `pytest`, `ruff check src tests`, `mypy src/codebase_index` pass +- [ ] `python scripts/sync_skill_copies.py --check` passes (if `skill_template/` changed) +- [ ] Goldens regenerated intentionally and the diff explained (if `--json` / MCP output changed) +- [ ] `CHANGELOG.md` updated under `[Unreleased]` (user-visible changes only; no version bump) +- [ ] No secrets, generated indexes, or local config committed diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..769cfd3 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,25 @@ +# Support + +**Something broken?** Run `codebase-index doctor` first; it catches most +install, config and stale-index problems. Then open a +[bug report](https://github.com/denfry/codebase-index/issues/new?template=bug_report.yml) +with the doctor output. + +**How do I…?** Check the [FAQ](../docs/FAQ.md), the +[quick start](../docs/QUICKSTART.md) and the +[installation guide](../docs/INSTALLATION.md). For anything else, start a +[discussion](https://github.com/denfry/codebase-index/discussions). + +**Agent integration** (Claude Code, Codex CLI, OpenCode, MCP clients): see +[docs/MCP.md](../docs/MCP.md) and [docs/INSTALLATION.md](../docs/INSTALLATION.md). +Ready-to-copy client configs are templates; if one does not work with the +current version of your client, that is a bug report we want. + +**Benchmarks**: reproduce a published run or add your own corpus with +[tests/eval](../tests/eval/README.md) and post the numbers with the +[benchmark report](https://github.com/denfry/codebase-index/issues/new?template=benchmark_report.yml) +template. + +**Security**: never in a public issue. See [SECURITY.md](../SECURITY.md). + +There is no paid support and no SLA; this is a volunteer-maintained project. diff --git a/.github/labels.yml b/.github/labels.yml new file mode 100644 index 0000000..c9da59a --- /dev/null +++ b/.github/labels.yml @@ -0,0 +1,63 @@ +# Repository labels. Apply with: scripts/apply_labels.sh (requires `gh` auth). +# Format: name, color (hex, no #), description. + +- name: bug + color: d73a4a + description: Something is broken or returns wrong results +- name: enhancement + color: a2eeef + description: New capability or improvement +- name: documentation + color: 0075ca + description: Docs, examples, demo, FAQ +- name: good first issue + color: 7057ff + description: Small, well-scoped, no prior context needed +- name: help wanted + color: 008672 + description: Maintainers would welcome a PR here +- name: benchmark + color: fbca04 + description: Corpora, metrics, baselines, reproductions of published runs +- name: language support + color: 1d76db + description: Tree-sitter LangSpecs, tiers, per-language extraction +- name: graph extraction + color: 5319e7 + description: Edge kinds, resolution, impact/path/describe +- name: retrieval quality + color: 0e8a16 + description: Ranking, fusion, priors, token budgeting +- name: integrations + color: c5def5 + description: Skills, hooks, installers, agent wiring +- name: mcp + color: bfd4f2 + description: MCP server, tools, envelopes, client configs +- name: claude-code + color: e99695 + description: Claude Code skill / plugin specifics +- name: codex + color: f9d0c4 + description: Codex CLI integration specifics +- name: opencode + color: fef2c0 + description: OpenCode integration specifics +- name: release + color: 006b75 + description: Versioning, changelog, packaging, publishing +- name: security + color: b60205 + description: Exclusion gates, redaction, network policy (report vulns privately) +- name: triage + color: ededed + description: Needs a maintainer look before it is actionable +- name: question + color: d876e3 + description: Usage question; may be moved to Discussions +- name: duplicate + color: cfd3d7 + description: Already tracked elsewhere +- name: wontfix + color: ffffff + description: Out of scope for this project diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 16837e0..5b10c9d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,162 +1,120 @@ # Contributing -Thank you for your interest in contributing to `codebase-index`. This project is a local-first Claude Code Skill for codebase indexing, and we welcome contributions of all kinds. +Thanks for helping make `codebase-index` better. It is a local-first retrieval +and code-graph layer for AI coding agents, and it lives or dies by three +invariants: -## Development Setup +1. **Retrieval quality is measured, not asserted.** +2. **The default path stays local and fails closed at security boundaries.** +3. **Machine-readable contracts (CLI `--json`, MCP envelopes) stay stable.** -### Prerequisites +The hands-on setup, test, benchmark and recipe guide is +[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). This page covers *what* to work on +and *how* to get it merged. -- Python 3.11 or later -- `pipx` or `uv` for package management (optional but recommended) -- Git - -### Clone and Install +## Quick start ```bash git clone https://github.com/denfry/codebase-index.git cd codebase-index - -# Using uv (recommended) -uv sync --all-extras - -# Or using pip -pip install -e ".[dev,embeddings-local,watch]" -``` - -### Run Tests - -```bash -pytest -pytest --cov=src/codebase_index --cov-report=term-missing -``` - -### Run Linting - -```bash -ruff check src/ tests/ -ruff format --check src/ tests/ -``` - -### Run Type Checking - -```bash +python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -e ".[dev,mcp]" +pytest # coverage gate: 80% +ruff check src tests mypy src/codebase_index +python scripts/sync_skill_copies.py --check ``` -## Commit Style - -We follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/): - -``` -(): - -[optional body] - -[optional footer(s)] -``` - -Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`, `ci` - -Examples: -``` -feat(retrieval): add RRF fusion for hybrid search -fix(storage): handle FTS5 trigger recreation on schema change -docs(readme): add comparison table and FAQ -test(parsers): add tree-sitter symbol extraction tests for Go -``` - -## Branch Naming - -Use the pattern `/`: - -- `feat/hybrid-search` -- `fix/fts5-trigger-recreate` -- `docs/readme-comparison` -- `test/treesitter-go` - -## Fork and Pull Request Workflow - -1. Fork the repository and clone your fork as `origin`. -2. Add the canonical repository as `upstream`: +Those four commands are exactly what CI runs. `ruff format` is not enforced. +Set `CBX_NO_SKILL_AUTO_UPDATE=1` before running the CLI inside the checkout. + +## Where help is wanted + +Issues are labelled by area so you can pick something that matches your +interests. `good first issue` and `help wanted` are the entry points. + +| Label | What it covers | Start in | +|---|---|---| +| `benchmark` | New corpora, metrics, baselines, reproductions of published runs | `tests/eval/`, `tests/benchmark_*.py` | +| `language support` | New Tier-A `LangSpec`s, better queries for existing grammars | `src/codebase_index/parsers/languages.py` | +| `graph extraction` | New edge kinds, better resolution, edge confidence | `src/codebase_index/parsers/treesitter.py`, `src/codebase_index/graph/` | +| `retrieval quality` | Ranking signals, fusion, priors, budgeting | `src/codebase_index/retrieval/` | +| `integrations` | Claude Code, Codex CLI, OpenCode, hooks, installers | `src/codebase_index/scaffold.py`, `adapters/`, `skill_template/` | +| `mcp` | MCP tools, envelopes, client configs | `src/codebase_index/mcp/server.py`, `docs/MCP.md` | +| `documentation` | Docs, examples, demo, FAQ | `docs/`, `examples/` | + +Reproducing a benchmark on a repository we have not seen is one of the most +useful contributions there is. Use the *Benchmark report* issue template even +when the result is unflattering. + +## What makes a good PR + +- **One concern per PR**, on a branch named `/` + (`feat/kotlin-imports`, `fix/fts-trigger-recreate`). +- **Tests.** New behaviour has tests; bug fixes have a regression test. The + coverage gate is 80% and CI runs on Linux, macOS and Windows with Python + 3.11–3.13. +- **Contracts.** If `--json` or an MCP payload changes, regenerate the goldens + intentionally (`UPDATE_GOLDEN=1 pytest tests/test_cli_golden.py + tests/test_mcp_golden.py`) and explain the diff. Removing a field or changing + a type bumps `MCP_SCHEMA_VERSION`. +- **Changelog.** Add user-visible changes under `[Unreleased]` in + `CHANGELOG.md`. Do not bump the version; maintainers do that at release time. +- **Skill copies.** After touching `src/codebase_index/skill_template/`, run + `python scripts/sync_skill_copies.py` and commit the regenerated copies. +- **Conventional Commits** for messages: `feat(retrieval): ...`, + `fix(storage): ...`, `docs(readme): ...`. + +### Retrieval-quality PRs specifically + +A ranking change is accepted on evidence, not on a plausible story. + +1. Put the new signal behind a boolean flag on `RetrievalTuning` + (`src/codebase_index/retrieval/tuning.py`) and add it to `ABLATABLE` in + `tests/eval/run_eval.py`. `RetrievalTuning.baseline()` must stay unchanged. +2. Run `python tests/eval/run_eval.py --ablate` on the shipped query sets *and* + at least one external corpus (`tests/eval/gen_queries.py` builds one from any + git repository in two commands). +3. Paste the pooled table and the paired significance row for your flag into + the PR description. Ship on `p < 0.05` for the pooled set; otherwise leave the + signal off by default or drop it. +4. Report mean tokens and latency alongside quality. Winning MRR while doubling + the token bill is not a win. + +## Fork and pull request workflow + +1. Fork, clone your fork as `origin`, add the canonical repo as `upstream`: ```bash git remote add upstream https://github.com/denfry/codebase-index.git git fetch upstream - ``` - -3. Create a focused branch from the latest `upstream/main`: - - ```bash - git fetch upstream git switch -c / upstream/main ``` -4. Keep unrelated changes in separate branches and pull requests. Do not commit - generated files, build artifacts, local configuration, credentials, or - editor-specific files. -5. Rebase onto the latest `upstream/main`, run the required checks, and push the - branch to your fork before opening the pull request. -6. Open the pull request against `denfry/codebase-index:main`. Explain the - problem, solution, verification performed, and any compatibility or migration - impact. - -Do not commit directly to `main`, merge `main` into a feature branch, or change -the project version unless a maintainer explicitly requests it. Add user-visible -changes to `CHANGELOG.md` under `[Unreleased]`; maintainers choose the release -version according to the project's versioning policy. - -The version lives in one place: `src/codebase_index/__init__.py` (`__version__`). -`pyproject.toml` reads it via hatch dynamic versioning. After changing the -version or anything under `src/codebase_index/skill_template/`, run -`python scripts/sync_skill_copies.py` to regenerate the committed skill copies -and version stamps; CI rejects the PR if they drift -(`python scripts/sync_skill_copies.py --check`). - -## Test Requirements - -- All new features must include tests. -- All bug fixes must include a regression test. -- Aim for >80% line coverage on new code. -- Tests must pass on Python 3.11+. -- Use the fixture repository under `tests/fixtures/sample_repo/` for integration tests. - -## Documentation Requirements - -- New commands or flags must be documented in the README and relevant docs files. -- New configuration options must be documented in `docs/INSTALLATION.md` and `examples/config.example.json`. -- API changes must be noted in `CHANGELOG.md` under `[Unreleased]`. - -## Pull Request Checklist - -Before submitting a PR, ensure: - -- [ ] Tests pass: `pytest` -- [ ] Linting passes: `ruff check src/ tests/` -- [ ] Formatting is correct: `ruff format src/ tests/` -- [ ] Type checking passes: `mypy src/codebase_index` (if applicable) -- [ ] CHANGELOG.md is updated under `[Unreleased]` -- [ ] The project version is unchanged unless a maintainer requested a bump -- [ ] Documentation is updated (README, docs/, examples/) -- [ ] Commit messages follow Conventional Commits -- [ ] No secrets or credentials are committed -- [ ] The PR description explains the change and links to any related issues - -## Code Review - -- All PRs require at least one approving review before merging. -- Reviewers will check for correctness, test coverage, documentation, and adherence to project conventions. -- Be respectful and constructive in code review comments. - -## Reporting Issues - -- Use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.yml) for bugs. -- Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.yml) for new features. -- Use the [skill listing request template](.github/ISSUE_TEMPLATE/skill_listing_request.yml) to request skill directory inclusion. - -## Code of Conduct - -This project follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). Please read it before participating. - -## License - -By contributing, you agree that your contributions will be licensed under the [MIT License](LICENSE). +2. Keep unrelated changes out. Do not commit generated indexes, build + artifacts, local configuration, credentials, or editor files. +3. Rebase onto the latest `upstream/main` before opening or updating the PR + (do not merge `main` into your branch). +4. Run the CI commands above, push to your fork, and open the PR against + `denfry/codebase-index:main`. Describe the problem, the solution, how you + verified it, and any compatibility impact. +5. Never force-push once review has started unless the rewrite is necessary and + announced in the PR. + +Maintainers cut releases from `main` following +[docs/RELEASE_CHECKLIST.md](docs/RELEASE_CHECKLIST.md); the version is +single-sourced in `src/codebase_index/__init__.py`. + +## Reporting issues + +- Bugs: [bug report template](.github/ISSUE_TEMPLATE/bug_report.yml), with + `codebase-index doctor` output. +- Features: [feature request template](.github/ISSUE_TEMPLATE/feature_request.yml). +- Benchmark runs: [benchmark report template](.github/ISSUE_TEMPLATE/benchmark_report.yml). +- Languages: [language support template](.github/ISSUE_TEMPLATE/language_support.yml). +- Security: **not** an issue. See [SECURITY.md](SECURITY.md). + +## Code of conduct and license + +This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). +Contributions are licensed under the [MIT License](LICENSE). diff --git a/SECURITY.md b/SECURITY.md index 1602659..5d1e94f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,51 +1,73 @@ # Security Policy -## Supported Versions - -| Version | Supported | -| ------- | ------------------ | -| 1.2.x | :white_check_mark: | -| < 1.2 | :x: | - -Only the latest minor version receives security updates. - -## Reporting a Vulnerability - -We take security seriously. If you discover a vulnerability, please report it responsibly: - -1. **Do not** open a public issue. -2. Email the maintainers with a description of the vulnerability and steps to reproduce. -3. We will acknowledge receipt within 48 hours and provide a timeline for a fix. -4. Once resolved, we will publish a security advisory and credit the reporter (if desired). - -## No Telemetry Promise - -`codebase-index` does **not** collect, transmit, or store any telemetry, usage data, or analytics. All indexing, search, and storage operations occur entirely on your local machine. There are no phone-home mechanisms, crash reporters, or usage counters. - -## Secret Handling - -- **Never indexed**: `.env` files, private keys (`.pem`, `.key`), certificates, tokens, credential files, and binary artifacts are excluded before parsing. -- **Redacted in output**: Any snippets that may contain secret-like patterns (AWS keys, JWTs, bearer tokens, connection strings) are masked before being returned to Claude or printed to the terminal. -- **Respects ignore files**: `.gitignore`, `.claudeignore`, `.codeindexignore`, and `.cursorignore` are all honored during discovery. - -## External Embeddings Opt-In - -The default configuration disables embeddings entirely (`backend = "noop"`). External embedding APIs (which would send code text to a remote service) require: - -1. Explicit `embeddings.allow_external = true` in configuration. -2. A user-provided API key via environment variable. -3. Warnings printed by both `doctor` and `index` commands. - -Without all three conditions, external embeddings are refused. - -## Threat Model - -- **Indexed content**: Treat indexing an untrusted repository the same as opening it in a text editor. Parsers operate over file content but do not execute code. -- **Cache location**: The SQLite index is stored in `.claude/cache/codebase-index/`. Ensure this directory is not committed to version control (it is in the default `.gitignore`). -- **World-writable directories**: `doctor` warns if the cache directory has insecure permissions. - -## Unsafe Patterns to Avoid - -- Do not commit the SQLite index file to a shared repository. -- Do not enable external embeddings on repositories containing proprietary or regulated code without reviewing your organization's data handling policies. -- Do not run `codebase-index index` on repositories you do not trust without reviewing the `doctor` output first. +`codebase-index` reads entire repositories, so its security posture matters +more than for a typical CLI. The full trust model, exclusion pipeline and +redaction rules are documented in [docs/SECURITY_MODEL.md](docs/SECURITY_MODEL.md); +this page covers reporting and the guarantees in short form. + +## Supported versions + +Only the latest minor release line receives security fixes. + +| Version | Supported | +|---|---| +| 1.9.x (latest) | Yes | +| < 1.9 | No — upgrade with `pip install -U codebase-index` | + +## Reporting a vulnerability + +Please do **not** open a public issue for security problems. + +1. Use GitHub private vulnerability reporting: + + (the *Report a vulnerability* button under the repository's **Security** tab). +2. Include the version (`pip show codebase-index`), + platform, a description, and reproduction steps. Redact any real secrets. +3. You will get an acknowledgement within a few days. Fixes are released as a + patch on the supported line and published as a GitHub security advisory, + crediting the reporter unless they prefer otherwise. + +If private reporting is unavailable for any reason, open a minimal issue that +says only "security report, please contact me" without details, and a +maintainer will reach out. + +## What the tool guarantees + +- **No telemetry.** No usage data, analytics, crash reports or phone-home of + any kind. +- **No network by default.** The base install makes no network requests. + The only code path that can send repository text off the machine is the + *external* embeddings backend, which is refused unless all three hold: + `embeddings.allow_external = true` in config, an API key provided via + environment variable, and the endpoint warning printed by `doctor` / `index`. +- **Secrets are never indexed.** `.env*`, private keys and certificates + (`*.pem`, `*.key`, `*.p12`, `*.pfx`, `id_rsa*`, `*.crt`, `*.keystore`), + `credentials*`, `secrets*`, binaries, dependency and build directories, + generated files and oversized files are excluded before parsing. +- **Secrets are redacted on output.** Snippets pass through + `output/redact.py` before reaching the agent or the terminal (AWS keys, + private-key blocks, JWTs and bearer tokens, connection strings with + credentials, Slack tokens, high-entropy values assigned to key-like names). +- **Ignore files are honoured.** `.gitignore`, `.claudeignore`, + `.codeindexignore` and `.cursorignore`. +- **Read-only agent surface.** The generated skill's `allowed-tools` and the + `cbx` wrappers whitelist read-only subcommands; `clean`, `init` and `watch` + are not callable from the skill. +- **Self-audit.** `codebase-index doctor --strict` exits non-zero if any gate + is misconfigured; use it in CI. + +## Threat model in brief + +- Indexing a repository is like opening it in an editor: parsers read file + content, nothing is executed. +- The derived index lives in `.claude/cache/codebase-index/` and is + gitignored by `init`. Do not commit it or share it as if it were sanitised — + redaction happens at output time, and the index stores indexed text. +- Treat `doctor` warnings about world-writable cache directories as real. + +## Release integrity + +Releases are built in GitHub Actions and published to PyPI with Trusted +Publishing (OIDC, no stored tokens). Signed checksums, SBOMs and build +attestations are on the roadmap and are **not** yet provided; do not assume +them. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..4b50efc --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,236 @@ +# Development guide + +Everything you need to make a first contribution in about fifteen minutes. For +the full design see [ARCHITECTURE.md](ARCHITECTURE.md); this page is the +hands-on version. + +## 1. Map of the code (two minutes) + +``` +src/codebase_index/ +├── cli.py Typer commands; every command delegates to service.py +├── service.py shared CLI/MCP layer: db resolution, search/impact/stats payloads +├── discovery/ walker.py (file walk), ignore.py (.gitignore & co), classify.py (language, secrets, binary, generated) +├── parsers/ languages.py (LangSpec per language), treesitter.py (symbols + edges), line_chunker.py +├── indexer/ pipeline.py (build_index / update_index), freshness.py +├── graph/ builder.py (edge resolution), expand.py (impact), analysis.py, navigate.py, export.py +├── retrieval/ pipeline.py (search), searchers.py, fusion.py, rerank.py, priors.py, tuning.py, budget.py, skeleton.py +├── storage/ db.py, schema.sql, repo.py (typed SQL accessors) +├── mcp/server.py stdio MCP server over the same service layer +├── output/ markdown.py, json.py, redact.py +└── skill_template/ canonical skill source; copies under .claude/ .codex/ .opencode/ skill/ skills/ are generated +``` + +Query path in one line: `retrieval/pipeline.py::search` → `detect_intent` → +`_run_retrievers` (path, symbol, FTS5, optional vector, optional graph) → `fuse` +(RRF) → `rerank` → dedup / diversify → token budget → payload. + +## 2. Set up (three minutes) + +Python 3.11+ and git are the only prerequisites. + +```bash +git clone https://github.com/denfry/codebase-index.git +cd codebase-index +python -m venv .venv +. .venv/bin/activate # Windows: .venv\Scripts\activate +pip install -e ".[dev,mcp]" +``` + +`dev` brings pytest, ruff, mypy, pyyaml and the `mcp` SDK; `mcp` is listed +separately so the server extra stays installable on its own. Optional extras: +`embeddings`, `embeddings-local`, `watch`, `build`. + +Before running the CLI **inside this checkout**, export +`CBX_NO_SKILL_AUTO_UPDATE=1`. Without it the skill auto-updater may rewrite the +committed `.skill_version` stamps when the installed package metadata is stale. +`tests/conftest.py` sets this for pytest automatically. + +## 3. Run the checks (what CI runs) + +CI (`.github/workflows/ci.yml`) runs exactly these: + +```bash +ruff check src tests +mypy src/codebase_index +python scripts/sync_skill_copies.py --check +pytest # coverage gate: --cov-fail-under=80 (pyproject.toml) +pytest tests/test_perf_smoke.py --runslow --no-cov # Linux + 3.12 only +``` + +Notes: + +- `ruff format` is **not** enforced; running `ruff format --check` on the + current tree reports many files. Format new code if you like, but do not + reformat files you are not otherwise touching. +- Tests marked `slow` are skipped unless you pass `--runslow` + (`tests/conftest.py::pytest_addoption`). +- Golden snapshots live in `tests/golden/*.json`. When an output contract + changes on purpose, regenerate them and review the diff: + + ```bash + UPDATE_GOLDEN=1 pytest tests/test_cli_golden.py tests/test_mcp_golden.py + ``` + + `tests/golden_utils.py` masks timestamps, commit SHAs and the package version + so snapshots are stable across machines; `schema_version` is deliberately not + masked because it is the contract under test. +- The skill copies gate: anything under `src/codebase_index/skill_template/` or + the version in `src/codebase_index/__init__.py` must be propagated with + `python scripts/sync_skill_copies.py` (no flag) before committing. + +Run a single test file quickly without the coverage gate: + +```bash +pytest tests/test_fusion.py -q --no-cov +``` + +## 4. Try the CLI on a fixture + +```bash +export CBX_NO_SKILL_AUTO_UPDATE=1 +codebase-index --root tests/fixtures/sample_repo index +codebase-index --root tests/fixtures/sample_repo search "refresh token" --json +codebase-index --root tests/fixtures/sample_repo impact User +``` + +`tests/fixtures/sample_repo/` deliberately contains files that must never be +indexed (`.env`, `secrets.pem`, `huge.json`, `logo.png`); see +`tests/fixtures/README.md`. + +## 5. Benchmark workflow + +The project rule is *measure improvements, do not assert them*. Three surfaces +exist; know which one you are using. + +| Surface | Command | Use it for | +|---|---|---| +| Retrieval eval (ranking gate) | `python tests/eval/run_eval.py` | Any change that can move ranking | +| Public synthetic suite | `python tests/benchmark_public.py --workdir .tmp-public-benchmark` | Metric-shape regression check (`tests/test_public_benchmark.py` wraps it in CI) | +| Older single-repo script | `python tests/benchmark_honest.py --repo ` | Index vs `rg`+window token/recall comparison against one repository | + +The retrieval eval (`tests/eval/`): + +- `harness.py` builds one index per corpus into a temp dir (`build_corpus_index`) + and reuses it for every variant, then computes recall@5/10, MRR, nDCG@10, + hit@3, P@5, MAP, `useful@budget`, mean tokens, duplicate rate, and latency + percentiles (`evaluate`, `format_table`, `pool`). +- `metrics.py` holds the IR metrics plus `paired_bootstrap_ci` and + `paired_permutation_p` (seeded, reproducible). +- `gen_queries.py` mints leak-free ground truth from git history (commit subject + → files that commit changed): + + ```bash + python tests/eval/gen_queries.py --repo ../some-repo --out /tmp/some-repo.yml + python tests/eval/run_eval.py --corpus ../some-repo:/tmp/some-repo.yml --ablate + ``` + +- `run_eval.py --ablate` runs a one-signal-off sweep over the boolean flags in + `ABLATABLE` and prints a paired significance table for each row. + +Checked-in query sets: `tests/eval/queries/self_repo.yml` (hand-written) and +`self_repo_git.yml` (generated). See [tests/eval/README.md](../tests/eval/README.md). + +## 6. Recipes + +### Add a language (Tier A symbol extraction) + +1. `src/codebase_index/discovery/classify.py`: add the extension to + `_LANG_BY_SUFFIX` and the language id to `_TREE_SITTER_LANGS`. +2. `src/codebase_index/parsers/languages.py`: add a `LangSpec(name, ts_name, + defs_query, calls_query, imports_query)` and register it in `LANGS`. + Definitions are captured as `@def.` with the name node as `@name`; + calls capture `@callee`; imports/inheritance capture `@import.module`, + `@extends.base`, `@implements.iface` (mapped by `_EDGE_PREFIXES` in + `parsers/treesitter.py`). +3. If the grammar's node kinds are not covered by `_definition_kind` / + `_name_node` / `_callee_node` in `parsers/treesitter.py`, extend them. +4. Add a fixture under `tests/fixtures/multilang/` and cases in + `tests/test_languages.py` (query compiles against the grammar) and + `tests/test_multilang_symbols.py` (`test_registry_consistency_every_treesitter_lang_extracts` + is parametrised over the registry, so a language with zero symbols fails loudly). +5. Update the tier table in [LANGUAGES.md](LANGUAGES.md). + +```bash +pytest tests/test_languages.py tests/test_multilang_symbols.py tests/test_graph.py -q --no-cov +``` + +### Add a graph edge kind + +Edges are rows in the `edges` table (`storage/schema.sql`): `edge_type`, +`src_kind`/`src_id`, `dst_kind`/`dst_id`/`dst_name`, `line`, `resolved`, and a +`confidence` of `extracted`, `inferred`, or `ambiguous`. + +1. Emit the edge from `parsers/treesitter.py::_extract_graph_edges` (query + captures) or `_extract_edges` (calls). Today's types are `call`, `reference`, + `import`, `extends`, `implements`. +2. Resolve it in `graph/builder.py::resolve_edges`. Symbol-target types are in + `_SYMBOL_EDGE_TYPES` and resolve only on a repo-unique name; imports resolve + by path suffix. Anything that cannot be pinned to one target is marked + `ambiguous` by `repo.mark_ambiguous_edges`. Never guess. +3. Make sure `graph/expand.py` (impact), `graph/navigate.py` (path/describe) and + `graph/export.py` (HTML/GraphML/DOT/Neo4j) do the right thing with the new + type, and document it in [SCHEMA.md](SCHEMA.md) and [LANGUAGES.md](LANGUAGES.md). +4. Add tests in `tests/test_graph.py` / `tests/test_graph_coverage.py`; goldens + that include edges (`impact_*`, `mcp_impact_of*`, `path_*`) may need + regeneration. + +### Change retrieval ranking + +1. Put every new signal behind a boolean on `RetrievalTuning` + (`src/codebase_index/retrieval/tuning.py`). Defaults are the shipped + configuration; `RetrievalTuning.baseline()` must keep reproducing the 1.7.0 + behaviour, otherwise the ablation table is meaningless. +2. Wire it in `retrieval/pipeline.py::search` (candidate generation, `fuse`, + `rerank`, selection) or in `retrieval/rerank.py` / `retrieval/priors.py` + (`source_role_prior`, capped by `MAX_ABS_PRIOR` so priors stay tiebreakers). +3. Add the flag to `ABLATABLE` in `tests/eval/run_eval.py`. +4. Run `python tests/eval/run_eval.py --ablate` on at least two corpora + (`--corpus`), and paste the pooled table plus the significance row into the + PR. A signal ships only if its row is significant (p < 0.05) on the pooled + set; otherwise it stays off by default or is removed. +5. Unit tests: `tests/test_fusion.py`, `tests/test_hybrid_ranking.py`, + `tests/test_priors.py`, `tests/test_diversity.py`. + +### Add an MCP tool + +1. `src/codebase_index/mcp/server.py`: decorate a function with `@_tool()` and + return `_emit("", payload)`. The `_emit` envelope adds + `schema_version` and `tool`; error paths must use it too + (`_no_index_payload()` is the standard no-index body). +2. Do the real work in `service.py` so the CLI command and the MCP tool share + one implementation. +3. Register the tool in `tests/test_mcp_server.py::test_mcp_server_has_expected_tools` + and in the envelope parametrisation there; add a golden case in + `tests/test_mcp_golden.py` and generate `tests/golden/mcp_.json` with + `UPDATE_GOLDEN=1`. +4. Document it in the tool table of [MCP.md](MCP.md). Bump `MCP_SCHEMA_VERSION` + only for a breaking change (field removal or type change). + +### Add a CLI command + +Add it in `cli.py`, delegate to `service.py`, support `--json`, and if the skill +should be allowed to call it, add it to the `ALLOWED` whitelist in +`src/codebase_index/skill_template/scripts/cbx` and `cbx.ps1`, then run +`python scripts/sync_skill_copies.py` and update `references/commands.md` in the +template. Add a golden in `tests/test_cli_golden.py` for `--json` output. + +## 7. Release process (maintainers) + +The version is single-sourced in `src/codebase_index/__init__.py` +(`__version__`); `pyproject.toml` reads it through hatch dynamic versioning. +Two mirrors are kept in sync by `scripts/sync_skill_copies.py`: +`.claude-plugin/plugin.json` (`version`) and `requirements.lock` (release tag +tarball). Tagging `vX.Y.Z` triggers `.github/workflows/release.yml`: test gate, +build, `twine check`, `scripts/release_smoke.py` (clean venv install → init → +index → search), GitHub release, and PyPI publish via Trusted Publishing. + +Follow [RELEASE_CHECKLIST.md](RELEASE_CHECKLIST.md) top to bottom. Contributors +never bump the version; they add entries under `[Unreleased]` in +`CHANGELOG.md`. + +## 8. Where to ask + +Open a [discussion](https://github.com/denfry/codebase-index/discussions) for +design questions, an issue for bugs and concrete proposals, and read +[CONTRIBUTING.md](../CONTRIBUTING.md) for the PR workflow. diff --git a/scripts/apply_labels.sh b/scripts/apply_labels.sh new file mode 100644 index 0000000..77f8cdf --- /dev/null +++ b/scripts/apply_labels.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Create or update the repository labels from .github/labels.yml. +# Requires: gh (authenticated), python3 with PyYAML (installed by the dev extra). +# scripts/apply_labels.sh # denfry/codebase-index +# scripts/apply_labels.sh owner/repo # another repository +set -euo pipefail + +REPO="${1:-denfry/codebase-index}" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +LABELS="$HERE/../.github/labels.yml" + +python3 - "$LABELS" <<'PY' | while IFS=$'\t' read -r name color desc; do +import sys, yaml +for row in yaml.safe_load(open(sys.argv[1], encoding="utf-8")): + print(f"{row['name']}\t{row['color']}\t{row.get('description', '')}") +PY + echo "label: $name" + gh label create "$name" --repo "$REPO" --color "$color" --description "$desc" --force +done From a509092aee1fdc781b6b83f1b0b339fddedef7c8 Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:10:36 +0300 Subject: [PATCH 06/12] test(eval): index vs rg+window and repo-map-style baselines on public repositories run_eval.py compares the ranker with its own earlier versions; it cannot say whether an index beats not having one. run_baselines.py asks that question on Flask, Gson and Fastify at pinned commits, with git-derived ground truth (150 newest localised commits each), symmetric tiktoken accounting on every side, and paired bootstrap/permutation significance. The logged run is under tests/eval/results/. Pooled over 450 queries: hit@3 0.547 (index) vs 0.304 (rg + 80-line windows), MRR 0.456 vs 0.263, p < 0.001 on every metric, at 3.8k vs 3.5k context tokens per query once the read plan is capped (6.8k before the cap). Repo-map-style context is scored on answer presence under a 2k/8k budget. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- tests/eval/baselines.py | 443 + .../results/2026-09-04-public-baselines.json | 14501 ++++++++++++++++ .../results/2026-09-04-public-baselines.md | 104 + tests/eval/run_baselines.py | 348 + tests/test_eval_baselines.py | 68 + tests/test_gen_queries.py | 15 + 6 files changed, 15479 insertions(+) create mode 100644 tests/eval/baselines.py create mode 100644 tests/eval/results/2026-09-04-public-baselines.json create mode 100644 tests/eval/results/2026-09-04-public-baselines.md create mode 100644 tests/eval/run_baselines.py create mode 100644 tests/test_eval_baselines.py diff --git a/tests/eval/baselines.py b/tests/eval/baselines.py new file mode 100644 index 0000000..27dd391 --- /dev/null +++ b/tests/eval/baselines.py @@ -0,0 +1,443 @@ +"""Baselines the index is compared against, with symmetric token accounting. + +Two baselines model what an agent does *without* an index: + +* ``rg_window`` — a disciplined grep agent. Drop stop-words from the question, + search the salient terms with ripgrep, rank files by match density, then read + an 80-line window around the densest hit of each of the top-K files. +* ``repo_map`` — a repo-map-style context blob in the spirit of Aider's map: + file paths plus definition signatures, ranked by graph degree (and, in the + query-aware variant, by identifier overlap with the question), packed under a + fixed token budget and handed to the agent up front. + +Both are approximations of a *style* of context construction, not a +re-implementation of any specific product. Neither uses the ranking pipeline; +``repo_map`` reads the symbol table from the index database only because +Tree-sitter signatures are what a repo map is built from. + +Symmetry rules +-------------- +Every side is charged with the same tokenizer for the text that actually enters +the agent's context: + +* index: the JSON payload the agent receives, plus the top-K + ``recommended_reads`` line ranges (a "follow-through read"); +* rg_window: the ripgrep listing (capped at ``LISTING_CAP`` lines — agents + truncate long tool output) plus the top-K windows it reads; +* repo_map: the map itself (the budget it was packed under). + +Latency is reported for context only. The index runs in-process, ripgrep is a +separate binary; neither is a fair wall-clock claim against the other. +""" + +from __future__ import annotations + +import json +import os +import re +import shutil +import sqlite3 +import subprocess +import time +from collections.abc import Iterable, Sequence +from dataclasses import dataclass, field +from pathlib import Path + +from codebase_index.retrieval.pipeline import search +from codebase_index.retrieval.tuning import RetrievalTuning + +from . import metrics +from .harness import EvalQuery + +# --- shared configuration --------------------------------------------------- +WINDOW = 80 +"""Lines read around a grep hit; matches the index's code window.""" +TOP_K = 3 +"""Files an agent opens after a search: it 'starts with ranks 1-3'.""" +LISTING_CAP = 50 +"""Lines of `rg` output an agent actually looks at before deciding what to open.""" +REPO_MAP_BUDGETS = (2000, 8000) +"""Token budgets for the repo-map baseline: a small chat-context map and a large one.""" + +# --- token counting (identical on every side) -------------------------------- +try: # pragma: no cover - exercised only when tiktoken is installed + import tiktoken + + _ENC = tiktoken.get_encoding("cl100k_base") + + def count_tokens(text: str) -> int: + return len(_ENC.encode(text, disallowed_special=())) + + TOKENIZER = "tiktoken/cl100k_base" +except Exception: # pragma: no cover + def count_tokens(text: str) -> int: + return max(0, len(text) // 4) + + TOKENIZER = "chars//4 (tiktoken not installed)" + + +# --- salient-term extraction (what an agent greps for) ------------------------ +STOPWORDS = frozenset( + """ + the a an is are was were be been being how does do did what where which who whom + when why to of in on for and or with from down up it this that these those i would + should could can will show me all work works happen happens other into during if + rename use used uses get got set via across between add adds added fix fixes fixed + remove removes removed update updates updated make makes made support supports + allow allows allowed change changes changed improve improves improved when not + """.split() +) +_TERM_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]+") + + +def salient_terms(query: str, *, max_terms: int = 6) -> list[str]: + """Terms an agent would grep for: identifiers kept whole, stop-words dropped.""" + out: list[str] = [] + seen: set[str] = set() + for term in _TERM_RE.findall(query): + key = term.lower() + if key in STOPWORDS or len(term) < 3 or key in seen: + continue + seen.add(key) + out.append(term) + # Longest terms first: an agent greps the most specific identifier before + # generic words, and ripgrep's OR of all of them is order-independent anyway. + out.sort(key=len, reverse=True) + return out[:max_terms] + + +# --- repository file access ---------------------------------------------------- +TEXT_EXTS = frozenset( + """ + .py .pyi .js .mjs .cjs .jsx .ts .tsx .java .kt .kts .go .rs .rb .php .cs .c .h .cc + .cpp .hpp .swift .scala .lua .sql .sh .ps1 .md .rst .txt .yml .yaml .toml .json + .xml .gradle .cfg .ini + """.split() +) +IGNORE_PARTS = frozenset( + """ + .git node_modules __pycache__ .venv venv dist build target .idea .gradle out + .claude .codex .opencode .tox .mypy_cache .pytest_cache .ruff_cache + """.split() +) + + +def _norm(path: str) -> str: + return path.replace("\\", "/") + + +@dataclass +class Corpus: + root: Path + _lines: dict[str, list[str]] = field(default_factory=dict) + + def lines(self, rel: str) -> list[str]: + rel = _norm(rel) + cached = self._lines.get(rel) + if cached is None: + try: + cached = (self.root / rel).read_text( + encoding="utf-8", errors="ignore" + ).splitlines() + except OSError: + cached = [] + self._lines[rel] = cached + return cached + + def read_range(self, rel: str, start: int, end: int) -> str: + lines = self.lines(rel) + if not lines: + return "" + start = max(1, start) + end = min(len(lines), end) + if end < start: + return "" + return "\n".join(lines[start - 1 : end]) + + def is_text(self, rel: str) -> bool: + parts = _norm(rel).split("/") + if any(p in IGNORE_PARTS for p in parts): + return False + return Path(rel).suffix.lower() in TEXT_EXTS + + +def _merge(ranges: Iterable[tuple[int, int]]) -> list[tuple[int, int]]: + merged: list[list[int]] = [] + for s, e in sorted(ranges): + if merged and s <= merged[-1][1] + 1: + merged[-1][1] = max(merged[-1][1], e) + else: + merged.append([s, e]) + return [(s, e) for s, e in merged] + + +def tokens_for_reads(corpus: Corpus, reads: dict[str, list[tuple[int, int]]]) -> int: + """Charge every (file, line range) once, ranges merged per file.""" + total = 0 + for rel, ranges in reads.items(): + for s, e in _merge(ranges): + total += count_tokens(corpus.read_range(rel, s, e)) + return total + + +# --- outcome ------------------------------------------------------------------- +@dataclass +class BaselineOutcome: + ranked_files: list[str] + tokens: int + """Everything that entered context: listing/packet + top-K reads.""" + packet_tokens: int + """The listing / packet / map alone, before any follow-through read.""" + latency_ms: float + extra: dict = field(default_factory=dict) + + +# --- index side ---------------------------------------------------------------- +def run_index( + conn: sqlite3.Connection, + corpus: Corpus, + q: EvalQuery, + *, + tuning: RetrievalTuning | None = None, + limit: int = 10, + token_budget: int = 1500, + max_read_lines: int = 120, +) -> BaselineOutcome: + start = time.perf_counter() + payload = search( + conn, + q.query, + mode="hybrid", + limit=limit, + token_budget=token_budget, + no_fallback=True, + tuning=tuning or RetrievalTuning(), + max_read_lines=max_read_lines, + ) + latency_ms = (time.perf_counter() - start) * 1000.0 + + ranked: list[str] = [] + for r in payload.get("results", []): + p = _norm(r["path"]) + if p not in ranked: + ranked.append(p) + + # The packet is what an MCP / --json agent literally receives. + packet_tokens = count_tokens(json.dumps(payload, ensure_ascii=False)) + + # Follow-through: the agent opens the top-K recommended ranges. + reads: dict[str, list[tuple[int, int]]] = {} + for rr in payload.get("recommended_reads", [])[:TOP_K]: + s = int(rr.get("line_start") or 1) + e = int(rr.get("line_end") or s) + if e <= s: + e = s + WINDOW - 1 + reads.setdefault(_norm(rr["path"]), []).append((s, e)) + read_tokens = tokens_for_reads(corpus, reads) + + return BaselineOutcome( + ranked_files=ranked, + tokens=packet_tokens + read_tokens, + packet_tokens=packet_tokens, + latency_ms=latency_ms, + extra={"confidence": payload.get("confidence"), "read_tokens": read_tokens}, + ) + + +# --- rg + window --------------------------------------------------------------- +RG = os.environ.get("CBX_RG") or shutil.which("rg") +"""Path to ripgrep; set CBX_RG when `rg` is a shell alias rather than a binary on PATH.""" + + +def rg_version() -> str | None: + """`ripgrep X.Y.Z` for the report, or None when the Python fallback is in use.""" + if not RG: + return None + try: + out = subprocess.run([RG, "--version"], capture_output=True, text=True, timeout=10).stdout + except (OSError, subprocess.SubprocessError): + return None + return out.splitlines()[0].strip() if out else None + + +def _rg_hits(corpus: Corpus, terms: Sequence[str]) -> tuple[dict[str, list[int]], list[str], float]: + """Return ({file: [line numbers]}, listing lines, elapsed ms) using ripgrep. + + Falls back to a pure-Python scan when ripgrep is not installed; the fallback + is labelled in the report because its latency is not comparable. + """ + if not terms: + return {}, [], 0.0 + start = time.perf_counter() + hits: dict[str, list[int]] = {} + listing: list[str] = [] + if RG: + cmd = [RG, "-n", "-i", "--no-heading", "--no-messages", "--color", "never"] + for t in terms: + cmd += ["-e", re.escape(t)] + for part in sorted(IGNORE_PARTS): + cmd += ["--glob", f"!{part}"] + proc = subprocess.run( + cmd, cwd=corpus.root, capture_output=True, text=True, + encoding="utf-8", errors="replace", + ) + for line in proc.stdout.splitlines(): + path, _, rest = line.partition(":") + lineno, _, _text = rest.partition(":") + rel = _norm(path) + if not corpus.is_text(rel) or not lineno.isdigit(): + continue + hits.setdefault(rel, []).append(int(lineno)) + listing.append(line) + else: # pragma: no cover - only without ripgrep + pats = [re.compile(re.escape(t), re.I) for t in terms] + for p in corpus.root.rglob("*"): + if not p.is_file(): + continue + rel = _norm(str(p.relative_to(corpus.root))) + if not corpus.is_text(rel): + continue + for i, text in enumerate(corpus.lines(rel), 1): + if any(pat.search(text) for pat in pats): + hits.setdefault(rel, []).append(i) + listing.append(f"{rel}:{i}:{text}") + return hits, listing, (time.perf_counter() - start) * 1000.0 + + +def run_rg_window(corpus: Corpus, q: EvalQuery) -> BaselineOutcome: + terms = salient_terms(q.query) + hits, listing, elapsed = _rg_hits(corpus, terms) + + # Rank by match density: the heuristic a grep agent applies when scanning + # `rg` output ("this file lights up the most"). + ranked = [rel for rel, _ in sorted(hits.items(), key=lambda kv: (-len(kv[1]), kv[0]))] + + reads: dict[str, list[tuple[int, int]]] = {} + for rel in ranked[:TOP_K]: + lines = hits[rel] + center = lines[len(lines) // 2] + reads.setdefault(rel, []).append((center - WINDOW // 2, center + WINDOW // 2)) + + listing_text = "\n".join(listing[:LISTING_CAP]) + packet_tokens = count_tokens(listing_text) + read_tokens = tokens_for_reads(corpus, reads) + return BaselineOutcome( + ranked_files=ranked, + tokens=packet_tokens + read_tokens, + packet_tokens=packet_tokens, + latency_ms=elapsed, + extra={ + "terms": terms, + "matched_files": len(hits), + "match_lines": sum(len(v) for v in hits.values()), + "read_tokens": read_tokens, + }, + ) + + +# --- repo-map style ------------------------------------------------------------ +@dataclass(frozen=True) +class MapEntry: + path: str + text: str + """Rendered map block: the path plus its definition signatures.""" + tokens: int + degree: int + names: frozenset[str] + + +def build_repo_map_entries(conn: sqlite3.Connection) -> list[MapEntry]: + """One block per file: path + top-level definition signatures, from the symbol table.""" + rows = conn.execute( + """ + SELECT f.path, s.name, s.kind, s.signature, s.in_degree, s.parent_id + FROM files f LEFT JOIN symbols s ON s.file_id = f.id + ORDER BY f.path, s.line_start + """ + ).fetchall() + by_file: dict[str, list[tuple]] = {} + for row in rows: + by_file.setdefault(_norm(row[0]), []).append(row) + + entries: list[MapEntry] = [] + for path, syms in by_file.items(): + lines = [f"{path}:"] + degree = 0 + names: set[str] = set() + for _p, name, kind, signature, in_degree, parent_id in syms: + if name is None: + continue + degree += int(in_degree or 0) + names.add(str(name).lower()) + if parent_id is None: # top-level definitions only, like a repo map + sig = (signature or f"{kind} {name}").strip().splitlines()[0] + lines.append(f" {sig}") + text = "\n".join(lines) + entries.append(MapEntry(path, text, count_tokens(text), degree, frozenset(names))) + return entries + + +def pack_repo_map( + entries: Sequence[MapEntry], + *, + budget: int, + query_terms: Sequence[str] = (), +) -> tuple[list[str], int]: + """Greedy pack of map blocks under `budget` tokens. + + Ordering: query-aware variant first ranks files whose definition names overlap + the question's identifiers, then by graph degree (a stand-in for the PageRank + a real repo map uses); the query-agnostic variant uses degree alone. + """ + terms = {t.lower() for t in query_terms} + + def overlap(e: MapEntry) -> int: + if not terms: + return 0 + return sum(1 for n in e.names if any(t in n or n in t for t in terms)) + + ordered = sorted(entries, key=lambda e: (-overlap(e), -e.degree, e.path)) + chosen: list[str] = [] + used = 0 + for e in ordered: + if used + e.tokens > budget: + continue + chosen.append(e.path) + used += e.tokens + return chosen, used + + +def run_repo_map( + entries: Sequence[MapEntry], q: EvalQuery, *, budget: int, query_aware: bool +) -> BaselineOutcome: + start = time.perf_counter() + terms = salient_terms(q.query) if query_aware else () + chosen, used = pack_repo_map(entries, budget=budget, query_terms=terms) + latency = (time.perf_counter() - start) * 1000.0 + # A repo map is a blob, not a ranking: `ranked_files` is the pack order, so + # hit@K on it says "is the answer even present in what the agent was handed". + return BaselineOutcome( + ranked_files=chosen, + tokens=used, + packet_tokens=used, + latency_ms=latency, + extra={"files_in_map": len(chosen), "budget": budget}, + ) + + +# --- scoring ------------------------------------------------------------------- +METRICS = ("hit@3", "recall@5", "MRR") + + +def score(outcome: BaselineOutcome, q: EvalQuery, *, present_only: bool = False) -> dict[str, float]: + rel = q.expected_files + ranked = outcome.ranked_files + if present_only: + # For a context blob, "present in the map" is the only meaningful hit. + present = 1.0 if any(p in set(ranked) for p in rel) else 0.0 + return {"present": present, "recall": metrics.recall_at_k(ranked, rel, len(ranked) or 1)} + return { + "hit@3": metrics.hit_rate_at_k(ranked, rel, 3), + "recall@5": metrics.recall_at_k(ranked, rel, 5), + "MRR": metrics.reciprocal_rank(ranked, rel), + } diff --git a/tests/eval/results/2026-09-04-public-baselines.json b/tests/eval/results/2026-09-04-public-baselines.json new file mode 100644 index 0000000..50752d5 --- /dev/null +++ b/tests/eval/results/2026-09-04-public-baselines.json @@ -0,0 +1,14501 @@ +{ + "date": "2026-09-04", + "version": "1.9.0", + "tokenizer": "tiktoken/cl100k_base", + "ripgrep": "ripgrep 14.1.1 (rev 4649aa9700)", + "platform": "Windows 11 / Python 3.12.12", + "corpora": [ + { + "corpus": "flask", + "root": "C:\\Users\\dabin\\AppData\\Local\\Temp\\claude\\D--Projects-codebase-index\\7f35eb46-c8c9-49e7-9d02-4c510c6877d0\\scratchpad\\repos\\flask", + "head": "d318b683471101618febed18996405ad26462110", + "n_queries": 150, + "index_build_ms": 3240.377099999023, + "files_indexed": 229, + "symbols_indexed": 1622, + "edges_indexed": 4425, + "summary": { + "index": { + "hit@3": 0.4866666666666667, + "recall@5": 0.5033333333333333, + "MRR": 0.3832539682539683, + "tokens_mean": 3590.76, + "tokens_median": 3576.5, + "packet_tokens_mean": 2358.9133333333334, + "latency_p50_ms": 45.75900000054389 + }, + "index (uncapped reads)": { + "hit@3": 0.4866666666666667, + "recall@5": 0.5033333333333333, + "MRR": 0.3832539682539683, + "tokens_mean": 5723.6, + "tokens_median": 3776.5, + "packet_tokens_mean": 2350.5, + "latency_p50_ms": 44.97279999850434 + }, + "rg+window": { + "hit@3": 0.23333333333333334, + "recall@5": 0.245, + "MRR": 0.22882812025866897, + "tokens_mean": 3078.7733333333335, + "tokens_median": 3187.0, + "packet_tokens_mean": 1008.8533333333334, + "latency_p50_ms": 27.9039000015473 + }, + "repo-map 2k": { + "present": 0.31333333333333335, + "recall": 0.22444444444444442, + "tokens_mean": 1998.0, + "tokens_median": 1998.0, + "packet_tokens_mean": 1998.0, + "latency_p50_ms": 0.11129999984405003 + }, + "repo-map 2k query-aware": { + "present": 0.2733333333333333, + "recall": 0.20277777777777778, + "tokens_mean": 1999.0933333333332, + "tokens_median": 1999.0, + "packet_tokens_mean": 1999.0933333333332, + "latency_p50_ms": 0.8749000007810537 + }, + "repo-map 8k": { + "present": 1.0, + "recall": 1.0, + "tokens_mean": 7728.0, + "tokens_median": 7728.0, + "packet_tokens_mean": 7728.0, + "latency_p50_ms": 0.07610000102431513 + }, + "repo-map 8k query-aware": { + "present": 1.0, + "recall": 1.0, + "tokens_mean": 7728.0, + "tokens_median": 7728.0, + "packet_tokens_mean": 7728.0, + "latency_p50_ms": 0.8187000021280255 + } + }, + "per_query": { + "index": { + "hit@3": [ + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0 + ], + "recall@5": [ + 1.0, + 0.5, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 0.75, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.25, + 1.0, + 0.3333333333333333, + 0.5, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.5, + 1.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.3333333333333333, + 0.25, + 1.0, + 0.5, + 1.0, + 0.0, + 0.5, + 0.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.75, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0 + ], + "MRR": [ + 0.5, + 0.25, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.25, + 0.0, + 0.0, + 0.5, + 0.5, + 0.3333333333333333, + 1.0, + 0.16666666666666666, + 0.0, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.2, + 1.0, + 0.25, + 0.0, + 0.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.2, + 0.25, + 0.0, + 0.3333333333333333, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.25, + 0.5, + 0.5, + 0.0, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 1.0, + 0.0, + 0.2, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.0, + 0.5, + 1.0, + 0.14285714285714285, + 0.3333333333333333, + 0.0, + 1.0, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 0.5, + 0.14285714285714285, + 0.0, + 0.5, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.5, + 0.25, + 1.0, + 0.5, + 1.0, + 0.25, + 0.3333333333333333, + 0.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.25, + 0.0, + 0.3333333333333333, + 0.5, + 0.0, + 1.0, + 0.0, + 0.5, + 0.25, + 0.0, + 0.0, + 0.14285714285714285, + 0.0, + 0.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.25, + 1.0, + 0.0, + 1.0, + 0.2, + 0.0, + 0.25, + 0.3333333333333333, + 0.3333333333333333, + 1.0, + 0.25, + 0.5, + 0.2, + 0.5, + 1.0, + 0.5, + 0.16666666666666666, + 0.0, + 1.0, + 0.14285714285714285, + 0.25, + 1.0, + 0.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.16666666666666666, + 0.0 + ] + }, + "index (uncapped reads)": { + "hit@3": [ + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0 + ], + "recall@5": [ + 1.0, + 0.5, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 0.75, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.25, + 1.0, + 0.3333333333333333, + 0.5, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.5, + 1.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.3333333333333333, + 0.25, + 1.0, + 0.5, + 1.0, + 0.0, + 0.5, + 0.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.75, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0 + ], + "MRR": [ + 0.5, + 0.25, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.25, + 0.0, + 0.0, + 0.5, + 0.5, + 0.3333333333333333, + 1.0, + 0.16666666666666666, + 0.0, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.2, + 1.0, + 0.25, + 0.0, + 0.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.2, + 0.25, + 0.0, + 0.3333333333333333, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.25, + 0.5, + 0.5, + 0.0, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 1.0, + 0.0, + 0.2, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.0, + 0.5, + 1.0, + 0.14285714285714285, + 0.3333333333333333, + 0.0, + 1.0, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 0.5, + 0.14285714285714285, + 0.0, + 0.5, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.5, + 0.25, + 1.0, + 0.5, + 1.0, + 0.25, + 0.3333333333333333, + 0.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.25, + 0.0, + 0.3333333333333333, + 0.5, + 0.0, + 1.0, + 0.0, + 0.5, + 0.25, + 0.0, + 0.0, + 0.14285714285714285, + 0.0, + 0.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.25, + 1.0, + 0.0, + 1.0, + 0.2, + 0.0, + 0.25, + 0.3333333333333333, + 0.3333333333333333, + 1.0, + 0.25, + 0.5, + 0.2, + 0.5, + 1.0, + 0.5, + 0.16666666666666666, + 0.0, + 1.0, + 0.14285714285714285, + 0.25, + 1.0, + 0.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.16666666666666666, + 0.0 + ] + }, + "rg+window": { + "hit@3": [ + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0 + ], + "recall@5": [ + 0.0, + 0.5, + 0.5, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.3333333333333333, + 0.75, + 0.0, + 0.5, + 0.0, + 0.0, + 1.0, + 0.0, + 0.25, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.6666666666666666, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.25, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0 + ], + "MRR": [ + 0.08333333333333333, + 1.0, + 1.0, + 0.125, + 0.125, + 1.0, + 0.5, + 0.045454545454545456, + 0.023255813953488372, + 0.0, + 0.09090909090909091, + 0.05263157894736842, + 0.0, + 0.07142857142857142, + 0.07692307692307693, + 0.5, + 0.037037037037037035, + 1.0, + 0.1, + 0.0625, + 0.0, + 0.08333333333333333, + 0.0, + 0.1, + 0.1, + 1.0, + 0.05555555555555555, + 0.0, + 0.0, + 0.16666666666666666, + 0.0, + 0.5, + 0.125, + 1.0, + 0.02702702702702703, + 0.14285714285714285, + 1.0, + 0.125, + 0.0, + 0.125, + 0.047619047619047616, + 0.07692307692307693, + 1.0, + 0.2, + 0.5, + 0.01818181818181818, + 0.029411764705882353, + 0.0, + 0.0625, + 0.2, + 0.02631578947368421, + 0.5, + 0.0, + 0.06666666666666667, + 0.1111111111111111, + 0.0, + 0.0, + 0.041666666666666664, + 0.0, + 1.0, + 0.25, + 1.0, + 0.1, + 0.5, + 0.16666666666666666, + 0.0, + 1.0, + 0.019230769230769232, + 1.0, + 0.09090909090909091, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 0.5, + 0.16666666666666666, + 0.3333333333333333, + 0.3333333333333333, + 0.0, + 0.125, + 0.0, + 0.030303030303030304, + 0.1111111111111111, + 0.023809523809523808, + 0.25, + 0.0, + 0.3333333333333333, + 0.2, + 0.0, + 0.0, + 0.0, + 0.1111111111111111, + 0.3333333333333333, + 0.03333333333333333, + 0.09090909090909091, + 0.012987012987012988, + 0.08333333333333333, + 0.0625, + 0.0, + 0.0, + 0.0, + 0.06666666666666667, + 0.1, + 0.125, + 0.043478260869565216, + 1.0, + 0.043478260869565216, + 0.03333333333333333, + 0.14285714285714285, + 0.0, + 0.017241379310344827, + 0.125, + 1.0, + 1.0, + 0.09090909090909091, + 0.09090909090909091, + 0.0, + 0.25, + 0.0, + 0.25, + 0.125, + 0.043478260869565216, + 0.25, + 0.16666666666666666, + 0.0, + 0.2, + 0.058823529411764705, + 1.0, + 0.037037037037037035, + 0.25, + 0.14285714285714285, + 0.0, + 1.0, + 0.25, + 0.3333333333333333, + 0.5, + 0.05, + 1.0, + 0.006802721088435374, + 0.3333333333333333, + 0.0, + 1.0, + 0.05, + 0.0, + 0.0625 + ] + }, + "repo-map 2k": { + "present": [ + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "recall": [ + 0.0, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.5, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.5, + 0.0, + 0.5, + 1.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.0, + 0.6666666666666666, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.6666666666666666, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "repo-map 2k query-aware": { + "present": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ], + "recall": [ + 1.0, + 0.5, + 0.5, + 1.0, + 1.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.6666666666666666, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.6666666666666666, + 0.5, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.25, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "repo-map 8k": { + "present": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "recall": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + }, + "repo-map 8k query-aware": { + "present": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "recall": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + }, + "tokens_per_query": { + "index": [ + 3714, + 3680, + 3425, + 1971, + 3112, + 3952, + 4956, + 6553, + 3511, + 1654, + 3373, + 3121, + 1494, + 2498, + 3704, + 5113, + 4314, + 3225, + 3568, + 3018, + 2828, + 1691, + 3880, + 3152, + 3154, + 3768, + 3877, + 1657, + 2358, + 5386, + 3997, + 2851, + 2669, + 4624, + 3511, + 4683, + 2753, + 3474, + 1656, + 3365, + 4509, + 4644, + 4011, + 4593, + 2887, + 3058, + 4687, + 1444, + 3599, + 5240, + 2577, + 3589, + 4648, + 3156, + 3740, + 5972, + 3145, + 4368, + 1625, + 3905, + 4239, + 4559, + 4217, + 4224, + 4096, + 3149, + 2649, + 2220, + 4157, + 6479, + 3321, + 4368, + 2425, + 6001, + 3128, + 3652, + 2650, + 2662, + 3747, + 3987, + 5200, + 3045, + 3552, + 4811, + 3806, + 5716, + 3045, + 3874, + 4270, + 1689, + 4136, + 4095, + 4081, + 4842, + 3305, + 4053, + 4120, + 2737, + 5518, + 2507, + 4077, + 2458, + 5359, + 4500, + 3741, + 3006, + 2859, + 4136, + 2563, + 3323, + 4764, + 2897, + 2515, + 4381, + 2225, + 2646, + 3073, + 3015, + 2913, + 4932, + 3027, + 2570, + 3177, + 3852, + 3711, + 4211, + 3519, + 2751, + 3851, + 3951, + 3085, + 3174, + 4214, + 2877, + 2454, + 2799, + 5621, + 3585, + 2683, + 4963, + 5104, + 1661, + 3538, + 3983, + 2455, + 4989, + 3951, + 2135, + 2385, + 3936 + ], + "index (uncapped reads)": [ + 3714, + 7191, + 3425, + 1957, + 3112, + 3952, + 6502, + 6553, + 3511, + 1640, + 3359, + 3121, + 1494, + 2498, + 15367, + 6659, + 4314, + 3211, + 10468, + 6852, + 2828, + 1691, + 3880, + 3152, + 3140, + 10634, + 3877, + 1657, + 2358, + 23927, + 3997, + 2851, + 2669, + 16301, + 3511, + 16360, + 2753, + 15151, + 1642, + 3365, + 8211, + 4644, + 5557, + 15016, + 2887, + 3058, + 15913, + 13121, + 3599, + 17246, + 2906, + 3589, + 11382, + 3142, + 3740, + 5972, + 3145, + 4368, + 1625, + 4336, + 15916, + 11579, + 4217, + 4213, + 15522, + 3149, + 2649, + 2220, + 4588, + 6479, + 3321, + 4368, + 2425, + 6863, + 3128, + 5198, + 2650, + 2662, + 3747, + 11007, + 21218, + 3045, + 3552, + 4811, + 3806, + 5716, + 3045, + 4203, + 11049, + 1689, + 4136, + 4081, + 10987, + 5185, + 3305, + 4053, + 4120, + 2737, + 17195, + 2507, + 4378, + 2458, + 17039, + 4500, + 3741, + 3006, + 2859, + 4136, + 2563, + 3323, + 16413, + 2897, + 2515, + 4367, + 2225, + 2646, + 3058, + 14427, + 6172, + 6092, + 14690, + 2570, + 3177, + 3852, + 3711, + 4211, + 3519, + 2751, + 3851, + 3951, + 3085, + 3174, + 8555, + 2877, + 2454, + 2799, + 5621, + 5117, + 2683, + 18172, + 20853, + 1661, + 3538, + 4312, + 2455, + 4989, + 3951, + 2135, + 2385, + 8184 + ], + "rg+window": [ + 2612, + 2868, + 3743, + 3534, + 2994, + 2853, + 3041, + 3067, + 3414, + 772, + 3668, + 3266, + 2860, + 2674, + 3196, + 3587, + 3493, + 3238, + 3612, + 3604, + 1272, + 3001, + 3089, + 3388, + 3370, + 3099, + 2582, + 3109, + 3089, + 3461, + 3535, + 2777, + 3459, + 3234, + 3199, + 3367, + 3040, + 3460, + 0, + 3105, + 3262, + 3188, + 2813, + 3385, + 2534, + 3314, + 3140, + 3518, + 3163, + 3594, + 2994, + 3014, + 2798, + 3002, + 3199, + 2162, + 3442, + 3189, + 2626, + 3529, + 3341, + 3284, + 3398, + 3342, + 2924, + 1853, + 2650, + 2649, + 3255, + 3411, + 2825, + 2866, + 2046, + 3399, + 3407, + 3255, + 3407, + 3202, + 3207, + 3284, + 2742, + 3292, + 3438, + 2966, + 3117, + 2242, + 2334, + 2499, + 2937, + 3706, + 3024, + 3175, + 3160, + 2773, + 3014, + 3209, + 3472, + 3091, + 3036, + 2813, + 2925, + 3344, + 3598, + 3321, + 3249, + 2903, + 3573, + 3430, + 3648, + 2468, + 3143, + 3340, + 3486, + 3123, + 2236, + 3445, + 3160, + 3347, + 3641, + 3714, + 3137, + 1225, + 3345, + 3057, + 3495, + 3041, + 3186, + 2884, + 2767, + 2695, + 2777, + 3511, + 3307, + 3516, + 3485, + 3090, + 3480, + 3435, + 2658, + 3600, + 3256, + 2859, + 3584, + 3386, + 2609, + 3073, + 3160, + 3249, + 2879, + 3333 + ], + "repo-map 2k": [ + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998 + ], + "repo-map 2k query-aware": [ + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 2000, + 1998, + 1999, + 1999, + 1999, + 2000, + 1999, + 1999, + 1998, + 1998, + 2000, + 2000, + 1999, + 1999, + 1998, + 1999, + 1999, + 1998, + 2000, + 1998, + 1999, + 2000, + 2000, + 2000, + 1998, + 1998, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1998, + 1998, + 1999, + 1998, + 2000, + 1999, + 1998, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1998, + 2000, + 1999, + 1998, + 1999, + 1999, + 1999, + 1998, + 1999, + 1998, + 1999, + 1998, + 1999, + 2000, + 1998, + 1998, + 1998, + 1998, + 1999, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 1998, + 2000, + 1998, + 1998, + 2000, + 1998, + 1998, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 1998, + 1998, + 1999, + 2000, + 1999, + 1999, + 1998, + 2000, + 2000, + 2000, + 1999, + 1998, + 2000, + 1998, + 2000, + 1999, + 2000, + 2000, + 1998, + 1998, + 2000, + 1998, + 1999, + 2000, + 1999, + 1998, + 1998, + 2000, + 1998, + 2000, + 1999, + 1998, + 2000, + 1999, + 2000, + 1998, + 1999, + 1998, + 2000, + 1998, + 1998, + 1999, + 1999, + 1999, + 1998 + ], + "repo-map 8k": [ + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728 + ], + "repo-map 8k query-aware": [ + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728 + ] + }, + "packet_tokens_per_query": { + "index": [ + 2545, + 2189, + 2774, + 1505, + 2100, + 2468, + 2356, + 3003, + 2928, + 1434, + 2687, + 2602, + 1167, + 2155, + 2294, + 2611, + 2245, + 2680, + 2012, + 1349, + 2217, + 1474, + 2710, + 2648, + 2127, + 2134, + 2837, + 1508, + 2012, + 2790, + 2931, + 2225, + 2241, + 2455, + 2928, + 2436, + 2417, + 1952, + 1436, + 2537, + 2786, + 2553, + 2585, + 2140, + 2776, + 2866, + 2150, + 230, + 2564, + 2536, + 1343, + 2769, + 2410, + 2614, + 2632, + 3190, + 2603, + 2609, + 1418, + 2037, + 2429, + 2662, + 3019, + 2437, + 2268, + 2538, + 2554, + 2104, + 2409, + 3202, + 2718, + 2785, + 1814, + 2954, + 2658, + 2397, + 1970, + 2359, + 2769, + 2548, + 2357, + 2031, + 2473, + 2824, + 2182, + 2804, + 2412, + 2603, + 2647, + 1341, + 2651, + 2658, + 2298, + 2913, + 2668, + 2930, + 2920, + 1994, + 2935, + 2263, + 2617, + 2258, + 2556, + 2469, + 2740, + 2280, + 2116, + 2454, + 2199, + 2443, + 2512, + 1901, + 2174, + 2547, + 1804, + 2416, + 2420, + 1422, + 1381, + 2337, + 1708, + 1959, + 2369, + 2097, + 2473, + 2516, + 2288, + 2288, + 2604, + 3355, + 2473, + 2559, + 2427, + 2605, + 1878, + 2651, + 2700, + 2139, + 2472, + 2194, + 2473, + 1454, + 2256, + 1986, + 2047, + 2923, + 2604, + 1991, + 2193, + 2681 + ], + "index (uncapped reads)": [ + 2545, + 2161, + 2774, + 1491, + 2100, + 2468, + 2342, + 3003, + 2928, + 1420, + 2673, + 2602, + 1167, + 2155, + 2266, + 2569, + 2245, + 2666, + 1998, + 1335, + 2217, + 1474, + 2710, + 2648, + 2113, + 2120, + 2837, + 1508, + 2012, + 2748, + 2931, + 2225, + 2241, + 2441, + 2928, + 2422, + 2417, + 1938, + 1422, + 2537, + 2772, + 2553, + 2571, + 2111, + 2776, + 2866, + 2122, + 216, + 2564, + 2494, + 1315, + 2769, + 2396, + 2600, + 2632, + 3190, + 2603, + 2609, + 1418, + 2009, + 2415, + 2648, + 3019, + 2409, + 2254, + 2538, + 2554, + 2104, + 2381, + 3202, + 2718, + 2785, + 1814, + 2926, + 2658, + 2383, + 1970, + 2359, + 2769, + 2534, + 2329, + 2031, + 2473, + 2824, + 2182, + 2804, + 2412, + 2575, + 2633, + 1341, + 2651, + 2644, + 2284, + 2899, + 2668, + 2930, + 2920, + 1994, + 2921, + 2263, + 2561, + 2258, + 2528, + 2469, + 2740, + 2280, + 2116, + 2454, + 2199, + 2443, + 2470, + 1901, + 2174, + 2533, + 1804, + 2416, + 2405, + 1394, + 1353, + 2309, + 1680, + 1959, + 2369, + 2097, + 2473, + 2516, + 2288, + 2288, + 2604, + 3355, + 2473, + 2559, + 2413, + 2605, + 1878, + 2651, + 2700, + 2111, + 2472, + 2152, + 2431, + 1454, + 2256, + 1958, + 2047, + 2923, + 2604, + 1991, + 2193, + 2667 + ], + "rg+window": [ + 1041, + 1198, + 1141, + 1115, + 1176, + 1059, + 1080, + 977, + 1271, + 43, + 1133, + 1187, + 1088, + 875, + 994, + 1142, + 1216, + 1178, + 1089, + 1149, + 36, + 902, + 1026, + 1011, + 1157, + 1029, + 903, + 1166, + 1165, + 924, + 1131, + 983, + 1101, + 1092, + 1056, + 1094, + 1087, + 1037, + 0, + 1087, + 1129, + 1063, + 1147, + 1082, + 368, + 1051, + 1122, + 1147, + 1105, + 1111, + 1239, + 1094, + 1061, + 1102, + 1070, + 351, + 1115, + 1013, + 845, + 1110, + 1056, + 1076, + 924, + 995, + 1007, + 159, + 1038, + 884, + 1083, + 1151, + 1084, + 1170, + 123, + 1022, + 1030, + 1083, + 1030, + 1062, + 1099, + 1098, + 962, + 1113, + 1102, + 1165, + 1031, + 614, + 1013, + 1105, + 1215, + 1204, + 826, + 1073, + 973, + 853, + 877, + 771, + 1095, + 1080, + 1087, + 959, + 1208, + 1106, + 1262, + 1104, + 865, + 945, + 1129, + 1033, + 1155, + 914, + 1061, + 1126, + 1103, + 1123, + 556, + 1129, + 1030, + 1097, + 1153, + 1087, + 1046, + 44, + 1073, + 1019, + 1047, + 1126, + 1060, + 1052, + 1023, + 914, + 1026, + 1112, + 1058, + 1111, + 1113, + 1190, + 1103, + 1167, + 503, + 1154, + 1095, + 1078, + 1113, + 1255, + 367, + 1236, + 1094, + 1092, + 995, + 1120 + ], + "repo-map 2k": [ + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998, + 1998 + ], + "repo-map 2k query-aware": [ + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 2000, + 1998, + 1999, + 1999, + 1999, + 2000, + 1999, + 1999, + 1998, + 1998, + 2000, + 2000, + 1999, + 1999, + 1998, + 1999, + 1999, + 1998, + 2000, + 1998, + 1999, + 2000, + 2000, + 2000, + 1998, + 1998, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1998, + 1998, + 1999, + 1998, + 2000, + 1999, + 1998, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1998, + 2000, + 1999, + 1998, + 1999, + 1999, + 1999, + 1998, + 1999, + 1998, + 1999, + 1998, + 1999, + 2000, + 1998, + 1998, + 1998, + 1998, + 1999, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 1998, + 2000, + 1998, + 1998, + 2000, + 1998, + 1998, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 1998, + 1998, + 1999, + 2000, + 1999, + 1999, + 1998, + 2000, + 2000, + 2000, + 1999, + 1998, + 2000, + 1998, + 2000, + 1999, + 2000, + 2000, + 1998, + 1998, + 2000, + 1998, + 1999, + 2000, + 1999, + 1998, + 1998, + 2000, + 1998, + 2000, + 1999, + 1998, + 2000, + 1999, + 2000, + 1998, + 1999, + 1998, + 2000, + 1998, + 1998, + 1999, + 1999, + 1999, + 1998 + ], + "repo-map 8k": [ + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728 + ], + "repo-map 8k query-aware": [ + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728, + 7728 + ] + } + }, + { + "corpus": "gson", + "root": "C:\\Users\\dabin\\AppData\\Local\\Temp\\claude\\D--Projects-codebase-index\\7f35eb46-c8c9-49e7-9d02-4c510c6877d0\\scratchpad\\repos\\gson", + "head": "b3f4ca20087f9066de4c340522ff84e0558e1ad1", + "n_queries": 150, + "index_build_ms": 5279.1104999996605, + "files_indexed": 311, + "symbols_indexed": 4193, + "edges_indexed": 22407, + "summary": { + "index": { + "hit@3": 0.5733333333333334, + "recall@5": 0.5233333333333333, + "MRR": 0.5034285714285714, + "tokens_mean": 4375.306666666666, + "tokens_median": 4376.0, + "packet_tokens_mean": 2418.1266666666666, + "latency_p50_ms": 112.98190000161412 + }, + "index (uncapped reads)": { + "hit@3": 0.5733333333333334, + "recall@5": 0.5233333333333333, + "MRR": 0.5034285714285714, + "tokens_mean": 10659.12, + "tokens_median": 8680.5, + "packet_tokens_mean": 2382.193333333333, + "latency_p50_ms": 114.68090000198572 + }, + "rg+window": { + "hit@3": 0.3466666666666667, + "recall@5": 0.33722222222222226, + "MRR": 0.2898899828278739, + "tokens_mean": 4117.386666666666, + "tokens_median": 4091.5, + "packet_tokens_mean": 1864.6533333333334, + "latency_p50_ms": 34.462199997506104 + }, + "repo-map 2k": { + "present": 0.48, + "recall": 0.3644444444444444, + "tokens_mean": 2000.0, + "tokens_median": 2000.0, + "packet_tokens_mean": 2000.0, + "latency_p50_ms": 0.1880999989225529 + }, + "repo-map 2k query-aware": { + "present": 0.6133333333333333, + "recall": 0.5516666666666666, + "tokens_mean": 1999.7333333333333, + "tokens_median": 2000.0, + "packet_tokens_mean": 1999.7333333333333, + "latency_p50_ms": 2.779500002361601 + }, + "repo-map 8k": { + "present": 1.0, + "recall": 1.0, + "tokens_mean": 6757.0, + "tokens_median": 6757.0, + "packet_tokens_mean": 6757.0, + "latency_p50_ms": 0.12520000018412247 + }, + "repo-map 8k query-aware": { + "present": 1.0, + "recall": 1.0, + "tokens_mean": 6757.0, + "tokens_median": 6757.0, + "packet_tokens_mean": 6757.0, + "latency_p50_ms": 2.672400001756614 + } + }, + "per_query": { + "index": { + "hit@3": [ + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0 + ], + "recall@5": [ + 0.25, + 0.5, + 0.25, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.6666666666666666, + 1.0, + 0.5, + 0.5, + 0.0, + 1.0, + 0.6666666666666666, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.5, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.3333333333333333, + 1.0, + 0.5, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 0.5, + 0.6666666666666666, + 0.0, + 1.0, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.3333333333333333, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 1.0, + 1.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.25, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.25, + 1.0, + 1.0, + 1.0, + 0.5, + 0.6666666666666666, + 0.6666666666666666, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5 + ], + "MRR": [ + 0.5, + 1.0, + 0.2, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.14285714285714285, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.25, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.25, + 1.0, + 1.0, + 0.3333333333333333, + 0.5, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.125, + 0.25, + 0.14285714285714285, + 0.25, + 1.0, + 1.0, + 0.0, + 0.2, + 1.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.14285714285714285, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.16666666666666666, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.5, + 0.3333333333333333, + 0.5, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.5, + 1.0, + 1.0, + 1.0, + 0.25, + 0.2, + 0.125, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.25, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.3333333333333333, + 0.3333333333333333, + 0.3333333333333333, + 0.5, + 0.2, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.25, + 0.0, + 1.0, + 0.16666666666666666, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.25, + 1.0, + 0.25, + 1.0, + 0.5, + 1.0, + 1.0, + 0.14285714285714285, + 0.14285714285714285, + 1.0, + 0.0, + 0.0, + 1.0 + ] + }, + "index (uncapped reads)": { + "hit@3": [ + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0 + ], + "recall@5": [ + 0.25, + 0.5, + 0.25, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.6666666666666666, + 1.0, + 0.5, + 0.5, + 0.0, + 1.0, + 0.6666666666666666, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.5, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.3333333333333333, + 1.0, + 0.5, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 0.5, + 0.6666666666666666, + 0.0, + 1.0, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.3333333333333333, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 1.0, + 1.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.25, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.25, + 1.0, + 1.0, + 1.0, + 0.5, + 0.6666666666666666, + 0.6666666666666666, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5 + ], + "MRR": [ + 0.5, + 1.0, + 0.2, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.14285714285714285, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.25, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.25, + 1.0, + 1.0, + 0.3333333333333333, + 0.5, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.125, + 0.25, + 0.14285714285714285, + 0.25, + 1.0, + 1.0, + 0.0, + 0.2, + 1.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.14285714285714285, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.16666666666666666, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.5, + 0.3333333333333333, + 0.5, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.5, + 1.0, + 1.0, + 1.0, + 0.25, + 0.2, + 0.125, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.25, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.3333333333333333, + 0.3333333333333333, + 0.3333333333333333, + 0.5, + 0.2, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.25, + 0.0, + 1.0, + 0.16666666666666666, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.25, + 1.0, + 0.25, + 1.0, + 0.5, + 1.0, + 1.0, + 0.14285714285714285, + 0.14285714285714285, + 1.0, + 0.0, + 0.0, + 1.0 + ] + }, + "rg+window": { + "hit@3": [ + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0 + ], + "recall@5": [ + 0.5, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.5, + 0.5, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 0.25, + 0.0, + 1.0, + 0.5, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.5, + 1.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.5, + 1.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.25, + 1.0, + 0.0, + 0.0, + 0.25 + ], + "MRR": [ + 1.0, + 0.5, + 0.0, + 0.2, + 0.0, + 0.09090909090909091, + 0.3333333333333333, + 0.00819672131147541, + 0.0625, + 0.16666666666666666, + 0.3333333333333333, + 0.5, + 1.0, + 0.2, + 1.0, + 0.047619047619047616, + 0.0, + 0.2, + 1.0, + 0.0625, + 0.5, + 0.5, + 0.14285714285714285, + 0.3333333333333333, + 0.25, + 0.025, + 0.14285714285714285, + 1.0, + 0.1111111111111111, + 0.5, + 0.09090909090909091, + 0.25, + 1.0, + 0.5, + 0.5, + 0.0, + 0.07692307692307693, + 1.0, + 0.037037037037037035, + 0.02702702702702703, + 0.25, + 0.25, + 0.038461538461538464, + 0.3333333333333333, + 0.3333333333333333, + 0.0, + 0.1111111111111111, + 0.14285714285714285, + 0.0, + 0.0, + 0.25, + 0.08333333333333333, + 0.25, + 0.022222222222222223, + 0.09090909090909091, + 0.14285714285714285, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.017857142857142856, + 0.0, + 0.015151515151515152, + 1.0, + 0.0, + 0.14285714285714285, + 0.25, + 0.02631578947368421, + 1.0, + 0.08333333333333333, + 0.07142857142857142, + 0.125, + 1.0, + 1.0, + 0.0625, + 0.015625, + 0.14285714285714285, + 0.007352941176470588, + 0.0, + 0.009900990099009901, + 0.008064516129032258, + 1.0, + 0.041666666666666664, + 0.006944444444444444, + 0.009615384615384616, + 1.0, + 0.04, + 1.0, + 1.0, + 0.0625, + 0.3333333333333333, + 0.009009009009009009, + 0.037037037037037035, + 0.04, + 0.0, + 0.02631578947368421, + 0.07142857142857142, + 1.0, + 0.1, + 0.07142857142857142, + 0.07142857142857142, + 0.25, + 0.3333333333333333, + 0.125, + 0.1111111111111111, + 0.05263157894736842, + 0.02631578947368421, + 0.0625, + 0.05555555555555555, + 1.0, + 0.5, + 0.5, + 0.5, + 0.5, + 0.2, + 0.3333333333333333, + 0.058823529411764705, + 1.0, + 0.09090909090909091, + 0.08333333333333333, + 0.5, + 0.5, + 0.3333333333333333, + 0.25, + 0.021739130434782608, + 0.2, + 0.3333333333333333, + 0.0, + 0.25, + 0.06666666666666667, + 0.08333333333333333, + 1.0, + 0.09090909090909091, + 0.5, + 0.0, + 0.1111111111111111, + 0.1, + 0.3333333333333333, + 0.008620689655172414, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.25, + 0.5, + 0.045454545454545456, + 0.0, + 0.5 + ] + }, + "repo-map 2k": { + "present": [ + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0 + ], + "recall": [ + 0.75, + 1.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5, + 0.5, + 0.5, + 0.6666666666666666, + 0.5, + 1.0, + 0.5, + 1.0, + 0.0, + 0.6666666666666666, + 0.75, + 0.0, + 0.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.6666666666666666, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.5, + 1.0, + 1.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.6666666666666666, + 0.0, + 0.0, + 0.3333333333333333, + 0.6666666666666666, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.5, + 0.5, + 0.5, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.25, + 0.3333333333333333, + 0.0, + 0.0, + 0.5, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.0, + 0.25, + 0.0, + 0.0, + 0.0, + 0.5, + 0.6666666666666666, + 1.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.5 + ] + }, + "repo-map 2k query-aware": { + "present": [ + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "recall": [ + 0.75, + 1.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.6666666666666666, + 0.75, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.0, + 1.0, + 1.0, + 0.5, + 0.5, + 1.0, + 1.0, + 0.6666666666666666, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.6666666666666666, + 0.0, + 0.0, + 1.0, + 0.6666666666666666, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.3333333333333333, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.75, + 1.0, + 1.0, + 1.0, + 0.5, + 0.0, + 0.5, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.25, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.75, + 1.0, + 1.0, + 1.0, + 1.0 + ] + }, + "repo-map 8k": { + "present": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "recall": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + }, + "repo-map 8k query-aware": { + "present": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "recall": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ] + } + }, + "tokens_per_query": { + "index": [ + 5059, + 6027, + 5470, + 5315, + 2845, + 4899, + 4064, + 3211, + 3556, + 3229, + 5950, + 4194, + 4752, + 5396, + 5761, + 5036, + 2164, + 4169, + 5272, + 2082, + 4745, + 4663, + 3110, + 2763, + 6487, + 4221, + 4616, + 4613, + 5390, + 5055, + 5095, + 4401, + 3568, + 4796, + 4955, + 3266, + 4726, + 4372, + 3179, + 5473, + 4149, + 4738, + 4650, + 6351, + 6625, + 4801, + 2803, + 4290, + 1766, + 4009, + 4654, + 4633, + 3589, + 4466, + 4924, + 4184, + 4352, + 4797, + 3755, + 2343, + 2540, + 4948, + 5321, + 2980, + 4310, + 4904, + 4930, + 3857, + 5260, + 4380, + 6344, + 5862, + 4099, + 6937, + 3969, + 4519, + 3972, + 5546, + 3074, + 5727, + 2940, + 4454, + 3396, + 4332, + 4814, + 3823, + 4463, + 5026, + 2696, + 5898, + 3750, + 5432, + 3815, + 4639, + 5007, + 5306, + 5482, + 2818, + 3864, + 3989, + 2649, + 5385, + 4119, + 4947, + 3331, + 3980, + 3371, + 3941, + 4323, + 5761, + 4847, + 3617, + 3100, + 6650, + 4958, + 5801, + 6244, + 4111, + 6039, + 5785, + 3828, + 4676, + 4939, + 3517, + 3676, + 5798, + 4984, + 3101, + 3756, + 4784, + 5186, + 4224, + 5030, + 2607, + 3577, + 2391, + 3946, + 3617, + 2314, + 4002, + 3491, + 3765, + 4161, + 4120, + 3085, + 3992, + 3763, + 2702, + 5863, + 5195 + ], + "index (uncapped reads)": [ + 22367, + 22931, + 5693, + 19077, + 2845, + 17752, + 4064, + 3211, + 9842, + 9487, + 22034, + 7241, + 8158, + 11626, + 16251, + 7752, + 2164, + 5046, + 5850, + 14935, + 4731, + 9332, + 23396, + 2763, + 30997, + 15156, + 8130, + 19436, + 18229, + 10227, + 13026, + 4401, + 17932, + 8878, + 13901, + 6720, + 10341, + 7689, + 3179, + 20130, + 12746, + 15659, + 15096, + 26637, + 22457, + 37777, + 3605, + 12940, + 1766, + 4009, + 4654, + 4619, + 16332, + 21370, + 13529, + 4184, + 17191, + 4783, + 3755, + 2343, + 2540, + 4934, + 13943, + 15833, + 18670, + 6488, + 4888, + 14812, + 7330, + 12702, + 32247, + 12832, + 4057, + 6909, + 20859, + 10777, + 3958, + 7993, + 3060, + 24230, + 9324, + 10253, + 3396, + 4318, + 6154, + 3809, + 4449, + 21421, + 2696, + 15474, + 6791, + 11920, + 11105, + 4639, + 5007, + 8483, + 9401, + 2825, + 7323, + 6031, + 4232, + 9583, + 12769, + 19642, + 7665, + 3952, + 4159, + 10665, + 19096, + 6535, + 26262, + 3603, + 3086, + 19647, + 15165, + 27304, + 7829, + 4083, + 10090, + 6448, + 3828, + 37801, + 9956, + 12167, + 12378, + 11110, + 13606, + 3101, + 3742, + 5161, + 15056, + 7602, + 11053, + 2607, + 3577, + 2377, + 13749, + 12237, + 2300, + 12621, + 6931, + 6142, + 12696, + 4120, + 3533, + 6028, + 6652, + 2660, + 18509, + 17500 + ], + "rg+window": [ + 4994, + 4821, + 4521, + 4553, + 4075, + 4559, + 4299, + 3396, + 4603, + 4064, + 4670, + 3943, + 3745, + 3587, + 3495, + 3364, + 4877, + 3833, + 3776, + 5068, + 3658, + 4628, + 5341, + 3372, + 4463, + 4167, + 4302, + 4442, + 4316, + 4210, + 3342, + 3790, + 4054, + 3897, + 3407, + 4114, + 4384, + 2136, + 4460, + 4118, + 4081, + 3204, + 4573, + 4961, + 3333, + 3567, + 3951, + 4093, + 3098, + 4640, + 3501, + 4694, + 3366, + 4279, + 4502, + 4069, + 4534, + 3653, + 4043, + 3147, + 3600, + 4436, + 4191, + 3683, + 4477, + 3831, + 4165, + 3646, + 4801, + 4165, + 3515, + 3359, + 4702, + 4540, + 3943, + 5681, + 3429, + 6258, + 4677, + 4483, + 4309, + 4153, + 3308, + 3841, + 3588, + 4194, + 4196, + 4068, + 3589, + 4287, + 3743, + 4310, + 3846, + 4691, + 3511, + 4500, + 3938, + 3411, + 3774, + 3700, + 4071, + 4287, + 3871, + 4124, + 5321, + 4071, + 4070, + 4492, + 4358, + 4342, + 3884, + 3508, + 4035, + 4735, + 3917, + 3374, + 5515, + 4699, + 4558, + 3683, + 4090, + 4685, + 3960, + 3701, + 4313, + 4295, + 3707, + 4420, + 4910, + 4141, + 3909, + 3894, + 4170, + 3870, + 4153, + 2889, + 4696, + 5305, + 3825, + 3807, + 6496, + 4648, + 3850, + 2969, + 4059, + 3713, + 4233, + 4331, + 2838, + 4144 + ], + "repo-map 2k": [ + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000 + ], + "repo-map 2k query-aware": [ + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999 + ], + "repo-map 8k": [ + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757 + ], + "repo-map 8k query-aware": [ + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757 + ] + }, + "packet_tokens_per_query": { + "index": [ + 2657, + 2973, + 2537, + 2120, + 2012, + 2719, + 2557, + 2512, + 1539, + 2353, + 2879, + 2088, + 2699, + 2888, + 2526, + 2833, + 2078, + 2005, + 2803, + 587, + 2564, + 2525, + 1309, + 2677, + 2933, + 2707, + 2780, + 2536, + 2398, + 2706, + 2948, + 2545, + 1522, + 2839, + 3159, + 2364, + 2619, + 2482, + 2564, + 2519, + 2421, + 1551, + 2352, + 2395, + 2934, + 2577, + 1254, + 2165, + 1696, + 2667, + 2585, + 2546, + 2358, + 2240, + 2981, + 2569, + 2595, + 2597, + 2784, + 1376, + 2454, + 2949, + 2525, + 1742, + 2260, + 2199, + 2183, + 2035, + 2734, + 2836, + 2684, + 2791, + 2640, + 2540, + 1677, + 2390, + 3033, + 2674, + 2885, + 2648, + 1442, + 2389, + 2688, + 2449, + 2953, + 2357, + 2627, + 2861, + 1915, + 2960, + 2048, + 2273, + 877, + 2544, + 2730, + 2967, + 2816, + 1633, + 2695, + 2489, + 1170, + 2376, + 2755, + 2840, + 1920, + 2446, + 2265, + 1968, + 2222, + 2584, + 2422, + 2323, + 2746, + 2781, + 2778, + 2876, + 2296, + 2652, + 2612, + 2940, + 2390, + 2567, + 2522, + 2211, + 1435, + 2469, + 2304, + 2268, + 2780, + 2457, + 2843, + 2171, + 2914, + 1861, + 2789, + 2202, + 2166, + 2290, + 2227, + 2709, + 2569, + 1773, + 2791, + 2527, + 2283, + 2363, + 2017, + 2616, + 2672, + 2265 + ], + "index (uncapped reads)": [ + 2587, + 2917, + 2509, + 2050, + 2012, + 2705, + 2557, + 2512, + 1525, + 2311, + 2823, + 2032, + 2629, + 2818, + 2442, + 2805, + 2078, + 1949, + 2747, + 573, + 2550, + 2483, + 1295, + 2677, + 2891, + 2679, + 2738, + 2480, + 2370, + 2664, + 2906, + 2545, + 1466, + 2755, + 3075, + 2322, + 2577, + 2412, + 2564, + 2449, + 2393, + 1509, + 2296, + 2381, + 2878, + 2535, + 1240, + 2151, + 1696, + 2667, + 2585, + 2532, + 2330, + 2184, + 2953, + 2569, + 2567, + 2583, + 2784, + 1376, + 2454, + 2935, + 2483, + 1728, + 2204, + 2129, + 2141, + 2007, + 2706, + 2794, + 2600, + 2721, + 2598, + 2512, + 1607, + 2348, + 3019, + 2618, + 2871, + 2592, + 1428, + 2361, + 2688, + 2435, + 2939, + 2343, + 2613, + 2819, + 1915, + 2904, + 1978, + 2231, + 821, + 2544, + 2730, + 2925, + 2718, + 1577, + 2639, + 2433, + 1156, + 2320, + 2741, + 2784, + 1906, + 2418, + 2237, + 1912, + 2166, + 2542, + 2352, + 2309, + 2732, + 2697, + 2708, + 2848, + 2282, + 2624, + 2570, + 2884, + 2390, + 2525, + 2480, + 2197, + 1407, + 2371, + 2262, + 2268, + 2766, + 2443, + 2773, + 2073, + 2858, + 1861, + 2789, + 2188, + 2082, + 2276, + 2213, + 2695, + 2513, + 1689, + 2735, + 2527, + 2269, + 2321, + 1989, + 2574, + 2616, + 2195 + ], + "rg+window": [ + 1959, + 2230, + 1527, + 2040, + 2362, + 1660, + 1616, + 1296, + 2294, + 2040, + 2407, + 1742, + 1692, + 1489, + 1643, + 1473, + 2309, + 1865, + 1579, + 2414, + 1358, + 2216, + 3044, + 1351, + 2369, + 2058, + 2527, + 2066, + 2225, + 1896, + 1534, + 1141, + 1919, + 1880, + 1544, + 2058, + 1508, + 279, + 2577, + 1988, + 1958, + 1190, + 2245, + 2477, + 1286, + 1984, + 1896, + 2058, + 784, + 1617, + 1795, + 2130, + 1477, + 1613, + 2197, + 1097, + 1565, + 1654, + 2333, + 1111, + 1740, + 1527, + 1972, + 1360, + 2168, + 1935, + 2058, + 1714, + 2234, + 2058, + 1533, + 1267, + 1512, + 1763, + 1866, + 2594, + 1723, + 2875, + 1736, + 1946, + 2190, + 1855, + 1109, + 1771, + 1478, + 1078, + 1900, + 1904, + 1420, + 1881, + 1765, + 2203, + 1824, + 1545, + 1730, + 2748, + 1916, + 1446, + 1718, + 1763, + 2150, + 1862, + 1631, + 1763, + 3202, + 2116, + 2213, + 2556, + 1878, + 2244, + 1725, + 1455, + 1998, + 2787, + 1951, + 1085, + 3112, + 2038, + 2193, + 1841, + 1688, + 2129, + 1531, + 1784, + 2215, + 2237, + 1735, + 1640, + 1817, + 2075, + 1790, + 1756, + 1855, + 1701, + 2046, + 1055, + 2604, + 2688, + 1589, + 1730, + 3121, + 2490, + 1743, + 797, + 2062, + 1585, + 2200, + 1851, + 221, + 1696 + ], + "repo-map 2k": [ + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000 + ], + "repo-map 2k query-aware": [ + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999 + ], + "repo-map 8k": [ + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757 + ], + "repo-map 8k query-aware": [ + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757, + 6757 + ] + } + }, + { + "corpus": "fastify", + "root": "C:\\Users\\dabin\\AppData\\Local\\Temp\\claude\\D--Projects-codebase-index\\7f35eb46-c8c9-49e7-9d02-4c510c6877d0\\scratchpad\\repos\\fastify", + "head": "15ebc8e2fe6932d3e91afa84709523804f8a2537", + "n_queries": 150, + "index_build_ms": 1983.2537000002048, + "files_indexed": 389, + "symbols_indexed": 1185, + "edges_indexed": 34297, + "summary": { + "index": { + "hit@3": 0.58, + "recall@5": 0.5472222222222222, + "MRR": 0.4817698412698413, + "tokens_mean": 3539.4733333333334, + "tokens_median": 3561.5, + "packet_tokens_mean": 2427.64, + "latency_p50_ms": 45.60020000280929 + }, + "index (uncapped reads)": { + "hit@3": 0.58, + "recall@5": 0.5472222222222222, + "MRR": 0.4817698412698413, + "tokens_mean": 4026.7066666666665, + "tokens_median": 3611.0, + "packet_tokens_mean": 2423.0666666666666, + "latency_p50_ms": 45.35319999922649 + }, + "rg+window": { + "hit@3": 0.3333333333333333, + "recall@5": 0.2972222222222222, + "MRR": 0.26914749481448863, + "tokens_mean": 3290.1866666666665, + "tokens_median": 3312.5, + "packet_tokens_mean": 1178.4266666666667, + "latency_p50_ms": 38.79890000098385 + }, + "repo-map 2k": { + "present": 0.36, + "recall": 0.20277777777777778, + "tokens_mean": 2000.0, + "tokens_median": 2000.0, + "packet_tokens_mean": 2000.0, + "latency_p50_ms": 0.1922999981616158 + }, + "repo-map 2k query-aware": { + "present": 0.36666666666666664, + "recall": 0.22333333333333333, + "tokens_mean": 1999.7266666666667, + "tokens_median": 2000.0, + "packet_tokens_mean": 1999.7266666666667, + "latency_p50_ms": 0.8546999997633975 + }, + "repo-map 8k": { + "present": 0.9733333333333334, + "recall": 0.9033333333333333, + "tokens_mean": 8000.0, + "tokens_median": 8000.0, + "packet_tokens_mean": 8000.0, + "latency_p50_ms": 0.1354000014543999 + }, + "repo-map 8k query-aware": { + "present": 0.8933333333333333, + "recall": 0.8188888888888889, + "tokens_mean": 7999.14, + "tokens_median": 8000.0, + "packet_tokens_mean": 7999.14, + "latency_p50_ms": 0.8130999995046295 + } + }, + "per_query": { + "index": { + "hit@3": [ + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0 + ], + "recall@5": [ + 1.0, + 0.5, + 0.6666666666666666, + 0.0, + 0.75, + 0.5, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.25, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 1.0, + 0.5, + 0.5, + 0.5, + 0.0, + 0.5, + 0.6666666666666666, + 0.5, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.5, + 0.0, + 1.0, + 0.6666666666666666, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.5, + 1.0, + 1.0, + 0.25, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.5, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.25, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.25, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.5 + ], + "MRR": [ + 1.0, + 0.3333333333333333, + 0.5, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.3333333333333333, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.2, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.25, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.16666666666666666, + 0.0, + 0.2, + 0.0, + 1.0, + 0.25, + 0.5, + 1.0, + 0.0, + 0.5, + 1.0, + 0.25, + 0.0, + 0.5, + 0.25, + 0.5, + 1.0, + 0.3333333333333333, + 0.5, + 0.0, + 0.0, + 1.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.5, + 0.2, + 0.14285714285714285, + 0.16666666666666666, + 0.5, + 1.0, + 0.0, + 0.14285714285714285, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.125, + 0.5, + 1.0, + 0.25, + 0.25, + 0.0, + 0.0, + 0.16666666666666666, + 0.0, + 0.3333333333333333, + 0.2, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 0.0, + 0.5, + 1.0, + 0.3333333333333333, + 0.2, + 1.0, + 1.0, + 1.0, + 0.5, + 0.14285714285714285, + 0.16666666666666666, + 0.5, + 0.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5, + 0.14285714285714285, + 0.3333333333333333, + 1.0, + 1.0, + 0.5, + 0.14285714285714285, + 0.25, + 0.0, + 0.14285714285714285, + 0.5, + 1.0, + 1.0, + 1.0, + 0.2, + 1.0 + ] + }, + "index (uncapped reads)": { + "hit@3": [ + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0 + ], + "recall@5": [ + 1.0, + 0.5, + 0.6666666666666666, + 0.0, + 0.75, + 0.5, + 0.0, + 0.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.25, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.0, + 1.0, + 0.5, + 0.5, + 0.5, + 0.0, + 0.5, + 0.6666666666666666, + 0.5, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.5, + 0.0, + 1.0, + 0.6666666666666666, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.5, + 1.0, + 1.0, + 0.25, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.5, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.25, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.25, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.5 + ], + "MRR": [ + 1.0, + 0.3333333333333333, + 0.5, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.3333333333333333, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.2, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.25, + 1.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.16666666666666666, + 0.0, + 0.2, + 0.0, + 1.0, + 0.25, + 0.5, + 1.0, + 0.0, + 0.5, + 1.0, + 0.25, + 0.0, + 0.5, + 0.25, + 0.5, + 1.0, + 0.3333333333333333, + 0.5, + 0.0, + 0.0, + 1.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.5, + 0.2, + 0.14285714285714285, + 0.16666666666666666, + 0.5, + 1.0, + 0.0, + 0.14285714285714285, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.125, + 0.5, + 1.0, + 0.25, + 0.25, + 0.0, + 0.0, + 0.16666666666666666, + 0.0, + 0.3333333333333333, + 0.2, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 0.0, + 0.5, + 1.0, + 0.3333333333333333, + 0.2, + 1.0, + 1.0, + 1.0, + 0.5, + 0.14285714285714285, + 0.16666666666666666, + 0.5, + 0.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5, + 0.14285714285714285, + 0.3333333333333333, + 1.0, + 1.0, + 0.5, + 0.14285714285714285, + 0.25, + 0.0, + 0.14285714285714285, + 0.5, + 1.0, + 1.0, + 1.0, + 0.2, + 1.0 + ] + }, + "rg+window": { + "hit@3": [ + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0 + ], + "recall@5": [ + 0.0, + 0.5, + 0.3333333333333333, + 0.0, + 0.0, + 0.25, + 0.0, + 1.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.6666666666666666, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.5, + 1.0, + 1.0, + 0.0, + 1.0, + 0.5, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.5, + 0.6666666666666666, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.25, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 1.0, + 0.25, + 1.0, + 1.0, + 0.3333333333333333, + 0.0, + 0.5 + ], + "MRR": [ + 0.125, + 0.5, + 0.3333333333333333, + 0.14285714285714285, + 0.14285714285714285, + 0.3333333333333333, + 0.023255813953488372, + 0.5, + 1.0, + 0.0058823529411764705, + 0.5, + 0.125, + 0.07692307692307693, + 0.3333333333333333, + 0.09090909090909091, + 0.058823529411764705, + 0.0, + 0.5, + 0.125, + 0.03333333333333333, + 0.5, + 0.5, + 0.125, + 0.02702702702702703, + 0.011904761904761904, + 0.125, + 0.1, + 1.0, + 1.0, + 0.07142857142857142, + 0.07142857142857142, + 0.3333333333333333, + 1.0, + 0.058823529411764705, + 0.2, + 0.034482758620689655, + 0.008547008547008548, + 0.0, + 0.1, + 0.14285714285714285, + 1.0, + 1.0, + 0.034482758620689655, + 0.07692307692307693, + 0.25, + 0.1, + 0.2, + 0.1, + 0.3333333333333333, + 0.0, + 1.0, + 0.14285714285714285, + 0.5, + 0.16666666666666666, + 0.5, + 0.0, + 0.1111111111111111, + 0.045454545454545456, + 0.1111111111111111, + 0.3333333333333333, + 0.03225806451612903, + 0.14285714285714285, + 0.16666666666666666, + 0.125, + 0.5, + 0.058823529411764705, + 1.0, + 0.024390243902439025, + 0.009615384615384616, + 0.25, + 0.2, + 0.14285714285714285, + 0.2, + 0.02631578947368421, + 0.07692307692307693, + 0.02564102564102564, + 0.04, + 0.045454545454545456, + 0.01694915254237288, + 1.0, + 0.5, + 0.5, + 0.3333333333333333, + 0.0, + 0.3333333333333333, + 0.3333333333333333, + 0.16666666666666666, + 0.05263157894736842, + 0.5, + 0.047619047619047616, + 0.2, + 0.03571428571428571, + 0.011363636363636364, + 0.14285714285714285, + 0.038461538461538464, + 0.05, + 0.5, + 0.0, + 0.058823529411764705, + 0.0, + 1.0, + 0.02857142857142857, + 0.16666666666666666, + 0.16666666666666666, + 0.5, + 0.5, + 1.0, + 0.017241379310344827, + 0.0, + 0.09090909090909091, + 0.5, + 0.03125, + 0.016129032258064516, + 0.14285714285714285, + 0.058823529411764705, + 0.0, + 1.0, + 0.5, + 0.0, + 0.023809523809523808, + 0.043478260869565216, + 0.014925373134328358, + 0.1111111111111111, + 0.0, + 0.01282051282051282, + 0.07692307692307693, + 0.08333333333333333, + 0.2, + 0.047619047619047616, + 0.0, + 0.08333333333333333, + 1.0, + 0.03225806451612903, + 0.034482758620689655, + 0.029411764705882353, + 0.5, + 0.3333333333333333, + 0.2, + 1.0, + 0.5, + 0.2, + 0.5, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.3333333333333333, + 0.04, + 1.0 + ] + }, + "repo-map 2k": { + "present": [ + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0 + ], + "recall": [ + 0.5, + 0.0, + 0.3333333333333333, + 0.0, + 0.75, + 0.25, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.5, + 0.5, + 0.5, + 0.0, + 0.5, + 0.0, + 1.0, + 0.5, + 0.5, + 0.5, + 1.0, + 0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.5, + 0.0, + 0.5, + 0.3333333333333333, + 1.0, + 0.5, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.5, + 0.5, + 0.0, + 0.5, + 0.6666666666666666, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.75, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.25, + 1.0, + 1.0, + 0.6666666666666666, + 0.5, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.5, + 0.6666666666666666, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.5, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.25, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5 + ] + }, + "repo-map 2k query-aware": { + "present": [ + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 1.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 1.0, + 1.0 + ], + "recall": [ + 0.5, + 0.0, + 0.3333333333333333, + 0.0, + 0.25, + 0.25, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 0.5, + 0.5, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 0.5, + 0.5, + 0.25, + 0.5, + 0.5, + 0.5, + 1.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.5, + 0.0, + 0.0, + 0.3333333333333333, + 1.0, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.0, + 0.5, + 0.3333333333333333, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.75, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.0, + 0.0, + 0.0, + 0.3333333333333333, + 0.0, + 0.5, + 0.25, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 0.6666666666666666, + 1.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.0, + 0.6666666666666666, + 0.5, + 0.0, + 0.0, + 0.0, + 0.0, + 0.5, + 0.0, + 0.0, + 0.5, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 1.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.25, + 0.0, + 0.0, + 0.0, + 1.0, + 0.5 + ] + }, + "repo-map 8k": { + "present": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0 + ], + "recall": [ + 1.0, + 0.5, + 1.0, + 0.6666666666666666, + 1.0, + 0.75, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.75, + 1.0, + 1.0, + 0.5, + 0.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.6666666666666666, + 1.0, + 0.5, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.5, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.6666666666666666, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.5, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.5 + ] + }, + "repo-map 8k query-aware": { + "present": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "recall": [ + 1.0, + 0.5, + 1.0, + 0.6666666666666666, + 1.0, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.75, + 1.0, + 1.0, + 0.5, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.6666666666666666, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 0.5, + 1.0, + 0.5, + 0.5, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 0.0, + 0.6666666666666666, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 0.0, + 0.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.3333333333333333, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 0.75, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 0.5, + 1.0, + 0.5, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 0.5, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 0.5 + ] + } + }, + "tokens_per_query": { + "index": [ + 4752, + 3963, + 3235, + 2620, + 3410, + 4526, + 4066, + 3907, + 2873, + 3324, + 4146, + 4758, + 4769, + 4478, + 3009, + 4731, + 1811, + 2891, + 4752, + 4904, + 3965, + 3181, + 4752, + 3609, + 2906, + 1098, + 3373, + 1602, + 3397, + 4635, + 890, + 1423, + 3947, + 4220, + 4103, + 2308, + 4554, + 5202, + 3567, + 3671, + 3216, + 3008, + 2991, + 3934, + 4115, + 2646, + 3519, + 3442, + 4910, + 3681, + 2581, + 2726, + 2578, + 3396, + 4143, + 2125, + 3637, + 2685, + 2319, + 2880, + 4193, + 3821, + 4096, + 5243, + 3689, + 2729, + 3276, + 4323, + 3937, + 3903, + 4672, + 4415, + 3665, + 4613, + 5390, + 3233, + 3613, + 2700, + 3297, + 3061, + 3760, + 3512, + 4516, + 2671, + 1201, + 2400, + 3514, + 5102, + 2825, + 2402, + 3478, + 2798, + 2704, + 3277, + 3938, + 4324, + 3071, + 3932, + 4020, + 369, + 3728, + 3365, + 4696, + 4300, + 4056, + 2834, + 4978, + 2685, + 3196, + 3459, + 1400, + 2734, + 4304, + 3329, + 3556, + 3718, + 2799, + 3449, + 2781, + 4346, + 5105, + 3206, + 3943, + 4433, + 4555, + 4269, + 4965, + 2331, + 3469, + 3494, + 2835, + 3503, + 4064, + 4302, + 3415, + 4383, + 1974, + 7514, + 4803, + 5023, + 2962, + 4249, + 2529, + 3951, + 3902, + 1576, + 620, + 2089, + 4546, + 3655 + ], + "index (uncapped reads)": [ + 4752, + 3963, + 3235, + 2620, + 3396, + 4515, + 4066, + 3907, + 2873, + 3324, + 4146, + 4758, + 4769, + 6536, + 3009, + 10609, + 1811, + 2891, + 4752, + 4904, + 3965, + 3181, + 4752, + 3609, + 2906, + 1098, + 3345, + 1588, + 3397, + 4621, + 890, + 1423, + 3947, + 4220, + 9981, + 2308, + 11715, + 5202, + 3567, + 4739, + 3216, + 3008, + 2991, + 3900, + 4231, + 2646, + 3519, + 3442, + 4896, + 3675, + 2581, + 2726, + 2578, + 3396, + 4143, + 2125, + 3637, + 2685, + 2319, + 2880, + 4485, + 3821, + 4096, + 5237, + 8541, + 2715, + 6407, + 9161, + 8789, + 3903, + 4672, + 4409, + 3665, + 4613, + 13387, + 3233, + 3613, + 2700, + 3297, + 3061, + 3760, + 3512, + 4516, + 2671, + 1201, + 2400, + 3514, + 5102, + 2811, + 2402, + 3478, + 2798, + 2704, + 3277, + 3938, + 9176, + 3071, + 3932, + 4020, + 369, + 3728, + 3365, + 5789, + 7431, + 4172, + 2834, + 4978, + 2685, + 3196, + 3459, + 1400, + 2734, + 4290, + 6474, + 3556, + 3718, + 2799, + 3449, + 2781, + 4346, + 5105, + 3206, + 3943, + 4433, + 4541, + 4269, + 4965, + 2331, + 3469, + 3494, + 2835, + 3503, + 8916, + 4288, + 3415, + 9235, + 1974, + 7514, + 4803, + 5023, + 2962, + 4249, + 2529, + 3951, + 3902, + 1576, + 620, + 2089, + 6604, + 4737 + ], + "rg+window": [ + 2855, + 3604, + 2856, + 3407, + 3366, + 2340, + 3020, + 3380, + 3074, + 3089, + 2942, + 2855, + 2471, + 3606, + 4411, + 3212, + 1729, + 3242, + 2855, + 3351, + 2820, + 3733, + 2855, + 3450, + 3499, + 2276, + 3905, + 350, + 3076, + 3802, + 2590, + 3131, + 3810, + 2456, + 3351, + 3894, + 2935, + 1164, + 3491, + 3602, + 3113, + 2951, + 3846, + 3315, + 3278, + 3229, + 3034, + 2682, + 3611, + 4194, + 3306, + 3186, + 3632, + 4434, + 3580, + 3292, + 3145, + 3812, + 3520, + 3453, + 4198, + 3449, + 4232, + 3418, + 3499, + 4052, + 3625, + 3724, + 2701, + 3948, + 2899, + 3494, + 2921, + 4025, + 4644, + 4252, + 4236, + 3072, + 3117, + 3619, + 3511, + 3561, + 3578, + 3808, + 2873, + 3357, + 3464, + 3756, + 3787, + 2911, + 2759, + 3252, + 3310, + 4036, + 4076, + 3530, + 3551, + 2966, + 3066, + 3207, + 3303, + 2594, + 3597, + 3445, + 3060, + 3435, + 3442, + 3119, + 2798, + 3005, + 2894, + 2786, + 2869, + 3008, + 2758, + 3842, + 3973, + 3603, + 2483, + 3243, + 3631, + 3712, + 2493, + 3178, + 3671, + 3457, + 2848, + 2429, + 3027, + 2825, + 3706, + 3237, + 3770, + 3565, + 3872, + 3774, + 3052, + 3056, + 3707, + 3366, + 2947, + 3689, + 2770, + 3227, + 2748, + 3231, + 3224, + 3387, + 2898, + 3822 + ], + "repo-map 2k": [ + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000 + ], + "repo-map 2k query-aware": [ + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000 + ], + "repo-map 8k": [ + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000 + ], + "repo-map 8k query-aware": [ + 8000, + 7996, + 7998, + 7999, + 8000, + 7998, + 8000, + 7999, + 8000, + 7999, + 8000, + 8000, + 8000, + 7999, + 8000, + 8000, + 8000, + 7999, + 8000, + 7997, + 7999, + 8000, + 8000, + 8000, + 7998, + 8000, + 7997, + 8000, + 8000, + 7996, + 8000, + 8000, + 7999, + 7997, + 8000, + 8000, + 8000, + 8000, + 8000, + 7998, + 8000, + 7996, + 8000, + 7999, + 7998, + 7996, + 8000, + 8000, + 8000, + 8000, + 7998, + 8000, + 7998, + 7999, + 7997, + 8000, + 8000, + 7998, + 7999, + 8000, + 8000, + 7997, + 8000, + 7999, + 7998, + 8000, + 7998, + 8000, + 8000, + 7997, + 8000, + 7999, + 8000, + 7999, + 7999, + 8000, + 7997, + 8000, + 7998, + 8000, + 7996, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 7998, + 8000, + 7997, + 7997, + 8000, + 8000, + 8000, + 7996, + 8000, + 8000, + 8000, + 7997, + 7998, + 7996, + 7998, + 8000, + 8000, + 8000, + 7996, + 8000, + 8000, + 7996, + 7998, + 8000, + 8000, + 8000, + 8000, + 8000, + 7999, + 8000, + 7999, + 7997, + 7997, + 7999, + 7997, + 8000, + 8000, + 8000, + 8000, + 7999, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 7996, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 7999, + 8000, + 7999, + 8000, + 8000 + ] + }, + "packet_tokens_per_query": { + "index": [ + 2946, + 2841, + 2765, + 2475, + 2622, + 2672, + 2718, + 2769, + 2322, + 2859, + 2913, + 2952, + 3035, + 2590, + 2341, + 2684, + 1469, + 2653, + 2946, + 2707, + 2703, + 2693, + 2946, + 2512, + 2664, + 970, + 2238, + 1529, + 2577, + 2629, + 380, + 1106, + 2662, + 2714, + 2440, + 1486, + 2797, + 2694, + 2775, + 2543, + 2308, + 2825, + 2753, + 2637, + 2942, + 2560, + 2888, + 2875, + 2551, + 2324, + 2106, + 2528, + 2314, + 2043, + 2538, + 1162, + 2482, + 1642, + 1792, + 2409, + 2605, + 2451, + 2400, + 2610, + 2036, + 2487, + 2239, + 2670, + 2294, + 2533, + 2666, + 2311, + 2751, + 2508, + 2853, + 2553, + 2653, + 2323, + 2073, + 2370, + 2863, + 2454, + 2506, + 2432, + 1201, + 2295, + 2906, + 2801, + 2095, + 1197, + 2870, + 2513, + 1859, + 2838, + 2732, + 2661, + 2701, + 2641, + 2839, + 369, + 2636, + 2669, + 2665, + 2464, + 2436, + 2128, + 2725, + 2443, + 2374, + 2320, + 1400, + 2238, + 2824, + 2130, + 2688, + 2411, + 2354, + 2769, + 2651, + 2620, + 2850, + 2753, + 3170, + 2589, + 2772, + 2655, + 2957, + 2025, + 2861, + 2886, + 2227, + 2559, + 2411, + 2622, + 2698, + 2730, + 1366, + 2584, + 2893, + 2526, + 2413, + 2702, + 2456, + 2762, + 2800, + 951, + 620, + 1194, + 2563, + 2431 + ], + "index (uncapped reads)": [ + 2946, + 2841, + 2765, + 2475, + 2608, + 2658, + 2718, + 2769, + 2322, + 2859, + 2913, + 2952, + 3035, + 2548, + 2341, + 2670, + 1469, + 2653, + 2946, + 2707, + 2703, + 2693, + 2946, + 2512, + 2664, + 970, + 2210, + 1515, + 2577, + 2615, + 380, + 1106, + 2662, + 2714, + 2426, + 1486, + 2783, + 2694, + 2775, + 2515, + 2308, + 2825, + 2753, + 2595, + 2928, + 2560, + 2888, + 2875, + 2537, + 2310, + 2106, + 2528, + 2314, + 2043, + 2538, + 1162, + 2482, + 1642, + 1792, + 2409, + 2591, + 2451, + 2400, + 2596, + 2022, + 2473, + 2211, + 2642, + 2280, + 2533, + 2666, + 2297, + 2751, + 2508, + 2825, + 2553, + 2653, + 2323, + 2073, + 2370, + 2863, + 2454, + 2506, + 2432, + 1201, + 2295, + 2906, + 2801, + 2081, + 1197, + 2870, + 2513, + 1859, + 2838, + 2732, + 2647, + 2701, + 2641, + 2839, + 369, + 2636, + 2669, + 2637, + 2436, + 2422, + 2128, + 2725, + 2443, + 2374, + 2320, + 1400, + 2238, + 2810, + 2116, + 2688, + 2411, + 2354, + 2769, + 2651, + 2620, + 2850, + 2753, + 3170, + 2589, + 2758, + 2655, + 2957, + 2025, + 2861, + 2886, + 2227, + 2559, + 2397, + 2608, + 2698, + 2716, + 1366, + 2584, + 2893, + 2526, + 2413, + 2702, + 2456, + 2762, + 2800, + 951, + 620, + 1194, + 2521, + 2417 + ], + "rg+window": [ + 1047, + 1626, + 1208, + 1119, + 1244, + 926, + 1107, + 876, + 1258, + 1194, + 1242, + 1047, + 848, + 1333, + 1235, + 1095, + 246, + 1545, + 1047, + 1360, + 1149, + 1417, + 1047, + 1329, + 1383, + 996, + 1201, + 30, + 1216, + 974, + 1123, + 1010, + 1436, + 995, + 1040, + 1056, + 1326, + 37, + 1021, + 990, + 1221, + 1247, + 1218, + 862, + 1250, + 1184, + 1148, + 962, + 1043, + 1103, + 1345, + 1391, + 1121, + 1149, + 1034, + 1518, + 1401, + 1089, + 1503, + 1384, + 1214, + 1134, + 1348, + 867, + 1015, + 1507, + 1806, + 1227, + 1115, + 1320, + 1239, + 832, + 1118, + 1394, + 1987, + 1181, + 1442, + 1106, + 1262, + 1813, + 1863, + 1002, + 1159, + 1827, + 1022, + 1034, + 1756, + 940, + 1282, + 1089, + 1176, + 1157, + 1177, + 1310, + 985, + 1009, + 1067, + 1390, + 1229, + 1475, + 1513, + 866, + 990, + 1664, + 1135, + 1656, + 1357, + 880, + 1302, + 1374, + 958, + 1126, + 1145, + 1172, + 994, + 1184, + 1157, + 1075, + 181, + 1171, + 1116, + 1375, + 912, + 1382, + 1330, + 1116, + 1314, + 830, + 1095, + 1197, + 1541, + 1048, + 1260, + 1134, + 1045, + 1154, + 1233, + 979, + 973, + 894, + 1157, + 1184, + 1113, + 1389, + 941, + 1675, + 547, + 1513, + 878, + 2111 + ], + "repo-map 2k": [ + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000 + ], + "repo-map 2k query-aware": [ + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 1999, + 1999, + 1999, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 2000, + 1999, + 1999, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 2000, + 2000, + 1999, + 2000, + 2000, + 1999, + 2000, + 2000 + ], + "repo-map 8k": [ + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000 + ], + "repo-map 8k query-aware": [ + 8000, + 7996, + 7998, + 7999, + 8000, + 7998, + 8000, + 7999, + 8000, + 7999, + 8000, + 8000, + 8000, + 7999, + 8000, + 8000, + 8000, + 7999, + 8000, + 7997, + 7999, + 8000, + 8000, + 8000, + 7998, + 8000, + 7997, + 8000, + 8000, + 7996, + 8000, + 8000, + 7999, + 7997, + 8000, + 8000, + 8000, + 8000, + 8000, + 7998, + 8000, + 7996, + 8000, + 7999, + 7998, + 7996, + 8000, + 8000, + 8000, + 8000, + 7998, + 8000, + 7998, + 7999, + 7997, + 8000, + 8000, + 7998, + 7999, + 8000, + 8000, + 7997, + 8000, + 7999, + 7998, + 8000, + 7998, + 8000, + 8000, + 7997, + 8000, + 7999, + 8000, + 7999, + 7999, + 8000, + 7997, + 8000, + 7998, + 8000, + 7996, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 7998, + 8000, + 7997, + 7997, + 8000, + 8000, + 8000, + 7996, + 8000, + 8000, + 8000, + 7997, + 7998, + 7996, + 7998, + 8000, + 8000, + 8000, + 7996, + 8000, + 8000, + 7996, + 7998, + 8000, + 8000, + 8000, + 8000, + 8000, + 7999, + 8000, + 7999, + 7997, + 7997, + 7999, + 7997, + 8000, + 8000, + 8000, + 8000, + 7999, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 7996, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 8000, + 7999, + 8000, + 7999, + 8000, + 8000 + ] + } + } + ], + "pooled": { + "n_queries": 450, + "summary": { + "index": { + "hit@3": 0.5466666666666666, + "recall@5": 0.5246296296296297, + "MRR": 0.45615079365079364, + "tokens_mean": 3835.18, + "tokens_median": 3860.5, + "packet_tokens_mean": 2401.56 + }, + "index (uncapped reads)": { + "hit@3": 0.5466666666666666, + "recall@5": 0.5246296296296297, + "MRR": 0.45615079365079364, + "tokens_mean": 6803.142222222222, + "tokens_median": 4207.0, + "packet_tokens_mean": 2385.2533333333336 + }, + "rg+window": { + "hit@3": 0.30444444444444446, + "recall@5": 0.2931481481481481, + "MRR": 0.2626218659670105, + "tokens_mean": 3495.448888888889, + "tokens_median": 3442.0, + "packet_tokens_mean": 1350.6444444444444 + }, + "repo-map 2k": { + "present": 0.3844444444444444, + "recall": 0.2638888888888889, + "tokens_mean": 1999.3333333333333, + "tokens_median": 2000.0, + "packet_tokens_mean": 1999.3333333333333 + }, + "repo-map 2k query-aware": { + "present": 0.4177777777777778, + "recall": 0.3259259259259259, + "tokens_mean": 1999.5177777777778, + "tokens_median": 2000.0, + "packet_tokens_mean": 1999.5177777777778 + }, + "repo-map 8k": { + "present": 0.9911111111111112, + "recall": 0.9677777777777777, + "tokens_mean": 7495.0, + "tokens_median": 7728.0, + "packet_tokens_mean": 7495.0 + }, + "repo-map 8k query-aware": { + "present": 0.9644444444444444, + "recall": 0.9396296296296296, + "tokens_mean": 7494.713333333333, + "tokens_median": 7728.0, + "packet_tokens_mean": 7494.713333333333 + } + }, + "index_vs_rg": { + "hit@3": { + "delta": 0.24222222222222223, + "ci_lo": 0.18666666666666668, + "ci_hi": 0.29777777777777775, + "p": 0.0 + }, + "recall@5": { + "delta": 0.23148148148148148, + "ci_lo": 0.18296296296296308, + "ci_hi": 0.2779629629629629, + "p": 0.0 + }, + "MRR": { + "delta": 0.19352892768378316, + "ci_lo": 0.15148047527838937, + "ci_hi": 0.23548311127822746, + "p": 0.0 + }, + "tokens": { + "delta": 339.7311111111111, + "ci_lo": 238.13111111111112, + "ci_hi": 439.5511111111111, + "p": 0.0 + } + } + } +} \ No newline at end of file diff --git a/tests/eval/results/2026-09-04-public-baselines.md b/tests/eval/results/2026-09-04-public-baselines.md new file mode 100644 index 0000000..856a769 --- /dev/null +++ b/tests/eval/results/2026-09-04-public-baselines.md @@ -0,0 +1,104 @@ +# Index vs grep-style and repo-map-style baselines on public repositories + +- **Date:** 2026-09-04 +- **codebase-index:** 1.9.0 +- **Tokenizer (both sides):** tiktoken/cl100k_base +- **ripgrep:** ripgrep 14.1.1 (rev 4649aa9700) +- **Platform:** Windows 11 / Python 3.12.12 +- **Read model:** top-3 follow-through reads, 80-line grep windows, `rg` listing capped at 50 lines; repo maps packed under 2000 / 8000 tokens +- **Ground truth:** commit subject → files that commit changed (`tests/eval/gen_queries.py`), newest N localised commits per repository; changelog-style files excluded from both answers and corpus + +## Corpora + +| corpus | commit | files indexed | symbols | edges | queries | index build | +|---|---|---:|---:|---:|---:|---:| +| flask | `d318b6834711` | 229 | 1622 | 4425 | 150 | 3.2 s | +| gson | `b3f4ca20087f` | 311 | 4193 | 22407 | 150 | 5.3 s | +| fastify | `15ebc8e2fe69` | 389 | 1185 | 34297 | 150 | 2.0 s | + +### Pooled — ranked retrieval (n=450) + +| method | hit@3 | recall@5 | MRR | packet / listing tokens | + top-3 reads (mean) | + top-3 reads (median) | +|---|---:|---:|---:|---:|---:|---:| +| index | 0.547 | 0.525 | 0.456 | 2,402 | 3,835 | 3,860 | +| index (uncapped reads) | 0.547 | 0.525 | 0.456 | 2,385 | 6,803 | 4,207 | +| rg+window | 0.304 | 0.293 | 0.263 | 1,351 | 3,495 | 3,442 | + +### Pooled — repo-map-style context (n=450) + +| method | answer present in map | tokens/query | +|---|---:|---:| +| repo-map 2k | 0.384 | 1,999 | +| repo-map 2k query-aware | 0.418 | 2,000 | +| repo-map 8k | 0.991 | 7,495 | +| repo-map 8k query-aware | 0.964 | 7,495 | + +### Index vs rg+window, paired significance (pooled) + +| metric | delta (index − rg) | 95% CI | p | significant | +|---|---:|---:|---:|---| +| hit@3 | +0.242 | [+0.187, +0.298] | 0.000 | yes | +| recall@5 | +0.231 | [+0.183, +0.278] | 0.000 | yes | +| MRR | +0.194 | [+0.151, +0.235] | 0.000 | yes | +| tokens | 340 | [238, 440] | 0.000 | yes | + +### flask — ranked retrieval (n=150) + +| method | hit@3 | recall@5 | MRR | packet / listing tokens | + top-3 reads (mean) | + top-3 reads (median) | +|---|---:|---:|---:|---:|---:|---:| +| index | 0.487 | 0.503 | 0.383 | 2,359 | 3,591 | 3,576 | +| index (uncapped reads) | 0.487 | 0.503 | 0.383 | 2,350 | 5,724 | 3,776 | +| rg+window | 0.233 | 0.245 | 0.229 | 1,009 | 3,079 | 3,187 | + +### flask — repo-map-style context (n=150) + +| method | answer present in map | tokens/query | +|---|---:|---:| +| repo-map 2k | 0.313 | 1,998 | +| repo-map 2k query-aware | 0.273 | 1,999 | +| repo-map 8k | 1.000 | 7,728 | +| repo-map 8k query-aware | 1.000 | 7,728 | + +### gson — ranked retrieval (n=150) + +| method | hit@3 | recall@5 | MRR | packet / listing tokens | + top-3 reads (mean) | + top-3 reads (median) | +|---|---:|---:|---:|---:|---:|---:| +| index | 0.573 | 0.523 | 0.503 | 2,418 | 4,375 | 4,376 | +| index (uncapped reads) | 0.573 | 0.523 | 0.503 | 2,382 | 10,659 | 8,680 | +| rg+window | 0.347 | 0.337 | 0.290 | 1,865 | 4,117 | 4,092 | + +### gson — repo-map-style context (n=150) + +| method | answer present in map | tokens/query | +|---|---:|---:| +| repo-map 2k | 0.480 | 2,000 | +| repo-map 2k query-aware | 0.613 | 2,000 | +| repo-map 8k | 1.000 | 6,757 | +| repo-map 8k query-aware | 1.000 | 6,757 | + +### fastify — ranked retrieval (n=150) + +| method | hit@3 | recall@5 | MRR | packet / listing tokens | + top-3 reads (mean) | + top-3 reads (median) | +|---|---:|---:|---:|---:|---:|---:| +| index | 0.580 | 0.547 | 0.482 | 2,428 | 3,539 | 3,562 | +| index (uncapped reads) | 0.580 | 0.547 | 0.482 | 2,423 | 4,027 | 3,611 | +| rg+window | 0.333 | 0.297 | 0.269 | 1,178 | 3,290 | 3,312 | + +### fastify — repo-map-style context (n=150) + +| method | answer present in map | tokens/query | +|---|---:|---:| +| repo-map 2k | 0.360 | 2,000 | +| repo-map 2k query-aware | 0.367 | 2,000 | +| repo-map 8k | 0.973 | 8,000 | +| repo-map 8k query-aware | 0.893 | 7,999 | + +## How to read this + +- `hit@3` is the metric that matters for an agent that opens the top three files. `recall@5` credits multi-file answers. `MRR` rewards putting the answer first. +- `packet / listing tokens` is what the agent sees before opening anything: the full JSON search payload for the index (snippets included), the first 50 `rg` lines for grep. `+ top-3 reads` adds the follow-through: the index's top-3 `recommended_reads` ranges in full, or three 80-line grep windows. Same tokenizer on every side. +- The index packet already carries budgeted snippets, so an agent that answers from the packet pays the first column only; an agent that re-reads every recommended range pays the second. Grep has no first-column equivalent — the listing alone rarely answers anything. +- A repo map is not a ranking, so it is scored on whether the answer file is *present* at all — an upper bound on what an agent could do with it. +- Ground truth is one commit's files; a query can have a correct answer the commit did not touch. Absolute numbers understate every method equally; read the *deltas*, and read the CI before the delta. +- Latency is deliberately not tabulated: the index runs in-process, ripgrep is a separate binary; neither number is a fair claim against the other. +- Not measured here: an LLM-driven agent exploring the repository. That needs model calls and is tracked as future work in `docs/BENCHMARKS.md`. diff --git a/tests/eval/run_baselines.py b/tests/eval/run_baselines.py new file mode 100644 index 0000000..ee305be --- /dev/null +++ b/tests/eval/run_baselines.py @@ -0,0 +1,348 @@ +#!/usr/bin/env python3 +"""Compare the index against grep-style and repo-map-style baselines on public repositories. + + # the pinned public corpora (cloned into --workdir at fixed commits) + python tests/eval/run_baselines.py --clone --workdir .tmp-baselines + + # any local git repository instead + python tests/eval/run_baselines.py --repo ../some-service + + # write raw JSON + Markdown next to the other logged runs + python tests/eval/run_baselines.py --clone --out tests/eval/results/public-baselines + +Why this exists +--------------- +`run_eval.py` measures the index against *its own earlier versions*; it can say +a ranking change helped, never that an index beats not having one. This script +asks the second question on repositories anyone can clone, at commits recorded +in the output, with ground truth mined from git history (`gen_queries.py`) so +neither the queries nor the answers are chosen by hand or by the retriever. + +What is compared (see `baselines.py` for the exact read model) +------------------------------------------------------------- +* index — `search()` with the shipped default tuning +* index, uncapped — same ranking with `max_read_lines=0` (the pre-1.9.1 read plan) +* rg + window — salient-term ripgrep, density-ranked, 80-line windows +* repo-map (2k/8k) — signature map packed under a budget, degree-ranked +* repo-map, query-aware — same map, files matching the question's identifiers first + +Every side is charged with the same tokenizer for the text that enters context. +Latency is context only. Significance is reported per metric with a paired +bootstrap CI and a paired permutation p-value against the rg baseline. +""" + +from __future__ import annotations + +import argparse +import json +import platform +import statistics +import subprocess +import sys +import tempfile +import time +from datetime import datetime, timezone +from pathlib import Path + +if sys.platform == "win32": # the report uses arrows/dashes; never die on a cp1251 console + sys.stdout.reconfigure(encoding="utf-8") # type: ignore[union-attr] + +REPO_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(REPO_ROOT / "tests")) +sys.path.insert(0, str(REPO_ROOT / "src")) + +from codebase_index import __version__ # noqa: E402 +from eval import baselines, gen_queries, harness, metrics # noqa: E402 + +# Public corpora: three languages, three sizes, pinned so a re-run sees the same +# tree and the same history. Update the SHA deliberately and re-log the run. +PUBLIC_CORPORA = { + "flask": ("https://github.com/pallets/flask.git", "d318b683471101618febed18996405ad26462110"), + "gson": ("https://github.com/google/gson.git", "b3f4ca20087f9066de4c340522ff84e0558e1ad1"), + "fastify": ("https://github.com/fastify/fastify.git", "15ebc8e2fe6932d3e91afa84709523804f8a2537"), +} + + +def _git(*args: str, cwd: Path | None = None) -> str: + return subprocess.run( + ["git", *args], cwd=cwd, capture_output=True, text=True, + encoding="utf-8", errors="replace", check=True, + ).stdout.strip() + + +def _clone_pinned(name: str, url: str, sha: str, workdir: Path) -> Path: + dest = workdir / name + if not (dest / ".git").is_dir(): + print(f"cloning {url} -> {dest}", flush=True) + _git("clone", "--quiet", url, str(dest)) + head = _git("rev-parse", "HEAD", cwd=dest) + if head != sha: + _git("fetch", "--quiet", "origin", sha, cwd=dest) + _git("checkout", "--quiet", sha, cwd=dest) + return dest + + +def _sig(base: list[float], cand: list[float], *, resamples: int) -> dict[str, float]: + deltas = [c - b for b, c in zip(base, cand)] + delta, lo, hi = metrics.paired_bootstrap_ci(deltas, resamples=resamples) + p = metrics.paired_permutation_p(deltas, resamples=resamples) + return {"delta": delta, "ci_lo": lo, "ci_hi": hi, "p": p} + + +def run_corpus( + name: str, + root: Path, + queries: list[harness.EvalQuery], + *, + tmp: Path, +) -> dict: + corpus = baselines.Corpus(root) + t0 = time.perf_counter() + db = harness.build_corpus_index(root, tmp / f"{name}.sqlite") + build_ms = (time.perf_counter() - t0) * 1000.0 + try: + conn = db.conn + n_files = conn.execute("SELECT COUNT(*) FROM files").fetchone()[0] + n_symbols = conn.execute("SELECT COUNT(*) FROM symbols").fetchone()[0] + n_edges = conn.execute("SELECT COUNT(*) FROM edges").fetchone()[0] + map_entries = baselines.build_repo_map_entries(conn) + + per_side: dict[str, dict[str, list[float]]] = {} + tokens: dict[str, list[int]] = {} + packet: dict[str, list[int]] = {} + latency: dict[str, list[float]] = {} + + def record(side: str, out: baselines.BaselineOutcome, scores: dict[str, float]) -> None: + for k, v in scores.items(): + per_side.setdefault(side, {}).setdefault(k, []).append(v) + tokens.setdefault(side, []).append(out.tokens) + packet.setdefault(side, []).append(out.packet_tokens) + latency.setdefault(side, []).append(out.latency_ms) + + for q in queries: + idx = baselines.run_index(conn, corpus, q) + record("index", idx, baselines.score(idx, q)) + # Same ranking, read plan uncapped (pre-1.9.1 behaviour): isolates what + # the `max_read_lines` cap saves without touching quality. + raw = baselines.run_index(conn, corpus, q, max_read_lines=0) + record("index (uncapped reads)", raw, baselines.score(raw, q)) + rg = baselines.run_rg_window(corpus, q) + record("rg+window", rg, baselines.score(rg, q)) + for budget in baselines.REPO_MAP_BUDGETS: + for aware in (False, True): + label = f"repo-map {budget // 1000}k" + (" query-aware" if aware else "") + rm = baselines.run_repo_map(map_entries, q, budget=budget, query_aware=aware) + record(label, rm, baselines.score(rm, q, present_only=True)) + finally: + db.close() + + summary = {} + for side, cols in per_side.items(): + summary[side] = {k: statistics.fmean(v) for k, v in cols.items()} + summary[side]["tokens_mean"] = statistics.fmean(tokens[side]) + summary[side]["tokens_median"] = statistics.median(tokens[side]) + summary[side]["packet_tokens_mean"] = statistics.fmean(packet[side]) + summary[side]["latency_p50_ms"] = metrics.percentile(latency[side], 50) + return { + "corpus": name, + "root": str(root), + "head": _git("rev-parse", "HEAD", cwd=root), + "n_queries": len(queries), + "index_build_ms": build_ms, + "files_indexed": n_files, + "symbols_indexed": n_symbols, + "edges_indexed": n_edges, + "summary": summary, + "per_query": per_side, + "tokens_per_query": tokens, + "packet_tokens_per_query": packet, + } + + +def pooled(corpora: list[dict], *, resamples: int) -> dict: + sides = list(corpora[0]["per_query"].keys()) + per_side: dict[str, dict[str, list[float]]] = {} + tokens: dict[str, list[int]] = {} + packet: dict[str, list[int]] = {} + for c in corpora: + for side in sides: + for k, v in c["per_query"][side].items(): + per_side.setdefault(side, {}).setdefault(k, []).extend(v) + tokens.setdefault(side, []).extend(c["tokens_per_query"][side]) + packet.setdefault(side, []).extend(c["packet_tokens_per_query"][side]) + summary = {} + for side, cols in per_side.items(): + summary[side] = {k: statistics.fmean(v) for k, v in cols.items()} + summary[side]["tokens_mean"] = statistics.fmean(tokens[side]) + summary[side]["tokens_median"] = statistics.median(tokens[side]) + summary[side]["packet_tokens_mean"] = statistics.fmean(packet[side]) + significance = { + k: _sig(per_side["rg+window"][k], per_side["index"][k], resamples=resamples) + for k in baselines.METRICS + } + significance["tokens"] = _sig( + [float(t) for t in tokens["rg+window"]], [float(t) for t in tokens["index"]], + resamples=resamples, + ) + return {"n_queries": sum(c["n_queries"] for c in corpora), "summary": summary, + "index_vs_rg": significance} + + +def render_markdown(report: dict) -> str: + L: list[str] = [] + L.append("# Index vs grep-style and repo-map-style baselines on public repositories\n") + L.append(f"- **Date:** {report['date']}") + L.append(f"- **codebase-index:** {report['version']}") + L.append(f"- **Tokenizer (both sides):** {report['tokenizer']}") + L.append(f"- **ripgrep:** {report['ripgrep'] or 'not found — Python scan fallback (latency not comparable)'}") + L.append(f"- **Platform:** {report['platform']}") + L.append(f"- **Read model:** top-{baselines.TOP_K} follow-through reads, " + f"{baselines.WINDOW}-line grep windows, `rg` listing capped at " + f"{baselines.LISTING_CAP} lines; repo maps packed under " + f"{' / '.join(str(b) for b in baselines.REPO_MAP_BUDGETS)} tokens") + L.append("- **Ground truth:** commit subject → files that commit changed " + "(`tests/eval/gen_queries.py`), newest N localised commits per repository; " + "changelog-style files excluded from both answers and corpus\n") + + L.append("## Corpora\n") + L.append("| corpus | commit | files indexed | symbols | edges | queries | index build |") + L.append("|---|---|---:|---:|---:|---:|---:|") + for c in report["corpora"]: + L.append(f"| {c['corpus']} | `{c['head'][:12]}` | {c['files_indexed']} | " + f"{c['symbols_indexed']} | {c['edges_indexed']} | {c['n_queries']} | " + f"{c['index_build_ms'] / 1000:.1f} s |") + L.append("") + + def ranked_table(summary: dict, n: int, title: str) -> None: + L.append(f"### {title} — ranked retrieval (n={n})\n") + L.append("| method | hit@3 | recall@5 | MRR | packet / listing tokens | " + "+ top-3 reads (mean) | + top-3 reads (median) |") + L.append("|---|---:|---:|---:|---:|---:|---:|") + for side in ("index", "index (uncapped reads)", "rg+window"): + s = summary[side] + L.append(f"| {side} | {s['hit@3']:.3f} | {s['recall@5']:.3f} | {s['MRR']:.3f} | " + f"{s['packet_tokens_mean']:,.0f} | {s['tokens_mean']:,.0f} | " + f"{s['tokens_median']:,.0f} |") + L.append("") + L.append(f"### {title} — repo-map-style context (n={n})\n") + L.append("| method | answer present in map | tokens/query |") + L.append("|---|---:|---:|") + for side, s in summary.items(): + if side.startswith("repo-map"): + L.append(f"| {side} | {s['present']:.3f} | {s['tokens_mean']:,.0f} |") + L.append("") + + ranked_table(report["pooled"]["summary"], report["pooled"]["n_queries"], "Pooled") + L.append("### Index vs rg+window, paired significance (pooled)\n") + L.append("| metric | delta (index − rg) | 95% CI | p | significant |") + L.append("|---|---:|---:|---:|---|") + for k, s in report["pooled"]["index_vs_rg"].items(): + fmt = ",.0f" if k == "tokens" else "+.3f" + L.append(f"| {k} | {s['delta']:{fmt}} | [{s['ci_lo']:{fmt}}, {s['ci_hi']:{fmt}}] | " + f"{s['p']:.3f} | {'yes' if s['p'] < 0.05 else 'no'} |") + L.append("") + for c in report["corpora"]: + ranked_table(c["summary"], c["n_queries"], c["corpus"]) + + L.append("## How to read this\n") + L.append("- `hit@3` is the metric that matters for an agent that opens the top three files. " + "`recall@5` credits multi-file answers. `MRR` rewards putting the answer first.") + L.append("- `packet / listing tokens` is what the agent sees before opening anything: " + "the full JSON search payload for the index (snippets included), the " + f"first {baselines.LISTING_CAP} `rg` lines for grep. `+ top-3 reads` adds the " + "follow-through: the index's top-3 `recommended_reads` ranges in full, " + f"or three {baselines.WINDOW}-line grep windows. Same tokenizer on every side.") + L.append("- The index packet already carries budgeted snippets, so an agent that answers " + "from the packet pays the first column only; an agent that re-reads every " + "recommended range pays the second. Grep has no first-column equivalent — the " + "listing alone rarely answers anything.") + L.append("- A repo map is not a ranking, so it is scored on whether the answer file is " + "*present* at all — an upper bound on what an agent could do with it.") + L.append("- Ground truth is one commit's files; a query can have a correct answer the " + "commit did not touch. Absolute numbers understate every method equally; " + "read the *deltas*, and read the CI before the delta.") + L.append("- Latency is deliberately not tabulated: the index runs in-process, ripgrep " + "is a separate binary; neither number is a fair claim against the other.") + L.append("- Not measured here: an LLM-driven agent exploring the repository. That " + "needs model calls and is tracked as future work in `docs/BENCHMARKS.md`.") + return "\n".join(L) + "\n" + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--repo", action="append", default=[], + help="local git repository to benchmark (repeatable)") + ap.add_argument("--clone", action="store_true", + help="clone the pinned public corpora into --workdir") + ap.add_argument("--only", action="append", default=[], + help="restrict --clone to these corpus names") + ap.add_argument("--workdir", type=Path, default=Path(".tmp-baselines")) + ap.add_argument("--queries-per-repo", type=int, default=150, + help="newest N git-derived queries per repository") + ap.add_argument("--max-files", type=int, default=4) + ap.add_argument("--resamples", type=int, default=5000) + ap.add_argument("--out", type=Path, default=None, + help="write .json and .md") + args = ap.parse_args(argv) + + roots: list[tuple[str, Path]] = [] + if args.clone: + args.workdir.mkdir(parents=True, exist_ok=True) + for name, (url, sha) in PUBLIC_CORPORA.items(): + if args.only and name not in args.only: + continue + roots.append((name, _clone_pinned(name, url, sha, args.workdir))) + for r in args.repo: + p = Path(r).resolve() + roots.append((p.name, p)) + if not roots: + ap.error("pass --clone and/or --repo") + + corpora: list[dict] = [] + with tempfile.TemporaryDirectory() as tmp: + for name, root in roots: + records = gen_queries.harvest( + root, max_commits=4000, max_files=args.max_files, min_words=3, + )[: args.queries_per_repo] + queries = [ + harness.EvalQuery( + query=r["query"], category=r["category"], + expected_files=tuple(r["expected_files"]), + ) + for r in records + ] + problems = harness.validate_queries(queries, root) + if problems: + print(f"ground truth invalid for {name}: {problems[:3]}", file=sys.stderr) + return 2 + print(f"{name}: {len(queries)} queries; indexing + running...", flush=True) + corpora.append(run_corpus(name, root, queries, tmp=Path(tmp))) + s = corpora[-1]["summary"] + print(f" index hit@3={s['index']['hit@3']:.3f} tokens={s['index']['tokens_mean']:.0f}") + print(f" rg+window hit@3={s['rg+window']['hit@3']:.3f} tokens={s['rg+window']['tokens_mean']:.0f}") + + report = { + "date": datetime.now(timezone.utc).strftime("%Y-%m-%d"), + "version": __version__, + "tokenizer": baselines.TOKENIZER, + "ripgrep": baselines.rg_version(), + "platform": f"{platform.system()} {platform.release()} / Python {platform.python_version()}", + "corpora": corpora, + "pooled": pooled(corpora, resamples=args.resamples), + } + md = render_markdown(report) + print() + print(md) + if args.out: + args.out.parent.mkdir(parents=True, exist_ok=True) + slim = json.loads(json.dumps(report)) + # Per-query vectors are what a re-run compares against; keep them in the raw file. + args.out.with_suffix(".json").write_text(json.dumps(slim, indent=2), encoding="utf-8") + args.out.with_suffix(".md").write_text(md, encoding="utf-8") + print(f"wrote {args.out.with_suffix('.json')} and {args.out.with_suffix('.md')}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_eval_baselines.py b/tests/test_eval_baselines.py new file mode 100644 index 0000000..79f62fc --- /dev/null +++ b/tests/test_eval_baselines.py @@ -0,0 +1,68 @@ +"""Unit checks for the baseline models in tests/eval/baselines.py. + +These pin the read model the public baseline benchmark relies on, so a change to +salient-term extraction or map packing cannot silently move a logged number. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from eval import baselines # noqa: E402 +from eval.harness import EvalQuery # noqa: E402 + + +def test_salient_terms_drop_stopwords_and_keep_identifiers(): + terms = baselines.salient_terms("fix how the SessionInterface saves a cookie when not set") + assert "SessionInterface" in terms + assert "cookie" in terms + assert not {"fix", "how", "the", "when", "not", "set"} & {t.lower() for t in terms} + + +def test_salient_terms_are_bounded_and_longest_first(): + terms = baselines.salient_terms("alpha betagamma de epsilonzeta eta theta iota kappa", max_terms=3) + assert len(terms) == 3 + assert terms[0] == "epsilonzeta" + + +def test_tokens_for_reads_merges_overlapping_ranges(tmp_path): + (tmp_path / "a.py").write_text("\n".join(f"line {i}" for i in range(1, 101)), encoding="utf-8") + corpus = baselines.Corpus(tmp_path) + once = baselines.tokens_for_reads(corpus, {"a.py": [(1, 50)]}) + twice = baselines.tokens_for_reads(corpus, {"a.py": [(1, 50), (10, 40)]}) + assert once == twice, "overlapping windows must be charged once" + assert baselines.tokens_for_reads(corpus, {"a.py": [(500, 600)]}) == 0 + + +def test_pack_repo_map_respects_budget_and_query_awareness(): + entries = [ + baselines.MapEntry("a.py", "a.py:\n def alpha()", 10, degree=5, names=frozenset({"alpha"})), + baselines.MapEntry("b.py", "b.py:\n def beta()", 10, degree=50, names=frozenset({"beta"})), + baselines.MapEntry("c.py", "c.py:\n def gamma()", 10, degree=1, names=frozenset({"gamma"})), + ] + chosen, used = baselines.pack_repo_map(entries, budget=20) + assert chosen == ["b.py", "a.py"] and used == 20 + chosen, _ = baselines.pack_repo_map(entries, budget=20, query_terms=["gamma"]) + assert chosen[0] == "c.py" + + +def test_score_ranked_and_present_only(): + q = EvalQuery(query="x", category="c", expected_files=("b.py",)) + out = baselines.BaselineOutcome(["a.py", "b.py"], tokens=0, packet_tokens=0, latency_ms=0) + assert baselines.score(out, q) == {"hit@3": 1.0, "recall@5": 1.0, "MRR": 0.5} + assert baselines.score(out, q, present_only=True)["present"] == 1.0 + + +@pytest.mark.skipif(baselines.RG is None, reason="ripgrep not available") +def test_rg_window_ranks_by_density(tmp_path): + (tmp_path / "dense.py").write_text("token\n" * 30, encoding="utf-8") + (tmp_path / "sparse.py").write_text("token\n" + "x\n" * 30, encoding="utf-8") + corpus = baselines.Corpus(tmp_path) + out = baselines.run_rg_window(corpus, EvalQuery("find the token handling", "c", ("dense.py",))) + assert out.ranked_files[0] == "dense.py" + assert out.tokens > out.packet_tokens > 0 diff --git a/tests/test_gen_queries.py b/tests/test_gen_queries.py index e6e0ebe..6fbec83 100644 --- a/tests/test_gen_queries.py +++ b/tests/test_gen_queries.py @@ -150,3 +150,18 @@ def test_strip_prefix_handles_unprefixed_and_breaking_subjects(): "perf", "speed up walking", ) + + +def test_changes_style_files_are_never_answers(): + """Flask keeps its changelog in CHANGES.rst; it paraphrases commit subjects + exactly like CHANGELOG.md and must be refused as an answer too.""" + for path in ("CHANGES.rst", "docs/CHANGES.md", "HISTORY.rst", "RELEASE_NOTES.md"): + assert not gen_queries._is_answerable(path), path + assert gen_queries._is_answerable("src/flask/sessions.py") + + +def test_changelog_excludes_reach_the_harness(): + from eval import harness + + assert harness.CHANGELOG_EXCLUDES is gen_queries.CHANGELOG_EXCLUDES + assert "CHANGES*" in gen_queries.CHANGELOG_EXCLUDES From b66572b61f49fd1fc6d37ffa89cf31a2ad359422 Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:24:37 +0300 Subject: [PATCH 07/12] docs: consolidate duplicate pages and replace fabricated examples with real output - One canonical page per topic: RETRIEVAL.md, SCHEMA.md, SECURITY_MODEL.md; the former duplicates are redirect stubs so inbound links keep working. SCHEMA.md now matches storage/schema.sql (the old copy described columns and tables that do not exist). - QUICKSTART and INSTALLATION showed invented output (an AuthService.ts table with a Score column, a fake doctor transcript); both now show output captured on pallets/flask at a pinned commit. - FAQ/INSTALLATION/MCP/ARCHITECTURE: version pins to 1.9.0, full MCP tool list, correct config path (.claude/cache/codebase-index/config.json), doctor's real check list, unverified install paths labelled as such. - Root ROADMAP.md is a milestone history; docs/ROADMAP.md owns forward work. - installer.md translated to English; flags verified against install.sh. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- ROADMAP.md | 142 +++++++--------------------- docs/ARCHITECTURE.md | 25 +++-- docs/DATABASE_SCHEMA.md | 187 +------------------------------------ docs/FAQ.md | 68 ++++++++------ docs/INSTALLATION.md | 41 ++++---- docs/MCP.md | 5 +- docs/QUICKSTART.md | 71 ++++++++------ docs/RETRIEVAL.md | 23 +++-- docs/RETRIEVAL_PIPELINE.md | 180 +---------------------------------- docs/ROADMAP.md | 7 +- docs/SCHEMA.md | 21 ++++- docs/SECURITY.md | 122 +----------------------- docs/SECURITY_MODEL.md | 53 +++++++---- docs/installer.md | 175 +++++++++++++++++----------------- src/codebase_index/cli.py | 3 +- 15 files changed, 326 insertions(+), 797 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index d046f83..254ce1e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,112 +1,34 @@ # Roadmap -`codebase-index` is developed in milestone-driven slices. Each milestone delivers a runnable, testable feature. - -## Milestones - -### M0 — Repository Packaging ✅ -- Polished README, documentation, badges, issue templates, CI workflows. -- MIT license, changelog, code of conduct, contributing guide. -- Claude Code Skill directory structure. -- **Exit:** Repository is ready for public listing and awesome-list submissions. - -### M1 — SQLite + FTS5 Index ✅ -- SQLite database with FTS5 virtual table for full-text search. -- File discovery with layered ignore rules (`.gitignore`, `.codeindexignore`, built-in denylist). -- Incremental indexing with file hash tracking. -- **Exit:** `codebase-index index` populates the database; secrets and binaries are excluded. - -### M2 — Tree-sitter Symbol Extraction ✅ -- AST-based symbol extraction, now expanded beyond the original Python/JavaScript/TypeScript - set to the Tier-A languages listed in `docs/LANGUAGES.md`. -- Symbol-aligned chunking with gap windows. -- `symbol` and `refs` commands for intra-file symbol lookup. -- **Exit:** `codebase-index symbol "AuthService"` returns symbol definitions and references. - -### M3 — Hybrid Retrieval ✅ -- Combined search: path match + symbol match + FTS5 + optional vector search. -- Reciprocal Rank Fusion (RRF) for result merging. -- Confidence scoring and fallback suggestions. -- **Exit:** `codebase-index search "query"` returns ranked results from multiple retrievers. - -### M4 — Graph Expansion ✅ -- Dependency, import, call, and inheritance edge extraction. -- Graph-based result expansion (related files, callers, callees). -- `impact` command for blast radius analysis. -- **Exit:** `codebase-index impact "src/auth/AuthService.ts"` shows affected files and symbols. - -### M5 — Token-Budgeted Retrieval Packets ✅ -- Ranked retrieval packets with file paths, line ranges, snippets, and "next files to read". -- Token budget enforcement (configurable max output size). -- Compact Markdown and JSON output formats. -- **Exit:** Claude reads only the recommended line ranges, not entire files. - -### M6 — Optional Local Embeddings ✅ -- `sqlite-vec` integration for vector similarity search. -- Local embedding models (sentence-transformers) as default. -- External embedding APIs behind explicit opt-in with warnings. -- **Exit:** Semantic queries improve recall when embeddings are enabled. - -### M7 — Multi-CLI skill packaging ✅ -- `init --target claude|codex|opencode|auto|all`. -- Claude Code skill, Codex instructions/resources, OpenCode command/agent files. -- **Exit:** AI CLI setup can be generated from the package without hand-copying files. - -### M8 — Optional Hooks + Watch Mode ✅ -- Post-tool-use hook for automatic index updates. -- `--with-hooks` flag and hook configuration. -- `doctor` reports enabled hooks and their status. -- **Exit:** Index stays fresh automatically after file edits. - -### M9 — Public Release ✅ -- Comprehensive test suite with coverage targets. -- Performance benchmarks on medium-sized repositories. -- GitHub release with tagged version. -- Release pipeline readiness for PyPI trusted publishing. -- Awesome-list submissions (Claude Code skills, AI coding tools). -- **Exit:** tagged GitHub install works on a clean machine. - -### M10 — Distribution hardening -- Publish `codebase-index` to PyPI after trusted publishing is configured and verified. -- Verify `pipx install codebase-index`, `uvx codebase-index init`, and `uv tool install codebase-index`. -- Add Homebrew tap formula: `brew install denfry/tap/codebase-index`. -- Publish signed release checksums and SBOMs for release artifacts. -- **Exit:** users do not need GitHub URL installs for the standard path, and artifacts have a clear supply-chain story. - -### M11 — First-class MCP server ✅ -- Model Context Protocol wrapper for external tool integration. -- MCP server exposing `healthcheck`, `search_code`, `find_symbol`, `find_refs`, - `impact_of`, `explain_code`, and `index_stats`. -- Versioned JSON schema and golden tests for every tool output. -- **Exit:** `codebase-index mcp --root ` can be used by any MCP-compatible client. - -### M11.5 — MCP hardening -- Ready-to-copy configs verified against Claude Desktop, Claude Code, Cursor, VS Code, Zed, - and Windsurf current versions. -- Golden snapshots for every tool output. -- Paging or progressive result support for large repositories. - -### M12 — Public benchmark suite ✅ -- Shipped reproducible public benchmark script: `tests/benchmark_public.py`. -- Retrieval quality: Recall@1/3/5, MRR, nDCG. -- Agent usefulness: answer-correctness proxy on the public fixture. -- Token economy versus grep-window baseline. -- Language-specific results, freshness latency after edits, graph tasks, and scale counters. - -### M12.5 — Real-repo benchmark expansion -- 10k, 100k, 1M LOC repository targets. -- Real-world Python, TypeScript, Java, Go, Rust, C#, PHP repos. -- Human-reviewed answer correctness on real codebase questions. -- Token economy versus repo-map style context and vanilla agent exploration. -- Framework graph tasks: route -> handler -> service -> DB, migrations, config consumers, CI/infra. - -### M13 — Code intelligence graph -- Framework-aware typed edges beyond import/call/reference. -- Routes, tests/fixtures, config consumers, migrations, event flows, DI wiring, - frontend component flows, and error/log-message traces. -- Edge confidence and resolver provenance so agents can distinguish precise - edges from heuristics. - ---- - -See [CHANGELOG.md](CHANGELOG.md) for released versions and their changes. +`codebase-index` was built in milestone-driven slices, each delivering a runnable, tested +feature. The milestones below have shipped; forward work is tracked in the product roadmap, +[docs/ROADMAP.md](docs/ROADMAP.md), which separates *now / next / then* and does not treat an +item as a product claim until it appears in [CHANGELOG.md](CHANGELOG.md). + +## Shipped milestones + +| Milestone | Delivered | Since | +|---|---|---| +| M0 Repository packaging | README, docs, CI, issue templates, MIT license, changelog | 1.0.0 | +| M1 SQLite + FTS5 index | Layered ignore rules, secret/binary/generated exclusion, incremental hashing | 1.0.0 | +| M2 Tree-sitter symbols | `LangSpec` extraction for the Tier-A languages, symbol-aligned chunks, `symbol` / `refs` | 1.0.0, widened in 1.3.0 | +| M3 Hybrid retrieval | Path + symbol + FTS5 (+ optional vector) fused with RRF, confidence and fallbacks | 1.0.0 | +| M4 Graph expansion | Import / call / reference / inheritance edges, `impact` | 1.0.0 | +| M5 Token-budgeted packets | Ranked `file:line` results, `recommended_reads`, Markdown and JSON | 1.0.0 | +| M6 Optional local embeddings | `sqlite-vec`, sentence-transformers, triple-gated external backend | 1.0.0 | +| M7 Multi-CLI packaging | `init --target claude\|codex\|opencode\|auto\|all` (1.0.2); Claude Code plugin one-command install (1.3.0) | 1.0.2 | +| M8 Hooks + watch mode | PostToolUse auto-update, `watch`, `doctor` hook reporting | 1.0.0 | +| M9 Public release | Coverage gate, tagged GitHub releases, clean-machine smoke | 1.0.0 | +| M10 Distribution (PyPI part) | `pip install codebase-index`, `pipx`, Trusted Publishing | 1.6.0 | +| M11 MCP server | Stdio server (1.1.0); versioned envelopes and golden tests for every tool (1.4.0) | 1.1.0 | +| M12 Public benchmark suite | `tests/benchmark_public.py` with Recall/MRR/nDCG/token/freshness/graph metrics (1.1.0); logged run (1.4.0) | 1.1.0 | +| — Retrieval evaluation | Leak-free git-derived ground truth, multi-corpus pooling, significance tests | 1.8.0 / 1.9.0 | + +## Still open (tracked in docs/ROADMAP.md) + +- M10 remainder: `uvx` verification on every CI OS, Homebrew tap, signed checksums / SBOM. +- M11.5: MCP client configs verified against current client releases; paged results. +- M12.5: larger public-repository runs (a 1M-LOC / monorepo target) and an LLM-agent + task-level evaluation. A multi-repository public baseline benchmark now exists at + `tests/eval/run_baselines.py` with a logged run under `tests/eval/results/`. +- M13: typed framework-aware edges (routes, tests, config consumers, migrations, DI). diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4bb88af..3f6579d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -2,8 +2,9 @@ ## 1. Overview -`codebase-index` is a **local-first** code intelligence layer for AI coding agents. In `1.9.0` -it has two shipped faces: +`codebase-index` is a **local-first** code intelligence layer for AI coding agents. This page is +the design reference; the hands-on guide (setup, tests, benchmarks, recipes) is +[DEVELOPMENT.md](DEVELOPMENT.md). It has two shipped faces: 1. **A Claude Code Skill** (`.claude/skills/codebase-index/SKILL.md`) that Claude auto-invokes for codebase questions. The skill is thin: it tells Claude *when* to search, *how* to call the CLI, @@ -71,7 +72,8 @@ codebase-index/ ├── adapters/ # per-CLI install logic (claude/codex/opencode, sh + ps1) ├── lib/ # shared shell helpers for the installer ├── bin/ # plugin wrappers (cbx resolves the provisioned venv) -├── scripts/ # bootstrap.sh/.ps1, release_smoke.py, sync_skill_copies.py +├── scripts/ # bootstrap.sh/.ps1, release_smoke.py, sync_skill_copies.py, +│ # check_versions.py, check_links.py, release_notes.py, gen_assets.py ├── hooks/ # plugin hooks.json (SessionStart bootstrap) ├── .claude-plugin/ # plugin manifest + marketplace catalog ├── .github/ # CI (lint, skill-sync gate, OS/Python test matrix), release @@ -81,6 +83,8 @@ codebase-index/ ├── .claude/ .codex/ .opencode/ # committed installed copies (generated — same script) ├── examples/ # sample queries, configs, hooks ├── tests/ # pytest suite + fixtures (sample_repo, multilang) +│ └── eval/ # retrieval evaluation: harness, metrics, gen_queries, run_eval, +│ # baselines + run_baselines (public-repo comparison), results/ └── src/codebase_index/ ├── cli.py # Typer app: all commands (delegates to service.py) ├── service.py # shared CLI/MCP service layer: paths, search sessions, stats @@ -94,8 +98,8 @@ codebase-index/ │ # symbol_chunks.py, base.py ├── indexer/ # pipeline.py (full + incremental build), freshness.py, │ # doc_chunks.py - ├── graph/ # builder.py (edge resolution), expand.py (impact), - │ # export.py (HTML graph) + ├── graph/ # builder.py (edge resolution), expand.py (impact), analysis.py + │ # (communities/god nodes), navigate.py (path/describe), export.py ├── storage/ # db.py (pragmas, schema, version guard), schema.sql, repo.py ├── retrieval/ # intent.py, searchers.py, fusion.py, rerank.py, priors.py, │ # lexical.py, fuzzy.py, diversity.py, skeleton.py, @@ -158,8 +162,9 @@ upward to nearest `.git`/`.claude`), and `--quiet`. Search-family commands accep | `clean` | `--yes`, `--all` | resets index DB (`--all` wipes cache dir) | removed-count | | `watch` | `--debounce ms` | long-running | event log | -The skill only ever calls the **read-only** family (`search`, `symbol`, `refs`, `impact`, `diff-impact`, -`explain`, `stats`) plus `update`. It never calls `clean` or `init`. See SECURITY.md. +The skill only ever calls the **read-only** family (`search`, `explain`, `architecture`, `symbol`, +`refs`, `impact`, `diff-impact`, `path`, `describe`, `graph`, `stats`, `doctor`) plus `update` and +`index`. It never calls `clean`, `init` or `watch`. See [SECURITY_MODEL.md](SECURITY_MODEL.md). ### Freshness contract @@ -192,9 +197,9 @@ If `exists=false` → skill runs `index`. If `stale=true` and cheap → skill ru ## 8. MCP server -The same `retrieval` + `storage` layers are wrapped in a stdio MCP server exposing tools like -`search_code`, `find_symbol`, `find_refs`, `impact_of`, `impact_of_diff`, `explain_code`, `index_stats`, and -`healthcheck`. +The same `retrieval` + `storage` layers are wrapped in a stdio MCP server exposing +`healthcheck`, `search_code`, `find_symbol`, `find_refs`, `impact_of`, `impact_of_diff`, +`explain_code`, `architecture_overview`, `path_between`, `describe_symbol`, and `index_stats`. Current implementation: diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index e0d0d7a..6f371e3 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -1,185 +1,6 @@ # Database Schema -SQLite database structure for `codebase-index`. - -## Overview - -The index is stored in a single SQLite file: `.claude/cache/codebase-index/index.sqlite` - -The schema is defined in `src/codebase_index/storage/schema.sql` and applied on first initialization. - -## Tables - -### files - -Stores metadata about indexed files. - -| Column | Type | Description | -|---|---|---| -| `id` | INTEGER PRIMARY KEY | Auto-incrementing file ID | -| `path` | TEXT UNIQUE NOT NULL | Relative path from project root | -| `language` | TEXT | Detected language (python, typescript, etc.) | -| `size_bytes` | INTEGER | File size in bytes | -| `content_hash` | TEXT NOT NULL | SHA-256 hash for change detection | -| `indexed_at` | TEXT | ISO 8601 timestamp of last indexing | -| `is_generated` | INTEGER DEFAULT 0 | Whether file is auto-generated | - -### chunks - -Stores text chunks for FTS5 indexing. - -| Column | Type | Description | -|---|---|---| -| `id` | INTEGER PRIMARY KEY | Auto-incrementing chunk ID | -| `file_id` | INTEGER REFERENCES files(id) | Parent file | -| `chunk_index` | INTEGER | Position within the file | -| `line_start` | INTEGER | Starting line number (1-indexed) | -| `line_end` | INTEGER | Ending line number (1-indexed) | -| `text` | TEXT NOT NULL | Chunk text content | -| `token_estimate` | INTEGER | Approximate token count | - -### symbols - -Stores extracted symbols from AST parsing. - -| Column | Type | Description | -|---|---|---| -| `id` | INTEGER PRIMARY KEY | Auto-incrementing symbol ID | -| `file_id` | INTEGER REFERENCES files(id) | Defining file | -| `name` | TEXT NOT NULL | Symbol name | -| `kind` | TEXT | Type: class, function, method, variable, etc. | -| `line_start` | INTEGER | Definition start line | -| `line_end` | INTEGER | Definition end line | -| `signature` | TEXT | Function/class signature if available | -| `docstring` | TEXT | Extracted docstring if available | - -### edges - -Stores relationships between symbols and files. - -| Column | Type | Description | -|---|---|---| -| `id` | INTEGER PRIMARY KEY | Auto-incrementing edge ID | -| `source_id` | INTEGER REFERENCES symbols(id) | Source symbol (caller, importer) | -| `target_id` | INTEGER REFERENCES symbols(id) | Target symbol (callee, imported) | -| `edge_type` | TEXT | Type: call, import, reference, inheritance | -| `line` | INTEGER | Line where the edge occurs | - -### modules - -Stores module-level information for import resolution. - -| Column | Type | Description | -|---|---|---| -| `id` | INTEGER PRIMARY KEY | Auto-incrementing module ID | -| `file_id` | INTEGER REFERENCES files(id) | File containing the module | -| `module_path` | TEXT | Resolved module path | -| `exports` | TEXT | JSON array of exported symbol names | - -### fts_chunks - -FTS5 virtual table for full-text search (auto-managed by triggers). - -| Column | Type | Description | -|---|---|---| -| `text` | TEXT | Chunk text (indexed by FTS5) | -| `chunk_id` | INTEGER | References chunks(id) | - -### vec_chunks (optional) - -Vector embeddings for semantic search. Created **only** when `embeddings.enabled = true`, via the -`sqlite-vec` extension (a `vec0` virtual table). - -| Column | Type | Description | -|---|---|---| -| `chunk_id` | INTEGER PRIMARY KEY | References chunks(id) | -| `embedding` | FLOAT[dim] | Embedding vector; `dim` is fixed per build by the configured model | - -### vec_meta (optional) - -Records which embedding model/dimension produced the vectors currently in `vec_chunks`. - -| Column | Type | Description | -|---|---|---| -| `model` | TEXT | Embedding model identifier | -| `dim` | INTEGER | Vector dimension | -| `built_at` | TEXT | ISO 8601 timestamp of the embedding pass | - -### vec_cache (optional) - -Content-addressed embedding cache. `chunk_id`s churn on every full rebuild (chunks are deleted and -re-inserted), so this cache is keyed by `(model, content_sha)` instead — letting unchanged content -reuse its vector for free across rebuilds, so only new or changed text hits the backend. - -| Column | Type | Description | -|---|---|---| -| `model` | TEXT NOT NULL | Embedding model identifier | -| `content_sha` | TEXT NOT NULL | SHA-256 of the chunk content | -| `embedding` | BLOB NOT NULL | Pre-serialized float32 vector | - -Primary key: `(model, content_sha)`. - -### summaries - -Stores file-level summaries for quick overview. - -| Column | Type | Description | -|---|---|---| -| `file_id` | INTEGER PRIMARY KEY REFERENCES files(id) | Associated file | -| `summary` | TEXT | Auto-generated or manual summary | -| `top_symbols` | TEXT | JSON array of most important symbols | - -### metadata - -Stores index-level metadata. - -| Column | Type | Description | -|---|---|---| -| `key` | TEXT PRIMARY KEY | Metadata key | -| `value` | TEXT | Metadata value | - -Common keys: -- `schema_version` — current schema version (for migration checks) -- `last_indexed_at` — timestamp of last full index -- `total_files` — number of indexed files -- `total_symbols` — number of extracted symbols -- `total_chunks` — number of text chunks -- `config_hash` — hash of the configuration used for indexing - -## Indexes - -| Index | Table | Columns | Purpose | -|---|---|---|---| -| `idx_files_path` | files | path | Fast path lookup | -| `idx_files_hash` | files | content_hash | Change detection | -| `idx_symbols_name` | symbols | name | Symbol name lookup | -| `idx_symbols_file` | symbols | file_id | Symbols by file | -| `idx_edges_source` | edges | source_id | Outgoing edges | -| `idx_edges_target` | edges | target_id | Incoming edges | -| `idx_chunks_file` | chunks | file_id | Chunks by file | - -## FTS5 Configuration - -The `fts_chunks` virtual table uses: - -- **Tokenizer:** `unicode61` (default SQLite tokenizer) -- **Content:** `chunks` table (external content mode) -- **Triggers:** INSERT, UPDATE, DELETE triggers keep FTS in sync with `chunks` - -### Query Syntax - -FTS5 supports: -- Phrase matching: `"exact phrase"` -- Prefix matching: `auth*` -- Boolean: `auth AND login`, `auth OR login`, `auth NOT test` -- NEAR: `auth NEAR/5 login` - -## Schema Migrations - -The `metadata.schema_version` key tracks the current schema version. On initialization: - -1. If the database doesn't exist, create it with the latest schema. -2. If the database exists, check `schema_version`: - - If equal to current, proceed. - - If less than current, run migrations (not yet implemented). - - If greater than current, refuse to open (future version protection). +This page moved. The canonical, code-verified schema reference is +[SCHEMA.md](SCHEMA.md), which mirrors `src/codebase_index/storage/schema.sql` +(tables `files`, `symbols`, `chunks`, `edges` with a `confidence` column, `modules`, +`meta`, the `fts_chunks` FTS5 table, and the opt-in `vec_*` tables). diff --git a/docs/FAQ.md b/docs/FAQ.md index 1e25c0d..c381aa2 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -13,8 +13,8 @@ or `pipx` (isolated): pip install codebase-index # or: pipx install codebase-index ``` -To pin an exact version or grab an unreleased commit, install from a GitHub tag -instead: `pip install "codebase-index @ git+https://github.com/denfry/codebase-index.git@v1.8.0"`. +To pin an exact version: `pip install codebase-index==1.9.0`. To try an unreleased +commit, install from git: `pip install "codebase-index @ git+https://github.com/denfry/codebase-index.git@main"`. Then run `codebase-index init` inside your project and `codebase-index index` to build the first index. In Claude Code you can instead install the plugin @@ -74,16 +74,12 @@ Yes. Run: codebase-index mcp --root /path/to/repo ``` -The stdio MCP server exposes: +The stdio MCP server exposes eleven tools (`src/codebase_index/mcp/server.py`): -- `healthcheck` -- `search_code` -- `find_symbol` -- `find_refs` -- `impact_of` -- `impact_of_diff` -- `explain_code` -- `index_stats` +- `healthcheck`, `index_stats` +- `search_code`, `explain_code`, `find_symbol`, `find_refs` +- `impact_of`, `impact_of_diff` +- `architecture_overview`, `path_between`, `describe_symbol` See [MCP.md](MCP.md) for schema and client config templates. @@ -158,27 +154,41 @@ Yes. Use any of these methods: 1. **`.codeindexignore`** — Tool-specific ignore file (highest priority) 2. **`.gitignore`** — Standard git ignore file 3. **`.claudeignore`** — Claude-specific ignore file -4. **Configuration** — `extra_ignore` patterns in `.codeindex.json` +4. **Configuration** — `extra_ignore` patterns in + `.claude/cache/codebase-index/config.json` (written by `init`; see + `examples/config.example.json`) ## Is it production-ready? -Yes — `codebase-index` is released as **v1.8.0**. The core indexing and search -functionality is implemented and tested. The current `1.8.0` package includes: - -- Hybrid FTS/path/symbol/vector retrieval with benchmark-calibrated lexical expansion - and bounded fuzzy identifier matching -- Import/call/reference graph expansion, intent-directed graph discovery, and `impact` -- Diff-aware blast-radius analysis for tracked working-tree changes -- Optional local embeddings, with external embeddings gated behind explicit opt-in -- Hooks and watch mode for freshness -- Multi-CLI setup for Claude Code, Codex CLI, and OpenCode - -Known gaps: the public benchmark suite is still small, the MCP server needs -verified client-specific docs and progressive/paged results, and the graph is -closer to an import/call/reference graph than a full framework-aware code -intelligence graph. - -See [ROADMAP.md](../ROADMAP.md) for the full milestone plan. +Yes, with the caveats below. The current line is **1.9.x** (see +[CHANGELOG.md](../CHANGELOG.md)). It ships: + +- Hybrid FTS5 / path / symbol retrieval with optional local embeddings; rank fusion that + scores cross-retriever agreement at file level; evidence-calibrated source priors, + lexical expansion, and fuzzy identifier fallback (1.8.0, 1.9.0). +- Tree-sitter symbols for twelve Tier-A languages; import / call / reference / + inheritance graph with per-edge confidence; `refs`, `path`, `describe`, + `architecture`, `impact`, and diff-aware `diff-impact` (1.5.0, 1.7.0). +- Token-budgeted, skeletonized retrieval packets with bounded `recommended_reads`. +- CLI, Claude Code skill and plugin, Codex CLI and OpenCode resources, and a stdio MCP + server sharing one service layer. +- A reproducible retrieval evaluation with leak-free git-derived ground truth, + multi-corpus pooling, and paired significance tests; every 1.9.0 ranking change had + to pass it. + +Known gaps: the graph is import/call/reference-level rather than framework-aware, MCP +client configs are templates not yet verified against each client release, and there +is no LLM-driven task-level evaluation yet. See [ROADMAP.md](ROADMAP.md). + +## How does it differ from Aider's repo-map, Cursor, or plain grep? + +Grep is exact text matching with no ranking, symbol awareness or read plan. A repo map +is a query-independent context blob fed to one agent. Cursor is an IDE with its own +proprietary index. `codebase-index` is a queryable, local, scriptable retrieval and +graph layer any shell-capable or MCP agent can call. The trade-offs, including when +each alternative is the better choice, are in [COMPARISON.md](COMPARISON.md); the +measured comparison against grep-style and repo-map-style baselines is in +[BENCHMARKS.md](BENCHMARKS.md). ## How do I contribute? diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 695cf68..ca1e417 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -64,10 +64,13 @@ ln -s ~/codebase-index/skill ~/.claude/skills/codebase-index # From PyPI (recommended) pip install codebase-index pipx install codebase-index # isolated environment -uv tool install codebase-index # uv-managed tool +uv tool install codebase-index # uv-managed tool (standard PyPI path; not part of CI yet) -# Pin to a GitHub tag for an exact or unreleased version -pip install "codebase-index @ git+https://github.com/denfry/codebase-index.git@v1.8.0" +# Pin an exact release +pip install codebase-index==1.9.0 + +# Or install an unreleased commit straight from git +pip install "codebase-index @ git+https://github.com/denfry/codebase-index.git@main" # From source (editable mode) git clone https://github.com/denfry/codebase-index.git @@ -90,10 +93,11 @@ pip install -e ".[embeddings-local,watch,dev]" ### uvx / Homebrew status -As of `1.8.0`, **PyPI is shipped** — `pip install codebase-index` and -`pipx install codebase-index` are the verified paths. `uvx codebase-index init`, -Homebrew tap installation, signed checksums, and SBOMs remain distribution -targets for a more complete release story. +As of `1.9.0`, **PyPI is shipped** — `pip install codebase-index` and +`pipx install codebase-index` are the paths exercised by the release smoke test. +`uv tool install` / `uvx` should work because the package is a normal PyPI wheel, but +they are not verified in CI. Homebrew tap installation, signed checksums, and SBOMs +remain distribution targets. Target future commands: @@ -124,21 +128,20 @@ codebase-index --help codebase-index doctor ``` -Expected output: +Real output (1.9.0, in a freshly cloned repository before `init`): ``` -=== codebase-index Doctor === - -[OK] Python 3.12 (requires 3.11+) -[OK] codebase-index package installed (v1.8.0) -[OK] tree-sitter is available -[INFO] Cache directory not yet created: ... -[INFO] Skill not installed in .claude/skills/ -[INFO] No config file (using defaults) - -All checks passed. +!! [high] cache_gitignored: add '.claude/cache/codebase-index/' to .gitignore (run `init`) +-- [info] hooks_enabled: no auto-update hook (run `init --with-hooks`) +OK [medium] index_fresh: index is fresh +OK [medium] symbol_extraction: tree-sitter languages extract symbols +OK [info] graph_coverage: all indexed languages have full dependency-graph support ``` +`!!` marks a failed high-severity check (`doctor --strict` exits non-zero on those), +`--` a failed lower-severity one, `OK` a pass. Running `codebase-index init` clears the +first finding. Package and Python versions: `pip show codebase-index`. + ## Claude Code Setup After installing the Python package, ensure the skill is available to Claude Code: @@ -269,7 +272,7 @@ codebase-index index If `doctor` warns about external embeddings, check your config: ```bash -cat .codeindex.json | grep allow_external +grep allow_external .claude/cache/codebase-index/config.json ``` Set `allow_external` to `false` to disable external API calls. diff --git a/docs/MCP.md b/docs/MCP.md index b89dd1d..8029c4e 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -126,7 +126,10 @@ Use the client's MCP server configuration UI or JSON file and register: ``` Client-specific config file paths and screenshots should be added only after -they are verified against the current client versions. +they are verified against the current client versions. The templates above are +the standard stdio-server shape every listed client accepts; none has been +re-verified against a specific client release yet (tracked in +[ROADMAP.md](ROADMAP.md)). ## Progressive results diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md index 2ae5744..748dc5b 100644 --- a/docs/QUICKSTART.md +++ b/docs/QUICKSTART.md @@ -48,40 +48,47 @@ This creates the cache directory, configuration, and the selected CLI instructio codebase-index index ``` -You should see output like: +The examples below are real output from indexing +[pallets/flask](https://github.com/pallets/flask) at commit `d318b683` with +codebase-index 1.9.0: ``` -Indexing... - Discovered 142 files (excluded 23 sensitive/generated) - Extracted 891 symbols - Built 2,340 chunks - Index built in 3.2s +Indexed 230 files (0 pruned). + parse failures: 0; tree-sitter files with 0 symbols: 17 ``` ## Step 4: Run Your First Search ```bash -codebase-index search "where is authentication implemented?" +codebase-index search "where is the session cookie signed and saved" --limit 5 ``` -Expected output: - ``` -Top matches: -┌──────┬──────────────────────────┬──────────────────┬───────┬────────────────────────────┐ -│ Rank │ Path │ Symbols │ Score │ Reason │ -├──────┼──────────────────────────┼──────────────────┼───────┼────────────────────────────┤ -│ 1 │ src/auth/AuthService.ts │ AuthService │ 0.92 │ exact symbol match │ -│ 2 │ src/routes/auth.ts │ login, logout │ 0.78 │ FTS match · 4 callers │ -│ 3 │ src/middleware/auth.ts │ requireAuth │ 0.65 │ path match · FTS match │ -└──────┴──────────────────────────┴──────────────────┴───────┴────────────────────────────┘ - -Recommended reads: - 1. src/auth/AuthService.ts:12-148 - 2. src/routes/auth.ts:20-91 - 3. src/middleware/auth.ts:5-42 +**Query:** where is the session cookie signed and saved +**Intent:** `locate_impl` · **Confidence:** medium + +| # | Path | Lines | Reason | +|---|------|-------|--------| +| 1 | `src/flask/sessions.py` | 24-54 | in src/flask/ · 2 callers · source prior +0.08 | +| 2 | `src/flask/sessions.py` | 284-385 | in src/flask/ · 2 callers · source prior +0.08 | +| 3 | `src/flask/sessions.py` | 57-80 | in src/flask/ · 1 callers · source prior +0.08 | +| 4 | `tests/test_basic.py` | 542-600 | source prior -0.06 · generated/test demoted | +| 5 | `tests/test_reqctx.py` | 201-249 | source prior -0.06 · generated/test demoted | + +`src/flask/sessions.py:284-385` +``` +class SecureCookieSessionInterface(SessionInterface): +``` +... + +**Recommended reads:** +- `src/flask/sessions.py:24-54` +- `src/flask/sessions.py:284-385` ``` +Rank 2 is the class that signs and saves the cookie. Add `--json` for the +machine-readable packet agents consume. + ## Step 5: Use with Your AI CLI When installed for Claude Code, Codex CLI, or OpenCode, the generated instructions @@ -100,14 +107,18 @@ The agent will: ## Interpreting Results -Each result includes: - -- **Rank** — position in the result list (start with 1-3) -- **Path** — file location -- **Symbols** — extracted symbols in the matched region -- **Score** — relevance score (0.0 to 1.0) -- **Reason** — why this result ranked (e.g., "exact symbol match") -- **Recommended reads** — exact line ranges to open +- **Intent** — how the query was classified (`locate_impl`, `how_it_works`, `impact`, ...); + it selects the retriever mix. +- **Confidence** — `high`: answer from the evidence; `medium`: read the recommended + ranges and confirm the key claim; `low`: follow the fallback suggestions (ripgrep + patterns) instead of trusting the list. +- **#, Path, Lines** — rank and the exact line range that matched. Start with ranks 1–3. +- **Reason** — why it ranked: exact symbol match, callers, path match, and the source + prior (implementation code is preferred over tests and documentation). +- **Snippets** — budgeted, often skeletonized (unrelated body lines folded) and + secret-redacted. +- **Recommended reads** — the read plan: exact ranges to open, capped at 120 lines + each (`truncated: true` in JSON when a longer symbol was cut at its head). ## What Success Looks Like diff --git a/docs/RETRIEVAL.md b/docs/RETRIEVAL.md index e375032..de233b3 100644 --- a/docs/RETRIEVAL.md +++ b/docs/RETRIEVAL.md @@ -1,5 +1,7 @@ # Retrieval Pipeline +(`docs/RETRIEVAL_PIPELINE.md` was merged into this page.) + The retrieval engine turns a natural-language or symbolic query into a **compact, ranked, token-budgeted** set of file/line ranges for Claude to read. It is hybrid: multiple independent retrievers run, their results are fused and reranked, then trimmed. Graph expansion and MMR are @@ -70,10 +72,12 @@ source)` list so fusion is source-agnostic. `fuzzy_fallback_min` rows. Measured over 305 queries on three repositories it moved no ranking metric while costing ~20% of query latency, so it is kept for typos and abbreviations but no longer runs when the query already spelled its identifier correctly. -- **FTS** — FTS5 `bm25()` over the `fts_chunks` virtual table (chunk text + symbol names + - summaries indexed). Query-time camelCase/snake_case splitting, small down-weighted synonym - expansion, and soft coverage scoring make natural-language questions robust without weakening - exact terms. +- **FTS** — FTS5 `bm25()` over the `fts_chunks` virtual table (chunk text + symbol names + indexed). Query-time camelCase/snake_case splitting, a small down-weighted synonym/inflection + vocabulary, OR-groups for soft matching with bounded term coverage, and ranking by + original-term coverage with BM25 as a tie-break make natural-language questions robust without + weakening exact terms. Every FTS term is quoted so query punctuation cannot inject MATCH + operators. - **Vector** *(opt-in)* — cosine similarity over chunk embeddings via `sqlite-vec`. Only runs if `embeddings.enabled = true`. Adds semantic recall for paraphrased queries. Absent → pipeline degrades gracefully to FTS+symbol. @@ -191,13 +195,18 @@ The point: Claude gets enough to decide, and a precise list of what to read next ## 8. Confidence & fallback -A `confidence` score (high/medium/low) is derived from: top RRF score, score gap between #1 and #2, -number of agreeing retrievers, and whether a symbol matched exactly. +A categorical `confidence` (high/medium/low) is derived from exact-symbol evidence, +multi-retriever agreement, score separation between #1 and #2, and result count. An exact symbol +match is `high`, including a single-result response. - **high** → Claude reads `recommended_reads` and answers. - **medium** → Claude reads, but may verify with one Grep. - **low** → skill instructs Claude to **fall back** to `ripgrep`/Grep/Glob with suggested patterns - emitted in `fallback_suggestions` (derived from query terms + detected symbols). + emitted in `fallback_suggestions` (derived from query terms + detected symbols): `rg` patterns, + likely paths, and query-broadening hints. + +Default token budget: 1500 for `search`, 2200 for `explain`, configurable per project in +`.claude/cache/codebase-index/config.json` (`retrieval.token_budget`). ## 9. Output payload (shared by Markdown + JSON) diff --git a/docs/RETRIEVAL_PIPELINE.md b/docs/RETRIEVAL_PIPELINE.md index 5bf0944..b03ddfd 100644 --- a/docs/RETRIEVAL_PIPELINE.md +++ b/docs/RETRIEVAL_PIPELINE.md @@ -1,179 +1,5 @@ # Retrieval Pipeline -How `codebase-index` finds and ranks relevant code for a query. - -## Overview - -The retrieval pipeline combines multiple search strategies into a single ranked result set. - -``` -User query - ↓ -Intent detection (keyword / symbol / impact / general) - ↓ -┌─────────────────────────────────────────┐ -│ Parallel Retrievers │ -├─────────────────────────────────────────┤ -│ 1. Exact symbol match │ -│ 2. Path-based search │ -│ 3. SQLite FTS5 lexical search │ -│ 4. Vector search (optional embeddings) │ -│ 5. Graph expansion (explicit opt-in) │ -└─────────────────────────────────────────┘ - ↓ -Reciprocal Rank Fusion (RRF) - ↓ -Reranking (boosts for symbol match, recency, file type) - ↓ -Token budget enforcement - ↓ -Ranked retrieval packet with confidence score -``` - -## 1. Exact Symbol Match - -**Trigger:** Query matches a known symbol name exactly or with minor variation. - -**Process:** -- Look up the symbol in the `symbols` table -- Return the definition file and line range -- Include all reference locations from the `edges` table - -**Score boost:** Highest priority — exact symbol matches are ranked first. - -## 2. Path-Based Search - -**Trigger:** Query contains file path fragments or recognizable path patterns. - -**Process:** -- Match query terms against file paths in the `files` table -- Use substring matching with path segment awareness - -**Score boost:** Moderate — path matches indicate the user knows where to look. - -## 3. SQLite FTS5 Lexical Search - -**Trigger:** General keyword and natural-language queries. - -**Process:** -- Parse identifiers into camelCase/PascalCase/snake_case subtokens. -- Add a small, explicit synonym/inflection vocabulary at lower weight. -- Use OR groups for soft matching, then require bounded term coverage and rank - by original-term coverage plus BM25. -- Quote every FTS term so query punctuation cannot inject MATCH operators. - -**Score:** Coverage is the primary signal; BM25 is a bounded tie-break. - -## 4. Vector Search (Optional) - -**Trigger:** Enabled when `embeddings.backend` is not "noop". - -**Process:** -- Embed the query using the configured backend -- Search `vec_chunks` for nearest neighbors -- Return chunks with cosine similarity scores - -**Score:** Cosine similarity (0.0 to 1.0). - -> **Indexing note:** chunk embeddings are reused across rebuilds via a content-addressed -> `vec_cache` (keyed by model + content SHA-256), so only new or changed chunks are re-embedded. -> See [DATABASE_SCHEMA.md](DATABASE_SCHEMA.md) and [SCHEMA.md](SCHEMA.md) for details. - -## 5. Graph Expansion - -**Trigger:** `RetrievalTuning(graph_source=True)` and an intent plan with a -graph strategy. It is disabled in the shipped default because the reproducible -self-repository ablation reduced direct-hit MRR. - -**Process:** -- Seed from lexical/symbol candidates already found in SQLite. -- Traverse only indexed, resolved edges with bounded depth and node count. -- Follow `up` (callers/importers), `down` (callees/imports), or `both` - according to the intent plan. -- Apply distance decay and preserve edge confidence in the candidate reason. - -Graph expansion is an opt-in context-discovery signal, not a replacement for -direct lexical or symbol evidence. - -## 6. Diversity and duplicate control - -SimHash suppresses near-duplicate snippets independently of MMR. It does not move -ranking metrics; it earns its place by taking the duplicate rate of returned -snippets from ~1.6% to ~0%. Bounded Maximal Marginal Relevance is available -through `RetrievalTuning(mmr=True)`; the shipped default keeps relevance-only -ordering because MMR moved no metric and roughly doubled p50 latency. - -The over-fetch that feeds these selection stages is `candidate_pool_multiplier` -(default 2), an explicit knob rather than a side effect of enabling dedup. - -## 7. Reciprocal Rank Fusion (RRF) - -Combines ranked lists from the enabled retrievers: - -``` -RRF_score(d) = Σ_r w_r · k / (k + rank_r(d)) -``` - -The implementation multiplies textbook RRF by `k` so fusion and bounded rerank -bonuses share a comparable scale; ordering is unchanged. - -Where: -- `k` is a constant (default 60) -- `rank_r(d)` is the rank of document `d` in retriever `r` -- `w_r` is the intent/tuning weight for retriever `r` - -Candidates are keyed by `(path, line-bucket)`, so co-located hits merge. Because a -symbol definition and a lexical hit in the same file are often *not* co-located, -each candidate additionally receives — at `file_agreement_weight` (0.4) — the RRF -mass of every retriever that found its file at a different locator. Retrievers -already counted at that locator are excluded, so nothing double-counts. Without -this, two retrievers agreeing on a file produced two weak candidates instead of one -strong one, and cross-source agreement never affected the score. - -## 8. Reranking - -After fusion, apply bounded explainable boosts and penalties: - -| Factor | Effect | Rationale | -|---|---:|---| -| Exact symbol match | +0.20 | User named a specific symbol | -| Symbol definition kind | +0.05 | Prefer actionable definitions | -| Path term match | +0.05 | User supplied a location clue | -| Degree / reference evidence | up to +0.08 | Stable structural tiebreaker | -| Implementation source prior | +0.08 | Prefer source over prose/tests | -| Documentation source prior | -0.20 | Prose describing a feature outmatches the code lexically | -| Generated/vendor/build | -0.25 | Suppress low-value derived code | -| Test path on non-test query | -0.06 | Keep tests as supporting evidence | - -## 9. Confidence - -Confidence is categorical (`high`, `medium`, `low`) and is derived from -exact-symbol evidence, multi-retriever agreement, score separation, and result -count. Exact symbol matches are `high`, including a single-result response. - -| Confidence | Action | -|---|---| -| `high` | Read recommended ranges and answer directly | -| `medium` | Read ranges; optionally confirm with one Grep | -| `low` | Use fallback suggestions (ripgrep, Glob) | - -## 10. Token Budget Enforcement - -The output is capped at a configurable token budget: - -1. Results are sorted by final score -2. Snippets are included until the budget is reached -3. Remaining results are listed without snippets -4. The `recommended_reads` field contains only the most critical line ranges - -Default budget: 1500 tokens (configurable in `.codeindex.json`). - -## 11. Fallback Suggestions - -When confidence is low, the pipeline generates fallback strategies: - -- **ripgrep patterns:** Extracted keywords from the query, formatted for `rg` -- **likely paths:** Common directories to search based on query terms -- **broaden query:** Suggestions for rewording the query - -These are included in the response so Claude can fall back gracefully. +This page moved. The canonical description of intent detection, the retrievers, RRF fusion with +file agreement, reranking and source priors, graph expansion, diversity control, token budgeting +and confidence/fallback is [RETRIEVAL.md](RETRIEVAL.md). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index fa67701..57ae43b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -46,13 +46,18 @@ release in `CHANGELOG.md`. The current priority is to make existing power obvious, measurable, and easy to invoke. -- [ ] Publish 10k and 100k LOC public-repository benchmark runs with raw logs. +- [~] Publish public-repository benchmark runs with raw logs. **Partially done:** + `tests/eval/run_baselines.py` compares the index with grep-style and + repo-map-style baselines on Flask (~18k LOC Python), Gson (~57k LOC Java) + and Fastify (~78k LOC JavaScript) at pinned commits, with a logged run under + `tests/eval/results/`. Remaining: a 1M-LOC / large-monorepo run. - [ ] Add task-level agent evaluation: success rate, tokens, files read, time, and citation correctness. - [ ] Verify MCP setup against current releases of each documented client. - [ ] Add progressive/paged MCP retrieval for large repositories. - [ ] Complete `uvx` and clean-machine install verification on every CI OS. - [ ] Publish signed checksums and an SBOM with releases. +- [ ] Homebrew tap (`brew install denfry/tap/codebase-index`). ## Next — task-native context diff --git a/docs/SCHEMA.md b/docs/SCHEMA.md index 490dc85..4726a54 100644 --- a/docs/SCHEMA.md +++ b/docs/SCHEMA.md @@ -1,7 +1,8 @@ # Database Schema Single SQLite database at `.claude/cache/codebase-index/index.sqlite`. WAL mode, foreign keys on. -The DDL below is the canonical source mirrored by `src/codebase_index/storage/schema.sql`. +`src/codebase_index/storage/schema.sql` is the applied DDL; this page mirrors it and explains it. +(`docs/DATABASE_SCHEMA.md` was merged into this page.) ## Pragmas @@ -87,6 +88,7 @@ CREATE INDEX idx_edges_src ON edges(src_kind, src_id); CREATE INDEX idx_edges_dst ON edges(dst_kind, dst_id); CREATE INDEX idx_edges_name ON edges(dst_name); CREATE INDEX idx_edges_type ON edges(edge_type); +CREATE INDEX idx_edges_file ON edges(file_id); -- replace_edges deletes per file on every update -- Module / package level summaries for architecture-intent queries. CREATE TABLE modules ( @@ -158,10 +160,21 @@ backend; everything else is copied straight from the cache into `vec_chunks`. Ne are written back to `vec_cache` so subsequent rebuilds reuse them. The reported "embedded" count reflects cache **misses** — i.e. the work actually performed. -## Migrations +## Schema versioning -`meta.schema_version` gates migrations in `storage/db.py`. On version mismatch the CLI either -migrates in place (for additive changes) or, for breaking changes, prompts a `index --rebuild`. +`meta.schema_version` is guarded in `storage/db.py`. There is no in-place migration framework: + +- a **newer** index than the installed CLI supports is refused on open with a message asking for + an updated CLI; +- an **older** index is still readable (queries never touch columns added later); the build + commands (`index`, `update`) detect the mismatch with `peek_schema_version` and rebuild from + scratch, because `schema.sql` is applied with `IF NOT EXISTS` and old tables would persist. + +## FTS5 query syntax + +FTS5 supports phrase matching (`"exact phrase"`), prefix matching (`auth*`), boolean operators +(`auth AND login`, `auth OR login`, `auth NOT test`) and `NEAR/5`. The lexical retriever quotes +every term it derives from the query, so user punctuation cannot inject MATCH operators. ## Incremental indexing keys diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 8e4909a..88123af 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -1,119 +1,7 @@ -# Security Model +# Security -`codebase-index` is **local-first and offline by default**. Its threat model assumes the indexed -repository may contain secrets and that a skill must not exfiltrate code or run dangerous commands. +This page moved. -> **Trust model in 60 seconds** -> 1. **Offline by default** — the base install has zero network dependencies; nothing leaves your machine (§1, §4). -> 2. **One opt-in exit, triple-gated** — external embeddings require `allow_external` **and** an env API key **and** a printed endpoint warning, or they are refused (§4). -> 3. **Secrets never get in** — `.env`, keys, certs, and credential files are excluded before parsing (§2). -> 4. **Secrets never get out** — every snippet is redacted before it reaches the agent (§3). -> 5. **No telemetry, ever** — no analytics, no phone-home, no usage data. -> 6. **Verify it yourself** — `codebase-index doctor --strict` audits all of the above and gates CI (§6). -> -> The same callout appears in the README so the trust story is identical wherever a reader lands. - -## 1. Principles - -1. **Local-first** — index, query, and storage all happen on the user's machine. -2. **No network by default** — the base install has no network dependency. The only code path that - can leave the machine is an *external embedding API*, which is **opt-in and off by default**. -3. **Never index sensitive material** — secrets, `.env`, keys, certs, build/dependency/binary/ - generated/huge files are excluded before parsing. -4. **Redact secrets in output** — even indexed snippets are scrubbed before being shown to Claude. -5. **Respect ignore files** — `.gitignore`, `.cursorignore`, `.claudeignore`, `.codeindexignore`. -6. **Minimal, safe tool surface** — the skill's `allowed-tools` only permits the read-only CLI - subcommands and read-only fallbacks (ripgrep/Grep/Glob). No `clean`, no arbitrary shell. -7. **Workspace trust** — indexing executes parsers over repo content; treat indexing an untrusted - repo as you would opening it in an editor. `doctor` warns before risky operations. - -## 2. Exclusion pipeline (`discovery/`) - -A file must pass **every** gate to be indexed: - -| Gate | Rule | -|---|---| -| Ignore files | Not matched by `.gitignore` / `.cursorignore` / `.claudeignore` / `.codeindexignore` | -| Built-in denylist | Not in `node_modules`, `.venv`, `dist`, `build`, `target`, `.git`, `vendor`, `__pycache__`, etc. | -| Secret filenames | Not `.env*`, `*.pem`, `*.key`, `*.p12`, `*.pfx`, `id_rsa*`, `*.crt`, `*.keystore`, `credentials*`, `secrets*` | -| Binary | No NUL bytes / not a known binary extension (images, archives, fonts, compiled artifacts) | -| Size | `size_bytes <= max_file_bytes` (default 1 MB) | -| Generated | Not matched by generated-file patterns (`*.min.js`, `*.lock`, `*.pb.go`, `*_pb2.py`, `*.generated.*`) — indexed as summary-only at most | - -`.codeindexignore` is the tool's **own** ignore file (highest specificity) so users can exclude -paths from indexing without affecting git or other tools. - -## 3. Secret redaction (`output/` + parsers) - -Two layers: - -- **At index time** — files that *look* like secret stores are excluded entirely (above). -- **At output time** — every snippet is passed through a redactor before emission. Patterns: - - high-entropy strings assigned to keys named `*key*`, `*secret*`, `*token*`, `*password*`, `*api*` - - common formats: AWS keys (`AKIA...`), private key headers (`-----BEGIN ... PRIVATE KEY-----`), - JWTs, bearer tokens, connection strings with credentials, `xox[baprs]-` Slack tokens. - - Matches are replaced with `«redacted:»`, preserving line numbers. - -Redaction is conservative: it never widens the snippet, only masks within it. - -## 4. Embeddings & network - -- Default: `embeddings.backend = "noop"` (disabled) — pure lexical+symbol+graph search. -- `embeddings.backend = "local"` → on-device model (e.g. sentence-transformers). Still no network - at query time (model downloaded once at setup, which the user initiates explicitly). -- `embeddings.backend = "external"` → sends chunk text to a configured API. This requires: - - explicit `embeddings.allow_external = true` in config, **and** - - an env-provided API key, **and** - - `doctor` and `index` both print a clear warning naming the endpoint. - Without all three, external embedding is refused. - -## 5. Skill tool surface - -`SKILL.md` declares a narrow `allowed-tools`: - -```yaml -allowed-tools: - - Bash(codebase-index search:*) - - Bash(codebase-index explain:*) - - Bash(codebase-index symbol:*) - - Bash(codebase-index refs:*) - - Bash(codebase-index impact:*) - - Bash(codebase-index diff-impact:*) - - Bash(codebase-index architecture:*) - - Bash(codebase-index path:*) - - Bash(codebase-index describe:*) - - Bash(codebase-index graph:*) - - Bash(codebase-index stats:*) - - Bash(codebase-index doctor:*) - - Bash(codebase-index update:*) - - Bash(codebase-index index:*) - - Grep - - Glob -``` - -Explicitly **not** allowed via the skill: `clean`, `init`, `watch`, an unscoped -`codebase-index *`, `python -m codebase_index *`, or unscoped `Bash`. -Destructive or scaffolding actions remain a human/manual decision. The wrapper -scripts (`scripts/cbx`) only forward to the installed `codebase-index` binary -and reject unknown subcommands. - -## 6. `doctor` — safety self-check - -`codebase-index doctor` (and `--strict` for CI) reports: - -- whether the cache is inside `.gitignore` (warns if the index could be committed) -- whether external embeddings are enabled and to which endpoint -- any indexed file that matches a secret pattern (should be none → flags a leak) -- ignore-file coverage and any oversized/binary files that slipped through -- the resolved `allowed-tools` vs. the recommended minimal set -- world-writable cache directory permissions - -`doctor` exits non-zero under `--strict` if any high-severity finding is present, so it can gate CI. - -## 7. What the skill must NOT do - -- Must not run `codebase-index index --rebuild` automatically on huge/untrusted repos without the - freshness check indicating it's needed. -- Must not echo raw `.env`/secret file contents even if a user pastes a path — the CLI refuses to - read excluded files for snippet output. -- Must not enable embeddings or any network path on its own. +- Trust model, exclusion pipeline, redaction, embeddings/network policy, skill tool surface and + what `doctor` checks: [SECURITY_MODEL.md](SECURITY_MODEL.md). +- Reporting a vulnerability and supported versions: the root [SECURITY.md](../SECURITY.md). diff --git a/docs/SECURITY_MODEL.md b/docs/SECURITY_MODEL.md index e383c11..abc96af 100644 --- a/docs/SECURITY_MODEL.md +++ b/docs/SECURITY_MODEL.md @@ -63,8 +63,20 @@ External embeddings require **all three** conditions: Without all three, external embedding is refused. +## Skill tool surface + +The generated `SKILL.md` frontmatter restricts the agent to read-only subcommands +(`search`, `explain`, `architecture`, `symbol`, `refs`, `impact`, `diff-impact`, `path`, +`describe`, `graph`, `stats`, `doctor`, `update`, `index`, and the `cbx` wrapper) plus `Read`, +`Grep`, `Glob`. `clean`, `init`, `watch`, unscoped `Bash`, and `python -m codebase_index` are not +allowed. The `cbx` wrappers (skill `scripts/cbx` and plugin `bin/cbx`) enforce the same whitelist +and refuse other subcommands. + ## Threat Model +(`docs/SECURITY.md` was merged into this page; the reporting policy lives in the root +[SECURITY.md](../SECURITY.md).) + ### Trusted Inputs - The user's own codebase (they control what's in it) - Configuration files they create @@ -83,29 +95,32 @@ Without all three, external embedding is refused. | Malicious file content | Parsers are read-only; no code execution | | Cache leakage | Cache in `.gitignore`; doctor checks permissions | -## Unsafe Patterns - -- **Do not commit the SQLite index** to a shared repository. It contains code snippets. -- **Do not enable external embeddings** on repositories containing proprietary or regulated code without reviewing your organization's data handling policies. -- **Do not run `codebase-index index`** on repositories you do not trust without reviewing the `doctor` output first. +## `doctor` — safety self-check -## Hook Risks +`codebase-index doctor` (`src/codebase_index/doctor.py`) reports exactly these findings today: -Optional hooks (e.g., post-tool-use auto-update) execute CLI commands automatically. Ensure: +| id | severity | what it checks | +|---|---|---| +| `cache_gitignored` | high | the derived index directory is in `.gitignore` (a committed index leaks indexed text) | +| `hooks_enabled` | info | whether the Claude Code PostToolUse auto-update hook is configured | +| `index_fresh` | medium | the index exists and is not stale relative to the tree | +| `symbol_extraction` | medium | Tree-sitter languages actually produce symbols (guards silent parser failure) | +| `graph_coverage` | info | every indexed language has dependency-graph support, or names the partial ones | -- Hook commands are read-only or safe (`codebase-index update --quiet`) -- Hook commands do not contain user-controlled input -- Hook output is not echoed to the user unless necessary +`--strict` exits non-zero when a high-severity finding fails, so it can gate CI. The external- +embedding warning (naming the endpoint) is printed by `index` and `search` when the backend is +resolved, not by `doctor`. Secret-pattern scans of the index, `allowed-tools` diffs and +cache-permission checks are **not** implemented; do not rely on `doctor` for them. -## `doctor` — Safety Self-Check +## Hook risks -`codebase-index doctor` reports: +Optional hooks (the PostToolUse auto-update) execute CLI commands automatically. Keep hook +commands read-only (`codebase-index update --quiet`), free of user-controlled input, and quiet. -- Whether the cache is inside `.gitignore` (warns if the index could be committed) -- Whether external embeddings are enabled and to which endpoint -- Any indexed file that matches a secret pattern (should be none) -- Ignore-file coverage and any oversized/binary files that slipped through -- The resolved `allowed-tools` vs. the recommended minimal set -- World-writable cache directory permissions +## Unsafe patterns -With `--strict` flag, `doctor` exits non-zero if any high-severity finding is present, suitable for CI gating. +- Do not commit the SQLite index to a shared repository: it stores indexed text, and redaction + happens only at output time. +- Do not enable external embeddings on proprietary or regulated code without reviewing your + organisation's data-handling policy. +- Review `doctor` output before indexing a repository you do not trust. diff --git a/docs/installer.md b/docs/installer.md index 10fddd1..fd8bd05 100644 --- a/docs/installer.md +++ b/docs/installer.md @@ -1,29 +1,32 @@ -# Multi-CLI Installer для codebase-index +# Multi-CLI installer for codebase-index -Единый GitHub-hosted installer, который раскладывает skill **codebase-index** -сразу в несколько AI-CLI сред: **Claude Code**, **Codex CLI** и **OpenCode**. +A single GitHub-hosted installer that lays out the **codebase-index** skill into several +AI-CLI environments at once: **Claude Code**, **Codex CLI** and **OpenCode**. -Архитектура: один entrypoint (`install.sh` / `install.ps1`) + отдельные -адаптеры под каждую среду (`adapters/.{sh,ps1}`). Источник правды — -каталог `skill/` в репозитории. Никакой «магии»: все пути логируются и -переопределяются. +Architecture: one entrypoint (`install.sh` / `install.ps1`) plus one adapter per environment +(`adapters/.{sh,ps1}`). The source of truth is the `skill/` directory in the +repository. No magic: every path is logged and can be overridden. + +> Most users should prefer `pip install codebase-index` followed by `codebase-index init` +> (see [INSTALLATION.md](INSTALLATION.md)). The shell installer exists for environments where +> the skill files need to be placed globally without a Python package install first. --- -## Что устанавливается +## What gets installed -| CLI | Что именно | Куда (global по умолчанию) | -|-----|------------|----------------------------| -| **Claude Code** | skill-директория с `SKILL.md` + scripts | `~/.claude/skills/codebase-index/` | -| **Codex CLI** | managed block в `AGENTS.md` + ресурсы (instruction package, **не** Claude Skill) | блок в `~/.codex/AGENTS.md`, ресурсы в `~/.codex/skills/codebase-index/` | -| **OpenCode** | markdown-команда `/codebase-index` + agent-файл + ресурсы | `~/.config/opencode/commands/`, `.../agents/`, `.../skills/codebase-index/` | +| CLI | What | Where (global scope, the default) | +|-----|------|-----------------------------------| +| **Claude Code** | skill directory with `SKILL.md` + scripts | `~/.claude/skills/codebase-index/` | +| **Codex CLI** | managed block in `AGENTS.md` + resources (an instruction package, **not** a Claude Skill) | block in `~/.codex/AGENTS.md`, resources in `~/.codex/skills/codebase-index/` | +| **OpenCode** | markdown command `/codebase-index` + agent file + resources | `~/.config/opencode/commands/`, `.../agents/`, `.../skills/codebase-index/` | -Для project-установки (`--scope project`) пути меняются на -`./.claude/...`, `./AGENTS.md`, `./.opencode/...`. +With a project install (`--scope project`) the paths become `./.claude/...`, `./AGENTS.md`, +`./.opencode/...`. --- -## Быстрая установка +## Quick install **macOS / Linux:** @@ -37,10 +40,9 @@ curl -fsSL https://raw.githubusercontent.com/denfry/codebase-index/main/install. irm https://raw.githubusercontent.com/denfry/codebase-index/main/install.ps1 | iex ``` -### Безопасная установка (с предпросмотром — рекомендуется) +### Safer install (download, read, then run — recommended) -Pipe-to-shell исполняет удалённый код «вслепую». Безопаснее скачать, прочитать -и только потом запустить: +Pipe-to-shell executes remote code blindly. Download, read, then run: ```sh curl -fsSL https://raw.githubusercontent.com/denfry/codebase-index/main/install.sh -o install.sh @@ -56,9 +58,9 @@ pwsh ./install.ps1 --- -## Сценарии использования +## Usage scenarios -**Конкретный target:** +**A specific target:** ```sh sh install.sh --target claude @@ -70,32 +72,32 @@ sh install.sh --target opencode pwsh ./install.ps1 -Target claude ``` -**Все сразу:** +**Everything at once:** ```sh sh install.sh --target all ``` -**Авто-определение (по умолчанию)** — installer сам находит CLI по PATH и -типичным конфиг-директориям (`~/.claude`, `~/.codex`, `~/.config/opencode`): +**Auto-detect (default)** — the installer looks for CLIs on `PATH` and in the usual config +directories (`~/.claude`, `~/.codex`, `~/.config/opencode`): ```sh sh install.sh # = --target auto ``` -**Dry-run** (ничего не меняет, показывает план): +**Dry run** (changes nothing, prints the plan): ```sh sh install.sh --target all --dry-run ``` -**Uninstall** (удаляет только установленное этим installer — по manifest): +**Uninstall** (removes only what this installer wrote, per its manifest): ```sh sh install.sh --target all --uninstall ``` -**Переопределение директории установки:** +**Override the install directory:** ```sh sh install.sh --target claude --install-dir "$HOME/my-skills/codebase-index" @@ -105,53 +107,53 @@ sh install.sh --target claude --install-dir "$HOME/my-skills/codebase-index" pwsh ./install.ps1 -Target claude -InstallDir "D:\skills\codebase-index" ``` -**Pinning по ветке/тегу** (воспроизводимость и безопасность): +**Pin to a branch or tag** (reproducibility and safety): ```sh -sh install.sh --branch v1.8.0 +sh install.sh --branch v1.9.0 ``` --- -## Флаги +## Flags -| install.sh | install.ps1 | Значение | -|------------|-------------|----------| +| install.sh | install.ps1 | Meaning | +|------------|-------------|---------| | `--target` | `-Target` | `claude\|codex\|opencode\|all\|auto` | -| `--install-dir` | `-InstallDir` | переопределить путь установки | -| `--repo-url` | `-RepoUrl` | URL репозитория-источника | -| `--branch` | `-Branch` | ветка/тег для скачивания | +| `--install-dir` | `-InstallDir` | override the install path | +| `--repo-url` | `-RepoUrl` | source repository URL | +| `--branch` | `-Branch` | branch or tag to download | | `--scope` | `-Scope` | `global\|project` | -| `--dry-run` | `-DryRun` | не вносить изменений | -| `--force` | `-Force` | перезаписать существующую установку (с backup) | -| `--no-python-bootstrap` | `-NoPythonBootstrap` | не создавать venv / не запускать bootstrap.py | -| `--verbose` | `-Verbose` | подробный лог | -| `--uninstall` | `-Uninstall` | удалить по manifest | -| `--help` | `-Help` | справка | +| `--dry-run` | `-DryRun` | make no changes | +| `--force` | `-Force` | overwrite an existing install (a backup is taken first) | +| `--no-python-bootstrap` | `-NoPythonBootstrap` | do not create a venv / run `bootstrap.py` | +| `--verbose` | `-Verbose` | verbose log | +| `--uninstall` | `-Uninstall` | remove per manifest | +| `--help` | `-Help` | show help | --- ## Python / runtime -После раскладки файлов installer (если не указан `--no-python-bootstrap`): +After laying out the files, unless `--no-python-bootstrap` was given, the installer: -1. ищет `python3`/`python` (Unix) или `py`/`python` (Windows), **минимум 3.9**; -2. если Python не найден — **не** ставит системный Python сам, а печатает - понятную инструкцию; -3. при наличии Python создаёт `.venv` внутри директории skill; -4. ставит зависимости из `requirements.txt`, если файл есть; -5. запускает `skill/scripts/bootstrap.py` (создаёт `runtime.json`, дополняет manifest). +1. looks for `python3` / `python` (Unix) or `py` / `python` (Windows), **3.9 minimum** for the + bootstrap itself (the `codebase-index` package requires 3.11+); +2. if no Python is found, does **not** install one, but prints a clear instruction; +3. otherwise creates a `.venv` inside the skill directory; +4. installs dependencies from `requirements.txt` if that file exists; +5. runs `skill/scripts/bootstrap.py` (creates `runtime.json`, extends the manifest). --- ## Manifest -После установки в директории skill создаётся `install_manifest.json`: +After installation the skill directory contains `install_manifest.json`: ```json { "skill_name": "codebase-index", - "version": "1.8.0", + "version": "1.9.0", "target": "claude", "os": "linux", "source_repo": "https://github.com/denfry/codebase-index", @@ -162,70 +164,67 @@ sh install.sh --branch v1.8.0 } ``` -Uninstall читает этот файл и удаляет **только** перечисленные в нём файлы. -Для `AGENTS.md` удаляется только managed block, сам файл сохраняется. +Uninstall reads this file and removes **only** the files listed in it. For `AGENTS.md` only the +managed block is removed; the file itself is kept. --- -## Как запускать в OpenCode +## Running in OpenCode -После установки команда доступна как: +After installation the command is available as: ``` -/codebase-index <запрос> +/codebase-index ``` --- ## Troubleshooting -- **«Python 3.9+ не найден»** — установите Python и повторите без - `--no-python-bootstrap`, либо игнорируйте, если venv не нужен. -- **«Уже установлено … (используйте --force)»** — добавьте `--force` - (создаётся backup перед перезаписью). -- **Неверный путь для вашей версии Claude Code** — задайте `--install-dir`. - Дефолтные пути вынесены в функции `*_default_dir` / `Get-*TargetDir`. -- **Нет curl/wget (Unix)** — установите один из них; на Windows используется - `Invoke-WebRequest`. -- **Авто-режим ничего не нашёл (exit 4)** — укажите `--target` явно. +- **"Python 3.9+ not found"** — install Python and re-run without `--no-python-bootstrap`, or + ignore it if you do not need the venv. +- **"Already installed … (use --force)"** — add `--force` (a backup is taken before overwriting). +- **Wrong path for your Claude Code version** — pass `--install-dir`. Default paths live in the + `*_default_dir` / `Get-*TargetDir` functions. +- **No curl/wget (Unix)** — install one of them; Windows uses `Invoke-WebRequest`. +- **Auto mode found nothing (exit 4)** — pass `--target` explicitly. --- ## Security notes -- Удалённый код не исполняется «на лету»: архив сначала скачивается, - проверяется структура (`SKILL.md` + frontmatter) и пути (запрет traversal). -- URL источника всегда печатается перед скачиванием. -- Поддерживается pinning по `--branch`. -- Без `--install-dir` installer не пишет за пределы `HOME`/проекта. -- `sudo` не используется. -- Есть `--dry-run`. +- Remote code is not executed on the fly: the archive is downloaded first, its structure is + checked (`SKILL.md` + frontmatter) and its paths are checked (no traversal). +- The source URL is always printed before downloading. +- Pinning with `--branch` is supported. +- Without `--install-dir` the installer never writes outside `HOME` / the project. +- `sudo` is never used. +- `--dry-run` is available. --- ## Developer notes -### Структура +### Layout ``` -install.sh / install.ps1 entrypoints -lib/common.sh / common.ps1 общие функции (лог, скачивание, manifest, bootstrap) -adapters/.{sh,ps1} логика конкретного CLI -skill/ источник правды (SKILL.md, scripts/bootstrap.py) -tests/installer/smoke.{sh,ps1} smoke-тесты +install.sh / install.ps1 entrypoints +lib/common.sh / common.ps1 shared functions (logging, download, manifest, bootstrap) +adapters/.{sh,ps1} per-CLI logic +skill/ source of truth (SKILL.md, scripts/bootstrap.py) +tests/installer/smoke.{sh,ps1} smoke tests ``` -### Как добавить новый adapter +### Adding an adapter -1. Создайте `adapters/.sh`, определив функции `adapter_install` и - `adapter_uninstall` (используйте функции из `lib/common.sh`). -2. Создайте `adapters/.ps1` с `Invoke-AdapterInstall` / - `Invoke-AdapterUninstall`. -3. Добавьте `` в `--target`/`-Target` и в авто-определение - (`detect_targets` / `Get-AutoTargets`). -4. Допишите строку в smoke-тесты. +1. Create `adapters/.sh` defining `adapter_install` and `adapter_uninstall` (use the + helpers from `lib/common.sh`). +2. Create `adapters/.ps1` with `Invoke-AdapterInstall` / `Invoke-AdapterUninstall`. +3. Add `` to `--target` / `-Target` and to auto-detection (`detect_targets` / + `Get-AutoTargets`). +4. Add a line to the smoke tests. -### Как тестировать локально +### Testing locally ```sh sh tests/installer/smoke.sh @@ -235,5 +234,5 @@ sh tests/installer/smoke.sh pwsh tests/installer/smoke.ps1 ``` -Smoke-тест прогоняет dry-run для всех целей, ставит skill в temp-директорию, -проверяет `SKILL.md` + manifest и выполняет uninstall. +The smoke test dry-runs every target, installs the skill into a temp directory, checks +`SKILL.md` + the manifest, and uninstalls. diff --git a/src/codebase_index/cli.py b/src/codebase_index/cli.py index ffd216f..bde9e85 100644 --- a/src/codebase_index/cli.py +++ b/src/codebase_index/cli.py @@ -2,8 +2,7 @@ Commands map 1:1 to docs/ARCHITECTURE.md §5 (CLI contract) and delegate to the `indexer`, `retrieval`, and `storage` layers through `service.py` — the same -layer the MCP server uses, so the two surfaces cannot drift. Only `clean` is -still a stub. +layer the MCP server uses, so the two surfaces cannot drift. Conventions: * every command accepts global options via the Typer context: --root, --json, --quiet From d0f4269981af3a1acb1a64dd80690f6a375770a7 Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:24:54 +0300 Subject: [PATCH 08/12] docs(demo): reproducible Flask demo, real-output terminal card, benchmark chart, architecture diagram - examples/demo/run_demo.sh|.ps1 clone pallets/flask at a pinned commit and run the Find / Trace / Predict scenarios; EXPECTED_OUTPUT.md is the captured run. - assets/demo-terminal.svg is rendered from that transcript by scripts/gen_terminal_svg.py; the previous demo.png was a mock-up with invented paths and is removed (gen_assets.py no longer builds it). - assets/benchmark.svg is rendered from the logged public baseline JSON by scripts/gen_benchmark_chart.py, so the chart cannot drift from the run. - assets/architecture.svg: index and query paths on one diagram. - docs/DEMO.md + docs/demo.tape: exact steps for a VHS/asciinema recording. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- assets/architecture.svg | 96 ++++++++++++++++ assets/benchmark.svg | 98 ++++++++++++++++ assets/demo-terminal.svg | 49 ++++++++ assets/demo.png | Bin 145558 -> 0 bytes docs/DEMO.md | 103 +++++++++++++++++ docs/demo.tape | 29 +++++ examples/demo/EXPECTED_OUTPUT.md | 187 +++++++++++++++++++++++++++++++ examples/demo/README.md | 53 +++++++++ examples/demo/run_demo.ps1 | 42 +++++++ examples/demo/run_demo.sh | 53 +++++++++ scripts/gen_assets.py | 111 ------------------ scripts/gen_benchmark_chart.py | 119 ++++++++++++++++++++ scripts/gen_terminal_svg.py | 149 ++++++++++++++++++++++++ 13 files changed, 978 insertions(+), 111 deletions(-) create mode 100644 assets/architecture.svg create mode 100644 assets/benchmark.svg create mode 100644 assets/demo-terminal.svg delete mode 100644 assets/demo.png create mode 100644 docs/DEMO.md create mode 100644 docs/demo.tape create mode 100644 examples/demo/EXPECTED_OUTPUT.md create mode 100644 examples/demo/README.md create mode 100644 examples/demo/run_demo.ps1 create mode 100644 examples/demo/run_demo.sh create mode 100644 scripts/gen_benchmark_chart.py create mode 100644 scripts/gen_terminal_svg.py diff --git a/assets/architecture.svg b/assets/architecture.svg new file mode 100644 index 0000000..e1d8664 --- /dev/null +++ b/assets/architecture.svg @@ -0,0 +1,96 @@ + + codebase-index architecture: repository to ranked file:line evidence + + + + + + + + + + Repository + discovery walk + .gitignore / .codeindexignore + secret · binary · generated gates + size cap (1 MB) + + + + Tree-sitter extraction + symbols (12 Tier-A languages) + import · call · reference · + extends / implements edges + symbol-aligned chunks + + + + Local SQLite + files · chunks · symbols + edges (with confidence) + FTS5 · optional sqlite-vec + .claude/cache/codebase-index/ + + + + One service layer + CLI (codebase-index / cbx) + Skill (Claude Code, Codex, OpenCode) + MCP (stdio, schema_version 1) + --json for anything else + + + + + + + Query path (per question) + + + intent detection + locate · trace · impact + + + retrievers + path · symbol · FTS5 + (+ vector, opt-in) + + + RRF fusion + file-level agreement + + + rerank · priors · dedup + every signal ablated + + + token-budgeted packet + ranked file:line · reads · confidence + + + + + + + + + Graph path (refs · path · impact · diff-impact · architecture) + + + bounded expansion over the edge table + direction · depth · node cap + + + edge confidence carried through + extracted · inferred · ambiguous + + + coverage.partial when a language lacks edges + so "no callers" is never silently wrong + + + + + + Network: off by default. Telemetry: none. External embeddings: opt-in, triple-gated. Source: docs/ARCHITECTURE.md + diff --git a/assets/benchmark.svg b/assets/benchmark.svg new file mode 100644 index 0000000..b1650dd --- /dev/null +++ b/assets/benchmark.svg @@ -0,0 +1,98 @@ + +codebase-index vs rg+window on public repositories + +Index vs disciplined grep on public repositories +flask, gson, fastify at pinned commits · 450 git-derived queries · codebase-index 1.9.0 · 2026-09-04 · same tokenizer on both sides +codebase-index +rg + 80-line windows +hit@3 — answer file among the top 3 + +0.00 + +0.25 + +0.50 + +0.75 + +1.00 +flask + +0.49 + +0.23 +gson + +0.57 + +0.35 +fastify + +0.58 + +0.33 +pooled + +0.55 + +0.30 +MRR — how high the first correct file ranks + +0.00 + +0.25 + +0.50 + +0.75 + +1.00 +flask + +0.38 + +0.23 +gson + +0.50 + +0.29 +fastify + +0.48 + +0.27 +pooled + +0.46 + +0.26 +context tokens per query (pooled mean) + +0 + +1,100 + +2,200 + +3,300 + +4,400 +packet + +2,402 + +1,351 ++ reads + +3,835 + +3,495 +Paired significance, index − rg (pooled) +hit@3: +0.242 95% CI [+0.187, +0.298] p < 0.001 +recall@5: +0.231 95% CI [+0.183, +0.278] p < 0.001 +MRR: +0.194 95% CI [+0.151, +0.235] p < 0.001 +tokens: 340 95% CI [238, 440] p < 0.001 +Ground truth: commit subject → files that commit changed. Raw run: tests/eval/results/. +Latency is not charted: in-process index vs a separate ripgrep binary is not a fair pair. + diff --git a/assets/demo-terminal.svg b/assets/demo-terminal.svg new file mode 100644 index 0000000..837ae95 --- /dev/null +++ b/assets/demo-terminal.svg @@ -0,0 +1,49 @@ + + codebase-index 1.9 · pallets/flask @ d318b683 · real output, see examples/demo + + + + codebase-index 1.9 · pallets/flask @ d318b683 · real output, see examples/demo + $ codebase-index search "where is the session cookie signed and saved" --limit 3 +Query: where is the session cookie signed and saved +Intent: `locate_impl` · Confidence: medium + +| # | Path | Lines | Reason | +|---|------|-------|--------| +| 1 | src/flask/sessions.py | 24-54 | in src/flask/ · 2 callers · source prior +0.08 | +| 2 | src/flask/sessions.py | 284-385 | in src/flask/ · 2 callers · source prior +0.08 | +| 3 | src/flask/sessions.py | 57-80 | in src/flask/ · 1 callers · source prior +0.08 | + +src/flask/sessions.py:24-54 +class SessionMixin(MutableMapping[str, t.Any]): +src/flask/sessions.py:284-385 +class SecureCookieSessionInterface(SessionInterface): +src/flask/sessions.py:57-80 +class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): + +Recommended reads: +- src/flask/sessions.py:24-54 +- src/flask/sessions.py:284-385 +- src/flask/sessions.py:57-80 + +$ codebase-index refs open_session +query: open_session | index: fresh + +| kind | path | line | confidence | +|------|------|------|------------| +| call | src/flask/ctx.py | 388 | ? ambiguous | +| definition | src/flask/sessions.py | 249 | exact | +| definition | src/flask/sessions.py | 323 | exact | +| call | src/flask/testing.py | 165 | ? ambiguous | +| definition | tests/test_reqctx.py | 182 | exact | +| definition | tests/test_session_interface.py | 16 | exact | + +$ codebase-index impact SecureCookieSessionInterface --direction up --depth 2 +impact: `SecureCookieSessionInterface` · direction: up · depth: 2 · affected files: + +| dist | via | kind | node | location | +|------|-----|------|------|----------| +| 1 | call | symbol | Flask | src/flask/app.py:110 | +| 1 | extends | symbol | PathAwareSessionInterface | tests/test_reqctx.py:204 | +| 2 | call | symbol | CustomFlask | tests/test_reqctx.py:211 | + diff --git a/assets/demo.png b/assets/demo.png deleted file mode 100644 index a8077fd238b5d4a6fe1eb2d9030dac3a0d04eb07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 145558 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 z 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

codebase-index

- Give AI coding agents a precise map of your codebase — locally, - privately, and with evidence. + A local code map for AI coding agents: find the right file, trace how it connects, predict what a change breaks.

- Find implementations. Trace behavior. Predict change impact. -

- -

- Quick start · - Install · - Benchmarks · - Security · - MCP + Works with Claude Code, Codex CLI, OpenCode and any MCP client. Runs on your machine. Sends nothing anywhere.

PyPI version CI Python 3.11+ - MCP ready - No network by default + MCP stdio server + No network by default MIT license

- codebase-index showing a Find, Trace, Predict workflow with precise file and line evidence + Try it · + Why · + Find / Trace / Predict · + Agents · + Evidence · + Contribute

-## The short version - -`codebase-index` is a local retrieval and code-graph layer for Claude Code, -Codex CLI, OpenCode, and MCP clients. It indexes a repository into SQLite, -extracts symbols and relationships with Tree-sitter, and gives agents ranked -`file:line` evidence instead of making them scan broad sets of files. - -```text -Question → ranked retrieval → dependency evidence → precise answer -``` - -It is not an IDE and not another coding agent. Your existing agent remains the -interface; `codebase-index` gives it better aim. - -## Find. Trace. Predict. - -| Job | Question | Command | -|---|---|---| -| **Find** | Where is authentication implemented? | `codebase-index search "authentication"` | -| **Trace** | How does checkout reach the database? | `codebase-index explain "checkout flow"` | -| **Trace** | How are two components connected? | `codebase-index path ApiController Database` | -| **Predict** | What breaks if `User` changes? | `codebase-index impact User` | -| **Predict** | What does my current diff affect? | `codebase-index diff-impact` | - -Every retrieval packet carries: - -- ranked matches and the reason each match scored; -- exact line ranges to read next; -- index freshness; -- answer confidence and targeted fallbacks; -- graph coverage and edge confidence where relevant. +

+ Real codebase-index output on Flask: a ranked search with line ranges, references with confidence, and an impact table +

-That evidence contract lets an agent distinguish “nothing references this” from -“the graph is partial, so verify with a targeted search.” +

Real output on pallets/flask. Reproduce it with examples/demo.

-## Install in five minutes +## Try it in 60 seconds ```bash pip install codebase-index cd your-project -codebase-index init -codebase-index index -codebase-index search "where is authentication implemented?" +codebase-index index # a few seconds for ~250 files +codebase-index search "where is the session cookie signed" +codebase-index impact SessionInterface --direction up # what depends on it +codebase-index init # wire it into Claude Code / Codex / OpenCode ``` -`init` can install resources for Claude Code, Codex CLI, OpenCode, and detected -MCP clients: +That is the whole setup. The index is a SQLite file under `.claude/cache/`, the +skill tells your agent to query it before reading files, and `codebase-index doctor` +tells you if anything is off. -```bash -codebase-index init --target auto -codebase-index init --target codex -codebase-index init --target claude -codebase-index init --target opencode -``` +## Why this exists -`pipx install codebase-index` is supported as an isolated alternative. See the -[installation guide](docs/INSTALLATION.md) for pinned releases, editable -installs, hooks, Windows details, and troubleshooting. +Claude Code and Codex can already read files. The problem is deciding *which* files. +On a repository with a few hundred files an agent that greps for keywords opens the +wrong file more often than the right one, then reads whole files to compensate. That +costs tokens, time, and, worst of all, wrong answers delivered with confidence. -### Claude Code plugin - -```text -/plugin marketplace add denfry/codebase-index -/plugin install codebase-index@codebase-index -``` - -The plugin provisions a private environment on first use. Later sessions run -offline, and the first codebase question builds the index automatically. - -## What the agent receives - -```json -{ - "query": "where is authentication implemented?", - "confidence": "high", - "results": [ - { - "path": "src/auth/AuthService.ts", - "line_start": 12, - "line_end": 148, - "score": 0.92, - "reason": "exact symbol match, 4 callers" - } - ], - "recommended_reads": [ - { - "path": "src/auth/AuthService.ts", - "line_start": 12, - "line_end": 148 - } - ], - "index": { - "exists": true, - "stale": false - } -} -``` +`codebase-index` gives the agent a ranked, evidence-bearing answer to three questions +before it opens anything: -Snippets are skeletonized when that preserves evidence while saving tokens. -Unrelated bodies collapse, but imports, signatures, matched lines, and exact -read ranges remain. +| | Question | What comes back | +|---|---|---| +| **Find** | Where is X implemented? | Ranked `file:line` ranges, a reason for each rank, and a bounded read plan | +| **Trace** | What calls this? How does A reach B? | Definitions vs references, shortest dependency paths, each edge tagged `extracted` / `inferred` / `ambiguous` | +| **Predict** | What breaks if I change this? What does my diff touch? | Upstream and downstream dependents by distance, with graph-coverage honesty | -## Core commands +Every answer carries its own uncertainty: index freshness, result confidence, and +whether the graph is partial for a language. The agent can tell "nothing references +this" from "the graph doesn't know", which grep never can. -```bash -# Find -codebase-index search "auth token refresh" -codebase-index symbol AuthService - -# Trace -codebase-index explain "authentication flow" -codebase-index refs send_email -codebase-index path ApiController Database -codebase-index describe Database -codebase-index architecture - -# Predict -codebase-index impact User --direction up --depth 2 -codebase-index diff-impact --base HEAD --direction up --depth 2 - -# Inspect and visualize -codebase-index graph User --direction both --depth 2 --output graph.html -codebase-index stats -codebase-index doctor -``` +**How it differs from what you already have** -Add `--json` for agents and automation. Search supports `hybrid`, `fts`, -`symbol`, and opt-in `vector` modes. +- **grep / ripgrep**: exact text, no ranking, no notion of definition vs call, no + dependency graph. Use it when you know the string. Use this when you know the + question. +- **Repo-map style context** (Aider and friends): a signature listing pushed into + every prompt. Good orientation, query-agnostic, and it must fit in the budget. + This is a queryable index instead: per-question ranking, line ranges, graph + traversal, and a stable JSON/MCP contract any agent can call. +- **Cloud code search / IDE indexing**: powerful, but your code leaves the machine + or you adopt an IDE. This is a small Python package on SQLite and Tree-sitter: + no server, no account, no telemetry. -## Why not just grep? +The [comparison guide](docs/COMPARISON.md) says when each of those is the better choice. -Grep is excellent when you know the exact text. Repository questions often -need more: +## Find / Trace / Predict -| Capability | `rg` / grep | codebase-index | -|---|---:|---:| -| Exact text matching | Yes | Yes | -| Ranked results | No | Yes | -| Symbol definitions vs calls | No | Yes | -| Dependency and impact graph | No | Yes | -| Token-budgeted read plan | No | Yes | -| Freshness and coverage signals | No | Yes | -| Local and scriptable | Yes | Yes | +All examples below are real output on Flask at commit `d318b683`; see +[examples/demo/EXPECTED_OUTPUT.md](examples/demo/EXPECTED_OUTPUT.md) for the full transcript. -Use grep for one known string. Use `codebase-index` when the agent must locate, -understand, or assess a change across a repository. +**Find** the implementation, not a keyword hit: -The [comparison guide](docs/COMPARISON.md) also covers Cursor, Aider repo-map, -Sourcegraph, Continue, Amp, and Codebase-Memory MCP—including when those tools -are the better choice. +```text +$ codebase-index search "where is the session cookie signed and saved" --limit 3 +| # | Path | Lines | Reason | +| 1 | src/flask/sessions.py | 24-54 | in src/flask/ · 2 callers · source prior +0.08 | +| 2 | src/flask/sessions.py | 284-385 | in src/flask/ · 2 callers · source prior +0.08 | +| 3 | src/flask/sessions.py | 57-80 | in src/flask/ · 1 callers · source prior +0.08 | +``` -## Measured results +**Trace** references with confidence, and paths between symbols: -On the published 55k LOC Java benchmark: +```text +$ codebase-index refs open_session +| kind | path | line | confidence | +| call | src/flask/ctx.py | 388 | ? ambiguous | +| definition | src/flask/sessions.py | 249 | exact | +| definition | src/flask/sessions.py | 323 | exact | + +$ codebase-index path wsgi_app dispatch_request +wsgi_app (src/flask/app.py) → full_dispatch_request → dispatch_request · 2 hop(s) +``` -- Recall@3: **70%** for `codebase-index` versus **40%** for the `rg` baseline; -- answer-context tokens: approximately **13× fewer**; -- raw results and methodology are checked into the repository. +**Predict** the blast radius of a symbol, or of the diff you have right now: -These results are evidence for that benchmark, not a claim of universal -superiority. Large public-repository and framework-graph evaluations remain on -the [roadmap](docs/ROADMAP.md). Read the complete methodology and limitations in -[BENCHMARKS.md](docs/BENCHMARKS.md). +```text +$ codebase-index impact SecureCookieSessionInterface --direction up --depth 2 +| dist | via | node | location | +| 1 | call | Flask | src/flask/app.py:110 | +| 1 | extends | PathAwareSessionInterface | tests/test_reqctx.py:204 | +| 2 | call | CustomFlask | tests/test_reqctx.py:211 | + +$ codebase-index diff-impact # after editing src/flask/sessions.py +affected files (8): src/flask/app.py, src/flask/ctx.py, src/flask/globals.py, ... (edge kind + confidence per row) +``` -## Local by default +More: `explain "how are blueprints registered"`, `describe dispatch_request`, +`architecture` (modules, god nodes, surprising links), `graph User --output graph.html`. +Add `--json` to any command for the machine-readable packet. -The base install: +## Agent integrations -- makes no network requests; -- sends no telemetry; -- stores the derived index inside the project cache; -- excludes dependency, build, binary, oversized, generated, and secret-like - files before indexing; -- redacts secret patterns again at output time; -- exposes `doctor --strict` for CI and security checks. +| Agent | Setup | What it gets | +|---|---|---| +| **Claude Code** | `codebase-index init --target claude`, or the plugin: `/plugin marketplace add denfry/codebase-index` then `/plugin install codebase-index@codebase-index` | A skill that routes repository questions to the index and reads only `recommended_reads` ranges; optional PostToolUse hook keeps the index fresh | +| **Codex CLI** | `codebase-index init --target codex` | A managed block in `AGENTS.md` plus the skill resources | +| **OpenCode** | `codebase-index init --target opencode` | `/codebase-index` command, agent file, skill resources | +| **Any MCP client** (Claude Desktop, Cursor, VS Code, Zed, Windsurf, ...) | `pip install "codebase-index[mcp]"` then `codebase-index mcp --root /path/to/repo` | 11 tools (`search_code`, `find_refs`, `impact_of`, `impact_of_diff`, `path_between`, ...) with a versioned JSON envelope | +| **Anything with a shell** | `codebase-index --json ...` | The same payloads as plain JSON, plus a local SQLite database you can query yourself | -Embeddings are optional. Local embeddings stay on the machine; external -embeddings require explicit configuration, an API key, and an endpoint -acknowledgement. +`init --target auto` detects which of these are present. See +[INSTALLATION.md](docs/INSTALLATION.md) and [MCP.md](docs/MCP.md). -See the [security model](docs/SECURITY_MODEL.md) for trust boundaries, gates, -failure modes, and residual risks. +## Evidence -## How it works +Numbers below come from runs whose raw logs are in this repository. Nothing here is a +claim about tools we have not benchmarked. -```text -Repository - │ - ├─ discovery + ignore and secret gates - ├─ Tree-sitter symbols and relationships - ├─ line and symbol-aligned chunks - └─ optional embeddings - │ - ▼ - local SQLite - FTS5 + symbols + graph - │ - ▼ - intent routing → hybrid retrieval → rerank → token budget - │ - ▼ - ranked file:line evidence for CLI, Skill, and MCP -``` - -The three product surfaces share one service layer, so retrieval behavior does -not drift between the CLI, installed agent skills, and MCP tools. +**Index vs a disciplined grep agent, on public repositories.** Three repos at pinned +commits (Flask, Gson, Fastify), 450 questions mined from git history (commit subject +→ files that commit changed, so neither side wrote the answer key), the same tokenizer +charging both sides for what enters context: -Detailed internals: +| pooled, n = 450 | hit@3 | MRR | context tokens / question | +|---|---:|---:|---:| +| codebase-index | **0.547** | **0.456** | 3,835 | +| `rg` + 80-line windows | 0.304 | 0.263 | 3,495 | -- [Architecture](docs/ARCHITECTURE.md) -- [Retrieval pipeline](docs/RETRIEVAL_PIPELINE.md) -- [Database schema](docs/DATABASE_SCHEMA.md) -- [Language and graph coverage](docs/LANGUAGES.md) +Every delta is significant at p < 0.001 (paired bootstrap CI in the log). Read it as: +**1.8× more likely to put the answer in the top three, at about 10% more context**. +Reproduce with `python tests/eval/run_baselines.py --clone`; details, caveats and the +repo-map-style comparison are in [BENCHMARKS.md](docs/BENCHMARKS.md). -## Supported surfaces +

Bar chart of hit@3, MRR and context tokens for codebase-index versus ripgrep with windows on Flask, Gson and Fastify

-| Surface | Integration | -|---|---| -| Claude Code | Skill, plugin, optional hooks | -| Codex CLI | `AGENTS.md` plus project skill | -| OpenCode | Command, agent, and skill resources | -| MCP clients | stdio server with versioned JSON envelopes | -| Shell and automation | CLI, `--json`, and local SQLite | +**Every ranking signal is ablated.** A change to the ranker ships only if it is +significant on a pooled multi-language query set +([tests/eval](tests/eval/README.md)). 1.9.0 removed two signals that could not +show a benefit and rejected five plausible ones. -Run the MCP server with: +**What is not measured yet**: whether an *agent* completes tasks better with the +index. That needs model calls and a rubric and is the top item in +[BENCHMARKS.md](docs/BENCHMARKS.md#future-work-in-priority-order). Please do not +quote task-success numbers for this project; there are none. -```bash -codebase-index mcp --root /path/to/repository -``` +## How it works -Available MCP tools include search, explain, symbols, references, impact, -diff impact, architecture, shortest path, node description, health, and index statistics. -See [MCP.md](docs/MCP.md) for client configuration. +

Architecture: discovery and secret gates, Tree-sitter symbols and edges, local SQLite with FTS5, one service layer behind CLI, skill and MCP; the query path runs intent detection, retrievers, RRF fusion, rerank and a token budget

+ +Indexing walks the repository through ignore and secret gates, extracts symbols and +import/call/reference/inheritance edges with Tree-sitter for 12 languages, and stores +chunks, symbols and edges in one SQLite file with FTS5. A query goes through intent +detection, path/symbol/FTS retrievers (vector is opt-in), reciprocal-rank fusion that +rewards cross-retriever agreement on a file, a rerank with calibrated source priors, +and a token budget that emits ranked ranges plus a bounded read plan. The CLI, the +installed skills and the MCP server all call the same service layer, so behaviour +cannot drift between surfaces. + +Deep dives: [Architecture](docs/ARCHITECTURE.md) · +[Retrieval](docs/RETRIEVAL.md) · [Schema](docs/SCHEMA.md) · +[Languages](docs/LANGUAGES.md) · [Skill design](docs/SKILL_DESIGN.md) + +## Privacy and security + +- **No network by default.** The base install has no network dependency and makes + no requests. There is no telemetry, no crash reporting, no usage counter. +- **Secrets never get in.** `.env*`, keys, certificates, credential files, + binaries, dependency and build directories, generated and oversized files are + excluded before parsing; `.gitignore`, `.codeindexignore`, `.claudeignore` and + `.cursorignore` are honoured. +- **Secrets never get out.** Snippets are redacted again at output time (AWS keys, + private-key blocks, JWTs, connection strings, Slack tokens, high-entropy values). +- **One opt-in exit, triple-gated.** External embeddings require an explicit config + flag, an API key in the environment, and a printed endpoint warning, or they are + refused. Local embeddings stay local. +- **Verify it yourself.** `codebase-index doctor --strict` audits the gates and exits + non-zero in CI if one is off. + +Details and residual risks: [SECURITY_MODEL.md](docs/SECURITY_MODEL.md). +Report vulnerabilities privately: [SECURITY.md](SECURITY.md). + +## Supported languages + +Symbol and graph extraction (Tier A): Python, JavaScript, TypeScript, Java, Go, Rust, +C, C++, C#, Ruby, PHP, Kotlin. Generic Tree-sitter definitions (Tier B): Lua. +Full-text search for everything else that is text, including Markdown, YAML, JSON, +TOML, SQL, Dockerfiles, Terraform and CI configs. `refs` and `impact` report +`coverage.partial` when a language has no graph edges, so an empty result is never +silently presented as "no callers". Tiers and how to add a language: +[LANGUAGES.md](docs/LANGUAGES.md). ## Project status -The latest released line is **1.9.0**. It includes: - -- hybrid and optional vector retrieval; -- Tree-sitter symbol extraction across the documented language tiers; -- import, call, reference, and inheritance graphs; -- architecture communities, central nodes, and surprising cross-module links; -- shortest dependency paths and node descriptions; -- token-budgeted and skeletonized retrieval packets; -- benchmark-calibrated lexical expansion, fuzzy identifier matching, and source-aware ranking; -- rank fusion that scores cross-retriever agreement at file level, not just at a locator; -- bounded, intent-directed graph discovery with optional diversity and duplicate suppression; -- CLI, Skill, plugin, and MCP delivery; -- incremental updates, watch hooks, diagnostics, skill rollback, and diff-aware - impact analysis; -- a multi-repository retrieval evaluation with leak-free git-derived ground truth, - one-signal ablations, and paired significance tests - ([tests/eval](tests/eval/README.md)). - -Every shipped ranking signal has to survive that evaluation: 1.9.0 removed the -cost of two signals that could not demonstrate a benefit and rejected several -plausible ones outright (IDF-weighted coverage, stemming, graph propagation, MMR, -a file-length prior). Planned work is deliberately separated from shipped -capability. The next product priorities are typed framework edges and an even more -direct task-context workflow. See the [roadmap](docs/ROADMAP.md). - -## Documentation - -| Start here | Deep dives | Project trust | -|---|---|---| -| [Quick start](docs/QUICKSTART.md) | [Retrieval](docs/RETRIEVAL.md) | [Benchmarks](docs/BENCHMARKS.md) | -| [Installation](docs/INSTALLATION.md) | [Architecture](docs/ARCHITECTURE.md) | [Security](docs/SECURITY.md) | -| [FAQ](docs/FAQ.md) | [MCP](docs/MCP.md) | [Release checklist](docs/RELEASE_CHECKLIST.md) | -| [Skill design](docs/SKILL_DESIGN.md) | [Schema](docs/SCHEMA.md) | [Changelog](CHANGELOG.md) | +Current line: **1.9.x**, on PyPI, MIT. CI runs Linux, macOS and Windows on Python +3.11–3.13 with an 80% coverage gate, golden snapshots for every CLI and MCP payload, +a packaging smoke test on every PR, and a skill-copy drift check. -## Contributing +What works today: hybrid retrieval with optional local vectors; Tree-sitter symbols +and edges; `search`, `explain`, `symbol`, `refs`, `impact`, `diff-impact`, `path`, +`describe`, `architecture`, `graph`; token-budgeted, skeletonized packets; incremental +`update`, `watch`, and hooks; CLI, Claude Code plugin/skill, Codex, OpenCode and +MCP delivery; a reproducible multi-repository benchmark. -Contributions should preserve three invariants: +What does not exist yet: framework-aware typed edges (routes, DI, migrations), +multi-repository workspaces, paged MCP results, an agent task-level benchmark, +signed release artifacts. See the [roadmap](docs/ROADMAP.md). -1. retrieval quality is measured, not asserted; -2. the default path remains local and fails closed at security boundaries; -3. machine-readable contracts stay stable across CLI and MCP. +## Contributing -Before opening a pull request: +Fifteen minutes from clone to a first PR: [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). +Where help is most useful: benchmark reproductions on your own repositories, new +Tier-A languages, graph edge kinds, and MCP client verification. Labels and the +retrieval-quality PR rules are in [CONTRIBUTING.md](CONTRIBUTING.md). ```bash -pytest -ruff check . -mypy src -python scripts/sync_skill_copies.py --check +pip install -e ".[dev,mcp]" +pytest && ruff check src tests && mypy src/codebase_index +python tests/eval/run_eval.py --ablate # required for any ranking change ``` -Add user-visible changes under `[Unreleased]` in [CHANGELOG.md](CHANGELOG.md). -See [CONTRIBUTING.md](CONTRIBUTING.md) if present and the repository -instructions for branch and review policy. +## Roadmap + +Next: verified MCP client configs and paged results; an agent task-level evaluation; +a large-monorepo benchmark run; then framework-aware typed edges behind a hand-labelled +graph benchmark. Full list with the "not a claim until it ships" rule: +[docs/ROADMAP.md](docs/ROADMAP.md). History: [CHANGELOG.md](CHANGELOG.md). ## License diff --git a/docs/BENCHMARKS.md b/docs/BENCHMARKS.md index 5b1d7db..604237f 100644 --- a/docs/BENCHMARKS.md +++ b/docs/BENCHMARKS.md @@ -1,134 +1,168 @@ # Benchmarks -`codebase-index` has four benchmark surfaces. Read them with their status in -mind — the whole point of this page is to keep evidence and aspiration separate. - -| Surface | What it is | Status | Use it as | +The rule of this project is *measure improvements, don't assert them*. This page +lists every benchmark surface, what it can and cannot prove, and the exact +numbers we are willing to put in a README. Anything not on this page is not a +claim. + +## The one-paragraph version + +On three public repositories at pinned commits (Flask, Gson, Fastify: Python, +Java, JavaScript), across 450 questions mined from git history, `codebase-index` +put the right file in its top three **55% of the time versus 30% for a +disciplined ripgrep agent**, at roughly the same context cost (3.8k vs 3.5k +tokens per question, same tokenizer on both sides). Every metric delta is +significant at p < 0.001 with paired bootstrap confidence intervals. Raw run: +[tests/eval/results/2026-09-04-public-baselines.md](../tests/eval/results/2026-09-04-public-baselines.md). +Reproduce it with one command (below). That is the headline; the rest of this +page is the fine print. + +## Surfaces + +| Surface | What it measures | Status | Use it as | |---|---|---|---| -| Retrieval eval (`tests/eval/`) | Ranking quality vs a fixed baseline across multiple real repositories, with significance tests | **Proven (relative)** | The gate for ranking changes; measures *deltas*, not absolute superiority | -| Public suite (`tests/benchmark_public.py`) | Deterministic synthetic multi-language fixture with the full metric framework | **Toy/synthetic** | CI regression gate + metric shape, **not** product-quality evidence | -| Smoke/perf (`test_perf_smoke.py`, `test_benchmark_comparison.py`) | Latency + output-size guards on a tiny fixture | **Toy/smoke** | Regression checks only | -| Honest real-repo (`tests/benchmark_honest.py`) | 55k LOC Java repo, recall@3 vs disciplined `rg` baseline, symmetric token accounting | **Proven (one repo)** | The only headline product-quality number we stand behind today | - -### Claims that should NOT be made yet - -Do not write, imply, or ship any of these until a run with published logs exists: - -- Any 10k / 100k / 1M LOC scale or speed claim (no real run at that size). -- "Beats Cursor / Sourcegraph / Codebase-Memory MCP" — no head-to-head exists. -- Per-language *absolute* quality claims beyond Java. The retrieval eval covers - Python, Java and TypeScript, but it measures this system against its own earlier - versions — it says a change helped, not that the product beats an alternative. -- Generic "Nx faster" / "Nx fewer tokens" without naming the baseline and repo. -- Latency claims against external tools — the honest run explicitly does not - headline latency (Python process start dominates; real `rg` is tens of ms). - Version-over-version latency from the retrieval eval is in-process and is only - comparable to other runs of that harness. - -The defensible headline today is exactly: **on one 55k LOC Java repo, recall@3 was -70% (index) vs 40% (`rg`+window), using ~13× fewer answer tokens.** Everything -else is roadmap. - -## Retrieval evaluation (ranking gate) - -See [tests/eval/README.md](../tests/eval/README.md) for the full protocol. In short: -ground truth comes from hand-written queries verified against the source tree and -from commit-subject → changed-files pairs mined from git history (leak-free: the -query text is not in the indexed corpus). Corpora are pooled across languages, one -index per corpus is shared by every variant, and every non-baseline row carries a -paired bootstrap CI and permutation p-value. - -`1.9.0` was measured over 305 queries across Python, Java and TypeScript corpora -against `1.8.0`: MRR +0.027, MAP +0.028, nDCG@10 +0.024, recall@5 +0.031 (all -p < 0.001), with p50 latency 78.6 ms → 51.2 ms. These are version-over-version -ranking deltas on those corpora, not a universal quality claim. - -## Public benchmark suite +| **Public baselines** (`tests/eval/run_baselines.py`) | Index vs `rg`+window and vs repo-map-style context on public repos, symmetric tokens, significance | **Proven, reproducible** | The headline comparison against *not having an index* | +| **Retrieval eval** (`tests/eval/run_eval.py`) | Ranking quality of the shipped default vs the 1.7.0 baseline and one-signal ablations, pooled across corpora, with significance | **Proven (relative)** | The gate every ranking change must pass; measures deltas between versions, not superiority over other tools | +| Public synthetic suite (`tests/benchmark_public.py`) | Full metric framework on a deterministic toy fixture | Toy | CI regression gate for metric *shape*; not product evidence | +| Smoke/perf (`test_perf_smoke.py`, `test_benchmark_comparison.py`) | Latency and output-size guards on a tiny fixture | Toy | Regression checks only | +| Single-repo honest run (`tests/benchmark_honest.py`) | recall@3 and tokens vs `rg` on one 55k LOC Java repository | **Historical, not reproducible by others** | The repository is private; the script is kept because its read model informed the public baselines. Do not quote its numbers | -Run: +## Public baselines (the headline) ```bash -python tests/benchmark_public.py --workdir .tmp-public-benchmark +pip install -e ".[dev]" tiktoken +python tests/eval/run_baselines.py --clone --workdir .tmp-baselines \ + --out tests/eval/results/$(date +%F)-public-baselines ``` -The public suite builds a deterministic multi-language fixture repository and -reports JSON metrics: - -- Retrieval quality: `recall_at_1`, `recall_at_3`, `recall_at_5`, `mrr`, `ndcg_at_5` -- Agent usefulness: `answer_correctness_at_3` -- Token economy: index context tokens versus a grep-window baseline -- Language breakdown: per-language recall and answer-correctness proxy -- Freshness: stale detection after an edit and incremental update latency -- Graph tasks: callers/dependencies/impact checks -- Scale counters: indexed files, symbols, edges, and bytes - -Example output shape: - -```json -{ - "retrieval_quality": { - "recall_at_1": 0.75, - "recall_at_3": 1.0, - "recall_at_5": 1.0, - "mrr": 0.875, - "ndcg_at_5": 0.9077 - }, - "answer_correctness": { - "answer_correctness_at_3": 1.0 - }, - "token_economy": { - "index_tokens_avg": 25.0, - "grep_window_tokens_avg": 72.375, - "compression_vs_grep": 2.895 - } -} -``` - -The CI gate is `tests/test_public_benchmark.py`. It verifies the suite reports -all required metric families and catches obvious quality, freshness, graph, and -token-accounting regressions. - -## Honest real-repo benchmark +`--clone` fetches the three corpora at the commits pinned in the script. Pass +`--repo /path/to/any/git/repo` to benchmark your own repository the same way; +please post the result with the *benchmark report* issue template, especially +when it is unflattering. + +### Protocol + +- **Ground truth** is mined from git history by `tests/eval/gen_queries.py`: the + query is a human-written commit subject, the answer is the set of files that + commit changed. Neither is produced by the retriever, and the subject text is + not in the indexed corpus, so the set is leak-free by construction. The newest + 150 localised commits per repository are used (no merges, reverts, releases, + formatting commits, or sweeping changes over more than four files). +- **Changelog-style files** (`CHANGELOG*`, `CHANGES*`, `HISTORY*`, `NEWS*`) are + excluded both as answers and from the indexed corpus, because they paraphrase + commit subjects. +- **Index side**: `search()` with the shipped defaults (`limit 10`, + `token_budget 1500`, hybrid mode). Context = the full JSON payload the agent + receives, plus the top-3 `recommended_reads` ranges read in full. +- **rg + window**: stop-words dropped from the question, up to six salient terms + searched with real ripgrep, files ranked by match density, an 80-line window + read around the densest hit in each of the top-3 files. Context = the first + 50 lines of `rg` output plus the three windows. +- **repo-map-style**: file paths plus top-level definition signatures packed + under a 2k or 8k token budget, ordered by graph degree (or by identifier + overlap with the question in the *query-aware* variant). This is an + approximation of the *style* of context Aider builds, not Aider itself. A map + is not a ranking, so it is scored on whether the answer file is present at + all, an upper bound on what an agent could do with it. +- **Tokens** are counted with `tiktoken/cl100k_base` on every side. +- **Significance**: paired bootstrap 95% CI and paired permutation p-value, + seeded, index vs rg. + +### Logged run (2026-09-04, codebase-index 1.9.0) + +| corpus | commit | language | files | queries | +|---|---|---|---:|---:| +| pallets/flask | `d318b683` | Python | 229 | 150 | +| google/gson | `b3f4ca20` | Java | 311 | 150 | +| fastify/fastify | `15ebc8e2` | JavaScript | 389 | 150 | + +Pooled, n = 450: + +| method | hit@3 | recall@5 | MRR | packet / listing tokens | + top-3 reads | +|---|---:|---:|---:|---:|---:| +| codebase-index | **0.547** | **0.525** | **0.456** | 2,402 | 3,835 | +| rg + 80-line windows | 0.304 | 0.293 | 0.263 | 1,351 | 3,495 | + +| index − rg | delta | 95% CI | p | +|---|---:|---:|---:| +| hit@3 | +0.242 | [+0.187, +0.298] | < 0.001 | +| recall@5 | +0.231 | [+0.183, +0.278] | < 0.001 | +| MRR | +0.194 | [+0.151, +0.235] | < 0.001 | +| tokens | +340 | [+238, +440] | < 0.001 | + +Repo-map-style context, pooled: the answer file is present in a 2k-token map +38% of the time (42% query-aware) and in an 8k-token map 99% of the time. Those +repositories are small enough that 8k tokens covers nearly every file, so the +8k row says little; on a large monorepo it would not. + +![chart](../assets/benchmark.svg) + +### How to read it honestly + +- The index is **1.8× more likely** to put the answer in the top three than a + disciplined grep agent, at **about 10% more context tokens**. It is not + "13× fewer tokens"; that earlier number came from charging the index for + signature snippets and grep for code windows, which is not symmetric. The + public run charges both sides for what actually enters context. +- The token parity is *new in this line*. Before the `max_read_lines` cap + (1.9.1), the index's follow-through reads averaged 6.8k tokens because a + symbol-aligned chunk can be a whole 1,500-line class; the benchmark found + that, and the cap fixed it without touching ranking (the "uncapped" row in the + raw log has identical quality). +- Ground truth is one commit's files. A question can have a correct answer the + commit did not touch, so absolute numbers understate every method equally. + Read the deltas, and read the confidence interval before the delta. +- Per-corpus results move together (hit@3 index/rg: 0.49/0.23 Flask, + 0.57/0.35 Gson, 0.58/0.33 Fastify), which is the point of pooling three + languages: the effect is not a Python artefact. +- Latency is deliberately not tabulated. The index runs in-process, ripgrep is + a separate binary; neither number is a fair claim against the other. + +## Retrieval evaluation (the ranking gate) + +See [tests/eval/README.md](../tests/eval/README.md) for the full protocol. This +harness compares the shipped ranker with its own earlier version and with +one-signal-off ablations, pooled across corpora, with the same significance +machinery. It cannot say the index beats an alternative; it says whether a +change to the ranker helped. -Run: - -```bash -python tests/benchmark_honest.py --repo /path/to/real/repo --rebuild -``` - -The current documented run is against a 55k LOC Java repository: -[tests/benchmark_honest_RESULTS.md](../tests/benchmark_honest_RESULTS.md). - -That benchmark compares the index against a disciplined grep-window agent with -objective recall@3 ground truth. - -## Smoke/perf benchmark - -`tests/test_benchmark_comparison.py` and `tests/test_perf_smoke.py` guard basic -latency and output-size behavior. They are useful regression checks, not product -quality evidence. - -## Remaining benchmark work (TODO checklist) +`1.9.0` was measured over 305 queries across Python, Java and TypeScript corpora +against `1.8.0`: MRR +0.027, MAP +0.028, nDCG@10 +0.024, recall@5 +0.031 (all +p < 0.001), with p50 latency 78.6 ms → 51.2 ms. Two of the three corpora used +for that run are private; the generator is the reproducible part, and the public +baselines above use the same generator on public repositories. -The public suite has the metric framework; the next step is real, larger, -documented repositories. Each task must publish raw logs alongside any headline -number (the pattern set by `tests/benchmark_honest_RESULTS.md`). +Every ranking signal that ships has an ablation row. 1.9.0 removed two signals +that could not demonstrate a benefit and rejected several plausible ones +(IDF-weighted coverage, stemming, graph propagation, MMR, a file-length prior). -- [ ] **10k LOC public repo** — Recall@1/3/5, MRR, nDCG, token economy; named repo + commit SHA. -- [ ] **100k LOC public repo** — same metrics, plus full index build time and incremental update latency. -- [ ] **1M LOC target** — feasibility + scale counters (files/symbols/edges/bytes); may be partial. -- [ ] **Multi-language repo** (≥3 Tier-A languages) — per-language recall and answer-correctness breakdown. -- [ ] **vs vanilla agent grep/read** — tokens and recall against an undisciplined agent exploring the same questions. -- [ ] **vs repo-map-style context** — tokens and recall against an Aider-repo-map-style context blob. -- [ ] **Graph task benchmark** — `refs`, `impact`, and route→handler→service paths against hand-labeled ground truth. -- [ ] **Answer grading** — human-reviewed expected answers, not just file-level recall proxies. -- [ ] **Framework graph tasks** — migrations, config consumers, CI/infra wiring once typed edges land. +## Claims that must NOT be made -How to add one without overclaiming: +Do not write, imply, or ship any of these until a run with published logs exists: -1. Pick a public repo; record its URL and commit SHA. -2. Derive ground truth independently of the index (e.g. naming convention), so the - index cannot grade its own homework. -3. Use a symmetric token estimator and read window on both sides. -4. Commit the raw run output next to a short `*_RESULTS.md` summary. -5. Only then update README/COMPARISON headline numbers. +- Any 1M-LOC or monorepo scale or speed claim (no real run at that size). +- "Beats Cursor / Sourcegraph / Aider / Codebase-Memory MCP": no head-to-head + exists. The repo-map baseline here is a style approximation, not Aider. +- Any *token savings* multiplier without naming the read model. Under + symmetric accounting the index costs about the same as disciplined grep. +- Latency comparisons against external tools. +- Any statement about *LLM agent task success*. The baselines here are + retrieval metrics; nobody has measured whether an agent with the index + finishes tasks faster or better. That is the most important open item. + +## Future work, in priority order + +1. **Agent task-level evaluation**: the same questions, an actual coding agent + (Claude Code or Codex CLI) with and without the skill, measuring answer + correctness, files read, tokens, and wall time. Needs model calls and a + grading rubric; not started. +2. **Large repository run**: a 500k–1M LOC monorepo, reporting index build + time, incremental update latency, memory, and the same retrieval metrics. +3. **Graph task benchmark**: hand-labelled `refs`, `impact`, and + route → handler → service paths. +4. **Framework-aware edges** benchmark once typed edges land (roadmap). + +How to add a benchmark without overclaiming: pick a public repository and pin +the commit; derive ground truth independently of the index; use one token +estimator and one read model on both sides; commit the raw run next to a short +summary; only then touch README numbers. diff --git a/docs/COMMUNITY_AUDIT.md b/docs/COMMUNITY_AUDIT.md new file mode 100644 index 0000000..9ca7b76 --- /dev/null +++ b/docs/COMMUNITY_AUDIT.md @@ -0,0 +1,262 @@ +# Community readiness audit + +Date: 2026-09-04. Baseline: `main` at 1.9.0 (`b48ed14`). Every finding lists what +was wrong, why it matters for adoption, the fix, and its status on the +`chore/community-readiness` branch. Severity: + +- **P0** blocks adoption or trust +- **P1** major friction +- **P2** worthwhile improvement +- **P3** polish + +Status legend: **fixed** (in this branch), **partial**, **open** (needs the +maintainer or is future work). + +## Summary + +The engineering under the hood was already unusually careful: a shared service +layer for CLI/skill/MCP, golden snapshots for every payload, a coverage gate, +an ablation harness with significance tests. What kept a serious developer from +trusting it in the first minute was the *presentation of evidence*: the README's +headline number came from a private repository with asymmetric token +accounting, several docs showed invented output, three doc pairs contradicted +each other, and the demo image was a mock-up. Those are fixed. The remaining +gaps are things only a maintainer can do (enable Discussions, upload the social +preview, apply labels, record a GIF) and two real benchmark holes (agent +task-level evaluation, a large-monorepo run). + +Scores are the maintainer-facing view; the definitions are at the end. + +| Dimension | Before | After | +|---|---:|---:| +| Technical quality | 72 | 84 | +| Onboarding | 62 | 82 | +| Documentation | 58 | 82 | +| Credibility | 45 | 78 | +| Discoverability | 65 | 70 | +| Contributor readiness | 50 | 82 | +| Demo quality | 30 | 75 | +| Launch readiness | 35 | 72 | + +## P0 + +### P0-1 Headline benchmark was not reproducible and not symmetric — fixed + +- **Wrong:** README, COMPARISON and BENCHMARKS led with "recall@3 70% vs 40%, + ~13× fewer tokens" from `tests/benchmark_honest.py` on `NewTowny`, a private + repository on the maintainer's disk. The 13× compared index signature snippets + (~27 tokens) with grep's 80-line windows. Nobody outside could run it. +- **Why it matters:** the first thing a Hacker News reader does with a benchmark + claim is try to reproduce it. A private corpus plus asymmetric accounting reads + as marketing, and it discredits the genuinely good ablation harness next to it. +- **Fix:** `tests/eval/run_baselines.py` runs on Flask, Gson and Fastify at pinned + commits with git-derived ground truth, real ripgrep, one tokenizer on both sides, + and significance tests. Logged run committed. Honest result: hit@3 0.547 vs 0.304 + (p < 0.001) at roughly equal tokens. The 13× claim is withdrawn everywhere; the + old script is labelled historical. + +### P0-2 Read plan cost more tokens than grep — fixed + +- **Wrong:** the new benchmark found the index's follow-through reads averaged + 6.8k tokens vs 3.5k for grep, because symbol-aligned chunks can span a whole + 1,500-line class and `recommended_reads` handed that span to the agent verbatim. +- **Why it matters:** the product promise is "read less"; under fair accounting it + was reading more. +- **Fix:** `retrieval.max_read_lines` (default 120) caps read-plan entries at the + definition head, with additive `truncated` / `line_end_full` fields. Tokens + 3.8k, ranking quality unchanged (the "uncapped" row in the log is identical on + every quality metric). + +### P0-3 Fabricated examples in user-facing docs — fixed + +- **Wrong:** QUICKSTART showed an `AuthService.ts` results table with a `Score` + column the renderer does not print; INSTALLATION showed an invented `doctor` + transcript; `assets/demo.png` was a mock-up with made-up paths; README's JSON + example was invented. +- **Why it matters:** a user who runs the quick start and sees different columns + assumes the tool is broken or the docs are stale. Either way they leave. +- **Fix:** every example is now captured output on `pallets/flask@d318b683`; + `examples/demo/EXPECTED_OUTPUT.md` is the transcript; the hero image is an SVG + rendered from it by a committed script. + +### P0-4 Plugin wrappers refused four documented commands — fixed + +- **Wrong:** `bin/cbx` and `bin/cbx.ps1` whitelisted ten subcommands; the skill + advertised fourteen. `architecture`, `diff-impact`, `path`, `describe` failed + from the Claude Code plugin with "refusing subcommand". +- **Why it matters:** the plugin is the lowest-friction install path and it broke + the Trace/Predict half of the pitch. +- **Fix:** whitelists aligned; `tests/test_plugin_wrappers.py` pins all four + wrappers to the same set. + +### P0-5 Security policy had no reporting channel — fixed + +- **Wrong:** SECURITY.md said "email the maintainers" with no address and listed + 1.2.x as the supported version. +- **Why it matters:** a tool that reads whole repositories must have a working + private disclosure path; an unreachable policy is worse than none. +- **Fix:** GitHub private vulnerability reporting URL, supported line 1.9.x, + explicit statement that checksums/SBOM are not yet shipped. **Open for + maintainer:** confirm private vulnerability reporting is enabled in repository + settings (Security → Policy). + +### P0-6 MCP server did not start on the current SDK — fixed + +- **Wrong:** `pip install "codebase-index[mcp]"` resolves `mcp>=1.0` to mcp 2.x, + where `FastMCP` was renamed `MCPServer` and the old import path raises. The + server printed "needs the optional extra" with the extra installed, and the + documented `codebase-index mcp --root ` form was rejected because + `--root` was only a global option. CI never noticed: the MCP test files + wrapped *our own module's* import in the skip guard, so on mcp 2.x all 41 MCP + tests skipped silently. +- **Why it matters:** "MCP-ready" was on the README badge line and the MCP path + is how Cursor/Zed/Windsurf/Claude Desktop users arrive. +- **Fix:** import `MCPServer` first, fall back to `FastMCP`; `--root` accepted on + the subcommand; tests skip only when the SDK itself is absent. Verified against + mcp 1.29.1 and 2.1.1 with a stdio initialize / tools/list / healthcheck + round-trip. + +## P1 + +### P1-1 Benchmark corpus leakage — fixed + +`gen_queries.py` documented that changelog-style files were excluded from the +evaluation corpus; the harness never imported the list, and Flask's `CHANGES.rst` +was not matched at all. Fixed in both places with regression tests. The 1.9.0 +version-over-version numbers were measured with the changelog in the corpus; +re-run on this repository after the fix, the shipped default still beats the +1.7.0 baseline on every metric at p ≤ 0.004, so the conclusion stands. + +### P1-2 Three duplicate doc pairs with contradictions — fixed + +`SCHEMA.md` vs `DATABASE_SCHEMA.md` (the latter described columns and tables that +do not exist), `RETRIEVAL.md` vs `RETRIEVAL_PIPELINE.md`, `docs/SECURITY.md` vs +`SECURITY_MODEL.md` (both claimed `doctor` checks that `doctor.py` does not +implement). One canonical page each; the others are redirect stubs. + +### P1-3 Two roadmaps — fixed + +Root `ROADMAP.md` (milestones, stale: said PyPI not shipped) and `docs/ROADMAP.md` +(product). Root is now a milestone history table; docs owns forward work. + +### P1-4 No contributor path — fixed + +CONTRIBUTING pointed at `uv sync --all-extras` (no lockfile) and `ruff format +--check` (not enforced, fails on 88 files). No architecture walk-through for a +newcomer, no recipe for the contributions the project actually wants. Added +`docs/DEVELOPMENT.md` with verified commands and recipes (language, edge kind, +ranking signal, MCP tool, CLI command), rewritten CONTRIBUTING with label → area → +starting-file table and evidence rules for ranking PRs. + +### P1-5 Release notes were auto-generated PR lists — fixed + +The changelog is well written but GitHub releases showed only "What's Changed". +`scripts/release_notes.py` now provides the body from the changelog section, and +an empty section fails the release job. `check_versions.py` refuses a tag whose +name does not match `__version__`. + +### P1-6 Packaging only tested at tag time — fixed + +`python -m build`, `twine check` and the clean-venv install smoke ran only in +`release.yml`. A `package` job now runs them on every PR. + +### P1-7 Stale version pins and wrong config path across docs — fixed + +`@v1.8.0` install pins, `1.4.0` doctor output, `.codeindex.json` (the config +lives at `.claude/cache/codebase-index/config.json`), FAQ MCP tool list missing +three tools, bug template placeholder `0.1.0`. All corrected; +`check_versions.py` and `check_links.py` run in CI so the next drift is caught. + +### P1-8 Discussions disabled, issue chooser allowed blank issues — partial + +`config.yml` now disables blank issues and links Discussions and the security +form. **Open for maintainer:** enable Discussions (Settings → General → +Features) or the link 404s; run `scripts/apply_labels.sh` once to create the +labels the templates and CONTRIBUTING refer to. + +## P2 + +### P2-1 No demo on a real repository — fixed + +`examples/demo-project/README.md` described a project that does not exist in the +repo. Replaced by `examples/demo/` on Flask with scripts for bash and PowerShell, +expected output, and `docs/DEMO.md` with VHS/asciinema recording steps. +**Open for maintainer:** record the GIF (the tape file is ready); the README +uses the static SVG until then. + +### P2-2 README first screen — fixed + +Twelve badges, a mock image, and a value proposition that needed the second +screen to explain "why not grep". Rebuilt: one-sentence proposition, agent list, +real terminal card, 60-second try-it, "why this exists" with the grep / repo-map +/ cloud comparison, evidence with the chart, then everything else. + +### P2-3 Social preview not uploaded — open + +`assets/social-preview.png` exists; `usesCustomOpenGraphImage` is false. Upload +under Settings → Social preview. Links on X/Slack/Discord render without an +image until then. + +### P2-4 `docs/SEO.md` carried stale launch templates — fixed + +Templates quoted v1.3.0 and `git+https` installs. Folded into +`docs/COMMUNITY_LAUNCH.md` and deleted. + +### P2-5 Installer docs in Russian — fixed + +`docs/installer.md` translated; flags verified against `install.sh`. +**Open:** `install.sh`, `install.ps1`, `lib/`, `adapters/` still print Russian +log/help strings. Low impact (the PyPI path is primary) but confusing for +contributors reading the scripts. + +### P2-6 `explain` lacks `--limit` — open + +`search` accepts `--limit`; `explain` does not, contradicting ARCHITECTURE's +"search-family commands accept `--limit`". Small, additive; left for a follow-up +PR since it changes the CLI contract and goldens. + +### P2-7 MCP client configs unverified — open + +Templates for Cursor / VS Code / Zed / Windsurf are marked unverified. Needs a +person with each client to confirm; the SUPPORT page asks for exactly that. + +### P2-8 `workflow_dispatch` on release.yml can publish untagged builds — open + +A manual run publishes whatever `main` builds to PyPI without a tag check. Low +risk while the maintainer is the only one with the button, but consider +requiring a version input. + +## P3 + +- `pyproject.toml` keywords gained `code-graph`, `impact-analysis`, + `claude-code-plugin`; classifiers unchanged. — fixed +- `assets/demo.png` and its generator removed; `gen_assets.py` still needs + Windows fonts (documented). — fixed / open +- `docs/superpowers/` contains internal planning documents. They are honest + history and cost nothing, but a newcomer may mistake them for current + design. A one-line README in that folder would help. — open +- CRLF/LF: `.gitattributes` covers scripts; Markdown files get CRLF on Windows + checkouts, producing noisy `git diff` warnings for Windows contributors. + Consider `* text=auto eol=lf`. — open + +## What was verified, not just read + +- Full test suite green in a clean venv (`pytest`, coverage 84%), `ruff check`, + `mypy`, `sync_skill_copies.py --check`, `check_versions.py`, `check_links.py`, + `python -m build` + `twine check` + `release_smoke.py`. +- Every README command run on Flask at the pinned commit; output captured. +- Public baseline benchmark run twice (before and after the read cap); both logs + kept in the raw JSON (`index (uncapped reads)` row). +- Self-repository retrieval eval re-run after the leakage fix. + +## Score definitions + +- **Technical quality:** tests, contracts, CI matrix, known bugs in shipped paths. +- **Onboarding:** minutes from landing to first useful result, with no guesswork. +- **Documentation:** accuracy first, then coverage, then duplication. +- **Credibility:** can a sceptic reproduce every number and every example? +- **Discoverability:** description, topics, PyPI page, social card, search terms. +- **Contributor readiness:** can a stranger land a useful PR in an evening? +- **Demo quality:** does the demo show real value on real code, reproducibly? +- **Launch readiness:** are the texts, links, and community channels ready for + the first wave of strangers? diff --git a/docs/COMMUNITY_LAUNCH.md b/docs/COMMUNITY_LAUNCH.md new file mode 100644 index 0000000..dda7d2d --- /dev/null +++ b/docs/COMMUNITY_LAUNCH.md @@ -0,0 +1,281 @@ +# Community launch kit + +Drafts for the first public wave, plus the feedback loop that decides what +happens after it. Every text below uses only claims backed by logs in this +repository. Do not add numbers that are not in +[BENCHMARKS.md](BENCHMARKS.md), and do not add social proof that does not exist. + +The one benchmark sentence every post may use: + +> On three public repos (Flask, Gson, Fastify; 450 questions mined from git +> history) the index put the right file in its top three 55% of the time vs 30% +> for a disciplined ripgrep agent, at about the same context tokens (p < 0.001, +> raw logs and a one-command rerun in the repo). + +Repository: https://github.com/denfry/codebase-index · PyPI: `pip install codebase-index` + +## Pre-launch checklist (maintainer) + +- [ ] Enable **Discussions** (Settings → General → Features); `config.yml`, + `SUPPORT.md` and `DEVELOPMENT.md` link to it. +- [ ] Upload `assets/social-preview.png` (Settings → Social preview). +- [ ] Run `scripts/apply_labels.sh` once (needs `gh auth`). +- [ ] Confirm private vulnerability reporting is enabled (Security tab). +- [ ] Record the GIF from `docs/demo.tape` (optional; the SVG card works). +- [ ] Tag the release that contains this branch, so the README, PyPI page and + release notes agree. +- [ ] Repository description (About): *Local code map for AI coding agents: + find, trace, and predict change impact with file:line evidence. Claude + Code · Codex · OpenCode · MCP. No network by default.* +- [ ] Topics (20 max, current set is fine): `ai-agents ai-coding claude-code + cli code-search codebase-indexing codex-cli context-engineering + developer-tools fts5 local-first mcp opencode python rag + semantic-code-search sqlite token-optimization tree-sitter + cursor-alternative`. If a slot frees, swap `cursor-alternative` for + `code-graph`; it describes the product better than a comparison. + +## Hacker News + +**Title** (under 80 chars, no superlatives): + +> Show HN: codebase-index – a local code map so Claude Code/Codex open the right file + +**Post:** + +> I built this because I kept watching Claude Code and Codex grep for a keyword, +> open the wrong file, then read three whole files to recover. On anything past a +> few hundred files that was most of the token bill and a good share of the wrong +> answers. +> +> codebase-index is a Python CLI that indexes a repo into SQLite (FTS5 + Tree-sitter +> symbols + an import/call/reference/inheritance graph) and answers three questions +> with file:line evidence: where is X implemented, what calls this / how does A +> reach B, and what breaks if I change this (including "what does my current diff +> touch"). It installs as a Claude Code skill/plugin, a Codex AGENTS.md block, an +> OpenCode command, or a stdio MCP server; all four call the same service layer. +> +> Things I tried to get right: +> +> - Every answer carries its uncertainty: index freshness, result confidence, +> graph coverage per language, and each edge tagged extracted/inferred/ambiguous, +> so the agent can tell "nothing references this" from "the graph doesn't know". +> - Local only. No network in the base install, no telemetry; secrets excluded +> before parsing and redacted again on output; `doctor --strict` audits that. +> - Measured, not asserted. Ranking signals ship only if they survive an ablation +> with significance tests on a pooled multi-language query set. Against a +> disciplined ripgrep agent on Flask, Gson and Fastify (450 questions mined from +> git history, same tokenizer on both sides) it puts the right file in the top +> three 55% vs 30% at about the same context tokens. Raw logs and a one-command +> rerun are in the repo; I'd genuinely like people to run it on their own repos +> and post the numbers, especially bad ones. +> +> What it is not: an IDE, a cloud search, or an agent. It also does not yet have +> framework-aware edges (routes → handlers → DB) or an agent task-level benchmark; +> both are on the roadmap and I say so in the README rather than implying them. +> +> https://github.com/denfry/codebase-index + +## Reddit + +Post once per community, days apart, with the wording matched to the audience. +Reply to every substantive comment; do not cross-post identical text. + +### r/ClaudeAI (or the Claude Code community) + +> **A local index skill so Claude Code opens the right file instead of grepping around** +> +> I wrote a skill + plugin that gives Claude Code a ranked, line-precise answer to +> "where is X", "who calls this", and "what breaks if I change this" before it +> opens anything. `pip install codebase-index`, `codebase-index init --target +> claude`, done; or `/plugin marketplace add denfry/codebase-index`. +> +> It's local (SQLite + Tree-sitter, no network, no telemetry), and it tells Claude +> when it isn't sure: freshness, confidence, and per-edge extracted/inferred/ +> ambiguous tags. There's also a PostToolUse hook that keeps the index fresh while +> Claude edits. +> +> Numbers, since "it feels better" is worthless: on Flask/Gson/Fastify with 450 +> questions mined from git history, top-3 hit rate 55% vs 30% for a disciplined +> ripgrep agent at about the same context tokens. Raw logs in the repo; the +> benchmark runs on any git repo with one command, and I'd love to see your +> results. +> +> Repo: https://github.com/denfry/codebase-index + +### r/LocalLLaMA / local AI tooling + +> **Local-first code retrieval + graph for coding agents (no embeddings required, MCP server included)** +> +> codebase-index builds a SQLite index of a repository: FTS5 for text, Tree-sitter +> symbols for 12 languages, and an import/call/reference/inheritance graph. It +> serves ranked file:line evidence, reference lookups, dependency paths and +> change-impact analysis over a CLI, a stdio MCP server, and skills for Claude +> Code/Codex/OpenCode. Embeddings are optional (local sentence-transformers via +> sqlite-vec); the default path is purely lexical + structural and makes zero +> network calls. External embedding APIs are refused unless you opt in three +> separate ways. +> +> Benchmarks are the part I care about most: a multi-language ablation harness +> with significance tests, and a public comparison against a ripgrep agent and a +> repo-map-style context blob on Flask/Gson/Fastify (55% vs 30% top-3 hit rate, +> tokens roughly equal). Everything reruns with one command. +> +> https://github.com/denfry/codebase-index + +### r/programming + +> **How I benchmarked a code index against grep without cheating (and what the first run got wrong)** +> +> Short write-up on building a retrieval layer for coding agents and the benchmark +> mistakes I made: a headline number from a private repo, token accounting that +> charged my tool for signatures and grep for whole windows, and a changelog that +> leaked commit subjects into the corpus. The fixed protocol mines ground truth from +> git history (commit subject → files changed), charges both sides with the same +> tokenizer for what actually enters context, and reports paired bootstrap CIs. +> The first honest run showed my read plan cost *more* tokens than grep, which led +> to a real fix. Code, logs, and the harness are all in the repo. +> +> https://github.com/denfry/codebase-index/blob/main/docs/BENCHMARKS.md + +### r/opensource / developer tools + +> **codebase-index: MIT, local, benchmarked code map for AI coding agents; looking for benchmark reproductions** +> +> The most useful contribution right now is running +> `python tests/eval/run_baselines.py --repo /path/to/your/repo` and posting the +> table with the benchmark-report issue template, especially if it's unflattering. +> Second most useful: a Tier-A language spec (Swift, Dart, Scala are the gaps). +> DEVELOPMENT.md gets you from clone to PR in about fifteen minutes. +> +> https://github.com/denfry/codebase-index + +## X / Twitter + +**Short announcement** + +> codebase-index: a local code map for Claude Code, Codex, OpenCode and MCP. +> Find the right file, trace what calls it, predict what a change breaks. SQLite + +> Tree-sitter, no network, no telemetry. `pip install codebase-index` +> https://github.com/denfry/codebase-index + +**Technical announcement** + +> Benchmarked codebase-index against a disciplined ripgrep agent on Flask, Gson +> and Fastify: 450 questions mined from git history, same tokenizer on both sides. +> Top-3 hit rate 0.55 vs 0.30, MRR 0.46 vs 0.26, p<0.001, ~same context tokens. +> Raw logs + one-command rerun: https://github.com/denfry/codebase-index/blob/main/docs/BENCHMARKS.md + +**Thread** + +> 1/ I built codebase-index because Claude Code and Codex kept opening the wrong +> file. Not because they're bad at reading; because grep is bad at deciding. +> Here's what a local code map does differently, with numbers. 🧵 +> +> 2/ Three questions, one SQLite file: FIND (ranked file:line ranges), TRACE +> (callers, shortest dependency path, each edge tagged extracted/inferred/ +> ambiguous), PREDICT (blast radius of a symbol, or of your current diff). +> +> 3/ Every answer carries its uncertainty: index freshness, confidence, graph +> coverage per language. An agent can tell "nothing references this" from "the +> graph doesn't know". grep can't. +> +> 4/ Local only. No network in the base install, no telemetry. Secrets excluded +> before parsing and redacted on output. `codebase-index doctor --strict` audits +> that and fails CI if a gate is off. +> +> 5/ Benchmarks: ground truth mined from git history (commit subject → files +> changed) so neither the tool nor I wrote the answer key. Flask + Gson + Fastify, +> 450 questions, ripgrep with 80-line windows as the baseline, one tokenizer. +> +> 6/ Result: top-3 hit rate 0.55 vs 0.30, MRR 0.46 vs 0.26, p < 0.001, about the +> same context tokens per question. The first run showed my read plan cost MORE +> tokens than grep; the fix (cap reads at the definition head) is in the same PR. +> +> 7/ What it doesn't do yet: framework-aware edges (route → handler → DB), agent +> task-level evaluation, a 1M-LOC run. All on the roadmap, none implied. +> +> 8/ `pip install codebase-index`, `codebase-index init`. Works as a Claude Code +> plugin, Codex AGENTS.md block, OpenCode command, or MCP server. Please run the +> benchmark on your repo and tell me what breaks. +> https://github.com/denfry/codebase-index + +## Discord (developer communities, one message) + +> **codebase-index** — local code map for coding agents (Claude Code / Codex / +> OpenCode / MCP). Ranked file:line search, callers and dependency paths with +> per-edge confidence, and change-impact for a symbol or your current diff. SQLite +> + Tree-sitter, no network, no telemetry. Benchmarked vs a ripgrep agent on +> Flask/Gson/Fastify: top-3 hit rate 55% vs 30% at ~equal tokens, raw logs in the +> repo. `pip install codebase-index` · https://github.com/denfry/codebase-index +> Happy to answer questions here, and benchmark reproductions on your repos are the +> most useful feedback I can get. + +## GitHub release announcement template + +Use `python scripts/release_notes.py` for the body; prepend this header when a +release is worth announcing beyond the changelog. + +> **codebase-index vX.Y.Z** +> +> One paragraph: what changed for a user, in plain words. +> +> **Evidence:** the metric that moved, with the log path +> (`tests/eval/results/-*.md`) or "no ranking change". +> +> **Upgrade:** `pip install -U codebase-index`; run `codebase-index index` if the +> changelog lists a schema bump; installed skills auto-update on the next command +> (`codebase-index skill-update` to force, `skill-rollback` to undo). +> +> _(changelog section follows)_ + +## Where to publish, in order + +Day 0 is the day the release with this branch is tagged. + +| When | Action | Goal | +|---|---|---| +| Day 0 | Tag + PyPI publish; verify `pip install codebase-index==X.Y.Z` on a clean machine; post the GitHub release with the template above | Everything a visitor clicks works | +| Day 0 | Discord message in the Claude Code and one AI-tooling community | Early, technical feedback from people who already use skills/MCP | +| Day 1 | Reddit: Claude Code community post | Install attempts from the primary audience; watch `doctor` bug reports | +| Day 1 | X short announcement + technical announcement | Link in circulation before HN | +| Day 3 | Show HN, morning US Pacific, weekday | Sceptical readers; expect benchmark methodology questions; answer with logs, not adjectives | +| Day 3 | X thread | Reuse HN discussion points | +| Week 1 | Reddit: r/LocalLLaMA and r/programming posts (the benchmark write-up), spaced two days apart | Reach people who care about local and about methodology | +| Week 1 | Submit to awesome-claude-code, awesome-mcp-servers, awesome-code-search lists; PyPI page check | Long-tail discovery | +| Week 2 | Publish a "what people reported" note in Discussions: bugs fixed, benchmark reproductions received, what changed | Show that feedback lands somewhere | +| Week 2 | Patch release with the first-wave fixes | Convert first-week issues into visible responsiveness | +| Month 1 | Pick the highest-signal gap from the loop below and ship it (likely agent task-level benchmark or MCP client verification); post the result where the question was first asked | Retention signal, second wave | + +## Feedback loop + +**Collect** + +- Install failures (`doctor` output) by OS and Python version. +- First-query outcomes: did `search` return a useful top-3 on the user's repo? + Ask for `--json` output with paths redacted if needed. +- Benchmark reproductions via the benchmark-report template, including negative + ones; each becomes a row in a Discussions thread. +- Integration requests by client (Cursor, Zed, Windsurf, VS Code): the + unverified MCP templates need exactly these people. +- Language requests, ranked by how many repos mention them. + +**Metrics that matter**, in order: + +1. Repeat usage: users who run `update`/`search` on day 7 (ask in Discussions; + there is no telemetry and there will not be). +2. Issues from people who are not the maintainer, and how many are closed + within a week. +3. External pull requests merged. +4. Benchmark reproductions posted, with the spread of results. +5. Forks that carry commits. +6. Integration requests and confirmations ("works with Zed 0.x"). +7. PyPI downloads, as a trend only. +8. Stars, as a lagging indicator. + +**Do not optimise for**: stars, follower counts, being first on a list, +"AI tool of the week" placements, or any metric that a bot could move. No +purchased engagement, no automated cross-posting, no fake issues, no +"reminder" comments in other projects' threads, no unsolicited DMs. + +**Respond to feedback with**: a reproduction or a log, a linked commit, or a +roadmap entry that names the gap. Never with a promise that has no issue number. diff --git a/docs/COMPARISON.md b/docs/COMPARISON.md index bed7020..5540628 100644 --- a/docs/COMPARISON.md +++ b/docs/COMPARISON.md @@ -40,12 +40,12 @@ platform. | Agent interface | CLI, Claude Code skill, Codex instructions, OpenCode resources, stdio MCP server | Cursor IDE | Aider CLI | IDE + Sourcegraph platform | MCP clients | Any shell-capable agent | | Retrieval granularity | File, symbol, line range, references, impact graph | IDE-managed code context | Repo map of important files/classes/functions/signatures within token budget | Code search and code graph | Varies by server | File/line text matches | | Offline guarantee | Default local/offline; external embeddings opt-in | Local IDE indexing plus model/provider behavior depends on setup | Local repo map; model calls depend on Aider config | Cloud by default | Varies; often local | Local | -| Benchmarked quality | Public suite with Recall@1/3/5, MRR, nDCG, tokens, freshness, graph tasks; honest 55k LOC Java run | Public methodology not portable to this repo | No direct local benchmark here | Enterprise/product claims; not benchmarked here | Varies | Baseline only | +| Benchmarked quality | Public multi-repo baseline run (Flask, Gson, Fastify) vs `rg`+window and repo-map-style context, with significance; version-over-version ranking eval with ablations | Public methodology not portable to this repo | No direct local benchmark here | Enterprise/product claims; not benchmarked here | Varies | Baseline only | | Multi-repo support | Single repo today | Workspace/project scoped | Current chat repo/worktree | Strong cross-repo support | Varies | Manual | | Language coverage | Tier-A: 12 code languages; Tier-B generic path; configs mostly FTS | IDE proprietary | Tree-sitter repo map support as provided by Aider | Broad enterprise language coverage | Varies | Any text | | Security posture | `.gitignore`/`.codeindexignore`, secret filename gates, redaction, no telemetry, no network by default | Proprietary behavior; depends on settings | Local map, but model provider path depends on Aider config | Requires platform trust and account policy | Varies by server | No built-in redaction | | Update model | Manual `index`/`update`, hooks, optional watcher | IDE-managed | Rebuilt as Aider manages context | Platform-managed | Varies | Always live but manual | -| Extensibility | CLI `--json`; MCP schema v1.0; SQLite local DB | Limited external contract | Aider internals/context | Sourcegraph APIs | MCP by design | Shell pipelines | +| Extensibility | CLI `--json`; MCP envelope `schema_version` 1; SQLite local DB | Limited external contract | Aider internals/context | Sourcegraph APIs | MCP by design | Shell pipelines | ## When to choose what @@ -133,8 +133,8 @@ This is the closest direct alternative, so the comparison is the most careful. OpenCode workflow. - **Transparency:** readable Python, 80% coverage gate, golden CLI snapshots, and a public benchmark suite wired as a CI regression gate. - - **Honest benchmarks:** we publish raw logs (see the 55k LOC Java run) and mark - unproven scale/graph claims as roadmap. + - **Honest benchmarks:** raw logs are committed next to every headline number + (`tests/eval/results/`), and unproven scale/graph claims stay on the roadmap. - **Choose Codebase-Memory MCP when:** you need its broader graph engine, static-binary distribution, or wider language/agent reach today. - **Choose codebase-index when:** you want a simpler, privacy-strict, transparent @@ -157,24 +157,24 @@ The meaningful distinction is different: - Aider's map is good context injection; `codebase-index` aims to be a queryable index with freshness checks, ignore/security gates, and agent-readable packets. -## Competitive benchmark bar +## Benchmark bar -Tiny fixture benchmarks are useful smoke tests, not product evidence. The -competitive bar now includes graph-first systems that report evaluations over -dozens of real repositories, large language coverage, answer-quality scoring, -token savings, and tool-call reductions. +Tiny fixture benchmarks are smoke tests, not product evidence. What this repository +can show today, with raw logs committed: -`codebase-index` now includes a public benchmark suite (`tests/benchmark_public.py`) with: +- **Index vs a disciplined grep agent** on three public repositories (Flask, Gson, + Fastify; 450 git-derived questions): hit@3 0.547 vs 0.304, MRR 0.456 vs 0.263, + p < 0.001, at about 10% more context tokens under symmetric accounting. +- **Index vs repo-map-style context**: a 2k-token signature map contains the answer + file 38–42% of the time; the index puts it in the top three 55% of the time for a + comparable budget. This is an approximation of the repo-map *style*, not a + measurement of Aider. +- **Version over version**: every ranking change passes a pooled, multi-language + ablation with significance testing. -- Retrieval quality: Recall@1/3/5, MRR, nDCG -- Agent usefulness: answer-correctness proxy on the public fixture, plus real-repo recall gate in the honest benchmark -- Token economy: tokens saved versus grep/read, Aider repo-map style context, and vanilla agent exploration -- Languages: separate results for Python, TypeScript, Java, Go, Rust, C#, PHP, and other supported languages -- Freshness: latency from file edit to usable updated result -- Graph tasks: callers, impact, architecture trace, route -> handler -> service -> DB - -The suite is CI-friendly and synthetic today. Real public 10k/100k/1M LOC scale targets remain -the next benchmark milestone. +What it cannot show: head-to-head results against Cursor, Sourcegraph, Continue or +Codebase-Memory MCP (no comparable harness exists), any 1M-LOC scale claim, or +agent task-success rates. See [BENCHMARKS.md](BENCHMARKS.md). ## Positioning diff --git a/pyproject.toml b/pyproject.toml index c967628..4761765 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,8 +11,9 @@ requires-python = ">=3.11" license = { text = "MIT" } authors = [{ name = "codebase-index contributors" }] keywords = [ - "claude-code", "codex-cli", "opencode", "mcp", "code-search", - "semantic-code-search", "codebase-indexing", "codebase-rag", "ai-agents", + "claude-code", "claude-code-plugin", "codex-cli", "opencode", "mcp", "mcp-server", + "code-search", "semantic-code-search", "codebase-indexing", "codebase-rag", + "code-graph", "impact-analysis", "ai-agents", "coding-agents", "context-engineering", "local-first", "tree-sitter", "rag", "sqlite", "fts5", "cli", ] classifiers = [ diff --git a/tests/benchmark_honest_RESULTS.md b/tests/benchmark_honest_RESULTS.md index 4bba730..46e506f 100644 --- a/tests/benchmark_honest_RESULTS.md +++ b/tests/benchmark_honest_RESULTS.md @@ -1,5 +1,11 @@ # Honest benchmark — codebase-index vs no-skill agent (NewTowny) +> **Historical.** The repository under test is private, so nobody else can rerun +> this, and its token accounting (index charged for signature snippets, grep charged +> for 80-line windows) is not symmetric. The reproducible successor is +> `tests/eval/run_baselines.py` on public repositories; see `docs/BENCHMARKS.md`. +> Do not quote the numbers below as product claims. + Script: `tests/benchmark_honest.py` · Raw run: `tests/benchmark_honest_newtowny.txt` Repo under test: `C:/Users/denfry/IdeaProjects/NewTowny` (303 Java files, ~55k LOC; 574 text files indexed) Token counter: `tiktoken/cl100k_base` (real tokens, identical estimator both sides) diff --git a/tests/benchmark_public_RESULTS.md b/tests/benchmark_public_RESULTS.md index b2a28b6..407abce 100644 --- a/tests/benchmark_public_RESULTS.md +++ b/tests/benchmark_public_RESULTS.md @@ -44,9 +44,10 @@ python tests/benchmark_public.py --workdir .tmp-public-benchmark only; the framework-aware edges that would lift it are designed in `docs/superpowers/specs/2026-06-14-typed-framework-edges-design.md` (M13) and not yet implemented. -- Token economy (~3.4× vs an rg+window baseline) is a synthetic-fixture figure; - the real-repo figure (~13× on a 55k LOC Java repo) lives in - `tests/benchmark_honest_RESULTS.md`. +- Token economy (~3.4× vs an rg+window baseline) is a synthetic-fixture figure and + charges the index only for its snippets. Under symmetric accounting on public + repositories (`tests/eval/results/`), the index costs about the same context as a + disciplined grep agent; the quality gap, not a token multiplier, is the headline. ## Raw output diff --git a/tests/eval/README.md b/tests/eval/README.md index 58903a1..878d28c 100644 --- a/tests/eval/README.md +++ b/tests/eval/README.md @@ -88,6 +88,19 @@ best rank. - `dup%` — fraction of returned results that near-duplicate an earlier result - `p50/p95/p99` latency, in-process, excluding interpreter start-up +## Baselines on public repositories + +`run_eval.py` compares the ranker with itself. `run_baselines.py` compares it with +*not having an index*: a disciplined `rg` agent and a repo-map-style context blob, on +Flask, Gson and Fastify at pinned commits, with symmetric token accounting and the +same significance tests. The logged run lives in `results/`; the read models are in +`baselines.py`. + +```bash +python tests/eval/run_baselines.py --clone --workdir .tmp-baselines --out tests/eval/results/-public-baselines +python tests/eval/run_baselines.py --repo ../your-repo # any local git repository +``` + ## Files | File | Role | @@ -96,4 +109,7 @@ best rank. | `harness.py` | Index build, query execution, aggregation, pooling, tables | | `metrics.py` | IR metrics + paired bootstrap / permutation tests | | `gen_queries.py` | Ground-truth generator from git history | +| `baselines.py` | rg+window and repo-map-style read models, symmetric token accounting | +| `run_baselines.py` | Index vs baselines on public repositories, with significance | +| `results/` | Logged runs (raw JSON + Markdown) | | `queries/` | Checked-in query sets | From 02768778ba00933520f6284b5bcc5172332687f0 Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:37:42 +0300 Subject: [PATCH 11/12] docs(readme): absolute image URLs so the PyPI project page renders the visuals Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7ce2c94..96340f7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- codebase-index logo + codebase-index logo

codebase-index

@@ -31,7 +31,7 @@

- Real codebase-index output on Flask: a ranked search with line ranges, references with confidence, and an impact table

@@ -165,7 +165,7 @@ Every delta is significant at p < 0.001 (paired bootstrap CI in the log). Read i Reproduce with `python tests/eval/run_baselines.py --clone`; details, caveats and the repo-map-style comparison are in [BENCHMARKS.md](docs/BENCHMARKS.md). -

Bar chart of hit@3, MRR and context tokens for codebase-index versus ripgrep with windows on Flask, Gson and Fastify

+

Bar chart of hit@3, MRR and context tokens for codebase-index versus ripgrep with windows on Flask, Gson and Fastify

**Every ranking signal is ablated.** A change to the ranker ships only if it is significant on a pooled multi-language query set @@ -179,7 +179,7 @@ quote task-success numbers for this project; there are none. ## How it works -

Architecture: discovery and secret gates, Tree-sitter symbols and edges, local SQLite with FTS5, one service layer behind CLI, skill and MCP; the query path runs intent detection, retrievers, RRF fusion, rerank and a token budget

+

Architecture: discovery and secret gates, Tree-sitter symbols and edges, local SQLite with FTS5, one service layer behind CLI, skill and MCP; the query path runs intent detection, retrievers, RRF fusion, rerank and a token budget

Indexing walks the repository through ignore and secret gates, extracts symbols and import/call/reference/inheritance edges with Tree-sitter for 12 languages, and stores From 97aca99ab839145a600ea03daee46a0fb18a1062 Mon Sep 17 00:00:00 2001 From: denfry Date: Sat, 5 Sep 2026 10:24:29 +0300 Subject: [PATCH 12/12] docs(launch): tick the checklist items applied via gh (labels, Discussions, About, PVR) Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- docs/COMMUNITY_LAUNCH.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/COMMUNITY_LAUNCH.md b/docs/COMMUNITY_LAUNCH.md index dda7d2d..63da4db 100644 --- a/docs/COMMUNITY_LAUNCH.md +++ b/docs/COMMUNITY_LAUNCH.md @@ -16,18 +16,18 @@ Repository: https://github.com/denfry/codebase-index · PyPI: `pip install codeb ## Pre-launch checklist (maintainer) -- [ ] Enable **Discussions** (Settings → General → Features); `config.yml`, +- [x] Enable **Discussions** (Settings → General → Features); `config.yml`, `SUPPORT.md` and `DEVELOPMENT.md` link to it. - [ ] Upload `assets/social-preview.png` (Settings → Social preview). -- [ ] Run `scripts/apply_labels.sh` once (needs `gh auth`). -- [ ] Confirm private vulnerability reporting is enabled (Security tab). +- [x] Run `scripts/apply_labels.sh` once (needs `gh auth`). +- [x] Confirm private vulnerability reporting is enabled (Security tab). - [ ] Record the GIF from `docs/demo.tape` (optional; the SVG card works). - [ ] Tag the release that contains this branch, so the README, PyPI page and release notes agree. -- [ ] Repository description (About): *Local code map for AI coding agents: +- [x] Repository description (About): *Local code map for AI coding agents: find, trace, and predict change impact with file:line evidence. Claude Code · Codex · OpenCode · MCP. No network by default.* -- [ ] Topics (20 max, current set is fine): `ai-agents ai-coding claude-code +- [x] Topics (20 max, current set is fine): `ai-agents ai-coding claude-code cli code-search codebase-indexing codex-cli context-engineering developer-tools fts5 local-first mcp opencode python rag semantic-code-search sqlite token-optimization tree-sitter

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 diff --git a/docs/DEMO.md b/docs/DEMO.md new file mode 100644 index 0000000..726c99b --- /dev/null +++ b/docs/DEMO.md @@ -0,0 +1,103 @@ +# Recording the demo + +The repository ships a reproducible terminal demo, [examples/demo](../examples/demo/), +and a static SVG rendering of its output, `assets/demo-terminal.svg`, which the README +embeds. This page is the recipe for turning the same session into an animated GIF or a +short video without fabricating anything: every frame comes from the real script. + +## What to record + +`examples/demo/run_demo.sh` (or `run_demo.ps1`) against Flask at the pinned commit. +Steps 2, 3, 5 and 6 (search, refs, impact, diff-impact) are the ones worth showing; +the JSON step is for the README text, not for a recording. + +Keep it under 45 seconds. Viewers stop at the first table. + +## Prerequisites + +```bash +pip install codebase-index +git clone https://github.com/pallets/flask.git .tmp-demo/flask +git -C .tmp-demo/flask checkout d318b683471101618febed18996405ad26462110 +``` + +Use a terminal that is 100 columns wide and about 34 rows tall so the Markdown +tables do not wrap. A dark theme with a monospace font at 14-16 px reads best in a +README at 960 px width. + +## Option A: VHS (recommended, fully scripted) + +[VHS](https://github.com/charmbracelet/vhs) renders a `.tape` script to GIF/MP4/WebM +deterministically, so the recording is as reproducible as the demo itself. + +`docs/demo.tape`: + +```tape +Output assets/demo.gif +Set FontSize 15 +Set Width 1200 +Set Height 720 +Set Theme "Catppuccin Mocha" +Set TypingSpeed 40ms + +Type "cd .tmp-demo/flask && codebase-index index" +Enter +Sleep 4s + +Type 'codebase-index search "where is the session cookie signed and saved" --limit 3' +Enter +Sleep 5s + +Type "codebase-index refs open_session" +Enter +Sleep 4s + +Type "codebase-index impact SecureCookieSessionInterface --direction up --depth 2" +Enter +Sleep 5s + +Type "codebase-index diff-impact" +Enter +Sleep 6s +``` + +Make the throwaway edit before recording (`run_demo.sh` shows the `sed` line) so +`diff-impact` has something to report, and revert it afterwards. + +```bash +vhs docs/demo.tape +``` + +## Option B: asciinema + agg + +```bash +asciinema rec --cols 100 --rows 34 demo.cast +bash examples/demo/run_demo.sh .tmp-demo/flask +exit +agg --theme monokai --font-size 15 demo.cast assets/demo.gif +``` + +Trim the `.cast` file to the interesting steps before converting; `agg` supports +`--idle-time-limit 2` to compress pauses. + +## Option C: static SVG (what the README uses today) + +`scripts/gen_terminal_svg.py` renders a captured transcript into an SVG "terminal +card". It has no dependencies and is what produced `assets/demo-terminal.svg`: + +```bash +bash examples/demo/run_demo.sh .tmp-demo/flask > /tmp/demo.txt 2>&1 +python scripts/gen_terminal_svg.py /tmp/demo.txt --out assets/demo-terminal.svg +``` + +The script strips ANSI colour, keeps the `$ command` lines highlighted, and cuts at +the first `--json` step so the card stays short. + +## Checklist before publishing a recording + +- The commit hash in the first frame matches `examples/demo/run_demo.sh`. +- No path from your home directory is visible (use a relative `.tmp-demo`). +- File size: GIF under 3 MB for the README; put the MP4 in the GitHub release, not + in git. +- Update `examples/demo/EXPECTED_OUTPUT.md` if the codebase-index version changed and + the output moved. diff --git a/docs/demo.tape b/docs/demo.tape new file mode 100644 index 0000000..79f1c59 --- /dev/null +++ b/docs/demo.tape @@ -0,0 +1,29 @@ +# VHS tape for the README demo (see docs/DEMO.md). Run from the repository root +# after cloning Flask into .tmp-demo/flask at the pinned commit: +# vhs docs/demo.tape +Output assets/demo.gif +Set FontSize 15 +Set Width 1200 +Set Height 720 +Set Theme "Catppuccin Mocha" +Set TypingSpeed 40ms + +Type "cd .tmp-demo/flask && codebase-index index" +Enter +Sleep 4s + +Type 'codebase-index search "where is the session cookie signed and saved" --limit 3' +Enter +Sleep 5s + +Type "codebase-index refs open_session" +Enter +Sleep 4s + +Type "codebase-index impact SecureCookieSessionInterface --direction up --depth 2" +Enter +Sleep 5s + +Type "codebase-index diff-impact" +Enter +Sleep 6s diff --git a/examples/demo/EXPECTED_OUTPUT.md b/examples/demo/EXPECTED_OUTPUT.md new file mode 100644 index 0000000..5f1e4ac --- /dev/null +++ b/examples/demo/EXPECTED_OUTPUT.md @@ -0,0 +1,187 @@ +# Expected output + +Captured by running `examples/demo/run_demo.sh` against +[pallets/flask](https://github.com/pallets/flask) at commit `d318b683` with +codebase-index 1.9.0 on 2026-09-04. Timestamps and ordering of equal-score rows +may differ on your machine; file paths, line ranges and edge kinds should not. + +```text +$ codebase-index index +Indexed 230 files (0 pruned). + parse failures: 0; tree-sitter files with 0 symbols: 17 + +$ codebase-index search where is the session cookie signed and saved --limit 5 +**Query:** where is the session cookie signed and saved +**Intent:** `locate_impl` · **Confidence:** medium + +| # | Path | Lines | Reason | +|---|------|-------|--------| +| 1 | `src/flask/sessions.py` | 24-54 | in src/flask/ · 2 callers · source prior +0.08 | +| 2 | `src/flask/sessions.py` | 284-385 | in src/flask/ · 2 callers · source prior +0.08 | +| 3 | `src/flask/sessions.py` | 57-80 | in src/flask/ · 1 callers · source prior +0.08 | +| 4 | `tests/test_basic.py` | 542-600 | source prior -0.06 · generated/test demoted | +| 5 | `tests/test_reqctx.py` | 201-249 | source prior -0.06 · generated/test demoted | + +`src/flask/sessions.py:24-54` +``` +class SessionMixin(MutableMapping[str, t.Any]): +``` +`src/flask/sessions.py:284-385` +``` +class SecureCookieSessionInterface(SessionInterface): +``` +`src/flask/sessions.py:57-80` +``` +class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): +``` +`tests/test_basic.py:542-600` +``` +def test_session_vary_cookie(app, client): +``` +`tests/test_reqctx.py:201-249` +``` +def test_session_dynamic_cookie_name(): +``` + +**Recommended reads:** +- `src/flask/sessions.py:24-54` +- `src/flask/sessions.py:284-385` +- `src/flask/sessions.py:57-80` +- `tests/test_basic.py:542-600` +- `tests/test_reqctx.py:201-249` + +$ codebase-index refs open_session +**query:** open_session | **index:** fresh + +| kind | path | line | confidence | +|------|------|------|------------| +| call | `src/flask/ctx.py` | 388 | ? ambiguous | +| definition | `src/flask/sessions.py` | 249 | exact | +| definition | `src/flask/sessions.py` | 323 | exact | +| call | `src/flask/testing.py` | 165 | ? ambiguous | +| definition | `tests/test_reqctx.py` | 182 | exact | +| definition | `tests/test_session_interface.py` | 16 | exact | + + +$ codebase-index path wsgi_app dispatch_request +**path:** `wsgi_app` → `dispatch_request` · **2 hop(s)** + +`wsgi_app` (src/flask/app.py) + → _call_ → +`full_dispatch_request` (src/flask/app.py) + → _call_ → +`dispatch_request` (src/flask/app.py) + + +$ codebase-index impact SecureCookieSessionInterface --direction up --depth 2 +**impact:** `SecureCookieSessionInterface` · **direction:** up · **depth:** 2 · **affected files:** 2 + +| dist | via | kind | node | location | +|------|-----|------|------|----------| +| 1 | call | symbol | `Flask` | `src/flask/app.py:110` | +| 1 | extends | symbol | `PathAwareSessionInterface` | `tests/test_reqctx.py:204` | +| 2 | call | symbol | `CustomFlask` | `tests/test_reqctx.py:211` | + + +$ codebase-index diff-impact +**diff impact:** `HEAD` → working tree +**direction:** `up` · **depth:** 2 + +**changed files (1):** +- `src/flask/sessions.py` + +**affected files (8):** +| distance | path | changed by | edge | confidence | +|---:|---|---|---|---| +| 1 | `src/flask/app.py` | `src/flask/sessions.py` | call | extracted | +| 1 | `src/flask/ctx.py` | `src/flask/sessions.py` | call | extracted | +| 1 | `src/flask/globals.py` | `src/flask/sessions.py` | extends | extracted | +| 1 | `src/flask/testing.py` | `src/flask/sessions.py` | call | extracted | +| 1 | `tests/test_reqctx.py` | `src/flask/sessions.py` | extends | extracted | +| 1 | `tests/test_session_interface.py` | `src/flask/sessions.py` | extends | extracted | +| 2 | `tests/test_signals.py` | `src/flask/sessions.py` | call | extracted | +| 2 | `tests/test_testing.py` | `src/flask/sessions.py` | call | extracted | + + +$ codebase-index --json search where is the session cookie signed and saved --limit 3 +{ + "query": "where is the session cookie signed and saved", + "intent": "locate_impl", + "mode": "hybrid", + "index": { + "exists": true, + "stale": false, + "files_changed_since_build": 0, + "built_at": "2026-09-04T13:13:31Z", + "head_commit": "d318b683471101618febed18996405ad26462110" + }, + "confidence": "medium", + "results": [ + { + "path": "src/flask/sessions.py", + "line_start": 24, + "line_end": 54, + "symbols": [ + "SessionMixin" + ], + "score": 1.9301, + "reason": "in src/flask/ · 2 callers · source prior +0.08", + "token_est": 11, + "rank": 1, + "skeletonized": false, + "elided_lines": 0, + "snippet": "class SessionMixin(MutableMapping[str, t.Any]):" + }, + { + "path": "src/flask/sessions.py", + "line_start": 284, + "line_end": 385, + "symbols": [ + "SecureCookieSessionInterface" + ], + "score": 1.8766, + "reason": "in src/flask/ · 2 callers · source prior +0.08", + "token_est": 13, + "rank": 2, + "skeletonized": false, + "elided_lines": 0, + "snippet": "class SecureCookieSessionInterface(SessionInterface):" + }, + { + "path": "src/flask/sessions.py", + "line_start": 57, + "line_end": 80, + "symbols": [ + "SecureCookieSession" + ], + "score": 1.8679, + "reason": "in src/flask/ · 1 callers · source prior +0.08", + "token_est": 16, + "rank": 3, + "skeletonized": false, + "elided_lines": 0, + "snippet": "class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin):" + } + ], + "recommended_reads": [ + { + "path": "src/flask/sessions.py", + "line_start": 24, + "line_end": 54 + }, + { + "path": "src/flask/sessions.py", + "line_start": 284, + "line_end": 385 + }, + { + "path": "src/flask/sessions.py", + "line_start": 57, + "line_end": 80 + } + ], + "fallback_suggestions": {} +} + +Done. Index lives in C:/Users/dabin/AppData/Local/Temp/claude/D--Projects-codebase-index/7f35eb46-c8c9-49e7-9d02-4c510c6877d0/scratchpad/repos/flask/.claude/cache/codebase-index/ and nothing was sent anywhere. +``` diff --git a/examples/demo/README.md b/examples/demo/README.md new file mode 100644 index 0000000..de4451e --- /dev/null +++ b/examples/demo/README.md @@ -0,0 +1,53 @@ +# Demo: Find, Trace, Predict on a real repository + +This demo runs `codebase-index` against [Flask](https://github.com/pallets/flask) +(about 230 files, 18k lines of Python) at a pinned commit, so what you see is what +[EXPECTED_OUTPUT.md](EXPECTED_OUTPUT.md) shows. It takes under a minute and makes no +network requests after the clone. + +```bash +pip install codebase-index +bash examples/demo/run_demo.sh # or: pwsh examples/demo/run_demo.ps1 +``` + +Pass a path to reuse an existing Flask checkout: `bash examples/demo/run_demo.sh ~/src/flask`. + +## What it shows + +| Step | Question | Command | What to look at | +|---|---|---|---| +| Find | Where is the session cookie signed and saved? | `codebase-index search "where is the session cookie signed and saved" --limit 5` | All three top hits are in `src/flask/sessions.py` with exact line ranges; tests are ranked below implementation ("generated/test demoted"). | +| Trace | Who calls `open_session`? | `codebase-index refs open_session` | Definitions vs calls, and the confidence column: two callers are marked `ambiguous` because several classes define `open_session`. The tool says so instead of guessing. | +| Trace | How does a request reach the view? | `codebase-index path wsgi_app dispatch_request` | A two-hop call chain `wsgi_app → full_dispatch_request → dispatch_request`, all in `app.py`. | +| Predict | What could break if `SecureCookieSessionInterface` changes? | `codebase-index impact SecureCookieSessionInterface --direction up --depth 2` | Upstream dependents at distance 1 and 2, with the edge kind (`call`, `extends`) that connects them. | +| Predict | What does my current diff affect? | edit `sessions.py`, then `codebase-index diff-impact` | Eight affected files ranked by distance from the changed file, each with the edge kind and `extracted` confidence. | +| Agent view | Same search as JSON | `codebase-index --json search ... --limit 3` | The packet an agent receives: freshness (`index`), `confidence`, ranked results with `snippet`, and `recommended_reads` with line ranges. | + +## Things worth noticing + +- **Confidence is part of the answer.** `refs` labels edges `exact` or `ambiguous`; + `impact` and `diff-impact` carry `extracted` / `inferred` / `ambiguous`. An agent can + distinguish "nothing references this" from "the graph is unsure". +- **The read plan is bounded.** `recommended_reads` entries longer than + `retrieval.max_read_lines` (default 120) are capped at the definition head and marked + `truncated: true` with `line_end_full`, so a 1,500-line class does not become a + 1,500-line read. +- **Tests are demoted, not hidden.** Search ranks `tests/test_basic.py` below + `src/flask/sessions.py` for an implementation question, but still lists it. +- **Nothing left the machine.** The index is a SQLite file under + `.claude/cache/codebase-index/`; `codebase-index doctor --strict` audits the + network-off default. + +## Try your own questions + +```bash +cd .tmp-demo/flask +codebase-index explain "how are blueprints registered" +codebase-index describe dispatch_request +codebase-index architecture +codebase-index search "jsonify" --mode symbol +``` + +To run the same demo on your own repository, replace the clone step with `cd your-repo` +and keep the rest. For recording a GIF or video of this session, see +[docs/DEMO.md](../../docs/DEMO.md). diff --git a/examples/demo/run_demo.ps1 b/examples/demo/run_demo.ps1 new file mode 100644 index 0000000..7330f8f --- /dev/null +++ b/examples/demo/run_demo.ps1 @@ -0,0 +1,42 @@ +# Reproducible demo of codebase-index on a real public repository (Flask). +# +# pwsh examples/demo/run_demo.ps1 # clones Flask into .\.tmp-demo +# pwsh examples/demo/run_demo.ps1 C:\src\flask # use an existing checkout +# +# Mirrors run_demo.sh. Nothing leaves the machine. +param([string]$WorkDir = ".tmp-demo\flask") +$ErrorActionPreference = "Stop" +$RepoUrl = "https://github.com/pallets/flask.git" +$RepoSha = "d318b683471101618febed18996405ad26462110" +$env:CBX_NO_SKILL_AUTO_UPDATE = "1" + +function Step { param([Parameter(ValueFromRemainingArguments)] [string[]]$Cmd) + Write-Host "`n$ $($Cmd -join ' ')" -ForegroundColor Blue + & $Cmd[0] @($Cmd[1..($Cmd.Length - 1)]) + if ($LASTEXITCODE -ne 0) { throw "command failed: $($Cmd -join ' ')" } +} + +if (-not (Get-Command codebase-index -ErrorAction SilentlyContinue)) { + Write-Error "codebase-index is not on PATH. Install it first: pip install codebase-index" +} +if (-not (Test-Path (Join-Path $WorkDir ".git"))) { + Write-Host "Cloning Flask into $WorkDir ..." + git clone --quiet $RepoUrl $WorkDir +} +git -C $WorkDir checkout --quiet $RepoSha +Set-Location $WorkDir + +Step codebase-index index +Step codebase-index search "where is the session cookie signed and saved" --limit 5 +Step codebase-index refs open_session +Step codebase-index path wsgi_app dispatch_request +Step codebase-index impact SecureCookieSessionInterface --direction up --depth 2 + +$sessions = "src/flask/sessions.py" +(Get-Content $sessions -Raw) -replace "(?m)^ def open_session\(", " def open_session( # demo edit" | + Set-Content $sessions -NoNewline -Encoding utf8 +Step codebase-index diff-impact +git checkout --quiet -- $sessions + +Step codebase-index --json search "where is the session cookie signed and saved" --limit 3 +Write-Host "`nDone. Index lives in $WorkDir\.claude\cache\codebase-index\ and nothing was sent anywhere." diff --git a/examples/demo/run_demo.sh b/examples/demo/run_demo.sh new file mode 100644 index 0000000..794b635 --- /dev/null +++ b/examples/demo/run_demo.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Reproducible demo of codebase-index on a real public repository (Flask). +# +# bash examples/demo/run_demo.sh # clones Flask into ./.tmp-demo +# bash examples/demo/run_demo.sh /path/to/flask # use an existing checkout +# +# Every command below is what an agent (or you) would run; the output is what +# examples/demo/EXPECTED_OUTPUT.md was captured from. Nothing leaves the machine. +set -euo pipefail + +REPO_URL="https://github.com/pallets/flask.git" +REPO_SHA="d318b683471101618febed18996405ad26462110" # pinned so the output is stable +WORKDIR="${1:-.tmp-demo/flask}" +export CBX_NO_SKILL_AUTO_UPDATE=1 + +step() { printf '\n\033[1;34m$ %s\033[0m\n' "$*"; "$@"; } + +if ! command -v codebase-index >/dev/null 2>&1; then + echo "codebase-index is not on PATH. Install it first: pip install codebase-index" >&2 + exit 1 +fi + +if [ ! -d "$WORKDIR/.git" ]; then + echo "Cloning Flask into $WORKDIR ..." + git clone --quiet "$REPO_URL" "$WORKDIR" +fi +git -C "$WORKDIR" checkout --quiet "$REPO_SHA" +cd "$WORKDIR" + +# 1. Build the index (about 230 files; a few seconds). +step codebase-index index + +# 2. FIND — where is the session cookie signed and saved? +step codebase-index search "where is the session cookie signed and saved" --limit 5 + +# 3. TRACE — who calls open_session? (definitions, callers, edge confidence) +step codebase-index refs open_session + +# 4. TRACE — how does a WSGI request reach the view function? +step codebase-index path wsgi_app dispatch_request + +# 5. PREDICT — what could break if SecureCookieSessionInterface changes? +step codebase-index impact SecureCookieSessionInterface --direction up --depth 2 + +# 6. PREDICT — what does my current diff affect? (make a throwaway edit, then revert) +sed -i.bak 's/^ def open_session(/ def open_session( # demo edit/' src/flask/sessions.py +step codebase-index diff-impact +git checkout --quiet -- src/flask/sessions.py && rm -f src/flask/sessions.py.bak + +# 7. The same packet as an agent sees it. +step codebase-index --json search "where is the session cookie signed and saved" --limit 3 + +printf '\nDone. Index lives in %s/.claude/cache/codebase-index/ and nothing was sent anywhere.\n' "$WORKDIR" diff --git a/scripts/gen_assets.py b/scripts/gen_assets.py index 0158659..19aef5f 100644 --- a/scripts/gen_assets.py +++ b/scripts/gen_assets.py @@ -9,7 +9,6 @@ Outputs: assets/mark.png 256x256 -> logo / avatar source assets/social-preview.png 1280x640 -> upload in Settings -> Social preview - assets/demo.png 1200x760 -> README product story """ from __future__ import annotations @@ -247,115 +246,6 @@ def build_social(out: Path) -> None: # --------------------------------------------------------------------------- # # README demo still: 1200 x 760 # --------------------------------------------------------------------------- # -def build_demo(out: Path) -> None: - W, H = 1200, 760 - w, h = W * SS, H * SS - img = gradient_bg(w, h) - add_glow(img, int(w * 0.5), int(h * -0.05), 460 * SS, BLUE, 26) - d = ImageDraw.Draw(img) - - # Header - wm = f_mono_b(34) - d.text((48 * SS, 46 * SS), "codebase", font=wm, fill=FG) - seg = d.textlength("codebase", font=wm) - d.text((48 * SS + seg, 46 * SS), "-index", font=wm, fill=BLUE) - d.text((48 * SS, 94 * SS), "One local map. Three engineering jobs.", - font=f_ui(23), fill=FG2) - - cards = [ - { - "title": "FIND", - "color": BLUE, - "query": '"where is auth implemented?"', - "lines": [ - ("01", "src/auth/AuthService.ts:12", "0.92"), - ("02", "src/routes/auth.ts:20", "0.78"), - ("03", "src/middleware/auth.ts:5", "0.65"), - ], - }, - { - "title": "TRACE", - "color": PURPLE, - "query": '"checkout → database"', - "lines": [ - ("", "CheckoutRoute", "extracted"), - ("→", "CheckoutService", "extracted"), - ("→", "OrderRepository", "inferred"), - ], - }, - { - "title": "PREDICT", - "color": GREEN, - "query": '"what changes with User?"', - "lines": [ - ("1", "direct dependents", "7"), - ("2", "affected tests", "4"), - ("!", "inferred edges", "2"), - ], - }, - ] - - card_w = 352 - card_h = 430 - gap = 24 - x_start = 48 - y0 = 162 - for idx, card in enumerate(cards): - x0 = (x_start + idx * (card_w + gap)) * SS - y = y0 * SS - x1 = x0 + card_w * SS - y1 = y + card_h * SS - d.rounded_rectangle( - [x0, y, x1, y1], radius=18 * SS, fill=PANEL, - outline=BORDER, width=SS + SS // 2, - ) - d.rounded_rectangle( - [x0 + 20 * SS, y + 20 * SS, x0 + 102 * SS, y + 51 * SS], - radius=15 * SS, fill=PANEL_BAR, outline=card["color"], width=SS, - ) - d.text((x0 + 34 * SS, y + 27 * SS), card["title"], - font=f_ui_b(15), fill=card["color"]) - d.text((x0 + 22 * SS, y + 78 * SS), card["query"], - font=f_mono(17), fill=CYAN) - d.line( - [x0 + 22 * SS, y + 122 * SS, x1 - 22 * SS, y + 122 * SS], - fill=BORDER, width=SS, - ) - - row_y = y + 150 * SS - for lead, label, tail in card["lines"]: - d.text((x0 + 24 * SS, row_y), lead, font=f_mono_b(17), - fill=card["color"]) - d.text((x0 + 58 * SS, row_y), label, font=f_mono(16), fill=FG2) - tw = d.textlength(tail, font=f_mono_b(15)) - d.text((x1 - 24 * SS - tw, row_y + 2 * SS), tail, - font=f_mono_b(15), - fill=YELLOW if tail == "inferred" else card["color"]) - row_y += 55 * SS - - d.text((x0 + 24 * SS, y1 - 58 * SS), - ("precise file:line evidence" if idx == 0 else - "auditable dependency chain" if idx == 1 else - "ranked blast radius"), - font=f_ui(15), fill=MUTED) - - # Evidence strip - sy = 626 * SS - d.rounded_rectangle( - [48 * SS, sy, (W - 48) * SS, 702 * SS], - radius=16 * SS, fill=PANEL_BAR, outline=BORDER, width=SS, - ) - d.text((72 * SS, sy + 25 * SS), "EVIDENCE CONTRACT", font=f_ui_b(15), fill=GREEN) - d.text((246 * SS, sy + 23 * SS), - "freshness · confidence · coverage · recommended reads", - font=f_mono(17), fill=FG2) - - d.text((48 * SS, 726 * SS), - "local by default · no telemetry · CLI + Skill + MCP", - font=f_ui(15), fill=MUTED) - - downsave(img, W, H, out) - def main() -> None: root = Path(__file__).resolve().parent.parent @@ -363,7 +253,6 @@ def main() -> None: print("Generating assets:") build_mark(assets / "mark.png") build_social(assets / "social-preview.png") - build_demo(assets / "demo.png") print("Done.") diff --git a/scripts/gen_benchmark_chart.py b/scripts/gen_benchmark_chart.py new file mode 100644 index 0000000..a81a585 --- /dev/null +++ b/scripts/gen_benchmark_chart.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Render the logged public-baseline benchmark as an SVG bar chart. + + python scripts/gen_benchmark_chart.py \ + tests/eval/results/2026-09-04-public-baselines.json --out assets/benchmark.svg + +Reads the raw JSON that `tests/eval/run_baselines.py --out` wrote, so the chart +can never drift from the logged run. Stdlib only. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from xml.sax.saxutils import escape + +FONT = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif" +INK = "#1f2328" +MUTED = "#656d76" +GRID = "#d0d7de" +INDEX = "#0969da" # index +RG = "#8c959f" # grep baseline +CARD = "#ffffff" +BORDER = "#d0d7de" + + +def bar_panel(x: int, y: int, w: int, h: int, title: str, rows: list[tuple[str, float, float]], + *, vmax: float, fmt: str, unit: str = "") -> str: + """Horizontal grouped bars: for each row label, an index bar and an rg bar.""" + out = [f'{escape(title)}'] + label_w = 70 + plot_x = x + label_w + plot_w = w - label_w - 70 + row_h = 40 + top = y + 14 + # grid lines + for i in range(0, 5): + gx = plot_x + plot_w * i / 4 + out.append(f'') + out.append(f'{(vmax * i / 4):{fmt}}{unit}') + for i, (label, a, b) in enumerate(rows): + ry = top + i * row_h + out.append(f'{escape(label)}') + for j, (val, colour) in enumerate(((a, INDEX), (b, RG))): + bw = plot_w * min(val, vmax) / vmax + by = ry + 6 + j * 15 + out.append(f'') + out.append(f'{val:{fmt}}{unit}') + return "\n".join(out) + + +def render(report: dict) -> str: + pooled = report["pooled"]["summary"] + n = report["pooled"]["n_queries"] + corpora = report["corpora"] + sig = report["pooled"]["index_vs_rg"] + + W, H = 960, 470 + parts = [ + f'', + 'codebase-index vs rg+window on public repositories', + f'', + f'Index vs disciplined grep on public repositories', + f'' + f'{escape(", ".join(c["corpus"] for c in corpora))} at pinned commits · {n} git-derived queries · ' + f'codebase-index {escape(report["version"])} · {escape(report["date"])} · same tokenizer on both sides', + # legend + f'codebase-index', + f'rg + 80-line windows', + ] + + # Panel 1: hit@3 per corpus + pooled + rows = [(c["corpus"], c["summary"]["index"]["hit@3"], c["summary"]["rg+window"]["hit@3"]) for c in corpora] + rows.append(("pooled", pooled["index"]["hit@3"], pooled["rg+window"]["hit@3"])) + parts.append(bar_panel(24, 90, 440, 200, "hit@3 — answer file among the top 3", rows, vmax=1.0, fmt=".2f")) + + # Panel 2: MRR per corpus + pooled + rows = [(c["corpus"], c["summary"]["index"]["MRR"], c["summary"]["rg+window"]["MRR"]) for c in corpora] + rows.append(("pooled", pooled["index"]["MRR"], pooled["rg+window"]["MRR"])) + parts.append(bar_panel(500, 90, 440, 200, "MRR — how high the first correct file ranks", rows, vmax=1.0, fmt=".2f")) + + # Panel 3: tokens (packet/listing + top-3 reads), pooled + rows = [ + ("packet", pooled["index"]["packet_tokens_mean"], pooled["rg+window"]["packet_tokens_mean"]), + ("+ reads", pooled["index"]["tokens_mean"], pooled["rg+window"]["tokens_mean"]), + ] + vmax = max(r[1] for r in rows + [("", 0, 0)]) * 1.15 + vmax = max(vmax, max(r[2] for r in rows) * 1.15) + parts.append(bar_panel(24, 320, 440, 110, "context tokens per query (pooled mean)", rows, vmax=round(vmax, -2), fmt=",.0f")) + + # Significance box + y = 320 + parts.append(f'Paired significance, index − rg (pooled)') + for i, (k, s) in enumerate(sig.items()): + fmt = ",.0f" if k == "tokens" else "+.3f" + p_text = "< 0.001" if s["p"] < 0.001 else f"= {s['p']:.3f}" + line = f'{k}: {s["delta"]:{fmt}} 95% CI [{s["ci_lo"]:{fmt}}, {s["ci_hi"]:{fmt}}] p {p_text}' + parts.append(f'{escape(line)}') + parts.append(f'Ground truth: commit subject → files that commit changed. Raw run: tests/eval/results/.') + parts.append(f'Latency is not charted: in-process index vs a separate ripgrep binary is not a fair pair.') + parts.append("") + return "\n".join(parts) + "\n" + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("results", type=Path) + ap.add_argument("--out", type=Path, required=True) + args = ap.parse_args(argv) + report = json.loads(args.results.read_text(encoding="utf-8")) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(render(report), encoding="utf-8") + print(f"wrote {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/gen_terminal_svg.py b/scripts/gen_terminal_svg.py new file mode 100644 index 0000000..fc2bace --- /dev/null +++ b/scripts/gen_terminal_svg.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +"""Render a captured terminal transcript as a static SVG "terminal card". + + bash examples/demo/run_demo.sh .tmp-demo/flask > demo.txt 2>&1 + python scripts/gen_terminal_svg.py demo.txt --out assets/demo-terminal.svg + +Why an SVG and not a screenshot: the card is regenerated from a real transcript +(see docs/DEMO.md), diffable in git, crisp at any width, and never shows text the +tool did not actually print. Stdlib only. +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path +from xml.sax.saxutils import escape + +ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") + +# GitHub-dark inspired palette; the card carries its own background so it reads +# identically on light and dark README themes. +BG = "#0d1117" +BAR = "#161b22" +BORDER = "#30363d" +FG = "#e6edf3" +MUTED = "#8b949e" +BLUE = "#58a6ff" +GREEN = "#3fb950" +PURPLE = "#bc8cff" +AMBER = "#d29922" + +CHAR_W = 8.4 # px per monospace character at 14px +LINE_H = 20 +PAD = 18 +FONT = "ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" + + +def load_transcript(path: Path, *, stop_at: str | None, max_lines: int) -> list[str]: + lines = [] + for raw in path.read_text(encoding="utf-8", errors="replace").splitlines(): + line = ANSI_RE.sub("", raw).rstrip() + if stop_at and line.startswith("$ ") and stop_at in line: + break + if line.startswith("```"): + continue # fence markers carry no information in a terminal card + lines.append(line) + # Trim leading/trailing blank lines and collapse runs of blanks. + out: list[str] = [] + for line in lines: + if line == "" and (not out or out[-1] == ""): + continue + out.append(line) + while out and out[-1] == "": + out.pop() + return out[:max_lines] + + +def _tspan(text: str, fill: str, *, weight: str | None = None) -> str: + attrs = f' fill="{fill}"' + if weight: + attrs += f' font-weight="{weight}"' + return f"{escape(text)}" + + +def render_line(line: str) -> str: + """Colour a line the way a reader would scan it: commands, headers, tables, confidence.""" + if line.startswith("$ "): + return _tspan("$ ", GREEN) + _tspan(line[2:], BLUE, weight="600") + if line.startswith("|"): + cells = line.split("|") + parts = [] + for i, cell in enumerate(cells): + if i: + parts.append(_tspan("|", BORDER)) + stripped = cell.strip() + if re.fullmatch(r"-+:?|:?-+", stripped): + parts.append(_tspan(cell, BORDER)) + elif stripped in ("extracted", "exact"): + parts.append(_tspan(cell, GREEN)) + elif "ambiguous" in stripped or stripped == "inferred": + parts.append(_tspan(cell, AMBER)) + elif stripped.startswith("`") and stripped.endswith("`"): + parts.append(_tspan(cell.replace("`", ""), FG)) + else: + parts.append(_tspan(cell, FG if i in (0, 1, 2, 3) else MUTED)) + return "".join(parts) + if line.startswith("**"): + # "**Query:** text · **Confidence:** medium" + out = [] + for chunk in re.split(r"(\*\*[^*]+\*\*)", line): + if chunk.startswith("**"): + out.append(_tspan(chunk.strip("*"), PURPLE, weight="600")) + else: + out.append(_tspan(chunk, FG)) + return "".join(out) + if line.startswith("- `") or line.startswith(" →") or line.startswith("`"): + return _tspan(line.replace("`", ""), FG) + if line.startswith("```"): + return _tspan("", MUTED) + if line.startswith("Indexed") or line.startswith(" parse"): + return _tspan(line, MUTED) + return _tspan(line, FG) + + +def render_svg(lines: list[str], *, title: str, cols: int) -> str: + width = int(PAD * 2 + cols * CHAR_W) + height = int(PAD * 2 + 36 + len(lines) * LINE_H) + body = [] + y = PAD + 36 + LINE_H - 6 + for line in lines: + body.append( + f'{render_line(line[:cols])}' + ) + y += LINE_H + return f""" + {escape(title)} + + + + {escape(title)} + {chr(10).join(body)} + +""" + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("transcript", type=Path) + ap.add_argument("--out", type=Path, required=True) + ap.add_argument("--title", default="codebase-index · Flask @ d318b683 · examples/demo/run_demo.sh") + ap.add_argument("--cols", type=int, default=100) + ap.add_argument("--max-lines", type=int, default=60) + ap.add_argument("--stop-at", default="--json", help="drop everything from the first command containing this") + args = ap.parse_args(argv) + + lines = load_transcript(args.transcript, stop_at=args.stop_at, max_lines=args.max_lines) + if not lines: + print("empty transcript", file=sys.stderr) + return 1 + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(render_svg(lines, title=args.title, cols=args.cols), encoding="utf-8") + print(f"wrote {args.out} ({len(lines)} lines)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 87146cf1443f4719af3dac7b20c41fcdb7e25e11 Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:35:36 +0300 Subject: [PATCH 09/12] fix(mcp): start on mcp 2.x (MCPServer) and accept --root on the mcp subcommand mcp 2.0 renamed FastMCP to MCPServer and removed mcp.server.fastmcp, so `codebase-index mcp` failed with "needs the optional extra" on a fresh install, and every MCP test skipped silently in CI because the skip guard wrapped our own module's import. The server now imports MCPServer first and falls back to FastMCP on 1.x; verified against mcp 1.29.1 and 2.1.1 (41 tests each, plus a stdio initialize / tools/list / healthcheck round-trip). Tests now skip only when the SDK itself is absent. `--root` was a global option only, so the documented `codebase-index mcp --root ` form was rejected; the subcommand accepts it. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- docs/MCP.md | 2 +- docs/SEO.md | 196 ------------------------------- examples/demo-project/README.md | 55 --------- src/codebase_index/cli.py | 17 ++- src/codebase_index/mcp/server.py | 16 ++- tests/test_mcp_golden.py | 12 +- tests/test_mcp_server.py | 39 ++++-- 7 files changed, 60 insertions(+), 277 deletions(-) delete mode 100644 docs/SEO.md delete mode 100644 examples/demo-project/README.md diff --git a/docs/MCP.md b/docs/MCP.md index 8029c4e..9cc6e84 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -7,7 +7,7 @@ pip install "codebase-index[mcp]" codebase-index mcp --root /path/to/repo ``` -The server speaks MCP over stdio through FastMCP. Build the index with +The server speaks MCP over stdio through the official Python SDK (`MCPServer` on mcp 2.x, `FastMCP` on 1.x; both are supported). Build the index with `codebase-index index` before connecting a client. Current shipped interfaces: diff --git a/docs/SEO.md b/docs/SEO.md deleted file mode 100644 index bd867d3..0000000 --- a/docs/SEO.md +++ /dev/null @@ -1,196 +0,0 @@ -# SEO Plan - -Repository SEO strategy for `codebase-index`. - -## Repository Metadata - -### Repository Name - -``` -codebase-index -``` - -Rationale: Matches the intended package name and primary product keyword. - -### GitHub About Description - -``` -Local-first codebase indexing for Claude Code, Codex CLI, OpenCode & AI coding agents — hybrid FTS5 + Tree-sitter + graph search, fully offline. -``` - -### GitHub Topics - -``` -ai-agents, ai-coding, claude-code, cli, code-search, codebase-indexing, codex-cli, context-engineering, cursor-alternative, developer-tools, fts5, local-first, mcp, opencode, python, rag, semantic-code-search, sqlite, token-optimization, tree-sitter -``` - -GitHub caps topics at 20; this list is the live set (all 20 slots used). `codebase-rag` -is a swap candidate if a slot frees up. - -### Website - -Leave blank initially. Can be set to GitHub Pages docs site later. - -## README SEO Strategy - -### First 150 Words - -The opening paragraph must contain these keywords naturally: - -- AI coding agents -- codebase indexing -- local-first -- Cursor-like indexing -- token-efficient context -- semantic code search -- AST symbol search - -### Searchable Headings - -Use these headings in the README (already implemented): - -1. "Local Codebase Indexing for AI Coding Agents" (hero section) -2. "What Is codebase-index?" (definition) -3. "What Problem Does codebase-index Solve?" (problem) -4. "How Does codebase-index Work?" (method) -5. "Which AI CLIs Does codebase-index Support?" (integration) - -### Keyword Density - -Each primary keyword should appear 1-3 times naturally: - -| Keyword | Target Count | Sections | -|---|---|---| -| AI coding agents | 3-5 | Hero, features, installation | -| codebase indexing | 2-4 | Hero, problem, solution | -| local-first | 3-5 | Hero, features, security | -| Cursor-like | 2-3 | Problem, comparison | -| token-efficient | 2-3 | Solution, features | -| semantic code search | 1-2 | Features, how it works | -| AST symbol search | 1-2 | Features, architecture | -| hybrid code search | 1-2 | Solution, retrieval | -| Tree-sitter | 2-3 | Features, architecture | -| SQLite FTS5 | 1-2 | Architecture, database schema | - -**Do not keyword-stuff.** Write naturally for humans first. - -## Badges - -Include shields.io badges in the README hero section: - -```markdown -![License](https://img.shields.io/badge/license-MIT-blue.svg) -![Python](https://img.shields.io/badge/python-3.11+-blue.svg) -![CI](https://github.com/denfry/codebase-index/actions/workflows/ci.yml/badge.svg) -![Claude Code Skill](https://img.shields.io/badge/Claude%20Code%20Skill-yes-green.svg) -![Codex CLI](https://img.shields.io/badge/Codex%20CLI-supported-green.svg) -![OpenCode](https://img.shields.io/badge/OpenCode-supported-green.svg) -![MCP](https://img.shields.io/badge/MCP-stdio%20server-green.svg) -![Local First](https://img.shields.io/badge/local--first-yes-green.svg) -![No Telemetry](https://img.shields.io/badge/no%20telemetry-yes-green.svg) -![No Network](https://img.shields.io/badge/no%20network%20by%20default-yes-green.svg) -![SQLite](https://img.shields.io/badge/database-SQLite-blue.svg) -![Tree-sitter](https://img.shields.io/badge/parsing-Tree--sitter-orange.svg) -``` - -## Social Preview Image - -Built and committed as `assets/social-preview.png` (1280×640). Regenerate with: - -```bash -python scripts/gen_assets.py -``` - -This also builds `assets/demo.png` (1200×760), the static terminal still embedded -near the top of `README.md`. - -- **Dimensions:** 1280×640 (GitHub recommended) -- **Background:** GitHub dark theme (#0d1117), accent glow -- **Text:** wordmark `codebase-index` + "Local codebase indexing for AI coding agents" -- **Elements:** terminal mock with a ranked search result + capability chips -- **Style:** clean, minimal, professional - -> **Action still required:** the file in the repo is not the social card by itself. -> Upload it in **Settings → General → Social preview** so GitHub serves it as the -> `og:image` on X / Slack / Discord / LinkedIn. (`usesCustomOpenGraphImage` is -> currently `false`.) - -## Launch Checklist - -- [x] Create v1.3.0 release with release notes -- [x] Add all GitHub topics (20/20 slots used; see list above) -- [x] Set repository description in About section -- [ ] Upload social preview image (`assets/social-preview.png` built; must be uploaded in Settings → Social preview) -- [x] Ensure README first 150 words contain target keywords -- [x] Verify all badges render correctly -- [ ] Submit to awesome Claude Code skills lists -- [ ] Submit to awesome AI coding tools lists -- [ ] Post announcement on: - - X/Twitter - - Reddit (r/LocalLLaMA, r/ClaudeAI, r/artificial) - - Hacker News (Show HN) - - Dev.to -- [~] Add demo GIF or terminal recording to README (`assets/demo.png` static still built; animated GIF still pending) -- [x] Ensure comparison page is complete (`docs/COMPARISON.md`) -- [x] Ensure security model page is complete (`docs/SECURITY_MODEL.md`) -- [x] Tag release on GitHub (`v1.3.0`) - -## Backlink Targets - -Submit to these lists for backlinks and discoverability: - -### Awesome Lists -- awesome-claude-code -- awesome-claude -- awesome-ai-coding-tools -- awesome-code-search -- awesome-developer-tools -- awesome-sqlite -- awesome-tree-sitter - -### Directories -- Claude Skill Directory (if exists) -- MCP Server Directory after client config docs are verified -- PyPI package listing after distribution hardening is complete - -### Communities -- Claude Code Discord -- AI Coding Tools communities -- Developer tool forums - -## Release Announcement Template - -``` -codebase-index v1.3.0 - -A local-first codebase index for AI coding agents. - -Instead of scanning your whole repo, Claude Code, Codex CLI, or OpenCode searches -a local hybrid index (FTS5 + symbols + graph) and reads only the relevant line ranges. - -Features: -- Local-first, no network by default -- SQLite + FTS5 full-text search -- Tree-sitter symbol extraction -- Claude Code, Codex CLI, and OpenCode setup -- Token-efficient retrieval packets -- Secret redaction -- Respects .gitignore - -Install: pip install "codebase-index @ git+https://github.com/denfry/codebase-index.git@v1.8.0" -GitHub: https://github.com/denfry/codebase-index -``` - -## Social Post Template - -``` -Gave Claude Code Cursor-like codebase awareness. - -codebase-index builds a local hybrid index so Claude finds the right files without scanning your whole repo. - -- FTS5 + symbols + graph search -- No network by default -- Token-efficient output - -pip install "codebase-index @ git+https://github.com/denfry/codebase-index.git@v1.8.0" -``` diff --git a/examples/demo-project/README.md b/examples/demo-project/README.md deleted file mode 100644 index ee8ed47..0000000 --- a/examples/demo-project/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Demo Project - -This is a sample project structure for demonstrating `codebase-index` capabilities. - -## Structure - -``` -demo-project/ -├── src/ -│ ├── auth/ -│ │ ├── __init__.py -│ │ ├── service.py # AuthService class -│ │ └── middleware.py # requireAuth middleware -│ ├── models/ -│ │ ├── __init__.py -│ │ └── user.py # User model -│ ├── routes/ -│ │ ├── __init__.py -│ │ └── auth.py # Login/logout routes -│ ├── config.py # Configuration loader -│ └── app.py # Application entry point -├── tests/ -│ ├── test_auth.py -│ └── test_user.py -├── .env # Should be excluded from index -├── .gitignore -└── package.json -``` - -## Try It - -```bash -cd examples/demo-project - -# Initialize and index -codebase-index init -codebase-index index - -# Try some queries -codebase-index search "authentication" -codebase-index symbol "AuthService" -codebase-index refs "login" -codebase-index impact "User" -codebase-index stats -``` - -## Expected Results - -After indexing, you should see: - -- `AuthService` class extracted from `src/auth/service.py` -- `User` model extracted from `src/models/user.py` -- Route handlers extracted from `src/routes/auth.py` -- `.env` file excluded (secret file) -- FTS5 index populated with chunk text from all source files diff --git a/src/codebase_index/cli.py b/src/codebase_index/cli.py index bde9e85..82c8f79 100644 --- a/src/codebase_index/cli.py +++ b/src/codebase_index/cli.py @@ -741,6 +741,11 @@ def doctor( def mcp( ctx: typer.Context, transport: str = typer.Option("stdio", "--transport", help="Transport: stdio (default)."), + root: Optional[Path] = typer.Option( + None, "--root", + help="Repository root (same as the global --root; accepted here too so client " + "configs can pass `mcp --root `).", + ), ) -> None: """Start the MCP server — exposes codebase-index tools to any MCP client (e.g. Claude Code). @@ -751,8 +756,7 @@ def mcp( "mcpServers": { "codebase-index": { "command": "codebase-index", - "args": ["mcp"], - "cwd": "/path/to/your/project" + "args": ["mcp", "--root", "/path/to/your/project"] } } } @@ -767,12 +771,13 @@ def mcp( ) raise typer.Exit(code=1) - root_opt = ctx.obj.get("root") if ctx.obj else None + # Every doc and client template writes `codebase-index mcp --root `; the + # global `--root` lives on the callback, so the subcommand accepts it as well. + root_opt = root or (ctx.obj.get("root") if ctx.obj else None) if root_opt: - import os - os.environ.setdefault("CBX_ROOT", str(root_opt)) + os.environ["CBX_ROOT"] = str(Path(root_opt).resolve()) - _mcp.run(transport=transport) # type: ignore[arg-type] + _mcp.run(transport=transport) # type: ignore[arg-type,call-overload] @app.command() diff --git a/src/codebase_index/mcp/server.py b/src/codebase_index/mcp/server.py index 6540297..fae85f5 100644 --- a/src/codebase_index/mcp/server.py +++ b/src/codebase_index/mcp/server.py @@ -29,12 +29,18 @@ if TYPE_CHECKING: from ..config import Config +# mcp 2.x renamed FastMCP to MCPServer and moved it; mcp 1.x only has FastMCP. +# Both expose the same `tool(structured_output=...)` decorator and `run(transport=)` +# entry point this module uses, so one server definition serves both lines. try: - from mcp.server.fastmcp import FastMCP # type: ignore[attr-defined] -except ImportError as exc: # pragma: no cover - raise ImportError( - "MCP server needs the optional extra: pip install codebase-index[mcp]" - ) from exc + from mcp.server.mcpserver import MCPServer as FastMCP # mcp >= 2.0 +except ImportError: # pragma: no cover - exercised on mcp 1.x only + try: + from mcp.server.fastmcp import FastMCP # type: ignore[attr-defined,no-redef] + except ImportError as exc: + raise ImportError( + "MCP server needs the optional extra: pip install codebase-index[mcp]" + ) from exc mcp = FastMCP( "codebase-index", diff --git a/tests/test_mcp_golden.py b/tests/test_mcp_golden.py index 6c76a3c..de04c64 100644 --- a/tests/test_mcp_golden.py +++ b/tests/test_mcp_golden.py @@ -17,13 +17,11 @@ import pytest from typer.testing import CliRunner -try: - from codebase_index.mcp import server as mcp_server - MCP_AVAILABLE = True -except ImportError: - MCP_AVAILABLE = False - -pytestmark = pytest.mark.skipif(not MCP_AVAILABLE, reason="mcp extra not installed") +# Skip only when the `mcp` SDK itself is absent. Our own server module must import +# cleanly against whatever SDK is installed; wrapping *that* import in a skip once +# hid a full CI run in which every MCP test was silently skipped on mcp 2.x. +pytest.importorskip("mcp", reason="mcp extra not installed") +from codebase_index.mcp import server as mcp_server # noqa: E402 from codebase_index.cli import app # noqa: E402 (after the skip guard) from tests.golden_utils import assert_matches_golden # noqa: E402 diff --git a/tests/test_mcp_server.py b/tests/test_mcp_server.py index fd47394..e98412f 100644 --- a/tests/test_mcp_server.py +++ b/tests/test_mcp_server.py @@ -9,13 +9,11 @@ import pytest -try: - from codebase_index.mcp import server as mcp_server - MCP_AVAILABLE = True -except ImportError: - MCP_AVAILABLE = False - -pytestmark = pytest.mark.skipif(not MCP_AVAILABLE, reason="mcp extra not installed") +# Skip only when the `mcp` SDK itself is absent. Our own server module must import +# cleanly against whatever SDK is installed; wrapping *that* import in a skip once +# hid a full CI run in which every MCP test was silently skipped on mcp 2.x. +pytest.importorskip("mcp", reason="mcp extra not installed") +from codebase_index.mcp import server as mcp_server # noqa: E402 # ── helpers ────────────────────────────────────────────────────────────────── @@ -213,3 +211,30 @@ def test_explain_code_accepts_raw_parameter(): """explain_code accepts raw without raising TypeError.""" result = _with_missing_db(lambda: _call(mcp_server.explain_code, query="foo", raw=True)) assert "error" in result + + +def test_mcp_subcommand_accepts_root_option(monkeypatch, tmp_path): + """Every client template writes `codebase-index mcp --root `; the global + --root lives on the Typer callback, so the subcommand must accept it too or the + documented config fails with "No such option: --root".""" + import os + + from typer.testing import CliRunner + + from codebase_index import cli + + captured: dict = {} + + class FakeServer: + def run(self, transport): # noqa: D401 - mimics FastMCP.run + captured["transport"] = transport + captured["root"] = os.environ.get("CBX_ROOT") + + import codebase_index.mcp.server as server_mod + + monkeypatch.setattr(server_mod, "mcp", FakeServer()) + monkeypatch.delenv("CBX_ROOT", raising=False) + result = CliRunner().invoke(cli.app, ["mcp", "--root", str(tmp_path)]) + assert result.exit_code == 0, result.output + assert captured["transport"] == "stdio" + assert captured["root"] == str(tmp_path.resolve()) From 5be25cd1fd1b50959ee77c73ad5e7e19961165eb Mon Sep 17 00:00:00 2001 From: denfry Date: Fri, 4 Sep 2026 16:36:17 +0300 Subject: [PATCH 10/12] docs: rebuild README around real output and the public benchmark; audit and launch kit - README: one-sentence proposition, agents list, terminal card rendered from a real Flask transcript, 60-second try-it, why-this-exists with grep / repo-map / cloud comparison, evidence section with the public baseline chart, then architecture, privacy, languages, status, contributing, roadmap. - BENCHMARKS.md leads with the public baseline run and withdraws the private 55k LOC Java headline (asymmetric accounting, not reproducible); COMPARISON.md and the older result logs point at the new run. - docs/COMMUNITY_AUDIT.md: P0-P3 findings with status and before/after scores. - docs/COMMUNITY_LAUNCH.md: HN / Reddit / X / Discord / release drafts using only logged numbers, a day-0 to month-1 sequence, and the feedback loop; replaces the stale docs/SEO.md. - examples/demo-project (a project that did not exist) removed in favour of examples/demo; package keywords extended (code-graph, impact-analysis, mcp-server, claude-code-plugin). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QDogWFhCH69TMddDcrtwHL --- CHANGELOG.md | 63 +++++ README.md | 445 +++++++++++++----------------- docs/BENCHMARKS.md | 276 ++++++++++-------- docs/COMMUNITY_AUDIT.md | 262 ++++++++++++++++++ docs/COMMUNITY_LAUNCH.md | 281 +++++++++++++++++++ docs/COMPARISON.md | 38 +-- pyproject.toml | 5 +- tests/benchmark_honest_RESULTS.md | 6 + tests/benchmark_public_RESULTS.md | 7 +- tests/eval/README.md | 16 ++ 10 files changed, 998 insertions(+), 401 deletions(-) create mode 100644 docs/COMMUNITY_AUDIT.md create mode 100644 docs/COMMUNITY_LAUNCH.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 64f7fa8..f65060c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,69 @@ All notable changes to this project are documented here. The format is based on ## [Unreleased] +### Added + +- **Public baseline benchmark.** `tests/eval/run_baselines.py` compares the index with + a disciplined `rg` + 80-line-window agent and with repo-map-style context on + Flask, Gson and Fastify at pinned commits, using git-derived ground truth, one + tokenizer on every side, and paired bootstrap / permutation significance. The + logged run is committed under `tests/eval/results/`: pooled over 450 queries, + hit@3 0.547 vs 0.304 and MRR 0.456 vs 0.263 (p < 0.001) at 3.8k vs 3.5k context + tokens per query. `scripts/gen_benchmark_chart.py` renders the chart from the log. +- **Reproducible demo.** `examples/demo/run_demo.sh|.ps1` run Find / Trace / Predict + on `pallets/flask` at a pinned commit; `EXPECTED_OUTPUT.md` is the captured run and + `assets/demo-terminal.svg` is rendered from it (`scripts/gen_terminal_svg.py`). + `docs/DEMO.md` documents GIF/video recording. +- **Release and CI gates.** `scripts/check_versions.py` (package, plugin manifest, + lock tag, skill stamps and changelog must agree), `scripts/check_links.py` + (relative Markdown links must resolve), `scripts/release_notes.py` (GitHub release + body comes from the changelog section and an empty section fails the release), and + a `package` CI job that builds, `twine check`s and runs the clean-venv install smoke + on every pull request. +- **Contributor onboarding.** `docs/DEVELOPMENT.md`, rewritten `CONTRIBUTING.md`, + benchmark-report and language-support issue templates, `SUPPORT.md`, a labels + manifest, and `docs/COMMUNITY_AUDIT.md` / `docs/COMMUNITY_LAUNCH.md`. + +### Changed + +- **Read plan is bounded.** `recommended_reads` entries are capped at + `retrieval.max_read_lines` (default 120) and carry `truncated: true` plus + `line_end_full` when capped. A symbol-aligned chunk can be a whole class; on the + public baseline benchmark the uncapped read plan cost 6.8k tokens per query against + 3.5k for grep, the capped one 3.8k, with identical ranking quality. Set the option to + 0 for the previous behaviour. +- **README, docs and examples show real output only.** Fabricated tables (an + `AuthService.ts` example with a `Score` column, an invented `doctor` transcript, + the mock-up `assets/demo.png`) are replaced by output captured on Flask. + Duplicate pages (`DATABASE_SCHEMA`, `RETRIEVAL_PIPELINE`, `docs/SECURITY`) are + redirect stubs; `SCHEMA.md` now matches `storage/schema.sql`. +- **Benchmark headline.** The "13× fewer tokens on a 55k LOC Java repo" figure is + withdrawn: the repository is private and the accounting was asymmetric. The + defensible claim is the public baseline run above. +- `SECURITY.md` points to GitHub private vulnerability reporting and states the + supported line as 1.9.x. + +### Fixed + +- **MCP server did not start on mcp 2.x.** The SDK renamed `FastMCP` to + `MCPServer` and removed the old import path, so `codebase-index mcp` reported + "needs the optional extra" even with the extra installed, and the MCP tests + skipped silently in CI because the skip guard wrapped our own module's import. + The server now imports `MCPServer` first and falls back to `FastMCP` on 1.x + (verified against mcp 1.29 and 2.1); the tests skip only when the SDK itself is + missing. +- **`codebase-index mcp --root ` was rejected.** Every client template + used that form, but `--root` was only a global option (`codebase-index --root + mcp`). The subcommand now accepts it as well. +- **Plugin wrappers refused four documented commands.** `bin/cbx` and `bin/cbx.ps1` + whitelisted ten subcommands while the skill allowed fourteen; `architecture`, + `diff-impact`, `path` and `describe` now work from the plugin. A parity test pins + the two lists together. +- **Benchmark leakage.** `gen_queries` documented that changelog-style files were + excluded from the evaluation corpus, but the harness never applied the list, and + Flask-style `CHANGES.rst` was not covered. Both are fixed; `CHANGES*`, `HISTORY*`, + `NEWS*` and `RELEASE_NOTES*` are refused as answers and excluded from the corpus. + ## [1.9.0] - 2026-09-02 ### Added diff --git a/README.md b/README.md index e6ad992..7ce2c94 100644 --- a/README.md +++ b/README.md @@ -5,326 +5,259 @@