@tea-agent/loop-agent 0.39.0-beta.1 → 0.39.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1645 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1150 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +42 -16
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +347 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -150
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +570 -457
  164. package/dist/workflows/dag/node-execution.js +306 -59
  165. package/dist/workflows/dag/prompt.js +10 -18
  166. package/dist/workflows/dag/rerun-plan.js +144 -5
  167. package/dist/workflows/dag/rerun-run.js +58 -6
  168. package/dist/workflows/dag/retry-policy.js +228 -4
  169. package/dist/workflows/dag/runner.js +314 -119
  170. package/dist/workflows/dag/scheduler.js +114 -16
  171. package/dist/workflows/dag/structured-output-repair.js +712 -0
  172. package/dist/workflows/dag/types.js +235 -19
  173. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  174. package/dist/workflows/dag/validate.js +60 -10
  175. package/docs/init-surface.manifest.json +5 -0
  176. package/docs/operations/README.md +1 -1
  177. package/docs/templates/agent-dag.schema.json +44 -0
  178. package/docs/templates/frontend-test-dag.json +8 -10
  179. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  180. package/docs/templates/init-managed-agents.md +13 -3
  181. package/harness.json +3 -3
  182. package/package.json +5 -1
  183. package/scripts/next-info.mjs +356 -0
  184. package/scripts/next-publish-gate.mjs +238 -0
  185. package/scripts/release-source-binding.mjs +251 -0
  186. package/skills/codebase-scout/SKILL.md +1 -1
  187. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  188. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  189. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  190. package/skills/frontend-implementation/SKILL.md +4 -4
  191. package/skills/frontend-implementation/references/design-spec.md +11 -7
  192. package/skills/frontend-implementation/references/node-contracts.md +20 -11
  193. package/skills/loop-agent/references/command-reference.md +11 -4
  194. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  195. package/skills/playwright-cli/SKILL.md +22 -0
  196. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  197. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  198. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  199. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  200. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  201. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  202. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -8,6 +8,13 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
8
8
  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
+ import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
12
+ // Cycle-safe value imports: the check functions are hoisted declarations used
13
+ // only at call time, so the contract module and the policy module can depend
14
+ // on each other without module-init ordering issues.
15
+ import { checkDependencies, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
16
+ import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
17
+ import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
11
18
  export const frontendNormalizationActionSchema = z.enum([
12
19
  "remove-trailing-commas",
13
20
  "strip-comments",
@@ -102,6 +109,39 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
102
109
  }
103
110
  return [...labels];
104
111
  }
112
+ /**
113
+ * Extract the design-policy allowedDependencies set from the DAG run spec
114
+ * (run.json), mirroring `deriveFrozenCommandLabelsFromRun`. Used by the plan
115
+ * node's front-loaded policy pre-check so dependency declarations can be
116
+ * corrected through the retry ladder before the policy shell (the authority)
117
+ * re-runs the same check on committed facts.
118
+ */
119
+ async function deriveAllowedDependenciesFromRun(runDir) {
120
+ const specPath = path.join(runDir, "run.json");
121
+ let raw;
122
+ try {
123
+ raw = await readFile(specPath, "utf8");
124
+ }
125
+ catch {
126
+ return [];
127
+ }
128
+ let spec;
129
+ try {
130
+ spec = JSON.parse(raw);
131
+ }
132
+ catch {
133
+ return [];
134
+ }
135
+ const deps = new Set();
136
+ for (const task of spec.tasks ?? []) {
137
+ const allowed = task.shell?.frontendDesignPolicy?.allowedDependencies;
138
+ if (allowed) {
139
+ for (const dep of allowed)
140
+ deps.add(dep);
141
+ }
142
+ }
143
+ return [...deps];
144
+ }
105
145
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
106
146
  export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
107
147
  /**
@@ -1079,6 +1119,40 @@ function looksLikeStrictFrontendContract(value) {
1079
1119
  asRecord(record.mockApi) !== null &&
1080
1120
  asRecord(record.designEvidence) !== null);
1081
1121
  }
1122
+ /**
1123
+ * Single shared completion layer for required-with-fallback contract fields.
1124
+ *
1125
+ * Structural invariant: the coerce normalizer has strict-shape and free-form
1126
+ * branches that reshape differently, but every required key that carries a
1127
+ * deterministic fallback (currently `designEvidence`) must be completed in
1128
+ * exactly ONE place — here — so the branches can never drift again (the
1129
+ * designEvidence Required and invented-uiStates failures both came from
1130
+ * branch-local rules diverging). This layer never invents values that later
1131
+ * refinements reject: it only fills documented fallbacks for keys already
1132
+ * present-but-partial, and is idempotent for already-complete inputs.
1133
+ */
1134
+ export function completeCanonicalRequiredFields(value) {
1135
+ const record = asRecord(value);
1136
+ if (!record)
1137
+ return value;
1138
+ const designEvidence = asRecord(record.designEvidence);
1139
+ if (!designEvidence)
1140
+ return value;
1141
+ const source = asString(designEvidence.source);
1142
+ const paths = Array.isArray(designEvidence.paths);
1143
+ const conflicts = Array.isArray(designEvidence.conflicts);
1144
+ if (source && paths && conflicts)
1145
+ return value;
1146
+ return {
1147
+ ...record,
1148
+ designEvidence: {
1149
+ ...designEvidence,
1150
+ ...(source ? {} : { source: "repository-fallback+task-source" }),
1151
+ ...(paths ? {} : { paths: [] }),
1152
+ ...(conflicts ? {} : { conflicts: [] }),
1153
+ },
1154
+ };
1155
+ }
1082
1156
  /**
1083
1157
  * Coerce common free-form plan JSON into frontend-implementation-contract-v1.
1084
1158
  * Near-schema payloads are left untouched so unknown-key fail-closed still holds.
@@ -1183,6 +1257,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1183
1257
  ...strictRecord,
1184
1258
  requirements,
1185
1259
  ...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
1260
+ // Required-with-fallback keys (designEvidence etc.) are completed
1261
+ // by the single shared `completeCanonicalRequiredFields` layer in
1262
+ // the analyze pipeline — never per-branch.
1186
1263
  };
1187
1264
  }
1188
1265
  }
@@ -1231,9 +1308,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1231
1308
  : typeRaw.includes("type")
1232
1309
  ? "static"
1233
1310
  : "unit";
1234
- const commandLabel = asString(vt.commandLabel) ||
1235
- asString(vt.command) ||
1236
- (type === "static" ? "npm run typecheck" : "npm run test:unit:fe");
1311
+ const commandLabel = asString(vt.commandLabel) || asString(vt.command);
1237
1312
  const file = asString(vt.file) ||
1238
1313
  (Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
1239
1314
  targetFiles.find((p) => p.includes("__tests__")) ||
@@ -1249,7 +1324,11 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1249
1324
  requirementIds: requirementIds.length > 0
1250
1325
  ? requirementIds
1251
1326
  : [...canonicalBinding.requirementIds],
1252
- uiStates: uiStateNames.length > 0 ? uiStateNames : ["success", "error"],
1327
+ // Never invent UI state references: "success" is not a canonical
1328
+ // state name, so a defaulted ["success","error"] was always rejected
1329
+ // by the schema's unknown-uiState refinement — a verification
1330
+ // target that declares no UI binding stays unbound ([]).
1331
+ uiStates: uiStateNames,
1253
1332
  });
1254
1333
  }
1255
1334
  if (verificationTargets.length === 0) {
@@ -1485,7 +1564,13 @@ export async function analyzeFrontendImplementationContract(input) {
1485
1564
  rawOutput = input.rawContractText;
1486
1565
  }
1487
1566
  else if (input.fromNodeId !== undefined) {
1488
- const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1567
+ let record;
1568
+ try {
1569
+ record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1570
+ }
1571
+ catch (error) {
1572
+ throw new Error(`cannot parse node record ${input.fromNodeId}: ${error instanceof Error ? error.message : String(error)}`);
1573
+ }
1489
1574
  rawOutput = record.assistantText ?? record.stdout ?? "";
1490
1575
  }
1491
1576
  else {
@@ -1593,8 +1678,9 @@ export async function analyzeFrontendImplementationContract(input) {
1593
1678
  // There is exactly one post-security candidate. A fallback candidate would
1594
1679
  // allow malformed raw fields to bypass the boundary checks above.
1595
1680
  const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
1681
+ const completedContract = completeCanonicalRequiredFields(normalizedContract);
1596
1682
  const candidate = deriveFrontendVerificationCoverage({
1597
- ...(asRecord(normalizedContract) ?? parsed),
1683
+ ...(asRecord(completedContract) ?? parsed),
1598
1684
  sourceBinding: canonicalBinding,
1599
1685
  }, canonicalBinding);
1600
1686
  const result = frontendImplementationContractSchema.safeParse(candidate);
@@ -1634,6 +1720,37 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
1634
1720
  "/targets/files",
1635
1721
  "/mockApi/productionDefaultOff",
1636
1722
  ];
1723
+ const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
1724
+ "target-surface",
1725
+ "component-choice",
1726
+ "state-flow",
1727
+ "data-flow",
1728
+ "mock-api",
1729
+ "design-deviation",
1730
+ "dependency",
1731
+ ];
1732
+ /**
1733
+ * Restore the editable RFC 7386 plan patch from a committed plan ledger.
1734
+ * This is the single compile authority for the frontend implementation
1735
+ * contract: the tool payload (`patch` on any committed plan fact) wins over
1736
+ * any narrative JSON in the plan node text.
1737
+ */
1738
+ export function restorePlanPatchFromCommittedFacts(records) {
1739
+ const planFacts = records
1740
+ .filter((record) => record.phase === "committed")
1741
+ .map((record) => record.fact)
1742
+ .filter((fact) => fact.origin === "plan" &&
1743
+ typeof fact.kind === "string" &&
1744
+ PLAN_LEDGER_FACT_KINDS_FOR_COMPILE.includes(fact.kind));
1745
+ if (planFacts.length === 0)
1746
+ return undefined;
1747
+ for (const fact of planFacts) {
1748
+ const patch = fact.patch;
1749
+ if (isPlainObject(patch))
1750
+ return patch;
1751
+ }
1752
+ return undefined;
1753
+ }
1637
1754
  function frontendPlanPatchProtectedPathViolations(patch) {
1638
1755
  const record = asRecord(patch);
1639
1756
  if (!record)
@@ -1663,34 +1780,34 @@ function workspaceRootFromDagRunDir(runDir) {
1663
1780
  const joined = segments.slice(0, harnessIndex).join(path.sep);
1664
1781
  return joined || path.parse(absolute).root;
1665
1782
  }
1666
- function verificationSymbolMatchesFile(symbol, content) {
1667
- if (symbol.trim().toLowerCase() === "all describe blocks")
1668
- return content.includes("describe(");
1669
- const symbols = symbol.includes(" / ")
1670
- ? symbol
1671
- .split(/[((]/, 1)[0]
1672
- .split("/")
1673
- .map((item) => item.trim())
1674
- .filter(Boolean)
1675
- : [symbol];
1676
- return symbols.every((item) => {
1677
- const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
1678
- return [item, describeTitle, describeTitle ? "describe(" : undefined]
1679
- .filter((candidate) => Boolean(candidate))
1680
- .some((candidate) => content.includes(candidate));
1681
- });
1682
- }
1683
1783
  async function dropUnresolvedVerificationSymbols(input) {
1684
1784
  const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
1685
1785
  const targets = asRecord(input.contract)?.verificationTargets;
1686
1786
  if (!workspaceRoot || !Array.isArray(targets))
1687
- return false;
1688
- let changed = false;
1787
+ return [];
1788
+ const actions = [];
1689
1789
  for (const targetValue of targets) {
1690
1790
  const target = asRecord(targetValue);
1691
1791
  const file = asString(target?.file);
1692
1792
  const symbol = asString(target?.symbol);
1693
- if (!target || !file || !symbol)
1793
+ if (!target || !file)
1794
+ continue;
1795
+ // Static targets (project-wide commands such as typecheck/build) verify
1796
+ // by command exit code, never by a per-file symbol. Strip any symbol
1797
+ // the planner attached — including fabricated placeholders like "null"
1798
+ // — so the plan shape check and the verify trace gate never evaluate
1799
+ // it. This is the root fix for the recurring fabricated-symbol error
1800
+ // class: the field was allowed for static, the prompt did not forbid
1801
+ // it, and models habitually fill it; the compiler removes it instead.
1802
+ if (target.type === "static") {
1803
+ if (symbol) {
1804
+ delete target.symbol;
1805
+ if (!actions.includes("strip-static-verification-symbols"))
1806
+ actions.push("strip-static-verification-symbols");
1807
+ }
1808
+ continue;
1809
+ }
1810
+ if (!symbol)
1694
1811
  continue;
1695
1812
  const absolute = path.resolve(workspaceRoot, file);
1696
1813
  const relative = path.relative(workspaceRoot, absolute);
@@ -1699,6 +1816,10 @@ async function dropUnresolvedVerificationSymbols(input) {
1699
1816
  relative.includes("..")) {
1700
1817
  continue;
1701
1818
  }
1819
+ // Configuration files (tsconfig.json, vite.config.*, ...) do not expose
1820
+ // source symbols; the trace gate skips them, so the drop must too.
1821
+ if (isConfigurationVerificationFile(file))
1822
+ continue;
1702
1823
  let content;
1703
1824
  try {
1704
1825
  content = await readFile(absolute, "utf8");
@@ -1708,60 +1829,78 @@ async function dropUnresolvedVerificationSymbols(input) {
1708
1829
  // changing the optional symbol during candidate normalization.
1709
1830
  continue;
1710
1831
  }
1711
- if (verificationSymbolMatchesFile(symbol, content))
1832
+ if (verificationSymbolMatchesContent(symbol, content))
1712
1833
  continue;
1713
1834
  delete target.symbol;
1714
- changed = true;
1835
+ if (!actions.includes("drop-unresolved-verification-symbols"))
1836
+ actions.push("drop-unresolved-verification-symbols");
1715
1837
  }
1716
- return changed;
1717
- }
1718
- function extractSingleOpenspecCitationsBlock(text) {
1719
- const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
1720
- if (blocks.length !== 1) {
1721
- throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
1722
- }
1723
- return blocks[0];
1724
- }
1725
- /**
1726
- * Frontend plan/revision output has two JSON-bearing regions: the primary
1727
- * payload and line-oriented OpenSpec citations. Keep their parsing domains
1728
- * separate so a citation row can never become a competing contract candidate.
1729
- * A bare JSON payload is retained only for historical node-output compatibility.
1730
- */
1731
- export function extractFrontendPlanPayload(text) {
1732
- const citationsBlock = extractSingleOpenspecCitationsBlock(text);
1733
- return {
1734
- payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
1735
- citationsBlock,
1736
- };
1737
- }
1738
- function extractFrontendPlanPrimaryPayload(text) {
1739
- const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
1740
- if (fences.length > 1) {
1741
- throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
1742
- }
1743
- if (fences.length === 1) {
1744
- try {
1745
- return JSON.parse(fences[0][1]);
1746
- }
1747
- catch (error) {
1748
- throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
1749
- }
1750
- }
1751
- return extractFrontendImplementationJson(text);
1838
+ return actions;
1752
1839
  }
1753
1840
  async function writeFrontendPlanCandidateRaw(input) {
1754
1841
  const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
1755
- await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text);
1842
+ await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
1756
1843
  return relativePath;
1757
1844
  }
1758
1845
  /**
1759
1846
  * Compile the planner's editable RFC 7386 patch against the deterministic
1760
1847
  * runtime skeleton. Every attempt is preserved under contracts/candidates so
1761
1848
  * format/schema failures can be inspected and replayed without trusting model
1762
- * prose. A successful result returns canonical full-contract text for all
1849
+ * prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
1763
1850
  * downstream review and prewrite nodes.
1764
1851
  */
1852
+ /**
1853
+ * Placeholder tokens that are never valid verification symbols ("null" is the
1854
+ * classic model artifact: it serializes a JS null into the symbol string).
1855
+ */
1856
+ const VERIFICATION_SYMBOL_PLACEHOLDERS = new Set([
1857
+ "null",
1858
+ "undefined",
1859
+ "none",
1860
+ "n/a",
1861
+ "na",
1862
+ "tbd",
1863
+ "todo",
1864
+ "...",
1865
+ "?",
1866
+ "-",
1867
+ ]);
1868
+ /** An over-long symbol can never resolve to a real declaration. */
1869
+ const VERIFICATION_SYMBOL_MAX_CHARS = 60;
1870
+ /**
1871
+ * Deterministic shape check for a verification target symbol. Catches obvious
1872
+ * model fabrication (placeholder tokens, prose fragments) at plan time so the
1873
+ * plan retry ladder can fix the facts in-node instead of the verify trace gate
1874
+ * failing at the very end of the run.
1875
+ *
1876
+ * Deliberately conservative: bare test titles without punctuation ("renders
1877
+ * loading") and `describe(...)` / `it(...)` forms stay valid — a real symbol
1878
+ * may be a function name, a dotted path, or a describe/it title.
1879
+ */
1880
+ function isSuspiciousVerificationSymbol(symbol) {
1881
+ const trimmed = symbol.trim();
1882
+ if (!trimmed)
1883
+ return false;
1884
+ if (VERIFICATION_SYMBOL_PLACEHOLDERS.has(trimmed.toLowerCase()))
1885
+ return true;
1886
+ if (trimmed.length > VERIFICATION_SYMBOL_MAX_CHARS)
1887
+ return true;
1888
+ // Deliberately conservative: bare test titles with punctuation (e.g.
1889
+ // "adds +1 button") are valid describe/it-style symbols and must pass;
1890
+ // the length + placeholder rules above already catch obvious fabrication
1891
+ // ("null", over-long prose fragments). Everything else is left to the
1892
+ // deterministic drop/trace resolvability check.
1893
+ return false;
1894
+ }
1895
+ function assertVerificationSymbolShapes(contract) {
1896
+ for (const target of contract.verificationTargets) {
1897
+ if (!target.symbol)
1898
+ continue;
1899
+ if (isSuspiciousVerificationSymbol(target.symbol)) {
1900
+ throw new Error(`frontend plan verification target "${target.id}" declares a fabricated symbol "${target.symbol}" for ${target.file}; fix it to a real symbol present in that file or leave the symbol empty (the deterministic trace gate verifies symbol resolvability after verification)`);
1901
+ }
1902
+ }
1903
+ }
1765
1904
  export async function validateFrontendPlanPatchNodeOutput(input) {
1766
1905
  const nodeId = input.nodeId ?? "frontend-plan-pi";
1767
1906
  const attempt = input.attempt ?? 1;
@@ -1798,20 +1937,18 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1798
1937
  const skeleton = input.structuredContractOutput?.skeleton;
1799
1938
  if (!skeleton)
1800
1939
  throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
1801
- const patch = extractFrontendPlanPrimaryPayload(input.text);
1940
+ const committed = await readTypedEventStoreFromJsonl(path.join(input.runDir, nodeId, "plan-typed-facts.jsonl"));
1941
+ const patch = restorePlanPatchFromCommittedFacts(committed);
1802
1942
  if (!isPlainObject(patch))
1803
- throw new Error("frontend plan patch must extract to one JSON object");
1943
+ throw new Error("frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority");
1804
1944
  extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
1805
1945
  const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
1806
1946
  extractedPatchSha256 = extractedArtifact.sha256;
1807
- const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
1808
- const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
1809
- await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
1810
1947
  const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1811
1948
  if (protectedViolations.length > 0)
1812
1949
  throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
1813
1950
  const merged = applyFrontendContractMergePatch(skeleton, patch);
1814
- const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
1951
+ const symbolNormalizationActions = await dropUnresolvedVerificationSymbols({
1815
1952
  runDir: input.runDir,
1816
1953
  contract: merged,
1817
1954
  });
@@ -1820,6 +1957,23 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1820
1957
  rawContractText: serializeDeterministicJson(merged),
1821
1958
  sourceBinding: input.sourceBinding,
1822
1959
  });
1960
+ // Front-load the verification-symbol shape check so fabricated symbols
1961
+ // are fixed by the plan retry ladder in-node instead of failing the
1962
+ // verify trace gate at the end of the run.
1963
+ assertVerificationSymbolShapes(analysis.canonical);
1964
+ // Front-load the plan-attributable design-policy checks so the §5.1
1965
+ // retry ladder can fix these facts in-node with diagnostics instead of
1966
+ // the run terminating at the policy shell. The policy shell remains the
1967
+ // final authority and re-runs the identical checks on committed facts.
1968
+ const policyPreFindings = [
1969
+ ...checkUiDesignCoverage(analysis.canonical),
1970
+ ...checkUiStateAttribution(analysis.canonical),
1971
+ ...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
1972
+ ];
1973
+ if (policyPreFindings.length > 0)
1974
+ throw new Error(`frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
1975
+ .map((finding) => `${finding.code}: ${finding.message}`)
1976
+ .join("; ")}`);
1823
1977
  const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
1824
1978
  await writeReport({
1825
1979
  classification: "accepted-normalized",
@@ -1827,47 +1981,60 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1827
1981
  normalizedContractSha256: normalizedArtifact.sha256,
1828
1982
  normalizationActions: [
1829
1983
  "apply-runtime-skeleton",
1830
- ...(droppedUnresolvedSymbols
1831
- ? ["drop-unresolved-verification-symbols"]
1832
- : []),
1984
+ ...symbolNormalizationActions,
1833
1985
  ...analysis.normalizationActions,
1834
1986
  ],
1835
1987
  errors: [],
1836
1988
  });
1989
+ const canonicalRel = canonicalContractRel(nodeId);
1990
+ const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
1991
+ await persistRuntimeSkeleton({
1992
+ runDir: input.runDir,
1993
+ nodeId,
1994
+ skeleton,
1995
+ });
1996
+ const artifact = {
1997
+ path: canonicalArtifact.path,
1998
+ sha256: canonicalArtifact.sha256,
1999
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
2000
+ };
1837
2001
  return {
1838
2002
  ok: true,
1839
2003
  contract: analysis.canonical,
1840
- structuredArtifact: {
1841
- path: normalizedArtifact.path,
1842
- sha256: normalizedArtifact.sha256,
1843
- schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
1844
- },
1845
- frontendPlanOutput: {
1846
- schemaVersion: 1,
1847
- payloadKind: "canonical-contract",
1848
- citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
1849
- citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
1850
- rawCandidatePath: path.join(input.runDir, rawPath),
1851
- rawCandidateSha256: sha256Hex(input.text),
1852
- },
1853
- normalizedText: [
1854
- "Frontend implementation contract compiled from the runtime skeleton.",
1855
- "",
1856
- "```json",
1857
- serializeDeterministicJson(analysis.canonical),
1858
- "```",
1859
- citationsBlock,
1860
- ].join("\n"),
2004
+ artifact,
2005
+ normalizedText: formatStructuredArtifactPointer(artifact),
1861
2006
  };
1862
2007
  }
1863
2008
  catch (error) {
1864
2009
  const reason = error instanceof Error ? error.message : String(error);
2010
+ const classified = classifyStructuredContractFailure({
2011
+ reason,
2012
+ kind: error instanceof FrontendContractFailure ? error.kind : undefined,
2013
+ });
1865
2014
  await writeReport({
1866
- classification: "retryable-invalid",
2015
+ classification: classified.classification,
1867
2016
  normalizationActions: [],
1868
- errors: [{ path: "$", code: "invalid-output", message: reason }],
2017
+ errors: classified.errors,
1869
2018
  });
1870
- return { ok: false, reason };
2019
+ return {
2020
+ ok: false,
2021
+ reason,
2022
+ classification: classified.classification,
2023
+ errors: classified.errors,
2024
+ };
2025
+ }
2026
+ }
2027
+ export async function readFrontendCanonicalCandidate(runDir, nodeId) {
2028
+ const candidatePath = path.join(runDir, canonicalContractRel(nodeId));
2029
+ try {
2030
+ const raw = await readFile(candidatePath, "utf8");
2031
+ const parsed = frontendImplementationContractSchema.safeParse(JSON.parse(raw));
2032
+ return parsed.success ? parsed.data : undefined;
2033
+ }
2034
+ catch (error) {
2035
+ if (error.code === "ENOENT")
2036
+ return undefined;
2037
+ return undefined;
1871
2038
  }
1872
2039
  }
1873
2040
  /**
@@ -1888,79 +2055,140 @@ export async function validateFrontendContractNodeOutput(input) {
1888
2055
  return { ok: true, contract: analysis.canonical };
1889
2056
  }
1890
2057
  catch (error) {
2058
+ const reason = error instanceof Error ? error.message : String(error);
2059
+ const classified = classifyStructuredContractFailure({
2060
+ reason,
2061
+ kind: error instanceof FrontendContractFailure ? error.kind : undefined,
2062
+ });
1891
2063
  return {
1892
2064
  ok: false,
1893
- reason: error instanceof Error ? error.message : String(error),
2065
+ reason,
2066
+ classification: classified.classification,
2067
+ errors: classified.errors,
1894
2068
  };
1895
2069
  }
1896
2070
  }
1897
2071
  /**
1898
- * Node-output self-check for the plan revision node, which emits an RFC 7386
1899
- * merge-patch delta against the original contract instead of a full contract.
1900
- * Deliberately loose and format-level: the output must contain exactly one
1901
- * ```openspec-citations fenced block and must extract (after the citations
1902
- * blocks are stripped) to exactly one JSON object. The delta carries no
1903
- * schemaVersion/targets, so no full-contract schema/semantic checks run here;
1904
- * content parsing, patch application, and merged-contract validation stay with
1905
- * the prewrite gate, which remains the only authority.
2072
+ * Deterministically assemble the canonical frontend implementation contract
2073
+ * from committed typed facts, never from plan/contract node text.
2074
+ *
2075
+ * Inputs (all read-only, all fail-closed):
2076
+ * - `<planNodeId>/plan-typed-facts.jsonl` (phase=committed) restores the
2077
+ * editable RFC 7386 patch;
2078
+ * - `<contractNodeId>/contract-typed-facts.jsonl` (phase=committed) must carry
2079
+ * a `contract-finalized` terminal fact;
2080
+ * - `<contractNodeId>/frontend-task-contract-vNext.json` must exist and parse.
2081
+ *
2082
+ * The patch is applied onto the runtime skeleton and re-validated through the
2083
+ * same `analyzeFrontendImplementationContract` pipeline used by the legacy
2084
+ * text path, so the downstream artifact semantics are unchanged. The function
2085
+ * is pure: it never writes candidate/audit artifacts and always produces the
2086
+ * same canonical serialization for the same committed facts. It never falls
2087
+ * back to parsing Plan/Contract node text.
1906
2088
  */
1907
- export async function validateFrontendRevisionPatchNodeOutput(input) {
1908
- const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
1909
- const attempt = input.attempt ?? 1;
1910
- const candidateDir = path.posix.join("contracts", "candidates", nodeId);
1911
- const rawPath = await writeFrontendPlanCandidateRaw({
1912
- runDir: input.runDir,
1913
- nodeId,
1914
- attempt,
1915
- text: input.text,
1916
- });
1917
- let citationsBlock;
1918
- let patch;
2089
+ export async function buildContractFromCommittedFacts(input) {
2090
+ const contractNodeId = input.contractNodeId ?? "frontend-contract-pi";
2091
+ // Skeleton guard: the runtime skeleton must carry the protected identity
2092
+ // fields so the merged contract can be validated against the full schema.
2093
+ const skeletonRecord = asRecord(input.skeleton);
2094
+ const skeletonTargets = asRecord(skeletonRecord?.targets);
2095
+ const skeletonRiskLevel = asString(skeletonRecord?.riskLevel);
2096
+ if (!skeletonRecord ||
2097
+ !["small", "standard", "high-risk"].includes(skeletonRiskLevel) ||
2098
+ !Array.isArray(skeletonTargets?.files) ||
2099
+ skeletonTargets.files.length === 0) {
2100
+ return {
2101
+ ok: false,
2102
+ reason: "frontend contract skeleton missing required runtime fields (riskLevel/targets.files)",
2103
+ failureCode: "missing-skeleton",
2104
+ };
2105
+ }
2106
+ // a. Committed plan ledger restores the editable patch.
2107
+ const planRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.planNodeId, "plan-typed-facts.jsonl"));
2108
+ const patch = restorePlanPatchFromCommittedFacts(planRecords);
2109
+ if (!isPlainObject(patch)) {
2110
+ return {
2111
+ ok: false,
2112
+ reason: "frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority",
2113
+ failureCode: "missing-plan-ledger",
2114
+ };
2115
+ }
2116
+ // b. Committed contract terminal fact (contract-finalized).
2117
+ const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
2118
+ const contractKinds = new Set(contractRecords
2119
+ .filter((record) => record.phase === "committed")
2120
+ .map((record) => record.fact.kind)
2121
+ .filter((kind) => typeof kind === "string"));
2122
+ if (!contractKinds.has("contract-finalized")) {
2123
+ return {
2124
+ ok: false,
2125
+ reason: "frontend contract facts missing: no committed contract-finalized terminal fact",
2126
+ failureCode: "missing-contract-finalize",
2127
+ };
2128
+ }
2129
+ // c. frontend-task-contract-vNext.json must exist and parse (input integrity).
2130
+ let rawVNext;
1919
2131
  try {
1920
- const parsed = extractFrontendPlanPayload(input.text);
1921
- citationsBlock = parsed.citationsBlock;
1922
- patch = parsed.payload;
2132
+ rawVNext = await readFile(path.join(input.runDir, contractNodeId, "frontend-task-contract-vNext.json"), "utf8");
2133
+ }
2134
+ catch {
2135
+ return {
2136
+ ok: false,
2137
+ reason: `frontend task contract vNext missing at ${contractNodeId}/frontend-task-contract-vNext.json`,
2138
+ failureCode: "contract-vnext-missing",
2139
+ };
2140
+ }
2141
+ try {
2142
+ JSON.parse(rawVNext);
1923
2143
  }
1924
2144
  catch (error) {
1925
2145
  return {
1926
2146
  ok: false,
1927
- reason: error instanceof Error ? error.message : String(error),
2147
+ reason: `frontend task contract vNext is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
2148
+ failureCode: "contract-vnext-invalid",
1928
2149
  };
1929
2150
  }
1930
- if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
2151
+ // d. Merge the editable patch onto the protected skeleton and validate.
2152
+ let merged;
2153
+ try {
2154
+ merged = applyFrontendContractMergePatch(input.skeleton, patch);
2155
+ }
2156
+ catch (error) {
1931
2157
  return {
1932
2158
  ok: false,
1933
- reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
2159
+ reason: `frontend contract patch could not be applied to the runtime skeleton: ${error instanceof Error ? error.message : String(error)}`,
2160
+ failureCode: "schema-drift",
2161
+ };
2162
+ }
2163
+ // The design-policy shell is the authoritative materializer of the
2164
+ // canonical contract; keep it on the same unresolved-symbol drop as the
2165
+ // plan validator so the verify trace gate never sees fabricated symbols.
2166
+ await dropUnresolvedVerificationSymbols({
2167
+ runDir: input.runDir,
2168
+ contract: merged,
2169
+ });
2170
+ try {
2171
+ const analysis = await analyzeFrontendImplementationContract({
2172
+ runDir: input.runDir,
2173
+ rawContractText: serializeDeterministicJson(merged),
2174
+ sourceBinding: input.sourceBinding,
2175
+ });
2176
+ return {
2177
+ ok: true,
2178
+ canonical: analysis.canonical,
2179
+ normalizationActions: analysis.normalizationActions,
2180
+ candidateJsonSha256: analysis.candidateJsonSha256,
1934
2181
  };
1935
2182
  }
1936
- const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1937
- if (protectedViolations.length > 0) {
2183
+ catch (error) {
2184
+ const failureKind = error instanceof FrontendContractFailure ? error.kind : undefined;
1938
2185
  return {
1939
2186
  ok: false,
1940
- reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
2187
+ reason: `frontend contract schema drift: ${error instanceof Error ? error.message : String(error)}`,
2188
+ failureCode: "schema-drift",
2189
+ ...(failureKind ? { failureKind } : {}),
1941
2190
  };
1942
2191
  }
1943
- const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
1944
- const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
1945
- const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
1946
- await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
1947
- return {
1948
- ok: true,
1949
- contract: patch,
1950
- structuredArtifact: {
1951
- path: payloadArtifact.path,
1952
- sha256: payloadArtifact.sha256,
1953
- schemaId: "frontend-implementation-contract-revision-patch-v1",
1954
- },
1955
- frontendPlanOutput: {
1956
- schemaVersion: 1,
1957
- payloadKind: "merge-patch",
1958
- citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
1959
- citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
1960
- rawCandidatePath: path.join(input.runDir, rawPath),
1961
- rawCandidateSha256: sha256Hex(input.text),
1962
- },
1963
- };
1964
2192
  }
1965
2193
  export async function materializeFrontendImplementationContract(input) {
1966
2194
  const analysis = await analyzeFrontendImplementationContract(input);