@tea-agent/loop-agent 0.44.0-next.1 → 0.44.0-next.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 (226) hide show
  1. package/CHANGELOG.md +379 -416
  2. package/dist/application/evaluation/budget.js +21 -0
  3. package/dist/application/evaluation/corpus-hash.js +10 -15
  4. package/dist/application/evaluation/corpus.js +2 -1
  5. package/dist/application/evaluation/frontend-browser-acceptance.js +77 -0
  6. package/dist/application/evaluation/frontend-corpus-browser-acceptance.js +175 -0
  7. package/dist/application/evaluation/frontend-gateway-evidence.js +226 -0
  8. package/dist/application/evaluation/frontend-pair-registration.js +143 -0
  9. package/dist/application/evaluation/frontend-paired-summary.js +150 -0
  10. package/dist/application/evaluation/frontend-run-observation.js +144 -0
  11. package/dist/application/evaluation/frontend-shared-evidence.js +105 -0
  12. package/dist/application/evaluation/types.js +45 -12
  13. package/dist/application/task-lifecycle/observe.js +26 -1
  14. package/dist/build-stamp.json +3 -3
  15. package/dist/cli/command-definitions.js +11 -0
  16. package/dist/cli/program.js +4 -0
  17. package/dist/commands/task-advance.js +3 -5
  18. package/dist/commands/task-source-prepare.js +13 -1
  19. package/dist/executors/dag-pi/guards.js +4 -0
  20. package/dist/executors/dag-pi/model-config.js +30 -0
  21. package/dist/executors/dag-pi/plan/fact-adoption.js +55 -0
  22. package/dist/{workflows/dag/frontend-plan-completeness.js → executors/dag-pi/plan/facts.js} +22 -28
  23. package/dist/executors/dag-pi/plan/finalize-tools.js +259 -0
  24. package/dist/executors/dag-pi/plan/ledger-contract.js +55 -0
  25. package/dist/executors/dag-pi/plan/record-tools.js +861 -0
  26. package/dist/executors/dag-pi/plan/schema.js +129 -0
  27. package/dist/executors/dag-pi/plan/session.js +1 -0
  28. package/dist/executors/dag-pi/plan/tool-helpers.js +16 -0
  29. package/dist/executors/dag-pi/tools/contract-tools.js +515 -0
  30. package/dist/executors/dag-pi/tools/design-terminal-tools.js +159 -0
  31. package/dist/executors/dag-pi/tools/plan-decision-tools.js +529 -0
  32. package/dist/executors/dag-pi/tools/review-terminal-tools.js +159 -0
  33. package/dist/executors/dag-pi/tools/scout-evidence-tools.js +270 -0
  34. package/dist/executors/dag-pi-executor.js +398 -3384
  35. package/dist/executors/pi-executor.js +6 -1
  36. package/dist/executors/pi-sdk-executor.js +112 -19
  37. package/dist/executors/shell-executor.js +88 -12
  38. package/dist/executors/shell-presets.js +1 -1
  39. package/dist/executors/shell-write-guard.js +26 -3
  40. package/dist/infrastructure/console/operation-store.js +48 -14
  41. package/dist/infrastructure/evaluation/corpus-store.js +37 -12
  42. package/dist/shared/file-identity.js +23 -0
  43. package/dist/shared/git-workspace-status.js +482 -0
  44. package/dist/shared/operator/capabilities.js +10 -5
  45. package/dist/shared/path-safety.js +2 -2
  46. package/dist/shared/wizard-local-telemetry.js +51 -0
  47. package/dist/task/config-types.js +22 -8
  48. package/dist/task/contract/project.js +13 -0
  49. package/dist/task/contract/schema.js +2 -7
  50. package/dist/task/frontend-preflight.js +1 -1
  51. package/dist/task/source-prepare/parse-intent.js +124 -5
  52. package/dist/task/source-prepare/prepare.js +6 -1
  53. package/dist/task/source-prepare/semantic-intake.js +18 -3
  54. package/dist/task/source-prepare/source-execution.js +65 -0
  55. package/dist/task/source-prepare/source-fidelity-pi.js +22 -9
  56. package/dist/task/source-prepare/source-provider-budget.js +290 -0
  57. package/dist/worker/console/chat/assistant-content.js +5 -34
  58. package/dist/worker/console/chat/chat-event-store.js +62 -10
  59. package/dist/worker/console/chat/context-insights.js +723 -0
  60. package/dist/worker/console/chat/conversation-nodes.js +383 -0
  61. package/dist/worker/console/chat/db-connections.js +854 -0
  62. package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +1 -1
  63. package/dist/worker/console/chat/pi-runtime/custom-tools/sql-tools.js +655 -0
  64. package/dist/worker/console/chat/pi-runtime.js +445 -36
  65. package/dist/worker/console/chat/provider-error.js +0 -12
  66. package/dist/worker/console/chat/repo-browser.js +52 -4
  67. package/dist/worker/console/chat/routes.js +577 -72
  68. package/dist/worker/console/chat/sdd-data-alignment.js +8 -31
  69. package/dist/worker/console/chat/session-store.js +147 -19
  70. package/dist/worker/console/chat/shortcuts.js +7 -0
  71. package/dist/worker/console/chat/timeline-message-order.js +22 -0
  72. package/dist/worker/console/chat/tools.js +34 -0
  73. package/dist/worker/console/chat/turn-process.js +237 -66
  74. package/dist/worker/console/chat/user-turn-outline.js +19 -0
  75. package/dist/worker/console/console-update-runtime.js +2 -2
  76. package/dist/worker/console/dag-confirmation.js +3 -0
  77. package/dist/worker/console/dag-execution-receipt.js +3 -0
  78. package/dist/worker/console/operator-actions.js +32 -3
  79. package/dist/worker/console/operator-user-error.js +28 -1
  80. package/dist/worker/console/pi-readiness.js +4 -4
  81. package/dist/worker/console/prd-intake-bridge.js +5 -12
  82. package/dist/worker/console/recovery-cta.js +14 -14
  83. package/dist/worker/console/server.js +5 -1
  84. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-8-j6y-sd.js → abnfDiagram-N423BO3Z-r1NAPU_5.js} +1 -1
  85. package/dist/worker/console/static/assets/{arc-eoQiMvuk.js → arc-dT5T4sVL.js} +1 -1
  86. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-C4A3uMcI.js → architectureDiagram-T3A2C74G-CP_cOq46.js} +1 -1
  87. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-D4zD2F-Q.js → blockDiagram-VBNYF7ZC-sToEZxEf.js} +1 -1
  88. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-j1RkJziL.js → c4Diagram-5PPSVZJV-DSxwwi2m.js} +1 -1
  89. package/dist/worker/console/static/assets/channel-CyBY_yGk.js +1 -0
  90. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-YBHmhik1.js → chunk-2GRJ4B5K-B9fh0IkF.js} +1 -1
  91. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-COfWyo9P.js → chunk-2Q5K7J3B-CAUNl-A6.js} +1 -1
  92. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-I99OUkHY.js → chunk-5RXB4S5H-BomeqbNX.js} +1 -1
  93. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-XKxoJNJ4.js → chunk-5VM5RSS4-BNAuTOLE.js} +1 -1
  94. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DLT_cYx1.js → chunk-6Q2QTUOP-DfzjRD5y.js} +1 -1
  95. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CxcKZ9mV.js → chunk-GF5L2VYU-C3uE-yc2.js} +1 -1
  96. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-V1EqXdjY.js → chunk-JWPE2WC7-C3sQMMYr.js} +1 -1
  97. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DebTtdFp.js → chunk-KBJHAD2P-D_AtEYQL.js} +1 -1
  98. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-B-ivNRDf.js → chunk-RYQCIY6F-DW4TwhKv.js} +1 -1
  99. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DC_Ds11b.js → chunk-XXDRQBXY-CTQDmJcp.js} +1 -1
  100. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D5lN4E_E.js +1 -0
  101. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D5lN4E_E.js +1 -0
  102. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-sWEqKwIb.js → cose-bilkent-JH36ORCC-iMqPuBsL.js} +1 -1
  103. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BXa_dcu4.js → cynefin-VYW2F7L2-DktIuDWs.js} +1 -1
  104. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-C-Mle84F.js → cynefinDiagram-MW4NZA55-BgVnjyEs.js} +1 -1
  105. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-CXPDBITe.js → dagre-VZM6K2ZE-CR25E4bu.js} +1 -1
  106. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-CC-WJQfa.js → diagram-7IWD3JNH-CPM2h5HY.js} +1 -1
  107. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CDAV5Vs4.js → diagram-B4RE2ZJO-DqF0x2g1.js} +1 -1
  108. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CmFiAcNz.js → diagram-LBJQPF4R-G8BYp2eb.js} +1 -1
  109. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-DPBZHuyn.js → diagram-Q27KOJAE-BbC4kT_D.js} +1 -1
  110. package/dist/worker/console/static/assets/{diagram-UB23O5K3-jUlm_Ds3.js → diagram-UB23O5K3-CKZwZ-G5.js} +1 -1
  111. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DWhQ3mfY.js → ebnfDiagram-BXEA7PRR-kJDmheia.js} +1 -1
  112. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Tr2gMqet.js → erDiagram-JOGREHBK-DFTVHyxB.js} +1 -1
  113. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-DJjVQHPA.js → flowDiagram-UKHOOZJN-BqdN5_os.js} +1 -1
  114. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-D74dQ4u0.js → ganttDiagram-PKOTCBZU-SYDjVakh.js} +1 -1
  115. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-DwW0tZ0X.js → gitGraphDiagram-DS77QQ5N-Bf8bNpGA.js} +1 -1
  116. package/dist/worker/console/static/assets/index-B28Onmfy.css +1 -0
  117. package/dist/worker/console/static/assets/index-CTMZsDmh.js +486 -0
  118. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-ILCbxJyb.js → infoDiagram-6WML65LV-DIeAmJ45.js} +1 -1
  119. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DeuWBQ89.js → ishikawaDiagram-WSZJBQD7-oikqUAlo.js} +1 -1
  120. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CF5ih8Fk.js → journeyDiagram-NVQOT4AX-mKEF_xYT.js} +1 -1
  121. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-C7yOSuRO.js → kanban-definition-27J2QSJJ-BdWJHPnN.js} +1 -1
  122. package/dist/worker/console/static/assets/{linear-BDZ9riWi.js → linear-DGRlieMH.js} +1 -1
  123. package/dist/worker/console/static/assets/{mermaid.core-7pKqYtpZ.js → mermaid.core-4P9yPC9c.js} +5 -5
  124. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-LeJDybSU.js → mindmap-definition-FAOFIHXS-5PTSkos6.js} +1 -1
  125. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BJvT3pMD.js → pegDiagram-VL7TDLO6-DONIFpG5.js} +1 -1
  126. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-_rGqpMan.js → pieDiagram-7S7Q4E2Y-D_Z4S70M.js} +1 -1
  127. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-DPcSv5aA.js → quadrantDiagram-CIZ2JOQS-CAsjh8iY.js} +1 -1
  128. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Drxx4hkJ.js → railroadDiagram-AXF67PYL-Cx4Dd6ac.js} +1 -1
  129. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BCTUdU4z.js → requirementDiagram-LRYGKXZP-cQb7mwyH.js} +1 -1
  130. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-B6wzbZmB.js → sankeyDiagram-W5VNT64P-Cd-DhUdf.js} +1 -1
  131. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BGt8d3QQ.js → sequenceDiagram-SI44F4Z6-AlKYRgIk.js} +1 -1
  132. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-7nlhJKo7.js → sizeCapture-X5ZJPWSS-CUy_ET4e.js} +1 -1
  133. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Cv2stqMA.js → stateDiagram-OKZ733FA-CTuVe6Sa.js} +1 -1
  134. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BsWhs8O7.js +1 -0
  135. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-qDAo4Yc1.js → swimlanes-SLNWSIFB-ChFLJb-v.js} +2 -2
  136. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CJcioYGH.js +8 -0
  137. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CKbDNKTr.js → timeline-definition-Z64GVDOM-DZZeiAPr.js} +1 -1
  138. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DI-9EHic.js → vennDiagram-T6HMQDX7-t4O91cZU.js} +1 -1
  139. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BrzzRDpX.js → wardleyDiagram-T6FBY63Y-CV5qGHZu.js} +1 -1
  140. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-BclH5hGh.js → xychartDiagram-ELKLHX3M-DyXbytUI.js} +1 -1
  141. package/dist/worker/console/static/index.html +2 -2
  142. package/dist/worker/console/static-src/app/console-types.js +9 -1
  143. package/dist/worker/console/static-src/operator-chat/animation-frame-batcher.js +38 -0
  144. package/dist/worker/console/static-src/operator-chat/chat-link.js +20 -0
  145. package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +186 -5
  146. package/dist/worker/console/static-src/operator-chat/chat-stream-continuity.js +101 -0
  147. package/dist/worker/console/static-src/operator-chat/compaction-message.js +18 -3
  148. package/dist/worker/console/static-src/operator-chat/context-insights-store.js +64 -0
  149. package/dist/worker/console/static-src/operator-chat/details-rail-surfaces.js +98 -0
  150. package/dist/worker/console/static-src/operator-chat/file-palette-nav.js +81 -0
  151. package/dist/worker/console/static-src/operator-chat/pending-user-message.js +17 -3
  152. package/dist/worker/console/static-src/operator-chat/process-label.js +27 -5
  153. package/dist/worker/console/static-src/operator-chat/tools-catalog.js +16 -7
  154. package/dist/worker/console/static-src/operator-chat/transcript-mode.js +21 -0
  155. package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +4 -1
  156. package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +13 -5
  157. package/dist/worker/console/static-src/operator-chat/turn-state-projection.js +166 -0
  158. package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +56 -2
  159. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +211 -40
  160. package/dist/worker/console/static-src/operator-chat/useChatStream.js +124 -27
  161. package/dist/worker/console/static-src/operator-chat/useChatThread.js +88 -20
  162. package/dist/worker/console/static-src/operator-chat/useComposer.js +80 -6
  163. package/dist/worker/console/static-src/operator-chat/useRepoBrowser.js +13 -3
  164. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +36 -17
  165. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +18 -0
  166. package/dist/worker/console/static-src/operator-chat/useWorkspaceBrowserGate.js +4 -0
  167. package/dist/worker/console/static-src/shell/console-update-reload.js +1 -1
  168. package/dist/worker/console/workspace-context.js +2 -0
  169. package/dist/worker/console/workspace-initialization.js +3 -3
  170. package/dist/worker/observability/read-model.js +23 -2
  171. package/dist/worker/observe/node-context-insights.js +479 -0
  172. package/dist/worker/observe/static/console-theme.css +174 -0
  173. package/dist/worker/observe/static/console-theme.js +36 -0
  174. package/dist/worker/observe/static/operator-chrome.css +85 -0
  175. package/dist/worker/observe/static/operator-chrome.js +51 -0
  176. package/dist/worker/scheduler/scheduled-goal-dispatch.js +3 -2
  177. package/dist/worker/scheduler/scheduled-goal-evidence.js +66 -3
  178. package/dist/worker/scheduler/scheduled-goal-store.js +10 -0
  179. package/dist/workflows/dag/budget-enforcement.js +31 -2
  180. package/dist/workflows/dag/frontend-closeout.js +3 -1
  181. package/dist/workflows/dag/frontend-contract-facts.js +11 -8
  182. package/dist/workflows/dag/frontend-design-policy.js +6 -6
  183. package/dist/workflows/dag/frontend-durable-tools.js +90 -70
  184. package/dist/workflows/dag/frontend-implementation-contract.js +146 -57
  185. package/dist/workflows/dag/frontend-plan-canary.js +66 -16
  186. package/dist/workflows/dag/frontend-plan-decision-contract.js +13 -21
  187. package/dist/workflows/dag/frontend-plan-progress.js +92 -0
  188. package/dist/workflows/dag/frontend-recovery-plan.js +10 -5
  189. package/dist/workflows/dag/frontend-recovery-run.js +51 -46
  190. package/dist/workflows/dag/frontend-repair-assertions.js +36 -0
  191. package/dist/workflows/dag/frontend-review-context.js +29 -1
  192. package/dist/workflows/dag/frontend-review-scopes.js +92 -2
  193. package/dist/workflows/dag/frontend-risk.js +8 -3
  194. package/dist/workflows/dag/frontend-root-observation.js +261 -0
  195. package/dist/workflows/dag/frontend-session-budget.js +52 -39
  196. package/dist/workflows/dag/frontend-session-context.js +10 -0
  197. package/dist/workflows/dag/frontend-test-execution-evidence.js +206 -48
  198. package/dist/workflows/dag/frontend-typed-event-store.js +11 -1
  199. package/dist/workflows/dag/frontend-verification-trace.js +6 -0
  200. package/dist/workflows/dag/frontend-writer-admission.js +3 -1
  201. package/dist/workflows/dag/init-hybrid.js +108 -37
  202. package/dist/workflows/dag/node-execution.js +11 -11
  203. package/dist/workflows/dag/output-protocol.js +1 -0
  204. package/dist/workflows/dag/rerun-feedback.js +35 -4
  205. package/dist/workflows/dag/rerun-task.js +62 -66
  206. package/dist/workflows/dag/runner.js +58 -12
  207. package/dist/workflows/dag/types.js +15 -8
  208. package/dist/workflows/loop/benchmark.js +11 -8
  209. package/docs/README.md +1 -1
  210. package/docs/architecture/runtime-boundaries.md +1 -1
  211. package/docs/governance/harness-methodology-verification.md +11 -0
  212. package/docs/templates/backend-test-dag.json +5 -4
  213. package/docs/templates/frontend-implementation-contract.schema.json +31 -11
  214. package/package.json +6 -4
  215. package/skills/frontend-design-review/SKILL.md +8 -11
  216. package/skills/frontend-design-review/references/review-checklist.md +3 -4
  217. package/skills/frontend-review/SKILL.md +5 -1
  218. package/skills/loop-agent/references/command-reference.md +8 -0
  219. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  220. package/dist/worker/console/static/assets/channel-ChE7y-cx.js +0 -1
  221. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C01TCf2X.js +0 -1
  222. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C01TCf2X.js +0 -1
  223. package/dist/worker/console/static/assets/index-BWkIfcrK.css +0 -1
  224. package/dist/worker/console/static/assets/index-Cdkvw_H6.js +0 -469
  225. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DC7V5vcq.js +0 -1
  226. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Bwy4QUTO.js +0 -8
@@ -1,3 +1,4 @@
1
+ import { frontendPlanSemanticDigest } from "./frontend-plan-progress.js";
1
2
  import { mkdir, open, rename, rm } from "node:fs/promises";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import path from "node:path";
@@ -12,7 +13,7 @@ export function readFrontendToolProgress(tools) {
12
13
  const counters = [...new Set((tools ?? []).flatMap(tool => tool && typeof tool === "object" && progressByTool.has(tool) ? [progressByTool.get(tool)] : []))];
13
14
  if (!counters.length)
14
15
  return undefined;
15
- return counters.reduce((sum, item) => ({ failedRecords: sum.failedRecords + item.failedRecords, duplicateRecords: sum.duplicateRecords + item.duplicateRecords, durableSubmissions: sum.durableSubmissions + item.durableSubmissions, durableReplacements: sum.durableReplacements + item.durableReplacements }), { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 });
16
+ return counters.reduce((sum, item) => ({ finalizeRejections: sum.finalizeRejections + item.finalizeRejections, failedRecords: sum.failedRecords + item.failedRecords, duplicateRecords: sum.duplicateRecords + item.duplicateRecords, durableSubmissions: sum.durableSubmissions + item.durableSubmissions, durableReplacements: sum.durableReplacements + item.durableReplacements }), { finalizeRejections: 0, failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 });
16
17
  }
17
18
  async function exclusive(key, work) {
18
19
  const previous = queues.get(key) ?? Promise.resolve();
@@ -61,7 +62,8 @@ export async function createDurableFrontendTools(input) {
61
62
  return { ...tool, parameters: { ...schema, properties: { ...schema.properties, replace: { type: "boolean", description: "Explicitly revise the current fact with the same identity. Omit for new facts or identical replay." } } } };
62
63
  });
63
64
  const bindingSha256 = hash({ version: 1, attemptId: input.attemptId, binding: input.binding, schemas: tools.map(t => ({ name: t.name, parameters: t.parameters })) });
64
- const publish = (store) => { input.store.records = store.records; input.store.revision = store.revision; input.setWorkingStore(input.store); };
65
+ const publish = (store) => { input.store.records = store.records; input.store.revision = store.revision; input.setWorkingStore(input.store); if (hasCommittedTerminalEvent(store, input.attemptId))
66
+ input.planControl?.acceptTerminal(); };
65
67
  const validate = async (records) => {
66
68
  let revision = 0;
67
69
  let terminal = false;
@@ -89,81 +91,99 @@ export async function createDurableFrontendTools(input) {
89
91
  throw ledgerError("FRONTEND_LEDGER_MISSING", "frontend durable ledger disappeared or became empty; refusing an unproven acknowledgement");
90
92
  };
91
93
  await exclusive(file, restore);
92
- const progress = { failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 };
94
+ const progress = { finalizeRejections: 0, failedRecords: 0, duplicateRecords: 0, durableSubmissions: 0, durableReplacements: 0 };
93
95
  const replay = (receipt) => { progress.duplicateRecords++; return receipt; };
94
96
  const wrapped = tools.map(tool => ({ ...tool, execute: (...args) => exclusive(file, async () => {
95
- const [callId, params] = args;
96
- const valid = Check(tool.parameters, params);
97
- if (!valid) {
98
- const errors = [...Errors(tool.parameters, params)];
99
- return failure("TOOL_SCHEMA_INVALID", `${tool.name}: ${errors.slice(0, 8).map(e => `${e.instancePath || "/"}: ${e.message}`).join("; ")}${errors.length > 8 ? `; ${errors.length - 8} more fields` : ""}`);
100
- }
101
- try {
102
- await restore();
103
- const paramsSha256 = hash(params);
104
- const singleton = new Set(["record_route_selection", "record_state_registry", "record_mock_api", "record_data_flow", "record_dependency", "record_design_deviation", "record_required_deliverables", "record_target_surface", "finalize_contract", "finalize_plan", "finalize_decision"]);
105
- const naturalId = params?.id ?? params?.scopeId ?? params?.entry?.id ?? params?.requirementId ?? params?.choice?.purpose ?? params?.entry?.purpose ?? params?.entry?.state ?? params?.entry?.interaction ?? params?.entry?.boundary ?? (tool.name === "record_openspec_selection" ? params?.path : undefined) ?? (params?.endpoint ? `${params.endpoint.method}:${params.endpoint.path}` : undefined) ?? (singleton.has(tool.name) ? "singleton" : undefined);
106
- const identity = `${tool.name}:${typeof naturalId === "string" ? naturalId : "incremental"}`;
107
- const submissions = input.store.records.flatMap(r => r.submission?.receipt ? [r.submission] : []);
108
- const previousCall = submissions.find(s => s.callId === callId);
109
- if (previousCall)
110
- return previousCall.tool === tool.name && previousCall.paramsSha256 === paramsSha256 ? replay(previousCall.receipt) : failure("REQUEST_ID_REUSE_CONFLICT", `${tool.name}: call id already acknowledged with different parameters`);
111
- const previousFact = submissions.filter(s => s.identity === identity).at(-1);
112
- if (tool.name !== "record_plan_group_coverage" && previousFact?.paramsSha256 === paramsSha256)
113
- return replay(previousFact.receipt);
114
- // A committed finalize terminal carries no domain payload in its params
115
- // (r15: the model acknowledged with {replace:false}, the executor's
116
- // auto-finalize sends {}) — both are acknowledgements of the same durable
117
- // terminal, so any later finalize call replays the receipt instead of
118
- // demanding a params match no caller can reproduce. Other singletons
119
- // (state registry, data flows, ...) carry domain payloads and keep the
120
- // replace:true correction path.
121
- if (FINALIZE_TERMINAL_TOOLS.has(tool.name) && previousFact?.receipt)
122
- return replay(previousFact.receipt);
123
- if (previousFact && naturalId !== undefined && params?.replace !== true && !["record_evidence_expectation", "record_required_deliverables", "record_data_flow"].includes(tool.name))
124
- return failure("FACT_IDENTITY_CONFLICT", `${identity}: changing a saved fact requires explicit replacement (replace:true)`);
125
- if (input.store.records.some(r => ["contract-finalized", "finalize_plan", "finalize_decision", "approve_review", "request_review_changes", "approve_design", "request_design_changes"].includes(String(r.fact.kind))))
126
- return failure("TERMINAL_CONFLICT", "A successful terminal is already durable; only identical acknowledged submissions may be replayed");
127
- const draft = structuredClone(input.store);
128
- input.setWorkingStore(draft);
129
- let result;
97
+ const beforeRevision = input.store.revision;
98
+ const execute = async () => {
99
+ const stopped = input.planControl?.readStop();
100
+ if (stopped)
101
+ return failure(stopped.code, stopped.error);
102
+ const [callId, params] = args;
103
+ const valid = Check(tool.parameters, params);
104
+ if (!valid) {
105
+ const errors = [...Errors(tool.parameters, params)];
106
+ return failure("TOOL_SCHEMA_INVALID", `${tool.name}: ${errors.slice(0, 8).map(e => `${e.instancePath || "/"}: ${e.message}`).join("; ")}${errors.length > 8 ? `; ${errors.length - 8} more fields` : ""}`);
107
+ }
130
108
  try {
131
- // Protocol controls must not be spread into domain facts by permissive callers.
132
- const original = input.tools.find(t => t.name === tool.name);
133
- const domainParams = { ...params };
134
- if (!original.parameters.properties?.replace)
135
- delete domainParams.replace;
136
- result = await tool.execute(args[0], domainParams, ...args.slice(2));
109
+ await restore();
110
+ const isReviewFinding = ["record_review_finding", "record_design_finding"].includes(tool.name);
111
+ const paramsSha256 = hash(isReviewFinding ? { ...params, replace: undefined } : params);
112
+ const singleton = new Set(["record_route_selection", "record_state_registry", "record_mock_api", "record_data_flow", "record_dependency", "record_design_deviation", "record_required_deliverables", "record_target_surface", "finalize_contract", "finalize_plan", "finalize_decision"]);
113
+ const naturalId = params?.id ?? params?.scopeId ?? params?.entry?.id ?? params?.requirementId ?? params?.choice?.purpose ?? params?.entry?.purpose ?? params?.entry?.state ?? params?.entry?.interaction ?? params?.entry?.boundary ?? (tool.name === "record_openspec_selection" ? params?.path : undefined) ?? (params?.endpoint ? `${params.endpoint.method}:${params.endpoint.path}` : undefined) ?? (singleton.has(tool.name) ? "singleton" : undefined);
114
+ const identity = `${tool.name}:${typeof naturalId === "string" ? naturalId : "incremental"}`;
115
+ const submissions = input.store.records.flatMap(r => r.submission?.receipt ? [r.submission] : []);
116
+ const previousCall = submissions.find(s => s.callId === callId);
117
+ if (previousCall)
118
+ return previousCall.tool === tool.name && previousCall.paramsSha256 === paramsSha256 ? replay(previousCall.receipt) : failure("REQUEST_ID_REUSE_CONFLICT", `${tool.name}: call id already acknowledged with different parameters`);
119
+ const previousFact = submissions.filter(s => s.identity === identity).at(-1);
120
+ if (isReviewFinding && !previousFact && params?.replace === true)
121
+ return failure("FACT_REPLACEMENT_NOT_FOUND", `${identity}: replacement requires an existing finding; omit replace to create it`);
122
+ if (tool.name !== "record_plan_group_coverage" && previousFact?.paramsSha256 === paramsSha256)
123
+ return replay(previousFact.receipt);
124
+ // A committed finalize terminal carries no domain payload in its params
125
+ // (r15: the model acknowledged with {replace:false}, the executor's
126
+ // auto-finalize sends {}) — both are acknowledgements of the same durable
127
+ // terminal, so any later finalize call replays the receipt instead of
128
+ // demanding a params match no caller can reproduce. Other singletons
129
+ // (state registry, data flows, ...) carry domain payloads and keep the
130
+ // replace:true correction path.
131
+ if (FINALIZE_TERMINAL_TOOLS.has(tool.name) && previousFact?.receipt)
132
+ return replay(previousFact.receipt);
133
+ if (previousFact && naturalId !== undefined && params?.replace !== true && !["record_evidence_expectation", "record_required_deliverables", "record_data_flow"].includes(tool.name))
134
+ return failure("FACT_IDENTITY_CONFLICT", `${identity}: changing a saved fact requires explicit replacement (replace:true)`);
135
+ if (input.store.records.some(r => ["contract-finalized", "finalize_plan", "finalize_decision", "approve_review", "request_review_changes", "approve_design", "request_design_changes"].includes(String(r.fact.kind))))
136
+ return failure("TERMINAL_CONFLICT", "A successful terminal is already durable; only identical acknowledged submissions may be replayed");
137
+ const draft = structuredClone(input.store);
138
+ input.setWorkingStore(draft);
139
+ let result;
140
+ try {
141
+ // Protocol controls must not be spread into domain facts by permissive callers.
142
+ const original = input.tools.find(t => t.name === tool.name);
143
+ const domainParams = { ...params };
144
+ if (!original.parameters.properties?.replace)
145
+ delete domainParams.replace;
146
+ result = await tool.execute(args[0], domainParams, ...args.slice(2));
147
+ }
148
+ finally {
149
+ input.setWorkingStore(input.store);
150
+ }
151
+ if (result.details?.ok !== true)
152
+ return result;
153
+ const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
154
+ if (!newRecords.length)
155
+ return replay(result);
156
+ for (const record of draft.records) {
157
+ record.submission ??= { version: 1, bindingSha256, tool: "bootstrap", callId: record.requestId, identity: record.eventId, paramsSha256: record.payloadSha256 };
158
+ }
159
+ for (const record of newRecords)
160
+ record.submission = { version: 1, bindingSha256, tool: tool.name, callId, identity, paramsSha256 };
161
+ newRecords.at(-1).submission.receipt = JSON.parse(JSON.stringify(result));
162
+ const committed = draft.records.filter(r => r.phase === "committed");
163
+ await validate(committed);
164
+ await persist(file, committed);
165
+ publish(draft);
166
+ progress.durableSubmissions++;
167
+ if (previousFact)
168
+ progress.durableReplacements++;
169
+ return result;
137
170
  }
138
- finally {
171
+ catch (error) {
139
172
  input.setWorkingStore(input.store);
173
+ return failure(typeof error?.code === "string" ? error.code : "FRONTEND_COMMIT_FAILED", error instanceof Error ? error.message : String(error));
140
174
  }
141
- if (result.details?.ok !== true)
142
- return result;
143
- const newRecords = draft.records.filter(r => r.phase === "committed" && r.revision > input.store.revision);
144
- if (!newRecords.length)
145
- return replay(result);
146
- for (const record of draft.records) {
147
- record.submission ??= { version: 1, bindingSha256, tool: "bootstrap", callId: record.requestId, identity: record.eventId, paramsSha256: record.payloadSha256 };
148
- }
149
- for (const record of newRecords)
150
- record.submission = { version: 1, bindingSha256, tool: tool.name, callId, identity, paramsSha256 };
151
- newRecords.at(-1).submission.receipt = JSON.parse(JSON.stringify(result));
152
- const committed = draft.records.filter(r => r.phase === "committed");
153
- await validate(committed);
154
- await persist(file, committed);
155
- publish(draft);
156
- progress.durableSubmissions++;
157
- if (previousFact)
158
- progress.durableReplacements++;
159
- return result;
160
- }
161
- catch (error) {
162
- input.setWorkingStore(input.store);
163
- return failure(typeof error?.code === "string" ? error.code : "FRONTEND_COMMIT_FAILED", error instanceof Error ? error.message : String(error));
175
+ };
176
+ const result = await execute();
177
+ if (result.details?.ok !== true) {
178
+ progress.failedRecords++;
179
+ if (FINALIZE_TERMINAL_TOOLS.has(tool.name))
180
+ progress.finalizeRejections++;
164
181
  }
165
- }).then(result => { if (result.details?.ok !== true)
166
- progress.failedRecords++; return result; }) }));
182
+ const note = input.planControl?.observe(tool.name, args[1], result.details, input.store.revision > beforeRevision, input.planControl ? frontendPlanSemanticDigest(input.store.records) : undefined);
183
+ if (note)
184
+ return { ...result, content: [...(Array.isArray(result.content) ? result.content : []), { type: "text", text: JSON.stringify(note) }], details: { ...result.details, planProgress: note } };
185
+ return result;
186
+ }) }));
167
187
  for (const tool of wrapped)
168
188
  progressByTool.set(tool, progress);
169
189
  return {
@@ -1,3 +1,5 @@
1
+ import { readFileBoundarySafe } from "../../shared/path-safety.js";
2
+ import { capabilityBoundarySchema } from "../../task/config-types.js";
1
3
  import { frontendExecutionSchema, collectFrontendExecutionGroups } from "./frontend-execution-groups.js";
2
4
  import { createHash } from "node:crypto";
3
5
  import { readFileSync } from "node:fs";
@@ -195,6 +197,20 @@ export function isRecognizedFrontendVerifyCommandText(command) {
195
197
  /\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
196
198
  /\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command));
197
199
  }
200
+ /**
201
+ * Repo-level shell scripts whose name marks them as governance/static checks
202
+ * rather than test runners: `scripts/check-*.sh`, `scripts/ci.sh`,
203
+ * `scripts/ci-governance.sh` and friends. The behavior lane is test-observed
204
+ * at writer admission, so these must not be admitted there — they cannot emit
205
+ * a test report.
206
+ *
207
+ * A test word in the name still wins, because the test-semantics check runs
208
+ * earlier in `classifyFrontendVerifyCommandText`, so a real test script such as
209
+ * `scripts/run-tests.sh` or `scripts/check-tests.sh` is unaffected.
210
+ */
211
+ const VERIFY_STATIC_SCRIPT_PATH_RE = /\bscripts[\\/]+(?:ci(?:-governance|-tests)?|check-[a-z0-9-]+)\.sh\b/i;
212
+ /** A shell script whose own name marks it as a test runner. */
213
+ const VERIFY_BEHAVIOR_SCRIPT_PATH_RE = /\bscripts[\\/]+[a-z0-9-]*(?:tests?|specs?|e2e)[a-z0-9-]*\.sh\b/i;
198
214
  /**
199
215
  * Classify a raw verification command text into the runtime-owned mode.
200
216
  * Shared single implementation used by DAG generation (lane split) and
@@ -207,6 +223,14 @@ export function isRecognizedFrontendVerifyCommandText(command) {
207
223
  * static scripts (`npm run check`, `check:types`) and leading binaries
208
224
  * (`tsc`, `eslint`) stay static. Package-manager flags (`npm --prefix
209
225
  * website run build`) are skipped when locating the script.
226
+ *
227
+ * Returns `undefined` for a command whose text carries no recognizable
228
+ * verification semantics. The previous fallback returned `"behavior"`, which
229
+ * silently pushed unknown commands into the strictest lane: the behavior lane
230
+ * is test-observed at writer admission, so a command that cannot emit a test
231
+ * report blocked admission deterministically with no way for the model to
232
+ * clear it. Callers must now decide explicitly instead of inheriting a lane
233
+ * from an unrecognized string.
210
234
  */
211
235
  export function classifyFrontendVerifyCommandText(command) {
212
236
  if (VERIFY_RUNNER_RE.test(command) || VERIFY_E2E_RE.test(command)) {
@@ -220,16 +244,24 @@ export function classifyFrontendVerifyCommandText(command) {
220
244
  }
221
245
  return segments.some((segment) => VERIFY_STATIC_SCRIPT_WORDS.has(segment))
222
246
  ? "static"
223
- : "behavior";
247
+ : undefined;
224
248
  }
225
249
  const head = command.trim().split(/\s+/, 1)[0] ?? "";
226
250
  const binary = head.split(/[\\/]/).pop() ?? "";
251
+ // A test word in the invoked script's own name wins over the static
252
+ // governance pattern: `scripts/check-tests.sh` runs tests even though it
253
+ // starts with `check-`, so it must stay in the test-observed lane.
254
+ if (VERIFY_BEHAVIOR_SCRIPT_PATH_RE.test(command)) {
255
+ return "behavior";
256
+ }
227
257
  if (/^(tsc|eslint|stylelint|lint|typecheck|check|build)$/i.test(binary) ||
228
258
  /\b(tsc|eslint|typecheck|check-types)\b/i.test(command) ||
229
- /\bscripts[\\/]+ci(?:-tests)?\.sh\b/i.test(command)) {
259
+ VERIFY_STATIC_SCRIPT_PATH_RE.test(command)) {
230
260
  return "static";
231
261
  }
232
- return "behavior";
262
+ // Unrecognized: no lane is inferred. Callers must require an operator
263
+ // declaration rather than defaulting into the test-observed behavior lane.
264
+ return undefined;
233
265
  }
234
266
  export function slugFrontendVerifyCommandId(label, taken) {
235
267
  const slug = label
@@ -591,23 +623,22 @@ async function deriveFrontendCapabilityBoundaryFromRun(runDir) {
591
623
  try {
592
624
  raw = await readFile(specPath, "utf8");
593
625
  }
594
- catch {
595
- return undefined;
626
+ catch (error) {
627
+ if (error.code === "ENOENT")
628
+ return undefined;
629
+ throw error;
596
630
  }
597
631
  let spec;
598
632
  try {
599
633
  spec = JSON.parse(raw);
600
634
  }
601
635
  catch {
602
- return undefined;
636
+ throw new Error("capability-boundary-source-invalid: run.json is malformed");
603
637
  }
604
638
  for (const task of spec.tasks ?? []) {
605
639
  const boundary = task.shell?.frontendDesignPolicy?.capabilityBoundary;
606
- if (boundary &&
607
- ((boundary.allowed?.length ?? 0) > 0 ||
608
- (boundary.forbidden?.length ?? 0) > 0)) {
609
- return boundary;
610
- }
640
+ if (boundary !== undefined)
641
+ return capabilityBoundarySchema.parse(boundary);
611
642
  }
612
643
  return undefined;
613
644
  }
@@ -617,44 +648,67 @@ async function deriveFrontendCapabilityBoundaryFromRun(runDir) {
617
648
  export function composeDependencyPolicyWithBoundary(policy, boundary) {
618
649
  if (!boundary)
619
650
  return policy;
620
- const allowed = (boundary.allowed ?? []).filter((item) => item.trim());
621
- const forbidden = (boundary.forbidden ?? []).filter((item) => item.trim());
622
- if (allowed.length === 0 && forbidden.length === 0)
623
- return policy;
624
- if (policy?.includes(CAPABILITY_BOUNDARY_MARKER))
625
- return policy;
626
- const sentence = `${CAPABILITY_BOUNDARY_MARKER}:` +
627
- (allowed.length > 0 ? `仅可使用 ${allowed.join("、")}` : "无允许清单") +
628
- (forbidden.length > 0 ? `;禁止 ${forbidden.join("、")}` : "") +
629
- "。";
630
- return policy?.trim() ? `${policy.trim()};${sentence}` : sentence;
651
+ const frozen = capabilityBoundarySchema.parse(boundary);
652
+ // Replace only our explicit complete block; a prose mention never grants
653
+ // ownership of the author's following constraints.
654
+ const begin = "<!-- frontend-capability-boundary:start -->";
655
+ const end = "<!-- frontend-capability-boundary:end -->";
656
+ const source = policy ?? "";
657
+ const start = source.indexOf(begin);
658
+ const finish = start < 0 ? -1 : source.indexOf(end, start + begin.length);
659
+ const prefix = (finish < 0 ? source : source.slice(0, start) + source.slice(finish + end.length)).trim();
660
+ const parts = [
661
+ frozen.allowed?.length ? `仅可使用 ${frozen.allowed.join("、")}` : undefined,
662
+ frozen.forbidden?.length ? `禁止 ${frozen.forbidden.join("、")}` : undefined,
663
+ frozen.requiredClauses?.length ? `必须满足:${frozen.requiredClauses.join(";")}` : undefined,
664
+ ].filter(Boolean);
665
+ const sentence = `${CAPABILITY_BOUNDARY_MARKER}:${parts.join(";")}。`;
666
+ const block = `${begin}\n${sentence}\n${end}`;
667
+ return prefix ? `${prefix}\n${block}` : block;
631
668
  }
632
669
  /** Read the generation-frozen requirement→file ownership from run.json
633
- * (frontendDesignPolicy spec). Absent (legacy fixtures) → no ownership gate. */
634
- export async function deriveRequirementOwnershipFromRun(runDir) {
670
+ * (frontendDesignPolicy spec), bound to the canonical source identity. */
671
+ export async function deriveRequirementOwnershipFromRun(runDir, expectedBinding) {
635
672
  const specPath = path.join(runDir, "run.json");
636
673
  let raw;
637
674
  try {
638
675
  raw = await readFile(specPath, "utf8");
639
676
  }
640
- catch {
641
- return [];
677
+ catch (error) {
678
+ if (error.code === "ENOENT" && (!expectedBinding || !isDagSourceBindingV2(expectedBinding)))
679
+ return [];
680
+ if (error.code === "ENOENT")
681
+ throw new Error("requirement-ownership-source-invalid: frozen run.json is missing");
682
+ throw error;
642
683
  }
643
684
  let spec;
644
685
  try {
645
686
  spec = JSON.parse(raw);
646
687
  }
647
688
  catch {
648
- return [];
689
+ throw new Error("requirement-ownership-source-invalid: run.json is malformed");
649
690
  }
650
691
  const merged = new Map();
651
692
  for (const task of spec.tasks ?? []) {
652
- for (const entry of task.shell?.frontendDesignPolicy?.requirementOwnership ?? []) {
693
+ const raw = task.shell?.frontendDesignPolicy?.requirementOwnership;
694
+ if (raw === undefined)
695
+ continue;
696
+ const parsed = z.array(z.object({ requirementIds: z.array(z.string().min(1)).min(1), implementationFiles: z.array(z.string().min(1)).optional(), verificationFiles: z.array(z.string().min(1)).optional() }).strict()).safeParse(raw);
697
+ if (!parsed.success)
698
+ throw new Error("requirement-ownership-source-invalid: frozen requirementOwnership is malformed");
699
+ for (const entry of parsed.data) {
653
700
  const key = [...(entry.requirementIds ?? [])].sort().join("|");
654
- if (!merged.has(key))
701
+ const previous = merged.get(key);
702
+ if (previous && serializeDeterministicJson(previous) !== serializeDeterministicJson(entry))
703
+ throw new Error("requirement-ownership-source-invalid: conflicting ownership declarations");
704
+ if (!previous)
655
705
  merged.set(key, entry);
656
706
  }
657
707
  }
708
+ if (merged.size && expectedBinding) {
709
+ if (!spec.sourceBinding || serializeDeterministicJson(spec.sourceBinding) !== serializeDeterministicJson(expectedBinding))
710
+ throw new Error("requirement-ownership-source-invalid: frozen source binding mismatch or missing");
711
+ }
658
712
  return [...merged.values()];
659
713
  }
660
714
  /** Deterministic requirement→file ownership check. A requirement's
@@ -700,27 +754,15 @@ function checkRequirementOwnership(canonical, ownership) {
700
754
  continue;
701
755
  if (implementationFiles.length > 0) {
702
756
  const allTargets = requirement.implementationTargets ?? [];
703
- // Test-shaped placements belong to the verification lane (a shared
704
- // behavior group's placement carries the test file for the
705
- // test-writing requirement); they are governed by allowedPaths and
706
- // the entry's verificationFiles, not by implementationFiles.
707
- const implementationFilesAreTestShaped = implementationFiles.some((file) => isFrontendTestFilePath(file));
708
- const targets = implementationFilesAreTestShaped
709
- ? allTargets
710
- : allTargets.filter((file) => !isFrontendTestFilePath(file));
711
- // Contract deliverables are existence obligations, not ownership
712
- // claims: a contract may require a mounting file to exist for a
713
- // requirement implemented elsewhere (smoke r55 — a deliverable
714
- // otherwise made plan finalize permanently unfixable).
715
- const deliverables = new Set(requirement.deliverableTargets ?? []);
716
- const ownedTargets = targets.filter((file) => !deliverables.has(file));
717
- // Only files OUTSIDE the declared set violate. An empty owned set
718
- // means every mapped file is a deliverable/test-shaped obligation —
719
- // unplaced requirements are rejected upstream, not here (r56:
720
- // "maps [X] expects [X]" still violated when deliverables covered
721
- // the whole mapping).
722
- const outside = ownedTargets.filter((file) => !implementationFiles.includes(file));
723
- if (outside.length > 0) {
757
+ // A verification placement must have explicit verification
758
+ // ownership AND a bound verification target. File names alone
759
+ // never exempt implementation responsibility.
760
+ const verificationPlacements = new Set([...(targetsByRequirement.get(id) ?? [])].filter(file => verificationFiles.includes(file)));
761
+ const targets = allTargets.filter(file => !verificationPlacements.has(file) || implementationFiles.includes(file));
762
+ // implementationTargets retains actual placements. Existence-only
763
+ // deliverableTargets never subtract from this ownership claim.
764
+ const outside = targets.filter(file => !implementationFiles.includes(file));
765
+ if (outside.length > 0 || !targets.some(file => implementationFiles.includes(file))) {
724
766
  findings.push({
725
767
  code: "requirement-ownership-violated",
726
768
  message: `requirement ${id} expects implementation within [${implementationFiles.join(", ")}] but plan maps [${targets.join(", ") || "none"}] — re-map the owning behavior group's module placement`,
@@ -759,7 +801,7 @@ const FRONTEND_CONTRACT_REVIEW_GROUPS = [
759
801
  },
760
802
  {
761
803
  id: "data-design",
762
- fields: ["mockApi", "designEvidence", "dependencyPolicy", "realIntegrationGap"],
804
+ fields: ["mockApi", "designEvidence", "capabilityBoundary", "dependencyPolicy", "realIntegrationGap"],
763
805
  },
764
806
  {
765
807
  id: "verification",
@@ -1101,11 +1143,15 @@ export const frontendImplementationContractSchema = z
1101
1143
  uiStates: z.array(z.string().min(1)),
1102
1144
  // Display-only test scope; never read by control flow.
1103
1145
  scope: frontendVerificationScopeSchema.optional(),
1146
+ // Planned evidence strength, never proof of observed behavior.
1147
+ evidenceLevel: z.enum(["unit", "mounted", "real-integration"]).optional(),
1148
+ boundary: z.string().min(1).optional(),
1104
1149
  })
1105
1150
  .strict()).min(1),
1106
1151
  evidenceGaps: z.array(gap).optional(),
1107
1152
  stylingStrategy: z.string().min(1).optional(),
1108
1153
  uiComponentChoices: uiComponentChoicesSchema.optional(),
1154
+ capabilityBoundary: capabilityBoundarySchema.optional(),
1109
1155
  dependencyPolicy: z.string().min(1).optional(),
1110
1156
  residualRisks: z.array(z.string().min(1)).optional(),
1111
1157
  realIntegrationGap: z.string().min(1).optional(),
@@ -2032,7 +2078,7 @@ function assertFrontendContractPathsSafe(value) {
2032
2078
  const record = asRecord(value);
2033
2079
  if (!record)
2034
2080
  return;
2035
- const pathFields = ["files", "file", "implementationTargets", "fixture", "consumer"];
2081
+ const pathFields = ["files", "file", "implementationTargets", "deliverableTargets", "fixture", "consumer"];
2036
2082
  for (const [key, child] of Object.entries(record)) {
2037
2083
  if (pathFields.includes(key) && Array.isArray(child)) {
2038
2084
  for (const item of child) {
@@ -2326,6 +2372,8 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
2326
2372
  file,
2327
2373
  symbol: asString(vt.symbol) || undefined,
2328
2374
  ...(scope ? { scope } : {}),
2375
+ ...(vt.evidenceLevel !== undefined ? { evidenceLevel: vt.evidenceLevel } : {}),
2376
+ ...(vt.boundary !== undefined ? { boundary: vt.boundary } : {}),
2329
2377
  requirementIds: requirementIds.length > 0
2330
2378
  ? requirementIds
2331
2379
  : [...canonicalBinding.requirementIds],
@@ -2755,7 +2803,10 @@ export async function analyzeFrontendImplementationContract(input) {
2755
2803
  // field (r10/r11 design-review findings).
2756
2804
  const referencedConcretePaths = [
2757
2805
  ...new Set([
2758
- ...result.data.requirements.flatMap((requirement) => requirement.implementationTargets),
2806
+ ...result.data.requirements.flatMap(requirement => [
2807
+ ...requirement.implementationTargets,
2808
+ ...(requirement.deliverableTargets ?? []),
2809
+ ]),
2759
2810
  // Mock endpoint paths stay in the set: the zod refine validated
2760
2811
  // them against the declared targets.files patterns, so the
2761
2812
  // narrowing must keep covering them. Verification-target files
@@ -2765,6 +2816,9 @@ export async function analyzeFrontendImplementationContract(input) {
2765
2816
  ...(result.data.mockApi?.endpoints ?? []).flatMap((endpoint) => [endpoint.fixture, endpoint.consumer].filter((value) => typeof value === "string" && value.length > 0)),
2766
2817
  ].filter((value) => value.length > 0)),
2767
2818
  ].sort();
2819
+ const outsideDelivery = result.data.requirements.flatMap(requirement => requirement.deliverableTargets ?? []).filter(file => !parsedTargetFiles.some(pattern => pathMatchesPattern(file, pattern)));
2820
+ if (outsideDelivery.length)
2821
+ fail("blocked", `target-path-outside-write-set: deliverable paths [${outsideDelivery.join(", ")}] exceed frozen targets`, candidateJsonSha256);
2768
2822
  if (referencedConcretePaths.length > 0) {
2769
2823
  result.data.targets = {
2770
2824
  ...result.data.targets,
@@ -2793,7 +2847,26 @@ export async function analyzeFrontendImplementationContract(input) {
2793
2847
  if (!result.data.requirements.some((item) => item.id === requirementId) &&
2794
2848
  !(result.data.evidenceGaps ?? []).some((item) => item.requirementId === requirementId))
2795
2849
  fail("blocked", `frontend contract does not cover ${requirementId}`, candidateJsonSha256);
2796
- result.data.dependencyPolicy = composeDependencyPolicyWithBoundary(result.data.dependencyPolicy, await deriveFrontendCapabilityBoundaryFromRun(input.runDir));
2850
+ // Both finalize prechecks and the final materializer enter this function.
2851
+ // Check the submitted placement before normalization can invent a fallback.
2852
+ const ownership = await deriveRequirementOwnershipFromRun(input.runDir, input.sourceBinding);
2853
+ for (const entry of ownership) {
2854
+ if (!entry.implementationFiles?.length)
2855
+ continue;
2856
+ for (const id of entry.requirementIds) {
2857
+ const original = asRecord(parsed)?.requirements?.find(item => asRecord(item)?.id === id);
2858
+ if (!original || asStringArray(asRecord(original)?.implementationTargets).length === 0)
2859
+ fail("retryable-invalid", `requirement-ownership-placement-missing: ${id} requires an explicit placement from its Plan facts`, candidateJsonSha256);
2860
+ }
2861
+ }
2862
+ const ownershipFindings = checkRequirementOwnership(result.data, ownership);
2863
+ if (ownershipFindings.length)
2864
+ throw new PlanPolicyPrecheckFailure(ownershipFindings.map(f => `${f.code}: ${f.message}`).join("; "), ownershipFindings);
2865
+ const frozenBoundary = await deriveFrontendCapabilityBoundaryFromRun(input.runDir);
2866
+ delete result.data.capabilityBoundary;
2867
+ if (frozenBoundary)
2868
+ result.data.capabilityBoundary = frozenBoundary;
2869
+ result.data.dependencyPolicy = composeDependencyPolicyWithBoundary(result.data.dependencyPolicy, frozenBoundary);
2797
2870
  return {
2798
2871
  canonical: result.data,
2799
2872
  candidateRawSha256,
@@ -2990,6 +3063,7 @@ export async function writeFrontendImplementationContractArtifact(input) {
2990
3063
  };
2991
3064
  }
2992
3065
  const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
3066
+ "/capabilityBoundary",
2993
3067
  "/schemaVersion",
2994
3068
  "/sourceBinding",
2995
3069
  "/riskLevel",
@@ -3036,7 +3110,7 @@ function frontendPlanPatchProtectedPathViolations(patch) {
3036
3110
  if (!record)
3037
3111
  return ["/"];
3038
3112
  const violations = [];
3039
- for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
3113
+ for (const key of ["schemaVersion", "sourceBinding", "riskLevel", "capabilityBoundary"]) {
3040
3114
  if (Object.hasOwn(record, key))
3041
3115
  violations.push(`/${key}`);
3042
3116
  }
@@ -3050,6 +3124,22 @@ function frontendPlanPatchProtectedPathViolations(patch) {
3050
3124
  violations.push("/mockApi/productionDefaultOff");
3051
3125
  return violations;
3052
3126
  }
3127
+ /** Read-only verification references do not grant writer permissions. */
3128
+ export async function collectExistingFrontendVerificationPaths(workspaceRoot, contract) {
3129
+ if (!workspaceRoot)
3130
+ return [];
3131
+ const existing = [];
3132
+ for (const target of contract.verificationTargets) {
3133
+ if (target.mode === "static" || existing.includes(target.file))
3134
+ continue;
3135
+ try {
3136
+ await readFileBoundarySafe({ boundaryRoot: workspaceRoot, targetPath: path.resolve(workspaceRoot, target.file), label: "frontend existing verification" });
3137
+ existing.push(target.file);
3138
+ }
3139
+ catch { /* Missing or unsafe files cannot bypass the write-set check. */ }
3140
+ }
3141
+ return existing;
3142
+ }
3053
3143
  function workspaceRootFromDagRunDir(runDir) {
3054
3144
  const absolute = path.resolve(runDir);
3055
3145
  const segments = absolute.split(path.sep);
@@ -3253,9 +3343,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
3253
3343
  ...checkRequiredDeliverables(analysis.canonical),
3254
3344
  ...checkUxNameCollisions(analysis.canonical),
3255
3345
  ...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
3256
- ...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
3346
+ ...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir), await collectExistingFrontendVerificationPaths(workspaceRootFromDagRunDir(input.runDir), analysis.canonical)),
3257
3347
  ...checkComponentChoiceSourceReferences(analysis.canonical, await deriveFrontendSourceBindingPathsFromRun(input.runDir)),
3258
- ...checkRequirementOwnership(analysis.canonical, await deriveRequirementOwnershipFromRun(input.runDir)),
3259
3348
  ...checkInteractionIdVocabulary(analysis.canonical, await deriveFrontendInteractionIdsFromRun(input.runDir)),
3260
3349
  ...checkMockApiStrategyAllowed(analysis.canonical, await deriveFrontendAllowedMockStrategiesFromRun(input.runDir)),
3261
3350
  ];