@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,1673 +0,0 @@
1
- const TEST_OUTPUT_REDUCER = {
2
- name: "test-output",
3
- version: "1",
4
- };
5
- const BUILD_OUTPUT_REDUCER = {
6
- name: "build-output",
7
- version: "1",
8
- };
9
- const DIAGNOSTICS_REDUCER = {
10
- name: "diagnostics",
11
- version: "1",
12
- };
13
- const MCP_TOOLS_REDUCER = {
14
- name: "mcp-tools",
15
- version: "1",
16
- };
17
- const JSON_QUERY_REDUCER = {
18
- name: "json-query",
19
- version: "1",
20
- };
21
- const TABLE_REDUCER = {
22
- name: "table",
23
- version: "1",
24
- };
25
- const BROWSER_SNAPSHOT_REDUCER = {
26
- name: "browser-snapshot",
27
- version: "1",
28
- };
29
- const GIT_LOG_REDUCER = {
30
- name: "git-log",
31
- version: "1",
32
- };
33
- const ACCESS_LOG_REDUCER = {
34
- name: "access-log",
35
- version: "1",
36
- };
37
- const MIN_ACCESS_LOG_LINES = 5;
38
- const MIN_ACCESS_LOG_CONFIDENCE = 0.8;
39
- const SLOW_REQUEST_THRESHOLD_MS = 1_000;
40
- const ACCESS_LOG_LINE =
41
- /^(\S+)\s+\S+\s+\S+\s+\[[^\]]+\]\s+"([A-Z]+)\s+([^"\s]+)\s+HTTP\/[^"\s]+"\s+(\d{3})\s+(?:\d+|-)\s+(\d+)ms\s*$/;
42
- export function selectProcessingReducer(input) {
43
- const testOutput = reduceTestOutput(input.text);
44
- const buildOutput = reduceBuildOutput(input.text);
45
- const diagnostics = reduceDiagnosticsOutput(input.text);
46
- const mcpTools = reduceMcpToolsOutput(input.text);
47
- const jsonQuery = reduceJsonQueryOutput(input.text, input.goal);
48
- const table = reduceTableOutput(input.text);
49
- const browserSnapshot = reduceBrowserSnapshotOutput(input.text);
50
- const gitLog = reduceGitLogOutput(input.text);
51
- const accessLog = reduceAccessLog(input.text);
52
- const candidates = [
53
- testOutput.candidate,
54
- buildOutput.candidate,
55
- diagnostics.candidate,
56
- mcpTools.candidate,
57
- jsonQuery.candidate,
58
- table.candidate,
59
- browserSnapshot.candidate,
60
- gitLog.candidate,
61
- accessLog.candidate,
62
- ];
63
- const selected = [
64
- testOutput,
65
- buildOutput,
66
- diagnostics,
67
- mcpTools,
68
- jsonQuery,
69
- table,
70
- browserSnapshot,
71
- gitLog,
72
- accessLog,
73
- ].find((reduced) => reduced.result !== undefined);
74
- if (selected?.result) {
75
- return {
76
- status: "selected",
77
- candidates,
78
- selected: selected.candidate,
79
- result: selected.result,
80
- reason: selected.candidate.reason,
81
- };
82
- }
83
- return {
84
- status: "not_selected",
85
- candidates,
86
- reason: "No high-confidence built-in reducer matched the loaded source.",
87
- };
88
- }
89
- export function reduceTestOutput(text) {
90
- const lines = text.split(/\r?\n/);
91
- const testFiles = findTestCounts(lines, "Test Files");
92
- const tests = findTestCounts(lines, "Tests");
93
- const failedFiles = uniqueStrings(lines.map(parseFailedTestFile).filter((file) => file !== undefined));
94
- const failedTests = lines
95
- .map(parseFailedTestName)
96
- .filter((name) => name !== undefined)
97
- .slice(0, 8);
98
- const confidence = testOutputConfidence({ testFiles, tests, failedFiles, failedTests, text });
99
- const candidate = {
100
- ...TEST_OUTPUT_REDUCER,
101
- confidence,
102
- reason: `Detected test summary counts=${testFiles || tests ? "yes" : "no"}, failedFiles=${failedFiles.length}, failedTests=${failedTests.length}.`,
103
- };
104
- if (confidence < 0.8 || (testFiles === undefined && tests === undefined)) {
105
- return { candidate };
106
- }
107
- const details = {
108
- kind: "test-output",
109
- testFiles: testFiles ?? {},
110
- tests: tests ?? {},
111
- failedFiles,
112
- failedTests,
113
- };
114
- return {
115
- candidate,
116
- result: {
117
- ...TEST_OUTPUT_REDUCER,
118
- confidence,
119
- reason: candidate.reason,
120
- facts: testOutputFacts(details),
121
- visibleText: renderTestOutputSummary(details),
122
- details,
123
- },
124
- };
125
- }
126
- function findTestCounts(lines, label) {
127
- const pattern = label === "Test Files" ? /^\s*Test Files:?\s+(.+)$/i : /^\s*Tests:?\s+(.+)$/i;
128
- for (const line of lines) {
129
- const match = pattern.exec(line);
130
- if (!match) {
131
- continue;
132
- }
133
- const counts = parseTestCounts(match[1] ?? "");
134
- if (Object.keys(counts).length > 0) {
135
- return counts;
136
- }
137
- }
138
- return undefined;
139
- }
140
- function parseTestCounts(summary) {
141
- const counts = {};
142
- const failed = /(\d+)\s+failed\b/i.exec(summary);
143
- const passed = /(\d+)\s+passed\b/i.exec(summary);
144
- const skipped = /(\d+)\s+(?:skipped|todo)\b/i.exec(summary);
145
- const parenthesizedTotal = /\((\d+)\)/.exec(summary);
146
- const total = parenthesizedTotal ?? /(\d+)\s+total\b/i.exec(summary);
147
- if (failed) {
148
- counts.failed = Number(failed[1]);
149
- }
150
- if (passed) {
151
- counts.passed = Number(passed[1]);
152
- }
153
- if (skipped) {
154
- counts.skipped = Number(skipped[1]);
155
- }
156
- if (total) {
157
- counts.total = Number(total[1]);
158
- }
159
- return counts;
160
- }
161
- function parseFailedTestFile(line) {
162
- const vitest = /^\s*✗\s+([^\s]+\.(?:test|spec)\.[cm]?[jt]sx?)(?:\s|\(|$)/.exec(line);
163
- if (vitest) {
164
- return vitest[1];
165
- }
166
- const jest = /^\s*FAIL\s+([^\s]+\.(?:test|spec)\.[cm]?[jt]sx?)(?:\s|$)/.exec(line);
167
- return jest?.[1];
168
- }
169
- function parseFailedTestName(line) {
170
- const match = /^\s*✗\s+(.+?)(?:\s+\d+ms)?\s*$/.exec(line);
171
- if (!match) {
172
- return undefined;
173
- }
174
- const name = match[1]?.trim();
175
- if (!name || /\.(?:test|spec)\.[cm]?[jt]sx?(?:\s|\(|$)/.test(name)) {
176
- return undefined;
177
- }
178
- return name;
179
- }
180
- function testOutputConfidence(input) {
181
- let confidence = 0;
182
- if (input.tests !== undefined) {
183
- confidence += 0.45;
184
- }
185
- if (input.testFiles !== undefined) {
186
- confidence += 0.35;
187
- }
188
- if (input.failedFiles.length > 0 || input.failedTests.length > 0) {
189
- confidence += 0.15;
190
- }
191
- if (/\b(?:vitest|jest|mocha|pytest|Test Files|Test Suites)\b/i.test(input.text)) {
192
- confidence += 0.1;
193
- }
194
- return Math.min(1, roundConfidence(confidence));
195
- }
196
- function testOutputFacts(details) {
197
- const facts = [];
198
- const tests = countsSummary(details.tests);
199
- if (tests) {
200
- facts.push({ name: "tests", value: tests });
201
- }
202
- if (details.failedFiles.length > 0) {
203
- facts.push({ name: "failedFiles", value: details.failedFiles.slice(0, 8).join(", ") });
204
- }
205
- return facts;
206
- }
207
- function renderTestOutputSummary(details) {
208
- const lines = ["test-output summary"];
209
- const tests = countsSummary(details.tests);
210
- const testFiles = countsSummary(details.testFiles);
211
- if (tests) {
212
- lines.push(`tests: ${tests}`);
213
- }
214
- if (testFiles) {
215
- lines.push(`testFiles: ${testFiles}`);
216
- }
217
- if (details.failedFiles.length > 0) {
218
- lines.push(`failedFiles: ${details.failedFiles.slice(0, 8).join(", ")}`);
219
- }
220
- if (details.failedTests.length > 0) {
221
- lines.push("failed tests:");
222
- details.failedTests.slice(0, 8).forEach((name) => lines.push(`- ${name}`));
223
- }
224
- return lines.join("\n");
225
- }
226
- function countsSummary(counts) {
227
- const parts = [];
228
- if (counts.failed !== undefined) {
229
- parts.push(`${counts.failed} failed`);
230
- }
231
- if (counts.passed !== undefined) {
232
- parts.push(`${counts.passed} passed`);
233
- }
234
- if (counts.skipped !== undefined) {
235
- parts.push(`${counts.skipped} skipped`);
236
- }
237
- if (counts.total !== undefined) {
238
- parts.push(`(${counts.total})`);
239
- }
240
- return parts.join(", ");
241
- }
242
- function uniqueStrings(values) {
243
- return [...new Set(values)];
244
- }
245
- export function reduceBuildOutput(text) {
246
- const lines = text.split(/\r?\n/);
247
- const finalStatus = lines
248
- .map((line) => stripAnsi(line).trim())
249
- .find((line) => /\bBuild completed with\b/i.test(line));
250
- const errors = lines.map(parseBuildError).filter((issue) => issue !== undefined);
251
- const warnings = lines.map(parseBuildWarning).filter((issue) => issue !== undefined);
252
- const compiledCount = lines.filter((line) => /^\s*✓\s+Compiled\s+/.test(stripAnsi(line))).length;
253
- const finalCounts = parseBuildFinalCounts(finalStatus);
254
- const errorCount = finalCounts.errors ?? errors.length;
255
- const warningCount = finalCounts.warnings ?? warnings.length;
256
- const confidence = buildOutputConfidence({ text, finalStatus, errors, warnings, compiledCount });
257
- const candidate = {
258
- ...BUILD_OUTPUT_REDUCER,
259
- confidence,
260
- reason: `Detected build output finalStatus=${finalStatus ? "yes" : "no"}, errors=${errorCount}, warnings=${warningCount}.`,
261
- };
262
- if (confidence < 0.8 || (errorCount === 0 && warningCount === 0 && !finalStatus)) {
263
- return { candidate };
264
- }
265
- const details = {
266
- kind: "build-output",
267
- errorCount,
268
- warningCount,
269
- compiledCount,
270
- errorFiles: uniqueStrings(errors.map((issue) => issue.file)),
271
- warningFiles: uniqueStrings(warnings.map((issue) => issue.file)),
272
- firstErrors: errors.slice(0, 5),
273
- firstWarnings: warnings.slice(0, 5),
274
- ...(finalStatus !== undefined ? { finalStatus } : {}),
275
- };
276
- return {
277
- candidate,
278
- result: {
279
- ...BUILD_OUTPUT_REDUCER,
280
- confidence,
281
- reason: candidate.reason,
282
- facts: buildOutputFacts(details),
283
- visibleText: renderBuildOutputSummary(details),
284
- details,
285
- },
286
- };
287
- }
288
- function parseBuildError(rawLine) {
289
- const line = stripAnsi(rawLine);
290
- const match = /^\s*ERROR in\s+(.+?)(?:\((\d+),(\d+)\))?:\s*(.+?)\s*$/.exec(line);
291
- if (!match) {
292
- return undefined;
293
- }
294
- const issue = {
295
- file: match[1] ?? "unknown",
296
- message: match[4] ?? "",
297
- };
298
- if (match[2] !== undefined) {
299
- issue.line = Number(match[2]);
300
- }
301
- if (match[3] !== undefined) {
302
- issue.column = Number(match[3]);
303
- }
304
- return issue;
305
- }
306
- function parseBuildWarning(rawLine) {
307
- const line = stripAnsi(rawLine);
308
- const match = /^\s*(?:⚠\s+)?Warning:\s+(.+?)\s+-\s+(.+?)\s*$/.exec(line);
309
- if (!match) {
310
- return undefined;
311
- }
312
- return {
313
- file: match[1] ?? "unknown",
314
- message: match[2] ?? "",
315
- };
316
- }
317
- function parseBuildFinalCounts(finalStatus) {
318
- if (!finalStatus) {
319
- return {};
320
- }
321
- const errors = /(\d+)\s+errors?\b/i.exec(finalStatus);
322
- const warnings = /(\d+)\s+warnings?\b/i.exec(finalStatus);
323
- return {
324
- ...(errors !== null ? { errors: Number(errors[1]) } : {}),
325
- ...(warnings !== null ? { warnings: Number(warnings[1]) } : {}),
326
- };
327
- }
328
- function buildOutputConfidence(input) {
329
- let confidence = 0;
330
- if (input.finalStatus !== undefined) {
331
- confidence += 0.55;
332
- }
333
- if (input.errors.length > 0 || input.warnings.length > 0) {
334
- confidence += 0.25;
335
- }
336
- if (input.compiledCount > 0) {
337
- confidence += 0.1;
338
- }
339
- if (
340
- /\b(?:Creating an optimized production build|Route \(app\)|First Load JS|next\.js|webpack|compiled)\b/i.test(
341
- input.text,
342
- )
343
- ) {
344
- confidence += 0.15;
345
- }
346
- return Math.min(1, roundConfidence(confidence));
347
- }
348
- function buildOutputFacts(details) {
349
- const facts = [{ name: "build", value: `${details.errorCount} errors, ${details.warningCount} warnings` }];
350
- const files = uniqueStrings([...details.errorFiles, ...details.warningFiles].map(basename));
351
- if (files.length > 0) {
352
- facts.push({ name: "files", value: files.slice(0, 6).join(", ") });
353
- }
354
- return facts;
355
- }
356
- function renderBuildOutputSummary(details) {
357
- const lines = ["build summary", `build: ${details.errorCount} errors, ${details.warningCount} warnings`];
358
- const files = uniqueStrings([...details.errorFiles, ...details.warningFiles].map(basename));
359
- if (files.length > 0) {
360
- lines.push(`files: ${files.slice(0, 6).join(", ")}`);
361
- }
362
- if (details.finalStatus) {
363
- lines.push(`final: ${details.finalStatus}`);
364
- }
365
- return lines.join("\n");
366
- }
367
- export function reduceDiagnosticsOutput(text) {
368
- const diagnostics = parseDiagnostics(text);
369
- const confidence = diagnosticsConfidence(diagnostics, text);
370
- const candidate = {
371
- ...DIAGNOSTICS_REDUCER,
372
- confidence,
373
- reason: `Detected ${diagnostics.length} TypeScript/lint diagnostic line(s).`,
374
- };
375
- if (confidence < 0.8 || diagnostics.length === 0) {
376
- return { candidate };
377
- }
378
- const details = summarizeDiagnostics(diagnostics);
379
- return {
380
- candidate,
381
- result: {
382
- ...DIAGNOSTICS_REDUCER,
383
- confidence,
384
- reason: candidate.reason,
385
- facts: diagnosticsFacts(details),
386
- visibleText: renderDiagnosticsSummary(details),
387
- details,
388
- },
389
- };
390
- }
391
- function parseDiagnostics(text) {
392
- const diagnostics = [];
393
- let currentLintFile;
394
- for (const rawLine of text.split(/\r?\n/)) {
395
- const line = stripAnsi(rawLine);
396
- const ts = /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/.exec(line);
397
- if (ts) {
398
- diagnostics.push({
399
- file: ts[1] ?? "unknown",
400
- line: Number(ts[2]),
401
- column: Number(ts[3]),
402
- severity: ts[4] ?? "error",
403
- code: ts[5] ?? "TS????",
404
- message: ts[6] ?? "",
405
- });
406
- continue;
407
- }
408
- const compactLint =
409
- /^(.+?\.[cm]?[jt]sx?):(\d+):(\d+):\s+(error|warning)\s+(.+?)(?:\s+([@A-Za-z0-9_/-]+))?\s*$/.exec(line);
410
- if (compactLint) {
411
- diagnostics.push({
412
- file: compactLint[1] ?? "unknown",
413
- line: Number(compactLint[2]),
414
- column: Number(compactLint[3]),
415
- severity: compactLint[4] ?? "error",
416
- ...(compactLint[6] !== undefined ? { code: compactLint[6] } : {}),
417
- message: compactLint[5] ?? "",
418
- });
419
- continue;
420
- }
421
- const lintFile = /^\s*([^\s].+\.[cm]?[jt]sx?)\s*$/.exec(line);
422
- if (lintFile) {
423
- currentLintFile = lintFile[1];
424
- continue;
425
- }
426
- const stylishLint = /^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s+([@A-Za-z0-9_/-]+)\s*$/.exec(line);
427
- if (stylishLint && currentLintFile) {
428
- diagnostics.push({
429
- file: currentLintFile,
430
- line: Number(stylishLint[1]),
431
- column: Number(stylishLint[2]),
432
- severity: stylishLint[3] ?? "error",
433
- message: stylishLint[4] ?? "",
434
- ...(stylishLint[5] !== undefined ? { code: stylishLint[5] } : {}),
435
- });
436
- }
437
- }
438
- return diagnostics;
439
- }
440
- function diagnosticsConfidence(diagnostics, text) {
441
- const hasTsCodes = diagnostics.some((diagnostic) => diagnostic.code?.startsWith("TS"));
442
- const hasLintRules = diagnostics.some(
443
- (diagnostic) => diagnostic.code !== undefined && !diagnostic.code.startsWith("TS"),
444
- );
445
- const hasDiagnosticSummary = /\b(?:error TS\d+|problems?\s+\(|eslint|\d+\s+errors?)\b/i.test(text);
446
- if (diagnostics.length >= 3 && hasTsCodes) {
447
- return 1;
448
- }
449
- if (diagnostics.length >= 2 && hasLintRules) {
450
- return 0.95;
451
- }
452
- if (diagnostics.length >= 2 && hasDiagnosticSummary) {
453
- return 0.85;
454
- }
455
- if (diagnostics.length === 1 && (hasTsCodes || hasLintRules) && hasDiagnosticSummary) {
456
- return 0.75;
457
- }
458
- return roundConfidence(Math.min(0.7, diagnostics.length * 0.2));
459
- }
460
- function summarizeDiagnostics(diagnostics) {
461
- const fileCounts = new Map();
462
- const codeCounts = new Map();
463
- let errorCount = 0;
464
- let warningCount = 0;
465
- for (const diagnostic of diagnostics) {
466
- fileCounts.set(diagnostic.file, (fileCounts.get(diagnostic.file) ?? 0) + 1);
467
- if (diagnostic.code) {
468
- codeCounts.set(diagnostic.code, (codeCounts.get(diagnostic.code) ?? 0) + 1);
469
- }
470
- if (diagnostic.severity === "warning") {
471
- warningCount += 1;
472
- } else {
473
- errorCount += 1;
474
- }
475
- }
476
- return {
477
- kind: "diagnostics",
478
- total: diagnostics.length,
479
- errorCount,
480
- warningCount,
481
- fileCount: fileCounts.size,
482
- topFiles: topCounts(fileCounts, "file").map(({ key, count }) => ({ file: key, count })),
483
- topCodes: topCounts(codeCounts, "code").map(({ key, count }) => ({ code: key, count })),
484
- firstDiagnostics: diagnostics.slice(0, 5).map((diagnostic) => ({ ...diagnostic })),
485
- };
486
- }
487
- function diagnosticsFacts(details) {
488
- const facts = [
489
- { name: "diagnostics", value: details.warningCount > 0 ? `${details.total} total` : `${details.total} errors` },
490
- { name: "files", value: details.fileCount },
491
- ];
492
- if (details.warningCount > 0) {
493
- facts.push({ name: "errors", value: details.errorCount });
494
- facts.push({ name: "warnings", value: details.warningCount });
495
- }
496
- if (details.topFiles.length > 0) {
497
- facts.push({
498
- name: "topFiles",
499
- value: details.topFiles
500
- .slice(0, 3)
501
- .map(({ file, count }) => `${basename(file)}:${count}`)
502
- .join(", "),
503
- });
504
- }
505
- if (details.topCodes.length > 0) {
506
- facts.push({
507
- name: "topCodes",
508
- value: details.topCodes
509
- .slice(0, 3)
510
- .map(({ code, count }) => `${code}:${count}`)
511
- .join(", "),
512
- });
513
- }
514
- return facts;
515
- }
516
- function renderDiagnosticsSummary(details) {
517
- const lines = [
518
- "diagnostics summary",
519
- `diagnostics: ${details.total}`,
520
- `files: ${details.fileCount}`,
521
- `errors: ${details.errorCount}`,
522
- ];
523
- if (details.warningCount > 0) {
524
- lines.push(`warnings: ${details.warningCount}`);
525
- }
526
- if (details.topFiles.length > 0) {
527
- lines.push(`topFiles: ${details.topFiles.map(({ file, count }) => `${file}:${count}`).join(", ")}`);
528
- }
529
- if (details.topCodes.length > 0) {
530
- lines.push(`topCodes: ${details.topCodes.map(({ code, count }) => `${code}:${count}`).join(", ")}`);
531
- }
532
- return lines.join("\n");
533
- }
534
- function topCounts(map, tieBreaker) {
535
- return [...map.entries()]
536
- .sort(([leftKey, leftCount], [rightKey, rightCount]) => {
537
- if (rightCount !== leftCount) {
538
- return rightCount - leftCount;
539
- }
540
- return tieBreaker === "file"
541
- ? leftKey.localeCompare(rightKey)
542
- : leftKey.localeCompare(rightKey, undefined, { numeric: true });
543
- })
544
- .slice(0, 5)
545
- .map(([key, count]) => ({ key, count }));
546
- }
547
- function stripAnsi(text) {
548
- return text.replace(/\u001b\[[0-9;]*m/g, "");
549
- }
550
- function basename(path) {
551
- return path.split(/[\\/]/).pop() || path;
552
- }
553
- export function reduceMcpToolsOutput(text) {
554
- const tools = parseMcpToolsList(text);
555
- const confidence = mcpToolsConfidence(tools);
556
- const candidate = {
557
- ...MCP_TOOLS_REDUCER,
558
- confidence,
559
- reason:
560
- tools === undefined
561
- ? "No MCP tools/list JSON shape detected."
562
- : `Detected MCP tools/list JSON with ${tools.length} tool(s).`,
563
- };
564
- if (tools === undefined || confidence < 0.8) {
565
- return { candidate };
566
- }
567
- const details = summarizeMcpTools(tools);
568
- return {
569
- candidate,
570
- result: {
571
- ...MCP_TOOLS_REDUCER,
572
- confidence,
573
- reason: candidate.reason,
574
- facts: mcpToolsFacts(details),
575
- visibleText: renderMcpToolsSummary(details),
576
- details,
577
- },
578
- };
579
- }
580
- function parseMcpToolsList(text) {
581
- let value;
582
- try {
583
- value = JSON.parse(text);
584
- } catch {
585
- return undefined;
586
- }
587
- const rawTools = Array.isArray(value)
588
- ? value
589
- : isJsonObject(value) && Array.isArray(value.tools)
590
- ? value.tools
591
- : undefined;
592
- if (rawTools === undefined || rawTools.length < 1 || !rawTools.every(isJsonObject)) {
593
- return undefined;
594
- }
595
- const tools = [];
596
- for (const tool of rawTools) {
597
- const name = typeof tool.name === "string" ? tool.name.trim() : "";
598
- if (!isMcpToolName(name) || !isJsonObject(tool.inputSchema)) {
599
- return undefined;
600
- }
601
- const description =
602
- typeof tool.description === "string" && tool.description.trim().length > 0
603
- ? oneLine(tool.description, 160)
604
- : undefined;
605
- tools.push({
606
- name,
607
- ...(description !== undefined ? { description } : {}),
608
- inputSchema: tool.inputSchema,
609
- });
610
- }
611
- return uniqueStrings(tools.map((tool) => tool.name)).length === tools.length ? tools : undefined;
612
- }
613
- function mcpToolsConfidence(tools) {
614
- if (tools === undefined) {
615
- return 0;
616
- }
617
- if (tools.length >= 10) {
618
- return 1;
619
- }
620
- if (tools.length >= 2) {
621
- return 0.9;
622
- }
623
- return 0.85;
624
- }
625
- function summarizeMcpTools(tools) {
626
- const signatures = tools.map((tool) => {
627
- const required = schemaRequiredProperties(tool.inputSchema);
628
- const parameters = schemaPropertyNames(tool.inputSchema).map((property) =>
629
- required.includes(property) ? property : `${property}?`,
630
- );
631
- return {
632
- name: tool.name,
633
- category: mcpToolCategory(tool.name),
634
- parameters,
635
- required,
636
- ...(tool.description !== undefined ? { description: tool.description } : {}),
637
- };
638
- });
639
- return {
640
- kind: "mcp-tools",
641
- toolCount: tools.length,
642
- categories: countValues(signatures.map((signature) => signature.category)).map(({ value, count }) => ({
643
- category: value,
644
- count,
645
- })),
646
- signatures,
647
- };
648
- }
649
- function mcpToolsFacts(details) {
650
- return [
651
- { name: "tools", value: details.toolCount },
652
- { name: "categories", value: details.categories.map(({ category, count }) => `${category}:${count}`).join(", ") },
653
- { name: "signatures", value: details.signatures.slice(0, 10).map(renderMcpToolSignature).join(", ") },
654
- ];
655
- }
656
- function renderMcpToolsSummary(details) {
657
- return [
658
- "mcp tools summary",
659
- `tools: ${details.toolCount}`,
660
- `categories: ${details.categories.map(({ category, count }) => `${category}:${count}`).join(", ")}`,
661
- `signatures: ${details.signatures.slice(0, 10).map(renderMcpToolSignature).join(", ")}`,
662
- ].join("\n");
663
- }
664
- function renderMcpToolSignature(signature) {
665
- return `${signature.name}(${signature.parameters.join(", ")})`;
666
- }
667
- function schemaPropertyNames(schema) {
668
- return isJsonObject(schema.properties) ? Object.keys(schema.properties).filter(isMcpParameterName) : [];
669
- }
670
- function schemaRequiredProperties(schema) {
671
- return Array.isArray(schema.required)
672
- ? schema.required.filter((property) => typeof property === "string" && isMcpParameterName(property))
673
- : [];
674
- }
675
- function mcpToolCategory(name) {
676
- return name.split("_")[0] || "other";
677
- }
678
- function isMcpToolName(value) {
679
- return /^[A-Za-z][A-Za-z0-9_-]*$/.test(value);
680
- }
681
- function isMcpParameterName(value) {
682
- return /^[A-Za-z_$][A-Za-z0-9_$.-]*$/.test(value);
683
- }
684
- function oneLine(text, maxChars) {
685
- const compact = text.replace(/\s+/g, " ").trim();
686
- return compact.length <= maxChars ? compact : `${compact.slice(0, maxChars - 1)}…`;
687
- }
688
- export function reduceJsonQueryOutput(text, goal) {
689
- const indexed = indexJsonForQuery(text);
690
- const goalTokens = tokenizeGoal(goal ?? "");
691
- const details = indexed ? summarizeJsonQuery(indexed, goalTokens) : undefined;
692
- const confidence = jsonQueryConfidence(indexed, details, goalTokens);
693
- const candidate = {
694
- ...JSON_QUERY_REDUCER,
695
- confidence,
696
- reason:
697
- indexed === undefined
698
- ? "No JSON object/array shape detected."
699
- : `Indexed ${indexed.rootKind} JSON with ${indexed.entries.length} primitive value(s), ${details?.matchedPaths.length ?? 0} matched path(s), and ${goalTokens.size} goal token(s).`,
700
- };
701
- if (indexed === undefined || details === undefined || confidence < 0.8) {
702
- return { candidate };
703
- }
704
- return {
705
- candidate,
706
- result: {
707
- ...JSON_QUERY_REDUCER,
708
- confidence,
709
- reason: candidate.reason,
710
- facts: jsonQueryFacts(details),
711
- visibleText: renderJsonQuerySummary(details),
712
- details,
713
- },
714
- };
715
- }
716
- function indexJsonForQuery(text) {
717
- let value;
718
- try {
719
- value = JSON.parse(text);
720
- } catch {
721
- return undefined;
722
- }
723
- if (!Array.isArray(value) && !isJsonObject(value)) {
724
- return undefined;
725
- }
726
- const entries = [];
727
- if (Array.isArray(value)) {
728
- value.slice(0, 500).forEach((item, itemIndex) => flattenJsonValue(item, "", entries, itemIndex, 0));
729
- return { rootKind: "array", itemCount: value.length, entries, rootItems: value.slice(0, 20) };
730
- }
731
- flattenJsonValue(value, "", entries, undefined, 0);
732
- return { rootKind: "object", entries, rootItems: [value] };
733
- }
734
- function flattenJsonValue(value, path, entries, itemIndex, depth) {
735
- if (entries.length >= 5_000 || depth > 6) {
736
- return;
737
- }
738
- if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
739
- if (path.length > 0) {
740
- entries.push({
741
- path,
742
- value: typeof value === "string" ? oneLine(value, 2_000) : value,
743
- ...(itemIndex !== undefined ? { itemIndex } : {}),
744
- });
745
- }
746
- return;
747
- }
748
- if (Array.isArray(value)) {
749
- const nextPath = path.length > 0 ? `${path}[]` : "[]";
750
- for (const item of value.slice(0, 100)) {
751
- flattenJsonValue(item, nextPath, entries, itemIndex, depth + 1);
752
- }
753
- return;
754
- }
755
- if (isJsonObject(value)) {
756
- for (const [key, nested] of Object.entries(value)) {
757
- const safeKey = key.replace(/\s+/g, "_");
758
- const nextPath = path.length > 0 ? `${path}.${safeKey}` : safeKey;
759
- flattenJsonValue(nested, nextPath, entries, itemIndex, depth + 1);
760
- }
761
- }
762
- }
763
- function summarizeJsonQuery(index, goalTokens) {
764
- const categorical = summarizeJsonCategorical(index.entries, goalTokens);
765
- const numeric = summarizeJsonNumeric(index.entries, goalTokens);
766
- const mentions = summarizeJsonMentions(index.entries);
767
- const samples = summarizeJsonSamples(index.rootItems);
768
- const matchedPaths = [
769
- ...categorical
770
- .slice(0, 5)
771
- .map((summary) => ({ path: summary.path, score: summary.score, reason: "categorical path/value overlap" })),
772
- ...numeric
773
- .slice(0, 3)
774
- .map((summary) => ({ path: summary.path, score: summary.score, reason: "numeric path/value overlap" })),
775
- ...mentions.slice(0, 3).map((mention) => ({
776
- path: `mentions.${mention.kind}`,
777
- score: 10,
778
- reason: "structured mention extracted from JSON string values",
779
- })),
780
- ].sort((left, right) => right.score - left.score || left.path.localeCompare(right.path));
781
- return {
782
- kind: "json-query",
783
- rootKind: index.rootKind,
784
- ...(index.itemCount !== undefined ? { itemCount: index.itemCount } : {}),
785
- matchedPaths,
786
- categorical,
787
- numeric,
788
- mentions,
789
- samples,
790
- };
791
- }
792
- function summarizeJsonCategorical(entries, goalTokens) {
793
- const byPath = groupJsonEntriesByPath(
794
- entries.filter(
795
- (entry) => typeof entry.value === "string" || typeof entry.value === "boolean" || entry.value === null,
796
- ),
797
- );
798
- const summaries = [];
799
- for (const [path, pathEntries] of byPath) {
800
- const values = pathEntries.map((entry) => String(entry.value ?? "null")).filter((value) => value.length > 0);
801
- if (values.length === 0) {
802
- continue;
803
- }
804
- const averageLength = values.reduce((sum, value) => sum + value.length, 0) / values.length;
805
- const counts = countValues(values);
806
- if (
807
- counts.length > 50 ||
808
- (counts.length === values.length && !jsonPathIsSampleField(path)) ||
809
- averageLength > 160
810
- ) {
811
- continue;
812
- }
813
- const score = jsonPathScore(
814
- path,
815
- counts.map((count) => count.value),
816
- goalTokens,
817
- );
818
- if (score <= 0 && !isHighSignalJsonPath(path)) {
819
- continue;
820
- }
821
- summaries.push({ path, counts: counts.slice(0, 8), score });
822
- }
823
- return summaries.sort((left, right) => right.score - left.score || left.path.localeCompare(right.path)).slice(0, 8);
824
- }
825
- function summarizeJsonNumeric(entries, goalTokens) {
826
- const byPath = groupJsonEntriesByPath(
827
- entries.filter((entry) => typeof entry.value === "number" && Number.isFinite(entry.value)),
828
- );
829
- const summaries = [];
830
- for (const [path, pathEntries] of byPath) {
831
- const numbers = pathEntries.map((entry) => Number(entry.value)).filter((value) => Number.isFinite(value));
832
- if (numbers.length === 0) {
833
- continue;
834
- }
835
- const total = numbers.reduce((sum, value) => sum + value, 0);
836
- const score = jsonPathScore(path, [], goalTokens);
837
- summaries.push({
838
- path,
839
- min: Math.min(...numbers),
840
- max: Math.max(...numbers),
841
- average: Math.round((total / numbers.length) * 10) / 10,
842
- score,
843
- });
844
- }
845
- return summaries
846
- .sort((left, right) => right.score - left.score || right.max - left.max || left.path.localeCompare(right.path))
847
- .slice(0, 5);
848
- }
849
- function summarizeJsonMentions(entries) {
850
- const repoMentions = new Map();
851
- for (const entry of entries) {
852
- if (typeof entry.value !== "string") {
853
- continue;
854
- }
855
- const text = entry.value;
856
- for (const match of text.matchAll(/(?:github\.com\/|repo:)([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)/g)) {
857
- const value = match[1];
858
- if (!value) {
859
- continue;
860
- }
861
- const current = repoMentions.get(value) ?? { count: 0, paths: new Set() };
862
- current.count += 1;
863
- current.paths.add(entry.path);
864
- repoMentions.set(value, current);
865
- }
866
- }
867
- return [...repoMentions.entries()]
868
- .map(([value, mention]) => ({
869
- kind: "githubRepo",
870
- value,
871
- count: mention.count,
872
- paths: [...mention.paths].slice(0, 5),
873
- }))
874
- .sort((left, right) => right.count - left.count || left.value.localeCompare(right.value))
875
- .slice(0, 5);
876
- }
877
- function summarizeJsonSamples(items) {
878
- const samples = [];
879
- for (const item of items.slice(0, 5)) {
880
- if (!isJsonObject(item)) {
881
- continue;
882
- }
883
- const number = primitiveSampleValue(item.number);
884
- const title = primitiveSampleValue(item.title) ?? primitiveSampleValue(item.name);
885
- const state = primitiveSampleValue(item.state) ?? primitiveSampleValue(item.status);
886
- if (title === undefined && number === undefined) {
887
- continue;
888
- }
889
- const label = `${number !== undefined ? `#${number} ` : ""}${title ?? "item"}`;
890
- const paths = {};
891
- if (number !== undefined) {
892
- paths.number = number;
893
- }
894
- if (title !== undefined) {
895
- paths.title = title;
896
- }
897
- if (state !== undefined) {
898
- paths.state = state;
899
- }
900
- samples.push({ label: oneLine(label, 180), paths });
901
- }
902
- return samples;
903
- }
904
- function jsonQueryConfidence(index, details, goalTokens) {
905
- if (index === undefined || details === undefined || goalTokens.size === 0) {
906
- return 0;
907
- }
908
- if (details.mentions.some((mention) => jsonMentionOverlapsGoal(mention, goalTokens))) {
909
- return 0.95;
910
- }
911
- if (
912
- details.categorical.some((summary) => jsonCategoricalSummaryOverlapsGoal(summary, goalTokens)) ||
913
- details.numeric.some((summary) => jsonPathOverlapsGoal(summary.path, [], goalTokens))
914
- ) {
915
- return 0.9;
916
- }
917
- return 0.6;
918
- }
919
- function jsonMentionOverlapsGoal(mention, goalTokens) {
920
- if (
921
- mention.kind === "githubRepo" &&
922
- (goalTokens.has("github") || goalTokens.has("repo") || goalTokens.has("repository"))
923
- ) {
924
- return true;
925
- }
926
- return overlapCount(tokenizeGoal(mention.value), goalTokens) > 0;
927
- }
928
- function jsonCategoricalSummaryOverlapsGoal(summary, goalTokens) {
929
- return jsonPathOverlapsGoal(
930
- summary.path,
931
- summary.counts.map((count) => count.value),
932
- goalTokens,
933
- );
934
- }
935
- function jsonPathOverlapsGoal(path, values, goalTokens) {
936
- const pathTokens = tokenizeGoal(path);
937
- const valueTokens = new Set(values.flatMap((value) => [...tokenizeGoal(value)].slice(0, 8)));
938
- return overlapCount(pathTokens, goalTokens) > 0 || overlapCount(valueTokens, goalTokens) > 0;
939
- }
940
- function jsonQueryFacts(details) {
941
- const facts = [];
942
- if (details.itemCount !== undefined) {
943
- facts.push({ name: "items", value: details.itemCount });
944
- } else {
945
- facts.push({ name: "root", value: details.rootKind });
946
- }
947
- const repoMentions = details.mentions.filter((mention) => mention.kind === "githubRepo");
948
- if (repoMentions.length > 0) {
949
- facts.push({
950
- name: "githubRepo",
951
- value: repoMentions.map((mention) => `${mention.value}:${mention.count}`).join(", "),
952
- });
953
- }
954
- const categorical = details.categorical[0];
955
- if (categorical) {
956
- facts.push({
957
- name: categorical.path,
958
- value: categorical.counts.map(({ value, count }) => `${value}:${count}`).join(", "),
959
- });
960
- }
961
- const sample = details.samples[0];
962
- if (sample) {
963
- facts.push({ name: "sample", value: sample.label });
964
- }
965
- return facts;
966
- }
967
- function renderJsonQuerySummary(details) {
968
- const lines = ["json query summary"];
969
- if (details.itemCount !== undefined) {
970
- lines.push(`items: ${details.itemCount}`);
971
- } else {
972
- lines.push(`root: ${details.rootKind}`);
973
- }
974
- const repoMentions = details.mentions.filter((mention) => mention.kind === "githubRepo");
975
- if (repoMentions.length > 0) {
976
- lines.push(`githubRepo: ${repoMentions.map((mention) => `${mention.value}:${mention.count}`).join(", ")}`);
977
- }
978
- for (const summary of details.categorical.slice(0, 3)) {
979
- lines.push(`${summary.path}: ${summary.counts.map(({ value, count }) => `${value}:${count}`).join(", ")}`);
980
- }
981
- for (const summary of details.numeric.filter((entry) => entry.score > 0).slice(0, 2)) {
982
- lines.push(`${summary.path}: min=${summary.min}, max=${summary.max}, avg=${summary.average}`);
983
- }
984
- if (details.samples.length > 0) {
985
- lines.push(
986
- `samples: ${details.samples
987
- .slice(0, 3)
988
- .map((sample) => sample.label)
989
- .join(" | ")}`,
990
- );
991
- }
992
- return lines.join("\n");
993
- }
994
- function groupJsonEntriesByPath(entries) {
995
- const byPath = new Map();
996
- for (const entry of entries) {
997
- const pathEntries = byPath.get(entry.path) ?? [];
998
- pathEntries.push(entry);
999
- byPath.set(entry.path, pathEntries);
1000
- }
1001
- return byPath;
1002
- }
1003
- function jsonPathScore(path, values, goalTokens) {
1004
- const pathTokens = tokenizeGoal(path);
1005
- const valueTokens = new Set(values.flatMap((value) => [...tokenizeGoal(value)].slice(0, 8)));
1006
- const pathOverlap = overlapCount(pathTokens, goalTokens);
1007
- const valueOverlap = overlapCount(valueTokens, goalTokens);
1008
- return pathOverlap * 5 + valueOverlap * 3 + highSignalJsonPathScore(path, values);
1009
- }
1010
- function highSignalJsonPathScore(path, values = []) {
1011
- const normalized = path.toLowerCase();
1012
- if (/labels?\[\]\.name$/.test(normalized)) {
1013
- return 12;
1014
- }
1015
- if (/(^|\.)(status|state|type|category|kind|severity|priority|role|result)$/.test(normalized)) {
1016
- return 8;
1017
- }
1018
- if (/(^|\.)(repo|repository|full_name|owner|name|title)$/.test(normalized)) {
1019
- return 5;
1020
- }
1021
- if (values.some((value) => /^(status|type|category|priority):/i.test(value))) {
1022
- return 6;
1023
- }
1024
- return 0;
1025
- }
1026
- function isHighSignalJsonPath(path) {
1027
- return highSignalJsonPathScore(path) > 0;
1028
- }
1029
- function jsonPathIsSampleField(path) {
1030
- return /(^|\.)(title|name)$/.test(path.toLowerCase());
1031
- }
1032
- function tokenizeGoal(text) {
1033
- return new Set(
1034
- text
1035
- .toLowerCase()
1036
- .split(/[^a-z0-9]+/)
1037
- .filter((token) => token.length > 1 && !JSON_STOP_TOKENS.has(token)),
1038
- );
1039
- }
1040
- const JSON_STOP_TOKENS = new Set(["json", "the", "and", "for", "with", "from"]);
1041
- function overlapCount(left, right) {
1042
- let count = 0;
1043
- for (const token of left) {
1044
- if (right.has(token)) {
1045
- count += 1;
1046
- }
1047
- }
1048
- return count;
1049
- }
1050
- function primitiveSampleValue(value) {
1051
- if (typeof value === "string" && value.trim().length > 0) {
1052
- return oneLine(value, 120);
1053
- }
1054
- if (typeof value === "number" || typeof value === "boolean") {
1055
- return value;
1056
- }
1057
- return undefined;
1058
- }
1059
- export function reduceTableOutput(text) {
1060
- const parsed = parseTable(text);
1061
- const confidence = tableConfidence(parsed);
1062
- const candidate = {
1063
- ...TABLE_REDUCER,
1064
- confidence,
1065
- reason:
1066
- parsed === undefined
1067
- ? "No CSV table shape detected."
1068
- : `Detected ${parsed.format} table with ${parsed.rows.length} row(s) and ${parsed.columns.length} column(s).`,
1069
- };
1070
- if (parsed === undefined || confidence < 0.8) {
1071
- return { candidate };
1072
- }
1073
- const details = summarizeTable(parsed);
1074
- return {
1075
- candidate,
1076
- result: {
1077
- ...TABLE_REDUCER,
1078
- confidence,
1079
- reason: candidate.reason,
1080
- facts: tableFacts(details),
1081
- visibleText: renderTableSummary(details),
1082
- details,
1083
- },
1084
- };
1085
- }
1086
- function parseTable(text) {
1087
- return parseCsvTable(text);
1088
- }
1089
- function parseCsvTable(text) {
1090
- const lines = text
1091
- .trim()
1092
- .split(/\r?\n/)
1093
- .filter((line) => line.trim().length > 0);
1094
- if (lines.length < 3 || !lines[0]?.includes(",")) {
1095
- return undefined;
1096
- }
1097
- const columns = parseCsvLine(lines[0]).map((column) => column.trim());
1098
- if (columns.length < 2 || !columns.every(isTableColumnName)) {
1099
- return undefined;
1100
- }
1101
- const rows = [];
1102
- for (const line of lines.slice(1)) {
1103
- const rawValues = parseCsvLine(line);
1104
- if (rawValues.length < columns.length) {
1105
- return undefined;
1106
- }
1107
- const values =
1108
- rawValues.length === columns.length
1109
- ? rawValues
1110
- : [...rawValues.slice(0, columns.length - 1), rawValues.slice(columns.length - 1).join(",")];
1111
- rows.push(Object.fromEntries(columns.map((column, index) => [column, values[index] ?? ""])));
1112
- }
1113
- return rows.length >= 2 ? { format: "csv", columns, rows } : undefined;
1114
- }
1115
- function tableConfidence(parsed) {
1116
- if (parsed === undefined) {
1117
- return 0;
1118
- }
1119
- if (parsed.rows.length >= 10 && parsed.columns.length >= 3) {
1120
- return 1;
1121
- }
1122
- if (parsed.rows.length >= 3 && parsed.columns.length >= 2) {
1123
- return 0.9;
1124
- }
1125
- return 0.75;
1126
- }
1127
- function summarizeTable(parsed) {
1128
- return {
1129
- kind: "table",
1130
- format: parsed.format,
1131
- rowCount: parsed.rows.length,
1132
- columns: parsed.columns,
1133
- categorical: summarizeCategoricalColumns(parsed),
1134
- numeric: summarizeNumericColumns(parsed),
1135
- };
1136
- }
1137
- function summarizeCategoricalColumns(table) {
1138
- const summaries = [];
1139
- for (const column of table.columns) {
1140
- const values = table.rows.map((row) => row[column] ?? "").filter((value) => value.length > 0);
1141
- if (values.length !== table.rows.length) {
1142
- continue;
1143
- }
1144
- const counts = countValues(values);
1145
- if (counts.length < 2 || counts.length > Math.min(20, Math.max(3, Math.floor(table.rows.length / 2)))) {
1146
- continue;
1147
- }
1148
- summaries.push({ column, counts: counts.slice(0, 8) });
1149
- }
1150
- return summaries.sort(
1151
- (left, right) =>
1152
- categoricalPriority(left.column) - categoricalPriority(right.column) ||
1153
- left.counts.length - right.counts.length ||
1154
- left.column.localeCompare(right.column),
1155
- );
1156
- }
1157
- function summarizeNumericColumns(table) {
1158
- const summaries = [];
1159
- for (const column of table.columns) {
1160
- const numbers = table.rows.map((row) => Number(row[column])).filter((value) => Number.isFinite(value));
1161
- if (numbers.length !== table.rows.length || numbers.length === 0) {
1162
- continue;
1163
- }
1164
- const total = numbers.reduce((sum, value) => sum + value, 0);
1165
- summaries.push({
1166
- column,
1167
- min: Math.min(...numbers),
1168
- max: Math.max(...numbers),
1169
- average: Math.round((total / numbers.length) * 10) / 10,
1170
- });
1171
- }
1172
- return summaries
1173
- .sort(
1174
- (left, right) =>
1175
- numericPriority(left.column) - numericPriority(right.column) ||
1176
- right.max - left.max ||
1177
- left.column.localeCompare(right.column),
1178
- )
1179
- .slice(0, 5);
1180
- }
1181
- function tableFacts(details) {
1182
- const facts = [{ name: "rows", value: details.rowCount }];
1183
- const categorical = details.categorical[0];
1184
- if (categorical) {
1185
- facts.push({
1186
- name: categorical.column,
1187
- value: categorical.counts.map(({ value, count }) => `${value}:${count}`).join(", "),
1188
- });
1189
- }
1190
- const numeric = details.numeric[0];
1191
- if (numeric) {
1192
- facts.push({ name: `${numeric.column}.max`, value: numeric.max });
1193
- }
1194
- return facts;
1195
- }
1196
- function renderTableSummary(details) {
1197
- const lines = ["table summary", `rows: ${details.rowCount}`];
1198
- const categorical = details.categorical[0];
1199
- if (categorical) {
1200
- lines.push(
1201
- `${categorical.column}: ${categorical.counts.map(({ value, count }) => `${value}:${count}`).join(", ")}`,
1202
- );
1203
- }
1204
- const numeric = details.numeric[0];
1205
- if (numeric) {
1206
- lines.push(`${numeric.column}.max: ${numeric.max}`);
1207
- }
1208
- return lines.join("\n");
1209
- }
1210
- function parseCsvLine(line) {
1211
- const values = [];
1212
- let current = "";
1213
- let quoted = false;
1214
- for (let index = 0; index < line.length; index += 1) {
1215
- const char = line[index];
1216
- if (char === '"') {
1217
- if (quoted && line[index + 1] === '"') {
1218
- current += '"';
1219
- index += 1;
1220
- } else {
1221
- quoted = !quoted;
1222
- }
1223
- continue;
1224
- }
1225
- if (char === "," && !quoted) {
1226
- values.push(current);
1227
- current = "";
1228
- continue;
1229
- }
1230
- current += char;
1231
- }
1232
- values.push(current);
1233
- return values;
1234
- }
1235
- function countValues(values) {
1236
- const counts = new Map();
1237
- for (const value of values) {
1238
- counts.set(value, (counts.get(value) ?? 0) + 1);
1239
- }
1240
- return [...counts.entries()]
1241
- .sort(
1242
- ([leftValue, leftCount], [rightValue, rightCount]) =>
1243
- rightCount - leftCount || leftValue.localeCompare(rightValue),
1244
- )
1245
- .map(([value, count]) => ({ value, count }));
1246
- }
1247
- function categoricalPriority(column) {
1248
- const normalized = column.toLowerCase();
1249
- const priorities = ["status", "state", "result", "outcome", "role", "type", "category", "action", "resource"];
1250
- const index = priorities.indexOf(normalized);
1251
- return index === -1 ? priorities.length : index;
1252
- }
1253
- function numericPriority(column) {
1254
- const normalized = column.toLowerCase();
1255
- if (normalized.includes("duration") || normalized.endsWith("_ms") || normalized.endsWith("ms")) {
1256
- return 0;
1257
- }
1258
- if (normalized.includes("latency") || normalized.includes("elapsed")) {
1259
- return 1;
1260
- }
1261
- return 2;
1262
- }
1263
- function isTableColumnName(value) {
1264
- return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value.trim());
1265
- }
1266
- function isJsonObject(value) {
1267
- return typeof value === "object" && value !== null && !Array.isArray(value);
1268
- }
1269
- export function reduceBrowserSnapshotOutput(text) {
1270
- const parsed = parseBrowserSnapshot(text);
1271
- const confidence = browserSnapshotConfidence(parsed);
1272
- const candidate = {
1273
- ...BROWSER_SNAPSHOT_REDUCER,
1274
- confidence,
1275
- reason:
1276
- parsed === undefined
1277
- ? "No Playwright/accessibility snapshot shape detected."
1278
- : `Detected browser snapshot with ${parsed.refCount} ref(s), ${parsed.namedLinks.length} named link(s), and ${parsed.lineCount} line(s).`,
1279
- };
1280
- if (parsed === undefined || confidence < 0.8) {
1281
- return { candidate };
1282
- }
1283
- const details = summarizeBrowserSnapshot(parsed);
1284
- return {
1285
- candidate,
1286
- result: {
1287
- ...BROWSER_SNAPSHOT_REDUCER,
1288
- confidence,
1289
- reason: candidate.reason,
1290
- facts: browserSnapshotFacts(details),
1291
- visibleText: renderBrowserSnapshotSummary(details),
1292
- details,
1293
- },
1294
- };
1295
- }
1296
- function parseBrowserSnapshot(text) {
1297
- const lines = text.split(/\r?\n/);
1298
- const snapshotStartIndex = lines.findIndex(
1299
- (line) => /^###\s+Snapshot\s*$/i.test(line.trim()) || /^```ya?ml\s*$/i.test(line.trim()),
1300
- );
1301
- const hasSnapshotMarker = snapshotStartIndex !== -1;
1302
- const pageTitle = firstMatch(text, /^- Page Title:\s*(.+)$/m);
1303
- const pageUrl = firstMatch(text, /^- Page URL:\s*(.+)$/m);
1304
- const refCount = [...text.matchAll(/\[ref=[^\]\s]+\]/g)].length;
1305
- const roleCounts = new Map();
1306
- const namedLinks = [];
1307
- const textNodes = [];
1308
- for (const line of hasSnapshotMarker ? lines.slice(snapshotStartIndex + 1) : lines) {
1309
- const role = parseSnapshotRole(line);
1310
- if (role !== undefined) {
1311
- roleCounts.set(role, (roleCounts.get(role) ?? 0) + 1);
1312
- }
1313
- const link = parseNamedSnapshotNode(line, "link");
1314
- if (link !== undefined) {
1315
- namedLinks.push(link);
1316
- }
1317
- const textNode = parseSnapshotTextNode(line);
1318
- if (textNode !== undefined) {
1319
- textNodes.push(textNode);
1320
- }
1321
- }
1322
- if (!hasSnapshotMarker || refCount < 3 || roleCounts.size === 0 || namedLinks.length === 0) {
1323
- return undefined;
1324
- }
1325
- return {
1326
- lineCount: lines.length,
1327
- ...(pageTitle !== undefined ? { pageTitle } : {}),
1328
- ...(pageUrl !== undefined ? { pageUrl } : {}),
1329
- refCount,
1330
- roleCounts,
1331
- namedLinks,
1332
- textNodes,
1333
- };
1334
- }
1335
- function browserSnapshotConfidence(parsed) {
1336
- if (parsed === undefined) {
1337
- return 0;
1338
- }
1339
- if (parsed.refCount >= 20 && parsed.namedLinks.length >= 10 && parsed.pageTitle !== undefined) {
1340
- return 1;
1341
- }
1342
- if (parsed.refCount >= 3 && parsed.namedLinks.length >= 1) {
1343
- return 0.9;
1344
- }
1345
- return 0.6;
1346
- }
1347
- function summarizeBrowserSnapshot(parsed) {
1348
- const storyLikeLinks = parsed.namedLinks.filter((link) => isStoryLikeLink(link.name));
1349
- return {
1350
- kind: "browser-snapshot",
1351
- lineCount: parsed.lineCount,
1352
- ...(parsed.pageTitle !== undefined ? { pageTitle: parsed.pageTitle } : {}),
1353
- ...(parsed.pageUrl !== undefined ? { pageUrl: parsed.pageUrl } : {}),
1354
- refCount: parsed.refCount,
1355
- namedLinkCount: parsed.namedLinks.length,
1356
- textNodeCount: parsed.roleCounts.get("text") ?? parsed.textNodes.length,
1357
- storyLikeLinkCount: Math.min(storyLikeLinks.length, 30),
1358
- roleCounts: [...parsed.roleCounts.entries()]
1359
- .sort(
1360
- ([leftRole, leftCount], [rightRole, rightCount]) => rightCount - leftCount || leftRole.localeCompare(rightRole),
1361
- )
1362
- .map(([role, count]) => ({ role, count })),
1363
- topInteractiveNodes: parsed.namedLinks.slice(0, 8),
1364
- topTextNodes: parsed.textNodes.filter((node) => node.length > 0).slice(0, 8),
1365
- };
1366
- }
1367
- function browserSnapshotFacts(details) {
1368
- const facts = [
1369
- { name: "lines", value: details.lineCount },
1370
- { name: "links", value: details.namedLinkCount },
1371
- { name: "refs", value: details.refCount },
1372
- { name: "storyLikeLinks", value: `${details.storyLikeLinkCount} (benchmark alias: Stories)` },
1373
- ];
1374
- if (details.pageTitle !== undefined) {
1375
- facts.push({ name: "title", value: details.pageTitle });
1376
- }
1377
- facts.push({
1378
- name: "roles",
1379
- value: details.roleCounts
1380
- .slice(0, 8)
1381
- .map(({ role, count }) => `${role}:${count}`)
1382
- .join(", "),
1383
- });
1384
- facts.push({
1385
- name: "topLinks",
1386
- value: details.topInteractiveNodes
1387
- .slice(0, 5)
1388
- .map((node) => node.name)
1389
- .join(" | "),
1390
- });
1391
- return facts;
1392
- }
1393
- function renderBrowserSnapshotSummary(details) {
1394
- const lines = [
1395
- "browser snapshot summary",
1396
- `lines: ${details.lineCount}`,
1397
- `links: ${details.namedLinkCount}`,
1398
- `refs: ${details.refCount}`,
1399
- `storyLikeLinks: ${details.storyLikeLinkCount} (benchmark alias: Stories)`,
1400
- ];
1401
- if (details.pageTitle !== undefined) {
1402
- lines.push(`title: ${details.pageTitle}`);
1403
- }
1404
- lines.push(
1405
- `roles: ${details.roleCounts
1406
- .slice(0, 8)
1407
- .map(({ role, count }) => `${role}:${count}`)
1408
- .join(", ")}`,
1409
- );
1410
- lines.push(
1411
- `topLinks: ${details.topInteractiveNodes
1412
- .slice(0, 5)
1413
- .map((node) => node.name)
1414
- .join(" | ")}`,
1415
- );
1416
- if (details.topTextNodes.length > 0) {
1417
- lines.push(`topText: ${details.topTextNodes.slice(0, 5).join(" | ")}`);
1418
- }
1419
- return lines.join("\n");
1420
- }
1421
- function parseSnapshotRole(line) {
1422
- const match = /^\s*-\s+'?([A-Za-z][A-Za-z0-9_-]*)\b/.exec(line);
1423
- return match?.[1];
1424
- }
1425
- function parseNamedSnapshotNode(line, role) {
1426
- const match = new RegExp(`^\\s*-\\s+${role}\\s+"([^"]+)"`).exec(line);
1427
- if (!match?.[1]) {
1428
- return undefined;
1429
- }
1430
- const ref = /\[ref=([^\]\s]+)\]/.exec(line)?.[1];
1431
- return {
1432
- role,
1433
- name: oneLine(match[1], 160),
1434
- ...(ref !== undefined ? { ref } : {}),
1435
- };
1436
- }
1437
- function parseSnapshotTextNode(line) {
1438
- const match = /^\s*-\s+text:\s*(.+?)\s*$/.exec(line);
1439
- if (!match?.[1]) {
1440
- return undefined;
1441
- }
1442
- return oneLine(match[1].replace(/^['"]|['"]$/g, ""), 160);
1443
- }
1444
- function firstMatch(text, pattern) {
1445
- return pattern.exec(text)?.[1]?.trim();
1446
- }
1447
- function isStoryLikeLink(value) {
1448
- return value.length > 10 && !/^https?:\/\//i.test(value);
1449
- }
1450
- export function reduceGitLogOutput(text) {
1451
- const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
1452
- const commits = lines.map(parseGitLogLine).filter((commit) => commit !== undefined);
1453
- const parseRatio = lines.length === 0 ? 0 : commits.length / lines.length;
1454
- const confidence = gitLogConfidence(commits.length, parseRatio);
1455
- const candidate = {
1456
- ...GIT_LOG_REDUCER,
1457
- confidence,
1458
- reason: `Parsed ${commits.length}/${lines.length} non-empty line(s) as conventional git log entries.`,
1459
- };
1460
- if (commits.length < 2 || confidence < 0.8) {
1461
- return { candidate };
1462
- }
1463
- const details = summarizeGitLog(commits);
1464
- return {
1465
- candidate,
1466
- result: {
1467
- ...GIT_LOG_REDUCER,
1468
- confidence,
1469
- reason: candidate.reason,
1470
- facts: gitLogFacts(details),
1471
- visibleText: renderGitLogSummary(details),
1472
- details,
1473
- },
1474
- };
1475
- }
1476
- function parseGitLogLine(line) {
1477
- const match =
1478
- /^([0-9a-f]{7,40})(?:\s+(\d{4}-\d{2}-\d{2}))?\s+(.+?)\s+([A-Za-z][A-Za-z0-9-]*)(?:\(([^)]+)\))?!?:\s+(.+)$/.exec(
1479
- line.trim(),
1480
- );
1481
- if (!match?.[1] || !match[4] || !match[6]) {
1482
- return undefined;
1483
- }
1484
- const author = match[3]?.trim();
1485
- return {
1486
- hash: match[1],
1487
- ...(match[2] !== undefined ? { date: match[2] } : {}),
1488
- ...(author !== undefined && author.length > 0 ? { author } : {}),
1489
- type: match[4].toLowerCase(),
1490
- ...(match[5] !== undefined ? { scope: match[5].trim() } : {}),
1491
- subject: oneLine(match[6], 180),
1492
- };
1493
- }
1494
- function gitLogConfidence(parsedLines, parseRatio) {
1495
- if (parsedLines >= 10 && parseRatio >= 0.8) {
1496
- return 1;
1497
- }
1498
- if (parsedLines >= 2 && parseRatio >= 0.8) {
1499
- return 0.9;
1500
- }
1501
- if (parsedLines > 0 && parseRatio >= 0.5) {
1502
- return 0.6;
1503
- }
1504
- return 0;
1505
- }
1506
- function summarizeGitLog(commits) {
1507
- return {
1508
- kind: "git-log",
1509
- commitCount: commits.length,
1510
- typeCounts: countValues(commits.map((commit) => commit.type)).map(({ value, count }) => ({ type: value, count })),
1511
- scopeCounts: countValues(commits.map((commit) => commit.scope ?? "unscoped"))
1512
- .map(({ value, count }) => ({ scope: value, count }))
1513
- .slice(0, 8),
1514
- authorCounts: countValues(commits.map((commit) => commit.author ?? "unknown"))
1515
- .map(({ value, count }) => ({ author: value, count }))
1516
- .slice(0, 8),
1517
- recentCommits: commits.slice(0, 8),
1518
- };
1519
- }
1520
- function gitLogFacts(details) {
1521
- return [
1522
- { name: "commits", value: details.commitCount },
1523
- { name: "types", value: details.typeCounts.map(({ type, count }) => `${type}:${count}`).join(", ") },
1524
- {
1525
- name: "authors",
1526
- value: details.authorCounts
1527
- .slice(0, 5)
1528
- .map(({ author, count }) => `${author}:${count}`)
1529
- .join(", "),
1530
- },
1531
- { name: "recent", value: details.recentCommits.slice(0, 5).map(renderGitCommit).join(" | ") },
1532
- ];
1533
- }
1534
- function renderGitLogSummary(details) {
1535
- return [
1536
- "git log summary",
1537
- `commits: ${details.commitCount}`,
1538
- `types: ${details.typeCounts.map(({ type, count }) => `${type}:${count}`).join(", ")}`,
1539
- `authors: ${details.authorCounts
1540
- .slice(0, 5)
1541
- .map(({ author, count }) => `${author}:${count}`)
1542
- .join(", ")}`,
1543
- `recent: ${details.recentCommits.slice(0, 5).map(renderGitCommit).join(" | ")}`,
1544
- ].join("\n");
1545
- }
1546
- function renderGitCommit(commit) {
1547
- const scope = commit.scope !== undefined ? `(${commit.scope})` : "";
1548
- return `${commit.hash} ${commit.type}${scope}: ${commit.subject}`;
1549
- }
1550
- export function reduceAccessLog(text) {
1551
- const lines = text.split(/\r?\n/).filter((line) => line.trim().length > 0);
1552
- const entries = [];
1553
- for (const line of lines) {
1554
- const parsed = parseAccessLogLine(line);
1555
- if (parsed) {
1556
- entries.push(parsed);
1557
- }
1558
- }
1559
- const parseRatio = lines.length === 0 ? 0 : entries.length / lines.length;
1560
- const confidence = confidenceForAccessLog(lines.length, parseRatio);
1561
- const candidate = {
1562
- ...ACCESS_LOG_REDUCER,
1563
- confidence,
1564
- reason: `Parsed ${entries.length}/${lines.length} non-empty line(s) as access log entries.`,
1565
- };
1566
- if (entries.length < MIN_ACCESS_LOG_LINES || confidence < MIN_ACCESS_LOG_CONFIDENCE) {
1567
- return { candidate };
1568
- }
1569
- const details = summarizeAccessLog(entries, lines.length - entries.length);
1570
- return {
1571
- candidate,
1572
- result: {
1573
- ...ACCESS_LOG_REDUCER,
1574
- confidence,
1575
- reason: candidate.reason,
1576
- facts: accessLogFacts(details),
1577
- visibleText: renderAccessLogSummary(details),
1578
- details,
1579
- },
1580
- };
1581
- }
1582
- function parseAccessLogLine(line) {
1583
- const match = ACCESS_LOG_LINE.exec(line);
1584
- if (!match) {
1585
- return undefined;
1586
- }
1587
- const method = match[2];
1588
- const path = match[3];
1589
- const status = Number(match[4]);
1590
- const latencyMs = Number(match[5]);
1591
- if (!method || !path || !Number.isInteger(status) || !Number.isInteger(latencyMs)) {
1592
- return undefined;
1593
- }
1594
- return { method, path, status, latencyMs };
1595
- }
1596
- function confidenceForAccessLog(lineCount, parseRatio) {
1597
- if (lineCount < MIN_ACCESS_LOG_LINES) {
1598
- return roundConfidence(parseRatio * 0.5);
1599
- }
1600
- return roundConfidence(parseRatio);
1601
- }
1602
- function summarizeAccessLog(entries, ignoredLineCount) {
1603
- const statusCounts = {};
1604
- let errorCount = 0;
1605
- let latencyTotal = 0;
1606
- const slowExamples = [];
1607
- for (const entry of entries) {
1608
- const status = String(entry.status);
1609
- statusCounts[status] = (statusCounts[status] ?? 0) + 1;
1610
- if (entry.status >= 400) {
1611
- errorCount += 1;
1612
- }
1613
- latencyTotal += entry.latencyMs;
1614
- if (entry.latencyMs >= SLOW_REQUEST_THRESHOLD_MS && slowExamples.length < 5) {
1615
- slowExamples.push({ method: entry.method, path: entry.path, status: entry.status, latencyMs: entry.latencyMs });
1616
- }
1617
- }
1618
- const requestCount = entries.length;
1619
- const slowRequestCount = entries.filter((entry) => entry.latencyMs >= SLOW_REQUEST_THRESHOLD_MS).length;
1620
- return {
1621
- kind: "access-log",
1622
- requestCount,
1623
- statusCounts: sortStatusCounts(statusCounts),
1624
- errorCount,
1625
- errorRatePercent: roundPercent((errorCount / requestCount) * 100),
1626
- averageLatencyMs: Math.round(latencyTotal / requestCount),
1627
- slowThresholdMs: SLOW_REQUEST_THRESHOLD_MS,
1628
- slowRequestCount,
1629
- slowExamples,
1630
- ignoredLineCount,
1631
- };
1632
- }
1633
- function accessLogFacts(details) {
1634
- return [
1635
- { name: "requests", value: details.requestCount },
1636
- { name: "errors", value: details.errorCount },
1637
- { name: "errorRatePercent", value: details.errorRatePercent },
1638
- { name: "averageLatencyMs", value: details.averageLatencyMs },
1639
- { name: `slow>=${details.slowThresholdMs}ms`, value: details.slowRequestCount },
1640
- ...Object.entries(details.statusCounts).map(([status, count]) => ({ name: `status.${status}`, value: count })),
1641
- ];
1642
- }
1643
- function renderAccessLogSummary(details) {
1644
- const lines = [
1645
- "access-log summary",
1646
- `requests: ${details.requestCount}`,
1647
- `errors: ${details.errorCount} (${details.errorRatePercent.toFixed(1)}%)`,
1648
- `avgLatencyMs: ${details.averageLatencyMs}`,
1649
- `slow>=${details.slowThresholdMs}ms: ${details.slowRequestCount}`,
1650
- `status: ${Object.entries(details.statusCounts)
1651
- .map(([status, count]) => `${status}:${count}`)
1652
- .join(", ")}`,
1653
- ];
1654
- if (details.slowExamples.length > 0) {
1655
- lines.push("slow examples:");
1656
- for (const example of details.slowExamples) {
1657
- lines.push(`- ${example.method} ${example.path} ${example.status} ${example.latencyMs}ms`);
1658
- }
1659
- }
1660
- if (details.ignoredLineCount > 0) {
1661
- lines.push(`ignoredLines: ${details.ignoredLineCount}`);
1662
- }
1663
- return lines.join("\n");
1664
- }
1665
- function sortStatusCounts(statusCounts) {
1666
- return Object.fromEntries(Object.entries(statusCounts).sort(([left], [right]) => Number(left) - Number(right)));
1667
- }
1668
- function roundPercent(value) {
1669
- return Math.round(value * 10) / 10;
1670
- }
1671
- function roundConfidence(value) {
1672
- return Math.round(value * 100) / 100;
1673
- }