@tea-agent/loop-agent 0.42.0-next.2 → 0.42.0-next.3
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/CHANGELOG.md +242 -1
- package/dist/application/dag/generate-task-dag.js +75 -19
- package/dist/application/task-lifecycle/advance.js +24 -5
- package/dist/application/task-lifecycle/observe.js +171 -17
- package/dist/application/task-lifecycle/plan-transitions.js +42 -7
- package/dist/application/task-lifecycle/recommendations.js +13 -2
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +5 -0
- package/dist/commands/client-recovery.js +3 -0
- package/dist/commands/dag-follow-up.js +138 -0
- package/dist/commands/init.js +27 -1
- package/dist/commands/task-advance.js +19 -0
- package/dist/executors/dag-pi-executor.js +3784 -231
- package/dist/executors/pi-executor.js +15 -4
- package/dist/executors/pi-read-budget-policy.js +239 -0
- package/dist/executors/shell-executor.js +942 -187
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/infrastructure/console/operation-store.js +226 -6
- package/dist/shared/dag-failure-category.js +12 -0
- package/dist/shared/openspec-spec.js +70 -4
- package/dist/shared/operator/capabilities.js +7 -0
- package/dist/task/config-types.js +34 -5
- package/dist/task/contract/adopt.js +4 -0
- package/dist/task/contract/import-revision.js +4 -0
- package/dist/task/contract/project.js +3 -0
- package/dist/task/contract/schema.js +2 -1
- package/dist/task/frontend-project-capability.js +203 -20
- package/dist/task/runtime.js +5 -2
- package/dist/task/source-prepare/build-draft.js +3 -3
- package/dist/task/source-prepare/fragment-inventory.js +15 -9
- package/dist/task/source-prepare/prepare.js +89 -1
- package/dist/task/source-prepare/semantic-intake.js +6 -2
- package/dist/task/source-references.js +22 -1
- package/dist/worker/console/chat/chat-ui-policy.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +173 -7
- package/dist/worker/console/chat/repo-browser.js +10 -3
- package/dist/worker/console/chat/routes.js +30 -19
- package/dist/worker/console/chat/session-store.js +117 -70
- package/dist/worker/console/chat/shortcuts.js +1 -1
- package/dist/worker/console/chat/turn-process.js +111 -56
- package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
- package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
- package/dist/worker/console/index.js +3 -0
- package/dist/worker/console/operator-actions.js +169 -7
- package/dist/worker/console/server.js +3 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DZZ8m3PO.js → abnfDiagram-N423BO3Z-CeznJ4Iw.js} +1 -1
- package/dist/worker/console/static/assets/{arc-D6PvaVd-.js → arc-B4u9u7SX.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B_OTOiI8.js → architectureDiagram-T3A2C74G-oghpT81p.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-Bv6rqHBg.js → blockDiagram-VBNYF7ZC-BPJ5Kvcj.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-B8eHr0oz.js → c4Diagram-5PPSVZJV-CoIu69pd.js} +1 -1
- package/dist/worker/console/static/assets/channel-DmIXqJ6B.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DYwR0im2.js → chunk-2GRJ4B5K-DtgiAsSc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-D2WPGqXt.js → chunk-2Q5K7J3B-YSu_EVSD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-CQISJ_I7.js → chunk-5RXB4S5H-DaRO-NB6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-C0o2Du1e.js → chunk-5VM5RSS4-Dh0ZKiGA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-4f8kr-U7.js → chunk-6Q2QTUOP-DOrvbR-H.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-D_OWvXzX.js → chunk-GF5L2VYU-C3b22LN2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CasdPz5X.js → chunk-JWPE2WC7-CXlqO8l6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Cj8lRrla.js → chunk-KBJHAD2P-BblTBrDu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CUvD1FSd.js → chunk-RYQCIY6F-DswqPALe.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-CqLw_eqB.js → chunk-XXDRQBXY-CfTwVOlc.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-MFV5Qxb5.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ku-WqJPx.js → cose-bilkent-JH36ORCC-bLD8jVFo.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-Bq_PYDhl.js → cynefin-VYW2F7L2-BvoG1fh5.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BiGZV641.js → cynefinDiagram-MW4NZA55-DEPAdJu3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-C4JRlglF.js → dagre-VZM6K2ZE-DmrNvBLy.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-B0dNeWYG.js → diagram-7IWD3JNH-CdwzIwvs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Cj35YvYM.js → diagram-B4RE2ZJO-CJ3hj-md.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-uzQoJ2-8.js → diagram-LBJQPF4R-C6YJT2a4.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D1a-Buoz.js → diagram-Q27KOJAE-C_LPfjhX.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-XjRrLRSs.js → diagram-UB23O5K3-xvsSb5Ie.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-Da_O24cW.js → ebnfDiagram-BXEA7PRR-Rxp2tKcD.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-BLJ8jrYU.js → erDiagram-JOGREHBK-DRyLYmZh.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-B9GrLjM0.js → flowDiagram-UKHOOZJN-TLSU_D2B.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CeJ0TqiK.js → ganttDiagram-PKOTCBZU-DGqeTvHC.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bm2eNNFX.js → gitGraphDiagram-DS77QQ5N-BZxyH2wh.js} +1 -1
- package/dist/worker/console/static/assets/index-C8V8K9b4.js +451 -0
- package/dist/worker/console/static/assets/index-Cnf_KxTQ.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DB26i3d8.js → infoDiagram-6WML65LV-BsYtzx01.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BOGRyeZT.js → ishikawaDiagram-WSZJBQD7-Qr-ezGv-.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CZRoBO_6.js → journeyDiagram-NVQOT4AX-DYbB3ibo.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-qlWhJyoB.js → kanban-definition-27J2QSJJ-BHJHewZO.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CfUiDDB3.js → linear-CmPIPmCG.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BQe6fpqj.js → mermaid.core-F--C1uV0.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-zFzWHw64.js → mindmap-definition-FAOFIHXS-YTXfL1st.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-DFGUqjZO.js → pegDiagram-VL7TDLO6-DdDMYXP9.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BB5i0l8Z.js → pieDiagram-7S7Q4E2Y-C3nH4KVd.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-obbVZ_X5.js → quadrantDiagram-CIZ2JOQS-kXyxjG7Y.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BwdaNzc1.js → railroadDiagram-AXF67PYL-KNzbl-7w.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CkTJaugh.js → requirementDiagram-LRYGKXZP-CSwI6dG0.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BwJ-hIgq.js → sankeyDiagram-W5VNT64P-Bl1AyuXy.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BzSgmm7w.js → sequenceDiagram-SI44F4Z6-2y4EmXQa.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-Bn3oj5Q1.js → sizeCapture-X5ZJPWSS-E8v8YrMg.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-dxhGtL8K.js → stateDiagram-OKZ733FA-eGGlR856.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8e4J3at.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BLpEInya.js → swimlanes-SLNWSIFB-B7g7xc07.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Do88I1Nb.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-ChJGYcXy.js → timeline-definition-Z64GVDOM-DR3VCHBG.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DK-qjRer.js → vennDiagram-T6HMQDX7-LUvapZIy.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-3qeaWAg-.js → wardleyDiagram-T6FBY63Y-C7GDcaXq.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CootlyP9.js → xychartDiagram-ELKLHX3M-CbNAqoGK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/active-run-badge.js +17 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +19 -8
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +15 -7
- package/dist/worker/console/workspace-context.js +111 -1
- package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
- package/dist/worker/observe/static/dag-node-purpose.js +5 -0
- package/dist/workflows/dag/contract-validator-registrations.js +1 -2
- package/dist/workflows/dag/dag-retry-schema.js +138 -0
- package/dist/workflows/dag/frontend-closeout.js +221 -0
- package/dist/workflows/dag/frontend-design-policy.js +400 -0
- package/dist/workflows/dag/frontend-human-decision.js +182 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +907 -162
- package/dist/workflows/dag/frontend-plan-render.js +2 -1
- package/dist/workflows/dag/frontend-prewrite-gate.js +255 -349
- package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
- package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
- package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
- package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
- package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
- package/dist/workflows/dag/frontend-recovery-run.js +33 -20
- package/dist/workflows/dag/frontend-repair.js +1 -432
- package/dist/workflows/dag/frontend-review-context.js +261 -15
- package/dist/workflows/dag/frontend-review-findings.js +270 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +914 -0
- package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
- package/dist/workflows/dag/frontend-shape-facts.js +419 -0
- package/dist/workflows/dag/frontend-shape.js +435 -0
- package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
- package/dist/workflows/dag/frontend-split-application-service.js +203 -0
- package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
- package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
- package/dist/workflows/dag/frontend-verification-trace.js +249 -24
- package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
- package/dist/workflows/dag/frontend-writer-admission.js +285 -0
- package/dist/workflows/dag/frontend-writer-status.js +256 -0
- package/dist/workflows/dag/init-hybrid.js +677 -492
- package/dist/workflows/dag/interrupt-request.js +7 -0
- package/dist/workflows/dag/node-execution.js +732 -4
- package/dist/workflows/dag/prompt.js +34 -1
- package/dist/workflows/dag/recovery-lease.js +80 -0
- package/dist/workflows/dag/report.js +37 -1
- package/dist/workflows/dag/rerun-feedback.js +256 -1
- package/dist/workflows/dag/rerun-plan.js +17 -6
- package/dist/workflows/dag/rerun-run.js +23 -0
- package/dist/workflows/dag/rerun-task.js +29 -0
- package/dist/workflows/dag/retry-policy.js +214 -104
- package/dist/workflows/dag/runner.js +433 -126
- package/dist/workflows/dag/scheduler.js +114 -16
- package/dist/workflows/dag/types.js +239 -12
- package/dist/workflows/dag/validate.js +28 -12
- package/docs/examples/README.md +5 -0
- package/docs/init-surface.manifest.json +30 -12
- package/docs/skills/vetted-skill-registry.md +4 -2
- package/docs/templates/README.md +2 -0
- package/docs/templates/agent-dag-report.schema.json +8 -2
- package/docs/templates/agent-dag.schema.json +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +4 -1
- package/docs/templates/frontend-implementation-dag.json +89 -0
- package/docs/templates/spec-registry.schema.json +45 -0
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +15 -14
- package/skills/frontend-bounded-implement/references/code-standards.md +19 -0
- package/skills/frontend-contract/SKILL.md +23 -0
- package/skills/frontend-contract/references/contract-protocol.md +34 -0
- package/skills/frontend-design-review/SKILL.md +22 -41
- package/skills/frontend-plan/SKILL.md +26 -0
- package/skills/frontend-plan/references/decision-contract.md +37 -0
- package/skills/frontend-plan/references/design-decisions.md +17 -0
- package/skills/frontend-review/SKILL.md +20 -15
- package/skills/frontend-review/references/review-findings.md +6 -7
- package/skills/frontend-scout/SKILL.md +25 -0
- package/skills/frontend-scout/references/design-evidence.md +16 -0
- package/skills/frontend-scout/references/scout-evidence.md +23 -0
- package/skills/frontend-verification/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/dist/worker/console/static/assets/channel-BU5gOilw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DIzKJGHr.js +0 -1
- package/dist/worker/console/static/assets/index-H9rFJiGL.css +0 -1
- package/dist/worker/console/static/assets/index-xwu9GxEc.js +0 -451
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Cld2qK9v.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-TIHpiT7w.js +0 -8
- package/skills/frontend-implementation/SKILL.md +0 -52
- package/skills/frontend-implementation/references/code-standards.md +0 -33
- package/skills/frontend-implementation/references/design-spec.md +0 -56
- package/skills/frontend-implementation/references/node-contracts.md +0 -31
|
@@ -49,15 +49,236 @@ function symbolEvidenceCandidates(symbol) {
|
|
|
49
49
|
return [symbol, describeTitle, "describe("];
|
|
50
50
|
return [symbol];
|
|
51
51
|
}
|
|
52
|
-
function isConfigurationVerificationFile(file) {
|
|
52
|
+
export function isConfigurationVerificationFile(file) {
|
|
53
53
|
return /(?:^|\/)(?:package\.json|tsconfig(?:\.[^/]+)?\.json|(?:vite|webpack|rollup|docusaurus)\.config\.[cm]?[jt]s)$/.test(file.replace(/\\/g, "/"));
|
|
54
54
|
}
|
|
55
|
+
function isIdentifierStart(char) {
|
|
56
|
+
return char !== undefined && /[A-Za-z_$]/.test(char);
|
|
57
|
+
}
|
|
58
|
+
function isIdentifierPart(char) {
|
|
59
|
+
return char !== undefined && /[A-Za-z0-9_$]/.test(char);
|
|
60
|
+
}
|
|
61
|
+
function readQuotedString(content, start) {
|
|
62
|
+
const quote = content[start];
|
|
63
|
+
if (quote !== "'" && quote !== '"' && quote !== "`")
|
|
64
|
+
return null;
|
|
65
|
+
let value = "";
|
|
66
|
+
for (let index = start + 1; index < content.length; index += 1) {
|
|
67
|
+
const char = content[index];
|
|
68
|
+
if (char === "\\") {
|
|
69
|
+
const escaped = content[index + 1];
|
|
70
|
+
if (escaped === undefined)
|
|
71
|
+
return null;
|
|
72
|
+
value += escaped;
|
|
73
|
+
index += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (quote === "`" && char === "$" && content[index + 1] === "{") {
|
|
77
|
+
// Interpolated templates are not literal titles: their runtime value is
|
|
78
|
+
// not deterministic from source text alone.
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
if (char === quote)
|
|
82
|
+
return { end: index + 1, value };
|
|
83
|
+
value += char;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function skipQuotedSource(content, start) {
|
|
88
|
+
const quote = content[start];
|
|
89
|
+
for (let index = start + 1; index < content.length; index += 1) {
|
|
90
|
+
if (content[index] === "\\") {
|
|
91
|
+
index += 1;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (content[index] === quote)
|
|
95
|
+
return index + 1;
|
|
96
|
+
}
|
|
97
|
+
return content.length;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Small JS/TS lexer for test-title extraction. It intentionally emits only
|
|
101
|
+
* identifiers, string literals, and punctuation while skipping comments. This
|
|
102
|
+
* prevents prose in comments or ordinary strings from becoming trace evidence
|
|
103
|
+
* without adding a parser as a production dependency.
|
|
104
|
+
*/
|
|
105
|
+
function lexFrontendTestSource(content) {
|
|
106
|
+
const tokens = [];
|
|
107
|
+
let index = 0;
|
|
108
|
+
while (index < content.length) {
|
|
109
|
+
const char = content[index];
|
|
110
|
+
if (/\s/.test(char ?? "")) {
|
|
111
|
+
index += 1;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (char === "/" && content[index + 1] === "/") {
|
|
115
|
+
index += 2;
|
|
116
|
+
while (index < content.length && content[index] !== "\n")
|
|
117
|
+
index += 1;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (char === "/" && content[index + 1] === "*") {
|
|
121
|
+
const end = content.indexOf("*/", index + 2);
|
|
122
|
+
index = end < 0 ? content.length : end + 2;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const quoted = readQuotedString(content, index);
|
|
126
|
+
if (quoted) {
|
|
127
|
+
tokens.push({ type: "string", value: quoted.value });
|
|
128
|
+
index = quoted.end;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
132
|
+
// Unterminated or interpolated strings are skipped instead of being
|
|
133
|
+
// tokenized as executable source.
|
|
134
|
+
index = skipQuotedSource(content, index);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (isIdentifierStart(char)) {
|
|
138
|
+
let end = index + 1;
|
|
139
|
+
while (isIdentifierPart(content[end]))
|
|
140
|
+
end += 1;
|
|
141
|
+
tokens.push({ type: "identifier", value: content.slice(index, end) });
|
|
142
|
+
index = end;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
tokens.push({ type: "punctuation", value: char ?? "" });
|
|
146
|
+
index += 1;
|
|
147
|
+
}
|
|
148
|
+
return tokens;
|
|
149
|
+
}
|
|
150
|
+
/** Extract literal titles from real describe/it/test calls in JS/TS source. */
|
|
151
|
+
export function extractFrontendTestTitles(content) {
|
|
152
|
+
const tokens = lexFrontendTestSource(content);
|
|
153
|
+
const titles = [];
|
|
154
|
+
const configurationModifiers = new Set(["each", "runIf", "skipIf"]);
|
|
155
|
+
function closingParenIndex(openIndex) {
|
|
156
|
+
let depth = 0;
|
|
157
|
+
for (let cursor = openIndex; cursor < tokens.length; cursor += 1) {
|
|
158
|
+
if (tokens[cursor]?.type !== "punctuation")
|
|
159
|
+
continue;
|
|
160
|
+
if (tokens[cursor]?.value === "(")
|
|
161
|
+
depth += 1;
|
|
162
|
+
if (tokens[cursor]?.value === ")") {
|
|
163
|
+
depth -= 1;
|
|
164
|
+
if (depth === 0)
|
|
165
|
+
return cursor;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
171
|
+
const token = tokens[index];
|
|
172
|
+
if (token?.type !== "identifier" ||
|
|
173
|
+
!["describe", "it", "test"].includes(token.value) ||
|
|
174
|
+
(tokens[index - 1]?.type === "punctuation" &&
|
|
175
|
+
tokens[index - 1]?.value === ".")) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
let cursor = index + 1;
|
|
179
|
+
let hasConfigurationModifier = false;
|
|
180
|
+
while (tokens[cursor]?.type === "punctuation" &&
|
|
181
|
+
tokens[cursor]?.value === "." &&
|
|
182
|
+
tokens[cursor + 1]?.type === "identifier") {
|
|
183
|
+
if (tokens[cursor + 1]?.type === "identifier" &&
|
|
184
|
+
configurationModifiers.has(tokens[cursor + 1].value)) {
|
|
185
|
+
hasConfigurationModifier = true;
|
|
186
|
+
}
|
|
187
|
+
cursor += 2;
|
|
188
|
+
}
|
|
189
|
+
if (tokens[cursor]?.type !== "punctuation" ||
|
|
190
|
+
tokens[cursor]?.value !== "(") {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (hasConfigurationModifier) {
|
|
194
|
+
const close = closingParenIndex(cursor);
|
|
195
|
+
if (close === null ||
|
|
196
|
+
tokens[close + 1]?.type !== "punctuation" ||
|
|
197
|
+
tokens[close + 1]?.value !== "(") {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
cursor = close + 1;
|
|
201
|
+
}
|
|
202
|
+
if (tokens[cursor + 1]?.type !== "string")
|
|
203
|
+
continue;
|
|
204
|
+
titles.push(tokens[cursor + 1].value);
|
|
205
|
+
}
|
|
206
|
+
return titles;
|
|
207
|
+
}
|
|
208
|
+
function traceKeyMatchesTitle(traceKey, title) {
|
|
209
|
+
const escaped = traceKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
210
|
+
return new RegExp(`(?:^|[^A-Za-z0-9_-])${escaped}(?:$|[^A-Za-z0-9_-])`).test(title);
|
|
211
|
+
}
|
|
212
|
+
function formatObservedTestTitles(titles) {
|
|
213
|
+
if (titles.length === 0)
|
|
214
|
+
return "(none)";
|
|
215
|
+
const limit = 8;
|
|
216
|
+
const preview = titles
|
|
217
|
+
.slice(0, limit)
|
|
218
|
+
.map((title) => title.length > 120 ? `${title.slice(0, 117)}...` : title)
|
|
219
|
+
.join(" | ");
|
|
220
|
+
return titles.length > limit
|
|
221
|
+
? `${preview} | ... (${titles.length - limit} more)`
|
|
222
|
+
: preview;
|
|
223
|
+
}
|
|
224
|
+
async function evaluateTargetFileTrace(input) {
|
|
225
|
+
const fileIssues = await assertFileAndSymbol({
|
|
226
|
+
workspaceRoot: input.workspaceRoot,
|
|
227
|
+
file: input.target.file,
|
|
228
|
+
});
|
|
229
|
+
if (fileIssues.length || input.target.type === "static") {
|
|
230
|
+
return { issues: fileIssues };
|
|
231
|
+
}
|
|
232
|
+
const absolute = path.resolve(input.workspaceRoot, input.target.file);
|
|
233
|
+
const content = await readFile(absolute, "utf8");
|
|
234
|
+
const observedTestTitles = extractFrontendTestTitles(content);
|
|
235
|
+
if (observedTestTitles.some((title) => traceKeyMatchesTitle(input.target.id, title))) {
|
|
236
|
+
return { issues: [], observedTestTitles };
|
|
237
|
+
}
|
|
238
|
+
const observed = formatObservedTestTitles(observedTestTitles);
|
|
239
|
+
return {
|
|
240
|
+
issues: [
|
|
241
|
+
`trace key not found in test title: ${input.target.id} in ${input.target.file}; observed test titles: ${observed}`,
|
|
242
|
+
],
|
|
243
|
+
observedTestTitles,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Lightweight writer-boundary check. It validates file existence and stable
|
|
248
|
+
* behavior trace tokens only; frozen commands remain the final trace gate's
|
|
249
|
+
* responsibility after command execution.
|
|
250
|
+
*/
|
|
251
|
+
export async function evaluateFrontendWriterTracePreflight(input) {
|
|
252
|
+
const targets = [];
|
|
253
|
+
const failures = [];
|
|
254
|
+
for (const target of input.contract.verificationTargets) {
|
|
255
|
+
const fileTrace = await evaluateTargetFileTrace({
|
|
256
|
+
workspaceRoot: input.workspaceRoot,
|
|
257
|
+
target,
|
|
258
|
+
});
|
|
259
|
+
for (const detail of fileTrace.issues) {
|
|
260
|
+
failures.push({ category: "coverage-gap", targetId: target.id, detail });
|
|
261
|
+
}
|
|
262
|
+
targets.push({
|
|
263
|
+
id: target.id,
|
|
264
|
+
commandLabel: target.commandLabel,
|
|
265
|
+
file: target.file,
|
|
266
|
+
...(target.symbol ? { symbol: target.symbol } : {}),
|
|
267
|
+
...(target.type !== "static" ? { traceKey: target.id } : {}),
|
|
268
|
+
status: fileTrace.issues.length ? "failed" : "ok",
|
|
269
|
+
matchedNodeIds: [],
|
|
270
|
+
issues: fileTrace.issues,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return { ok: failures.length === 0, targets, failures };
|
|
274
|
+
}
|
|
55
275
|
/**
|
|
56
276
|
* Pure verification-target evaluation shared by the trace gate and the repair
|
|
57
277
|
* assess gate. It derives, for each contract verification target, whether the
|
|
58
278
|
* target bound to frozen evidence (commandLabel owners), and whether the file
|
|
59
|
-
* and
|
|
60
|
-
* nothing: failures are returned as `hardIssues` plus
|
|
279
|
+
* and stable behavior-target id exist in a real test title. It performs no
|
|
280
|
+
* writes and throws nothing: failures are returned as `hardIssues` plus
|
|
281
|
+
* per-target `status`.
|
|
61
282
|
*/
|
|
62
283
|
export async function evaluateVerificationTargets(input) {
|
|
63
284
|
const targets = [];
|
|
@@ -75,17 +296,11 @@ export async function evaluateVerificationTargets(input) {
|
|
|
75
296
|
if (!owners?.length) {
|
|
76
297
|
issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
|
|
77
298
|
}
|
|
78
|
-
const
|
|
299
|
+
const fileTrace = await evaluateTargetFileTrace({
|
|
79
300
|
workspaceRoot: input.workspaceRoot,
|
|
80
|
-
|
|
81
|
-
// Configuration files prove that the command's entrypoint exists;
|
|
82
|
-
// they do not expose source symbols. LLMs sometimes derive a
|
|
83
|
-
// filename fragment such as "build" from tsconfig.build.json.
|
|
84
|
-
symbol: isConfigurationVerificationFile(target.file)
|
|
85
|
-
? undefined
|
|
86
|
-
: target.symbol,
|
|
301
|
+
target,
|
|
87
302
|
});
|
|
88
|
-
issues.push(...
|
|
303
|
+
issues.push(...fileTrace.issues);
|
|
89
304
|
const status = issues.length ? "failed" : "ok";
|
|
90
305
|
if (issues.length)
|
|
91
306
|
hardIssues.push(...issues.map((i) => `${target.id}: ${i}`));
|
|
@@ -94,6 +309,7 @@ export async function evaluateVerificationTargets(input) {
|
|
|
94
309
|
commandLabel: target.commandLabel,
|
|
95
310
|
file: target.file,
|
|
96
311
|
symbol: target.symbol,
|
|
312
|
+
...(target.type !== "static" ? { traceKey: target.id } : {}),
|
|
97
313
|
status,
|
|
98
314
|
matchedNodeIds: owners ?? [],
|
|
99
315
|
issues,
|
|
@@ -101,6 +317,25 @@ export async function evaluateVerificationTargets(input) {
|
|
|
101
317
|
}
|
|
102
318
|
return { targets, hardIssues };
|
|
103
319
|
}
|
|
320
|
+
/**
|
|
321
|
+
* Legacy deterministic symbol resolvability check retained by canonical
|
|
322
|
+
* normalization (`dropUnresolvedVerificationSymbols`). New plan tools do not
|
|
323
|
+
* expose symbol and runtime trace binding uses the stable target id. This
|
|
324
|
+
* helper still handles old `all describe blocks`, `describe('title')`, and
|
|
325
|
+
* slash-delimited labels so existing run artifacts normalize consistently.
|
|
326
|
+
*/
|
|
327
|
+
export function verificationSymbolMatchesContent(symbol, content) {
|
|
328
|
+
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
329
|
+
return content.includes("describe(");
|
|
330
|
+
const symbols = symbol.includes(" / ")
|
|
331
|
+
? symbol
|
|
332
|
+
.split(/[((]/, 1)[0]
|
|
333
|
+
.split("/")
|
|
334
|
+
.map((item) => item.trim())
|
|
335
|
+
.filter(Boolean)
|
|
336
|
+
: [symbol];
|
|
337
|
+
return symbols.every((item) => symbolEvidenceCandidates(item).some((candidate) => content.includes(candidate)));
|
|
338
|
+
}
|
|
104
339
|
async function assertFileAndSymbol(input) {
|
|
105
340
|
const issues = [];
|
|
106
341
|
const absolute = path.resolve(input.workspaceRoot, input.file);
|
|
@@ -124,18 +359,8 @@ async function assertFileAndSymbol(input) {
|
|
|
124
359
|
}
|
|
125
360
|
if (input.symbol) {
|
|
126
361
|
const content = await readFile(absolute, "utf8");
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
.split(/[((]/, 1)[0]
|
|
130
|
-
.split("/")
|
|
131
|
-
.map((symbol) => symbol.trim())
|
|
132
|
-
.filter(Boolean)
|
|
133
|
-
: [input.symbol];
|
|
134
|
-
for (const symbol of symbols) {
|
|
135
|
-
const matched = symbolEvidenceCandidates(symbol).some((candidate) => content.includes(candidate));
|
|
136
|
-
if (!matched) {
|
|
137
|
-
issues.push(`symbol not found: ${symbol} in ${input.file}`);
|
|
138
|
-
}
|
|
362
|
+
if (!verificationSymbolMatchesContent(input.symbol, content)) {
|
|
363
|
+
issues.push(`symbol not found: ${input.symbol} in ${input.file}`);
|
|
139
364
|
}
|
|
140
365
|
}
|
|
141
366
|
return issues;
|
|
@@ -5,25 +5,152 @@ import { runResilientGitCommand } from "../../shared/resilient-git.js";
|
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
7
7
|
export const FRONTEND_WORKTREE_DIFF_SCHEMA_ID = "frontend-worktree-diff-v1";
|
|
8
|
+
export const FRONTEND_REVIEW_DIFF_INDEX_SCHEMA_ID = "frontend-review-diff-index-v1";
|
|
9
|
+
const MAX_REVIEW_DIFF_SUMMARY_ENTRIES = 48;
|
|
10
|
+
/** Maximum diff payload per review artifact; metadata adds a small fixed header. */
|
|
11
|
+
const MAX_REVIEW_DIFF_FRAGMENT_BYTES = 20 * 1024;
|
|
8
12
|
export const FRONTEND_WORKTREE_BASELINE_SCHEMA_ID = "frontend-worktree-baseline-v1";
|
|
9
13
|
async function runGit(cwd, args) {
|
|
10
14
|
const result = await runResilientGitCommand({ cwd, args, readOnly: true });
|
|
11
15
|
return { code: result.code, stdout: result.stdout, stderr: result.stderr };
|
|
12
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Ephemeral interpreter/tool caches that writers may create while drafting
|
|
19
|
+
* tests (kept in sync with `isEphemeralToolCachePath` in
|
|
20
|
+
* src/executors/shell-write-guard.ts). They are not product evidence: tool
|
|
21
|
+
* cache writes must neither fail the write guard nor appear in the review
|
|
22
|
+
* diff as pre-existing changes.
|
|
23
|
+
*/
|
|
24
|
+
export function isEphemeralToolCachePath(filePath) {
|
|
25
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
26
|
+
if (!normalized)
|
|
27
|
+
return false;
|
|
28
|
+
if (normalized === ".pytest_cache" ||
|
|
29
|
+
normalized.startsWith(".pytest_cache/")) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (normalized === ".mypy_cache" || normalized.startsWith(".mypy_cache/")) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (normalized === ".ruff_cache" || normalized.startsWith(".ruff_cache/")) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (normalized === ".playwright-cli" ||
|
|
39
|
+
normalized.startsWith(".playwright-cli/")) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
if (normalized === ".coverage" || normalized.startsWith(".coverage.")) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// vite/vitest runtime caches live under node_modules/.vite (e.g.
|
|
46
|
+
// node_modules/.vite/vitest/results.json). node_modules is the gitignored
|
|
47
|
+
// dependency tree — tool cache writes there are never a product change.
|
|
48
|
+
if (normalized.startsWith("node_modules/.vite/")) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (/(?:^|\/)__pycache__(?:\/|$)/.test(normalized))
|
|
52
|
+
return true;
|
|
53
|
+
if (/\.(?:pyc|pyo)$/.test(normalized))
|
|
54
|
+
return true;
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
13
57
|
function splitLines(text) {
|
|
14
58
|
return text
|
|
15
59
|
.split(/\r?\n/)
|
|
16
60
|
.map((line) => line.trim())
|
|
17
61
|
.filter(Boolean);
|
|
18
62
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
63
|
+
function buildReviewDiffSummary(input) {
|
|
64
|
+
const files = [...input.hunks.reduce((grouped, hunk) => {
|
|
65
|
+
const existing = grouped.get(hunk.path);
|
|
66
|
+
if (existing) {
|
|
67
|
+
existing.bytes += hunk.bytes;
|
|
68
|
+
existing.lastPatchPath = hunk.patchPath;
|
|
69
|
+
return grouped;
|
|
70
|
+
}
|
|
71
|
+
grouped.set(hunk.path, {
|
|
72
|
+
path: hunk.path,
|
|
73
|
+
bytes: hunk.bytes,
|
|
74
|
+
parts: hunk.parts,
|
|
75
|
+
firstPatchPath: hunk.patchPath,
|
|
76
|
+
lastPatchPath: hunk.patchPath,
|
|
77
|
+
});
|
|
78
|
+
return grouped;
|
|
79
|
+
}, new Map()).values()];
|
|
80
|
+
const listedFiles = files.slice(0, MAX_REVIEW_DIFF_SUMMARY_ENTRIES);
|
|
81
|
+
const remainingFiles = files.length - listedFiles.length;
|
|
82
|
+
const listedUntracked = input.untrackedFiles.slice(0, MAX_REVIEW_DIFF_SUMMARY_ENTRIES);
|
|
83
|
+
const remainingUntracked = input.untrackedFiles.length - listedUntracked.length;
|
|
84
|
+
return [
|
|
85
|
+
"# Frontend review diff summary",
|
|
86
|
+
"",
|
|
87
|
+
"Read this bounded summary first. Do not read artifacts/diff_patch.patch in full; it is retained only as the canonical audit artifact.",
|
|
88
|
+
"For a semantic finding, read only the matching per-file diff fragment below and then the current source file when necessary. Read fragments in part order when a file has more than one.",
|
|
89
|
+
"",
|
|
90
|
+
`Canonical patch SHA-256: ${input.patchSha256}`,
|
|
91
|
+
`Canonical patch bytes: ${input.patchBytes}`,
|
|
92
|
+
`Tracked changed files: ${files.length}`,
|
|
93
|
+
`Review diff fragments: ${input.hunks.length}`,
|
|
94
|
+
`Untracked files (inventory only): ${input.untrackedFiles.length}`,
|
|
95
|
+
"",
|
|
96
|
+
"## Tracked file diff fragment references",
|
|
97
|
+
...(listedFiles.length > 0
|
|
98
|
+
? listedFiles.map((file) => file.parts === 1
|
|
99
|
+
? `- ${file.path} — ${file.bytes} bytes — read ${file.firstPatchPath} only if this file needs review`
|
|
100
|
+
: `- ${file.path} — ${file.bytes} bytes across ${file.parts} ordered fragments — read ${file.firstPatchPath} through ${file.lastPatchPath} only if this file needs review`)
|
|
101
|
+
: ["- (none)"]),
|
|
102
|
+
...(remainingFiles > 0
|
|
103
|
+
? [`- … ${remainingFiles} additional tracked files are recorded in contracts/frontend-review-diff-index.json; do not bulk-read them.`]
|
|
104
|
+
: []),
|
|
105
|
+
"",
|
|
106
|
+
"## Untracked inventory",
|
|
107
|
+
...(listedUntracked.length > 0
|
|
108
|
+
? listedUntracked.map((file) => `- ${file}`)
|
|
109
|
+
: ["- (none)"]),
|
|
110
|
+
...(remainingUntracked > 0
|
|
111
|
+
? [`- … ${remainingUntracked} additional untracked files are recorded in contracts/frontend-review-diff-index.json.`]
|
|
112
|
+
: []),
|
|
113
|
+
"",
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
function splitReviewDiffFragments(text) {
|
|
117
|
+
const fragments = [];
|
|
118
|
+
let current = "";
|
|
119
|
+
for (const line of text.match(/[^\n]*\n|[^\n]+/g) ?? []) {
|
|
120
|
+
const lineBytes = Buffer.byteLength(line);
|
|
121
|
+
if (current.length > 0 &&
|
|
122
|
+
Buffer.byteLength(current) + lineBytes >
|
|
123
|
+
MAX_REVIEW_DIFF_FRAGMENT_BYTES) {
|
|
124
|
+
fragments.push(current);
|
|
125
|
+
current = "";
|
|
126
|
+
}
|
|
127
|
+
if (lineBytes <= MAX_REVIEW_DIFF_FRAGMENT_BYTES) {
|
|
128
|
+
current += line;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
// Generated/minified files can produce a single diff line larger than the
|
|
132
|
+
// review budget. Split by Unicode code point so no fragment exceeds the
|
|
133
|
+
// byte limit while preserving the original patch bytes when concatenated.
|
|
134
|
+
let oversizedLineFragment = "";
|
|
135
|
+
let oversizedLineFragmentBytes = 0;
|
|
136
|
+
for (const codePoint of line) {
|
|
137
|
+
const codePointBytes = Buffer.byteLength(codePoint);
|
|
138
|
+
if (oversizedLineFragmentBytes > 0 &&
|
|
139
|
+
oversizedLineFragmentBytes + codePointBytes >
|
|
140
|
+
MAX_REVIEW_DIFF_FRAGMENT_BYTES) {
|
|
141
|
+
fragments.push(oversizedLineFragment);
|
|
142
|
+
oversizedLineFragment = "";
|
|
143
|
+
oversizedLineFragmentBytes = 0;
|
|
144
|
+
}
|
|
145
|
+
oversizedLineFragment += codePoint;
|
|
146
|
+
oversizedLineFragmentBytes += codePointBytes;
|
|
147
|
+
}
|
|
148
|
+
if (oversizedLineFragment)
|
|
149
|
+
fragments.push(oversizedLineFragment);
|
|
150
|
+
}
|
|
151
|
+
if (current.length > 0)
|
|
152
|
+
fragments.push(current);
|
|
153
|
+
return fragments.length > 0 ? fragments : [""];
|
|
27
154
|
}
|
|
28
155
|
async function sha256File(filePath) {
|
|
29
156
|
try {
|
|
@@ -37,15 +164,26 @@ async function sha256File(filePath) {
|
|
|
37
164
|
export async function captureFrontendWorktreeBaseline(input) {
|
|
38
165
|
const run = input.dependencies?.runGit ?? runGit;
|
|
39
166
|
const root = path.resolve(input.workspaceRoot);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
167
|
+
// Baseline = the full concrete workspace inventory the writer may touch
|
|
168
|
+
// (tracked + untracked, gitignored excluded), NOT just currently-changed
|
|
169
|
+
// files. Using `git status` here made a clean repository baseline-empty
|
|
170
|
+
// (status has no entries when nothing changed), which wrongly blocked
|
|
171
|
+
// writer admission with baseline-empty.
|
|
172
|
+
const ls = await run(root, [
|
|
173
|
+
"ls-files",
|
|
174
|
+
"-co",
|
|
175
|
+
"--exclude-standard",
|
|
176
|
+
"-z",
|
|
44
177
|
]);
|
|
45
|
-
if (
|
|
46
|
-
throw new Error(`frontend worktree baseline failed: ${
|
|
178
|
+
if (ls.code !== 0) {
|
|
179
|
+
throw new Error(`frontend worktree baseline failed: ${ls.stderr.trim() || ls.stdout.trim()}`);
|
|
47
180
|
}
|
|
48
|
-
const files =
|
|
181
|
+
const files = ls.stdout
|
|
182
|
+
.split("\0")
|
|
183
|
+
.map((line) => line.trim())
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.filter((file) => !isEphemeralToolCachePath(file))
|
|
186
|
+
.sort();
|
|
49
187
|
const hashes = {};
|
|
50
188
|
for (const file of files)
|
|
51
189
|
hashes[file] = await sha256File(path.join(root, file));
|
|
@@ -112,8 +250,12 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
112
250
|
if (untracked.code !== 0) {
|
|
113
251
|
throw new Error(`frontend worktree diff gate ls-files failed: ${untracked.stderr.trim() || untracked.stdout.trim()}`);
|
|
114
252
|
}
|
|
115
|
-
const changedFilesAll = splitLines(nameOnly.stdout)
|
|
116
|
-
|
|
253
|
+
const changedFilesAll = splitLines(nameOnly.stdout)
|
|
254
|
+
.filter((file) => !isEphemeralToolCachePath(file))
|
|
255
|
+
.sort();
|
|
256
|
+
const untrackedFilesAll = splitLines(untracked.stdout)
|
|
257
|
+
.filter((file) => !isEphemeralToolCachePath(file))
|
|
258
|
+
.sort();
|
|
117
259
|
const authorizedChangedPaths = input.authorizedChangedPaths ?? [];
|
|
118
260
|
if (baseline) {
|
|
119
261
|
for (const file of baseline.files) {
|
|
@@ -160,11 +302,65 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
160
302
|
const absolutePatch = path.join(input.runDir, ...relativePatch.split("/"));
|
|
161
303
|
await mkdir(path.dirname(absolutePatch), { recursive: true });
|
|
162
304
|
await writeFile(absolutePatch, patchText, "utf8");
|
|
305
|
+
// Keep the complete patch as immutable audit evidence, but create a bounded
|
|
306
|
+
// reviewer entrypoint plus one file-scoped artifact per tracked change. This
|
|
307
|
+
// prevents final review from loading a multi-thousand-line patch into a
|
|
308
|
+
// single Pi session merely to inspect one or two relevant files.
|
|
309
|
+
const reviewHunks = [];
|
|
310
|
+
const reviewHunksDir = path.join(input.runDir, "artifacts", "diff-hunks");
|
|
311
|
+
await mkdir(reviewHunksDir, { recursive: true });
|
|
312
|
+
for (const [index, file] of changedFiles.entries()) {
|
|
313
|
+
const filePatch = await run(root, ["diff", "--binary", "HEAD", "--", file]);
|
|
314
|
+
if (filePatch.code !== 0) {
|
|
315
|
+
throw new Error(`frontend worktree diff hunk failed for ${file}: ${filePatch.stderr.trim() || filePatch.stdout.trim()}`);
|
|
316
|
+
}
|
|
317
|
+
const text = filePatch.stdout.endsWith("\n") || filePatch.stdout.length === 0
|
|
318
|
+
? filePatch.stdout
|
|
319
|
+
: `${filePatch.stdout}\n`;
|
|
320
|
+
const fragments = splitReviewDiffFragments(text);
|
|
321
|
+
for (const [partIndex, fragment] of fragments.entries()) {
|
|
322
|
+
const part = partIndex + 1;
|
|
323
|
+
const patchPath = `artifacts/diff-hunks/${String(index + 1).padStart(3, "0")}-${String(part).padStart(3, "0")}.patch`;
|
|
324
|
+
const artifact = [
|
|
325
|
+
"# Frontend review diff fragment",
|
|
326
|
+
`# path: ${file}`,
|
|
327
|
+
`# part: ${part}/${fragments.length}`,
|
|
328
|
+
"",
|
|
329
|
+
fragment,
|
|
330
|
+
].join("\n");
|
|
331
|
+
await writeFile(path.join(input.runDir, ...patchPath.split("/")), artifact, "utf8");
|
|
332
|
+
reviewHunks.push({
|
|
333
|
+
path: file,
|
|
334
|
+
patchPath,
|
|
335
|
+
sha256: createHash("sha256").update(artifact).digest("hex"),
|
|
336
|
+
bytes: Buffer.byteLength(artifact),
|
|
337
|
+
part,
|
|
338
|
+
parts: fragments.length,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const reviewIndexPath = "contracts/frontend-review-diff-index.json";
|
|
343
|
+
await writeDagRunJsonArtifact(input.runDir, reviewIndexPath, {
|
|
344
|
+
schemaVersion: 1,
|
|
345
|
+
schemaId: FRONTEND_REVIEW_DIFF_INDEX_SCHEMA_ID,
|
|
346
|
+
patchSha256,
|
|
347
|
+
hunks: reviewHunks,
|
|
348
|
+
untrackedFiles,
|
|
349
|
+
});
|
|
350
|
+
const reviewSummaryPath = "artifacts/diff_summary.md";
|
|
351
|
+
await writeFile(path.join(input.runDir, ...reviewSummaryPath.split("/")), buildReviewDiffSummary({
|
|
352
|
+
patchSha256,
|
|
353
|
+
patchBytes: Buffer.byteLength(patchText),
|
|
354
|
+
hunks: reviewHunks,
|
|
355
|
+
untrackedFiles,
|
|
356
|
+
}), "utf8");
|
|
163
357
|
const payload = {
|
|
164
358
|
schemaVersion: 1,
|
|
165
359
|
schemaId: FRONTEND_WORKTREE_DIFF_SCHEMA_ID,
|
|
166
360
|
patchPath: relativePatch,
|
|
167
361
|
patchSha256,
|
|
362
|
+
reviewSummaryPath,
|
|
363
|
+
reviewIndexPath,
|
|
168
364
|
changedFiles,
|
|
169
365
|
untrackedFiles,
|
|
170
366
|
empty: changedFiles.length === 0 && untrackedFiles.length === 0,
|
|
@@ -177,6 +373,8 @@ export async function runFrontendWorktreeDiffGate(input) {
|
|
|
177
373
|
patchPath: relativePatch,
|
|
178
374
|
patchAbsolutePath: absolutePatch,
|
|
179
375
|
patchSha256,
|
|
376
|
+
reviewSummaryPath,
|
|
377
|
+
reviewIndexPath,
|
|
180
378
|
changedFiles,
|
|
181
379
|
untrackedFiles,
|
|
182
380
|
empty: payload.empty,
|
|
@@ -189,6 +387,8 @@ export function formatFrontendWorktreeDiffStdout(result) {
|
|
|
189
387
|
`schemaId=${result.schemaId}`,
|
|
190
388
|
`patchPath=${result.patchPath}`,
|
|
191
389
|
`patchSha256=${result.patchSha256}`,
|
|
390
|
+
`reviewSummaryPath=${result.reviewSummaryPath}`,
|
|
391
|
+
`reviewIndexPath=${result.reviewIndexPath}`,
|
|
192
392
|
`changedFiles=${result.changedFiles.length}`,
|
|
193
393
|
`untrackedFiles=${result.untrackedFiles.length}`,
|
|
194
394
|
`empty=${result.empty ? "true" : "false"}`,
|
|
@@ -199,3 +399,36 @@ export function formatFrontendWorktreeDiffStdout(result) {
|
|
|
199
399
|
.filter(Boolean)
|
|
200
400
|
.join("\n");
|
|
201
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Expand glob-shaped contract paths into concrete workspace file paths.
|
|
404
|
+
*
|
|
405
|
+
* The frontend contract schema allows `targets.files` entries such as
|
|
406
|
+
* `src/**` (skeleton inherits the task allowedPaths), but writer admission
|
|
407
|
+
* derives the concrete writeSet and requires real file paths. This helper
|
|
408
|
+
* matches every candidate against the enumerated workspace files and returns
|
|
409
|
+
* the concrete paths. Candidates that are already concrete are kept as-is;
|
|
410
|
+
* globs that match nothing are dropped (the derived writeSet only contains
|
|
411
|
+
* paths the writer can actually touch).
|
|
412
|
+
*/
|
|
413
|
+
export function expandFrontendGlobPaths(input) {
|
|
414
|
+
const concrete = new Set();
|
|
415
|
+
const patterns = [];
|
|
416
|
+
for (const candidate of input.candidates) {
|
|
417
|
+
const normalized = candidate.replace(/\\/g, "/");
|
|
418
|
+
if (normalized.includes("*")) {
|
|
419
|
+
patterns.push(normalized);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
concrete.add(normalized);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (patterns.length === 0)
|
|
426
|
+
return [...concrete].sort();
|
|
427
|
+
const workspace = [...new Set(input.workspaceFiles.map((f) => f.replace(/\\/g, "/")))];
|
|
428
|
+
for (const file of workspace) {
|
|
429
|
+
if (patterns.some((pattern) => pathMatchesPattern(file, pattern))) {
|
|
430
|
+
concrete.add(file);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return [...concrete].sort();
|
|
434
|
+
}
|