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,537 @@
1
+ /**
2
+ * @fileoverview Compare two branch benchmark JSON outputs and produce a side-by-side
3
+ * analysis report (console table + markdown file).
4
+ *
5
+ * Usage: node --import tsx src/eval/compare-merge.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/branch-compare-report.md
9
+ */
10
+ import { readFileSync, writeFileSync } from "node:fs";
11
+ import path from "node:path";
12
+ function parseArgs() {
13
+ const args = process.argv.slice(2);
14
+ let main = "";
15
+ let branch = "";
16
+ let output = ".opencode/rag_db/eval-results/branch-compare-report.md";
17
+ for (let i = 0; i < args.length; i++) {
18
+ if (args[i] === "--main" && args[i + 1]) {
19
+ main = path.resolve(args[i + 1]);
20
+ i++;
21
+ }
22
+ else if (args[i] === "--branch" && args[i + 1]) {
23
+ branch = path.resolve(args[i + 1]);
24
+ i++;
25
+ }
26
+ else if (args[i] === "--output" && args[i + 1]) {
27
+ output = path.resolve(args[i + 1]);
28
+ i++;
29
+ }
30
+ }
31
+ if (!main || !branch) {
32
+ console.error("Usage: node --import tsx src/eval/compare-merge.ts --main <json> --branch <json> [--output <md>]");
33
+ process.exit(1);
34
+ }
35
+ return { main, branch, output };
36
+ }
37
+ // ---- Statistics helpers ----
38
+ function avg(arr) {
39
+ if (arr.length === 0)
40
+ return 0;
41
+ return arr.reduce((s, v) => s + v, 0) / arr.length;
42
+ }
43
+ function median(arr) {
44
+ if (arr.length === 0)
45
+ return 0;
46
+ const sorted = [...arr].sort((a, b) => a - b);
47
+ const mid = Math.floor(sorted.length / 2);
48
+ return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
49
+ }
50
+ function percentile(arr, p) {
51
+ if (arr.length === 0)
52
+ return 0;
53
+ const sorted = [...arr].sort((a, b) => a - b);
54
+ const idx = Math.ceil((p / 100) * sorted.length) - 1;
55
+ return sorted[Math.max(0, Math.min(idx, sorted.length - 1))];
56
+ }
57
+ function stdev(arr) {
58
+ if (arr.length < 2)
59
+ return 0;
60
+ const m = avg(arr);
61
+ const sqDiffs = arr.map((v) => (v - m) ** 2);
62
+ return Math.sqrt(sqDiffs.reduce((s, v) => s + v, 0) / (arr.length - 1));
63
+ }
64
+ function jaccardSimilarity(a, b) {
65
+ const setA = new Set(a);
66
+ const setB = new Set(b);
67
+ const intersection = new Set([...setA].filter((x) => setB.has(x)));
68
+ const union = new Set([...setA, ...setB]);
69
+ return union.size === 0 ? 1 : intersection.size / union.size;
70
+ }
71
+ // ---- Formatting helpers ----
72
+ function fmt(n, decimals = 3) {
73
+ return n.toFixed(decimals);
74
+ }
75
+ function deltaStr(a, b, decimals = 3) {
76
+ const diff = b - a;
77
+ const sign = diff >= 0 ? "+" : "";
78
+ return `${sign}${fmt(diff, decimals)}`;
79
+ }
80
+ function deltaStrPct(a, b) {
81
+ if (a === 0)
82
+ return b === 0 ? "±0" : "∞";
83
+ const pct = ((b - a) / Math.abs(a)) * 100;
84
+ const sign = pct >= 0 ? "+" : "";
85
+ return `${sign}${pct.toFixed(1)}%`;
86
+ }
87
+ // ---- Table rendering ----
88
+ const SEP = "─";
89
+ function cell(s, width, align = "left") {
90
+ if (s.length >= width)
91
+ return s.slice(0, width);
92
+ return align === "left" ? s + " ".repeat(width - s.length) : " ".repeat(width - s.length) + s;
93
+ }
94
+ function tableRow(cols, widths, aligns) {
95
+ return "│ " + cols.map((c, i) => cell(c, widths[i], aligns[i])).join(" │ ") + " │";
96
+ }
97
+ function tableSep(widths) {
98
+ return "├─" + widths.map((w) => SEP.repeat(w)).join("─┼─") + "─┤";
99
+ }
100
+ function tableTop(widths) {
101
+ return "┌─" + widths.map((w) => SEP.repeat(w)).join("─┬─") + "─┐";
102
+ }
103
+ function tableBot(widths) {
104
+ return "└─" + widths.map((w) => SEP.repeat(w)).join("─┴─") + "─┘";
105
+ }
106
+ function compare(main, branch) {
107
+ const perQuery = [];
108
+ const topScoreMain = [];
109
+ const topScoreBranch = [];
110
+ const countMain = [];
111
+ const countBranch = [];
112
+ const latencyMain = [];
113
+ const latencyBranch = [];
114
+ const jaccards = [];
115
+ const maxQueries = Math.max(main.queries.length, branch.queries.length);
116
+ for (let i = 0; i < maxQueries; i++) {
117
+ const mq = main.queries[i];
118
+ const bq = branch.queries[i];
119
+ const query = (mq ?? bq)?.query ?? `[query ${i}]`;
120
+ const mCount = mq?.resultCount ?? 0;
121
+ const bCount = bq?.resultCount ?? 0;
122
+ const mTop = mq?.topResults[0]?.score ?? 0;
123
+ const bTop = bq?.topResults[0]?.score ?? 0;
124
+ const mLat = mq?.latencyMs ?? 0;
125
+ const bLat = bq?.latencyMs ?? 0;
126
+ const mTop3 = (mq?.topResults ?? []).slice(0, 3).map((r) => r.filePath);
127
+ const bTop3 = (bq?.topResults ?? []).slice(0, 3).map((r) => r.filePath);
128
+ topScoreMain.push(mTop);
129
+ topScoreBranch.push(bTop);
130
+ countMain.push(mCount);
131
+ countBranch.push(bCount);
132
+ latencyMain.push(mLat);
133
+ latencyBranch.push(bLat);
134
+ if (mTop3.length > 0 || bTop3.length > 0) {
135
+ jaccards.push(jaccardSimilarity(mTop3, bTop3));
136
+ }
137
+ perQuery.push({
138
+ query,
139
+ index: i,
140
+ main: { count: mCount, topScore: mTop, latencyMs: mLat, top3Files: mTop3 },
141
+ branch: { count: bCount, topScore: bTop, latencyMs: bLat, top3Files: bTop3 },
142
+ });
143
+ }
144
+ // Threshold analysis
145
+ const thresholds = [0.85, 0.75, 0.65, 0.50, 0.35];
146
+ const thresholdAnalysis = thresholds.map((t) => {
147
+ const mainInject = main.queries.filter((q) => q.thresholdAnalysis.find((ta) => ta.threshold === t)?.wouldInject).length;
148
+ const branchInject = branch.queries.filter((q) => q.thresholdAnalysis.find((ta) => ta.threshold === t)?.wouldInject).length;
149
+ return { threshold: t, mainInject, branchInject };
150
+ });
151
+ return {
152
+ main,
153
+ branch,
154
+ perQuery,
155
+ stats: {
156
+ topScore: { main: topScoreMain, branch: topScoreBranch },
157
+ resultCount: { main: countMain, branch: countBranch },
158
+ latency: { main: latencyMain, branch: latencyBranch },
159
+ jaccardTop3: jaccards,
160
+ },
161
+ thresholdAnalysis,
162
+ };
163
+ }
164
+ function printConsole(result) {
165
+ const { main, branch, stats, perQuery, thresholdAnalysis } = result;
166
+ // ── Header ──
167
+ console.log("\n" + SEP.repeat(80));
168
+ console.log(` BRANCH COMPARISON: ${main.branch} (${main.commit}) vs ${branch.branch} (${branch.commit})`);
169
+ console.log(SEP.repeat(80) + "\n");
170
+ // ── Config table ──
171
+ const cfgCols = ["", "main", branch.branch];
172
+ const cfgW = [18, 24, 24];
173
+ const cfgA = ["left", "right", "right"];
174
+ console.log(" Config:");
175
+ console.log(" " + tableTop(cfgW));
176
+ console.log(" " + tableRow(cfgCols, cfgW, cfgA));
177
+ console.log(" " + tableSep(cfgW));
178
+ console.log(" " + tableRow(["minScore", fmt(main.config.minScore, 2), fmt(branch.config.minScore, 2)], cfgW, cfgA));
179
+ console.log(" " + tableRow(["keywordWeight", fmt(main.config.keywordWeight, 2), fmt(branch.config.keywordWeight, 2)], cfgW, cfgA));
180
+ console.log(" " + tableRow(["topK", String(main.config.topK), String(branch.config.topK)], cfgW, cfgA));
181
+ console.log(" " + tableRow(["hybrid", String(main.config.hybridEnabled), String(branch.config.hybridEnabled)], cfgW, cfgA));
182
+ console.log(" " + tableRow(["embedding", main.config.embeddingModel, branch.config.embeddingModel], cfgW, cfgA));
183
+ console.log(" " + tableRow(["index chunks", String(main.indexChunkCount), String(branch.indexChunkCount)], cfgW, cfgA));
184
+ console.log(" " + tableBot(cfgW));
185
+ console.log();
186
+ // ── Score quality ──
187
+ const scoreCols = ["", "avg", "median", "p95", "p5", "stdev"];
188
+ const scoreW = [12, 10, 10, 10, 10, 10];
189
+ const scoreA = ["left", "right", "right", "right", "right", "right"];
190
+ console.log(" Score Quality (top-1 per query):");
191
+ console.log(" " + tableTop(scoreW));
192
+ console.log(" " + tableRow(scoreCols, scoreW, scoreA));
193
+ console.log(" " + tableSep(scoreW));
194
+ const mScores = stats.topScore.main;
195
+ const bScores = stats.topScore.branch;
196
+ console.log(" " + tableRow([main.branch, fmt(avg(mScores)), fmt(median(mScores)), fmt(percentile(mScores, 95)), fmt(percentile(mScores, 5)), fmt(stdev(mScores))], scoreW, scoreA));
197
+ console.log(" " + tableRow([branch.branch, fmt(avg(bScores)), fmt(median(bScores)), fmt(percentile(bScores, 95)), fmt(percentile(bScores, 5)), fmt(stdev(bScores))], scoreW, scoreA));
198
+ console.log(" " + tableRow(["Δ", deltaStr(avg(mScores), avg(bScores)), deltaStr(median(mScores), median(bScores)), deltaStr(percentile(mScores, 95), percentile(bScores, 95)), deltaStr(percentile(mScores, 5), percentile(bScores, 5)), ""], scoreW, scoreA));
199
+ console.log(" " + tableBot(scoreW));
200
+ console.log();
201
+ // ── Result count ──
202
+ console.log(" Result Count (per query):");
203
+ console.log(" " + tableTop(scoreW));
204
+ console.log(" " + tableRow(scoreCols, scoreW, scoreA));
205
+ console.log(" " + tableSep(scoreW));
206
+ const mCount = stats.resultCount.main;
207
+ const bCount = stats.resultCount.branch;
208
+ console.log(" " + tableRow([main.branch, fmt(avg(mCount), 1), fmt(median(mCount), 1), fmt(percentile(mCount, 95), 1), fmt(percentile(mCount, 5), 1), fmt(stdev(mCount), 1)], scoreW, scoreA));
209
+ console.log(" " + tableRow([branch.branch, fmt(avg(bCount), 1), fmt(median(bCount), 1), fmt(percentile(bCount, 95), 1), fmt(percentile(bCount, 5), 1), fmt(stdev(bCount), 1)], scoreW, scoreA));
210
+ console.log(" " + tableRow(["Δ", deltaStr(avg(mCount), avg(bCount), 1), deltaStr(median(mCount), median(bCount), 1), "", "", ""], scoreW, scoreA));
211
+ console.log(" " + tableBot(scoreW));
212
+ console.log();
213
+ // ── Latency ──
214
+ console.log(" Latency (ms per query):");
215
+ console.log(" " + tableTop(scoreW));
216
+ console.log(" " + tableRow(scoreCols, scoreW, scoreA));
217
+ console.log(" " + tableSep(scoreW));
218
+ const mLat = stats.latency.main;
219
+ const bLat = stats.latency.branch;
220
+ console.log(" " + tableRow([main.branch, fmt(avg(mLat), 1), fmt(median(mLat), 1), fmt(percentile(mLat, 95), 1), fmt(percentile(mLat, 5), 1), fmt(stdev(mLat), 1)], scoreW, scoreA));
221
+ console.log(" " + tableRow([branch.branch, fmt(avg(bLat), 1), fmt(median(bLat), 1), fmt(percentile(bLat, 95), 1), fmt(percentile(bLat, 5), 1), fmt(stdev(bLat), 1)], scoreW, scoreA));
222
+ console.log(" " + tableRow(["Δ", deltaStr(avg(mLat), avg(bLat), 1), deltaStr(median(mLat), median(bLat), 1), "", "", ""], scoreW, scoreA));
223
+ console.log(" " + tableBot(scoreW));
224
+ console.log();
225
+ // ── Threshold coverage ──
226
+ const thCols = ["threshold", main.branch, branch.branch, "Δ"];
227
+ const thW = [12, 12, 16, 10];
228
+ const thA = ["left", "right", "right", "right"];
229
+ console.log(" Threshold Coverage (queries that would inject):");
230
+ console.log(" " + tableTop(thW));
231
+ console.log(" " + tableRow(thCols, thW, thA));
232
+ console.log(" " + tableSep(thW));
233
+ for (const ta of thresholdAnalysis) {
234
+ const diff = ta.branchInject - ta.mainInject;
235
+ const d = diff >= 0 ? `+${diff}` : `${diff}`;
236
+ console.log(" " + tableRow([fmt(ta.threshold, 2), `${ta.mainInject}/${perQuery.length}`, `${ta.branchInject}/${perQuery.length}`, d], thW, thA));
237
+ }
238
+ console.log(" " + tableBot(thW));
239
+ console.log();
240
+ // ── Rank stability ──
241
+ const jaccard = stats.jaccardTop3;
242
+ console.log(` Rank Stability (Jaccard top-3 files per query):`);
243
+ console.log(` avg: ${fmt(avg(jaccard), 3)}`);
244
+ console.log(` median: ${fmt(median(jaccard), 3)}`);
245
+ console.log(` min: ${fmt(Math.min(...jaccard), 3)}`);
246
+ console.log(` max: ${fmt(Math.max(...jaccard), 3)}`);
247
+ console.log();
248
+ // ── Per-query table ──
249
+ const pqCols = ["#", "Query (truncated)", `${main.branch} score`, `${branch.branch} score`, "Δ", `${main.branch} files`, `${branch.branch} files`];
250
+ const pqW = [3, 42, 14, 16, 10, 28, 28];
251
+ const pqA = ["right", "left", "right", "right", "right", "left", "left"];
252
+ console.log(" Per-Query Summary:");
253
+ console.log(" " + tableTop(pqW));
254
+ console.log(" " + tableRow(pqCols, pqW, pqA));
255
+ console.log(" " + tableSep(pqW));
256
+ for (const pq of perQuery) {
257
+ const q = pq.query.length > 39 ? pq.query.substring(0, 36) + "..." : pq.query;
258
+ const mFiles = pq.main.top3Files.map((f) => f.split("/").pop() ?? f).join(", ");
259
+ const bFiles = pq.branch.top3Files.map((f) => f.split("/").pop() ?? f).join(", ");
260
+ console.log(" " + tableRow([
261
+ String(pq.index + 1),
262
+ q,
263
+ fmt(pq.main.topScore),
264
+ fmt(pq.branch.topScore),
265
+ deltaStr(pq.main.topScore, pq.branch.topScore),
266
+ mFiles.length > 27 ? mFiles.substring(0, 24) + "..." : mFiles,
267
+ bFiles.length > 27 ? bFiles.substring(0, 24) + "..." : bFiles,
268
+ ], pqW, pqA));
269
+ }
270
+ console.log(" " + tableBot(pqW));
271
+ console.log();
272
+ }
273
+ function generateReport(result) {
274
+ const { main, branch, stats, perQuery, thresholdAnalysis } = result;
275
+ const lines = [];
276
+ lines.push("# Branch Comparison Report");
277
+ lines.push("");
278
+ lines.push(`**Generated:** ${new Date().toISOString()}`);
279
+ lines.push(`**Main branch:** \`${main.branch}\` @ \`${main.commit}\` (${main.timestamp.split("T")[0]})`);
280
+ lines.push(`**Feature branch:** \`${branch.branch}\` @ \`${branch.commit}\` (${branch.timestamp.split("T")[0]})`);
281
+ lines.push("");
282
+ // ── Configuration ──
283
+ lines.push("## Configuration");
284
+ lines.push("");
285
+ lines.push("| Setting | `" + main.branch + "` | `" + branch.branch + "` |");
286
+ lines.push("|---|---|---|");
287
+ lines.push(`| Embedding provider | ${main.config.embeddingProvider} | ${branch.config.embeddingProvider} |`);
288
+ lines.push(`| Embedding model | ${main.config.embeddingModel} | ${branch.config.embeddingModel} |`);
289
+ lines.push(`| topK | ${main.config.topK} | ${branch.config.topK} |`);
290
+ lines.push(`| minScore | ${main.config.minScore} | ${branch.config.minScore} |`);
291
+ lines.push(`| Hybrid search | ${main.config.hybridEnabled} | ${branch.config.hybridEnabled} |`);
292
+ lines.push(`| Keyword weight | ${main.config.keywordWeight} | ${branch.config.keywordWeight} |`);
293
+ lines.push(`| Indexed chunks | ${main.indexChunkCount} | ${branch.indexChunkCount} |`);
294
+ lines.push("");
295
+ // ── Scoring method ──
296
+ lines.push("## Scoring Method Differences");
297
+ lines.push("");
298
+ lines.push("| Aspect | `" + main.branch + "` | `" + branch.branch + "` |");
299
+ lines.push("|---|---|---|");
300
+ lines.push("| Vector scoring | `1 / (1 + L2 distance)` | Cosine similarity via L2-normalized vectors |");
301
+ lines.push("| Hybrid fusion | Weighted linear: `(1-kw)*normV + kw*normK` | Reciprocal Rank Fusion (RRF, K=60) |");
302
+ lines.push("| Default minScore | " + main.config.minScore + " | " + branch.config.minScore + " |");
303
+ lines.push("| Metadata filter | Not supported | `MetadataFilter` support added |");
304
+ lines.push("");
305
+ // ── Score quality ──
306
+ const mScores = stats.topScore.main;
307
+ const bScores = stats.topScore.branch;
308
+ lines.push("## Top-1 Score Quality");
309
+ lines.push("");
310
+ lines.push("| Metric | `" + main.branch + "` | `" + branch.branch + "` | Δ | Δ% |");
311
+ lines.push("|---|---|---|---|---|");
312
+ lines.push(`| Average | ${fmt(avg(mScores))} | ${fmt(avg(bScores))} | ${deltaStr(avg(mScores), avg(bScores))} | ${deltaStrPct(avg(mScores), avg(bScores))} |`);
313
+ lines.push(`| Median | ${fmt(median(mScores))} | ${fmt(median(bScores))} | ${deltaStr(median(mScores), median(bScores))} | ${deltaStrPct(median(mScores), median(bScores))} |`);
314
+ lines.push(`| P95 | ${fmt(percentile(mScores, 95))} | ${fmt(percentile(bScores, 95))} | ${deltaStr(percentile(mScores, 95), percentile(bScores, 95))} | ${deltaStrPct(percentile(mScores, 95), percentile(bScores, 95))} |`);
315
+ lines.push(`| P5 | ${fmt(percentile(mScores, 5))} | ${fmt(percentile(bScores, 5))} | ${deltaStr(percentile(mScores, 5), percentile(bScores, 5))} | ${deltaStrPct(percentile(mScores, 5), percentile(bScores, 5))} |`);
316
+ lines.push(`| Std Dev | ${fmt(stdev(mScores))} | ${fmt(stdev(bScores))} | ${deltaStr(stdev(mScores), stdev(bScores))} | — |`);
317
+ lines.push("");
318
+ lines.push("> **Interpretation:** Cosine similarity produces scores in a tighter 0-1 range.");
319
+ lines.push("> RRF further shifts scores based on rank rather than raw similarity, so comparing");
320
+ lines.push("> absolute scores across branches is misleading. The key metric is **whether the same");
321
+ lines.push("> relevant files appear in the top results** (see Rank Stability below).");
322
+ lines.push("");
323
+ // ── Result count ──
324
+ const mCount = stats.resultCount.main;
325
+ const bCount = stats.resultCount.branch;
326
+ lines.push("## Result Count (per query)");
327
+ lines.push("");
328
+ lines.push("| Metric | `" + main.branch + "` | `" + branch.branch + "` | Δ |");
329
+ lines.push("|---|---|---|---|");
330
+ lines.push(`| Average | ${fmt(avg(mCount), 1)} | ${fmt(avg(bCount), 1)} | ${deltaStr(avg(mCount), avg(bCount), 1)} |`);
331
+ lines.push(`| Median | ${fmt(median(mCount), 1)} | ${fmt(median(bCount), 1)} | ${deltaStr(median(mCount), median(bCount), 1)} |`);
332
+ lines.push(`| P95 | ${fmt(percentile(mCount, 95), 1)} | ${fmt(percentile(bCount, 95), 1)} | — |`);
333
+ lines.push(`| Zero-result queries | ${mCount.filter((c) => c === 0).length} | ${bCount.filter((c) => c === 0).length} | — |`);
334
+ lines.push("");
335
+ // ── Latency ──
336
+ const mLat = stats.latency.main;
337
+ const bLat = stats.latency.branch;
338
+ lines.push("## Latency (ms per query)");
339
+ lines.push("");
340
+ lines.push("| Metric | `" + main.branch + "` | `" + branch.branch + "` | Δ |");
341
+ lines.push("|---|---|---|---|");
342
+ lines.push(`| Average | ${fmt(avg(mLat), 1)} | ${fmt(avg(bLat), 1)} | ${deltaStr(avg(mLat), avg(bLat), 1)} |`);
343
+ lines.push(`| Median | ${fmt(median(mLat), 1)} | ${fmt(median(bLat), 1)} | ${deltaStr(median(mLat), median(bLat), 1)} |`);
344
+ lines.push(`| P95 | ${fmt(percentile(mLat, 95), 1)} | ${fmt(percentile(bLat, 95), 1)} | — |`);
345
+ lines.push("");
346
+ // ── Threshold coverage ──
347
+ lines.push("## Threshold Coverage");
348
+ lines.push("");
349
+ lines.push("Shows how many queries would trigger RAG context injection at each `minScore` threshold.");
350
+ lines.push("");
351
+ lines.push("| Threshold | `" + main.branch + "` | `" + branch.branch + "` | Δ |");
352
+ lines.push("|---|---|---|---|");
353
+ for (const ta of thresholdAnalysis) {
354
+ const diff = ta.branchInject - ta.mainInject;
355
+ const d = diff >= 0 ? `+${diff}` : `${diff}`;
356
+ lines.push(`| ${fmt(ta.threshold, 2)} | ${ta.mainInject}/${perQuery.length} | ${ta.branchInject}/${perQuery.length} | ${d} |`);
357
+ }
358
+ lines.push("");
359
+ // ── Rank stability ──
360
+ const jaccard = stats.jaccardTop3;
361
+ lines.push("## Rank Stability (Jaccard Similarity)");
362
+ lines.push("");
363
+ lines.push("For each query, computes the Jaccard similarity of the top-3 file paths between branches.");
364
+ lines.push("1.0 = identical top-3 files, 0.0 = completely different.");
365
+ lines.push("");
366
+ lines.push("| Metric | Value |");
367
+ lines.push("|---|---|");
368
+ lines.push(`| Average | ${fmt(avg(jaccard), 3)} |`);
369
+ lines.push(`| Median | ${fmt(median(jaccard), 3)} |`);
370
+ lines.push(`| Minimum | ${fmt(Math.min(...jaccard), 3)} |`);
371
+ lines.push(`| Maximum | ${fmt(Math.max(...jaccard), 3)} |`);
372
+ lines.push("");
373
+ // ── Per-query detailed results ──
374
+ lines.push("## Per-Query Results");
375
+ lines.push("");
376
+ lines.push("| # | Query | `" + main.branch + "` results | `" + main.branch + "` top score | `" + branch.branch + "` results | `" + branch.branch + "` top score | Δ score | Jaccard (top-3) |");
377
+ lines.push("|---|---|---|---|---|---|---|---|");
378
+ for (const pq of perQuery) {
379
+ const q = pq.query.length > 50 ? pq.query.substring(0, 47) + "..." : pq.query;
380
+ const jac = jaccardSimilarity(pq.main.top3Files, pq.branch.top3Files);
381
+ lines.push(`| ${pq.index + 1} | ${q} | ${pq.main.count} | ${fmt(pq.main.topScore)} | ${pq.branch.count} | ${fmt(pq.branch.topScore)} | ${deltaStr(pq.main.topScore, pq.branch.topScore)} | ${fmt(jac, 3)} |`);
382
+ }
383
+ lines.push("");
384
+ // ── Raw top-5 outputs side by side ──
385
+ lines.push("## Raw Top-5 Results by Query");
386
+ lines.push("");
387
+ lines.push("Each query shows the top-5 file paths and scores for both branches side by side.");
388
+ lines.push("");
389
+ for (const pq of perQuery) {
390
+ const mq = main.queries[pq.index];
391
+ const bq = branch.queries[pq.index];
392
+ if (!mq && !bq)
393
+ continue;
394
+ lines.push(`### Query ${pq.index + 1}: ${pq.query}`);
395
+ lines.push("");
396
+ if (mq && mq.topResults.length > 0) {
397
+ lines.push("**`" + main.branch + "`** ");
398
+ lines.push("| Rank | Score | File | Lines | Language |");
399
+ lines.push("|------|-------|------|-------|----------|");
400
+ for (const tr of mq.topResults) {
401
+ lines.push(`| ${tr.rank + 1} | ${fmt(tr.score)} | \`${tr.filePath}\` | ${tr.startLine}-${tr.endLine} | ${tr.language} |`);
402
+ }
403
+ }
404
+ else {
405
+ lines.push("**`" + main.branch + "`** — no results");
406
+ }
407
+ lines.push("");
408
+ if (bq && bq.topResults.length > 0) {
409
+ lines.push("**`" + branch.branch + "`** ");
410
+ lines.push("| Rank | Score | File | Lines | Language |");
411
+ lines.push("|------|-------|------|-------|----------|");
412
+ for (const tr of bq.topResults) {
413
+ lines.push(`| ${tr.rank + 1} | ${fmt(tr.score)} | \`${tr.filePath}\` | ${tr.startLine}-${tr.endLine} | ${tr.language} |`);
414
+ }
415
+ }
416
+ else {
417
+ lines.push("**`" + branch.branch + "`** — no results");
418
+ }
419
+ lines.push("");
420
+ }
421
+ // ── Explanation comparison (first 3 queries) ──
422
+ lines.push("## Explanation / Score Breakdown (Sample)");
423
+ lines.push("");
424
+ lines.push("First 3 queries with explanation details when available.");
425
+ lines.push("");
426
+ for (let i = 0; i < Math.min(3, perQuery.length); i++) {
427
+ const pq = perQuery[i];
428
+ const mq = main.queries[pq.index];
429
+ const bq = branch.queries[pq.index];
430
+ lines.push(`### Query ${pq.index + 1}: ${pq.query}`);
431
+ lines.push("");
432
+ const mExp = mq?.topResults[0]?.explanation;
433
+ const bExp = bq?.topResults[0]?.explanation;
434
+ if (mExp) {
435
+ lines.push("**`" + main.branch + "`** (top-1 explanation) ");
436
+ lines.push("| Component | Value |");
437
+ lines.push("|-----------|-------|");
438
+ lines.push(`| vectorScore | ${fmt(mExp.vectorScore)} |`);
439
+ lines.push(`| keywordScore | ${fmt(mExp.keywordScore)} |`);
440
+ lines.push(`| rawVectorScore | ${fmt(mExp.rawVectorScore)} |`);
441
+ lines.push(`| rawKeywordScore | ${fmt(mExp.rawKeywordScore)} |`);
442
+ lines.push(`| keywordWeight | ${fmt(mExp.keywordWeight)} |`);
443
+ if (mExp.vectorRank !== undefined)
444
+ lines.push(`| vectorRank | ${mExp.vectorRank} |`);
445
+ if (mExp.keywordRank !== undefined)
446
+ lines.push(`| keywordRank | ${mExp.keywordRank} |`);
447
+ if (mExp.matchedTerms?.length)
448
+ lines.push(`| matchedTerms | ${mExp.matchedTerms.join(", ")} |`);
449
+ lines.push("");
450
+ }
451
+ if (bExp) {
452
+ lines.push("**`" + branch.branch + "`** (top-1 explanation) ");
453
+ lines.push("| Component | Value |");
454
+ lines.push("|-----------|-------|");
455
+ lines.push(`| vectorScore | ${fmt(bExp.vectorScore)} |`);
456
+ lines.push(`| keywordScore | ${fmt(bExp.keywordScore)} |`);
457
+ lines.push(`| rawVectorScore | ${fmt(bExp.rawVectorScore)} |`);
458
+ lines.push(`| rawKeywordScore | ${fmt(bExp.rawKeywordScore)} |`);
459
+ lines.push(`| keywordWeight | ${fmt(bExp.keywordWeight)} |`);
460
+ if (bExp.vectorRank !== undefined)
461
+ lines.push(`| vectorRank | ${bExp.vectorRank} |`);
462
+ if (bExp.keywordRank !== undefined)
463
+ lines.push(`| keywordRank | ${bExp.keywordRank} |`);
464
+ if (bExp.matchedTerms?.length)
465
+ lines.push(`| matchedTerms | ${bExp.matchedTerms.join(", ")} |`);
466
+ lines.push("");
467
+ }
468
+ }
469
+ // ── Verdict ──
470
+ lines.push("## Verdict");
471
+ lines.push("");
472
+ const mAvg = avg(mScores);
473
+ const bAvg = avg(bScores);
474
+ const mji = thresholdAnalysis.find((t) => t.threshold === 0.75)?.mainInject ?? 0;
475
+ const bji = thresholdAnalysis.find((t) => t.threshold === 0.75)?.branchInject ?? 0;
476
+ const mZero = mCount.filter((c) => c === 0).length;
477
+ const bZero = bCount.filter((c) => c === 0).length;
478
+ const avgJac = avg(jaccard);
479
+ const improvements = [];
480
+ if (bAvg > mAvg)
481
+ improvements.push(`higher top-1 scores (avg ${fmt(bAvg)} vs ${fmt(mAvg)})`);
482
+ if (bji > mji)
483
+ improvements.push(`better threshold coverage (${bji}/${perQuery.length} vs ${mji}/${perQuery.length} at minScore 0.75)`);
484
+ if (bZero < mZero)
485
+ improvements.push(`fewer zero-result queries (${bZero} vs ${mZero})`);
486
+ if (avgJac > 0.5)
487
+ improvements.push("high rank stability with main (Jaccard " + fmt(avgJac, 3) + ")");
488
+ else
489
+ improvements.push("notable rank shift (Jaccard " + fmt(avgJac, 3) + ")");
490
+ lines.push("The `" + branch.branch + "` branch shows:");
491
+ lines.push("");
492
+ for (const imp of improvements) {
493
+ lines.push(`- **${imp}**`);
494
+ }
495
+ lines.push("");
496
+ lines.push("### Caveats");
497
+ lines.push("");
498
+ lines.push("- Cosine similarity + RRF produce fundamentally different score distributions than L2 + linear fusion.");
499
+ lines.push(" **Absolute scores are not directly comparable** between the two approaches.");
500
+ lines.push("- The key quality indicator is **whether relevant files rank highly**, not the raw score value.");
501
+ lines.push("- RRF de-emphasizes raw similarity magnitude and focuses on rank agreement between vector and keyword signals.");
502
+ lines.push("- This means an RRF score of 0.05 can be just as meaningful as an L2 score of 0.85 — they are different scales.");
503
+ lines.push("");
504
+ lines.push("### Recommendation");
505
+ lines.push("");
506
+ lines.push("Review the raw top-5 results per query above to confirm that the cosine+RRF approach");
507
+ lines.push("retrieves the same or better files. If rank stability is high (Jaccard > 0.5) and");
508
+ lines.push("threshold coverage improves, the new scoring is likely a net positive.");
509
+ lines.push("");
510
+ return lines.join("\n");
511
+ }
512
+ async function main() {
513
+ const { main: mainPath, branch: branchPath, output } = parseArgs();
514
+ const mainData = JSON.parse(readFileSync(mainPath, "utf-8"));
515
+ const branchData = JSON.parse(readFileSync(branchPath, "utf-8"));
516
+ console.log(` Main: ${mainPath}`);
517
+ console.log(` Branch: ${branchPath}`);
518
+ console.log(` Output: ${output}\n`);
519
+ const result = compare(mainData, branchData);
520
+ printConsole(result);
521
+ const report = generateReport(result);
522
+ const dir = path.dirname(output);
523
+ try {
524
+ const { mkdirSync } = await import("node:fs");
525
+ mkdirSync(dir, { recursive: true });
526
+ }
527
+ catch {
528
+ // dir exists
529
+ }
530
+ writeFileSync(output, report, "utf-8");
531
+ console.log(` Report written to: ${output}\n`);
532
+ }
533
+ main().catch((err) => {
534
+ console.error("Compare failed:", err);
535
+ process.exit(1);
536
+ });
537
+ //# sourceMappingURL=compare-merge.js.map
@@ -0,0 +1,10 @@
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
+ export {};