@hassangameryt/freeflow 0.5.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +148 -154
- package/capabilities/cognitive-routing/SKILL.md +229 -0
- package/capabilities/context-virtualization/SKILL.md +103 -0
- package/capabilities/conversation-history/SKILL.md +53 -0
- package/command-surface.json +6 -13
- package/hooks/freeflow-runtime-context.mjs +35 -91
- package/hooks/tests/check-runtime-context-hook.sh +56 -39
- package/package.json +24 -16
- package/pi-extension/dist/cognitive-routing/commands.js +96 -0
- package/pi-extension/dist/cognitive-routing/config.js +236 -0
- package/pi-extension/dist/cognitive-routing/controller.js +862 -0
- package/pi-extension/dist/cognitive-routing/history.js +439 -0
- package/pi-extension/dist/cognitive-routing/host.js +126 -0
- package/pi-extension/dist/cognitive-routing/runtime.js +43 -0
- package/pi-extension/dist/cognitive-routing/tool.js +211 -0
- package/pi-extension/dist/cognitive-routing/types.js +6 -0
- package/pi-extension/dist/context-virtualization/commands.js +89 -0
- package/pi-extension/dist/context-virtualization/projector.js +42 -0
- package/pi-extension/dist/context-virtualization/runtime.js +356 -0
- package/pi-extension/dist/context-virtualization/state.js +57 -0
- package/pi-extension/dist/context-virtualization/types.js +10 -0
- package/pi-extension/dist/conversation-history/passages.js +66 -0
- package/pi-extension/dist/conversation-history/ranking.js +105 -0
- package/pi-extension/dist/conversation-history/runtime.js +226 -0
- package/pi-extension/dist/conversation-history/search.js +56 -0
- package/pi-extension/dist/conversation-history/types.js +1 -0
- package/pi-extension/dist/freeflow-context/resolver.js +120 -0
- package/pi-extension/dist/freeflow-context/runtime.js +80 -0
- package/pi-extension/dist/freeflow-context/source-projector.js +95 -0
- package/pi-extension/dist/freeflow-context/tool.js +308 -0
- package/pi-extension/dist/freeflow-context/types.js +12 -0
- package/pi-extension/dist/freeflow-context/visibility.js +28 -0
- package/pi-extension/dist/index.js +516 -98
- package/pi-extension/dist/runtime/runtime-context.js +318 -271
- package/pi-extension/dist/runtime/runtime-identity.js +5 -0
- package/pi-extension/dist/settings/settings-tui.js +78 -0
- package/pi-extension/dist/settings/settings-ui.js +593 -563
- package/pi-extension/dist/ui/text-component.js +57 -0
- package/pi-extension/src/cognitive-routing/commands.ts +119 -0
- package/pi-extension/src/cognitive-routing/config.ts +307 -0
- package/pi-extension/src/cognitive-routing/controller.ts +1118 -0
- package/pi-extension/src/cognitive-routing/history.ts +638 -0
- package/pi-extension/src/cognitive-routing/host.ts +172 -0
- package/pi-extension/src/cognitive-routing/runtime.ts +57 -0
- package/pi-extension/src/cognitive-routing/tool.ts +272 -0
- package/pi-extension/src/cognitive-routing/types.ts +89 -0
- package/pi-extension/src/context-virtualization/commands.ts +111 -0
- package/pi-extension/src/context-virtualization/projector.ts +62 -0
- package/pi-extension/src/context-virtualization/runtime.ts +406 -0
- package/pi-extension/src/context-virtualization/state.ts +79 -0
- package/pi-extension/src/context-virtualization/types.ts +92 -0
- package/pi-extension/src/conversation-history/passages.ts +81 -0
- package/pi-extension/src/conversation-history/ranking.ts +155 -0
- package/pi-extension/src/conversation-history/runtime.ts +250 -0
- package/pi-extension/src/conversation-history/search.ts +79 -0
- package/pi-extension/src/conversation-history/types.ts +31 -0
- package/pi-extension/src/freeflow-context/resolver.ts +139 -0
- package/pi-extension/src/freeflow-context/runtime.ts +100 -0
- package/pi-extension/src/freeflow-context/source-projector.ts +106 -0
- package/pi-extension/src/freeflow-context/tool.ts +360 -0
- package/pi-extension/src/freeflow-context/types.ts +70 -0
- package/pi-extension/src/freeflow-context/visibility.ts +53 -0
- package/pi-extension/src/index.ts +553 -101
- package/pi-extension/src/runtime/runtime-context.ts +339 -281
- package/pi-extension/src/runtime/runtime-identity.ts +8 -0
- package/pi-extension/src/settings/settings-tui.ts +93 -0
- package/pi-extension/src/settings/settings-ui.ts +700 -613
- package/pi-extension/src/ui/text-component.ts +66 -0
- package/pi-extension/tests/cognitive-routing/commands.test.js +134 -0
- package/pi-extension/tests/cognitive-routing/controller.test.js +1099 -0
- package/pi-extension/tests/cognitive-routing/extension-lifecycle.test.js +1071 -0
- package/pi-extension/tests/cognitive-routing/history.test.js +274 -0
- package/pi-extension/tests/cognitive-routing/host-fixture.js +4 -0
- package/pi-extension/tests/cognitive-routing/host-identity.test.js +63 -0
- package/pi-extension/tests/cognitive-routing/profile-resolution.test.js +236 -0
- package/pi-extension/tests/cognitive-routing/tool.test.js +230 -0
- package/pi-extension/tests/context-virtualization/extension.test.js +749 -0
- package/pi-extension/tests/context-virtualization/projector.test.js +110 -0
- package/pi-extension/tests/context-virtualization/runtime.test.js +216 -0
- package/pi-extension/tests/conversation-history/runtime.test.js +98 -0
- package/pi-extension/tests/conversation-history/search.test.js +97 -0
- package/pi-extension/tests/integration/pi-extension.test.js +927 -2048
- package/pi-extension/tests/integration/prompt-architecture.test.js +305 -0
- package/runtime/prompts/cognitive-routing.md +9 -0
- package/runtime/prompts/context-virtualization.md +3 -0
- package/runtime/prompts/conversation-history.md +3 -0
- package/runtime/prompts/core.md +13 -0
- package/runtime/prompts/interaction-contract.md +7 -0
- package/runtime/prompts/skills.md +65 -0
- package/skills/action-selection/SKILL.md +91 -0
- package/skills/bypass/SKILL.md +3 -2
- package/skills/commit-work/SKILL.md +1 -1
- package/skills/commit-work/references/staging-decisions.md +1 -1
- package/skills/decision-gate/SKILL.md +1 -1
- package/skills/design-for-depth/SKILL.md +14 -33
- package/skills/design-for-depth/references/design-pressure-signals.md +1 -1
- package/skills/design-for-depth/references/interface-design-loop.md +1 -1
- package/skills/design-for-depth/references/software-design-philosophy.md +1 -1
- package/skills/diagnose-failure/SKILL.md +20 -14
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +1 -3
- package/skills/diagnose-failure/references/{flaky-and-performance.md → flaky-and-performance-diagnosis.md} +1 -1
- package/skills/discuss/SKILL.md +34 -22
- package/skills/discuss/references/discussion-continuity.md +71 -0
- package/skills/evaluate-skill/SKILL.md +5 -5
- package/skills/evaluate-skill/references/{definition-schema.md → evaluation-definition-schema.md} +42 -2
- package/skills/evaluate-skill/references/execution-and-evidence.md +8 -3
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +114 -3
- package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +156 -1
- package/skills/evaluate-skill/scripts/lib/grade.mjs +9 -1
- package/skills/evaluate-skill/scripts/lib/pi.mjs +147 -32
- package/skills/evaluate-skill/scripts/lib/rpc.mjs +4 -4
- package/skills/evaluate-skill/scripts/lib/runner.mjs +36 -14
- package/skills/evaluate-skill/scripts/lib/sandbox.mjs +92 -0
- package/skills/evaluate-skill/scripts/pi-guard.mjs +1 -1
- package/skills/evaluate-skill/scripts/pi-observer.mjs +66 -0
- package/skills/evaluate-skill/scripts/skill-eval.mjs +6 -5
- package/skills/execute-work/SKILL.md +18 -15
- package/skills/execute-work/references/code-practices.md +1 -1
- package/skills/execute-work/references/execute-work-edges.md +42 -0
- package/skills/finish-branch/SKILL.md +2 -2
- package/skills/finish-branch/references/integration-options.md +1 -1
- package/skills/handoff/SKILL.md +3 -3
- package/skills/launch-work/SKILL.md +1 -1
- package/skills/launch-work/references/launch-readiness.md +2 -2
- package/skills/migration-work/SKILL.md +2 -2
- package/skills/migration-work/references/migration-lifecycle.md +1 -1
- package/skills/mode-contract/SKILL.md +4 -4
- package/skills/release-work/SKILL.md +1 -1
- package/skills/release-work/references/release-evidence.md +1 -1
- package/skills/review-artifact/SKILL.md +28 -26
- package/skills/review-work/SKILL.md +28 -26
- package/skills/review-work/references/{reviewer-prompt.md → independent-work-reviewer-contract.md} +1 -1
- package/skills/review-work/references/security-risk-lens.md +1 -1
- package/skills/setup-freeflow/SKILL.md +3 -1
- package/skills/setup-freeflow/references/activation-contract.md +2 -2
- package/skills/setup-freeflow/references/host-setup.md +2 -2
- package/skills/simplify-code/SKILL.md +1 -1
- package/skills/simplify-code/references/simplification-patterns.md +1 -1
- package/skills/tdd/SKILL.md +18 -38
- package/skills/tdd/references/test-design.md +2 -2
- package/skills/track-work/SKILL.md +362 -58
- package/skills/track-work/scripts/lib/codec.mjs +570 -0
- package/skills/track-work/scripts/lib/commands.mjs +568 -0
- package/skills/track-work/scripts/lib/contract.mjs +303 -0
- package/skills/track-work/scripts/lib/model.mjs +506 -0
- package/skills/track-work/scripts/lib/operations.mjs +965 -0
- package/skills/track-work/scripts/lib/record-store.mjs +264 -0
- package/skills/track-work/scripts/lib/result.mjs +37 -0
- package/skills/track-work/scripts/lib/rewrites.mjs +740 -0
- package/skills/track-work/scripts/lib/schema.mjs +277 -0
- package/skills/track-work/scripts/lib/source-inventory.mjs +162 -0
- package/skills/track-work/scripts/lib/views.mjs +443 -0
- package/skills/track-work/scripts/lib/workspace.mjs +134 -0
- package/skills/track-work/scripts/working-record.mjs +122 -0
- package/skills/verify-work/SKILL.md +14 -2
- package/skills/verify-work/references/browser-runtime-evidence.md +1 -1
- package/skills/verify-work/references/integration-evidence.md +1 -1
- package/skills/verify-work/references/performance-evidence.md +1 -1
- package/skills/workflow/SKILL.md +105 -53
- package/skills/workflow/references/domain-skill-composition.md +22 -42
- package/skills/write-plan/SKILL.md +1 -1
- package/skills/write-skill/SKILL.md +3 -2
- package/skills/write-skill/references/progressive-disclosure.md +17 -3
- package/capabilities/output-router/SKILL.md +0 -206
- package/capabilities/output-router/references/safety-policy.md +0 -40
- package/capabilities/output-router/references/setup.md +0 -110
- package/pi-extension/dist/output-router/host-producer-identification.js +0 -232
- package/pi-extension/dist/output-router/native-safety-net.js +0 -216
- package/pi-extension/dist/output-router/observed-tool-routing.js +0 -122
- package/pi-extension/dist/output-router/renderers.js +0 -1023
- package/pi-extension/dist/output-router/router-tools.js +0 -554
- package/pi-extension/dist/output-router/schemas.js +0 -383
- package/pi-extension/dist/output-router/status.js +0 -744
- package/pi-extension/dist/output-router/utils.js +0 -502
- package/pi-extension/src/output-router/host-producer-identification.ts +0 -256
- package/pi-extension/src/output-router/native-safety-net.ts +0 -248
- package/pi-extension/src/output-router/observed-tool-routing.ts +0 -134
- package/pi-extension/src/output-router/renderers.ts +0 -1108
- package/pi-extension/src/output-router/router-tools.ts +0 -579
- package/pi-extension/src/output-router/schemas.ts +0 -387
- package/pi-extension/src/output-router/status.ts +0 -791
- package/pi-extension/src/output-router/utils.ts +0 -554
- package/pi-extension/tests/output-router/observed-producers.test.js +0 -132
- package/pi-extension/tests/output-router/search.test.js +0 -722
- package/router/dist/benchmarks/benchmark-harness.d.ts +0 -35
- package/router/dist/benchmarks/benchmark-harness.js +0 -92
- package/router/dist/benchmarks/benchmarks.d.ts +0 -109
- package/router/dist/benchmarks/benchmarks.js +0 -979
- package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +0 -95
- package/router/dist/benchmarks/codex-qa-benchmarks.js +0 -500
- package/router/dist/benchmarks/command-benchmarks.d.ts +0 -143
- package/router/dist/benchmarks/command-benchmarks.js +0 -698
- package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +0 -82
- package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +0 -773
- package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +0 -132
- package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +0 -1380
- package/router/dist/benchmarks/context-saving-baseline.d.ts +0 -60
- package/router/dist/benchmarks/context-saving-baseline.js +0 -470
- package/router/dist/benchmarks/index-benchmarks.d.ts +0 -103
- package/router/dist/benchmarks/index-benchmarks.js +0 -862
- package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +0 -101
- package/router/dist/benchmarks/storage-policy-benchmarks.js +0 -618
- package/router/dist/benchmarks/vault-index-benchmarks.d.ts +0 -56
- package/router/dist/benchmarks/vault-index-benchmarks.js +0 -322
- package/router/dist/config/config.d.ts +0 -103
- package/router/dist/config/config.js +0 -632
- package/router/dist/config/router-contract.d.ts +0 -10
- package/router/dist/config/router-contract.js +0 -54
- package/router/dist/config/schema.d.ts +0 -27
- package/router/dist/config/schema.js +0 -648
- package/router/dist/config/types.d.ts +0 -573
- package/router/dist/config/types.js +0 -41
- package/router/dist/evidence/bounded-evidence.d.ts +0 -40
- package/router/dist/evidence/bounded-evidence.js +0 -323
- package/router/dist/evidence/evidence-range-selector.d.ts +0 -24
- package/router/dist/evidence/evidence-range-selector.js +0 -232
- package/router/dist/evidence/evidence-search.d.ts +0 -27
- package/router/dist/evidence/evidence-search.js +0 -490
- package/router/dist/evidence/evidence.d.ts +0 -38
- package/router/dist/evidence/evidence.js +0 -200
- package/router/dist/evidence/failure-contracts.d.ts +0 -37
- package/router/dist/evidence/failure-contracts.js +0 -166
- package/router/dist/evidence/line-ranges.d.ts +0 -24
- package/router/dist/evidence/line-ranges.js +0 -22
- package/router/dist/experiments/local-index.d.ts +0 -64
- package/router/dist/experiments/local-index.js +0 -581
- package/router/dist/index.d.ts +0 -30
- package/router/dist/index.js +0 -32
- package/router/dist/local/local-traversal.d.ts +0 -18
- package/router/dist/local/local-traversal.js +0 -334
- package/router/dist/processing/engine.d.ts +0 -132
- package/router/dist/processing/engine.js +0 -533
- package/router/dist/processing/reducers.d.ts +0 -280
- package/router/dist/processing/reducers.js +0 -1673
- package/router/dist/processing/renderers.d.ts +0 -31
- package/router/dist/processing/renderers.js +0 -195
- package/router/dist/processing/scripts.d.ts +0 -91
- package/router/dist/processing/scripts.js +0 -386
- package/router/dist/repo/repo-traversal.d.ts +0 -18
- package/router/dist/repo/repo-traversal.js +0 -245
- package/router/dist/routing/observed-reducers.d.ts +0 -41
- package/router/dist/routing/observed-reducers.js +0 -661
- package/router/dist/routing/observed-routing.d.ts +0 -26
- package/router/dist/routing/observed-routing.js +0 -378
- package/router/dist/routing/parsers.d.ts +0 -16
- package/router/dist/routing/parsers.js +0 -521
- package/router/dist/routing/run-filters.d.ts +0 -60
- package/router/dist/routing/run-filters.js +0 -238
- package/router/dist/routing/run-reducers.d.ts +0 -49
- package/router/dist/routing/run-reducers.js +0 -122
- package/router/dist/sandbox/adapter-roots.d.ts +0 -46
- package/router/dist/sandbox/adapter-roots.js +0 -66
- package/router/dist/sandbox/eryx-python-adapter.d.ts +0 -13
- package/router/dist/sandbox/eryx-python-adapter.js +0 -868
- package/router/dist/sandbox/jq-wasm-adapter.d.ts +0 -9
- package/router/dist/sandbox/jq-wasm-adapter.js +0 -429
- package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +0 -13
- package/router/dist/sandbox/quickjs-wasi-adapter.js +0 -384
- package/router/dist/sandbox/script-sandbox.d.ts +0 -134
- package/router/dist/sandbox/script-sandbox.js +0 -337
- package/router/dist/setup/script-transform-adapters.d.ts +0 -32
- package/router/dist/setup/script-transform-adapters.js +0 -278
- package/router/dist/tools/batch.d.ts +0 -33
- package/router/dist/tools/batch.js +0 -586
- package/router/dist/tools/run.d.ts +0 -62
- package/router/dist/tools/run.js +0 -1304
- package/router/dist/tools/search.d.ts +0 -66
- package/router/dist/tools/search.js +0 -1364
- package/router/dist/transform/engine.d.ts +0 -168
- package/router/dist/transform/engine.js +0 -2072
- package/router/dist/vault/vault-index.d.ts +0 -125
- package/router/dist/vault/vault-index.js +0 -416
- package/router/dist/vault/vault.d.ts +0 -138
- package/router/dist/vault/vault.js +0 -630
- package/runtime/interaction-contract.md +0 -16
- package/skills/discuss/references/checkpoints.md +0 -66
- package/skills/execute-work/references/execution-loop.md +0 -77
- package/skills/track-work/references/working-record-schema.md +0 -378
- package/skills/workflow/references/workflow-loop.md +0 -92
- /package/skills/handoff/references/{templates.md → handoff-templates.md} +0 -0
- /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
|
-
}
|