@tea-agent/loop-agent 0.42.0-next.9 → 0.43.0-next.1
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 +65 -59
- package/dist/application/dag/run-dag.js +3 -2
- package/dist/application/task-lifecycle/advance.js +9 -4
- package/dist/build-stamp.json +3 -3
- package/dist/cli/program.js +1 -1
- package/dist/commands/dag-rerun-task.js +2 -0
- package/dist/commands/task-source-prepare.js +3 -1
- package/dist/executors/dag-pi-executor.js +1514 -175
- package/dist/executors/pi-extension-resolver.js +14 -2
- package/dist/executors/shell-executor.js +131 -54
- package/dist/shared/dag-failure-category.js +6 -0
- package/dist/task/contract/apply.js +36 -2
- package/dist/task/source-prepare/parse-intent.js +7 -0
- package/dist/worker/console/chat/chat-event-store.js +4 -2
- package/dist/worker/console/chat/pi-runtime.js +45 -2
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +28 -12
- package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
- package/dist/worker/console/chat/session-store.js +5 -1
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/tool-preview.js +10 -0
- package/dist/worker/console/chat/tools.js +11 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-B0Q0nClf.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-DCPjC19G.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-CP5n9jmG.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-CmaWi_Wg.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Ct5tcMfZ.js} +1 -1
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-C0osm1Zf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-CDWviED7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-mtSk1-j7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-D5J9PHC7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-Begg4WAa.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-sF1AEy7T.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-BqbsWXZb.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BKJrPcJ6.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-CcwNMRho.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-BEJi3iIs.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-DRLgTvVu.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-BoAVG3cs.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-Bzol06hR.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-UmliJM77.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BWLcJkFo.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CgApOm-O.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-Bh3RgTLs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-Dsh-D5nE.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-Dkbbpcpb.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-3EBaA3t3.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-Bq2Dc5ok.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-De7y55ha.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-fLiDbQRh.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-Du617mp1.js} +1 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +449 -0
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-BwmlF-XP.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-zsGtS59u.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-DTTPTe3f.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BkXg9aP-.js} +1 -1
- package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-7U2ue5IE.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-BUuGHmWO.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-7q6fX0Sv.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-CkQPJN55.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-CCal_tfX.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-CJmww7D-.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-D6ldWJEv.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-VGh33I9e.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CCkDrIjU.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-NC8f5Otb.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-_92ezZdF.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-BdUCGtbP.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-CdD1H8ia.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-Dkdk3oFo.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-CKu2uPPY.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-ClqAnuG8.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +133 -5
- package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
- package/dist/worker/observe/static/constants.js +22 -22
- package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
- package/dist/worker/observe/static/dag-history-labels.js +1 -0
- package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
- package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
- package/dist/worker/observe/static/dag-node-purpose.js +10 -10
- package/dist/worker/observe/static/index.html +4 -4
- package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
- package/dist/worker/observe/static/relations.js +5 -5
- package/dist/worker/observe/static/styles.css +171 -0
- package/dist/worker/observe/static/views/dag-graph.js +1 -1
- package/dist/worker/observe/static/views/dag-inspector.js +329 -174
- package/dist/worker/observe/static/views/dag.js +16 -12
- package/dist/worker/observe/static/views/session-timeline.js +5 -3
- package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
- package/dist/workflows/dag/dag-retry-schema.js +3 -0
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +101 -16
- package/dist/workflows/dag/frontend-implementation-contract.js +410 -41
- package/dist/workflows/dag/frontend-plan-render.js +13 -2
- package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
- package/dist/workflows/dag/frontend-recovery-controller.js +25 -1
- package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
- package/dist/workflows/dag/frontend-recovery-run.js +142 -16
- package/dist/workflows/dag/frontend-risk.js +2 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
- package/dist/workflows/dag/frontend-shape.js +16 -6
- package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
- package/dist/workflows/dag/frontend-verification-trace.js +38 -4
- package/dist/workflows/dag/frontend-writer-admission.js +2 -2
- package/dist/workflows/dag/init-hybrid.js +57 -28
- package/dist/workflows/dag/node-execution.js +201 -85
- package/dist/workflows/dag/recovery-lease.js +106 -16
- package/dist/workflows/dag/rerun-feedback.js +1 -0
- package/dist/workflows/dag/rerun-plan.js +90 -4
- package/dist/workflows/dag/rerun-run.js +28 -6
- package/dist/workflows/dag/rerun-task.js +221 -18
- package/dist/workflows/dag/retry-policy.js +27 -10
- package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
- package/dist/workflows/dag/runner.js +220 -18
- package/dist/workflows/dag/structured-output-repair.js +4 -1
- package/dist/workflows/dag/types.js +4 -3
- package/dist/workflows/dag/validate.js +10 -8
- package/dist/workflows/dag/workspace-checkpoint.js +66 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/agent-dag.schema.json +2 -2
- package/docs/templates/backend-test-dag.json +6 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/harness.json +1 -1
- package/package.json +4 -5
- package/skills/frontend-plan/SKILL.md +14 -1
- package/skills/frontend-plan/references/decision-contract.md +101 -5
- package/skills/frontend-plan/references/design-decisions.md +32 -0
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
- package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +0 -8
|
@@ -9,10 +9,12 @@ import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
11
|
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
12
|
+
import { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
|
|
13
|
+
export { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
|
|
12
14
|
// Cycle-safe value imports: the check functions are hoisted declarations used
|
|
13
15
|
// only at call time, so the contract module and the policy module can depend
|
|
14
16
|
// on each other without module-init ordering issues.
|
|
15
|
-
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
17
|
+
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, checkRequiredDeliverables, checkUxNameCollisions, } from "./frontend-design-policy.js";
|
|
16
18
|
import { isDagSourceBindingV2 } from "./types.js";
|
|
17
19
|
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
18
20
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
@@ -25,6 +27,7 @@ export const frontendNormalizationActionSchema = z.enum([
|
|
|
25
27
|
"drop-unresolved-verification-symbols",
|
|
26
28
|
"inject-source-binding",
|
|
27
29
|
"compact-canonical-contract",
|
|
30
|
+
"resolve-verification-command-ids",
|
|
28
31
|
]);
|
|
29
32
|
export function sha256Hex(input) {
|
|
30
33
|
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
@@ -95,20 +98,200 @@ export async function writeCompactDeterministicJsonArtifact(runDir, relativePath
|
|
|
95
98
|
return { path: targetPath, sha256: sha256Hex(json) };
|
|
96
99
|
}
|
|
97
100
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
+
* Verification mode v2 (AC-V2-001..004).
|
|
102
|
+
*
|
|
103
|
+
* The mode is owned by the runtime, never by the model: plan tools submit a
|
|
104
|
+
* `commandId` referencing the generation-time frozen command directory, and
|
|
105
|
+
* materialization resolves `mode` + `commandLabel` from that directory. This
|
|
106
|
+
* makes contradictory combinations such as `type: component` bound to
|
|
107
|
+
* `npm run build` unrepresentable instead of failing late at the writer
|
|
108
|
+
* focused-check.
|
|
109
|
+
*/
|
|
110
|
+
export const frontendVerificationModeSchema = z.enum(["static", "behavior"]);
|
|
111
|
+
/**
|
|
112
|
+
* Optional display-only test scope. It never participates in control flow:
|
|
113
|
+
* trace checks, writeSet derivation and behavior-required evaluation read
|
|
114
|
+
* only the runtime-derived `mode`.
|
|
115
|
+
*/
|
|
116
|
+
export const frontendVerificationScopeSchema = z.enum([
|
|
117
|
+
"unit",
|
|
118
|
+
"component",
|
|
119
|
+
"integration",
|
|
120
|
+
]);
|
|
121
|
+
function laneToVerificationMode(lane) {
|
|
122
|
+
return lane === "static" || lane === "lint" ? "static" : "behavior";
|
|
123
|
+
}
|
|
124
|
+
const VERIFY_RUNNER_RE = /\b(vitest|jest|mocha|ava|tap|playwright|cypress|testcafe|nightwatch|webdriverio|pytest|go test)\b/i;
|
|
125
|
+
const VERIFY_E2E_RE = /(^|[\s:"'=/-])e2e([\s"'/-]|$)/i;
|
|
126
|
+
const VERIFY_STATIC_SCRIPT_WORDS = new Set([
|
|
127
|
+
"typecheck",
|
|
128
|
+
"check",
|
|
129
|
+
"lint",
|
|
130
|
+
"eslint",
|
|
131
|
+
"tsc",
|
|
132
|
+
"build",
|
|
133
|
+
]);
|
|
134
|
+
// A script segment carrying test semantics wins over static words:
|
|
135
|
+
// `test:build` builds nothing observable by itself — it runs tests, and
|
|
136
|
+
// `check:unit` is a unit-test entrypoint, not a project-wide check.
|
|
137
|
+
const VERIFY_BEHAVIOR_SCRIPT_WORDS = new Set([
|
|
138
|
+
"test",
|
|
139
|
+
"tests",
|
|
140
|
+
"unit",
|
|
141
|
+
"spec",
|
|
142
|
+
"specs",
|
|
143
|
+
"e2e",
|
|
144
|
+
"integration",
|
|
145
|
+
"coverage",
|
|
146
|
+
]);
|
|
147
|
+
function extractRunScriptName(command) {
|
|
148
|
+
// Skip package-manager flags between the binary and the script, e.g.
|
|
149
|
+
// `npm --prefix website run build` executes the `build` script.
|
|
150
|
+
const tokens = command.match(/(?:npm|pnpm|yarn|bun)(?:\.cmd|\.ps1)?\s+(.+)/i)?.[1];
|
|
151
|
+
if (!tokens)
|
|
152
|
+
return undefined;
|
|
153
|
+
const parts = tokens.split(/\s+/).filter(Boolean);
|
|
154
|
+
let index = 0;
|
|
155
|
+
while (index < parts.length) {
|
|
156
|
+
const part = parts[index].replace(/["']/g, "");
|
|
157
|
+
if (part === "--prefix" ||
|
|
158
|
+
part === "--cwd" ||
|
|
159
|
+
part === "-C" ||
|
|
160
|
+
part === "--workspace") {
|
|
161
|
+
index += 2;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (part.startsWith("-")) {
|
|
165
|
+
index += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (/^(run|exec)$/i.test(part)) {
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
return part;
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Whether a command carries known verification semantics. Markdown intake
|
|
178
|
+
* uses this before lane classification so prose cannot authorize an arbitrary
|
|
179
|
+
* package script such as `npm run deploy`. Explicit task verifyCommands remain
|
|
180
|
+
* operator-authorized and intentionally do not use this admission check.
|
|
101
181
|
*/
|
|
102
|
-
|
|
182
|
+
export function isRecognizedFrontendVerifyCommandText(command) {
|
|
183
|
+
if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command))
|
|
184
|
+
return true;
|
|
185
|
+
const scriptName = extractRunScriptName(command);
|
|
186
|
+
if (scriptName) {
|
|
187
|
+
const segments = scriptName.toLowerCase().split(/[:/_-]+/);
|
|
188
|
+
return segments.some((segment) => VERIFY_BEHAVIOR_SCRIPT_WORDS.has(segment) ||
|
|
189
|
+
VERIFY_STATIC_SCRIPT_WORDS.has(segment));
|
|
190
|
+
}
|
|
191
|
+
const head = command.trim().split(/\s+/, 1)[0] ?? "";
|
|
192
|
+
const binary = head.split(/[\\/]/).pop() ?? "";
|
|
193
|
+
return (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
|
|
194
|
+
/\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
|
|
195
|
+
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command));
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Classify a raw verification command text into the runtime-owned mode.
|
|
199
|
+
* Shared single implementation used by DAG generation (lane split) and
|
|
200
|
+
* documented here so generation and validation can never disagree.
|
|
201
|
+
*
|
|
202
|
+
* Order matters: test runners and e2e harnesses always prove behavior, even
|
|
203
|
+
* when their arguments mention build/check/lint paths (e.g.
|
|
204
|
+
* `npx vitest run test/build.test.ts`). For npm scripts, a segment with
|
|
205
|
+
* test semantics (`test:build`, `check:unit`) wins over static words; pure
|
|
206
|
+
* static scripts (`npm run check`, `check:types`) and leading binaries
|
|
207
|
+
* (`tsc`, `eslint`) stay static. Package-manager flags (`npm --prefix
|
|
208
|
+
* website run build`) are skipped when locating the script.
|
|
209
|
+
*/
|
|
210
|
+
export function classifyFrontendVerifyCommandText(command) {
|
|
211
|
+
if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command)) {
|
|
212
|
+
return "behavior";
|
|
213
|
+
}
|
|
214
|
+
const scriptName = extractRunScriptName(command);
|
|
215
|
+
if (scriptName) {
|
|
216
|
+
const segments = scriptName.toLowerCase().split(/[:/_-]+/);
|
|
217
|
+
if (segments.some((segment) => VERIFY_BEHAVIOR_SCRIPT_WORDS.has(segment))) {
|
|
218
|
+
return "behavior";
|
|
219
|
+
}
|
|
220
|
+
return segments.some((segment) => VERIFY_STATIC_SCRIPT_WORDS.has(segment))
|
|
221
|
+
? "static"
|
|
222
|
+
: "behavior";
|
|
223
|
+
}
|
|
224
|
+
const head = command.trim().split(/\s+/, 1)[0] ?? "";
|
|
225
|
+
const binary = head.split(/[\\/]/).pop() ?? "";
|
|
226
|
+
if (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
|
|
227
|
+
/\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
|
|
228
|
+
/\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)) {
|
|
229
|
+
return "static";
|
|
230
|
+
}
|
|
231
|
+
return "behavior";
|
|
232
|
+
}
|
|
233
|
+
export function slugFrontendVerifyCommandId(label, taken) {
|
|
234
|
+
const slug = label
|
|
235
|
+
.toLowerCase()
|
|
236
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
237
|
+
.replace(/^-+|-+$/g, "")
|
|
238
|
+
.slice(0, 48) || "verify";
|
|
239
|
+
let candidate = `verify-${slug}`;
|
|
240
|
+
let counter = 2;
|
|
241
|
+
while (taken.has(candidate)) {
|
|
242
|
+
candidate = `verify-${slug}-${counter++}`;
|
|
243
|
+
}
|
|
244
|
+
taken.add(candidate);
|
|
245
|
+
return candidate;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Build the generation-time frozen command directory. Lanes come from the
|
|
249
|
+
* DAG generation split (init-hybrid classifies each verify command once);
|
|
250
|
+
* labels stay human-readable while `commandId` is the stable machine key
|
|
251
|
+
* the plan references. Mock/lint lanes keep their provenance in `lane` but
|
|
252
|
+
* map to the behavior/static mode the trace gate enforces. Command texts
|
|
253
|
+
* (parallel to labels; usually from the bundle evidence) bind each id to
|
|
254
|
+
* the exact frozen command so same-label entries across lanes cannot drift
|
|
255
|
+
* into the same identity silently.
|
|
256
|
+
*/
|
|
257
|
+
export function buildFrontendVerifyCommandDirectory(lanes) {
|
|
258
|
+
const taken = new Set();
|
|
259
|
+
const entries = [];
|
|
260
|
+
const push = (labels, lane, texts) => {
|
|
261
|
+
labels.forEach((label, index) => {
|
|
262
|
+
const trimmed = label.trim();
|
|
263
|
+
if (!trimmed)
|
|
264
|
+
return;
|
|
265
|
+
entries.push({
|
|
266
|
+
commandId: slugFrontendVerifyCommandId(trimmed, taken),
|
|
267
|
+
label: trimmed,
|
|
268
|
+
mode: laneToVerificationMode(lane),
|
|
269
|
+
lane,
|
|
270
|
+
command: texts?.[index]?.trim() ?? "",
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
push(lanes.staticLabels, "static", lanes.staticCommandTexts);
|
|
275
|
+
push(lanes.lintLabels ?? [], "lint", lanes.lintCommandTexts);
|
|
276
|
+
push(lanes.behaviorLabels, "behavior", lanes.behaviorCommandTexts);
|
|
277
|
+
push(lanes.mockLabels ?? [], "mock", lanes.mockCommandTexts);
|
|
278
|
+
return entries;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Derive the frozen command directory from the DAG run spec (run.json),
|
|
282
|
+
* reading the per-lane evidence of the `frontend-verify-shell` bundle. The
|
|
283
|
+
* run spec is written before any node executes, so it is always available
|
|
284
|
+
* when the prewrite gate materializes the contract. When absent (e.g. test
|
|
285
|
+
* fixtures), there is no directory to validate against and callers skip
|
|
286
|
+
* directory validation downstream — mirroring the legacy label-set behavior.
|
|
287
|
+
*/
|
|
288
|
+
export async function deriveFrontendVerifyCommandDirectoryFromRun(runDir) {
|
|
103
289
|
const specPath = path.join(runDir, "run.json");
|
|
104
290
|
let raw;
|
|
105
291
|
try {
|
|
106
292
|
raw = await readFile(specPath, "utf8");
|
|
107
293
|
}
|
|
108
294
|
catch {
|
|
109
|
-
// run.json is guaranteed to exist in production (DAG runner writes it
|
|
110
|
-
// before any node executes). When absent (e.g. test fixtures), there is
|
|
111
|
-
// no frozen set to validate against and the check is skipped downstream.
|
|
112
295
|
return [];
|
|
113
296
|
}
|
|
114
297
|
let spec;
|
|
@@ -116,21 +299,90 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
116
299
|
spec = JSON.parse(raw);
|
|
117
300
|
}
|
|
118
301
|
catch (error) {
|
|
119
|
-
throw new Error(`cannot derive
|
|
120
|
-
}
|
|
121
|
-
const
|
|
302
|
+
throw new Error(`cannot derive frontend verify command directory: run.json is not valid JSON at ${specPath}: ${error.message}`);
|
|
303
|
+
}
|
|
304
|
+
const lanes = {
|
|
305
|
+
staticLabels: [],
|
|
306
|
+
behaviorLabels: [],
|
|
307
|
+
mockLabels: [],
|
|
308
|
+
lintLabels: [],
|
|
309
|
+
staticCommandTexts: [],
|
|
310
|
+
behaviorCommandTexts: [],
|
|
311
|
+
mockCommandTexts: [],
|
|
312
|
+
lintCommandTexts: [],
|
|
313
|
+
};
|
|
122
314
|
for (const task of spec.tasks ?? []) {
|
|
123
|
-
const
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
315
|
+
const bundle = task.shell?.frontendVerificationBundle;
|
|
316
|
+
if (!bundle)
|
|
317
|
+
continue;
|
|
318
|
+
const lanePairs = [
|
|
319
|
+
["static", bundle.staticEvidence],
|
|
320
|
+
["behavior", bundle.behaviorEvidence],
|
|
321
|
+
["mock", bundle.mockEvidence],
|
|
322
|
+
["lint", bundle.lintEvidence],
|
|
323
|
+
];
|
|
324
|
+
for (const [lane, evidence] of lanePairs) {
|
|
325
|
+
const labels = evidence?.commandLabels ?? [];
|
|
326
|
+
const texts = evidence?.commandTexts ?? [];
|
|
327
|
+
// run.json is untrusted input: labels and texts must zip 1:1.
|
|
328
|
+
// A lane that violates the alignment fails closed instead of
|
|
329
|
+
// binding ids to the wrong commands.
|
|
330
|
+
if (texts.length > 0 && texts.length !== labels.length) {
|
|
331
|
+
throw new Error(`cannot derive frontend verify command directory: ${lane} evidence commandTexts length (${texts.length}) does not match commandLabels length (${labels.length}) in ${specPath}`);
|
|
332
|
+
}
|
|
333
|
+
lanes[`${lane}Labels`].push(...labels);
|
|
334
|
+
lanes[`${lane}CommandTexts`].push(...texts);
|
|
127
335
|
}
|
|
128
336
|
}
|
|
129
|
-
|
|
337
|
+
if (lanes.staticLabels.length === 0 &&
|
|
338
|
+
lanes.behaviorLabels.length === 0 &&
|
|
339
|
+
lanes.mockLabels.length === 0 &&
|
|
340
|
+
lanes.lintLabels.length === 0) {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
return buildFrontendVerifyCommandDirectory(lanes);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* A behavior verification target must bind a test file: the trace gate
|
|
347
|
+
* proves behavior targets by finding the target id in a real
|
|
348
|
+
* describe/it/test title inside `file`. Enforced at the plan boundary and
|
|
349
|
+
* at materialization (path shape only — the writer may create the file).
|
|
350
|
+
*
|
|
351
|
+
* Only code files count (the gate reads titles from source): fixtures and
|
|
352
|
+
* data files such as `test/fixtures/data.json` are rejected even under a
|
|
353
|
+
* test directory, as are helper/support/setup files (`cypress/fixtures/*`,
|
|
354
|
+
* `tests/support/*`, `setup.*`) that never carry describe/it/test titles.
|
|
355
|
+
* Cypress (`*.cy.*`, `cypress/`) and e2e (`e2e/`) entries are accepted —
|
|
356
|
+
* the repo supports those harnesses.
|
|
357
|
+
*/
|
|
358
|
+
export function isFrontendTestFilePath(filePath) {
|
|
359
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
360
|
+
const base = normalized.split("/").pop() ?? "";
|
|
361
|
+
if (!/\.(c|m)?[jt]sx?$/.test(base))
|
|
362
|
+
return false;
|
|
363
|
+
const segments = normalized.toLowerCase().split("/");
|
|
364
|
+
// `helpers` is deliberately NOT excluded: source helper directories
|
|
365
|
+
// (e.g. `src/helpers/format.test.ts`) legitimately contain unit tests.
|
|
366
|
+
if (segments.some((segment) => segment === "fixtures" || segment === "support")) {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
if (/^setup\.[^/]+$/.test(base))
|
|
370
|
+
return false;
|
|
371
|
+
if (/\.cy\.[^/]+$/.test(base))
|
|
372
|
+
return true;
|
|
373
|
+
if (/(^|\/)__tests__\//.test(normalized))
|
|
374
|
+
return true;
|
|
375
|
+
if (/(^|\/)(tests?|e2e|cypress)\//.test(normalized))
|
|
376
|
+
return true;
|
|
377
|
+
if (/\.test\.[^/]+$/.test(normalized))
|
|
378
|
+
return true;
|
|
379
|
+
if (/\.spec\.[^/]+$/.test(normalized))
|
|
380
|
+
return true;
|
|
381
|
+
return false;
|
|
130
382
|
}
|
|
131
383
|
/**
|
|
132
384
|
* Extract the design-policy allowedDependencies set from the DAG run spec
|
|
133
|
-
* (run.json), mirroring `
|
|
385
|
+
* (run.json), mirroring `deriveFrontendVerifyCommandDirectoryFromRun`. Used by the plan
|
|
134
386
|
* node's front-loaded policy pre-check so dependency declarations can be
|
|
135
387
|
* corrected through the retry ladder before the policy shell (the authority)
|
|
136
388
|
* re-runs the same check on committed facts.
|
|
@@ -350,12 +602,30 @@ const uiComponentChoiceSchema = z
|
|
|
350
602
|
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
351
603
|
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
352
604
|
// Optional: the decision enum already carries the semantic (specified →
|
|
353
|
-
// specReference, new → task-source traceability, reuse-existing →
|
|
354
|
-
//
|
|
355
|
-
// Omit for reuse-existing to keep plan output small.
|
|
605
|
+
// specReference, new → task-source traceability, reuse-existing →
|
|
606
|
+
// evidencePath). Omit for reuse-existing to keep plan output small.
|
|
356
607
|
rationale: z.string().min(1).optional(),
|
|
608
|
+
// Multi-id coverage (dogfood r-plan-quality): ONE choice may cover many
|
|
609
|
+
// UI states/interactions explicitly instead of the old implicit
|
|
610
|
+
// "reuse-existing covers everything" wildcard.
|
|
611
|
+
covers: z.array(z.string().min(1)).optional(),
|
|
612
|
+
// reuse-existing evidence (dogfood dag-1788504923861-0f7b17a9: 18
|
|
613
|
+
// reuse-existing rows cited conventions in a not-yet-written
|
|
614
|
+
// src/planner.ts): a repo-relative path to the existing file that
|
|
615
|
+
// proves the reuse. Enforced by superRefine + fresh-existence checks.
|
|
616
|
+
evidencePath: safePath.optional(),
|
|
357
617
|
})
|
|
358
|
-
.strict()
|
|
618
|
+
.strict()
|
|
619
|
+
.superRefine((choice, ctx) => {
|
|
620
|
+
if (choice.decision === "reuse-existing" &&
|
|
621
|
+
!(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
|
|
622
|
+
ctx.addIssue({
|
|
623
|
+
code: z.ZodIssueCode.custom,
|
|
624
|
+
path: ["evidencePath"],
|
|
625
|
+
message: "decision=reuse-existing requires evidencePath naming the existing repo file that proves the reuse; use decision=new for greenfield paths",
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
});
|
|
359
629
|
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
360
630
|
/**
|
|
361
631
|
* Normalize unstable model emissions before the strict schema sees them:
|
|
@@ -430,6 +700,11 @@ export const frontendImplementationContractSchema = z
|
|
|
430
700
|
files: z.array(safePath).min(1),
|
|
431
701
|
routes: z.array(z.string().startsWith("/")).optional(),
|
|
432
702
|
publicApiChanges: z.array(z.string().min(1)).optional(),
|
|
703
|
+
// Runtime-owned deliverable contract (dogfood r-plan-quality):
|
|
704
|
+
// explicit, source-bound file inventory committed by Contract.
|
|
705
|
+
// The design-policy completeness gate checks ONLY these —
|
|
706
|
+
// allowedPaths is a write boundary, not a coverage obligation.
|
|
707
|
+
requiredDeliverables: z.array(safePath).optional(),
|
|
433
708
|
})
|
|
434
709
|
.strict(),
|
|
435
710
|
requirements: z.array(z
|
|
@@ -507,12 +782,18 @@ export const frontendImplementationContractSchema = z
|
|
|
507
782
|
verificationTargets: z.array(z
|
|
508
783
|
.object({
|
|
509
784
|
id: z.string().min(1),
|
|
510
|
-
|
|
785
|
+
// Runtime-owned verification mode, resolved from commandId
|
|
786
|
+
// against the frozen command directory at materialization.
|
|
787
|
+
mode: frontendVerificationModeSchema,
|
|
788
|
+
// Stable machine key into the frozen command directory.
|
|
789
|
+
commandId: z.string().min(1),
|
|
511
790
|
commandLabel: z.string().min(1),
|
|
512
791
|
file: safePath,
|
|
513
792
|
symbol: z.preprocess((value) => (value === "" || value === null ? undefined : value), z.string().min(1).optional()),
|
|
514
793
|
requirementIds: z.array(id),
|
|
515
794
|
uiStates: z.array(z.string().min(1)),
|
|
795
|
+
// Display-only test scope; never read by control flow.
|
|
796
|
+
scope: frontendVerificationScopeSchema.optional(),
|
|
516
797
|
})
|
|
517
798
|
.strict()).min(1),
|
|
518
799
|
evidenceGaps: z.array(gap).optional(),
|
|
@@ -587,6 +868,27 @@ export const frontendImplementationContractSchema = z
|
|
|
587
868
|
});
|
|
588
869
|
}
|
|
589
870
|
}
|
|
871
|
+
// Behavior evidence must be anchored to be machine-traceable: a
|
|
872
|
+
// behavior target that names no requirement and no UI state produces
|
|
873
|
+
// coverage the trace gate and review cannot verify. Contracts that
|
|
874
|
+
// declare no UI state and no interaction (pure-logic) are exempt from
|
|
875
|
+
// the UI-state half — the schema allows `uiStates: []` there.
|
|
876
|
+
if (target.mode === "behavior" && target.requirementIds.length === 0) {
|
|
877
|
+
ctx.addIssue({
|
|
878
|
+
code: "custom",
|
|
879
|
+
message: `behavior verification target ${target.id} must reference at least one requirement`,
|
|
880
|
+
path: ["verificationTargets"],
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
if (target.mode === "behavior" &&
|
|
884
|
+
target.uiStates.length === 0 &&
|
|
885
|
+
(value.uiStates.length > 0 || value.interactions.length > 0)) {
|
|
886
|
+
ctx.addIssue({
|
|
887
|
+
code: "custom",
|
|
888
|
+
message: `behavior verification target ${target.id} must reference at least one declared UI state`,
|
|
889
|
+
path: ["verificationTargets"],
|
|
890
|
+
});
|
|
891
|
+
}
|
|
590
892
|
}
|
|
591
893
|
for (const requirement of value.requirements) {
|
|
592
894
|
if (!known.has(requirement.id))
|
|
@@ -1357,7 +1659,12 @@ export function compactFrontendImplementationContract(value) {
|
|
|
1357
1659
|
const targets = asRecord(record.targets);
|
|
1358
1660
|
if (targets) {
|
|
1359
1661
|
const compactTargets = { ...targets };
|
|
1360
|
-
for (const field of [
|
|
1662
|
+
for (const field of [
|
|
1663
|
+
"files",
|
|
1664
|
+
"routes",
|
|
1665
|
+
"publicApiChanges",
|
|
1666
|
+
"requiredDeliverables",
|
|
1667
|
+
]) {
|
|
1361
1668
|
if (Object.hasOwn(compactTargets, field)) {
|
|
1362
1669
|
compactTargets[field] = dedupeJsonArray(compactTargets[field]);
|
|
1363
1670
|
}
|
|
@@ -1666,13 +1973,32 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1666
1973
|
for (const [index, item] of rawVerification.entries()) {
|
|
1667
1974
|
const vt = asRecord(item) ?? {};
|
|
1668
1975
|
const id = asString(vt.id) || `VT-${String(index + 1).padStart(3, "0")}`;
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1976
|
+
// Legacy compatibility input may still carry a model-written `type`
|
|
1977
|
+
// and a bare `commandLabel`. Map them onto the v2 shape: the five
|
|
1978
|
+
// legacy types collapse to the runtime mode (only `static` stays
|
|
1979
|
+
// static), the commandId is slugged from the label, and a legacy
|
|
1980
|
+
// unit/component/integration type is preserved as display-only scope.
|
|
1981
|
+
// Production plan facts already carry commandId and are resolved
|
|
1982
|
+
// against the frozen directory during materialization instead.
|
|
1983
|
+
const legacyType = asString(vt.type) || asString(vt.phase);
|
|
1984
|
+
const mode = asString(vt.mode) === "static" ||
|
|
1985
|
+
asString(vt.mode) === "behavior"
|
|
1986
|
+
? asString(vt.mode)
|
|
1987
|
+
: legacyType === "static"
|
|
1673
1988
|
? "static"
|
|
1674
|
-
: "
|
|
1989
|
+
: "behavior";
|
|
1675
1990
|
const commandLabel = asString(vt.commandLabel) || asString(vt.command);
|
|
1991
|
+
const commandId = asString(vt.commandId) ||
|
|
1992
|
+
slugFrontendVerifyCommandId(commandLabel || id, new Set());
|
|
1993
|
+
const scope = asString(vt.scope) === "unit" ||
|
|
1994
|
+
asString(vt.scope) === "component" ||
|
|
1995
|
+
asString(vt.scope) === "integration"
|
|
1996
|
+
? asString(vt.scope)
|
|
1997
|
+
: legacyType === "unit" ||
|
|
1998
|
+
legacyType === "component" ||
|
|
1999
|
+
legacyType === "integration"
|
|
2000
|
+
? legacyType
|
|
2001
|
+
: undefined;
|
|
1676
2002
|
const file = asString(vt.file) ||
|
|
1677
2003
|
(Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
|
|
1678
2004
|
targetFiles.find((p) => p.includes("__tests__")) ||
|
|
@@ -1681,10 +2007,12 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1681
2007
|
const uiStateNames = asStringArray(vt.uiStates);
|
|
1682
2008
|
verificationTargets.push({
|
|
1683
2009
|
id,
|
|
1684
|
-
|
|
2010
|
+
mode,
|
|
2011
|
+
commandId,
|
|
1685
2012
|
commandLabel,
|
|
1686
2013
|
file,
|
|
1687
2014
|
symbol: asString(vt.symbol) || undefined,
|
|
2015
|
+
...(scope ? { scope } : {}),
|
|
1688
2016
|
requirementIds: requirementIds.length > 0
|
|
1689
2017
|
? requirementIds
|
|
1690
2018
|
: [...canonicalBinding.requirementIds],
|
|
@@ -2008,19 +2336,42 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2008
2336
|
catch (error) {
|
|
2009
2337
|
fail("blocked", error instanceof Error ? error.message : String(error), candidateJsonSha256);
|
|
2010
2338
|
}
|
|
2011
|
-
//
|
|
2012
|
-
//
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2339
|
+
// Resolve verificationTarget commandIds against the frozen command
|
|
2340
|
+
// directory (per-lane evidence of the frontend-verify-shell bundle in
|
|
2341
|
+
// run.json). The directory is the only authority for mode and label: a
|
|
2342
|
+
// plan that references an unknown commandId, or binds a behavior command
|
|
2343
|
+
// to a non-test file, is rejected here with plan-attributable codes
|
|
2344
|
+
// instead of failing late at the writer focused-check.
|
|
2345
|
+
const verifyDirectory = await deriveFrontendVerifyCommandDirectoryFromRun(input.runDir);
|
|
2346
|
+
if (verifyDirectory.length > 0) {
|
|
2347
|
+
const byCommandId = new Map(verifyDirectory.map((entry) => [entry.commandId, entry]));
|
|
2348
|
+
const available = verifyDirectory
|
|
2349
|
+
.map((entry) => `${entry.commandId} (${entry.mode}: ${entry.label})`)
|
|
2350
|
+
.join(", ");
|
|
2351
|
+
const parsedRecord = asRecord(parsed);
|
|
2352
|
+
const parsedVt = parsedRecord?.verificationTargets;
|
|
2017
2353
|
if (Array.isArray(parsedVt)) {
|
|
2018
2354
|
for (const vt of parsedVt) {
|
|
2019
|
-
const
|
|
2020
|
-
if (
|
|
2021
|
-
|
|
2355
|
+
const record = asRecord(vt);
|
|
2356
|
+
if (!record)
|
|
2357
|
+
continue;
|
|
2358
|
+
const vtId = asString(record.id) || "(missing id)";
|
|
2359
|
+
const commandId = asString(record.commandId);
|
|
2360
|
+
if (!commandId) {
|
|
2361
|
+
fail("retryable-invalid", `invalid-output: verification target "${vtId}" must reference a frozen command: set commandId to one of [${available}]`, candidateJsonSha256);
|
|
2362
|
+
}
|
|
2363
|
+
const entry = byCommandId.get(commandId);
|
|
2364
|
+
if (!entry) {
|
|
2365
|
+
fail("retryable-invalid", `invalid-output: verification-target-unknown-command: verification target "${vtId}" references unknown commandId "${commandId}"; available frozen commands: [${available}]`, candidateJsonSha256);
|
|
2366
|
+
}
|
|
2367
|
+
record.mode = entry.mode;
|
|
2368
|
+
record.commandLabel = entry.label;
|
|
2369
|
+
const file = asString(record.file);
|
|
2370
|
+
if (entry.mode === "behavior" && file && !isFrontendTestFilePath(file)) {
|
|
2371
|
+
fail("retryable-invalid", `invalid-output: verification-target-phase-mismatch: verification target "${vtId}" binds behavior command "${entry.label}" (${entry.commandId}) to non-test file "${file}"; behavior targets must bind a test file (__tests__/, tests?/, e2e/, cypress/, *.test.*, *.spec.*, *.cy.*) so the trace gate can match the target id in a real describe/it/test title`, candidateJsonSha256);
|
|
2022
2372
|
}
|
|
2023
2373
|
}
|
|
2374
|
+
pushAction("resolve-verification-command-ids");
|
|
2024
2375
|
}
|
|
2025
2376
|
}
|
|
2026
2377
|
const parsedTargets = asRecord(parsed)?.targets;
|
|
@@ -2104,6 +2455,16 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2104
2455
|
files: referencedConcretePaths,
|
|
2105
2456
|
};
|
|
2106
2457
|
}
|
|
2458
|
+
// Both Plan precheck and policy materialization consume this exact inventory.
|
|
2459
|
+
// Never trust a Plan patch or infer delivery obligations from prose.
|
|
2460
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.contractNodeId ?? "frontend-contract-pi", "contract-typed-facts.jsonl"));
|
|
2461
|
+
const requiredDeliverables = extractRequiredDeliverablePaths(contractRecords
|
|
2462
|
+
.filter((record) => record.phase === "committed")
|
|
2463
|
+
.map((record) => record.fact));
|
|
2464
|
+
delete result.data.targets.requiredDeliverables;
|
|
2465
|
+
if (requiredDeliverables.length > 0) {
|
|
2466
|
+
result.data.targets.requiredDeliverables = requiredDeliverables;
|
|
2467
|
+
}
|
|
2107
2468
|
const blockingGaps = [
|
|
2108
2469
|
...(result.data.evidenceGaps ?? []),
|
|
2109
2470
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
@@ -2311,6 +2672,7 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
2311
2672
|
"/sourceBinding",
|
|
2312
2673
|
"/riskLevel",
|
|
2313
2674
|
"/targets/files",
|
|
2675
|
+
"/targets/requiredDeliverables",
|
|
2314
2676
|
"/mockApi/productionDefaultOff",
|
|
2315
2677
|
];
|
|
2316
2678
|
const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
@@ -2359,6 +2721,8 @@ function frontendPlanPatchProtectedPathViolations(patch) {
|
|
|
2359
2721
|
const targets = asRecord(record.targets);
|
|
2360
2722
|
if (targets && Object.hasOwn(targets, "files"))
|
|
2361
2723
|
violations.push("/targets/files");
|
|
2724
|
+
if (targets && Object.hasOwn(targets, "requiredDeliverables"))
|
|
2725
|
+
violations.push("/targets/requiredDeliverables");
|
|
2362
2726
|
const mockApi = asRecord(record.mockApi);
|
|
2363
2727
|
if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
|
|
2364
2728
|
violations.push("/mockApi/productionDefaultOff");
|
|
@@ -2395,7 +2759,7 @@ async function dropUnresolvedVerificationSymbols(input) {
|
|
|
2395
2759
|
// it. This is the root fix for the recurring fabricated-symbol error
|
|
2396
2760
|
// class: the field was allowed for static, the prompt did not forbid
|
|
2397
2761
|
// it, and models habitually fill it; the compiler removes it instead.
|
|
2398
|
-
if (target.
|
|
2762
|
+
if (target.mode === "static") {
|
|
2399
2763
|
if (symbol) {
|
|
2400
2764
|
delete target.symbol;
|
|
2401
2765
|
if (!actions.includes("strip-static-verification-symbols"))
|
|
@@ -2543,6 +2907,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
|
2543
2907
|
const policyPreFindings = [
|
|
2544
2908
|
...checkUiDesignCoverage(analysis.canonical),
|
|
2545
2909
|
...checkUiStateAttribution(analysis.canonical),
|
|
2910
|
+
...checkRequiredDeliverables(analysis.canonical),
|
|
2911
|
+
...checkUxNameCollisions(analysis.canonical),
|
|
2546
2912
|
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2547
2913
|
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
2548
2914
|
];
|
|
@@ -2761,9 +3127,11 @@ export async function buildContractFromCommittedFacts(input) {
|
|
|
2761
3127
|
}
|
|
2762
3128
|
// b. Committed contract terminal fact (contract-finalized).
|
|
2763
3129
|
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
|
|
2764
|
-
const
|
|
3130
|
+
const committedContractFacts = contractRecords
|
|
2765
3131
|
.filter((record) => record.phase === "committed")
|
|
2766
|
-
.map((record) => record.fact
|
|
3132
|
+
.map((record) => record.fact);
|
|
3133
|
+
const contractKinds = new Set(committedContractFacts
|
|
3134
|
+
.map((fact) => fact?.kind)
|
|
2767
3135
|
.filter((kind) => typeof kind === "string"));
|
|
2768
3136
|
if (!contractKinds.has("contract-finalized")) {
|
|
2769
3137
|
return {
|
|
@@ -2818,6 +3186,7 @@ export async function buildContractFromCommittedFacts(input) {
|
|
|
2818
3186
|
runDir: input.runDir,
|
|
2819
3187
|
rawContractText: serializeDeterministicJson(merged),
|
|
2820
3188
|
sourceBinding: input.sourceBinding,
|
|
3189
|
+
contractNodeId,
|
|
2821
3190
|
});
|
|
2822
3191
|
return {
|
|
2823
3192
|
ok: true,
|
|
@@ -12,8 +12,19 @@ function bulletList(items) {
|
|
|
12
12
|
return "";
|
|
13
13
|
return items.map((item) => `- ${item}`).join("\n");
|
|
14
14
|
}
|
|
15
|
+
/** The runtime's deterministic fallback for an outcome the plan did not
|
|
16
|
+
* author. It is bookkeeping for the schema's non-empty constraint, not user
|
|
17
|
+
* prose — the renderer must never present it as requirement content. */
|
|
18
|
+
const IMPLIED_OUTCOME_PREFIX = "implied by contract:";
|
|
15
19
|
function requirementsCoverage(contract) {
|
|
16
|
-
const lines = contract.requirements.map((requirement) =>
|
|
20
|
+
const lines = contract.requirements.map((requirement) => {
|
|
21
|
+
const targets = requirement.implementationTargets.join(", ");
|
|
22
|
+
const verificationTargets = requirement.verificationTargetIds.join(", ");
|
|
23
|
+
const authoredOutcome = requirement.expectedOutcome.startsWith(IMPLIED_OUTCOME_PREFIX)
|
|
24
|
+
? ""
|
|
25
|
+
: ` — ${requirement.expectedOutcome}`;
|
|
26
|
+
return `- ${requirement.id} → ${targets || "(no targets)"} [${verificationTargets || "no VT"}]${authoredOutcome}`;
|
|
27
|
+
});
|
|
17
28
|
return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
|
|
18
29
|
}
|
|
19
30
|
function uiStateHandling(contract) {
|
|
@@ -98,7 +109,7 @@ export function renderFrontendPlanMarkdown(contract) {
|
|
|
98
109
|
interactionNotes(contract),
|
|
99
110
|
"",
|
|
100
111
|
"## Mock & API Strategy",
|
|
101
|
-
contract.mockApi
|
|
112
|
+
contract.mockApi?.strategy || "_(not specified)_",
|
|
102
113
|
"",
|
|
103
114
|
"## Dependency Policy",
|
|
104
115
|
contract.dependencyPolicy || "_(not specified)_",
|
|
@@ -734,7 +734,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
734
734
|
failureCode: "target-outside-write-set",
|
|
735
735
|
});
|
|
736
736
|
}
|
|
737
|
-
const nonStaticVTs = analysis.canonical.verificationTargets.filter((vt) => vt.
|
|
737
|
+
const nonStaticVTs = analysis.canonical.verificationTargets.filter((vt) => vt.mode !== "static");
|
|
738
738
|
const uncoveredVTs = nonStaticVTs.filter((vt) => ![...writeSet].some((pattern) => pathMatchesPattern(vt.file, pattern) || vt.file === pattern));
|
|
739
739
|
if (uncoveredVTs.length > 0) {
|
|
740
740
|
return finalizePrewrite(input, {
|