@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.
Files changed (157) hide show
  1. package/CHANGELOG.md +65 -59
  2. package/dist/application/dag/run-dag.js +3 -2
  3. package/dist/application/task-lifecycle/advance.js +9 -4
  4. package/dist/build-stamp.json +3 -3
  5. package/dist/cli/program.js +1 -1
  6. package/dist/commands/dag-rerun-task.js +2 -0
  7. package/dist/commands/task-source-prepare.js +3 -1
  8. package/dist/executors/dag-pi-executor.js +1514 -175
  9. package/dist/executors/pi-extension-resolver.js +14 -2
  10. package/dist/executors/shell-executor.js +131 -54
  11. package/dist/shared/dag-failure-category.js +6 -0
  12. package/dist/task/contract/apply.js +36 -2
  13. package/dist/task/source-prepare/parse-intent.js +7 -0
  14. package/dist/worker/console/chat/chat-event-store.js +4 -2
  15. package/dist/worker/console/chat/pi-runtime.js +45 -2
  16. package/dist/worker/console/chat/resource-loader.js +4 -1
  17. package/dist/worker/console/chat/routes.js +28 -12
  18. package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
  19. package/dist/worker/console/chat/session-store.js +5 -1
  20. package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
  21. package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
  22. package/dist/worker/console/chat/subagents/index.js +5 -0
  23. package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
  24. package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
  25. package/dist/worker/console/chat/subagents/types.js +13 -0
  26. package/dist/worker/console/chat/tool-preview.js +10 -0
  27. package/dist/worker/console/chat/tools.js +11 -1
  28. package/dist/worker/console/chat/turn-process.js +1 -0
  29. package/dist/worker/console/interview/tools.js +1 -0
  30. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C6n9_m0P.js → abnfDiagram-N423BO3Z-B0Q0nClf.js} +1 -1
  31. package/dist/worker/console/static/assets/{arc-DQh-IfZ1.js → arc-DCPjC19G.js} +1 -1
  32. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-54NnrwUC.js → architectureDiagram-T3A2C74G-CP5n9jmG.js} +1 -1
  33. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-pivRALGK.js → blockDiagram-VBNYF7ZC-CmaWi_Wg.js} +1 -1
  34. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-BR7OV2NJ.js → c4Diagram-5PPSVZJV-Ct5tcMfZ.js} +1 -1
  35. package/dist/worker/console/static/assets/channel-DAS07MdS.js +1 -0
  36. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-B1Aq6BcK.js → chunk-2GRJ4B5K-C0osm1Zf.js} +1 -1
  37. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CbdU0rjo.js → chunk-2Q5K7J3B-CDWviED7.js} +1 -1
  38. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-Dqi2GJeD.js → chunk-5RXB4S5H-mtSk1-j7.js} +1 -1
  39. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BYn1Hu4R.js → chunk-5VM5RSS4-D5J9PHC7.js} +1 -1
  40. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DRLjDL0k.js → chunk-6Q2QTUOP-Begg4WAa.js} +1 -1
  41. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CY9Xx2jV.js → chunk-GF5L2VYU-sF1AEy7T.js} +1 -1
  42. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BNCZs7_z.js → chunk-JWPE2WC7-BqbsWXZb.js} +1 -1
  43. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DZ8AStLL.js → chunk-KBJHAD2P-BKJrPcJ6.js} +1 -1
  44. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DsxjYrzz.js → chunk-RYQCIY6F-CcwNMRho.js} +1 -1
  45. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-Ddx1KC1l.js → chunk-XXDRQBXY-BEJi3iIs.js} +1 -1
  46. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +1 -0
  47. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +1 -0
  48. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-W9TveCnK.js → cose-bilkent-JH36ORCC-DRLgTvVu.js} +1 -1
  49. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-l9j_ztZH.js → cynefin-VYW2F7L2-BoAVG3cs.js} +1 -1
  50. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BMJMKi4G.js → cynefinDiagram-MW4NZA55-Bzol06hR.js} +1 -1
  51. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-Bb4mG9pH.js → dagre-VZM6K2ZE-UmliJM77.js} +1 -1
  52. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BMgL_Qv0.js → diagram-7IWD3JNH-BWLcJkFo.js} +1 -1
  53. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-Bvo2T4OQ.js → diagram-B4RE2ZJO-CgApOm-O.js} +1 -1
  54. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-_5kWRN9c.js → diagram-LBJQPF4R-Bh3RgTLs.js} +1 -1
  55. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DqdMrltM.js → diagram-Q27KOJAE-Dsh-D5nE.js} +1 -1
  56. package/dist/worker/console/static/assets/{diagram-UB23O5K3-CDDYsNkp.js → diagram-UB23O5K3-Dkbbpcpb.js} +1 -1
  57. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-c4nfnZUV.js → ebnfDiagram-BXEA7PRR-3EBaA3t3.js} +1 -1
  58. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-DnkUcNMs.js → erDiagram-JOGREHBK-Bq2Dc5ok.js} +1 -1
  59. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-Dz0KGLZE.js → flowDiagram-UKHOOZJN-De7y55ha.js} +1 -1
  60. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Cj1t1uka.js → ganttDiagram-PKOTCBZU-fLiDbQRh.js} +1 -1
  61. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-tp2FrBHd.js → gitGraphDiagram-DS77QQ5N-Du617mp1.js} +1 -1
  62. package/dist/worker/console/static/assets/index-C0O48S_P.js +449 -0
  63. package/dist/worker/console/static/assets/index-CzKf4U8P.css +1 -0
  64. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQwJS8DH.js → infoDiagram-6WML65LV-BwmlF-XP.js} +1 -1
  65. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-eimCQWD4.js → ishikawaDiagram-WSZJBQD7-zsGtS59u.js} +1 -1
  66. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-Dd9Gbwgv.js → journeyDiagram-NVQOT4AX-DTTPTe3f.js} +1 -1
  67. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Qve3cyft.js → kanban-definition-27J2QSJJ-BkXg9aP-.js} +1 -1
  68. package/dist/worker/console/static/assets/{linear-UXKSe36Z.js → linear-7U2ue5IE.js} +1 -1
  69. package/dist/worker/console/static/assets/{mermaid.core-CWhj4JXN.js → mermaid.core-BUuGHmWO.js} +5 -5
  70. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-yTcwf4SJ.js → mindmap-definition-FAOFIHXS-7q6fX0Sv.js} +1 -1
  71. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-_7qToaZP.js → pegDiagram-VL7TDLO6-CkQPJN55.js} +1 -1
  72. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CXmrKmDm.js → pieDiagram-7S7Q4E2Y-CCal_tfX.js} +1 -1
  73. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-hMmrJyaS.js → quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js} +1 -1
  74. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BXw8tCBe.js → railroadDiagram-AXF67PYL-CJmww7D-.js} +1 -1
  75. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BmjgnLZl.js → requirementDiagram-LRYGKXZP-D6ldWJEv.js} +1 -1
  76. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-Bj77SPpN.js → sankeyDiagram-W5VNT64P-VGh33I9e.js} +1 -1
  77. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-B2FDVysL.js → sequenceDiagram-SI44F4Z6-CCkDrIjU.js} +1 -1
  78. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-UChNY9ZZ.js → sizeCapture-X5ZJPWSS-NC8f5Otb.js} +1 -1
  79. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-wbAEkbd7.js → stateDiagram-OKZ733FA-_92ezZdF.js} +1 -1
  80. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +1 -0
  81. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DCEkU8HR.js → swimlanes-SLNWSIFB-BdUCGtbP.js} +2 -2
  82. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +8 -0
  83. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-BGp7H06U.js → timeline-definition-Z64GVDOM-CdD1H8ia.js} +1 -1
  84. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DsCIQuWR.js → vennDiagram-T6HMQDX7-Dkdk3oFo.js} +1 -1
  85. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-cRZLP4Xe.js → wardleyDiagram-T6FBY63Y-CKu2uPPY.js} +1 -1
  86. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CP0xLR9Y.js → xychartDiagram-ELKLHX3M-ClqAnuG8.js} +1 -1
  87. package/dist/worker/console/static/index.html +2 -2
  88. package/dist/worker/console/static-src/chat-view-types.js +1 -1
  89. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
  90. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
  91. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
  92. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
  93. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +2 -0
  94. package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
  95. package/dist/worker/console/static-src/operator-chat/useChatThread.js +133 -5
  96. package/dist/worker/console/static-src/shell/workspace-route.js +4 -0
  97. package/dist/worker/observe/static/constants.js +22 -22
  98. package/dist/worker/observe/static/dag-context-reason-labels.js +19 -0
  99. package/dist/worker/observe/static/dag-history-labels.js +1 -0
  100. package/dist/worker/observe/static/dag-inspector-humanize.d.ts +16 -0
  101. package/dist/worker/observe/static/dag-inspector-humanize.js +339 -0
  102. package/dist/worker/observe/static/dag-node-purpose.js +10 -10
  103. package/dist/worker/observe/static/index.html +4 -4
  104. package/dist/worker/observe/static/prompt-restart-candidates.js +4 -2
  105. package/dist/worker/observe/static/relations.js +5 -5
  106. package/dist/worker/observe/static/styles.css +171 -0
  107. package/dist/worker/observe/static/views/dag-graph.js +1 -1
  108. package/dist/worker/observe/static/views/dag-inspector.js +329 -174
  109. package/dist/worker/observe/static/views/dag.js +16 -12
  110. package/dist/worker/observe/static/views/session-timeline.js +5 -3
  111. package/dist/workflows/dag/backend-test-plan-protocol.js +104 -0
  112. package/dist/workflows/dag/dag-retry-schema.js +3 -0
  113. package/dist/workflows/dag/frontend-contract-facts.js +130 -0
  114. package/dist/workflows/dag/frontend-design-policy.js +101 -16
  115. package/dist/workflows/dag/frontend-implementation-contract.js +410 -41
  116. package/dist/workflows/dag/frontend-plan-render.js +13 -2
  117. package/dist/workflows/dag/frontend-prewrite-gate.js +1 -1
  118. package/dist/workflows/dag/frontend-recovery-controller.js +25 -1
  119. package/dist/workflows/dag/frontend-recovery-plan.js +4 -1
  120. package/dist/workflows/dag/frontend-recovery-run.js +142 -16
  121. package/dist/workflows/dag/frontend-risk.js +2 -0
  122. package/dist/workflows/dag/frontend-shadow-dual-write.js +17 -1
  123. package/dist/workflows/dag/frontend-shape.js +16 -6
  124. package/dist/workflows/dag/frontend-test-execution-evidence.js +48 -0
  125. package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
  126. package/dist/workflows/dag/frontend-verification-trace.js +38 -4
  127. package/dist/workflows/dag/frontend-writer-admission.js +2 -2
  128. package/dist/workflows/dag/init-hybrid.js +57 -28
  129. package/dist/workflows/dag/node-execution.js +201 -85
  130. package/dist/workflows/dag/recovery-lease.js +106 -16
  131. package/dist/workflows/dag/rerun-feedback.js +1 -0
  132. package/dist/workflows/dag/rerun-plan.js +90 -4
  133. package/dist/workflows/dag/rerun-run.js +28 -6
  134. package/dist/workflows/dag/rerun-task.js +221 -18
  135. package/dist/workflows/dag/retry-policy.js +27 -10
  136. package/dist/workflows/dag/runner-exit-diagnostics.js +125 -0
  137. package/dist/workflows/dag/runner.js +220 -18
  138. package/dist/workflows/dag/structured-output-repair.js +4 -1
  139. package/dist/workflows/dag/types.js +4 -3
  140. package/dist/workflows/dag/validate.js +10 -8
  141. package/dist/workflows/dag/workspace-checkpoint.js +66 -0
  142. package/docs/operations/README.md +1 -0
  143. package/docs/templates/agent-dag.schema.json +2 -2
  144. package/docs/templates/backend-test-dag.json +6 -4
  145. package/docs/templates/frontend-implementation-contract.schema.json +34 -2
  146. package/harness.json +1 -1
  147. package/package.json +4 -5
  148. package/skills/frontend-plan/SKILL.md +14 -1
  149. package/skills/frontend-plan/references/decision-contract.md +101 -5
  150. package/skills/frontend-plan/references/design-decisions.md +32 -0
  151. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +0 -1
  152. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +0 -1
  153. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +0 -1
  154. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +0 -449
  155. package/dist/worker/console/static/assets/index-DDQc5a50.css +0 -1
  156. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +0 -1
  157. 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
- * Extract frozen command labels from the DAG run spec (run.json).
99
- * The run spec is written before any node executes, so it is always available
100
- * when the prewrite gate materializes the contract.
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
- async function deriveFrozenCommandLabelsFromRun(runDir) {
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 frozen command labels: run.json is not valid JSON at ${specPath}: ${error.message}`);
120
- }
121
- const labels = new Set();
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 cmdLabels = task.shell?.verifyEvidence?.commandLabels;
124
- if (cmdLabels) {
125
- for (const label of cmdLabels)
126
- labels.add(label);
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
- return [...labels];
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 `deriveFrozenCommandLabelsFromRun`. Used by the plan
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 → no
354
- // rationale needed).
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
- type: z.enum(["static", "unit", "component", "integration", "mock"]),
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 ["files", "routes", "publicApiChanges"]) {
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
- const typeRaw = asString(vt.type) || asString(vt.phase) || "unit";
1670
- const type = ["static", "unit", "component", "integration", "mock"].includes(typeRaw)
1671
- ? typeRaw
1672
- : typeRaw.includes("type")
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
- : "unit";
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
- type,
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
- // Validate verificationTarget commandLabels against frozen command set.
2012
- // The frozen set is derived from DAG verification shell task verifyEvidence.
2013
- const frozenLabels = await deriveFrozenCommandLabelsFromRun(input.runDir);
2014
- if (frozenLabels.length > 0) {
2015
- const frozen = new Set(frozenLabels);
2016
- const parsedVt = asRecord(parsed)?.verificationTargets;
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 label = asString(asRecord(vt)?.commandLabel);
2020
- if (label && !frozen.has(label)) {
2021
- fail("blocked", `invalid-output: verificationTarget commandLabel "${label}" is not in the frozen command set [${[...frozen].join(", ")}]`, candidateJsonSha256);
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.type === "static") {
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 contractKinds = new Set(contractRecords
3130
+ const committedContractFacts = contractRecords
2765
3131
  .filter((record) => record.phase === "committed")
2766
- .map((record) => record.fact.kind)
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) => `- ${requirement.id}: ${requirement.expectedOutcome}`);
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.strategy,
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.type !== "static");
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, {