@hassangameryt/freeflow 0.4.0 → 0.6.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 (285) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.codex-plugin/plugin.json +2 -1
  4. package/README.md +150 -155
  5. package/capabilities/cognitive-routing/SKILL.md +229 -0
  6. package/capabilities/context-virtualization/SKILL.md +103 -0
  7. package/capabilities/conversation-history/SKILL.md +53 -0
  8. package/command-surface.json +17 -13
  9. package/hooks/freeflow-runtime-context.mjs +393 -159
  10. package/hooks/hooks.json +26 -1
  11. package/hooks/tests/check-runtime-context-hook.sh +223 -54
  12. package/package.json +25 -16
  13. package/pi-extension/dist/cognitive-routing/commands.js +96 -0
  14. package/pi-extension/dist/cognitive-routing/config.js +236 -0
  15. package/pi-extension/dist/cognitive-routing/controller.js +862 -0
  16. package/pi-extension/dist/cognitive-routing/history.js +439 -0
  17. package/pi-extension/dist/cognitive-routing/host.js +126 -0
  18. package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
  19. package/pi-extension/dist/cognitive-routing/tool.js +211 -0
  20. package/pi-extension/dist/cognitive-routing/types.js +6 -0
  21. package/pi-extension/dist/context-virtualization/commands.js +89 -0
  22. package/pi-extension/dist/context-virtualization/projector.js +42 -0
  23. package/pi-extension/dist/context-virtualization/runtime.js +356 -0
  24. package/pi-extension/dist/context-virtualization/state.js +57 -0
  25. package/pi-extension/dist/context-virtualization/types.js +10 -0
  26. package/pi-extension/dist/conversation-history/passages.js +66 -0
  27. package/pi-extension/dist/conversation-history/ranking.js +105 -0
  28. package/pi-extension/dist/conversation-history/runtime.js +226 -0
  29. package/pi-extension/dist/conversation-history/search.js +56 -0
  30. package/pi-extension/dist/conversation-history/types.js +1 -0
  31. package/pi-extension/dist/freeflow-context/resolver.js +120 -0
  32. package/pi-extension/dist/freeflow-context/runtime.js +80 -0
  33. package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
  34. package/pi-extension/dist/freeflow-context/tool.js +308 -0
  35. package/pi-extension/dist/freeflow-context/types.js +12 -0
  36. package/pi-extension/dist/freeflow-context/visibility.js +28 -0
  37. package/pi-extension/dist/index.js +516 -98
  38. package/pi-extension/dist/runtime/runtime-context.js +318 -271
  39. package/pi-extension/dist/runtime/runtime-identity.js +5 -0
  40. package/pi-extension/dist/settings/settings-tui.js +78 -0
  41. package/pi-extension/dist/settings/settings-ui.js +593 -563
  42. package/pi-extension/dist/ui/text-component.js +57 -0
  43. package/pi-extension/src/cognitive-routing/commands.ts +119 -0
  44. package/pi-extension/src/cognitive-routing/config.ts +307 -0
  45. package/pi-extension/src/cognitive-routing/controller.ts +1118 -0
  46. package/pi-extension/src/cognitive-routing/history.ts +638 -0
  47. package/pi-extension/src/cognitive-routing/host.ts +172 -0
  48. package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
  49. package/pi-extension/src/cognitive-routing/tool.ts +272 -0
  50. package/pi-extension/src/cognitive-routing/types.ts +89 -0
  51. package/pi-extension/src/context-virtualization/commands.ts +111 -0
  52. package/pi-extension/src/context-virtualization/projector.ts +62 -0
  53. package/pi-extension/src/context-virtualization/runtime.ts +406 -0
  54. package/pi-extension/src/context-virtualization/state.ts +79 -0
  55. package/pi-extension/src/context-virtualization/types.ts +92 -0
  56. package/pi-extension/src/conversation-history/passages.ts +81 -0
  57. package/pi-extension/src/conversation-history/ranking.ts +155 -0
  58. package/pi-extension/src/conversation-history/runtime.ts +250 -0
  59. package/pi-extension/src/conversation-history/search.ts +79 -0
  60. package/pi-extension/src/conversation-history/types.ts +31 -0
  61. package/pi-extension/src/freeflow-context/resolver.ts +139 -0
  62. package/pi-extension/src/freeflow-context/runtime.ts +100 -0
  63. package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
  64. package/pi-extension/src/freeflow-context/tool.ts +360 -0
  65. package/pi-extension/src/freeflow-context/types.ts +70 -0
  66. package/pi-extension/src/freeflow-context/visibility.ts +53 -0
  67. package/pi-extension/src/index.ts +553 -101
  68. package/pi-extension/src/runtime/runtime-context.ts +339 -281
  69. package/pi-extension/src/runtime/runtime-identity.ts +8 -0
  70. package/pi-extension/src/settings/settings-tui.ts +93 -0
  71. package/pi-extension/src/settings/settings-ui.ts +700 -613
  72. package/pi-extension/src/ui/text-component.ts +66 -0
  73. package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
  74. package/pi-extension/tests/cognitive-routing/controller.test.js +1099 -0
  75. package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1071 -0
  76. package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
  77. package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
  78. package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
  79. package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +236 -0
  80. package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
  81. package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
  82. package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
  83. package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
  84. package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
  85. package/pi-extension/tests/conversation-history/search.test.js +97 -0
  86. package/pi-extension/tests/integration/pi-extension.test.js +928 -2047
  87. package/pi-extension/tests/integration/prompt-architecture.test.js +305 -0
  88. package/runtime/prompts/cognitive-routing.md +9 -0
  89. package/runtime/prompts/context-virtualization.md +3 -0
  90. package/runtime/prompts/conversation-history.md +3 -0
  91. package/runtime/prompts/core.md +13 -0
  92. package/runtime/prompts/interaction-contract.md +7 -0
  93. package/runtime/prompts/skills.md +65 -0
  94. package/skills/action-selection/SKILL.md +91 -0
  95. package/skills/bypass/SKILL.md +3 -2
  96. package/skills/commit-work/SKILL.md +1 -1
  97. package/skills/commit-work/references/staging-decisions.md +1 -1
  98. package/skills/decision-gate/SKILL.md +1 -1
  99. package/skills/design-for-depth/SKILL.md +14 -33
  100. package/skills/design-for-depth/references/design-pressure-signals.md +1 -1
  101. package/skills/design-for-depth/references/interface-design-loop.md +1 -1
  102. package/skills/design-for-depth/references/software-design-philosophy.md +1 -1
  103. package/skills/diagnose-failure/SKILL.md +20 -14
  104. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +1 -3
  105. package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +1 -1
  106. package/skills/discuss/SKILL.md +34 -22
  107. package/skills/discuss/references/discussion-continuity.md +71 -0
  108. package/skills/evaluate-skill/SKILL.md +5 -5
  109. package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +42 -2
  110. package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
  111. package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
  112. package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
  113. package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
  114. package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
  115. package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
  116. package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
  117. package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
  118. package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
  119. package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
  120. package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
  121. package/skills/execute-work/SKILL.md +18 -15
  122. package/skills/execute-work/references/code-practices.md +1 -1
  123. package/skills/execute-work/references/execute-work-edges.md +42 -0
  124. package/skills/finish-branch/SKILL.md +2 -2
  125. package/skills/finish-branch/references/integration-options.md +1 -1
  126. package/skills/handoff/SKILL.md +3 -3
  127. package/skills/launch-work/SKILL.md +1 -1
  128. package/skills/launch-work/references/launch-readiness.md +2 -2
  129. package/skills/migration-work/SKILL.md +2 -2
  130. package/skills/migration-work/references/migration-lifecycle.md +1 -1
  131. package/skills/mode-contract/SKILL.md +32 -10
  132. package/skills/release-work/SKILL.md +1 -1
  133. package/skills/release-work/references/release-evidence.md +1 -1
  134. package/skills/review-artifact/SKILL.md +28 -26
  135. package/skills/review-work/SKILL.md +28 -26
  136. package/skills/review-work/references/{reviewer-prompt.md → independent-work-reviewer-contract.md} +1 -1
  137. package/skills/review-work/references/security-risk-lens.md +1 -1
  138. package/skills/setup-freeflow/SKILL.md +6 -10
  139. package/skills/setup-freeflow/references/activation-contract.md +5 -10
  140. package/skills/setup-freeflow/references/host-setup.md +3 -7
  141. package/skills/simplify-code/SKILL.md +1 -1
  142. package/skills/simplify-code/references/simplification-patterns.md +1 -1
  143. package/skills/tdd/SKILL.md +18 -38
  144. package/skills/tdd/references/test-design.md +2 -2
  145. package/skills/track-work/SKILL.md +362 -58
  146. package/skills/track-work/scripts/lib/codec.mjs +570 -0
  147. package/skills/track-work/scripts/lib/commands.mjs +568 -0
  148. package/skills/track-work/scripts/lib/contract.mjs +303 -0
  149. package/skills/track-work/scripts/lib/model.mjs +506 -0
  150. package/skills/track-work/scripts/lib/operations.mjs +965 -0
  151. package/skills/track-work/scripts/lib/record-store.mjs +264 -0
  152. package/skills/track-work/scripts/lib/result.mjs +37 -0
  153. package/skills/track-work/scripts/lib/rewrites.mjs +740 -0
  154. package/skills/track-work/scripts/lib/schema.mjs +277 -0
  155. package/skills/track-work/scripts/lib/source-inventory.mjs +162 -0
  156. package/skills/track-work/scripts/lib/views.mjs +443 -0
  157. package/skills/track-work/scripts/lib/workspace.mjs +134 -0
  158. package/skills/track-work/scripts/working-record.mjs +122 -0
  159. package/skills/verify-work/SKILL.md +14 -2
  160. package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
  161. package/skills/verify-work/references/integration-evidence.md +1 -1
  162. package/skills/verify-work/references/performance-evidence.md +1 -1
  163. package/skills/workflow/SKILL.md +105 -53
  164. package/skills/workflow/references/domain-skill-composition.md +22 -42
  165. package/skills/write-plan/SKILL.md +1 -1
  166. package/skills/write-skill/SKILL.md +3 -2
  167. package/skills/write-skill/references/progressive-disclosure.md +17 -3
  168. package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
  169. package/pi-extension/dist/output-router/native-safety-net.js +0 -216
  170. package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
  171. package/pi-extension/dist/output-router/renderers.js +0 -1023
  172. package/pi-extension/dist/output-router/router-tools.js +0 -554
  173. package/pi-extension/dist/output-router/schemas.js +0 -383
  174. package/pi-extension/dist/output-router/status.js +0 -744
  175. package/pi-extension/dist/output-router/utils.js +0 -502
  176. package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
  177. package/pi-extension/src/output-router/native-safety-net.ts +0 -248
  178. package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
  179. package/pi-extension/src/output-router/renderers.ts +0 -1108
  180. package/pi-extension/src/output-router/router-tools.ts +0 -579
  181. package/pi-extension/src/output-router/schemas.ts +0 -387
  182. package/pi-extension/src/output-router/status.ts +0 -791
  183. package/pi-extension/src/output-router/utils.ts +0 -554
  184. package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
  185. package/pi-extension/tests/output-router/search.test.js +0 -722
  186. package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
  187. package/router/dist/benchmarks/benchmark-harness.js +0 -92
  188. package/router/dist/benchmarks/benchmarks.d.ts +0 -109
  189. package/router/dist/benchmarks/benchmarks.js +0 -979
  190. package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
  191. package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
  192. package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
  193. package/router/dist/benchmarks/command-benchmarks.js +0 -698
  194. package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
  195. package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
  196. package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
  197. package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
  198. package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
  199. package/router/dist/benchmarks/context-saving-baseline.js +0 -470
  200. package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
  201. package/router/dist/benchmarks/index-benchmarks.js +0 -862
  202. package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
  203. package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
  204. package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
  205. package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
  206. package/router/dist/config/config.d.ts +0 -103
  207. package/router/dist/config/config.js +0 -632
  208. package/router/dist/config/router-contract.d.ts +0 -10
  209. package/router/dist/config/router-contract.js +0 -54
  210. package/router/dist/config/schema.d.ts +0 -27
  211. package/router/dist/config/schema.js +0 -648
  212. package/router/dist/config/types.d.ts +0 -573
  213. package/router/dist/config/types.js +0 -41
  214. package/router/dist/evidence/bounded-evidence.d.ts +0 -40
  215. package/router/dist/evidence/bounded-evidence.js +0 -323
  216. package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
  217. package/router/dist/evidence/evidence-range-selector.js +0 -232
  218. package/router/dist/evidence/evidence-search.d.ts +0 -27
  219. package/router/dist/evidence/evidence-search.js +0 -490
  220. package/router/dist/evidence/evidence.d.ts +0 -38
  221. package/router/dist/evidence/evidence.js +0 -200
  222. package/router/dist/evidence/failure-contracts.d.ts +0 -37
  223. package/router/dist/evidence/failure-contracts.js +0 -166
  224. package/router/dist/evidence/line-ranges.d.ts +0 -24
  225. package/router/dist/evidence/line-ranges.js +0 -22
  226. package/router/dist/experiments/local-index.d.ts +0 -64
  227. package/router/dist/experiments/local-index.js +0 -581
  228. package/router/dist/index.d.ts +0 -30
  229. package/router/dist/index.js +0 -32
  230. package/router/dist/local/local-traversal.d.ts +0 -18
  231. package/router/dist/local/local-traversal.js +0 -334
  232. package/router/dist/processing/engine.d.ts +0 -132
  233. package/router/dist/processing/engine.js +0 -533
  234. package/router/dist/processing/reducers.d.ts +0 -280
  235. package/router/dist/processing/reducers.js +0 -1673
  236. package/router/dist/processing/renderers.d.ts +0 -31
  237. package/router/dist/processing/renderers.js +0 -195
  238. package/router/dist/processing/scripts.d.ts +0 -91
  239. package/router/dist/processing/scripts.js +0 -386
  240. package/router/dist/repo/repo-traversal.d.ts +0 -18
  241. package/router/dist/repo/repo-traversal.js +0 -245
  242. package/router/dist/routing/observed-reducers.d.ts +0 -41
  243. package/router/dist/routing/observed-reducers.js +0 -661
  244. package/router/dist/routing/observed-routing.d.ts +0 -26
  245. package/router/dist/routing/observed-routing.js +0 -378
  246. package/router/dist/routing/parsers.d.ts +0 -16
  247. package/router/dist/routing/parsers.js +0 -521
  248. package/router/dist/routing/run-filters.d.ts +0 -60
  249. package/router/dist/routing/run-filters.js +0 -238
  250. package/router/dist/routing/run-reducers.d.ts +0 -49
  251. package/router/dist/routing/run-reducers.js +0 -122
  252. package/router/dist/sandbox/adapter-roots.d.ts +0 -46
  253. package/router/dist/sandbox/adapter-roots.js +0 -66
  254. package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
  255. package/router/dist/sandbox/eryx-python-adapter.js +0 -868
  256. package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
  257. package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
  258. package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
  259. package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
  260. package/router/dist/sandbox/script-sandbox.d.ts +0 -134
  261. package/router/dist/sandbox/script-sandbox.js +0 -337
  262. package/router/dist/setup/script-transform-adapters.d.ts +0 -32
  263. package/router/dist/setup/script-transform-adapters.js +0 -278
  264. package/router/dist/tools/batch.d.ts +0 -33
  265. package/router/dist/tools/batch.js +0 -586
  266. package/router/dist/tools/run.d.ts +0 -62
  267. package/router/dist/tools/run.js +0 -1304
  268. package/router/dist/tools/search.d.ts +0 -66
  269. package/router/dist/tools/search.js +0 -1364
  270. package/router/dist/transform/engine.d.ts +0 -168
  271. package/router/dist/transform/engine.js +0 -2072
  272. package/router/dist/vault/vault-index.d.ts +0 -125
  273. package/router/dist/vault/vault-index.js +0 -416
  274. package/router/dist/vault/vault.d.ts +0 -138
  275. package/router/dist/vault/vault.js +0 -630
  276. package/runtime/interaction-contract.md +0 -16
  277. package/skills/discuss/references/checkpoints.md +0 -66
  278. package/skills/execute-work/references/execution-loop.md +0 -77
  279. package/skills/output-router/SKILL.md +0 -206
  280. package/skills/output-router/references/safety-policy.md +0 -40
  281. package/skills/setup-freeflow/references/output-router-setup.md +0 -110
  282. package/skills/track-work/references/working-record-schema.md +0 -378
  283. package/skills/workflow/references/workflow-loop.md +0 -92
  284. /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
  285. /package/skills/review-artifact/references/{reviewer-prompt.md → independent-artifact-reviewer-contract.md} +0 -0
@@ -1,862 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
3
- import { createRequire } from "node:module";
4
- import { tmpdir } from "node:os";
5
- import { dirname, relative, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
- import { performance } from "node:perf_hooks";
8
- import {
9
- defaultJsonRunReportPath,
10
- escapeMarkdownTableCell as escapeTable,
11
- formatPercent,
12
- latencySummary,
13
- normalizeIterations,
14
- parseBenchmarkCliArgs,
15
- reductionPercent,
16
- writeBenchmarkReportPair,
17
- } from "./benchmark-harness.js";
18
- import { freeflowSearch } from "../tools/search.js";
19
- import { buildOrLoadExperimentalRepoIndex, queryExperimentalRepoIndex } from "../experiments/local-index.js";
20
- const DEFAULT_ITERATIONS = 3;
21
- const BENCHMARK_TOP_K = 3;
22
- const FTS_CONTEXT_LINES = 2;
23
- const require = createRequire(import.meta.url);
24
- let cachedFtsCandidateStatus;
25
- export async function runIndexBenchmarks(options = {}) {
26
- const iterations = normalizeIterations(options.iterations, DEFAULT_ITERATIONS);
27
- const fixtures = indexFixtureDefinitions();
28
- const fixtureResults = [];
29
- for (const fixture of fixtures) {
30
- const results = [];
31
- for (const mode of indexBenchmarkModes()) {
32
- results.push(await runIndexFixtureMode(fixture, mode, iterations));
33
- }
34
- fixtureResults.push({
35
- id: fixture.id,
36
- title: fixture.title,
37
- expected: fixtureExpectedForMode(fixture, "index-warm"),
38
- results,
39
- });
40
- }
41
- return {
42
- generatedAt: options.generatedAt ?? new Date().toISOString(),
43
- iterations,
44
- summary: summarizeIndexReport(fixtureResults),
45
- fixtures: fixtureResults,
46
- };
47
- }
48
- export function renderIndexBenchmarkReport(report) {
49
- const date = report.generatedAt.slice(0, 10);
50
- const lines = [
51
- "# Output Router Index Benchmark Report - Iteration 1",
52
- "",
53
- `Date: ${date}`,
54
- "",
55
- "## Scope",
56
- "",
57
- "Repo Search Backend Benchmark for `freeflow_search`. The scanner remains the product default; this benchmark compares scanner-only retrieval, the no-dependency local lexical index, a conservative hybrid scanner+index path, and records whether an FTS5/BM25/trigram candidate is available.",
58
- "",
59
- "The index cache is keyed by repo root and stores outside the repo by default. No external service, vector DB, or native dependency is required for the local lexical index.",
60
- "",
61
- "## Command",
62
- "",
63
- "```sh",
64
- "npm run bench:router:index",
65
- "```",
66
- "",
67
- "The CLI writes machine-readable JSON under `router/evals/runs/` by default. That JSON is generated run data; this Markdown file is the durable runtime report.",
68
- "",
69
- "## Summary",
70
- "",
71
- `- Fixtures: ${report.summary.fixtures}`,
72
- `- Scanner remains default: ${report.summary.scannerDefault ? "yes" : "no"}`,
73
- `- Index adopted by default: ${report.summary.indexAdopted ? "yes" : "no"}`,
74
- `- Scanner pass: ${report.summary.scanner.passed}/${report.summary.fixtures} (recall@${BENCHMARK_TOP_K}: ${report.summary.scanner.recallAtK}/${report.summary.fixtures})`,
75
- `- Index warm pass: ${report.summary.index.passed}/${report.summary.fixtures} (recall@${BENCHMARK_TOP_K}: ${report.summary.index.recallAtK}/${report.summary.fixtures})`,
76
- `- FTS5/BM25/trigram pass: ${report.summary.fts.passed}/${report.summary.fixtures} (skipped: ${report.summary.fts.skipped}; recall@${BENCHMARK_TOP_K}: ${report.summary.fts.recallAtK}/${report.summary.fixtures})`,
77
- `- Hybrid warm pass: ${report.summary.hybrid.passed}/${report.summary.fixtures} (recall@${BENCHMARK_TOP_K}: ${report.summary.hybrid.recallAtK}/${report.summary.fixtures})`,
78
- `- FTS5/BM25/trigram candidate: ${report.summary.ftsCandidate.available ? "available" : "unavailable"} — ${report.summary.ftsCandidate.reason}`,
79
- `- Generated false positives: ${report.summary.generatedFalsePositiveCount}/${report.summary.modeResults}`,
80
- `- Cold build p50/p95: ${report.summary.coldBuildMs.p50.toFixed(2)}/${report.summary.coldBuildMs.p95.toFixed(2)} ms`,
81
- `- Warm query p50/p95: ${report.summary.warmQueryMs.p50.toFixed(2)}/${report.summary.warmQueryMs.p95.toFixed(2)} ms`,
82
- `- Stale refresh p50/p95: ${report.summary.staleRefreshMs.p50.toFixed(2)}/${report.summary.staleRefreshMs.p95.toFixed(2)} ms`,
83
- `- Index warm context reduction: ${formatPercent(report.summary.index.weightedContextReductionPercent)} (${report.summary.index.totalRawBytes} raw bytes to ${report.summary.index.totalContextBytes} context bytes)`,
84
- "",
85
- "## Results",
86
- "",
87
- "| fixture | mode | correctness | checks | path | lines | candidates | raw/context bytes | context reduction | latency p50/p95 ms | build/query ms | index mode | notes |",
88
- "| --- | --- | --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | --- | --- |",
89
- ];
90
- for (const fixture of report.fixtures) {
91
- for (const result of fixture.results) {
92
- const correctness = result.skipped ? "skipped" : result.correctness.passed ? "pass" : "fail";
93
- const checks = formatCorrectnessChecks(result.correctness);
94
- const buildQuery = `${(result.buildMs ?? 0).toFixed(2)}/${(result.queryMs ?? 0).toFixed(2)}`;
95
- const notes = result.notes.length ? result.notes.join("; ") : "";
96
- lines.push(
97
- `| ${escapeTable(fixture.id)} | ${escapeTable(result.mode)} | ${correctness} | ${escapeTable(checks)} | ${escapeTable(result.actualPath ?? "-")} | ${escapeTable(result.actualLines ?? "-")} | ${escapeTable(result.candidatePaths.join(", ") || "-")} | ${result.rawBytes}/${result.contextBytes} | ${formatPercent(result.contextReductionPercent)} | ${result.latencyMs.p50.toFixed(2)}/${result.latencyMs.p95.toFixed(2)} | ${buildQuery} | ${escapeTable(result.indexMode ?? "-")} | ${escapeTable(notes)} |`,
98
- );
99
- }
100
- }
101
- lines.push(
102
- "",
103
- "## Adoption Decision",
104
- "",
105
- "Index adopted by default: no. This slice only records benchmark evidence; scanner behavior remains the default until a later adoption checkpoint explicitly approves otherwise.",
106
- "",
107
- "## Regression Status",
108
- "",
109
- report.summary.index.failed === 0 && report.summary.fts.failed === 0 && report.summary.hybrid.failed === 0
110
- ? "Warm experimental index, FTS5/BM25/trigram, and hybrid modes passed all gated fixtures without generated-artifact false positives."
111
- : `Failures: warm index ${report.summary.index.failed}, FTS5/BM25/trigram ${report.summary.fts.failed}, hybrid ${report.summary.hybrid.failed}.`,
112
- "",
113
- );
114
- return lines.join("\n");
115
- }
116
- export async function writeIndexBenchmarkReport(report, reportPath) {
117
- await mkdir(dirname(reportPath), { recursive: true });
118
- await writeFile(reportPath, renderIndexBenchmarkReport(report), "utf8");
119
- }
120
- export async function writeIndexBenchmarkJsonReport(report, reportPath) {
121
- await mkdir(dirname(reportPath), { recursive: true });
122
- await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
123
- }
124
- export async function writeIndexBenchmarkReports(report, markdownReportPath, options = {}) {
125
- return writeBenchmarkReportPair({
126
- report,
127
- markdownReportPath,
128
- jsonReportPath: options.jsonReportPath,
129
- renderMarkdown: renderIndexBenchmarkReport,
130
- });
131
- }
132
- async function runIndexFixtureMode(fixture, mode, iterations) {
133
- const latencies = [];
134
- let observation = null;
135
- for (let index = 0; index < iterations; index += 1) {
136
- const startedAt = performance.now();
137
- observation = await observeIndexFixture(fixture, mode);
138
- latencies.push(performance.now() - startedAt);
139
- }
140
- if (!observation) {
141
- observation = {
142
- toolPathUsed: mode,
143
- skipped: true,
144
- skipReason: "No observation was produced.",
145
- rawBytes: 0,
146
- contextBytes: 0,
147
- excerpt: "",
148
- notes: ["no observation"],
149
- };
150
- }
151
- const correctness = scoreCorrectness(fixtureExpectedForMode(fixture, mode), observation);
152
- const result = {
153
- mode,
154
- toolPathUsed: observation.toolPathUsed,
155
- skipped: observation.skipped ?? false,
156
- rawBytes: observation.rawBytes,
157
- contextBytes: observation.contextBytes,
158
- contextReductionPercent: reductionPercent(observation.rawBytes, observation.contextBytes),
159
- latencyMs: latencySummary(latencies),
160
- candidatePaths: observation.candidatePaths ?? [],
161
- excerpt: observation.excerpt,
162
- correctness,
163
- notes: observation.notes ?? [],
164
- };
165
- if (observation.skipReason) {
166
- result.skipReason = observation.skipReason;
167
- }
168
- if (observation.buildMs !== undefined) {
169
- result.buildMs = observation.buildMs;
170
- }
171
- if (observation.queryMs !== undefined) {
172
- result.queryMs = observation.queryMs;
173
- }
174
- if (observation.indexMode !== undefined) {
175
- result.indexMode = observation.indexMode;
176
- }
177
- if (observation.actualPath !== undefined) {
178
- result.actualPath = observation.actualPath;
179
- }
180
- if (observation.actualLines !== undefined) {
181
- result.actualLines = observation.actualLines;
182
- }
183
- return result;
184
- }
185
- async function observeIndexFixture(fixture, mode) {
186
- const root = await createTempDir("freeflow-router-index-benchmark-repo-");
187
- const cacheRoot = await createTempDir("freeflow-router-index-benchmark-cache-");
188
- try {
189
- if (mode === "index-stale-refresh") {
190
- await writeFixtureFiles(root.path, fixture.files);
191
- await buildOrLoadExperimentalRepoIndex({ root: root.path, cacheRoot: cacheRoot.path });
192
- await writeFixtureFiles(root.path, fixture.staleFiles ?? { "stale-marker.txt": "stale refresh marker" });
193
- return await indexObservation(root.path, cacheRoot.path, fixture.staleQuery ?? fixture.query, mode);
194
- }
195
- const finalFiles =
196
- mode === "scanner-default" ||
197
- mode === "index-cold" ||
198
- mode === "index-warm" ||
199
- mode === "fts5-bm25-trigram" ||
200
- mode === "hybrid-warm"
201
- ? { ...fixture.files, ...(fixture.staleFiles && fixture.staleExpected ? fixture.staleFiles : {}) }
202
- : fixture.files;
203
- await writeFixtureFiles(root.path, finalFiles);
204
- if (mode === "scanner-default") {
205
- return await scannerObservation(
206
- root.path,
207
- fixture.staleQuery && fixture.staleExpected ? fixture.staleQuery : fixture.query,
208
- );
209
- }
210
- if (mode === "fts5-bm25-trigram") {
211
- return await ftsObservation(
212
- root.path,
213
- fixture.staleQuery && fixture.staleExpected ? fixture.staleQuery : fixture.query,
214
- );
215
- }
216
- if (mode === "hybrid-warm") {
217
- await buildOrLoadExperimentalRepoIndex({ root: root.path, cacheRoot: cacheRoot.path });
218
- return await hybridObservation(
219
- root.path,
220
- cacheRoot.path,
221
- fixture.staleQuery && fixture.staleExpected ? fixture.staleQuery : fixture.query,
222
- );
223
- }
224
- if (mode === "index-warm") {
225
- await buildOrLoadExperimentalRepoIndex({ root: root.path, cacheRoot: cacheRoot.path });
226
- return await indexObservation(
227
- root.path,
228
- cacheRoot.path,
229
- fixture.staleQuery && fixture.staleExpected ? fixture.staleQuery : fixture.query,
230
- mode,
231
- );
232
- }
233
- return await indexObservation(
234
- root.path,
235
- cacheRoot.path,
236
- fixture.staleQuery && fixture.staleExpected ? fixture.staleQuery : fixture.query,
237
- mode,
238
- );
239
- } finally {
240
- await Promise.all([root.cleanup(), cacheRoot.cleanup()]);
241
- }
242
- }
243
- async function scannerObservation(root, query) {
244
- const rawBytes = await repoRawBytes(root);
245
- const queryStartedAt = performance.now();
246
- const result = await freeflowSearch({
247
- action: "query",
248
- source: { kind: "repo", root },
249
- query,
250
- preserve: "important",
251
- topK: BENCHMARK_TOP_K,
252
- });
253
- const queryMs = performance.now() - queryStartedAt;
254
- const evidence = result.evidence?.[0];
255
- const candidatePaths = uniqueCandidatePaths(result.evidence?.map((packet) => packet.path));
256
- return {
257
- toolPathUsed: "scanner-default: freeflowSearch repo scanner",
258
- rawBytes,
259
- contextBytes: byteLength(JSON.stringify(result)),
260
- ...(evidence?.path !== undefined ? { actualPath: evidence.path } : {}),
261
- ...(evidence?.lines !== undefined ? { actualLines: evidence.lines } : {}),
262
- candidatePaths,
263
- excerpt: evidence?.excerpt ?? "",
264
- queryMs,
265
- notes: [result.routing.reason],
266
- };
267
- }
268
- async function indexObservation(root, cacheRoot, query, mode) {
269
- const rawBytes = await repoRawBytes(root);
270
- const load = await buildOrLoadExperimentalRepoIndex({ root, cacheRoot });
271
- const queryStartedAt = performance.now();
272
- const candidates = queryExperimentalRepoIndex(load.index, query, { topK: BENCHMARK_TOP_K });
273
- const queryMs = performance.now() - queryStartedAt;
274
- const candidate = candidates[0];
275
- return {
276
- toolPathUsed: `${mode}: experimental local index`,
277
- rawBytes,
278
- contextBytes: byteLength(JSON.stringify(candidates)),
279
- ...(candidate?.path !== undefined ? { actualPath: candidate.path } : {}),
280
- ...(candidate?.lines !== undefined ? { actualLines: candidate.lines } : {}),
281
- candidatePaths: uniqueCandidatePaths(candidates.map((entry) => entry.path)),
282
- excerpt: candidate?.excerpt ?? "",
283
- buildMs: load.buildMs,
284
- queryMs,
285
- indexMode: load.mode,
286
- notes: candidate
287
- ? [
288
- candidate.reason,
289
- `cachePath=${load.cachePath}`,
290
- ...(load.refreshReason ? [`refresh=${load.refreshReason}`] : []),
291
- ]
292
- : ["no index candidate"],
293
- };
294
- }
295
- async function ftsObservation(root, query) {
296
- const rawBytes = await repoRawBytes(root);
297
- const ftsStatus = detectFtsCandidateStatus();
298
- if (!ftsStatus.available) {
299
- return {
300
- toolPathUsed: "fts5-bm25-trigram: unavailable optional candidate",
301
- skipped: true,
302
- skipReason: ftsStatus.reason,
303
- rawBytes,
304
- contextBytes: 0,
305
- candidatePaths: [],
306
- excerpt: "",
307
- notes: [ftsStatus.reason],
308
- };
309
- }
310
- const files = await readFtsTextFiles(root);
311
- const sqlite = loadSqliteModule();
312
- const startedAt = performance.now();
313
- const db = new sqlite.DatabaseSync(":memory:");
314
- try {
315
- db.exec("CREATE VIRTUAL TABLE docs USING fts5(path UNINDEXED, body, tokenize='trigram')");
316
- const insert = db.prepare("INSERT INTO docs(path, body) VALUES (?, ?)");
317
- for (const file of files) {
318
- insert.run(file.path, file.text);
319
- }
320
- const matchQuery = ftsMatchQuery(query);
321
- const rows = matchQuery
322
- ? db
323
- .prepare("SELECT path, bm25(docs) AS rank FROM docs WHERE docs MATCH ? ORDER BY rank LIMIT ?")
324
- .all(matchQuery, BENCHMARK_TOP_K)
325
- : [];
326
- const queryMs = performance.now() - startedAt;
327
- const byPath = new Map(files.map((file) => [file.path, file]));
328
- const candidates = rows.flatMap((row) => {
329
- const file = byPath.get(row.path);
330
- if (!file) {
331
- return [];
332
- }
333
- const range = bestFtsLineRange(file, query);
334
- return [
335
- {
336
- path: file.path,
337
- lines: `${range.start}-${range.end}`,
338
- excerpt: file.lines.slice(range.start - 1, range.end).join("\n"),
339
- rank: row.rank,
340
- reason: `SQLite FTS5 trigram MATCH with bm25 rank ${row.rank.toFixed(6)}`,
341
- },
342
- ];
343
- });
344
- const first = candidates[0];
345
- return {
346
- toolPathUsed: "fts5-bm25-trigram: node:sqlite FTS5 trigram virtual table",
347
- rawBytes,
348
- contextBytes: byteLength(JSON.stringify(candidates)),
349
- ...(first?.path !== undefined ? { actualPath: first.path } : {}),
350
- ...(first?.lines !== undefined ? { actualLines: first.lines } : {}),
351
- candidatePaths: uniqueCandidatePaths(candidates.map((candidate) => candidate.path)),
352
- excerpt: first?.excerpt ?? "",
353
- buildMs: queryMs,
354
- queryMs,
355
- notes: first ? [first.reason] : ["no FTS5 candidate"],
356
- };
357
- } finally {
358
- db.close();
359
- }
360
- }
361
- async function hybridObservation(root, cacheRoot, query) {
362
- const rawBytes = await repoRawBytes(root);
363
- const load = await buildOrLoadExperimentalRepoIndex({ root, cacheRoot });
364
- const indexStartedAt = performance.now();
365
- const indexCandidates = queryExperimentalRepoIndex(load.index, query, { topK: BENCHMARK_TOP_K });
366
- const indexQueryMs = performance.now() - indexStartedAt;
367
- const scannerStartedAt = performance.now();
368
- const scanner = await freeflowSearch({
369
- action: "query",
370
- source: { kind: "repo", root },
371
- query,
372
- preserve: "important",
373
- topK: BENCHMARK_TOP_K,
374
- });
375
- const scannerQueryMs = performance.now() - scannerStartedAt;
376
- const scannerEvidence = scanner.evidence?.[0];
377
- const scannerPaths = uniqueCandidatePaths(scanner.evidence?.map((packet) => packet.path));
378
- const indexPaths = uniqueCandidatePaths(indexCandidates.map((candidate) => candidate.path));
379
- const hybridInput = { scannerPaths, indexCandidates, indexPaths };
380
- if (scannerEvidence !== undefined) {
381
- hybridInput.scannerEvidence = scannerEvidence;
382
- }
383
- const selected = chooseHybridCandidate(hybridInput);
384
- const contextPayload = {
385
- selected: selected.payload,
386
- scannerCandidateCount: scannerPaths.length,
387
- indexCandidateCount: indexPaths.length,
388
- selection: selected.selection,
389
- };
390
- return {
391
- toolPathUsed: "hybrid-warm: scanner fallback with warm lexical-index candidates",
392
- rawBytes,
393
- contextBytes: byteLength(JSON.stringify(contextPayload)),
394
- ...(selected.path !== undefined ? { actualPath: selected.path } : {}),
395
- ...(selected.lines !== undefined ? { actualLines: selected.lines } : {}),
396
- candidatePaths: uniqueCandidatePaths([...scannerPaths, ...indexPaths]),
397
- excerpt: selected.excerpt,
398
- buildMs: load.buildMs,
399
- queryMs: indexQueryMs + scannerQueryMs,
400
- indexMode: load.mode,
401
- notes: [
402
- `selection=${selected.selection}`,
403
- `scannerQueryMs=${scannerQueryMs.toFixed(2)}`,
404
- `indexQueryMs=${indexQueryMs.toFixed(2)}`,
405
- ...(load.refreshReason ? [`refresh=${load.refreshReason}`] : []),
406
- ],
407
- };
408
- }
409
- function chooseHybridCandidate(options) {
410
- const scannerEvidence = options.scannerEvidence;
411
- if (scannerEvidence?.path) {
412
- const indexAgrees = options.indexPaths.includes(scannerEvidence.path);
413
- return {
414
- selection: indexAgrees ? "scanner-index-agree" : "scanner-fallback",
415
- path: scannerEvidence.path,
416
- ...(scannerEvidence.lines !== undefined ? { lines: scannerEvidence.lines } : {}),
417
- excerpt: scannerEvidence.excerpt ?? "",
418
- payload: {
419
- source: "scanner",
420
- path: scannerEvidence.path,
421
- ...(scannerEvidence.lines !== undefined ? { lines: scannerEvidence.lines } : {}),
422
- indexAgrees,
423
- },
424
- };
425
- }
426
- const indexCandidate = options.indexCandidates[0];
427
- if (indexCandidate) {
428
- return {
429
- selection: "index-only",
430
- path: indexCandidate.path,
431
- lines: indexCandidate.lines,
432
- excerpt: indexCandidate.excerpt,
433
- payload: {
434
- source: "index",
435
- path: indexCandidate.path,
436
- lines: indexCandidate.lines,
437
- score: indexCandidate.score,
438
- },
439
- };
440
- }
441
- return { selection: "none", excerpt: "", payload: { source: "none" } };
442
- }
443
- function detectFtsCandidateStatus() {
444
- if (cachedFtsCandidateStatus !== undefined) {
445
- return cachedFtsCandidateStatus;
446
- }
447
- let db;
448
- try {
449
- const sqlite = loadSqliteModule();
450
- db = new sqlite.DatabaseSync(":memory:");
451
- db.exec("CREATE VIRTUAL TABLE docs USING fts5(path UNINDEXED, body, tokenize='trigram')");
452
- db.exec("INSERT INTO docs(path, body) VALUES ('probe', 'freeflow fts probe')");
453
- const rows = db.prepare("SELECT path, bm25(docs) AS rank FROM docs WHERE docs MATCH ?").all("freeflow AND probe");
454
- cachedFtsCandidateStatus =
455
- rows.length > 0
456
- ? {
457
- available: true,
458
- engine: "fts5-bm25-trigram",
459
- reason: "Node node:sqlite is available and supports SQLite FTS5 trigram tokenization with bm25 ranking.",
460
- }
461
- : {
462
- available: false,
463
- engine: "none",
464
- reason: "SQLite FTS5 probe returned no rows for a known trigram query.",
465
- };
466
- } catch (error) {
467
- const message = error instanceof Error ? error.message : String(error);
468
- cachedFtsCandidateStatus = {
469
- available: false,
470
- engine: "none",
471
- reason: `SQLite FTS5/BM25/trigram candidate unavailable: ${message}`,
472
- };
473
- } finally {
474
- db?.close();
475
- }
476
- return cachedFtsCandidateStatus;
477
- }
478
- function loadSqliteModule() {
479
- return require("node:sqlite");
480
- }
481
- async function readFtsTextFiles(root) {
482
- const files = [];
483
- await collectFtsTextFiles(root, root, files);
484
- return files.sort((a, b) => a.path.localeCompare(b.path));
485
- }
486
- async function collectFtsTextFiles(root, currentPath, files) {
487
- const currentStat = await stat(currentPath);
488
- if (currentStat.isDirectory()) {
489
- const dirEntries = await readdir(currentPath, { withFileTypes: true });
490
- for (const entry of dirEntries.sort((a, b) => a.name.localeCompare(b.name))) {
491
- await collectFtsTextFiles(root, resolve(currentPath, entry.name), files);
492
- }
493
- return;
494
- }
495
- if (!currentStat.isFile()) {
496
- return;
497
- }
498
- const path = relative(root, currentPath)
499
- .split(/[\\/]+/)
500
- .join("/");
501
- if (isGeneratedBenchmarkPath(path)) {
502
- return;
503
- }
504
- try {
505
- const text = await readFile(currentPath, "utf8");
506
- if (!text.includes("\0")) {
507
- files.push({ path, text, lines: splitLines(text) });
508
- }
509
- } catch {
510
- // ignore non-text fixture files
511
- }
512
- }
513
- function ftsMatchQuery(query) {
514
- return searchTokens(query)
515
- .filter((token) => token.length >= 3)
516
- .join(" AND ");
517
- }
518
- function bestFtsLineRange(file, query) {
519
- const tokens = searchTokens(query);
520
- let bestIndex = 0;
521
- let bestScore = -1;
522
- for (let index = 0; index < file.lines.length; index += 1) {
523
- const normalizedLine = (file.lines[index] ?? "").toLowerCase();
524
- const score = tokens.reduce((sum, token) => sum + (normalizedLine.includes(token) ? 1 : 0), 0);
525
- if (score > bestScore) {
526
- bestScore = score;
527
- bestIndex = index;
528
- }
529
- }
530
- const start = Math.max(1, bestIndex + 1 - FTS_CONTEXT_LINES);
531
- const end = Math.min(file.lines.length, bestIndex + 1 + FTS_CONTEXT_LINES);
532
- return { start, end };
533
- }
534
- function searchTokens(value) {
535
- const seen = new Set();
536
- const tokens = [];
537
- for (const token of value.toLowerCase().match(/[a-z0-9_]+/g) ?? []) {
538
- if (!seen.has(token)) {
539
- seen.add(token);
540
- tokens.push(token);
541
- }
542
- }
543
- return tokens;
544
- }
545
- function summarizeIndexReport(fixtures) {
546
- const scanner = summarizeMode(fixtures, "scanner-default");
547
- const index = summarizeMode(fixtures, "index-warm");
548
- const fts = summarizeMode(fixtures, "fts5-bm25-trigram");
549
- const hybrid = summarizeMode(fixtures, "hybrid-warm");
550
- const generatedFalsePositiveCount = fixtures
551
- .flatMap((fixture) => fixture.results)
552
- .filter((result) => result.correctness.generatedFalsePositive).length;
553
- const coldBuildMs = latencyFor(fixtures, "index-cold", "buildMs");
554
- const warmQueryMs = latencyFor(fixtures, "index-warm", "queryMs");
555
- const staleRefreshMs = latencyFor(fixtures, "index-stale-refresh", "buildMs");
556
- return {
557
- fixtures: fixtures.length,
558
- modeResults: fixtures.reduce((count, fixture) => count + fixture.results.length, 0),
559
- scannerDefault: true,
560
- indexAdopted: false,
561
- ftsCandidate: detectFtsCandidateStatus(),
562
- scanner,
563
- index,
564
- fts,
565
- hybrid,
566
- generatedFalsePositiveCount,
567
- coldBuildMs,
568
- warmQueryMs,
569
- staleRefreshMs,
570
- };
571
- }
572
- function summarizeMode(fixtures, mode) {
573
- const results = fixtures
574
- .map((fixture) => fixture.results.find((result) => result.mode === mode))
575
- .filter((result) => Boolean(result));
576
- const totalRawBytes = results.reduce((sum, result) => sum + result.rawBytes, 0);
577
- const totalContextBytes = results.reduce((sum, result) => sum + result.contextBytes, 0);
578
- return results.reduce(
579
- (summary, result) => {
580
- if (result.skipped) {
581
- summary.skipped += 1;
582
- } else if (result.correctness.passed) {
583
- summary.passed += 1;
584
- } else {
585
- summary.failed += 1;
586
- }
587
- if (result.correctness.pathCorrect) {
588
- summary.pathCorrect += 1;
589
- }
590
- if (result.correctness.spanCorrect) {
591
- summary.spanCorrect += 1;
592
- }
593
- if (result.correctness.excerptComplete) {
594
- summary.excerptComplete += 1;
595
- }
596
- if (result.correctness.recallAtK) {
597
- summary.recallAtK += 1;
598
- }
599
- return summary;
600
- },
601
- {
602
- passed: 0,
603
- failed: 0,
604
- skipped: 0,
605
- pathCorrect: 0,
606
- spanCorrect: 0,
607
- excerptComplete: 0,
608
- recallAtK: 0,
609
- totalRawBytes,
610
- totalContextBytes,
611
- weightedContextReductionPercent: reductionPercent(totalRawBytes, totalContextBytes),
612
- },
613
- );
614
- }
615
- function latencyFor(fixtures, mode, field) {
616
- const values = fixtures
617
- .map((fixture) => fixture.results.find((result) => result.mode === mode)?.[field])
618
- .filter((value) => typeof value === "number");
619
- return latencySummary(values);
620
- }
621
- function indexFixtureDefinitions() {
622
- return [
623
- {
624
- id: "index-exact-copied-text",
625
- title: "Exact copied text block",
626
- files: {
627
- "target.md": ["# Target", "", "ExperimentalIndex exact copied text source truth."].join("\n"),
628
- "decoy.md": "ExperimentalIndex ExperimentalIndex ExperimentalIndex without copied source truth",
629
- },
630
- query: "ExperimentalIndex exact copied text source truth",
631
- expected: {
632
- path: "target.md",
633
- lines: "1-3",
634
- requiredExcerpt: ["ExperimentalIndex exact copied text source truth"],
635
- },
636
- },
637
- {
638
- id: "index-generated-artifact-decoy",
639
- title: "Generated-artifact decoy",
640
- files: {
641
- "router/evals/fixtures/output-router-corpus/sandbox-permissions.md": [
642
- "# Pass 3",
643
- "",
644
- "### Sandbox Permissions",
645
- "",
646
- "`SandboxPermissions` is a per-command request shape.",
647
- "",
648
- "Plain-language meaning:",
649
- "",
650
- "UseDefault: run with the turn's normal sandbox.",
651
- ].join("\n"),
652
- "graphify-out/graph.html": [
653
- "<html><body>",
654
- `${"Sandbox Permissions SandboxPermissions Plain-language meaning ".repeat(5000)}GENERATED_GRAPH_DECOY_SENTINEL`,
655
- "</body></html>",
656
- ].join("\n"),
657
- },
658
- query: "Sandbox Permissions SandboxPermissions Plain-language meaning",
659
- expected: {
660
- path: "router/evals/fixtures/output-router-corpus/sandbox-permissions.md",
661
- lines: "3-7",
662
- requiredExcerpt: ["SandboxPermissions", "Plain-language meaning"],
663
- },
664
- },
665
- {
666
- id: "index-stale-refresh",
667
- title: "Changed-file stale refresh",
668
- files: {
669
- "target.md": ["# Target", "", "StaleIndexNeedle original source truth."].join("\n"),
670
- },
671
- query: "StaleIndexNeedle original source truth",
672
- expected: {
673
- path: "target.md",
674
- lines: "1-3",
675
- requiredExcerpt: ["StaleIndexNeedle original"],
676
- },
677
- staleFiles: {
678
- "target.md": ["# Target", "", "StaleIndexNeedle refreshed source truth."].join("\n"),
679
- },
680
- staleQuery: "StaleIndexNeedle refreshed source truth",
681
- staleExpected: {
682
- path: "target.md",
683
- lines: "1-3",
684
- requiredExcerpt: ["StaleIndexNeedle refreshed"],
685
- },
686
- },
687
- ];
688
- }
689
- function fixtureExpectedForMode(fixture, mode) {
690
- if ((mode === "index-stale-refresh" || fixture.staleQuery) && fixture.staleExpected) {
691
- return fixture.staleExpected;
692
- }
693
- return fixture.expected;
694
- }
695
- function scoreCorrectness(expected, observation) {
696
- if (observation.skipped) {
697
- return {
698
- passed: false,
699
- pathCorrect: false,
700
- spanCorrect: false,
701
- excerptComplete: false,
702
- recallAtK: false,
703
- generatedFalsePositive: false,
704
- };
705
- }
706
- const pathCorrect = observation.actualPath === expected.path;
707
- const spanCorrect = expected.lines
708
- ? Boolean(observation.actualLines && rangesOverlap(expected.lines, observation.actualLines))
709
- : true;
710
- const excerptComplete = expected.requiredExcerpt.every((snippet) =>
711
- observation.excerpt.toLowerCase().includes(snippet.toLowerCase()),
712
- );
713
- const candidatePaths = observation.candidatePaths ?? [];
714
- const recallAtK = candidatePaths.length === 0 ? pathCorrect : candidatePaths.includes(expected.path);
715
- const generatedFalsePositive = observation.actualPath ? isGeneratedBenchmarkPath(observation.actualPath) : false;
716
- return {
717
- passed: pathCorrect && spanCorrect && excerptComplete && recallAtK && !generatedFalsePositive,
718
- pathCorrect,
719
- spanCorrect,
720
- excerptComplete,
721
- recallAtK,
722
- generatedFalsePositive,
723
- };
724
- }
725
- async function writeFixtureFiles(root, files) {
726
- for (const [path, text] of Object.entries(files)) {
727
- const absolutePath = resolve(root, path);
728
- await mkdir(dirname(absolutePath), { recursive: true });
729
- await writeFile(absolutePath, text, "utf8");
730
- }
731
- }
732
- async function repoRawBytes(root) {
733
- const entries = [];
734
- await collectRawText(root, root, entries);
735
- return byteLength(entries.join("\n"));
736
- }
737
- async function collectRawText(root, currentPath, entries) {
738
- const currentStat = await stat(currentPath);
739
- if (currentStat.isDirectory()) {
740
- const dirEntries = await readdir(currentPath, { withFileTypes: true });
741
- for (const entry of dirEntries.sort((a, b) => a.name.localeCompare(b.name))) {
742
- await collectRawText(root, resolve(currentPath, entry.name), entries);
743
- }
744
- return;
745
- }
746
- if (!currentStat.isFile()) {
747
- return;
748
- }
749
- try {
750
- const text = await readFile(currentPath, "utf8");
751
- if (!text.includes("\0")) {
752
- entries.push(text);
753
- }
754
- } catch {
755
- // ignore non-text fixture files
756
- }
757
- }
758
- async function createTempDir(prefix) {
759
- const path = await mkdtemp(resolve(tmpdir(), prefix));
760
- return {
761
- path,
762
- cleanup: async () => {
763
- await rm(path, { recursive: true, force: true });
764
- },
765
- };
766
- }
767
- function indexBenchmarkModes() {
768
- return ["scanner-default", "index-cold", "index-warm", "index-stale-refresh", "fts5-bm25-trigram", "hybrid-warm"];
769
- }
770
- function rangesOverlap(left, right) {
771
- const leftRange = parseRange(left);
772
- const rightRange = parseRange(right);
773
- if (!leftRange || !rightRange) {
774
- return false;
775
- }
776
- return leftRange.start <= rightRange.end && rightRange.start <= leftRange.end;
777
- }
778
- function parseRange(range) {
779
- const match = /^(\d+)-(\d+)$/.exec(range);
780
- if (!match) {
781
- return null;
782
- }
783
- const start = Number(match[1]);
784
- const end = Number(match[2]);
785
- return Number.isInteger(start) && Number.isInteger(end) ? { start, end } : null;
786
- }
787
- function uniqueCandidatePaths(paths) {
788
- const seen = new Set();
789
- const unique = [];
790
- for (const path of paths ?? []) {
791
- if (!path || seen.has(path)) {
792
- continue;
793
- }
794
- seen.add(path);
795
- unique.push(path);
796
- }
797
- return unique;
798
- }
799
- function isGeneratedBenchmarkPath(path) {
800
- const segments = path.split(/[\\/]+/);
801
- if (segments.includes("graphify-out")) {
802
- return true;
803
- }
804
- const name = segments.at(-1)?.toLowerCase() ?? path.toLowerCase();
805
- return (
806
- name.endsWith(".log") ||
807
- name.endsWith(".map") ||
808
- name.endsWith(".min.js") ||
809
- name.endsWith(".min.css") ||
810
- name.includes(".bundle.")
811
- );
812
- }
813
- function splitLines(text) {
814
- return text.split(/\r?\n/);
815
- }
816
- function byteLength(value) {
817
- return Buffer.byteLength(value, "utf8");
818
- }
819
- function formatCorrectnessChecks(correctness) {
820
- return [
821
- `path ${correctness.pathCorrect ? "✓" : "✗"}`,
822
- `span ${correctness.spanCorrect ? "✓" : "✗"}`,
823
- `excerpt ${correctness.excerptComplete ? "✓" : "✗"}`,
824
- `recall@${BENCHMARK_TOP_K} ${correctness.recallAtK ? "✓" : "✗"}`,
825
- `gen-fp ${correctness.generatedFalsePositive ? "✗" : "✓"}`,
826
- ].join(" ");
827
- }
828
- function defaultReportPath() {
829
- return resolve(process.cwd(), "router/evals/reports/output-router-index-benchmark-1-report.md");
830
- }
831
- async function runCli() {
832
- const { iterations, reportPath, jsonReportPath } = parseBenchmarkCliArgs(process.argv.slice(2), {
833
- reportPath: defaultReportPath(),
834
- });
835
- const options = {};
836
- if (iterations !== undefined) {
837
- options.iterations = iterations;
838
- }
839
- const report = await runIndexBenchmarks(options);
840
- const reports = await writeIndexBenchmarkReports(report, reportPath, {
841
- jsonReportPath: jsonReportPath === undefined ? defaultJsonRunReportPath(reportPath) : jsonReportPath,
842
- });
843
- const shortId = createHash("sha256").update(JSON.stringify(report.summary)).digest("hex").slice(0, 8);
844
- console.log(
845
- `Freeflow repo search backend benchmark ${shortId}: scanner ${report.summary.scanner.passed}/${report.summary.fixtures}, index ${report.summary.index.passed}/${report.summary.fixtures}, fts ${report.summary.fts.passed}/${report.summary.fixtures}, hybrid ${report.summary.hybrid.passed}/${report.summary.fixtures} pass`,
846
- );
847
- console.log(`Markdown report: ${reports.markdown}`);
848
- if (reports.json) {
849
- console.log(`JSON run data: ${reports.json}`);
850
- }
851
- if (
852
- report.summary.scanner.failed > 0 ||
853
- report.summary.index.failed > 0 ||
854
- report.summary.fts.failed > 0 ||
855
- report.summary.hybrid.failed > 0
856
- ) {
857
- process.exitCode = 1;
858
- }
859
- }
860
- if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
861
- await runCli();
862
- }