@tea-agent/loop-agent 0.39.0-beta.2 → 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 -160
  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 +572 -500
  164. package/dist/workflows/dag/node-execution.js +307 -70
  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 -23
  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,65 +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, citationsRequired = true) {
1719
- const blocks = [...text.matchAll(/^```openspec-citations[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim)].map((match) => match[0]);
1720
- if (!citationsRequired) {
1721
- if (blocks.length === 0)
1722
- return "";
1723
- throw new Error("frontend plan output must not include an openspec-citations block when the generation-frozen OpenSpec candidate set is empty");
1724
- }
1725
- if (blocks.length !== 1) {
1726
- throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
1727
- }
1728
- return blocks[0];
1729
- }
1730
- /**
1731
- * Frontend plan/revision output has two JSON-bearing regions: the primary
1732
- * payload and line-oriented OpenSpec citations. Keep their parsing domains
1733
- * separate so a citation row can never become a competing contract candidate.
1734
- * A bare JSON payload is retained only for historical node-output compatibility.
1735
- */
1736
- export function extractFrontendPlanPayload(text, options) {
1737
- const citationsBlock = extractSingleOpenspecCitationsBlock(text, options?.citationsRequired !== false);
1738
- return {
1739
- payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
1740
- citationsBlock,
1741
- };
1742
- }
1743
- function extractFrontendPlanPrimaryPayload(text) {
1744
- const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
1745
- if (fences.length > 1) {
1746
- throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
1747
- }
1748
- if (fences.length === 1) {
1749
- try {
1750
- return JSON.parse(fences[0][1]);
1751
- }
1752
- catch (error) {
1753
- throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
1754
- }
1755
- }
1756
- return extractFrontendImplementationJson(text);
1838
+ return actions;
1757
1839
  }
1758
1840
  async function writeFrontendPlanCandidateRaw(input) {
1759
1841
  const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
1760
- 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`);
1761
1843
  return relativePath;
1762
1844
  }
1763
1845
  /**
1764
1846
  * Compile the planner's editable RFC 7386 patch against the deterministic
1765
1847
  * runtime skeleton. Every attempt is preserved under contracts/candidates so
1766
1848
  * format/schema failures can be inspected and replayed without trusting model
1767
- * 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
1768
1850
  * downstream review and prewrite nodes.
1769
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
+ }
1770
1904
  export async function validateFrontendPlanPatchNodeOutput(input) {
1771
1905
  const nodeId = input.nodeId ?? "frontend-plan-pi";
1772
1906
  const attempt = input.attempt ?? 1;
@@ -1803,22 +1937,18 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1803
1937
  const skeleton = input.structuredContractOutput?.skeleton;
1804
1938
  if (!skeleton)
1805
1939
  throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
1806
- // Preserve the raw primary candidate even if the separately parsed
1807
- // citation fence is malformed; the retry report is an audit artifact.
1808
- const patch = extractFrontendPlanPrimaryPayload(input.text);
1940
+ const committed = await readTypedEventStoreFromJsonl(path.join(input.runDir, nodeId, "plan-typed-facts.jsonl"));
1941
+ const patch = restorePlanPatchFromCommittedFacts(committed);
1809
1942
  if (!isPlainObject(patch))
1810
- 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");
1811
1944
  extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
1812
1945
  const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
1813
1946
  extractedPatchSha256 = extractedArtifact.sha256;
1814
- const citationsBlock = extractSingleOpenspecCitationsBlock(input.text, input.structuredContractOutput?.citationsRequired !== false);
1815
- const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
1816
- await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
1817
1947
  const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1818
1948
  if (protectedViolations.length > 0)
1819
1949
  throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
1820
1950
  const merged = applyFrontendContractMergePatch(skeleton, patch);
1821
- const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
1951
+ const symbolNormalizationActions = await dropUnresolvedVerificationSymbols({
1822
1952
  runDir: input.runDir,
1823
1953
  contract: merged,
1824
1954
  });
@@ -1827,6 +1957,23 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1827
1957
  rawContractText: serializeDeterministicJson(merged),
1828
1958
  sourceBinding: input.sourceBinding,
1829
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("; ")}`);
1830
1977
  const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
1831
1978
  await writeReport({
1832
1979
  classification: "accepted-normalized",
@@ -1834,47 +1981,60 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1834
1981
  normalizedContractSha256: normalizedArtifact.sha256,
1835
1982
  normalizationActions: [
1836
1983
  "apply-runtime-skeleton",
1837
- ...(droppedUnresolvedSymbols
1838
- ? ["drop-unresolved-verification-symbols"]
1839
- : []),
1984
+ ...symbolNormalizationActions,
1840
1985
  ...analysis.normalizationActions,
1841
1986
  ],
1842
1987
  errors: [],
1843
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
+ };
1844
2001
  return {
1845
2002
  ok: true,
1846
2003
  contract: analysis.canonical,
1847
- structuredArtifact: {
1848
- path: normalizedArtifact.path,
1849
- sha256: normalizedArtifact.sha256,
1850
- schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
1851
- },
1852
- frontendPlanOutput: {
1853
- schemaVersion: 1,
1854
- payloadKind: "canonical-contract",
1855
- citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
1856
- citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
1857
- rawCandidatePath: path.join(input.runDir, rawPath),
1858
- rawCandidateSha256: sha256Hex(input.text),
1859
- },
1860
- normalizedText: [
1861
- "Frontend implementation contract compiled from the runtime skeleton.",
1862
- "",
1863
- "```json",
1864
- serializeDeterministicJson(analysis.canonical),
1865
- "```",
1866
- citationsBlock,
1867
- ].join("\n"),
2004
+ artifact,
2005
+ normalizedText: formatStructuredArtifactPointer(artifact),
1868
2006
  };
1869
2007
  }
1870
2008
  catch (error) {
1871
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
+ });
1872
2014
  await writeReport({
1873
- classification: "retryable-invalid",
2015
+ classification: classified.classification,
1874
2016
  normalizationActions: [],
1875
- errors: [{ path: "$", code: "invalid-output", message: reason }],
2017
+ errors: classified.errors,
1876
2018
  });
1877
- 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;
1878
2038
  }
1879
2039
  }
1880
2040
  /**
@@ -1895,82 +2055,140 @@ export async function validateFrontendContractNodeOutput(input) {
1895
2055
  return { ok: true, contract: analysis.canonical };
1896
2056
  }
1897
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
+ });
1898
2063
  return {
1899
2064
  ok: false,
1900
- reason: error instanceof Error ? error.message : String(error),
2065
+ reason,
2066
+ classification: classified.classification,
2067
+ errors: classified.errors,
1901
2068
  };
1902
2069
  }
1903
2070
  }
1904
2071
  /**
1905
- * Node-output self-check for the plan revision node, which emits an RFC 7386
1906
- * merge-patch delta against the original contract instead of a full contract.
1907
- * Deliberately loose and format-level: when OpenSpec candidates exist, the
1908
- * output contains exactly one ```openspec-citations fenced block; otherwise it
1909
- * contains no citation fence. It must extract (after the citations block is
1910
- * stripped) to exactly one JSON object. The delta carries no
1911
- * schemaVersion/targets, so no full-contract schema/semantic checks run here;
1912
- * content parsing, patch application, and merged-contract validation stay with
1913
- * 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.
1914
2088
  */
1915
- export async function validateFrontendRevisionPatchNodeOutput(input) {
1916
- const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
1917
- const attempt = input.attempt ?? 1;
1918
- const candidateDir = path.posix.join("contracts", "candidates", nodeId);
1919
- const rawPath = await writeFrontendPlanCandidateRaw({
1920
- runDir: input.runDir,
1921
- nodeId,
1922
- attempt,
1923
- text: input.text,
1924
- });
1925
- let citationsBlock;
1926
- 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;
1927
2131
  try {
1928
- const parsed = extractFrontendPlanPayload(input.text, {
1929
- citationsRequired: input.structuredContractOutput?.citationsRequired,
1930
- });
1931
- citationsBlock = parsed.citationsBlock;
1932
- 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);
1933
2143
  }
1934
2144
  catch (error) {
1935
2145
  return {
1936
2146
  ok: false,
1937
- 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",
1938
2149
  };
1939
2150
  }
1940
- 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) {
1941
2157
  return {
1942
2158
  ok: false,
1943
- 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,
1944
2181
  };
1945
2182
  }
1946
- const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1947
- if (protectedViolations.length > 0) {
2183
+ catch (error) {
2184
+ const failureKind = error instanceof FrontendContractFailure ? error.kind : undefined;
1948
2185
  return {
1949
2186
  ok: false,
1950
- 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 } : {}),
1951
2190
  };
1952
2191
  }
1953
- const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
1954
- const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
1955
- const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
1956
- await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
1957
- return {
1958
- ok: true,
1959
- contract: patch,
1960
- structuredArtifact: {
1961
- path: payloadArtifact.path,
1962
- sha256: payloadArtifact.sha256,
1963
- schemaId: "frontend-implementation-contract-revision-patch-v1",
1964
- },
1965
- frontendPlanOutput: {
1966
- schemaVersion: 1,
1967
- payloadKind: "merge-patch",
1968
- citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
1969
- citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
1970
- rawCandidatePath: path.join(input.runDir, rawPath),
1971
- rawCandidateSha256: sha256Hex(input.text),
1972
- },
1973
- };
1974
2192
  }
1975
2193
  export async function materializeFrontendImplementationContract(input) {
1976
2194
  const analysis = await analyzeFrontendImplementationContract(input);