codebase-index 1.8.0__tar.gz → 1.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. codebase_index-1.9.0/.claude/skills/codebase-index/.skill_version +1 -0
  2. {codebase_index-1.8.0 → codebase_index-1.9.0}/.claude-plugin/plugin.json +1 -1
  3. codebase_index-1.9.0/.codex/skills/codebase-index/.skill_version +1 -0
  4. codebase_index-1.9.0/.opencode/skills/codebase-index/.skill_version +1 -0
  5. {codebase_index-1.8.0 → codebase_index-1.9.0}/CHANGELOG.md +83 -0
  6. {codebase_index-1.8.0 → codebase_index-1.9.0}/PKG-INFO +14 -8
  7. {codebase_index-1.8.0 → codebase_index-1.9.0}/README.md +13 -7
  8. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/ARCHITECTURE.md +4 -3
  9. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/BENCHMARKS.md +23 -4
  10. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/RETRIEVAL.md +66 -17
  11. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/RETRIEVAL_PIPELINE.md +18 -9
  12. {codebase_index-1.8.0 → codebase_index-1.9.0}/requirements.lock +1 -1
  13. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/__init__.py +1 -1
  14. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/mcp/server.py +1 -1
  15. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/diversity.py +47 -12
  16. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/fusion.py +38 -0
  17. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/pipeline.py +13 -6
  18. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/priors.py +18 -2
  19. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/searchers.py +30 -8
  20. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/tuning.py +41 -1
  21. codebase_index-1.9.0/tests/eval/README.md +99 -0
  22. codebase_index-1.9.0/tests/eval/gen_queries.py +253 -0
  23. codebase_index-1.9.0/tests/eval/harness.py +419 -0
  24. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/eval/metrics.py +78 -0
  25. codebase_index-1.9.0/tests/eval/queries/self_repo_git.yml +627 -0
  26. codebase_index-1.9.0/tests/eval/run_eval.py +186 -0
  27. codebase_index-1.9.0/tests/test_diversity.py +120 -0
  28. codebase_index-1.9.0/tests/test_fusion.py +145 -0
  29. codebase_index-1.9.0/tests/test_gen_queries.py +152 -0
  30. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_priors.py +9 -1
  31. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_symbol_ranking.py +76 -0
  32. codebase_index-1.9.0/tests/test_tuning_contract.py +141 -0
  33. codebase_index-1.8.0/.claude/skills/codebase-index/.skill_version +0 -1
  34. codebase_index-1.8.0/.codex/skills/codebase-index/.skill_version +0 -1
  35. codebase_index-1.8.0/.opencode/skills/codebase-index/.skill_version +0 -1
  36. codebase_index-1.8.0/tests/eval/harness.py +0 -257
  37. codebase_index-1.8.0/tests/eval/run_eval.py +0 -104
  38. codebase_index-1.8.0/tests/test_diversity.py +0 -49
  39. codebase_index-1.8.0/tests/test_fusion.py +0 -66
  40. {codebase_index-1.8.0 → codebase_index-1.9.0}/.claude/skills/codebase-index/SKILL.md +0 -0
  41. {codebase_index-1.8.0 → codebase_index-1.9.0}/.claude/skills/codebase-index/examples/hooks/settings.json +0 -0
  42. {codebase_index-1.8.0 → codebase_index-1.9.0}/.claude/skills/codebase-index/references/commands.md +0 -0
  43. {codebase_index-1.8.0 → codebase_index-1.9.0}/.claude/skills/codebase-index/references/response-contract.md +0 -0
  44. {codebase_index-1.8.0 → codebase_index-1.9.0}/.claude/skills/codebase-index/scripts/cbx +0 -0
  45. {codebase_index-1.8.0 → codebase_index-1.9.0}/.claude/skills/codebase-index/scripts/cbx.ps1 +0 -0
  46. {codebase_index-1.8.0 → codebase_index-1.9.0}/.claude-plugin/marketplace.json +0 -0
  47. {codebase_index-1.8.0 → codebase_index-1.9.0}/.codex/skills/codebase-index/SKILL.md +0 -0
  48. {codebase_index-1.8.0 → codebase_index-1.9.0}/.codex/skills/codebase-index/examples/hooks/settings.json +0 -0
  49. {codebase_index-1.8.0 → codebase_index-1.9.0}/.codex/skills/codebase-index/references/commands.md +0 -0
  50. {codebase_index-1.8.0 → codebase_index-1.9.0}/.codex/skills/codebase-index/references/response-contract.md +0 -0
  51. {codebase_index-1.8.0 → codebase_index-1.9.0}/.codex/skills/codebase-index/scripts/cbx +0 -0
  52. {codebase_index-1.8.0 → codebase_index-1.9.0}/.codex/skills/codebase-index/scripts/cbx.ps1 +0 -0
  53. {codebase_index-1.8.0 → codebase_index-1.9.0}/.editorconfig +0 -0
  54. {codebase_index-1.8.0 → codebase_index-1.9.0}/.gitattributes +0 -0
  55. {codebase_index-1.8.0 → codebase_index-1.9.0}/.github/FUNDING.yml +0 -0
  56. {codebase_index-1.8.0 → codebase_index-1.9.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  57. {codebase_index-1.8.0 → codebase_index-1.9.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  58. {codebase_index-1.8.0 → codebase_index-1.9.0}/.github/ISSUE_TEMPLATE/skill_listing_request.yml +0 -0
  59. {codebase_index-1.8.0 → codebase_index-1.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  60. {codebase_index-1.8.0 → codebase_index-1.9.0}/.github/workflows/ci.yml +0 -0
  61. {codebase_index-1.8.0 → codebase_index-1.9.0}/.github/workflows/release.yml +0 -0
  62. {codebase_index-1.8.0 → codebase_index-1.9.0}/.gitignore +0 -0
  63. {codebase_index-1.8.0 → codebase_index-1.9.0}/.opencode/agents/codebase-index.md +0 -0
  64. {codebase_index-1.8.0 → codebase_index-1.9.0}/.opencode/commands/codebase-index.md +0 -0
  65. {codebase_index-1.8.0 → codebase_index-1.9.0}/.opencode/skills/codebase-index/SKILL.md +0 -0
  66. {codebase_index-1.8.0 → codebase_index-1.9.0}/.opencode/skills/codebase-index/examples/hooks/settings.json +0 -0
  67. {codebase_index-1.8.0 → codebase_index-1.9.0}/.opencode/skills/codebase-index/references/commands.md +0 -0
  68. {codebase_index-1.8.0 → codebase_index-1.9.0}/.opencode/skills/codebase-index/references/response-contract.md +0 -0
  69. {codebase_index-1.8.0 → codebase_index-1.9.0}/.opencode/skills/codebase-index/scripts/cbx +0 -0
  70. {codebase_index-1.8.0 → codebase_index-1.9.0}/.opencode/skills/codebase-index/scripts/cbx.ps1 +0 -0
  71. {codebase_index-1.8.0 → codebase_index-1.9.0}/AGENTS.md +0 -0
  72. {codebase_index-1.8.0 → codebase_index-1.9.0}/CODE_OF_CONDUCT.md +0 -0
  73. {codebase_index-1.8.0 → codebase_index-1.9.0}/CONTRIBUTING.md +0 -0
  74. {codebase_index-1.8.0 → codebase_index-1.9.0}/LICENSE +0 -0
  75. {codebase_index-1.8.0 → codebase_index-1.9.0}/ROADMAP.md +0 -0
  76. {codebase_index-1.8.0 → codebase_index-1.9.0}/SECURITY.md +0 -0
  77. {codebase_index-1.8.0 → codebase_index-1.9.0}/adapters/claude.ps1 +0 -0
  78. {codebase_index-1.8.0 → codebase_index-1.9.0}/adapters/claude.sh +0 -0
  79. {codebase_index-1.8.0 → codebase_index-1.9.0}/adapters/codex.ps1 +0 -0
  80. {codebase_index-1.8.0 → codebase_index-1.9.0}/adapters/codex.sh +0 -0
  81. {codebase_index-1.8.0 → codebase_index-1.9.0}/adapters/opencode.ps1 +0 -0
  82. {codebase_index-1.8.0 → codebase_index-1.9.0}/adapters/opencode.sh +0 -0
  83. {codebase_index-1.8.0 → codebase_index-1.9.0}/assets/demo.png +0 -0
  84. {codebase_index-1.8.0 → codebase_index-1.9.0}/assets/mark.png +0 -0
  85. {codebase_index-1.8.0 → codebase_index-1.9.0}/assets/social-preview.png +0 -0
  86. {codebase_index-1.8.0 → codebase_index-1.9.0}/bin/cbx +0 -0
  87. {codebase_index-1.8.0 → codebase_index-1.9.0}/bin/cbx.ps1 +0 -0
  88. {codebase_index-1.8.0 → codebase_index-1.9.0}/bin/codebase-index +0 -0
  89. {codebase_index-1.8.0 → codebase_index-1.9.0}/bin/codebase-index.ps1 +0 -0
  90. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/COMPARISON.md +0 -0
  91. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/DATABASE_SCHEMA.md +0 -0
  92. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/FAQ.md +0 -0
  93. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/INSTALLATION.md +0 -0
  94. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/LANGUAGES.md +0 -0
  95. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/MCP.md +0 -0
  96. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/PRODUCT_UPGRADE_PLAN.md +0 -0
  97. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/QUICKSTART.md +0 -0
  98. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/RELEASE_CHECKLIST.md +0 -0
  99. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/ROADMAP.md +0 -0
  100. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/SCHEMA.md +0 -0
  101. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/SECURITY.md +0 -0
  102. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/SECURITY_MODEL.md +0 -0
  103. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/SEO.md +0 -0
  104. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/SKILL_DESIGN.md +0 -0
  105. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/installer.md +0 -0
  106. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-benchmark-index-improvements.md +0 -0
  107. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-generalize-skill-research-discipline.md +0 -0
  108. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m1-storage-discovery.md +0 -0
  109. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m2-fts-lexical.md +0 -0
  110. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m3-treesitter-symbols.md +0 -0
  111. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m4-hybrid-search.md +0 -0
  112. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m5-graph-impact.md +0 -0
  113. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m6-embeddings-vector.md +0 -0
  114. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m7-5-plugin-one-command-install.md +0 -0
  115. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m7-skill-packaging.md +0 -0
  116. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m8-hooks-watch.md +0 -0
  117. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-m9-release.md +0 -0
  118. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-05-29-multilang-symbols-make-skill-win.md +0 -0
  119. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/plans/2026-06-24-snippet-skeletonization.md +0 -0
  120. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/specs/2026-05-29-plugin-one-command-install-design.md +0 -0
  121. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/specs/2026-06-14-typed-framework-edges-design.md +0 -0
  122. {codebase_index-1.8.0 → codebase_index-1.9.0}/docs/superpowers/specs/2026-06-24-snippet-skeletonization-design.md +0 -0
  123. {codebase_index-1.8.0 → codebase_index-1.9.0}/examples/config.example.json +0 -0
  124. {codebase_index-1.8.0 → codebase_index-1.9.0}/examples/demo-project/README.md +0 -0
  125. {codebase_index-1.8.0 → codebase_index-1.9.0}/examples/hooks/settings.json +0 -0
  126. {codebase_index-1.8.0 → codebase_index-1.9.0}/examples/queries.md +0 -0
  127. {codebase_index-1.8.0 → codebase_index-1.9.0}/examples/sample-queries.md +0 -0
  128. {codebase_index-1.8.0 → codebase_index-1.9.0}/examples/sample-retrieval-output.md +0 -0
  129. {codebase_index-1.8.0 → codebase_index-1.9.0}/hooks/hooks.json +0 -0
  130. {codebase_index-1.8.0 → codebase_index-1.9.0}/install.ps1 +0 -0
  131. {codebase_index-1.8.0 → codebase_index-1.9.0}/install.sh +0 -0
  132. {codebase_index-1.8.0 → codebase_index-1.9.0}/lib/common.ps1 +0 -0
  133. {codebase_index-1.8.0 → codebase_index-1.9.0}/lib/common.sh +0 -0
  134. {codebase_index-1.8.0 → codebase_index-1.9.0}/pyproject.toml +0 -0
  135. {codebase_index-1.8.0 → codebase_index-1.9.0}/scripts/bootstrap.ps1 +0 -0
  136. {codebase_index-1.8.0 → codebase_index-1.9.0}/scripts/bootstrap.sh +0 -0
  137. {codebase_index-1.8.0 → codebase_index-1.9.0}/scripts/gen_assets.py +0 -0
  138. {codebase_index-1.8.0 → codebase_index-1.9.0}/scripts/release_smoke.py +0 -0
  139. {codebase_index-1.8.0 → codebase_index-1.9.0}/scripts/sync_skill_copies.py +0 -0
  140. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/SKILL.md +0 -0
  141. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/examples/basic-usage.md +0 -0
  142. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/examples/claude-md-example.md +0 -0
  143. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/examples/hooks-example.json +0 -0
  144. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/references/commands.md +0 -0
  145. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/references/response-contract.md +0 -0
  146. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/scripts/bootstrap.py +0 -0
  147. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/scripts/cbx +0 -0
  148. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/scripts/cbx.ps1 +0 -0
  149. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/scripts/doctor.py +0 -0
  150. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/scripts/install.py +0 -0
  151. {codebase_index-1.8.0 → codebase_index-1.9.0}/skill/scripts/smoke_test.py +0 -0
  152. {codebase_index-1.8.0 → codebase_index-1.9.0}/skills/codebase-index/SKILL.md +0 -0
  153. {codebase_index-1.8.0 → codebase_index-1.9.0}/skills/codebase-index/references/commands.md +0 -0
  154. {codebase_index-1.8.0 → codebase_index-1.9.0}/skills/codebase-index/references/response-contract.md +0 -0
  155. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/__main__.py +0 -0
  156. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/cli.py +0 -0
  157. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/config.py +0 -0
  158. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/discovery/__init__.py +0 -0
  159. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/discovery/classify.py +0 -0
  160. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/discovery/ignore.py +0 -0
  161. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/discovery/walker.py +0 -0
  162. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/doctor.py +0 -0
  163. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/embeddings/__init__.py +0 -0
  164. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/embeddings/backend.py +0 -0
  165. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/embeddings/external.py +0 -0
  166. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/embeddings/local.py +0 -0
  167. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/embeddings/noop.py +0 -0
  168. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/graph/__init__.py +0 -0
  169. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/graph/analysis.py +0 -0
  170. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/graph/builder.py +0 -0
  171. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/graph/expand.py +0 -0
  172. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/graph/export.py +0 -0
  173. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/graph/navigate.py +0 -0
  174. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/graph/retrieval.py +0 -0
  175. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/indexer/__init__.py +0 -0
  176. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/indexer/doc_chunks.py +0 -0
  177. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/indexer/freshness.py +0 -0
  178. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/indexer/pipeline.py +0 -0
  179. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/mcp/__init__.py +0 -0
  180. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/models.py +0 -0
  181. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/output/__init__.py +0 -0
  182. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/output/json.py +0 -0
  183. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/output/markdown.py +0 -0
  184. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/output/redact.py +0 -0
  185. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/parsers/__init__.py +0 -0
  186. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/parsers/base.py +0 -0
  187. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/parsers/languages.py +0 -0
  188. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/parsers/line_chunker.py +0 -0
  189. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/parsers/symbol_chunks.py +0 -0
  190. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/parsers/treesitter.py +0 -0
  191. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/__init__.py +0 -0
  192. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/budget.py +0 -0
  193. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/fuzzy.py +0 -0
  194. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/intent.py +0 -0
  195. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/lexical.py +0 -0
  196. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/rerank.py +0 -0
  197. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/skeleton.py +0 -0
  198. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/retrieval/types.py +0 -0
  199. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/scaffold.py +0 -0
  200. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/service.py +0 -0
  201. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/skill_template/SKILL.md +0 -0
  202. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/skill_template/examples/hooks/settings.json +0 -0
  203. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/skill_template/references/commands.md +0 -0
  204. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/skill_template/references/response-contract.md +0 -0
  205. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/skill_template/scripts/cbx +0 -0
  206. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/skill_template/scripts/cbx.ps1 +0 -0
  207. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/skill_update.py +0 -0
  208. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/storage/__init__.py +0 -0
  209. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/storage/db.py +0 -0
  210. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/storage/repo.py +0 -0
  211. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/storage/schema.sql +0 -0
  212. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/watch/__init__.py +0 -0
  213. {codebase_index-1.8.0 → codebase_index-1.9.0}/src/codebase_index/watch/watcher.py +0 -0
  214. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/__init__.py +0 -0
  215. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/benchmark_honest.py +0 -0
  216. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/benchmark_honest_RESULTS.md +0 -0
  217. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/benchmark_honest_newtowny.txt +0 -0
  218. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/benchmark_public.py +0 -0
  219. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/benchmark_public_RESULTS.md +0 -0
  220. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/benchmark_real_repo.py +0 -0
  221. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/conftest.py +0 -0
  222. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/eval/__init__.py +0 -0
  223. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/eval/queries/self_repo.yml +0 -0
  224. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/README.md +0 -0
  225. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/expected_answers.yml +0 -0
  226. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/multilang/sample.go +0 -0
  227. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/multilang/sample.java +0 -0
  228. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/multilang/sample.rs +0 -0
  229. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/.env +0 -0
  230. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/.gitignore +0 -0
  231. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/huge.json +0 -0
  232. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/logo.png +0 -0
  233. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/node_modules/leftpad/index.js +0 -0
  234. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/secrets.pem +0 -0
  235. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/src/api/service.py +0 -0
  236. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/src/auth/token.py +0 -0
  237. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/src/models/user.py +0 -0
  238. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/src/schema.generated.ts +0 -0
  239. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/fixtures/sample_repo/web/app.ts +0 -0
  240. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/architecture.json +0 -0
  241. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/describe_refresh.json +0 -0
  242. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/explain_auth.json +0 -0
  243. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/impact_user_model.json +0 -0
  244. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_architecture.json +0 -0
  245. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_describe_symbol.json +0 -0
  246. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_explain_code.json +0 -0
  247. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_find_refs.json +0 -0
  248. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_find_symbol.json +0 -0
  249. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_healthcheck.json +0 -0
  250. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_impact_of.json +0 -0
  251. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_impact_of_diff.json +0 -0
  252. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_index_stats.json +0 -0
  253. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_path_between.json +0 -0
  254. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/mcp_search_code.json +0 -0
  255. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/path_renew_to_token.json +0 -0
  256. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/refs_refresh_access_token.json +0 -0
  257. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/search_token.json +0 -0
  258. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/stats.json +0 -0
  259. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden/symbol_user.json +0 -0
  260. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/golden_utils.py +0 -0
  261. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/installer/smoke.ps1 +0 -0
  262. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/installer/smoke.sh +0 -0
  263. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_analysis.py +0 -0
  264. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_benchmark_comparison.py +0 -0
  265. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_bootstrap.py +0 -0
  266. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_budget.py +0 -0
  267. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_chunker.py +0 -0
  268. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_classify.py +0 -0
  269. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_clean_cli.py +0 -0
  270. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_cli.py +0 -0
  271. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_cli_golden.py +0 -0
  272. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_config.py +0 -0
  273. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_diff_impact.py +0 -0
  274. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_discovery.py +0 -0
  275. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_doc_chunks.py +0 -0
  276. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_doctor.py +0 -0
  277. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_embeddings_backend.py +0 -0
  278. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_embeddings_external.py +0 -0
  279. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_embeddings_local.py +0 -0
  280. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_freshness.py +0 -0
  281. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_fuzzy.py +0 -0
  282. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_graph.py +0 -0
  283. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_graph_coverage.py +0 -0
  284. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_graph_export_cli.py +0 -0
  285. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_graph_retrieval.py +0 -0
  286. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_hooks_merge.py +0 -0
  287. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_hybrid_ranking.py +0 -0
  288. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_ignore.py +0 -0
  289. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_impact_cli.py +0 -0
  290. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_impact_decay.py +0 -0
  291. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_index_cli.py +0 -0
  292. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_init_cli.py +0 -0
  293. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_intent.py +0 -0
  294. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_languages.py +0 -0
  295. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_lexical.py +0 -0
  296. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_mcp_golden.py +0 -0
  297. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_mcp_server.py +0 -0
  298. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_multilang_symbols.py +0 -0
  299. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_navigate.py +0 -0
  300. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_output.py +0 -0
  301. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_packaging.py +0 -0
  302. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_parse_guardrails.py +0 -0
  303. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_perf_smoke.py +0 -0
  304. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_pipeline.py +0 -0
  305. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_pipeline_search.py +0 -0
  306. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_pipeline_vectors.py +0 -0
  307. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_plugin_manifest.py +0 -0
  308. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_plugin_skill_parity.py +0 -0
  309. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_plugin_wrappers.py +0 -0
  310. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_public_benchmark.py +0 -0
  311. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_quality_benchmark.py +0 -0
  312. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_redact.py +0 -0
  313. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_repo_search.py +0 -0
  314. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_rerank.py +0 -0
  315. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_retrieval_types.py +0 -0
  316. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_scaffold.py +0 -0
  317. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_search_cli.py +0 -0
  318. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_searchers.py +0 -0
  319. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_seeded_index.py +0 -0
  320. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_service.py +0 -0
  321. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_skeleton.py +0 -0
  322. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_skill_update.py +0 -0
  323. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_storage.py +0 -0
  324. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_symbol_cli.py +0 -0
  325. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_symbol_refs.py +0 -0
  326. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_sync_skill_copies.py +0 -0
  327. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_treesitter.py +0 -0
  328. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_update.py +0 -0
  329. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_update_cli.py +0 -0
  330. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_vector_search.py +0 -0
  331. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_vectors_storage.py +0 -0
  332. {codebase_index-1.8.0 → codebase_index-1.9.0}/tests/test_watcher.py +0 -0
@@ -3,7 +3,7 @@
3
3
  "name": "codebase-index",
4
4
  "displayName": "Codebase Index",
5
5
  "description": "Give Claude a precise local map of your codebase: find implementations, trace behavior, and predict change impact with file-line evidence.",
6
- "version": "1.8.0",
6
+ "version": "1.9.0",
7
7
  "author": {
8
8
  "name": "codebase-index contributors"
9
9
  },
@@ -6,6 +6,89 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.9.0] - 2026-09-02
10
+
11
+ ### Added
12
+
13
+ - **Objective ground truth from git history.** `tests/eval/gen_queries.py` mints a
14
+ retrieval benchmark from any git repository by pairing a human-written commit
15
+ subject with the files that commit actually changed. Unlike docstring-derived
16
+ benchmarks the query text is not copied into the document being retrieved, so the
17
+ set is leak-free by construction; merges, reverts, releases, version bumps,
18
+ sweeping refactors, changelog-style answers and benchmark scaffolding are all
19
+ filtered out.
20
+ - **Multi-corpus, multi-language evaluation.** `run_eval.py --corpus REPO:QUERIES`
21
+ pools several repositories into one benchmark so ranking changes are validated
22
+ outside this repository and outside Python. The 1.9.0 defaults were measured on
23
+ 305 queries across Python, Java and TypeScript corpora.
24
+ - **Significance testing.** Every non-baseline row now reports a paired bootstrap
25
+ 95% confidence interval and a paired permutation p-value (both seeded, therefore
26
+ reproducible). Query sets of this size have a noise floor of several MRR points;
27
+ signals now ship on the strength of that test rather than the sign of a delta.
28
+ - **Context-noise metrics.** The report adds mean emitted snippet tokens, duplicate
29
+ rate of returned results, and p99 latency alongside the existing IR metrics.
30
+
31
+ ### Changed
32
+
33
+ - **Fusion now scores cross-retriever agreement.** RRF fuses on
34
+ `(path, line-bucket)`, so a symbol definition at line 40 and a lexical hit at line
35
+ 120 in the same file fused as two unrelated candidates — two retrievers agreeing
36
+ on a file produced two weak results instead of one strong one, and cross-source
37
+ agreement never reached the score. Each candidate now also receives, at
38
+ `file_agreement_weight` (0.4), the RRF mass of every retriever that found its file
39
+ at another locator, excluding retrievers already counted at that locator.
40
+ Ablatable via `RetrievalTuning(file_agreement=False)`.
41
+ - **Documentation demotion deepened** from -0.05 to -0.20. Prose describing a
42
+ feature matches a natural-language question more literally than the code
43
+ implementing it, so design notes and plans were displacing the modules they
44
+ describe. Because this only reorders prose relative to code, documentation-seeking
45
+ queries improved as well (category MRR 0.579 → 0.612). Generated/vendor paths move
46
+ to -0.25 so they remain the least-preferred role, and `MAX_ABS_PRIOR` now pins the
47
+ invariant that priors stay tiebreakers.
48
+ - **Fuzzy identifier matching is now a recall fallback.** It runs only when the
49
+ precise symbol lookup named no symbol and returned fewer than
50
+ `fuzzy_fallback_min` (3) rows. It moved no ranking metric across 305 queries while
51
+ accounting for ~20% of query latency; typo and acronym recall is unchanged because
52
+ those are exactly the queries where the precise lookup comes up empty.
53
+ - **Candidate over-fetch is explicit.** `candidate_pool_multiplier` (default 2)
54
+ replaces the implicit widening that happened whenever dedup or MMR was enabled.
55
+ Making it explicit revealed that the quality previously credited to SimHash dedup
56
+ was really the wider pool; dedup is retained for what it does measurably do, which
57
+ is cutting the duplicate rate of returned snippets from ~1.6% to ~0%.
58
+
59
+ ### Fixed
60
+
61
+ - **Synonym matches were reported as exact symbol matches.** `is_exact` came from
62
+ SQL and was relative to whichever needle retrieved the row, so a synonym
63
+ expansion ("config" for "configuration") marked an unrelated symbol as an exact
64
+ match — worth a +0.20 rerank bonus and an unconditional `high` confidence.
65
+ Exactness is now judged against the terms the user actually typed.
66
+ - **Duplicate suppression was order-dependent on ties.** Equal-scoring duplicates
67
+ handed the slot to whichever copy arrived last, contradicting the documented
68
+ "ties favor input order" and making the retained snippet depend on retriever
69
+ emission order.
70
+
71
+ ### Performance
72
+
73
+ - Query latency roughly halves: p50 78.6 ms → 51.2 ms, p95 193.2 ms → 94.9 ms,
74
+ p99 277.1 ms → 152.2 ms on the pooled three-repository benchmark, from the fuzzy
75
+ fallback plus a SimHash fingerprint that folds repeated tokens by multiplicity and
76
+ caches token digests. Fingerprint output is bit-for-bit unchanged.
77
+ - Mean emitted snippet tokens fall 1183 → 1091 per query.
78
+
79
+ ### Retrieval quality
80
+
81
+ Pooled over 305 queries (Python, Java, TypeScript), v1.8.0 → 1.9.0:
82
+
83
+ | Metric | v1.8.0 | 1.9.0 | Δ | p |
84
+ |---|---|---|---|---|
85
+ | MRR | 0.564 | 0.591 | +0.027 | <0.001 |
86
+ | MAP | 0.433 | 0.461 | +0.028 | <0.001 |
87
+ | nDCG@10 | 0.503 | 0.527 | +0.024 | <0.001 |
88
+ | recall@5 | 0.529 | 0.560 | +0.031 | <0.001 |
89
+ | P@5 | 0.182 | 0.192 | +0.009 | 0.001 |
90
+ | hit@3 | 0.649 | 0.666 | +0.016 | 0.124 |
91
+
9
92
  ## [1.8.0] - 2026-09-02
10
93
 
11
94
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codebase-index
3
- Version: 1.8.0
3
+ Version: 1.9.0
4
4
  Summary: A precise local codebase map for AI coding agents: find, trace, and predict with file-line evidence.
5
5
  Project-URL: Homepage, https://github.com/denfry/codebase-index
6
6
  Project-URL: Documentation, https://github.com/denfry/codebase-index/tree/main/docs
@@ -321,7 +321,7 @@ See [MCP.md](docs/MCP.md) for client configuration.
321
321
 
322
322
  ## Project status
323
323
 
324
- The latest released line is **1.8.0**. It includes:
324
+ The latest released line is **1.9.0**. It includes:
325
325
 
326
326
  - hybrid and optional vector retrieval;
327
327
  - Tree-sitter symbol extraction across the documented language tiers;
@@ -330,15 +330,21 @@ The latest released line is **1.8.0**. It includes:
330
330
  - shortest dependency paths and node descriptions;
331
331
  - token-budgeted and skeletonized retrieval packets;
332
332
  - benchmark-calibrated lexical expansion, fuzzy identifier matching, and source-aware ranking;
333
+ - rank fusion that scores cross-retriever agreement at file level, not just at a locator;
333
334
  - bounded, intent-directed graph discovery with optional diversity and duplicate suppression;
334
335
  - CLI, Skill, plugin, and MCP delivery;
335
336
  - incremental updates, watch hooks, diagnostics, skill rollback, and diff-aware
336
- impact analysis.
337
-
338
- Planned work is deliberately separated from shipped capability. The next
339
- product priorities are stronger real-repository evaluations, typed framework
340
- edges, and an even more direct task-context workflow. See the
341
- [roadmap](docs/ROADMAP.md).
337
+ impact analysis;
338
+ - a multi-repository retrieval evaluation with leak-free git-derived ground truth,
339
+ one-signal ablations, and paired significance tests
340
+ ([tests/eval](tests/eval/README.md)).
341
+
342
+ Every shipped ranking signal has to survive that evaluation: 1.9.0 removed the
343
+ cost of two signals that could not demonstrate a benefit and rejected several
344
+ plausible ones outright (IDF-weighted coverage, stemming, graph propagation, MMR,
345
+ a file-length prior). Planned work is deliberately separated from shipped
346
+ capability. The next product priorities are typed framework edges and an even more
347
+ direct task-context workflow. See the [roadmap](docs/ROADMAP.md).
342
348
 
343
349
  ## Documentation
344
350
 
@@ -271,7 +271,7 @@ See [MCP.md](docs/MCP.md) for client configuration.
271
271
 
272
272
  ## Project status
273
273
 
274
- The latest released line is **1.8.0**. It includes:
274
+ The latest released line is **1.9.0**. It includes:
275
275
 
276
276
  - hybrid and optional vector retrieval;
277
277
  - Tree-sitter symbol extraction across the documented language tiers;
@@ -280,15 +280,21 @@ The latest released line is **1.8.0**. It includes:
280
280
  - shortest dependency paths and node descriptions;
281
281
  - token-budgeted and skeletonized retrieval packets;
282
282
  - benchmark-calibrated lexical expansion, fuzzy identifier matching, and source-aware ranking;
283
+ - rank fusion that scores cross-retriever agreement at file level, not just at a locator;
283
284
  - bounded, intent-directed graph discovery with optional diversity and duplicate suppression;
284
285
  - CLI, Skill, plugin, and MCP delivery;
285
286
  - incremental updates, watch hooks, diagnostics, skill rollback, and diff-aware
286
- impact analysis.
287
-
288
- Planned work is deliberately separated from shipped capability. The next
289
- product priorities are stronger real-repository evaluations, typed framework
290
- edges, and an even more direct task-context workflow. See the
291
- [roadmap](docs/ROADMAP.md).
287
+ impact analysis;
288
+ - a multi-repository retrieval evaluation with leak-free git-derived ground truth,
289
+ one-signal ablations, and paired significance tests
290
+ ([tests/eval](tests/eval/README.md)).
291
+
292
+ Every shipped ranking signal has to survive that evaluation: 1.9.0 removed the
293
+ cost of two signals that could not demonstrate a benefit and rejected several
294
+ plausible ones outright (IDF-weighted coverage, stemming, graph propagation, MMR,
295
+ a file-length prior). Planned work is deliberately separated from shipped
296
+ capability. The next product priorities are typed framework edges and an even more
297
+ direct task-context workflow. See the [roadmap](docs/ROADMAP.md).
292
298
 
293
299
  ## Documentation
294
300
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 1. Overview
4
4
 
5
- `codebase-index` is a **local-first** code intelligence layer for AI coding agents. In `1.8.0`
5
+ `codebase-index` is a **local-first** code intelligence layer for AI coding agents. In `1.9.0`
6
6
  it has two shipped faces:
7
7
 
8
8
  1. **A Claude Code Skill** (`.claude/skills/codebase-index/SKILL.md`) that Claude auto-invokes for
@@ -97,8 +97,9 @@ codebase-index/
97
97
  ├── graph/ # builder.py (edge resolution), expand.py (impact),
98
98
  │ # export.py (HTML graph)
99
99
  ├── storage/ # db.py (pragmas, schema, version guard), schema.sql, repo.py
100
- ├── retrieval/ # intent.py, searchers.py, fusion.py, rerank.py,
101
- │ # budget.py, pipeline.py, types.py
100
+ ├── retrieval/ # intent.py, searchers.py, fusion.py, rerank.py, priors.py,
101
+ │ # lexical.py, fuzzy.py, diversity.py, skeleton.py,
102
+ │ # tuning.py (ablation contract), budget.py, pipeline.py, types.py
102
103
  ├── embeddings/ # backend.py, noop.py (default), local.py, external.py — opt-in
103
104
  ├── output/ # markdown.py, json.py, redact.py
104
105
  ├── watch/ # watcher.py (optional, watchdog-based)
@@ -1,10 +1,11 @@
1
1
  # Benchmarks
2
2
 
3
- `codebase-index` has three benchmark surfaces. Read them with their status in
3
+ `codebase-index` has four benchmark surfaces. Read them with their status in
4
4
  mind — the whole point of this page is to keep evidence and aspiration separate.
5
5
 
6
6
  | Surface | What it is | Status | Use it as |
7
7
  |---|---|---|---|
8
+ | 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 |
8
9
  | 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 |
9
10
  | Smoke/perf (`test_perf_smoke.py`, `test_benchmark_comparison.py`) | Latency + output-size guards on a tiny fixture | **Toy/smoke** | Regression checks only |
10
11
  | 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 |
@@ -15,15 +16,33 @@ Do not write, imply, or ship any of these until a run with published logs exists
15
16
 
16
17
  - Any 10k / 100k / 1M LOC scale or speed claim (no real run at that size).
17
18
  - "Beats Cursor / Sourcegraph / Codebase-Memory MCP" — no head-to-head exists.
18
- - Per-language quality claims beyond Java (the honest run is Java-only).
19
+ - Per-language *absolute* quality claims beyond Java. The retrieval eval covers
20
+ Python, Java and TypeScript, but it measures this system against its own earlier
21
+ versions — it says a change helped, not that the product beats an alternative.
19
22
  - Generic "Nx faster" / "Nx fewer tokens" without naming the baseline and repo.
20
- - Latency claims — the honest run explicitly does not headline latency
21
- (Python process start dominates; real `rg` is tens of ms).
23
+ - Latency claims against external tools — the honest run explicitly does not
24
+ headline latency (Python process start dominates; real `rg` is tens of ms).
25
+ Version-over-version latency from the retrieval eval is in-process and is only
26
+ comparable to other runs of that harness.
22
27
 
23
28
  The defensible headline today is exactly: **on one 55k LOC Java repo, recall@3 was
24
29
  70% (index) vs 40% (`rg`+window), using ~13× fewer answer tokens.** Everything
25
30
  else is roadmap.
26
31
 
32
+ ## Retrieval evaluation (ranking gate)
33
+
34
+ See [tests/eval/README.md](../tests/eval/README.md) for the full protocol. In short:
35
+ ground truth comes from hand-written queries verified against the source tree and
36
+ from commit-subject → changed-files pairs mined from git history (leak-free: the
37
+ query text is not in the indexed corpus). Corpora are pooled across languages, one
38
+ index per corpus is shared by every variant, and every non-baseline row carries a
39
+ paired bootstrap CI and permutation p-value.
40
+
41
+ `1.9.0` was measured over 305 queries across Python, Java and TypeScript corpora
42
+ against `1.8.0`: MRR +0.027, MAP +0.028, nDCG@10 +0.024, recall@5 +0.031 (all
43
+ p < 0.001), with p50 latency 78.6 ms → 51.2 ms. These are version-over-version
44
+ ranking deltas on those corpora, not a universal quality claim.
45
+
27
46
  ## Public benchmark suite
28
47
 
29
48
  Run:
@@ -65,6 +65,11 @@ source)` list so fusion is source-agnostic.
65
65
  first when the query clearly names a path.
66
66
  - **Symbol** — query against `symbols` (exact, identifier parts, bounded fuzzy matching). Carries
67
67
  `kind` (function/class/method/...) and signature. Primary for `locate_impl` / `find_refs`.
68
+ Fuzzy identifier matching (acronym / concatenation / edit distance) runs only as a **recall
69
+ fallback**, when the precise lookup named no symbol and returned fewer than
70
+ `fuzzy_fallback_min` rows. Measured over 305 queries on three repositories it moved no ranking
71
+ metric while costing ~20% of query latency, so it is kept for typos and abbreviations but no
72
+ longer runs when the query already spelled its identifier correctly.
68
73
  - **FTS** — FTS5 `bm25()` over the `fts_chunks` virtual table (chunk text + symbol names +
69
74
  summaries indexed). Query-time camelCase/snake_case splitting, small down-weighted synonym
70
75
  expansion, and soft coverage scoring make natural-language questions robust without weakening
@@ -79,28 +84,66 @@ source)` list so fusion is source-agnostic.
79
84
  scores:
80
85
 
81
86
  ```
82
- RRF(d) = Σ_r w_r / (k + rank_r(d)) # k ≈ 60, w_r = per-intent retriever weight
87
+ RRF(d) = Σ_r w_r · k / (k + rank_r(d)) # k ≈ 60, w_r = per-intent retriever weight
83
88
  ```
84
89
 
85
90
  - Robust to scale differences between BM25 and cosine.
86
91
  - Per-intent weights `w_r` let `locate_impl` favor the symbol list and `how_it_works` favor FTS.
92
+ - Scaled by `k` so fused scores and the reranker's bounded bonuses share an O(1) scale. This is a
93
+ monotonic rescale; fusion order is unchanged.
87
94
  - Ties broken by rerank features (next step).
88
95
 
89
- ## 4. Reranking (`retrieval/rerank.py`)
96
+ ### Cross-locator file agreement
97
+
98
+ Fusion keys on `(path, line-bucket)`, not `(path, start, end)`, because different retrievers report
99
+ different line ranges for the same place. Bucketing alone was not enough: a symbol defined at line
100
+ 40 and a lexical hit at line 120 are genuinely different locators, so a file that **two retrievers
101
+ agreed on** still fused as two separate candidates, each carrying one retriever's evidence — and
102
+ cross-source agreement, the entire point of RRF, never fired.
103
+
104
+ Each candidate therefore also receives, at weight `file_agreement_weight`, the RRF mass of every
105
+ retriever that found its *file* at some other locator:
106
+
107
+ ```
108
+ score(d) = RRF(d) + α · Σ_{r ∉ sources(d)} w_r · k / (k + best_rank_r(path(d)))
109
+ ```
110
+
111
+ Retrievers already counted at the candidate's own locator are excluded, so nothing double-counts,
112
+ and the term is bounded by the same weights as fusion itself. `α = 0.4`; the 0.3–0.6 plateau peaks
113
+ there. Set `RetrievalTuning(file_agreement=False)` to recover plain locator-only RRF.
90
114
 
91
- A lightweight, explainable feature score (no external model required) layered on the fused order:
115
+ ## 4. Reranking (`retrieval/rerank.py`)
92
116
 
93
- | Feature | Intuition |
94
- |---|---|
95
- | symbol-kind match | a `def`/`class` outranks an incidental mention |
96
- | path proximity | files near a query-named path score higher |
97
- | graph centrality | high in/out-degree nodes matter more for `architecture` |
98
- | recency | recently changed files (git mtime) slightly boosted |
99
- | exact-name bonus | exact symbol-name match dominates fuzzy |
100
- | test/generated penalty | test files and generated code demoted unless asked |
117
+ A lightweight, explainable feature score (no external model required) layered on the fused order.
118
+ Every term is bounded, so reranking reorders near-neighbours rather than overruling retrieval:
119
+
120
+ | Feature | Effect | Intuition |
121
+ |---|---:|---|
122
+ | Exact symbol match | +0.20 | the user named a specific symbol |
123
+ | Symbol definition kind | +0.05 | a `def`/`class` outranks an incidental mention |
124
+ | Symbol name among query terms | +0.05 | the name was asked for, not just matched |
125
+ | Path term match | +0.05 | the user supplied a location clue |
126
+ | Graph centrality (`in_degree`) | ≤ +0.08 | `log1p`-damped, so a god class cannot dominate |
127
+ | Reference-count fallback | ≤ +0.04 | for names too common to resolve a precise `in_degree` |
128
+ | Source role prior | −0.25…+0.08 | see below |
129
+ | Generated, or test on a non-test query | −0.15 | supporting evidence, not the answer |
130
+
131
+ ### Source role priors (`retrieval/priors.py`)
132
+
133
+ | Role | Prior | Rationale |
134
+ |---|---:|---|
135
+ | Implementation | +0.08 | the answer to a code question is usually code |
136
+ | Test | −0.06 | flips to +0.05 when the query or intent is test-oriented |
137
+ | Documentation | −0.20 | prose *about* a feature matches a natural-language question more literally than the code implementing it, so design notes and plans crowded out the modules they describe |
138
+ | Generated / vendor / build | −0.25 | never the answer; kept strictly below documentation |
139
+
140
+ The documentation prior deepened from −0.05 in 1.8.0. Because it only reorders prose relative to
141
+ code — never below other prose — documentation-seeking queries improved too (MRR 0.579 → 0.612).
142
+ At −0.35 that reverses and the `docs` category collapses, so the optimum is interior, not a
143
+ "more is better" knob. `MAX_ABS_PRIOR` caps every prior so this stays a tiebreaker.
101
144
 
102
145
  The reranker also produces the human-readable **`reason`** string per result
103
- (e.g. *"exact symbol match · called by 4 sites · in src/auth/"*).
146
+ (e.g. *"exact symbol match · 4 callers · in src/auth/"*).
104
147
 
105
148
  ## 5. Graph expansion (`graph/retrieval.py`; `graph/expand.py` for impact APIs)
106
149
 
@@ -119,11 +162,17 @@ Expanded nodes retain edge confidence and receive distance-decayed scores so see
119
162
 
120
163
  ## 6. Diversity and duplicate control
121
164
 
122
- `retrieval.diversity` provides bounded MMR selection and SimHash near-duplicate
123
- suppression. MMR is disabled in the shipped default because the reproducible
124
- benchmark favored relevance-only ranking; callers that need broader snippet
125
- coverage can enable `RetrievalTuning(mmr=True)`. Duplicate suppression remains
126
- available independently.
165
+ `retrieval.diversity` provides bounded MMR selection and SimHash near-duplicate suppression.
166
+
167
+ MMR is disabled in the shipped default: it moved no ranking metric on the benchmark and roughly
168
+ doubled p50 latency. Callers that need broader snippet coverage can enable
169
+ `RetrievalTuning(mmr=True)`.
170
+
171
+ SimHash duplicate suppression stays on, but on noise grounds rather than ranking grounds: it does
172
+ not move MRR, and it takes the duplicate rate of returned snippets from ~1.6% to ~0%. The
173
+ over-fetch that feeds selection is now an explicit `candidate_pool_multiplier` rather than an
174
+ implicit side effect of enabling dedup — an earlier ablation credited dedup with a quality win that
175
+ was really the wider pool doing the work.
127
176
 
128
177
  ## 7. Token budgeting (`retrieval/budget.py`)
129
178
 
@@ -97,17 +97,21 @@ direct lexical or symbol evidence.
97
97
 
98
98
  ## 6. Diversity and duplicate control
99
99
 
100
- SimHash suppresses near-duplicate snippets independently of MMR. Bounded
101
- Maximal Marginal Relevance is available through `RetrievalTuning(mmr=True)`;
102
- the shipped default keeps relevance-only ordering because the benchmark favored
103
- direct hits.
100
+ SimHash suppresses near-duplicate snippets independently of MMR. It does not move
101
+ ranking metrics; it earns its place by taking the duplicate rate of returned
102
+ snippets from ~1.6% to ~0%. Bounded Maximal Marginal Relevance is available
103
+ through `RetrievalTuning(mmr=True)`; the shipped default keeps relevance-only
104
+ ordering because MMR moved no metric and roughly doubled p50 latency.
105
+
106
+ The over-fetch that feeds these selection stages is `candidate_pool_multiplier`
107
+ (default 2), an explicit knob rather than a side effect of enabling dedup.
104
108
 
105
109
  ## 7. Reciprocal Rank Fusion (RRF)
106
110
 
107
111
  Combines ranked lists from the enabled retrievers:
108
112
 
109
113
  ```
110
- RRF_score(d) = Σ w_r · k / (k + rank_r(d))
114
+ RRF_score(d) = Σ_r w_r · k / (k + rank_r(d))
111
115
  ```
112
116
 
113
117
  The implementation multiplies textbook RRF by `k` so fusion and bounded rerank
@@ -118,8 +122,13 @@ Where:
118
122
  - `rank_r(d)` is the rank of document `d` in retriever `r`
119
123
  - `w_r` is the intent/tuning weight for retriever `r`
120
124
 
121
- The implementation merges co-located chunks into one per-file bucket before
122
- fusion, preventing a large file from dominating the result list.
125
+ Candidates are keyed by `(path, line-bucket)`, so co-located hits merge. Because a
126
+ symbol definition and a lexical hit in the same file are often *not* co-located,
127
+ each candidate additionally receives — at `file_agreement_weight` (0.4) — the RRF
128
+ mass of every retriever that found its file at a different locator. Retrievers
129
+ already counted at that locator are excluded, so nothing double-counts. Without
130
+ this, two retrievers agreeing on a file produced two weak candidates instead of one
131
+ strong one, and cross-source agreement never affected the score.
123
132
 
124
133
  ## 8. Reranking
125
134
 
@@ -132,8 +141,8 @@ After fusion, apply bounded explainable boosts and penalties:
132
141
  | Path term match | +0.05 | User supplied a location clue |
133
142
  | Degree / reference evidence | up to +0.08 | Stable structural tiebreaker |
134
143
  | Implementation source prior | +0.08 | Prefer source over prose/tests |
135
- | Documentation source prior | -0.05 | Avoid docs displacing implementation |
136
- | Generated/vendor/build | -0.12 | Suppress low-value derived code |
144
+ | Documentation source prior | -0.20 | Prose describing a feature outmatches the code lexically |
145
+ | Generated/vendor/build | -0.25 | Suppress low-value derived code |
137
146
  | Test path on non-test query | -0.06 | Keep tests as supporting evidence |
138
147
 
139
148
  ## 9. Confidence
@@ -1,3 +1,3 @@
1
- codebase-index @ https://github.com/denfry/codebase-index/archive/refs/tags/v1.8.0.tar.gz
1
+ codebase-index @ https://github.com/denfry/codebase-index/archive/refs/tags/v1.9.0.tar.gz
2
2
  tree-sitter==0.25.2
3
3
  tree-sitter-language-pack==1.8.1
@@ -4,4 +4,4 @@ Public surface is intentionally small; the CLI (`codebase_index.cli:app`) is the
4
4
  See docs/ARCHITECTURE.md for the module map.
5
5
  """
6
6
 
7
- __version__ = "1.8.0"
7
+ __version__ = "1.9.0"
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
30
30
  from ..config import Config
31
31
 
32
32
  try:
33
- from mcp.server.fastmcp import FastMCP
33
+ from mcp.server.fastmcp import FastMCP # type: ignore[attr-defined]
34
34
  except ImportError as exc: # pragma: no cover
35
35
  raise ImportError(
36
36
  "MCP server needs the optional extra: pip install codebase-index[mcp]"
@@ -10,7 +10,9 @@ from __future__ import annotations
10
10
 
11
11
  import hashlib
12
12
  import math
13
+ from collections import Counter
13
14
  from collections.abc import Callable, Iterable, Sequence
15
+ from functools import lru_cache
14
16
 
15
17
  from .types import Candidate
16
18
 
@@ -90,6 +92,18 @@ def normalize_code_tokens(content: str | None) -> tuple[str, ...]:
90
92
  return tuple(tokens)
91
93
 
92
94
 
95
+ @lru_cache(maxsize=1 << 16)
96
+ def _token_digest(token: str) -> int:
97
+ """64-bit blake2b digest of one token. Pure, so caching is safe and bounded.
98
+
99
+ Source chunks repeat identifiers and keywords heavily both inside a chunk and
100
+ across the candidate pool, and hashing dominated fingerprint cost.
101
+ """
102
+ return int.from_bytes(
103
+ hashlib.blake2b(token.encode("utf-8"), digest_size=8).digest(), "big", signed=False
104
+ )
105
+
106
+
93
107
  def token_fingerprint(tokens: Iterable[str] | str | None) -> int:
94
108
  """Compute a stable unsigned 64-bit SimHash for tokens or source content."""
95
109
  if tokens is None:
@@ -100,12 +114,14 @@ def token_fingerprint(tokens: Iterable[str] | str | None) -> int:
100
114
  if not values:
101
115
  return 0
102
116
 
117
+ # Weighting distinct tokens by multiplicity is arithmetically identical to
118
+ # summing each occurrence, and collapses the per-occurrence hash and 64-bit
119
+ # accumulation loop to per-distinct-token work. Output is bit-for-bit unchanged.
103
120
  weights = [0] * 64
104
- for token in values:
105
- digest = hashlib.blake2b(str(token).encode("utf-8"), digest_size=8).digest()
106
- hashed = int.from_bytes(digest, "big", signed=False)
121
+ for token, count in Counter(str(value) for value in values).items():
122
+ hashed = _token_digest(token)
107
123
  for bit in range(64):
108
- weights[bit] += 1 if (hashed >> bit) & 1 else -1
124
+ weights[bit] += count if (hashed >> bit) & 1 else -count
109
125
 
110
126
  fingerprint = 0
111
127
  for bit, weight in enumerate(weights):
@@ -133,8 +149,14 @@ def _score_value(candidate: Candidate) -> float:
133
149
 
134
150
 
135
151
  def _same_or_better(left: Candidate, right: Candidate) -> bool:
136
- """Whether left is the representative to retain; ties favor input order."""
137
- return _score_value(left) > _score_value(right)
152
+ """Whether `left` (the incumbent) should stay the representative.
153
+
154
+ Ties favor the incumbent, i.e. input order. Candidates arrive sorted by score,
155
+ so a later duplicate is never strictly better; deciding ties by ">" instead
156
+ handed the slot to whichever equal-scoring copy happened to arrive last, making
157
+ the retained snippet depend on retriever emission order.
158
+ """
159
+ return _score_value(left) >= _score_value(right)
138
160
 
139
161
 
140
162
  def deduplicate(candidates: Sequence[Candidate], hamming_distance: int = 3) -> list[Candidate]:
@@ -146,14 +168,21 @@ def deduplicate(candidates: Sequence[Candidate], hamming_distance: int = 3) -> l
146
168
  fingerprints: list[int | None] = []
147
169
 
148
170
  for candidate in candidates:
149
- _, _, fingerprint = _features(candidate)
171
+ # Only the fingerprint is needed here; building the token set as well
172
+ # doubled the per-candidate cost of the pipeline's hottest stage. "No
173
+ # fingerprint" means "no tokens", never "fingerprint happened to be 0".
174
+ tokens = normalize_code_tokens(candidate.content)
175
+ fingerprint = token_fingerprint(tokens) if tokens else None
150
176
  if fingerprint is None:
151
177
  representatives.append(candidate)
152
178
  fingerprints.append(None)
153
179
  continue
154
180
 
181
+ # Carry each match's fingerprint alongside its index: it is known non-None
182
+ # by construction here, and re-reading it from the list later would lose
183
+ # that guarantee.
155
184
  matches = [
156
- index
185
+ (index, existing)
157
186
  for index, existing in enumerate(fingerprints)
158
187
  if existing is not None and simhash_distance(fingerprint, existing) <= threshold
159
188
  ]
@@ -162,17 +191,23 @@ def deduplicate(candidates: Sequence[Candidate], hamming_distance: int = 3) -> l
162
191
  fingerprints.append(fingerprint)
163
192
  continue
164
193
 
165
- first = matches[0]
194
+ indices = [index for index, _ in matches]
195
+ first = indices[0]
166
196
  winner = candidate
167
- for index in matches:
197
+ # The winner is either this candidate or an incumbent, and both
198
+ # fingerprints are already known — recomputing one from content was pure
199
+ # duplicated work.
200
+ winner_fingerprint = fingerprint
201
+ for index, existing in matches:
168
202
  incumbent = representatives[index]
169
203
  if _same_or_better(incumbent, winner):
170
204
  winner = incumbent
171
- for index in reversed(matches):
205
+ winner_fingerprint = existing
206
+ for index in reversed(indices):
172
207
  representatives.pop(index)
173
208
  fingerprints.pop(index)
174
209
  representatives.insert(first, winner)
175
- fingerprints.insert(first, token_fingerprint(normalize_code_tokens(winner.content)))
210
+ fingerprints.insert(first, winner_fingerprint)
176
211
 
177
212
  return representatives
178
213
 
@@ -13,6 +13,14 @@ Two deliberate departures from the textbook formula:
13
13
  retrievers report different line ranges for the same place; an exact key almost
14
14
  never coincides across sources, so cross-source agreement — RRF's whole point —
15
15
  would never fire. `agreeing_sources` is therefore counted at file granularity.
16
+ * Cross-locator agreement is scored, not merely counted. Bucketing alone does not
17
+ rescue agreement: a symbol defined at line 40 and a lexical hit at line 120 sit
18
+ in different buckets of the same file, so two retrievers pointing at one file
19
+ still fused as two unrelated candidates, each carrying a single retriever's
20
+ evidence. `file_agreement` adds the missing evidence back at a discount — a
21
+ candidate also receives, weighted by `file_agreement_weight`, the RRF mass of
22
+ every retriever that found its *file* somewhere else. Retrievers already
23
+ counted at the candidate's own locator are excluded, so nothing double-counts.
16
24
 
17
25
  On merge, the candidate carrying the most signal (symbol > fts > path) is kept as
18
26
  the representative so downstream rerank/snippet logic has the richest fields.
@@ -36,11 +44,21 @@ def fuse(
36
44
  *,
37
45
  weights: dict[str, float],
38
46
  k: int,
47
+ file_agreement: float = 0.0,
39
48
  ) -> list[Candidate]:
49
+ """Fuse per-source ranked lists into one ordered candidate list.
50
+
51
+ `file_agreement` is the discount applied to same-file, different-locator
52
+ evidence; 0.0 reproduces plain locator-only RRF.
53
+ """
40
54
  accum: dict[tuple, float] = {}
41
55
  rep: dict[tuple, Candidate] = {}
42
56
  seen: set[tuple] = set()
43
57
  file_sources: dict[str, set[str]] = {}
58
+ # Best (lowest) rank each source achieved for each file, and which sources
59
+ # already contributed at each fused locator.
60
+ best_file_rank: dict[tuple[str, str], int] = {}
61
+ key_sources: dict[tuple, set[str]] = {}
44
62
 
45
63
  for source, candidates in lists.items():
46
64
  w = weights.get(source, 0.0)
@@ -48,7 +66,12 @@ def fuse(
48
66
  continue
49
67
  for rank, cand in enumerate(candidates):
50
68
  file_sources.setdefault(cand.path, set()).add(source)
69
+ file_key = (source, cand.path)
70
+ previous = best_file_rank.get(file_key)
71
+ if previous is None or rank < previous:
72
+ best_file_rank[file_key] = rank
51
73
  key = cand.fuse_key()
74
+ key_sources.setdefault(key, set()).add(source)
52
75
  # One contribution per source per locator: a file matching three FTS
53
76
  # chunks in the same bucket is one lexical signal, not three.
54
77
  if (source, key) in seen:
@@ -57,6 +80,21 @@ def fuse(
57
80
  accum[key] = accum.get(key, 0.0) + w * k / (k + rank)
58
81
  rep[key] = _richer(rep[key], cand) if key in rep else cand
59
82
 
83
+ if file_agreement > 0.0:
84
+ for key, base in accum.items():
85
+ path = rep[key].path
86
+ own = key_sources[key]
87
+ extra = 0.0
88
+ for source in file_sources.get(path, ()):
89
+ if source in own:
90
+ continue
91
+ w = weights.get(source, 0.0)
92
+ if w <= 0.0:
93
+ continue
94
+ extra += w * k / (k + best_file_rank[(source, path)])
95
+ if extra:
96
+ accum[key] = base + file_agreement * extra
97
+
60
98
  fused = [_replace(rep[key], score=score) for key, score in accum.items()]
61
99
  fused.sort(key=lambda c: c.score, reverse=True)
62
100
  return [_replace(c, agreeing_sources=len(file_sources[c.path])) for c in fused]