opencode-rag-plugin 1.15.1 → 1.17.0

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 (232) hide show
  1. package/ReadMe.md +4 -6
  2. package/dist/api.js +44 -24
  3. package/dist/chunker/base.d.ts +34 -3
  4. package/dist/chunker/base.js +70 -21
  5. package/dist/chunker/factory.d.ts +4 -1
  6. package/dist/chunker/factory.js +12 -1
  7. package/dist/chunker/grammar.js +3 -0
  8. package/dist/chunker/image.js +8 -8
  9. package/dist/chunker/pdf.js +11 -13
  10. package/dist/chunker/xml.d.ts +2 -0
  11. package/dist/chunker/xml.js +2 -0
  12. package/dist/cli/commands/index-command.js +0 -4
  13. package/dist/cli/commands/index.d.ts +1 -1
  14. package/dist/cli/commands/index.js +1 -1
  15. package/dist/cli/commands/init-helpers.d.ts +12 -0
  16. package/dist/cli/commands/init-helpers.js +84 -2
  17. package/dist/cli/commands/init.js +20 -2
  18. package/dist/cli/commands/query.js +1 -0
  19. package/dist/cli/commands/setup.d.ts +2 -0
  20. package/dist/cli/commands/setup.js +113 -0
  21. package/dist/cli/commands/status.js +64 -2
  22. package/dist/cli/index.js +2 -2
  23. package/dist/content/image.js +24 -3
  24. package/dist/content/reader.d.ts +6 -1
  25. package/dist/content/reader.js +49 -4
  26. package/dist/core/config.d.ts +19 -0
  27. package/dist/core/config.js +34 -4
  28. package/dist/core/desc-cache.d.ts +31 -0
  29. package/dist/core/desc-cache.js +124 -0
  30. package/dist/core/interfaces.d.ts +58 -3
  31. package/dist/core/manifest.d.ts +24 -1
  32. package/dist/core/manifest.js +34 -3
  33. package/dist/core/resolve-api-key.js +4 -2
  34. package/dist/core/runtime-overrides.js +9 -7
  35. package/dist/core/setup-runtime.d.ts +23 -0
  36. package/dist/core/setup-runtime.js +183 -0
  37. package/dist/core/version-check.d.ts +9 -0
  38. package/dist/core/version-check.js +49 -0
  39. package/dist/describer/anthropic.d.ts +2 -2
  40. package/dist/describer/anthropic.js +5 -7
  41. package/dist/describer/describer.d.ts +2 -2
  42. package/dist/describer/describer.js +6 -8
  43. package/dist/describer/gemini.d.ts +2 -2
  44. package/dist/describer/gemini.js +5 -7
  45. package/dist/embedder/factory.d.ts +3 -1
  46. package/dist/embedder/factory.js +7 -1
  47. package/dist/embedder/health.js +4 -0
  48. package/dist/embedder/http.d.ts +1 -1
  49. package/dist/embedder/http.js +74 -43
  50. package/dist/eval/compare-merge.d.ts +10 -0
  51. package/dist/eval/compare-merge.js +537 -0
  52. package/dist/eval/compare-rankings.d.ts +10 -0
  53. package/dist/eval/compare-rankings.js +245 -0
  54. package/dist/eval/dump-descriptions.d.ts +7 -0
  55. package/dist/eval/dump-descriptions.js +58 -0
  56. package/dist/eval/fast-index.d.ts +8 -0
  57. package/dist/eval/fast-index.js +283 -0
  58. package/dist/eval/run-branch-compare.d.ts +7 -0
  59. package/dist/eval/run-branch-compare.js +220 -0
  60. package/dist/eval/run-token-test.js +1 -0
  61. package/dist/eval/test-kw.d.ts +1 -0
  62. package/dist/eval/test-kw.js +22 -0
  63. package/dist/eval/update-descriptions.d.ts +7 -0
  64. package/dist/eval/update-descriptions.js +84 -0
  65. package/dist/index.d.ts +1 -0
  66. package/dist/index.js +1 -0
  67. package/dist/indexer/embed-stage.js +2 -1
  68. package/dist/indexer/git-diff.js +21 -9
  69. package/dist/indexer/pipeline.d.ts +6 -0
  70. package/dist/indexer/pipeline.js +290 -37
  71. package/dist/indexer/watch.js +1 -3
  72. package/dist/indexer/worker.d.ts +15 -2
  73. package/dist/indexer/worker.js +25 -12
  74. package/dist/mcp/handlers.d.ts +9 -0
  75. package/dist/mcp/handlers.js +23 -6
  76. package/dist/mcp/server.js +2 -0
  77. package/dist/opencode/create-read-tool.d.ts +2 -0
  78. package/dist/opencode/create-read-tool.js +8 -2
  79. package/dist/opencode/read-fallback.d.ts +1 -5
  80. package/dist/opencode/read-fallback.js +1 -18
  81. package/dist/opencode/read-format.js +5 -3
  82. package/dist/opencode/tools.js +5 -7
  83. package/dist/plugin.js +141 -57
  84. package/dist/retriever/keyword-index.d.ts +3 -2
  85. package/dist/retriever/keyword-index.js +25 -1
  86. package/dist/retriever/retriever.d.ts +4 -1
  87. package/dist/retriever/retriever.js +34 -56
  88. package/dist/vectorstore/lancedb.d.ts +35 -4
  89. package/dist/vectorstore/lancedb.js +146 -23
  90. package/dist/vectorstore/memory.d.ts +6 -1
  91. package/dist/vectorstore/memory.js +58 -0
  92. package/dist/watcher.js +3 -0
  93. package/dist/web/api.js +10 -2
  94. package/dist/web/server.js +18 -3
  95. package/package.json +8 -9
  96. package/scripts/postinstall-setup.js +82 -0
  97. package/dist/api.js.map +0 -1
  98. package/dist/chunker/base.js.map +0 -1
  99. package/dist/chunker/bash.js.map +0 -1
  100. package/dist/chunker/c.js.map +0 -1
  101. package/dist/chunker/cpp.js.map +0 -1
  102. package/dist/chunker/csharp.js.map +0 -1
  103. package/dist/chunker/css.js.map +0 -1
  104. package/dist/chunker/doc.js.map +0 -1
  105. package/dist/chunker/dockerfile.js.map +0 -1
  106. package/dist/chunker/docx.js.map +0 -1
  107. package/dist/chunker/excel.js.map +0 -1
  108. package/dist/chunker/factory.js.map +0 -1
  109. package/dist/chunker/fallback.js.map +0 -1
  110. package/dist/chunker/go.js.map +0 -1
  111. package/dist/chunker/grammar.js.map +0 -1
  112. package/dist/chunker/html.js.map +0 -1
  113. package/dist/chunker/image.js.map +0 -1
  114. package/dist/chunker/ini.js.map +0 -1
  115. package/dist/chunker/java.js.map +0 -1
  116. package/dist/chunker/javascript.js.map +0 -1
  117. package/dist/chunker/json.js.map +0 -1
  118. package/dist/chunker/kotlin.js.map +0 -1
  119. package/dist/chunker/loader.js.map +0 -1
  120. package/dist/chunker/markdown.js.map +0 -1
  121. package/dist/chunker/pdf.js.map +0 -1
  122. package/dist/chunker/php.js.map +0 -1
  123. package/dist/chunker/powershell.js.map +0 -1
  124. package/dist/chunker/python.js.map +0 -1
  125. package/dist/chunker/razor.js.map +0 -1
  126. package/dist/chunker/ruby.js.map +0 -1
  127. package/dist/chunker/rust.js.map +0 -1
  128. package/dist/chunker/sln.js.map +0 -1
  129. package/dist/chunker/sql.js.map +0 -1
  130. package/dist/chunker/ssl.js.map +0 -1
  131. package/dist/chunker/swift.js.map +0 -1
  132. package/dist/chunker/tex.js.map +0 -1
  133. package/dist/chunker/toml.js.map +0 -1
  134. package/dist/chunker/typescript.js.map +0 -1
  135. package/dist/chunker/uuid.js.map +0 -1
  136. package/dist/chunker/xml.js.map +0 -1
  137. package/dist/chunker/yaml.js.map +0 -1
  138. package/dist/cli/commands/clear.js.map +0 -1
  139. package/dist/cli/commands/describe-image.js.map +0 -1
  140. package/dist/cli/commands/dump.js.map +0 -1
  141. package/dist/cli/commands/eval.js.map +0 -1
  142. package/dist/cli/commands/index-command.js.map +0 -1
  143. package/dist/cli/commands/index.js.map +0 -1
  144. package/dist/cli/commands/init-helpers.js.map +0 -1
  145. package/dist/cli/commands/init.js.map +0 -1
  146. package/dist/cli/commands/list.js.map +0 -1
  147. package/dist/cli/commands/mcp.js.map +0 -1
  148. package/dist/cli/commands/query.js.map +0 -1
  149. package/dist/cli/commands/show.js.map +0 -1
  150. package/dist/cli/commands/status.js.map +0 -1
  151. package/dist/cli/commands/ui.js.map +0 -1
  152. package/dist/cli/commands/update.d.ts +0 -17
  153. package/dist/cli/commands/update.js +0 -79
  154. package/dist/cli/commands/update.js.map +0 -1
  155. package/dist/cli/format.js.map +0 -1
  156. package/dist/cli/helpers.js.map +0 -1
  157. package/dist/cli/index.js.map +0 -1
  158. package/dist/cli/progress.d.ts +0 -42
  159. package/dist/cli/progress.js +0 -137
  160. package/dist/cli/progress.js.map +0 -1
  161. package/dist/cli/types.js.map +0 -1
  162. package/dist/cli.js.map +0 -1
  163. package/dist/content/doc.js.map +0 -1
  164. package/dist/content/docx.js.map +0 -1
  165. package/dist/content/excel.js.map +0 -1
  166. package/dist/content/image.js.map +0 -1
  167. package/dist/content/pdf.js.map +0 -1
  168. package/dist/content/reader.js.map +0 -1
  169. package/dist/content/types.js.map +0 -1
  170. package/dist/core/bootstrap.js.map +0 -1
  171. package/dist/core/config.js.map +0 -1
  172. package/dist/core/doc-progress.js.map +0 -1
  173. package/dist/core/fileLogger.js.map +0 -1
  174. package/dist/core/interfaces.js.map +0 -1
  175. package/dist/core/manifest.js.map +0 -1
  176. package/dist/core/provider-defaults.js.map +0 -1
  177. package/dist/core/rag-injection-flag.js.map +0 -1
  178. package/dist/core/resolve-api-key.js.map +0 -1
  179. package/dist/core/runtime-overrides.js.map +0 -1
  180. package/dist/describer/anthropic.js.map +0 -1
  181. package/dist/describer/describer.js.map +0 -1
  182. package/dist/describer/factory.js.map +0 -1
  183. package/dist/describer/gemini.js.map +0 -1
  184. package/dist/describer/shared.js.map +0 -1
  185. package/dist/embedder/cohere.js.map +0 -1
  186. package/dist/embedder/factory.js.map +0 -1
  187. package/dist/embedder/health.js.map +0 -1
  188. package/dist/embedder/http.js.map +0 -1
  189. package/dist/embedder/ollama.js.map +0 -1
  190. package/dist/embedder/openai.js.map +0 -1
  191. package/dist/eval/index.js.map +0 -1
  192. package/dist/eval/run-token-test.js.map +0 -1
  193. package/dist/eval/session-logger.js.map +0 -1
  194. package/dist/eval/storage.js.map +0 -1
  195. package/dist/eval/token-analysis.js.map +0 -1
  196. package/dist/eval/token-counter.js.map +0 -1
  197. package/dist/eval/types.js.map +0 -1
  198. package/dist/index.js.map +0 -1
  199. package/dist/indexer/description-stage.js.map +0 -1
  200. package/dist/indexer/embed-stage.js.map +0 -1
  201. package/dist/indexer/git-diff.js.map +0 -1
  202. package/dist/indexer/metadata.js.map +0 -1
  203. package/dist/indexer/pipeline.js.map +0 -1
  204. package/dist/indexer/stats.js.map +0 -1
  205. package/dist/indexer/watch.js.map +0 -1
  206. package/dist/indexer/worker.js.map +0 -1
  207. package/dist/indexer.js.map +0 -1
  208. package/dist/mcp/cli.js.map +0 -1
  209. package/dist/mcp/handlers.js.map +0 -1
  210. package/dist/mcp/server.js.map +0 -1
  211. package/dist/opencode/create-read-tool.js.map +0 -1
  212. package/dist/opencode/read-fallback.js.map +0 -1
  213. package/dist/opencode/read-format.js.map +0 -1
  214. package/dist/opencode/read-query.js.map +0 -1
  215. package/dist/opencode/tool-args.js.map +0 -1
  216. package/dist/opencode/tools.js.map +0 -1
  217. package/dist/plugin-entry.js.map +0 -1
  218. package/dist/plugin.js.map +0 -1
  219. package/dist/retriever/context-optimizer.js.map +0 -1
  220. package/dist/retriever/keyword-index.js.map +0 -1
  221. package/dist/retriever/retriever.js.map +0 -1
  222. package/dist/tui.js.map +0 -1
  223. package/dist/updater.d.ts +0 -45
  224. package/dist/updater.js +0 -175
  225. package/dist/updater.js.map +0 -1
  226. package/dist/vectorstore/factory.js.map +0 -1
  227. package/dist/vectorstore/lancedb.js.map +0 -1
  228. package/dist/vectorstore/memory.js.map +0 -1
  229. package/dist/watcher.js.map +0 -1
  230. package/dist/web/api.js.map +0 -1
  231. package/dist/web/server.js.map +0 -1
  232. package/dist/web/static.js.map +0 -1
@@ -0,0 +1,245 @@
1
+ /**
2
+ * @fileoverview Ranking order comparison between two branch benchmark runs.
3
+ * Focuses on rank agreement rather than absolute scores.
4
+ *
5
+ * Usage: node --import tsx src/eval/compare-rankings.ts
6
+ * --main .opencode/rag_db/eval-results/main.json
7
+ * --branch .opencode/rag_db/eval-results/t1-cosine-l2.json
8
+ * --output .opencode/rag_db/eval-results/ranking-report.md
9
+ */
10
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
11
+ import path from "node:path";
12
+ function parseArgs() {
13
+ const args = process.argv.slice(2);
14
+ let main = "", branch = "", output = ".opencode/rag_db/eval-results/ranking-report.md";
15
+ for (let i = 0; i < args.length; i++) {
16
+ if (args[i] === "--main" && args[i + 1]) {
17
+ main = path.resolve(args[i + 1]);
18
+ i++;
19
+ }
20
+ else if (args[i] === "--branch" && args[i + 1]) {
21
+ branch = path.resolve(args[i + 1]);
22
+ i++;
23
+ }
24
+ else if (args[i] === "--output" && args[i + 1]) {
25
+ output = path.resolve(args[i + 1]);
26
+ i++;
27
+ }
28
+ }
29
+ if (!main || !branch) {
30
+ console.error("Usage: node --import tsx src/eval/compare-rankings.ts --main <json> --branch <json> [--output <md>]");
31
+ process.exit(1);
32
+ }
33
+ return { main, branch, output };
34
+ }
35
+ function normalizePath(fp) {
36
+ // Strip clone-directory prefix from paths like "../OpenCodeRAG-main/src/foo.ts"
37
+ return fp.replace(/^\.\.[\/\\][^\/\\]+[\/\\]/, "");
38
+ }
39
+ function identityKey(r) {
40
+ return normalizePath(r.filePath) + ":" + r.startLine;
41
+ }
42
+ /** Kendall's tau-b rank correlation for two orderings of the same set. */
43
+ function kendallTau(rankA, rankB) {
44
+ const ids = [...rankA.keys()];
45
+ let concordant = 0, discordant = 0;
46
+ for (let i = 0; i < ids.length; i++) {
47
+ for (let j = i + 1; j < ids.length; j++) {
48
+ const aI = rankA.get(ids[i]), aJ = rankA.get(ids[j]);
49
+ const bI = rankB.get(ids[i]), bJ = rankB.get(ids[j]);
50
+ const diffA = aI - aJ, diffB = bI - bJ;
51
+ if (diffA * diffB > 0)
52
+ concordant++;
53
+ else if (diffA * diffB < 0)
54
+ discordant++;
55
+ }
56
+ }
57
+ const total = concordant + discordant;
58
+ return total === 0 ? 1 : (concordant - discordant) / total;
59
+ }
60
+ function avg(arr) {
61
+ return arr.length === 0 ? 0 : arr.reduce((s, v) => s + v, 0) / arr.length;
62
+ }
63
+ async function main() {
64
+ const { main: mainPath, branch: branchPath, output } = parseArgs();
65
+ const mainData = JSON.parse(readFileSync(mainPath, "utf-8"));
66
+ const branchData = JSON.parse(readFileSync(branchPath, "utf-8"));
67
+ const mBranch = mainData.branch + " (" + mainData.commit + ")";
68
+ const bBranch = branchData.branch + " (" + branchData.commit + ")";
69
+ const perQuery = [];
70
+ let totalTop1Same = 0, totalTop3Same = 0, totalTop5Same = 0, totalFullSame = 0;
71
+ let totalOverlap5 = 0, totalOverlap20 = 0;
72
+ const kendalls = [];
73
+ const maxQ = Math.min(mainData.queries.length, branchData.queries.length);
74
+ for (let i = 0; i < maxQ; i++) {
75
+ const mq = mainData.queries[i], bq = branchData.queries[i];
76
+ const mTop = mq.topResults, bTop = bq.topResults;
77
+ const mKey = mTop.map(identityKey);
78
+ const bKey = bTop.map(identityKey);
79
+ const top1Same = mKey[0] === bKey[0];
80
+ const top3Same = mKey[0] === bKey[0] && mKey[1] === bKey[1] && mKey[2] === bKey[2];
81
+ const top5Same = mKey.slice(0, 5).every((k, j) => k === bKey[j]);
82
+ const topKFullSame = mKey.every((k, j) => k === bKey[j]);
83
+ const set5 = new Set(bKey.slice(0, 5));
84
+ const overlapTop5 = mKey.slice(0, 5).filter((k) => set5.has(k)).length;
85
+ const set20 = new Set(bKey);
86
+ const overlapTop20 = mKey.filter((k) => set20.has(k)).length;
87
+ const aRank = new Map(mKey.map((k, j) => [k, j]));
88
+ const bRank = new Map(bKey.map((k, j) => [k, j]));
89
+ const sharedIds = [...aRank.keys()].filter((k) => bRank.has(k));
90
+ const sharedRankA = new Map(sharedIds.map((k) => [k, aRank.get(k)]));
91
+ const sharedRankB = new Map(sharedIds.map((k) => [k, bRank.get(k)]));
92
+ const tau = kendallTau(sharedRankA, sharedRankB);
93
+ if (top1Same)
94
+ totalTop1Same++;
95
+ if (top3Same)
96
+ totalTop3Same++;
97
+ if (top5Same)
98
+ totalTop5Same++;
99
+ if (topKFullSame)
100
+ totalFullSame++;
101
+ totalOverlap5 += overlapTop5;
102
+ totalOverlap20 += overlapTop20;
103
+ kendalls.push(tau);
104
+ perQuery.push({
105
+ query: mq.query,
106
+ top1Same,
107
+ top3Same,
108
+ top5Same,
109
+ topKFullSame,
110
+ overlapTop5,
111
+ overlapTop20,
112
+ kendallTau: tau,
113
+ mTopScore: mTop[0]?.score ?? 0,
114
+ bTopScore: bTop[0]?.score ?? 0,
115
+ });
116
+ }
117
+ // Print console output
118
+ const SEP = "─";
119
+ console.log("\n" + SEP.repeat(80));
120
+ console.log(" RANKING ORDER COMPARISON");
121
+ console.log(" " + mBranch + " vs " + bBranch);
122
+ console.log(SEP.repeat(80) + "\n");
123
+ const w = [38, 14];
124
+ const top = "┌─" + w.map((x) => SEP.repeat(x)).join("─┬─") + "─┐";
125
+ const sep = "├─" + w.map((x) => SEP.repeat(x)).join("─┼─") + "─┤";
126
+ const bot = "└─" + w.map((x) => SEP.repeat(x)).join("─┴─") + "─┘";
127
+ const cell = (s, i) => s.length >= w[i] ? s.slice(0, w[i]) : s + " ".repeat(w[i] - s.length);
128
+ console.log(" Overlap Summary:");
129
+ console.log(" " + top);
130
+ console.log(" │ " + cell("Metric", 0) + " │ " + cell("Value", 1) + " │");
131
+ console.log(" " + sep);
132
+ console.log(" │ " + cell("Top-1 match", 0) + " │ " + cell(totalTop1Same + "/" + maxQ, 1) + " │");
133
+ console.log(" │ " + cell("Top-3 identical", 0) + " │ " + cell(totalTop3Same + "/" + maxQ, 1) + " │");
134
+ console.log(" │ " + cell("Top-5 identical", 0) + " │ " + cell(totalTop5Same + "/" + maxQ, 1) + " │");
135
+ console.log(" │ " + cell("Full top-K identical", 0) + " │ " + cell(totalFullSame + "/" + maxQ, 1) + " │");
136
+ console.log(" │ " + cell("Avg overlap in top-5", 0) + " │ " + cell((totalOverlap5 / maxQ).toFixed(1), 1) + " │");
137
+ console.log(" │ " + cell("Avg overlap in top-K", 0) + " │ " + cell((totalOverlap20 / maxQ).toFixed(1), 1) + " │");
138
+ console.log(" │ " + cell("Kendall's τ (avg)", 0) + " │ " + cell(avg(kendalls).toFixed(4), 1) + " │");
139
+ console.log(" " + bot + "\n");
140
+ if (totalFullSame === maxQ) {
141
+ console.log(" ▲ Ranking is IDENTICAL across all " + maxQ + " queries.");
142
+ console.log(" RRF and linear fusion produce the same rank order\n");
143
+ }
144
+ else if (totalTop5Same === maxQ) {
145
+ console.log(" ▲ Top-5 ranking is IDENTICAL across all queries.");
146
+ console.log(" Minor differences exist beyond position 5.\n");
147
+ }
148
+ else {
149
+ console.log(" ◆ Ranking order differs between branches.\n");
150
+ console.log(" With keyword contributions active, RRF and linear fusion");
151
+ console.log(" produce different rank orderings. RRF rewards results that");
152
+ console.log(" rank highly in BOTH signals over results that rank well in");
153
+ console.log(" only ONE signal.\n");
154
+ }
155
+ // Also show queries where keyword matched
156
+ const queriesWithKeyword = mainData.queries.filter((q) => q.topResults.some((r) => r.explanation && r.explanation.rawKeywordScore > 0));
157
+ console.log(" Queries with keyword contributions: " + queriesWithKeyword.length + "/" + maxQ);
158
+ if (queriesWithKeyword.length === 0) {
159
+ console.log(" (No keyword index matches found in this index)\n");
160
+ }
161
+ // Write markdown report
162
+ const report = [];
163
+ report.push("# Ranking Order Comparison");
164
+ report.push("");
165
+ report.push("**" + mBranch + "** vs **" + bBranch + "**");
166
+ report.push("**Generated:** " + new Date().toISOString());
167
+ report.push("");
168
+ report.push("## Config");
169
+ report.push("");
170
+ report.push("| Setting | `" + mainData.branch + "` | `" + branchData.branch + "` |");
171
+ report.push("|---|---|---|");
172
+ report.push("| Embedding | " + mainData.config.embeddingModel + " | " + branchData.config.embeddingModel + " |");
173
+ report.push("| topK | " + mainData.config.topK + " | " + branchData.config.topK + " |");
174
+ report.push("| minScore | " + mainData.config.minScore + " | " + branchData.config.minScore + " |");
175
+ report.push("| Hybrid | " + mainData.config.hybridEnabled + " | " + branchData.config.hybridEnabled + " |");
176
+ report.push("| keywordWeight | " + mainData.config.keywordWeight + " | " + branchData.config.keywordWeight + " |");
177
+ report.push("| Index chunks | " + mainData.indexChunkCount + " | " + branchData.indexChunkCount + " |");
178
+ report.push("");
179
+ report.push("## Ranking Agreement");
180
+ report.push("");
181
+ report.push("| Metric | Value |");
182
+ report.push("|---|---|");
183
+ report.push("| Top-1 match | " + totalTop1Same + "/" + maxQ + " |");
184
+ report.push("| Top-3 identical | " + totalTop3Same + "/" + maxQ + " |");
185
+ report.push("| Top-5 identical | " + totalTop5Same + "/" + maxQ + " |");
186
+ report.push("| Full top-K identical | " + totalFullSame + "/" + maxQ + " |");
187
+ report.push("| Avg overlap in top-5 | " + (totalOverlap5 / maxQ).toFixed(1) + " |");
188
+ report.push("| Avg overlap in top-K | " + (totalOverlap20 / maxQ).toFixed(1) + " |");
189
+ report.push("| Kendall's τ (avg) | " + avg(kendalls).toFixed(4) + " |");
190
+ report.push("| Queries with keyword contribution | " + queriesWithKeyword.length + "/" + maxQ + " |");
191
+ report.push("");
192
+ if (totalFullSame === maxQ) {
193
+ report.push("## Verdict");
194
+ report.push("");
195
+ report.push("**Ranking is 100% identical across all queries.**");
196
+ report.push("");
197
+ report.push("The `t1-cosine-l2` branch changes two things simultaneously:");
198
+ report.push("- Vector scoring: L2 distance → cosine similarity");
199
+ report.push("- Hybrid fusion: Weighted linear combination → RRF (K=60)");
200
+ report.push("");
201
+ report.push("However, in this benchmark, **keyword scores are zero on every query**");
202
+ report.push("because the keyword index doesn't match any query terms. When only one signal");
203
+ report.push("(vector similarity) contributes, both fusion methods produce the same");
204
+ report.push("rank order. This is because both are **monotonically decreasing functions**");
205
+ report.push("of the vector rank:");
206
+ report.push("");
207
+ report.push("- **Linear**: `score = (1-kw) · normVectorScore` (monotonic in vector score)");
208
+ report.push("- **RRF**: `score = (1-kw) / (K + rank + 1)` (monotonic in vector rank)");
209
+ report.push("");
210
+ report.push("Since vector rank is itself monotonic with vector score, the final ordering");
211
+ report.push("is identical regardless of which formula is used.");
212
+ report.push("");
213
+ report.push("### When would RRF make a difference?");
214
+ report.push("");
215
+ report.push("RRF excels when **both vector AND keyword signals contribute** to a query.");
216
+ report.push("It can boost results that rank highly in both sources while demoting results");
217
+ report.push("that only rank well in one. To see this effect:");
218
+ report.push("- Index more files (including docs with token-rich content)");
219
+ report.push("- Use queries with specific identifier/keyword terms that match the keyword index");
220
+ report.push("- Increase keywordWeight to amplify keyword contributions");
221
+ report.push("");
222
+ }
223
+ report.push("## Per-Query Detail");
224
+ report.push("");
225
+ report.push("| # | Query | Top-1 same | Top-5 same | Full same | τ | main score | branch score |");
226
+ report.push("|---|---|:---:|:---:|:---:|:---:|:---:|:---:|");
227
+ for (let i = 0; i < perQuery.length; i++) {
228
+ const pq = perQuery[i];
229
+ const q = pq.query.length > 48 ? pq.query.substring(0, 45) + "..." : pq.query;
230
+ report.push("| " + (i + 1) + " | " + q +
231
+ " | " + (pq.top1Same ? "✓" : "✗") +
232
+ " | " + (pq.top5Same ? "✓" : "✗") +
233
+ " | " + (pq.topKFullSame ? "✓" : "✗") +
234
+ " | " + pq.kendallTau.toFixed(3) +
235
+ " | " + pq.mTopScore.toFixed(3) +
236
+ " | " + pq.bTopScore.toFixed(3) +
237
+ " |");
238
+ }
239
+ report.push("");
240
+ mkdirSync(path.dirname(output), { recursive: true });
241
+ writeFileSync(output, report.join("\n"), "utf-8");
242
+ console.log(" Report written to: " + output + "\n");
243
+ }
244
+ main().catch((err) => { console.error("Ranking comparison failed:", err); process.exit(1); });
245
+ //# sourceMappingURL=compare-rankings.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Dump all chunk descriptions from a LanceDB index to a JSON file.
3
+ * Used to copy descriptions from one branch to another for fair comparison.
4
+ *
5
+ * Usage: node --import tsx src/eval/dump-descriptions.ts --output .opencode/rag_db/chunk-descriptions.json
6
+ */
7
+ export {};
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @fileoverview Dump all chunk descriptions from a LanceDB index to a JSON file.
3
+ * Used to copy descriptions from one branch to another for fair comparison.
4
+ *
5
+ * Usage: node --import tsx src/eval/dump-descriptions.ts --output .opencode/rag_db/chunk-descriptions.json
6
+ */
7
+ import { writeFileSync, mkdirSync } from "node:fs";
8
+ import path from "node:path";
9
+ const WORKTREE = process.cwd();
10
+ const STORE_PATH = path.join(WORKTREE, ".opencode", "rag_db");
11
+ async function main() {
12
+ const args = process.argv.slice(2);
13
+ let outputPath = path.join(WORKTREE, ".opencode", "rag_db", "chunk-descriptions.json");
14
+ for (let i = 0; i < args.length; i++) {
15
+ if (args[i] === "--output" && args[i + 1]) {
16
+ outputPath = path.resolve(WORKTREE, args[i + 1]);
17
+ break;
18
+ }
19
+ }
20
+ console.log(` Dumping descriptions from: ${STORE_PATH}`);
21
+ console.log(` Output: ${outputPath}`);
22
+ const lancedb = await import("@lancedb/lancedb");
23
+ const db = await lancedb.connect(STORE_PATH);
24
+ const tableNames = await db.tableNames();
25
+ if (!tableNames.includes("chunks")) {
26
+ console.error(" No chunks table found in the store.");
27
+ await db.close();
28
+ process.exit(1);
29
+ }
30
+ const table = await db.openTable("chunks");
31
+ const count = await table.countRows();
32
+ console.log(` Total rows: ${count}`);
33
+ const rows = await table.query().select(["id", "description", "filePath", "startLine", "endLine"]).limit(count).toArray();
34
+ const descriptions = {};
35
+ let hasDesc = 0;
36
+ for (const row of rows) {
37
+ const filePath = row.filePath.replace(/\\/g, "/");
38
+ const startLine = row.startLine;
39
+ const endLine = row.endLine;
40
+ const desc = row.description ?? "";
41
+ const key = `${filePath}:${startLine}:${endLine}`;
42
+ descriptions[key] = desc;
43
+ if (desc)
44
+ hasDesc++;
45
+ }
46
+ const dir = path.dirname(outputPath);
47
+ mkdirSync(dir, { recursive: true });
48
+ writeFileSync(outputPath, JSON.stringify(descriptions, null, 2), "utf-8");
49
+ console.log(` Dumped ${Object.keys(descriptions).length} chunks`);
50
+ console.log(` ${hasDesc} have non-empty descriptions`);
51
+ console.log(` Written to: ${outputPath}`);
52
+ await db.close();
53
+ }
54
+ main().catch((err) => {
55
+ console.error("Dump failed:", err);
56
+ process.exit(1);
57
+ });
58
+ //# sourceMappingURL=dump-descriptions.js.map
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Fast indexer that uses pre-dumped descriptions instead of LLM generation.
3
+ * Indexes only files unchanged between the current branch and main.
4
+ * Run on the main branch after copying chunk-descriptions.json from t1-cosine-l2.
5
+ *
6
+ * Usage: node --import tsx src/eval/fast-index.ts --descriptions .opencode/rag_db/chunk-descriptions.json
7
+ */
8
+ export {};
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @fileoverview Fast indexer that uses pre-dumped descriptions instead of LLM generation.
3
+ * Indexes only files unchanged between the current branch and main.
4
+ * Run on the main branch after copying chunk-descriptions.json from t1-cosine-l2.
5
+ *
6
+ * Usage: node --import tsx src/eval/fast-index.ts --descriptions .opencode/rag_db/chunk-descriptions.json
7
+ */
8
+ import { readFileSync, readdirSync } from "node:fs";
9
+ import fs from "node:fs/promises";
10
+ import path from "node:path";
11
+ import { loadConfig, DEFAULT_CONFIG } from "../core/config.js";
12
+ import { createEmbedder } from "../embedder/factory.js";
13
+ import { createVectorStore } from "../vectorstore/factory.js";
14
+ import { loadRuntimeOverrides, applyRuntimeOverrides, } from "../core/runtime-overrides.js";
15
+ import { resolveApiKey } from "../core/resolve-api-key.js";
16
+ import { chunkFile } from "../chunker/factory.js";
17
+ import { embedBatch } from "../embedder/factory.js";
18
+ import { normalizeFilePath } from "../core/manifest.js";
19
+ import { KeywordIndex } from "../retriever/keyword-index.js";
20
+ const WORKTREE = process.cwd();
21
+ const STORE_PATH = path.join(WORKTREE, ".opencode", "rag_db");
22
+ /** Directories to skip entirely when scanning for files. */
23
+ const SKIP_DIRS = new Set([
24
+ "node_modules",
25
+ ".git",
26
+ ".opencode",
27
+ "dist",
28
+ "build",
29
+ "__pycache__",
30
+ ".venv",
31
+ ".claude",
32
+ ".github",
33
+ "memory:",
34
+ "wasm",
35
+ ".commandcode",
36
+ ".agents",
37
+ "graphify-out",
38
+ "__tests__",
39
+ ]);
40
+ /** File extensions to include. */
41
+ const INCLUDE_EXTS = new Set([".ts", ".tsx"]);
42
+ /** Changed files between main and t1-cosine-l2 (relative paths, / separators). */
43
+ const CHANGED_FILES = new Set([
44
+ "src/api.ts",
45
+ "src/chunker/pdf.ts",
46
+ "src/cli/commands/init-helpers.ts",
47
+ "src/core/config.ts",
48
+ "src/core/interfaces.ts",
49
+ "src/core/manifest.ts",
50
+ "src/core/setup-runtime.ts",
51
+ "src/indexer/embed-stage.ts",
52
+ "src/retriever/keyword-index.ts",
53
+ "src/retriever/retriever.ts",
54
+ "src/vectorstore/lancedb.ts",
55
+ "src/vectorstore/memory.ts",
56
+ ]);
57
+ /** Our own eval scripts (not part of the index). */
58
+ const EVAL_FILES = new Set([
59
+ "src/eval/run-branch-compare.ts",
60
+ "src/eval/compare-merge.ts",
61
+ "src/eval/dump-descriptions.ts",
62
+ "src/eval/update-descriptions.ts",
63
+ "src/eval/fast-index.ts",
64
+ ]);
65
+ function isChangedFile(relPath) {
66
+ return CHANGED_FILES.has(relPath) || EVAL_FILES.has(relPath);
67
+ }
68
+ function walkFiles(dir) {
69
+ const results = [];
70
+ try {
71
+ const entries = readdirSync(dir, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ const fullPath = path.join(dir, entry.name);
74
+ if (entry.isDirectory()) {
75
+ if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) {
76
+ results.push(...walkFiles(fullPath));
77
+ }
78
+ }
79
+ else if (entry.isFile()) {
80
+ const ext = path.extname(entry.name).toLowerCase();
81
+ if (INCLUDE_EXTS.has(ext)) {
82
+ results.push(fullPath);
83
+ }
84
+ }
85
+ }
86
+ }
87
+ catch {
88
+ // directory not found or no permissions
89
+ }
90
+ return results;
91
+ }
92
+ function parseArgs() {
93
+ const args = process.argv.slice(2);
94
+ let descriptionsPath = path.join(WORKTREE, ".opencode", "rag_db", "chunk-descriptions.json");
95
+ let force = false;
96
+ for (let i = 0; i < args.length; i++) {
97
+ if (args[i] === "--descriptions" && args[i + 1]) {
98
+ descriptionsPath = path.resolve(WORKTREE, args[i + 1]);
99
+ i++;
100
+ }
101
+ else if (args[i] === "--force") {
102
+ force = true;
103
+ }
104
+ }
105
+ return { descriptionsPath, force };
106
+ }
107
+ function getConfig() {
108
+ const configPath = path.join(WORKTREE, "opencode-rag.json");
109
+ let cfg;
110
+ try {
111
+ cfg = loadConfig(configPath);
112
+ }
113
+ catch {
114
+ cfg = DEFAULT_CONFIG;
115
+ }
116
+ const overrides = loadRuntimeOverrides(STORE_PATH);
117
+ cfg = applyRuntimeOverrides(cfg, overrides);
118
+ resolveApiKey(cfg, WORKTREE);
119
+ return cfg;
120
+ }
121
+ async function main() {
122
+ const { descriptionsPath, force } = parseArgs();
123
+ console.log(`\n Fast Indexer — skipping description generation`);
124
+ console.log(` Descriptions: ${descriptionsPath}\n`);
125
+ // Helper: split "absPath:startLine:endLine" key (handles Windows drive letters)
126
+ function splitLocationKey(k) {
127
+ const lastColon = k.lastIndexOf(":");
128
+ const secondLastColon = k.lastIndexOf(":", lastColon - 1);
129
+ return {
130
+ absPath: k.substring(0, secondLastColon),
131
+ startLine: parseInt(k.substring(secondLastColon + 1, lastColon), 10),
132
+ endLine: parseInt(k.substring(lastColon + 1), 10),
133
+ };
134
+ }
135
+ // Load pre-dumped descriptions and remap paths to this worktree
136
+ const rawDescs = JSON.parse(readFileSync(descriptionsPath, "utf-8"));
137
+ // Detect source worktree from first key
138
+ const sampleKey = Object.keys(rawDescs)[0] ?? "";
139
+ const sampleParts = splitLocationKey(sampleKey);
140
+ const samplePath = sampleParts.absPath;
141
+ const srcIdx = samplePath.lastIndexOf("/src/");
142
+ const docIdx = samplePath.lastIndexOf("/doc/");
143
+ const cutIdx = Math.max(srcIdx, docIdx);
144
+ const sourceWorktree = cutIdx > 0 ? samplePath.substring(0, cutIdx) : "";
145
+ const thisWorktree = WORKTREE.replace(/\\/g, "/");
146
+ const descriptions = {};
147
+ for (const [key, desc] of Object.entries(rawDescs)) {
148
+ if (!sourceWorktree) {
149
+ descriptions[key] = desc;
150
+ continue;
151
+ }
152
+ const { absPath, startLine, endLine } = splitLocationKey(key);
153
+ if (!absPath.startsWith(sourceWorktree + "/")) {
154
+ descriptions[key] = desc;
155
+ continue;
156
+ }
157
+ const relativePath = absPath.substring(sourceWorktree.length);
158
+ const newKey = `${thisWorktree}${relativePath}:${startLine}:${endLine}`;
159
+ descriptions[newKey] = desc;
160
+ }
161
+ console.log(` Loaded ${Object.keys(rawDescs).length} descriptions from dump`);
162
+ console.log(` Source worktree: ${sourceWorktree}`);
163
+ console.log(` This worktree: ${thisWorktree}`);
164
+ // Set up config, embedder, store
165
+ const cfg = getConfig();
166
+ const embedder = createEmbedder(cfg);
167
+ // Probe dimension
168
+ const probe = await embedder.embed(["dimension-probe"], "query");
169
+ const dimension = probe[0]?.length ?? 384;
170
+ console.log(` Embedding dimension: ${dimension}\n`);
171
+ // Clear existing store (requires --force if data exists)
172
+ const store = createVectorStore(cfg, STORE_PATH, dimension);
173
+ const existingCount = await store.count();
174
+ if (existingCount > 0 && !force) {
175
+ console.error(` Store already has ${existingCount} chunks. Use --force to overwrite.`);
176
+ await store.close();
177
+ process.exit(1);
178
+ }
179
+ if (existingCount > 0) {
180
+ await store.clear();
181
+ console.log(` Cleared existing store (backed up ${existingCount} chunks)\n`);
182
+ }
183
+ else {
184
+ console.log(" Store is empty\n");
185
+ }
186
+ // Walk files
187
+ const srcDir = path.join(WORKTREE, "src");
188
+ const allFiles = walkFiles(srcDir);
189
+ const filteredFiles = allFiles
190
+ .map((f) => path.relative(WORKTREE, f).replace(/\\/g, "/"))
191
+ .filter((f) => !isChangedFile(f));
192
+ console.log(` Found ${allFiles.length} .ts files in src/`);
193
+ console.log(` After filtering out changed files: ${filteredFiles.length}\n`);
194
+ // Process files sequentially (chunking + description lookup)
195
+ const allChunks = [];
196
+ let skippedNoDesc = 0;
197
+ for (let i = 0; i < filteredFiles.length; i++) {
198
+ const relPath = filteredFiles[i];
199
+ const absPath = path.join(WORKTREE, relPath);
200
+ process.stdout.write(` [${i + 1}/${filteredFiles.length}] ${relPath}...`);
201
+ try {
202
+ const content = await fs.readFile(absPath, "utf-8");
203
+ if (!content.trim()) {
204
+ console.log(" empty");
205
+ continue;
206
+ }
207
+ const chunks = await chunkFile(absPath, content);
208
+ if (!chunks || chunks.length === 0) {
209
+ console.log(" no chunks");
210
+ continue;
211
+ }
212
+ // Attach descriptions from dump
213
+ const normalizedPath = normalizeFilePath(absPath);
214
+ let attached = 0;
215
+ for (const chunk of chunks) {
216
+ const key = `${normalizedPath}:${chunk.metadata.startLine}:${chunk.metadata.endLine}`;
217
+ const desc = descriptions[key];
218
+ if (desc) {
219
+ chunk.description = desc;
220
+ attached++;
221
+ }
222
+ else {
223
+ // Try alternate key ending (some chunkers add +1 to endLine)
224
+ const altKey = `${normalizedPath}:${chunk.metadata.startLine}:${chunk.metadata.startLine + (chunk.metadata.endLine - chunk.metadata.startLine) + 1}`;
225
+ const altDesc = descriptions[altKey];
226
+ if (altDesc) {
227
+ chunk.description = altDesc;
228
+ attached++;
229
+ }
230
+ }
231
+ }
232
+ if (attached === 0) {
233
+ skippedNoDesc++;
234
+ }
235
+ allChunks.push(...chunks);
236
+ console.log(` ${chunks.length} chunks, ${attached} descriptions`);
237
+ }
238
+ catch (err) {
239
+ console.log(` error: ${err.message}`);
240
+ }
241
+ }
242
+ console.log(`\n Total chunks to embed: ${allChunks.length}`);
243
+ console.log(` Files with no description match: ${skippedNoDesc}`);
244
+ // Build texts to embed and batch-embed
245
+ const textsToEmbed = [];
246
+ for (const chunk of allChunks) {
247
+ const desc = chunk.description ?? "";
248
+ if (desc.trim()) {
249
+ textsToEmbed.push(`${path.relative(WORKTREE, chunk.metadata.filePath).replace(/\\/g, "/")}\n\n${desc}\n\n${chunk.content}`);
250
+ }
251
+ else {
252
+ textsToEmbed.push(`${path.relative(WORKTREE, chunk.metadata.filePath).replace(/\\/g, "/")}\n\n${chunk.content}`);
253
+ }
254
+ }
255
+ console.log(` Embedding ${textsToEmbed.length} texts...`);
256
+ const embeddings = await embedBatch(embedder, textsToEmbed, cfg.indexing.embedBatchSize ?? 100, "document", cfg.indexing.embedConcurrency ?? 3);
257
+ console.log(` Got ${embeddings.length} embeddings`);
258
+ // Attach embeddings to chunks and store
259
+ for (let i = 0; i < allChunks.length; i++) {
260
+ const emb = embeddings[i];
261
+ if (emb && emb.length > 0 && typeof emb[0] === "number") {
262
+ allChunks[i].embedding = emb;
263
+ }
264
+ }
265
+ const validChunks = allChunks.filter((c) => c.embedding && c.embedding.length > 0);
266
+ console.log(` Storing ${validChunks.length} chunks...`);
267
+ await store.addChunks(validChunks);
268
+ const count = await store.count();
269
+ console.log(` Store now has ${count} chunks`);
270
+ await store.close();
271
+ // Build keyword index from the same chunks
272
+ console.log(" Building keyword index...");
273
+ const ki = new KeywordIndex();
274
+ ki.addChunks(validChunks);
275
+ await ki.save(STORE_PATH);
276
+ console.log(` Keyword index saved (${ki.count()} entries)`);
277
+ console.log("\n Fast indexing complete!\n");
278
+ }
279
+ main().catch((err) => {
280
+ console.error("Fast index failed:", err);
281
+ process.exit(1);
282
+ });
283
+ //# sourceMappingURL=fast-index.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Branch comparison benchmark — runs N queries against the indexed codebase
3
+ * and outputs per-query results as JSON for later comparison.
4
+ *
5
+ * Usage: node --import tsx src/eval/run-branch-compare.ts --output .opencode/rag_db/eval-results/branch.json
6
+ */
7
+ export {};