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

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 (331) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +248 -95
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/advance.js +12 -0
  6. package/dist/application/task-lifecycle/recommendations.js +13 -2
  7. package/dist/build-stamp.json +3 -3
  8. package/dist/cli/command-definitions.js +23 -5
  9. package/dist/cli/program.js +21 -2
  10. package/dist/cli/update/notifier.js +2 -2
  11. package/dist/cli/update/runtime-activity.js +1 -29
  12. package/dist/cli.js +2 -1
  13. package/dist/commands/client-recovery.js +3 -0
  14. package/dist/commands/dag-follow-up.js +138 -0
  15. package/dist/commands/dag-rerun.js +55 -1
  16. package/dist/commands/init-model-catalog.js +464 -0
  17. package/dist/commands/init-upgrade.js +265 -97
  18. package/dist/commands/init.js +455 -28
  19. package/dist/commands/inspect-next.js +8 -0
  20. package/dist/commands/task-advance.js +19 -0
  21. package/dist/executors/dag-pi-executor.js +2306 -61
  22. package/dist/executors/pi-executor.js +7 -0
  23. package/dist/executors/pi-extension-resolver.js +233 -0
  24. package/dist/executors/pi-playwright-cli-tool.js +74 -28
  25. package/dist/executors/pi-read-budget-policy.js +239 -0
  26. package/dist/executors/pi-sdk-executor.js +180 -33
  27. package/dist/executors/shell-executor.js +1249 -224
  28. package/dist/executors/shell-write-guard.js +14 -0
  29. package/dist/executors/workspace-write-snapshot.js +68 -0
  30. package/dist/infrastructure/harness/atomic-write.js +23 -0
  31. package/dist/shared/dag-failure-category.js +150 -0
  32. package/dist/shared/dag-prompt-override.js +27 -0
  33. package/dist/shared/openspec-spec.js +70 -4
  34. package/dist/shared/operator/capabilities.js +39 -0
  35. package/dist/shared/playwright-cli-command-policy.js +15 -0
  36. package/dist/shared/update/console-notifier.js +100 -0
  37. package/dist/{cli → shared}/update/npm-client.js +70 -15
  38. package/dist/{cli → shared}/update/state.js +41 -13
  39. package/dist/task/config-types.js +50 -5
  40. package/dist/task/contract/apply.js +11 -1
  41. package/dist/task/contract/constants.js +2 -0
  42. package/dist/task/contract/hash.js +3 -0
  43. package/dist/task/contract/observe.js +25 -4
  44. package/dist/task/contract/paths.js +2 -1
  45. package/dist/task/contract/project.js +18 -1
  46. package/dist/task/contract/schema.js +4 -1
  47. package/dist/task/contract/transaction.js +12 -0
  48. package/dist/task/frontend-project-capability.js +199 -18
  49. package/dist/task/source-prepare/completeness.js +188 -0
  50. package/dist/task/source-prepare/fragment-inventory.js +477 -0
  51. package/dist/task/source-prepare/index.js +3 -0
  52. package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
  53. package/dist/task/source-prepare/ledger-review.js +214 -0
  54. package/dist/task/source-prepare/ledger.js +545 -0
  55. package/dist/task/source-prepare/parse-intent.js +24 -4
  56. package/dist/task/source-prepare/prepare.js +298 -1
  57. package/dist/task/source-prepare/semantic-intake.js +25 -5
  58. package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
  59. package/dist/task/source-prepare/types.js +22 -0
  60. package/dist/task/source-references.js +22 -1
  61. package/dist/worker/cli.js +18 -2
  62. package/dist/worker/console/chat/chat-event-store.js +62 -4
  63. package/dist/worker/console/chat/context-panel.js +7 -0
  64. package/dist/worker/console/chat/deferred-turn.js +12 -0
  65. package/dist/worker/console/chat/operation-card.js +8 -1
  66. package/dist/worker/console/chat/pi-console-config.js +81 -10
  67. package/dist/worker/console/chat/pi-runtime.js +264 -16
  68. package/dist/worker/console/chat/provider-error.js +98 -0
  69. package/dist/worker/console/chat/routes.js +298 -32
  70. package/dist/worker/console/chat/session-stats.js +179 -0
  71. package/dist/worker/console/chat/session-store.js +93 -63
  72. package/dist/worker/console/chat/todos.js +115 -0
  73. package/dist/worker/console/chat/tool-preview.js +42 -0
  74. package/dist/worker/console/chat/turn-process.js +20 -55
  75. package/dist/worker/console/chat/user-questions.js +266 -0
  76. package/dist/worker/console/chat/workspace-landing.js +14 -0
  77. package/dist/worker/console/console-handoff.js +82 -0
  78. package/dist/worker/console/console-update-and-init.js +115 -0
  79. package/dist/worker/console/console-update-runtime.js +84 -0
  80. package/dist/worker/console/dag-execution-receipt.js +33 -0
  81. package/dist/worker/console/draft-store.js +49 -0
  82. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  83. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  84. package/dist/worker/console/index.js +3 -0
  85. package/dist/worker/console/inspect-split.js +18 -0
  86. package/dist/worker/console/native-directory-picker.js +13 -0
  87. package/dist/worker/console/operation-run-facts.js +19 -3
  88. package/dist/worker/console/operation-store.js +227 -7
  89. package/dist/worker/console/operator-actions.js +388 -0
  90. package/dist/worker/console/operator-surface-health.js +1 -0
  91. package/dist/worker/console/operator-user-error.js +2 -2
  92. package/dist/worker/console/prd-intake-bridge.js +13 -0
  93. package/dist/worker/console/routes.js +355 -3
  94. package/dist/worker/console/security.js +67 -13
  95. package/dist/worker/console/server.js +609 -164
  96. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-BAsjg7ew.js} +1 -1
  97. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-CyIlrwZw.js} +1 -1
  98. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-Bo4tobhH.js} +1 -1
  99. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-Dfpow9gu.js} +1 -1
  100. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-DPmzG_B4.js} +1 -1
  101. package/dist/worker/console/static/assets/channel-DSrJAn0-.js +1 -0
  102. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-pKQpB1UW.js} +1 -1
  103. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-5n9YYquZ.js} +1 -1
  104. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-DP3CtA2a.js} +1 -1
  105. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-DYxMJPUy.js} +1 -1
  106. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-CS3D50Io.js} +1 -1
  107. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-gSdDfo5_.js} +1 -1
  108. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-A48q0ZNt.js} +1 -1
  109. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-ow8Mm3DY.js} +1 -1
  110. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-CMKfLWYE.js} +1 -1
  111. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BiTBdQPX.js} +1 -1
  112. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DeMmPl0_.js +1 -0
  113. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DeMmPl0_.js +1 -0
  114. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-Cm7exRz_.js} +1 -1
  115. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-D9O5qGOu.js} +1 -1
  116. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-CkvQnako.js} +1 -1
  117. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-B1rzp7CB.js} +1 -1
  118. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-BP07Ipcc.js} +1 -1
  119. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-BlPzweQb.js} +1 -1
  120. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-CxPa7n0S.js} +1 -1
  121. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-CMTZs53A.js} +1 -1
  122. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-CxG_X52r.js} +1 -1
  123. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-D8axtJM0.js} +1 -1
  124. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-DIsGzn7b.js} +1 -1
  125. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CU9zkDG0.js} +1 -1
  126. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-DCjTmz5P.js} +1 -1
  127. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-BksdmFzg.js} +1 -1
  128. package/dist/worker/console/static/assets/index-B-O5ChYq.css +1 -0
  129. package/dist/worker/console/static/assets/index-CfnY8AQx.js +407 -0
  130. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-CGzzzJtL.js} +1 -1
  131. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-BdseB80N.js} +1 -1
  132. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-DM9gsgha.js} +1 -1
  133. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-3q-_9R2b.js} +1 -1
  134. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-B8N-Mxsd.js} +1 -1
  135. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-EPIHUUsu.js} +5 -5
  136. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-CxwO12aa.js} +1 -1
  137. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-bPqbVX6Y.js} +1 -1
  138. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-KzqIvzeX.js} +1 -1
  139. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-CyofpWiV.js} +1 -1
  140. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-cTWJvLTP.js} +1 -1
  141. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-BK--q27B.js} +1 -1
  142. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-C0SDOHXK.js} +1 -1
  143. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-Cong6YIo.js} +1 -1
  144. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DUVkgZx8.js} +1 -1
  145. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C7XgxuZg.js} +1 -1
  146. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dzbnd5oX.js +1 -0
  147. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-D9Ayvwcy.js} +2 -2
  148. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BY0hHk6G.js +8 -0
  149. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-Byb000cH.js} +1 -1
  150. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DMLegPt4.js} +1 -1
  151. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-ChMMeOsP.js} +1 -1
  152. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-bEMEPisW.js} +1 -1
  153. package/dist/worker/console/static/index.html +2 -2
  154. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  155. package/dist/worker/console/static-src/app/console-types.js +68 -4
  156. package/dist/worker/console/static-src/app/useConsoleShell.js +30 -12
  157. package/dist/worker/console/static-src/app/useOperatorActions.js +41 -6
  158. package/dist/worker/console/static-src/app/useRecoveryConsole.js +45 -10
  159. package/dist/worker/console/static-src/app/useRunProgress.js +47 -0
  160. package/dist/worker/console/static-src/app/useTaskWizard.js +74 -2
  161. package/dist/worker/console/static-src/night/useNightBoard.js +8 -5
  162. package/dist/worker/console/static-src/operator-chat/cards/failure-category-advice.js +25 -0
  163. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +140 -8
  164. package/dist/worker/console/static-src/operator-chat/details-dag-actions.js +122 -0
  165. package/dist/worker/console/static-src/operator-chat/input-history.js +76 -0
  166. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  167. package/dist/worker/console/static-src/operator-chat/refs.js +3 -0
  168. package/dist/worker/console/static-src/operator-chat/runtime-selection-labels.js +27 -0
  169. package/dist/worker/console/static-src/operator-chat/turn-submission.js +16 -3
  170. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +168 -72
  171. package/dist/worker/console/static-src/operator-chat/useChatStream.js +102 -21
  172. package/dist/worker/console/static-src/operator-chat/useChatThread.js +184 -4
  173. package/dist/worker/console/static-src/operator-chat/useComposer.js +32 -0
  174. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +63 -9
  175. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  176. package/dist/worker/console/static-src/operator-chat/workspace-layout-mode.js +3 -3
  177. package/dist/worker/console/static-src/pages/tasks/run-id-resolution.js +79 -0
  178. package/dist/worker/console/static-src/pages/tasks/run-ownership-verify.js +23 -0
  179. package/dist/worker/console/static-src/pages/tasks/run-panel-progress.js +110 -0
  180. package/dist/worker/console/static-src/shell/useWorkspaces.js +299 -0
  181. package/dist/worker/console/static-src/shell/workspace-route.js +337 -0
  182. package/dist/worker/console/workspace-context.js +344 -0
  183. package/dist/worker/console/workspace-registry.js +214 -0
  184. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  185. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  186. package/dist/worker/materialize/harness-task-lineage.js +5 -2
  187. package/dist/worker/observability/init-runtime-activity.js +26 -0
  188. package/dist/worker/observability/read-model.js +22 -0
  189. package/dist/worker/observe/node-input.js +160 -3
  190. package/dist/worker/observe/node-process.js +377 -0
  191. package/dist/worker/observe/routes.js +224 -3
  192. package/dist/worker/observe/server.js +4 -0
  193. package/dist/worker/observe/shell-handler-keys.js +34 -0
  194. package/dist/worker/observe/static/api.js +90 -4
  195. package/dist/worker/observe/static/app.js +18 -70
  196. package/dist/worker/observe/static/constants.js +8 -1
  197. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  198. package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
  199. package/dist/worker/observe/static/dag-node-purpose.js +208 -0
  200. package/dist/worker/observe/static/format.js +60 -0
  201. package/dist/worker/observe/static/index.html +6 -1
  202. package/dist/worker/observe/static/inspect-workspace.js +307 -0
  203. package/dist/worker/observe/static/markdown-render.js +20 -1
  204. package/dist/worker/observe/static/operator-chrome.css +145 -20
  205. package/dist/worker/observe/static/operator-chrome.d.ts +20 -3
  206. package/dist/worker/observe/static/operator-chrome.js +330 -96
  207. package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
  208. package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
  209. package/dist/worker/observe/static/router.d.ts +46 -0
  210. package/dist/worker/observe/static/router.js +25 -1
  211. package/dist/worker/observe/static/state.d.ts +50 -0
  212. package/dist/worker/observe/static/state.js +184 -1
  213. package/dist/worker/observe/static/styles.css +395 -0
  214. package/dist/worker/observe/static/views/dag-graph.js +4 -2
  215. package/dist/worker/observe/static/views/dag-inspector.js +1067 -9
  216. package/dist/worker/observe/static/views/dag.d.ts +13 -0
  217. package/dist/worker/observe/static/views/dag.js +14 -5
  218. package/dist/worker/observe/static/views/dashboard.js +6 -4
  219. package/dist/worker/observe/static/views/failures.js +5 -2
  220. package/dist/worker/observe/static/views/night.js +2 -2
  221. package/dist/worker/observe/static/views/pool.js +1 -0
  222. package/dist/worker/observe/static/views/run.js +3 -1
  223. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  224. package/dist/worker/observe/static/views/task.js +56 -1
  225. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +49 -3
  226. package/dist/workflows/dag/backend-test-markdown-workflow.js +176 -1
  227. package/dist/workflows/dag/backend-test-pytest-collection.js +98 -14
  228. package/dist/workflows/dag/backend-test-writer-completeness.js +10 -5
  229. package/dist/workflows/dag/contract-output-registry.js +15 -0
  230. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  231. package/dist/workflows/dag/dynamic-runtime/loop-until.js +1 -1
  232. package/dist/workflows/dag/dynamic-runtime/map.js +1 -1
  233. package/dist/workflows/dag/failure-category.js +7 -116
  234. package/dist/workflows/dag/frontend-closeout.js +221 -0
  235. package/dist/workflows/dag/frontend-design-policy.js +400 -0
  236. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  237. package/dist/workflows/dag/frontend-implementation-contract.js +538 -166
  238. package/dist/workflows/dag/frontend-plan-render.js +24 -0
  239. package/dist/workflows/dag/frontend-prewrite-gate.js +388 -392
  240. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  241. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  242. package/dist/workflows/dag/frontend-recovery-controller.js +202 -0
  243. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  244. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  245. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  246. package/dist/workflows/dag/frontend-repair.js +7 -424
  247. package/dist/workflows/dag/frontend-review-context.js +288 -14
  248. package/dist/workflows/dag/frontend-review-findings.js +270 -0
  249. package/dist/workflows/dag/frontend-risk.js +15 -2
  250. package/dist/workflows/dag/frontend-shadow-dual-write.js +814 -0
  251. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  252. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  253. package/dist/workflows/dag/frontend-shape.js +427 -0
  254. package/dist/workflows/dag/frontend-source-fidelity-ledger.js +108 -0
  255. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  256. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  257. package/dist/workflows/dag/frontend-test-case-checklist.js +30 -4
  258. package/dist/workflows/dag/frontend-test-case-manifest.js +11 -4
  259. package/dist/workflows/dag/frontend-test-case-quality.js +11 -16
  260. package/dist/workflows/dag/frontend-test-environment-probe.js +230 -0
  261. package/dist/workflows/dag/frontend-test-html-report.js +3 -1
  262. package/dist/workflows/dag/frontend-test-l5-report.js +3 -1
  263. package/dist/workflows/dag/frontend-test-layout.js +159 -0
  264. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  265. package/dist/workflows/dag/frontend-test-result-contract.js +188 -42
  266. package/dist/workflows/dag/frontend-test-standard-scenarios.js +70 -0
  267. package/dist/workflows/dag/frontend-typed-event-store.js +452 -0
  268. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  269. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  270. package/dist/workflows/dag/frontend-worktree-diff.js +250 -17
  271. package/dist/workflows/dag/frontend-writer-admission.js +272 -0
  272. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  273. package/dist/workflows/dag/init-hybrid.js +984 -613
  274. package/dist/workflows/dag/interrupt-request.js +10 -3
  275. package/dist/workflows/dag/lifecycle.js +3 -3
  276. package/dist/workflows/dag/node-execution.js +468 -58
  277. package/dist/workflows/dag/prompt.js +44 -19
  278. package/dist/workflows/dag/recovery-lease.js +80 -0
  279. package/dist/workflows/dag/report.js +37 -1
  280. package/dist/workflows/dag/rerun-feedback.js +124 -1
  281. package/dist/workflows/dag/rerun-plan.js +144 -5
  282. package/dist/workflows/dag/rerun-run.js +81 -6
  283. package/dist/workflows/dag/rerun-task.js +29 -0
  284. package/dist/workflows/dag/retry-policy.js +318 -8
  285. package/dist/workflows/dag/runner.js +429 -122
  286. package/dist/workflows/dag/scheduler.js +114 -16
  287. package/dist/workflows/dag/structured-output-repair.js +712 -0
  288. package/dist/workflows/dag/types.js +321 -31
  289. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  290. package/dist/workflows/dag/validate.js +92 -32
  291. package/docs/README.md +2 -0
  292. package/docs/architecture/evolution.md +45 -0
  293. package/docs/init-surface.manifest.json +9 -0
  294. package/docs/operations/README.md +1 -1
  295. package/docs/operations/local-development-environment.md +21 -0
  296. package/docs/templates/README.md +2 -1
  297. package/docs/templates/agent-dag-report.schema.json +8 -2
  298. package/docs/templates/agent-dag.schema.json +45 -1
  299. package/docs/templates/frontend-implementation-contract.schema.json +684 -24
  300. package/docs/templates/frontend-test-case-checklist.md +2 -2
  301. package/docs/templates/frontend-test-dag.json +8 -10
  302. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  303. package/docs/templates/init-managed-agents.md +13 -3
  304. package/docs/templates/spec-registry.schema.json +45 -0
  305. package/harness.json +3 -3
  306. package/package.json +7 -1
  307. package/scripts/next-info.mjs +356 -0
  308. package/scripts/next-publish-gate.mjs +238 -0
  309. package/scripts/release-source-binding.mjs +251 -0
  310. package/skills/codebase-scout/SKILL.md +1 -1
  311. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  312. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  313. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  314. package/skills/frontend-bounded-implement/SKILL.md +8 -14
  315. package/skills/frontend-design-review/SKILL.md +20 -41
  316. package/skills/frontend-implementation/SKILL.md +3 -4
  317. package/skills/frontend-implementation/references/design-spec.md +15 -10
  318. package/skills/frontend-implementation/references/node-contracts.md +23 -13
  319. package/skills/frontend-review/SKILL.md +22 -14
  320. package/skills/frontend-review/references/review-findings.md +6 -7
  321. package/skills/loop-agent/references/command-reference.md +12 -4
  322. package/skills/loop-agent/references/hybrid-dag.md +8 -1
  323. package/skills/playwright-cli/SKILL.md +23 -1
  324. package/skills/playwright-cli-case-generator/SKILL.md +29 -8
  325. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  326. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  327. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  328. package/dist/worker/console/static/assets/index-DTZOKgAn.js +0 -404
  329. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  330. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  331. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -8,6 +8,15 @@ 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, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
16
+ import { isDagSourceBindingV2 } from "./types.js";
17
+ import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
18
+ import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
19
+ import { isConfigurationVerificationFile, verificationSymbolMatchesContent, } from "./frontend-verification-trace.js";
11
20
  export const frontendNormalizationActionSchema = z.enum([
12
21
  "remove-trailing-commas",
13
22
  "strip-comments",
@@ -102,6 +111,68 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
102
111
  }
103
112
  return [...labels];
104
113
  }
114
+ /**
115
+ * Extract the design-policy allowedDependencies set from the DAG run spec
116
+ * (run.json), mirroring `deriveFrozenCommandLabelsFromRun`. Used by the plan
117
+ * node's front-loaded policy pre-check so dependency declarations can be
118
+ * corrected through the retry ladder before the policy shell (the authority)
119
+ * re-runs the same check on committed facts.
120
+ */
121
+ async function deriveAllowedDependenciesFromRun(runDir) {
122
+ const specPath = path.join(runDir, "run.json");
123
+ let raw;
124
+ try {
125
+ raw = await readFile(specPath, "utf8");
126
+ }
127
+ catch {
128
+ return [];
129
+ }
130
+ let spec;
131
+ try {
132
+ spec = JSON.parse(raw);
133
+ }
134
+ catch {
135
+ return [];
136
+ }
137
+ const deps = new Set();
138
+ for (const task of spec.tasks ?? []) {
139
+ const allowed = task.shell?.frontendDesignPolicy?.allowedDependencies;
140
+ if (allowed) {
141
+ for (const dep of allowed)
142
+ deps.add(dep);
143
+ }
144
+ }
145
+ return [...deps];
146
+ }
147
+ /** Read the frozen implementation writeSet patterns from run.json so the plan
148
+ * validator can front-load checkTargetPaths (write-set-too-large) before the
149
+ * design-policy shell terminates the run. */
150
+ async function deriveWriteSetFromRun(runDir) {
151
+ const specPath = path.join(runDir, "run.json");
152
+ let raw;
153
+ try {
154
+ raw = await readFile(specPath, "utf8");
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ let spec;
160
+ try {
161
+ spec = JSON.parse(raw);
162
+ }
163
+ catch {
164
+ return [];
165
+ }
166
+ const writeSet = new Set();
167
+ for (const task of spec.tasks ?? []) {
168
+ const patterns = task.shell?.frontendDesignPolicy?.implementationWriteSet;
169
+ if (patterns) {
170
+ for (const pattern of patterns)
171
+ writeSet.add(pattern);
172
+ }
173
+ }
174
+ return [...writeSet];
175
+ }
105
176
  export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
106
177
  export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
107
178
  /**
@@ -169,7 +240,6 @@ export function loadFrontendImplementationContractJsonSchema(startDir = path.dir
169
240
  "mockApi",
170
241
  "designEvidence",
171
242
  "verificationTargets",
172
- "evidenceGaps",
173
243
  ];
174
244
  const actualRequired = Array.isArray(schema.required) ? schema.required : [];
175
245
  const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
@@ -218,13 +288,13 @@ const gap = z
218
288
  * `specReference` is required (path/section/line hit).
219
289
  * - `reuse-existing`: reuse an existing repo component/convention not named by
220
290
  * the spec → `specReference` may be null, rationale explains the basis.
221
- * - `new`: neither spec nor existing code fits → `specReference` must be null,
222
- * rationale must declare the deviation (design-review approves it).
291
+ * - `new`: the task source mandates a component absent from the repository →
292
+ * `specReference` is required and points at that task-source declaration;
293
+ * rationale explains the bounded addition.
223
294
  *
224
- * `specReference` is nullable + optional because `stripNullValuesDeep` strips
225
- * null object values before validation, so a `new` choice's `specReference:
226
- * null` becomes an absent field; the superRefine below treats absent and null
227
- * identically.
295
+ * `specReference` is nullable + optional at the wire boundary because
296
+ * `stripNullValuesDeep` removes model-emitted nulls before validation. The
297
+ * decision-specific requirements are enforced by the superRefine below.
228
298
  */
229
299
  const uiComponentSpecReferenceSchema = z
230
300
  .object({
@@ -239,7 +309,11 @@ const uiComponentChoiceSchema = z
239
309
  component: z.string().min(1),
240
310
  decision: z.enum(["specified", "reuse-existing", "new"]),
241
311
  specReference: uiComponentSpecReferenceSchema.nullable().optional(),
242
- rationale: z.string().min(1),
312
+ // Optional: the decision enum already carries the semantic (specified →
313
+ // specReference, new → task-source traceability, reuse-existing → no
314
+ // rationale needed).
315
+ // Omit for reuse-existing to keep plan output small.
316
+ rationale: z.string().min(1).optional(),
243
317
  })
244
318
  .strict();
245
319
  const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
@@ -268,7 +342,12 @@ function stripNullValuesDeep(value) {
268
342
  return value;
269
343
  }
270
344
  export const frontendImplementationContractSchema = z
271
- .preprocess(stripNullValuesDeep, z
345
+ .preprocess((value) => {
346
+ const record = asRecord(value);
347
+ if (!record)
348
+ return stripNullValuesDeep(value);
349
+ return stripNullValuesDeep(record);
350
+ }, z
272
351
  .object({
273
352
  schemaVersion: z.literal(1),
274
353
  sourceBinding: z
@@ -278,8 +357,33 @@ export const frontendImplementationContractSchema = z
278
357
  requirementSha256: z.string().regex(/^[a-f0-9]{64}$/),
279
358
  referencePaths: z.array(safePath),
280
359
  requirementIds: z.array(id),
360
+ // Source binding v2 (frontend): bound ledger pointer + fragment map.
361
+ ledgerPath: safePath.optional(),
362
+ ledgerSha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
363
+ inputDigest: z.string().regex(/^[a-f0-9]{64}$/).optional(),
364
+ requirementToFragments: z
365
+ .record(z.string(), z.array(z.string()))
366
+ .optional(),
281
367
  })
282
- .strict(),
368
+ .strict()
369
+ .superRefine((value, ctx) => {
370
+ // v2 all-or-none:任一 ledger 字段出现则四项必须齐全,防止 v1+optional 冒充 v2。
371
+ const anyLedgerField = value.ledgerPath !== undefined ||
372
+ value.ledgerSha256 !== undefined ||
373
+ value.inputDigest !== undefined ||
374
+ value.requirementToFragments !== undefined;
375
+ const allLedgerFields = value.ledgerPath !== undefined &&
376
+ value.ledgerSha256 !== undefined &&
377
+ value.inputDigest !== undefined &&
378
+ value.requirementToFragments !== undefined;
379
+ if (anyLedgerField && !allLedgerFields) {
380
+ ctx.addIssue({
381
+ code: z.ZodIssueCode.custom,
382
+ path: ["sourceBinding"],
383
+ message: "source binding v2 ledger fields (ledgerPath/ledgerSha256/inputDigest/requirementToFragments) must be provided together; partial or optional v1+ledger bindings are rejected",
384
+ });
385
+ }
386
+ }),
283
387
  riskLevel: z.enum(["small", "standard", "high-risk"]),
284
388
  targets: z
285
389
  .object({
@@ -295,6 +399,10 @@ export const frontendImplementationContractSchema = z
295
399
  implementationTargets: z.array(safePath),
296
400
  verificationTargetIds: z.array(z.string().min(1)),
297
401
  evidenceGap: gap.optional(),
402
+ // Source fidelity ledger binding (AC-005/AC-006): 每个 requirement
403
+ // 携带来源 fragment 证据,prewrite 门禁校验其真实性与覆盖。
404
+ sourceFragmentIds: z.array(z.string().min(1)).optional(),
405
+ sourceRefs: z.array(safePath).optional(),
298
406
  })
299
407
  .strict()).min(1),
300
408
  uiStates: z.array(z
@@ -367,7 +475,7 @@ export const frontendImplementationContractSchema = z
367
475
  uiStates: z.array(z.string().min(1)),
368
476
  })
369
477
  .strict()).min(1),
370
- evidenceGaps: z.array(gap),
478
+ evidenceGaps: z.array(gap).optional(),
371
479
  implementationSteps: z.array(z.string().min(1)).optional(),
372
480
  stylingStrategy: z.string().min(1).optional(),
373
481
  uiComponentChoices: uiComponentChoicesSchema.optional(),
@@ -431,6 +539,32 @@ export const frontendImplementationContractSchema = z
431
539
  message: `unknown verification target ${targetId}`,
432
540
  path: ["requirements"],
433
541
  });
542
+ // Source fidelity ledger binding (AC-005/AC-006): 绑定携带
543
+ // requirementToFragments(ledger v2)时,每个 requirement 必须携带
544
+ // 非空 sourceFragmentIds,否则 fail closed(防引用伪造/缺失)。
545
+ // sourceRefs(fragment→path 的派生展示)不设硬门槛:模型从 binding
546
+ // 只能拿到 fragment id 拿不到权威 path,强制只会诱导编造;防伪造的
547
+ // 真实性校验由 fidelity gate 按 sourceFragmentIds 执行(存在性 +
548
+ // 与 ledger boundFragments 一致)。
549
+ const boundFragments = value.sourceBinding.requirementToFragments?.[requirement.id];
550
+ if (value.sourceBinding.ledgerPath &&
551
+ (!requirement.sourceFragmentIds ||
552
+ requirement.sourceFragmentIds.length === 0)) {
553
+ ctx.addIssue({
554
+ code: "custom",
555
+ message: `requirement ${requirement.id} must carry sourceFragmentIds when a source fidelity ledger is bound`,
556
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
557
+ });
558
+ }
559
+ else if (boundFragments !== undefined &&
560
+ (!requirement.sourceFragmentIds ||
561
+ requirement.sourceFragmentIds.length === 0)) {
562
+ ctx.addIssue({
563
+ code: "custom",
564
+ message: `requirement ${requirement.id} is bound to ledger fragments ${boundFragments.join(", ")} but declares none`,
565
+ path: ["requirements", requirement.id, "sourceFragmentIds"],
566
+ });
567
+ }
434
568
  }
435
569
  for (const state of value.uiStates) {
436
570
  if (state.applicable &&
@@ -460,10 +594,10 @@ export const frontendImplementationContractSchema = z
460
594
  }
461
595
  }
462
596
  else if (choice.decision === "new") {
463
- if (choice.specReference) {
597
+ if (!choice.specReference || !choice.specReference.path) {
464
598
  ctx.addIssue({
465
599
  code: "custom",
466
- message: "new component choice must not carry a specReference",
600
+ message: "new component choice requires a task-source specReference.path",
467
601
  path: ["uiComponentChoices", index, "specReference"],
468
602
  });
469
603
  }
@@ -567,6 +701,29 @@ export async function assertFrontendSourceBindingFresh(input) {
567
701
  throw new Error(`frontend source binding is stale: ${source.path}`);
568
702
  }
569
703
  }
704
+ // Source binding v2 ledger 新鲜度(AC-FIDELITY-REPAIR-001/002):v2 绑定携带的
705
+ // ledger 文件必须存在且 sha256 与绑定一致,否则 fail closed;v1 绑定无此路径。
706
+ if (isDagSourceBindingV2(input.binding)) {
707
+ const absolute = resolveDagTaskSourcePath({
708
+ workspaceRoot: input.workspaceRoot,
709
+ taskId: input.binding.taskId,
710
+ sourcePath: input.binding.ledgerPath,
711
+ });
712
+ let content;
713
+ try {
714
+ content = await readFile(absolute);
715
+ const info = await stat(absolute);
716
+ if (!info.isFile())
717
+ throw new Error("not a regular file");
718
+ }
719
+ catch (error) {
720
+ throw new Error(`frontend source fidelity ledger unavailable: ${input.binding.ledgerPath}: ${error instanceof Error ? error.message : String(error)}`);
721
+ }
722
+ const actual = createHash("sha256").update(content).digest("hex");
723
+ if (actual !== input.binding.ledgerSha256) {
724
+ throw new Error(`frontend source fidelity ledger is stale or tampered: ${input.binding.ledgerPath} (bound ${input.binding.ledgerSha256}, observed ${actual})`);
725
+ }
726
+ }
570
727
  }
571
728
  const SECRET_KEY = /(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential|authorization)/i;
572
729
  const SECRET_VALUE = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|ghp|github_pat|xox[baprs]|AKIA)[-_A-Za-z0-9]{12,}\b)/;
@@ -741,7 +898,7 @@ export function canonicalFrontendContractSourceBinding(binding) {
741
898
  const requirement = binding.sources.find((source) => source.kind === "requirement");
742
899
  if (!requirement)
743
900
  throw new Error("frontend implementation contract gate requires a requirement source in DAG sourceBinding");
744
- return {
901
+ const sourceBinding = {
745
902
  taskId: binding.taskId,
746
903
  requirementPath: requirement.path,
747
904
  requirementSha256: requirement.sha256,
@@ -751,6 +908,15 @@ export function canonicalFrontendContractSourceBinding(binding) {
751
908
  .sort(),
752
909
  requirementIds: binding.requirementIds.map(canonicalizeRequirementId),
753
910
  };
911
+ // Source binding v2(schemaVersion=2 强制字段):透传 ledger 指针与
912
+ // requirement→fragment 映射。v1 绑定不得以 optional 字段冒充 v2。
913
+ if (isDagSourceBindingV2(binding)) {
914
+ sourceBinding.ledgerPath = binding.ledgerPath;
915
+ sourceBinding.ledgerSha256 = binding.ledgerSha256;
916
+ sourceBinding.inputDigest = binding.inputDigest;
917
+ sourceBinding.requirementToFragments = binding.requirementToFragments;
918
+ }
919
+ return sourceBinding;
754
920
  }
755
921
  function asRecord(value) {
756
922
  return value && typeof value === "object" && !Array.isArray(value)
@@ -1079,6 +1245,40 @@ function looksLikeStrictFrontendContract(value) {
1079
1245
  asRecord(record.mockApi) !== null &&
1080
1246
  asRecord(record.designEvidence) !== null);
1081
1247
  }
1248
+ /**
1249
+ * Single shared completion layer for required-with-fallback contract fields.
1250
+ *
1251
+ * Structural invariant: the coerce normalizer has strict-shape and free-form
1252
+ * branches that reshape differently, but every required key that carries a
1253
+ * deterministic fallback (currently `designEvidence`) must be completed in
1254
+ * exactly ONE place — here — so the branches can never drift again (the
1255
+ * designEvidence Required and invented-uiStates failures both came from
1256
+ * branch-local rules diverging). This layer never invents values that later
1257
+ * refinements reject: it only fills documented fallbacks for keys already
1258
+ * present-but-partial, and is idempotent for already-complete inputs.
1259
+ */
1260
+ export function completeCanonicalRequiredFields(value) {
1261
+ const record = asRecord(value);
1262
+ if (!record)
1263
+ return value;
1264
+ const designEvidence = asRecord(record.designEvidence);
1265
+ if (!designEvidence)
1266
+ return value;
1267
+ const source = asString(designEvidence.source);
1268
+ const paths = Array.isArray(designEvidence.paths);
1269
+ const conflicts = Array.isArray(designEvidence.conflicts);
1270
+ if (source && paths && conflicts)
1271
+ return value;
1272
+ return {
1273
+ ...record,
1274
+ designEvidence: {
1275
+ ...designEvidence,
1276
+ ...(source ? {} : { source: "repository-fallback+task-source" }),
1277
+ ...(paths ? {} : { paths: [] }),
1278
+ ...(conflicts ? {} : { conflicts: [] }),
1279
+ },
1280
+ };
1281
+ }
1082
1282
  /**
1083
1283
  * Coerce common free-form plan JSON into frontend-implementation-contract-v1.
1084
1284
  * Near-schema payloads are left untouched so unknown-key fail-closed still holds.
@@ -1183,6 +1383,9 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1183
1383
  ...strictRecord,
1184
1384
  requirements,
1185
1385
  ...(strictMockApi ? { mockApi: { ...strictMockApi, endpoints: mockEndpoints } } : {}),
1386
+ // Required-with-fallback keys (designEvidence etc.) are completed
1387
+ // by the single shared `completeCanonicalRequiredFields` layer in
1388
+ // the analyze pipeline — never per-branch.
1186
1389
  };
1187
1390
  }
1188
1391
  }
@@ -1231,9 +1434,7 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1231
1434
  : typeRaw.includes("type")
1232
1435
  ? "static"
1233
1436
  : "unit";
1234
- const commandLabel = asString(vt.commandLabel) ||
1235
- asString(vt.command) ||
1236
- (type === "static" ? "npm run typecheck" : "npm run test:unit:fe");
1437
+ const commandLabel = asString(vt.commandLabel) || asString(vt.command);
1237
1438
  const file = asString(vt.file) ||
1238
1439
  (Array.isArray(vt.symbols) ? targetFiles.find((p) => p.includes("__tests__")) : "") ||
1239
1440
  targetFiles.find((p) => p.includes("__tests__")) ||
@@ -1249,7 +1450,11 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1249
1450
  requirementIds: requirementIds.length > 0
1250
1451
  ? requirementIds
1251
1452
  : [...canonicalBinding.requirementIds],
1252
- uiStates: uiStateNames.length > 0 ? uiStateNames : ["success", "error"],
1453
+ // Never invent UI state references: "success" is not a canonical
1454
+ // state name, so a defaulted ["success","error"] was always rejected
1455
+ // by the schema's unknown-uiState refinement — a verification
1456
+ // target that declares no UI binding stays unbound ([]).
1457
+ uiStates: uiStateNames,
1253
1458
  });
1254
1459
  }
1255
1460
  if (verificationTargets.length === 0) {
@@ -1284,6 +1489,14 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
1284
1489
  verificationTargetIds: verificationTargetIds.length > 0
1285
1490
  ? verificationTargetIds
1286
1491
  : defaultVerificationIds,
1492
+ // Source fidelity provenance 透传:兼容分支重建 requirement 时必须
1493
+ // 保留已声明的 sourceFragmentIds/sourceRefs(strict 分支通过
1494
+ // `...requirement` 保留,两边必须一致),否则 ledger-bound 契约在
1495
+ // designEvidence 缺失等情况下走兼容分支时丢失防伪造证据。
1496
+ ...(Array.isArray(req.sourceFragmentIds)
1497
+ ? { sourceFragmentIds: req.sourceFragmentIds }
1498
+ : {}),
1499
+ ...(Array.isArray(req.sourceRefs) ? { sourceRefs: req.sourceRefs } : {}),
1287
1500
  };
1288
1501
  // Do not mark free-form realIntegrationGap as blocking; defer to FE-TEST/FINAL-VERIFY.
1289
1502
  if (gapText) {
@@ -1485,7 +1698,13 @@ export async function analyzeFrontendImplementationContract(input) {
1485
1698
  rawOutput = input.rawContractText;
1486
1699
  }
1487
1700
  else if (input.fromNodeId !== undefined) {
1488
- const record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1701
+ let record;
1702
+ try {
1703
+ record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
1704
+ }
1705
+ catch (error) {
1706
+ throw new Error(`cannot parse node record ${input.fromNodeId}: ${error instanceof Error ? error.message : String(error)}`);
1707
+ }
1489
1708
  rawOutput = record.assistantText ?? record.stdout ?? "";
1490
1709
  }
1491
1710
  else {
@@ -1593,15 +1812,16 @@ export async function analyzeFrontendImplementationContract(input) {
1593
1812
  // There is exactly one post-security candidate. A fallback candidate would
1594
1813
  // allow malformed raw fields to bypass the boundary checks above.
1595
1814
  const normalizedContract = coerceFrontendImplementationContractInput(parsed, canonicalBinding);
1815
+ const completedContract = completeCanonicalRequiredFields(normalizedContract);
1596
1816
  const candidate = deriveFrontendVerificationCoverage({
1597
- ...(asRecord(normalizedContract) ?? parsed),
1817
+ ...(asRecord(completedContract) ?? parsed),
1598
1818
  sourceBinding: canonicalBinding,
1599
1819
  }, canonicalBinding);
1600
1820
  const result = frontendImplementationContractSchema.safeParse(candidate);
1601
1821
  if (!result.success)
1602
1822
  fail("retryable-invalid", `invalid-output: ${result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`, candidateJsonSha256);
1603
1823
  const blockingGaps = [
1604
- ...result.data.evidenceGaps,
1824
+ ...(result.data.evidenceGaps ?? []),
1605
1825
  ...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
1606
1826
  ].filter((item) => item.blocking);
1607
1827
  if (blockingGaps.length > 0)
@@ -1610,7 +1830,7 @@ export async function analyzeFrontendImplementationContract(input) {
1610
1830
  .join(", ")}`, candidateJsonSha256);
1611
1831
  for (const requirementId of canonicalBinding.requirementIds)
1612
1832
  if (!result.data.requirements.some((item) => item.id === requirementId) &&
1613
- !result.data.evidenceGaps.some((item) => item.requirementId === requirementId))
1833
+ !(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
1614
1834
  fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
1615
1835
  return {
1616
1836
  canonical: result.data,
@@ -1634,6 +1854,40 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
1634
1854
  "/targets/files",
1635
1855
  "/mockApi/productionDefaultOff",
1636
1856
  ];
1857
+ const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
1858
+ "target-surface",
1859
+ "component-choice",
1860
+ "state-flow",
1861
+ "data-flow",
1862
+ "mock-api",
1863
+ "design-deviation",
1864
+ "dependency",
1865
+ "plan-requirement",
1866
+ "plan-verification-target",
1867
+ "plan-evidence-gap",
1868
+ ];
1869
+ /**
1870
+ * Restore the editable RFC 7386 plan patch from a committed plan ledger.
1871
+ * This is the single compile authority for the frontend implementation
1872
+ * contract: the tool payload (`patch` on any committed plan fact) wins over
1873
+ * any narrative JSON in the plan node text.
1874
+ */
1875
+ export function restorePlanPatchFromCommittedFacts(records) {
1876
+ const planFacts = records
1877
+ .filter((record) => record.phase === "committed")
1878
+ .map((record) => record.fact)
1879
+ .filter((fact) => fact.origin === "plan" &&
1880
+ typeof fact.kind === "string" &&
1881
+ PLAN_LEDGER_FACT_KINDS_FOR_COMPILE.includes(fact.kind));
1882
+ if (planFacts.length === 0)
1883
+ return undefined;
1884
+ for (const fact of planFacts) {
1885
+ const patch = fact.patch;
1886
+ if (isPlainObject(patch))
1887
+ return patch;
1888
+ }
1889
+ return undefined;
1890
+ }
1637
1891
  function frontendPlanPatchProtectedPathViolations(patch) {
1638
1892
  const record = asRecord(patch);
1639
1893
  if (!record)
@@ -1663,34 +1917,34 @@ function workspaceRootFromDagRunDir(runDir) {
1663
1917
  const joined = segments.slice(0, harnessIndex).join(path.sep);
1664
1918
  return joined || path.parse(absolute).root;
1665
1919
  }
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
1920
  async function dropUnresolvedVerificationSymbols(input) {
1684
1921
  const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
1685
1922
  const targets = asRecord(input.contract)?.verificationTargets;
1686
1923
  if (!workspaceRoot || !Array.isArray(targets))
1687
- return false;
1688
- let changed = false;
1924
+ return [];
1925
+ const actions = [];
1689
1926
  for (const targetValue of targets) {
1690
1927
  const target = asRecord(targetValue);
1691
1928
  const file = asString(target?.file);
1692
1929
  const symbol = asString(target?.symbol);
1693
- if (!target || !file || !symbol)
1930
+ if (!target || !file)
1931
+ continue;
1932
+ // Static targets (project-wide commands such as typecheck/build) verify
1933
+ // by command exit code, never by a per-file symbol. Strip any symbol
1934
+ // the planner attached — including fabricated placeholders like "null"
1935
+ // — so the plan shape check and the verify trace gate never evaluate
1936
+ // it. This is the root fix for the recurring fabricated-symbol error
1937
+ // class: the field was allowed for static, the prompt did not forbid
1938
+ // it, and models habitually fill it; the compiler removes it instead.
1939
+ if (target.type === "static") {
1940
+ if (symbol) {
1941
+ delete target.symbol;
1942
+ if (!actions.includes("strip-static-verification-symbols"))
1943
+ actions.push("strip-static-verification-symbols");
1944
+ }
1945
+ continue;
1946
+ }
1947
+ if (!symbol)
1694
1948
  continue;
1695
1949
  const absolute = path.resolve(workspaceRoot, file);
1696
1950
  const relative = path.relative(workspaceRoot, absolute);
@@ -1699,6 +1953,10 @@ async function dropUnresolvedVerificationSymbols(input) {
1699
1953
  relative.includes("..")) {
1700
1954
  continue;
1701
1955
  }
1956
+ // Configuration files (tsconfig.json, vite.config.*, ...) do not expose
1957
+ // source symbols; the trace gate skips them, so the drop must too.
1958
+ if (isConfigurationVerificationFile(file))
1959
+ continue;
1702
1960
  let content;
1703
1961
  try {
1704
1962
  content = await readFile(absolute, "utf8");
@@ -1708,60 +1966,78 @@ async function dropUnresolvedVerificationSymbols(input) {
1708
1966
  // changing the optional symbol during candidate normalization.
1709
1967
  continue;
1710
1968
  }
1711
- if (verificationSymbolMatchesFile(symbol, content))
1969
+ if (verificationSymbolMatchesContent(symbol, content))
1712
1970
  continue;
1713
1971
  delete target.symbol;
1714
- changed = true;
1972
+ if (!actions.includes("drop-unresolved-verification-symbols"))
1973
+ actions.push("drop-unresolved-verification-symbols");
1715
1974
  }
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);
1975
+ return actions;
1752
1976
  }
1753
1977
  async function writeFrontendPlanCandidateRaw(input) {
1754
1978
  const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
1755
- await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text);
1979
+ await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
1756
1980
  return relativePath;
1757
1981
  }
1758
1982
  /**
1759
1983
  * Compile the planner's editable RFC 7386 patch against the deterministic
1760
1984
  * runtime skeleton. Every attempt is preserved under contracts/candidates so
1761
1985
  * format/schema failures can be inspected and replayed without trusting model
1762
- * prose. A successful result returns canonical full-contract text for all
1986
+ * prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
1763
1987
  * downstream review and prewrite nodes.
1764
1988
  */
1989
+ /**
1990
+ * Placeholder tokens that are never valid verification symbols ("null" is the
1991
+ * classic model artifact: it serializes a JS null into the symbol string).
1992
+ */
1993
+ const VERIFICATION_SYMBOL_PLACEHOLDERS = new Set([
1994
+ "null",
1995
+ "undefined",
1996
+ "none",
1997
+ "n/a",
1998
+ "na",
1999
+ "tbd",
2000
+ "todo",
2001
+ "...",
2002
+ "?",
2003
+ "-",
2004
+ ]);
2005
+ /** An over-long symbol can never resolve to a real declaration. */
2006
+ const VERIFICATION_SYMBOL_MAX_CHARS = 60;
2007
+ /**
2008
+ * Deterministic shape check for a verification target symbol. Catches obvious
2009
+ * model fabrication (placeholder tokens, prose fragments) at plan time so the
2010
+ * plan retry ladder can fix the facts in-node instead of the verify trace gate
2011
+ * failing at the very end of the run.
2012
+ *
2013
+ * Deliberately conservative: bare test titles without punctuation ("renders
2014
+ * loading") and `describe(...)` / `it(...)` forms stay valid — a real symbol
2015
+ * may be a function name, a dotted path, or a describe/it title.
2016
+ */
2017
+ function isSuspiciousVerificationSymbol(symbol) {
2018
+ const trimmed = symbol.trim();
2019
+ if (!trimmed)
2020
+ return false;
2021
+ if (VERIFICATION_SYMBOL_PLACEHOLDERS.has(trimmed.toLowerCase()))
2022
+ return true;
2023
+ if (trimmed.length > VERIFICATION_SYMBOL_MAX_CHARS)
2024
+ return true;
2025
+ // Deliberately conservative: bare test titles with punctuation (e.g.
2026
+ // "adds +1 button") are valid describe/it-style symbols and must pass;
2027
+ // the length + placeholder rules above already catch obvious fabrication
2028
+ // ("null", over-long prose fragments). Everything else is left to the
2029
+ // deterministic drop/trace resolvability check.
2030
+ return false;
2031
+ }
2032
+ function assertVerificationSymbolShapes(contract) {
2033
+ for (const target of contract.verificationTargets) {
2034
+ if (!target.symbol)
2035
+ continue;
2036
+ if (isSuspiciousVerificationSymbol(target.symbol)) {
2037
+ 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)`);
2038
+ }
2039
+ }
2040
+ }
1765
2041
  export async function validateFrontendPlanPatchNodeOutput(input) {
1766
2042
  const nodeId = input.nodeId ?? "frontend-plan-pi";
1767
2043
  const attempt = input.attempt ?? 1;
@@ -1798,20 +2074,18 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1798
2074
  const skeleton = input.structuredContractOutput?.skeleton;
1799
2075
  if (!skeleton)
1800
2076
  throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
1801
- const patch = extractFrontendPlanPrimaryPayload(input.text);
2077
+ const committed = await readTypedEventStoreFromJsonl(path.join(input.runDir, nodeId, "plan-typed-facts.jsonl"));
2078
+ const patch = restorePlanPatchFromCommittedFacts(committed);
1802
2079
  if (!isPlainObject(patch))
1803
- throw new Error("frontend plan patch must extract to one JSON object");
2080
+ throw new Error("frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority");
1804
2081
  extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
1805
2082
  const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
1806
2083
  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
2084
  const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1811
2085
  if (protectedViolations.length > 0)
1812
2086
  throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
1813
2087
  const merged = applyFrontendContractMergePatch(skeleton, patch);
1814
- const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
2088
+ const symbolNormalizationActions = await dropUnresolvedVerificationSymbols({
1815
2089
  runDir: input.runDir,
1816
2090
  contract: merged,
1817
2091
  });
@@ -1820,6 +2094,24 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1820
2094
  rawContractText: serializeDeterministicJson(merged),
1821
2095
  sourceBinding: input.sourceBinding,
1822
2096
  });
2097
+ // Front-load the verification-symbol shape check so fabricated symbols
2098
+ // are fixed by the plan retry ladder in-node instead of failing the
2099
+ // verify trace gate at the end of the run.
2100
+ assertVerificationSymbolShapes(analysis.canonical);
2101
+ // Front-load the plan-attributable design-policy checks so the §5.1
2102
+ // retry ladder can fix these facts in-node with diagnostics instead of
2103
+ // the run terminating at the policy shell. The policy shell remains the
2104
+ // final authority and re-runs the identical checks on committed facts.
2105
+ const policyPreFindings = [
2106
+ ...checkUiDesignCoverage(analysis.canonical),
2107
+ ...checkUiStateAttribution(analysis.canonical),
2108
+ ...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
2109
+ ...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
2110
+ ];
2111
+ if (policyPreFindings.length > 0)
2112
+ throw new Error(`frontend plan policy pre-check failed (fix these plan facts, then re-commit and finalize): ${policyPreFindings
2113
+ .map((finding) => `${finding.code}: ${finding.message}`)
2114
+ .join("; ")}`);
1823
2115
  const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
1824
2116
  await writeReport({
1825
2117
  classification: "accepted-normalized",
@@ -1827,47 +2119,66 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
1827
2119
  normalizedContractSha256: normalizedArtifact.sha256,
1828
2120
  normalizationActions: [
1829
2121
  "apply-runtime-skeleton",
1830
- ...(droppedUnresolvedSymbols
1831
- ? ["drop-unresolved-verification-symbols"]
1832
- : []),
2122
+ ...symbolNormalizationActions,
1833
2123
  ...analysis.normalizationActions,
1834
2124
  ],
1835
2125
  errors: [],
1836
2126
  });
2127
+ const canonicalRel = canonicalContractRel(nodeId);
2128
+ const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
2129
+ await persistRuntimeSkeleton({
2130
+ runDir: input.runDir,
2131
+ nodeId,
2132
+ skeleton,
2133
+ });
2134
+ const artifact = {
2135
+ path: canonicalArtifact.path,
2136
+ sha256: canonicalArtifact.sha256,
2137
+ schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
2138
+ };
2139
+ // Human-readable plan summary appended after the artifact pointer. The
2140
+ // pointer remains the machine authority (path/schema/sha256); the
2141
+ // rendered summary is a deterministic view of the canonical contract so
2142
+ // Console/reviewers can read the plan without opening the JSON. It never
2143
+ // participates in downstream validation.
2144
+ const planSummary = renderFrontendPlanMarkdown(analysis.canonical);
1837
2145
  return {
1838
2146
  ok: true,
1839
2147
  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"),
2148
+ artifact,
2149
+ normalizedText: `${formatStructuredArtifactPointer(artifact)}\n\n${planSummary}`,
1861
2150
  };
1862
2151
  }
1863
2152
  catch (error) {
1864
2153
  const reason = error instanceof Error ? error.message : String(error);
2154
+ const classified = classifyStructuredContractFailure({
2155
+ reason,
2156
+ kind: error instanceof FrontendContractFailure ? error.kind : undefined,
2157
+ });
1865
2158
  await writeReport({
1866
- classification: "retryable-invalid",
2159
+ classification: classified.classification,
1867
2160
  normalizationActions: [],
1868
- errors: [{ path: "$", code: "invalid-output", message: reason }],
2161
+ errors: classified.errors,
1869
2162
  });
1870
- return { ok: false, reason };
2163
+ return {
2164
+ ok: false,
2165
+ reason,
2166
+ classification: classified.classification,
2167
+ errors: classified.errors,
2168
+ };
2169
+ }
2170
+ }
2171
+ export async function readFrontendCanonicalCandidate(runDir, nodeId) {
2172
+ const candidatePath = path.join(runDir, canonicalContractRel(nodeId));
2173
+ try {
2174
+ const raw = await readFile(candidatePath, "utf8");
2175
+ const parsed = frontendImplementationContractSchema.safeParse(JSON.parse(raw));
2176
+ return parsed.success ? parsed.data : undefined;
2177
+ }
2178
+ catch (error) {
2179
+ if (error.code === "ENOENT")
2180
+ return undefined;
2181
+ return undefined;
1871
2182
  }
1872
2183
  }
1873
2184
  /**
@@ -1888,79 +2199,140 @@ export async function validateFrontendContractNodeOutput(input) {
1888
2199
  return { ok: true, contract: analysis.canonical };
1889
2200
  }
1890
2201
  catch (error) {
2202
+ const reason = error instanceof Error ? error.message : String(error);
2203
+ const classified = classifyStructuredContractFailure({
2204
+ reason,
2205
+ kind: error instanceof FrontendContractFailure ? error.kind : undefined,
2206
+ });
1891
2207
  return {
1892
2208
  ok: false,
1893
- reason: error instanceof Error ? error.message : String(error),
2209
+ reason,
2210
+ classification: classified.classification,
2211
+ errors: classified.errors,
1894
2212
  };
1895
2213
  }
1896
2214
  }
1897
2215
  /**
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.
2216
+ * Deterministically assemble the canonical frontend implementation contract
2217
+ * from committed typed facts, never from plan/contract node text.
2218
+ *
2219
+ * Inputs (all read-only, all fail-closed):
2220
+ * - `<planNodeId>/plan-typed-facts.jsonl` (phase=committed) restores the
2221
+ * editable RFC 7386 patch;
2222
+ * - `<contractNodeId>/contract-typed-facts.jsonl` (phase=committed) must carry
2223
+ * a `contract-finalized` terminal fact;
2224
+ * - `<contractNodeId>/frontend-task-contract-vNext.json` must exist and parse.
2225
+ *
2226
+ * The patch is applied onto the runtime skeleton and re-validated through the
2227
+ * same `analyzeFrontendImplementationContract` pipeline used by the legacy
2228
+ * text path, so the downstream artifact semantics are unchanged. The function
2229
+ * is pure: it never writes candidate/audit artifacts and always produces the
2230
+ * same canonical serialization for the same committed facts. It never falls
2231
+ * back to parsing Plan/Contract node text.
1906
2232
  */
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;
2233
+ export async function buildContractFromCommittedFacts(input) {
2234
+ const contractNodeId = input.contractNodeId ?? "frontend-contract-pi";
2235
+ // Skeleton guard: the runtime skeleton must carry the protected identity
2236
+ // fields so the merged contract can be validated against the full schema.
2237
+ const skeletonRecord = asRecord(input.skeleton);
2238
+ const skeletonTargets = asRecord(skeletonRecord?.targets);
2239
+ const skeletonRiskLevel = asString(skeletonRecord?.riskLevel);
2240
+ if (!skeletonRecord ||
2241
+ !["small", "standard", "high-risk"].includes(skeletonRiskLevel) ||
2242
+ !Array.isArray(skeletonTargets?.files) ||
2243
+ skeletonTargets.files.length === 0) {
2244
+ return {
2245
+ ok: false,
2246
+ reason: "frontend contract skeleton missing required runtime fields (riskLevel/targets.files)",
2247
+ failureCode: "missing-skeleton",
2248
+ };
2249
+ }
2250
+ // a. Committed plan ledger restores the editable patch.
2251
+ const planRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.planNodeId, "plan-typed-facts.jsonl"));
2252
+ const patch = restorePlanPatchFromCommittedFacts(planRecords);
2253
+ if (!isPlainObject(patch)) {
2254
+ return {
2255
+ ok: false,
2256
+ reason: "frontend plan ledger missing: no committed origin=plan facts (record_* + finalize_plan); narrative JSON is not the compile authority",
2257
+ failureCode: "missing-plan-ledger",
2258
+ };
2259
+ }
2260
+ // b. Committed contract terminal fact (contract-finalized).
2261
+ const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
2262
+ const contractKinds = new Set(contractRecords
2263
+ .filter((record) => record.phase === "committed")
2264
+ .map((record) => record.fact.kind)
2265
+ .filter((kind) => typeof kind === "string"));
2266
+ if (!contractKinds.has("contract-finalized")) {
2267
+ return {
2268
+ ok: false,
2269
+ reason: "frontend contract facts missing: no committed contract-finalized terminal fact",
2270
+ failureCode: "missing-contract-finalize",
2271
+ };
2272
+ }
2273
+ // c. frontend-task-contract-vNext.json must exist and parse (input integrity).
2274
+ let rawVNext;
1919
2275
  try {
1920
- const parsed = extractFrontendPlanPayload(input.text);
1921
- citationsBlock = parsed.citationsBlock;
1922
- patch = parsed.payload;
2276
+ rawVNext = await readFile(path.join(input.runDir, contractNodeId, "frontend-task-contract-vNext.json"), "utf8");
2277
+ }
2278
+ catch {
2279
+ return {
2280
+ ok: false,
2281
+ reason: `frontend task contract vNext missing at ${contractNodeId}/frontend-task-contract-vNext.json`,
2282
+ failureCode: "contract-vnext-missing",
2283
+ };
2284
+ }
2285
+ try {
2286
+ JSON.parse(rawVNext);
1923
2287
  }
1924
2288
  catch (error) {
1925
2289
  return {
1926
2290
  ok: false,
1927
- reason: error instanceof Error ? error.message : String(error),
2291
+ reason: `frontend task contract vNext is not valid JSON: ${error instanceof Error ? error.message : String(error)}`,
2292
+ failureCode: "contract-vnext-invalid",
1928
2293
  };
1929
2294
  }
1930
- if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
2295
+ // d. Merge the editable patch onto the protected skeleton and validate.
2296
+ let merged;
2297
+ try {
2298
+ merged = applyFrontendContractMergePatch(input.skeleton, patch);
2299
+ }
2300
+ catch (error) {
1931
2301
  return {
1932
2302
  ok: false,
1933
- reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
2303
+ reason: `frontend contract patch could not be applied to the runtime skeleton: ${error instanceof Error ? error.message : String(error)}`,
2304
+ failureCode: "schema-drift",
2305
+ };
2306
+ }
2307
+ // The design-policy shell is the authoritative materializer of the
2308
+ // canonical contract; keep it on the same unresolved-symbol drop as the
2309
+ // plan validator so the verify trace gate never sees fabricated symbols.
2310
+ await dropUnresolvedVerificationSymbols({
2311
+ runDir: input.runDir,
2312
+ contract: merged,
2313
+ });
2314
+ try {
2315
+ const analysis = await analyzeFrontendImplementationContract({
2316
+ runDir: input.runDir,
2317
+ rawContractText: serializeDeterministicJson(merged),
2318
+ sourceBinding: input.sourceBinding,
2319
+ });
2320
+ return {
2321
+ ok: true,
2322
+ canonical: analysis.canonical,
2323
+ normalizationActions: analysis.normalizationActions,
2324
+ candidateJsonSha256: analysis.candidateJsonSha256,
1934
2325
  };
1935
2326
  }
1936
- const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
1937
- if (protectedViolations.length > 0) {
2327
+ catch (error) {
2328
+ const failureKind = error instanceof FrontendContractFailure ? error.kind : undefined;
1938
2329
  return {
1939
2330
  ok: false,
1940
- reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
2331
+ reason: `frontend contract schema drift: ${error instanceof Error ? error.message : String(error)}`,
2332
+ failureCode: "schema-drift",
2333
+ ...(failureKind ? { failureKind } : {}),
1941
2334
  };
1942
2335
  }
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
2336
  }
1965
2337
  export async function materializeFrontendImplementationContract(input) {
1966
2338
  const analysis = await analyzeFrontendImplementationContract(input);