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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/AGENTS.md +4 -1
  2. package/CHANGELOG.md +187 -92
  3. package/README.md +9 -5
  4. package/bin/loop-agent.js +7 -3
  5. package/dist/application/task-lifecycle/recommendations.js +13 -2
  6. package/dist/build-stamp.json +3 -3
  7. package/dist/cli/command-definitions.js +16 -5
  8. package/dist/cli/program.js +16 -2
  9. package/dist/cli/update/notifier.js +2 -2
  10. package/dist/cli.js +2 -1
  11. package/dist/commands/dag-rerun.js +55 -1
  12. package/dist/commands/init-model-catalog.js +464 -0
  13. package/dist/commands/init-upgrade.js +265 -97
  14. package/dist/commands/init.js +429 -28
  15. package/dist/commands/inspect-next.js +8 -0
  16. package/dist/executors/dag-pi-executor.js +1645 -29
  17. package/dist/executors/pi-extension-resolver.js +233 -0
  18. package/dist/executors/pi-playwright-cli-tool.js +51 -12
  19. package/dist/executors/pi-sdk-executor.js +180 -33
  20. package/dist/executors/shell-executor.js +1150 -171
  21. package/dist/executors/shell-write-guard.js +14 -0
  22. package/dist/shared/dag-prompt-override.js +28 -0
  23. package/dist/shared/openspec-spec.js +70 -4
  24. package/dist/shared/operator/capabilities.js +32 -0
  25. package/dist/shared/playwright-cli-command-policy.js +15 -0
  26. package/dist/shared/update/console-notifier.js +100 -0
  27. package/dist/{cli → shared}/update/npm-client.js +41 -8
  28. package/dist/{cli → shared}/update/state.js +41 -13
  29. package/dist/task/config-types.js +19 -5
  30. package/dist/task/contract/project.js +3 -0
  31. package/dist/task/contract/schema.js +2 -1
  32. package/dist/task/frontend-project-capability.js +42 -16
  33. package/dist/task/source-prepare/parse-intent.js +24 -4
  34. package/dist/task/source-prepare/prepare.js +36 -0
  35. package/dist/worker/cli.js +31 -1
  36. package/dist/worker/console/chat/pi-console-config.js +81 -10
  37. package/dist/worker/console/chat/routes.js +1 -0
  38. package/dist/worker/console/dag-execution-receipt.js +33 -0
  39. package/dist/worker/console/frontend-human-decision-adapter.js +19 -0
  40. package/dist/worker/console/frontend-split-operation-adapter.js +20 -0
  41. package/dist/worker/console/index.js +3 -0
  42. package/dist/worker/console/operation-store.js +226 -6
  43. package/dist/worker/console/operator-actions.js +263 -0
  44. package/dist/worker/console/operator-user-error.js +2 -2
  45. package/dist/worker/console/routes.js +43 -2
  46. package/dist/worker/console/security.js +63 -9
  47. package/dist/worker/console/server.js +151 -6
  48. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4kVYweA.js → abnfDiagram-N423BO3Z-zsNyvGaH.js} +1 -1
  49. package/dist/worker/console/static/assets/{arc-Bj2M8iO5.js → arc-BDjZ5kE1.js} +1 -1
  50. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CLlXe4-6.js → architectureDiagram-T3A2C74G-BkgaujnN.js} +1 -1
  51. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-psEp5xH0.js → blockDiagram-VBNYF7ZC-CqxOJi0j.js} +1 -1
  52. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-DeKcOCGJ.js → c4Diagram-5PPSVZJV-C2JHLdRi.js} +1 -1
  53. package/dist/worker/console/static/assets/channel-BuqaGKAT.js +1 -0
  54. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-K0C744p1.js → chunk-2GRJ4B5K-DF_YSvOS.js} +1 -1
  55. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-xbNw4J9-.js → chunk-2Q5K7J3B-D3Ort5Vo.js} +1 -1
  56. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DjLaSDUO.js → chunk-5RXB4S5H-XfAfZeUC.js} +1 -1
  57. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CtiPlKel.js → chunk-5VM5RSS4-SLBibhf5.js} +1 -1
  58. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-DZtolir7.js → chunk-6Q2QTUOP-Dz29Ye_f.js} +1 -1
  59. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CPcdNeew.js → chunk-GF5L2VYU-DB2G4H8V.js} +1 -1
  60. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-NgHc6V37.js → chunk-JWPE2WC7-CIoGkxdu.js} +1 -1
  61. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DQ_T-hg8.js → chunk-KBJHAD2P-CG78FGkn.js} +1 -1
  62. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-DgLsxcVP.js → chunk-RYQCIY6F-ZDHAvXPd.js} +1 -1
  63. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-UrVoM9z1.js → chunk-XXDRQBXY-BKkkC4VA.js} +1 -1
  64. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DNdSHjH9.js +1 -0
  65. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DNdSHjH9.js +1 -0
  66. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-CLkYvTb3.js → cose-bilkent-JH36ORCC-SposUZsO.js} +1 -1
  67. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BwT_xKrE.js → cynefin-VYW2F7L2-BSCtQpD7.js} +1 -1
  68. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bi9tuzif.js → cynefinDiagram-MW4NZA55-DgFHrw9l.js} +1 -1
  69. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BCIqkWBV.js → dagre-VZM6K2ZE-BVBasYbj.js} +1 -1
  70. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-Dip6l9_Z.js → diagram-7IWD3JNH-6r_4rN_c.js} +1 -1
  71. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-B-xkh_wK.js → diagram-B4RE2ZJO-JmuaB41P.js} +1 -1
  72. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-DZO0kTt_.js → diagram-LBJQPF4R-xXvAY3sk.js} +1 -1
  73. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-D6ZplNbZ.js → diagram-Q27KOJAE-D2B3lY2o.js} +1 -1
  74. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BPekbhoS.js → diagram-UB23O5K3-tK54tMEX.js} +1 -1
  75. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BtmaJpK5.js → ebnfDiagram-BXEA7PRR-CNtELzV-.js} +1 -1
  76. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-6DS0Xc44.js → erDiagram-JOGREHBK-C1xKMKcW.js} +1 -1
  77. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CihnROSm.js → flowDiagram-UKHOOZJN-CZ7dlPMz.js} +1 -1
  78. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-CZC4zOE2.js → ganttDiagram-PKOTCBZU-BqGb42Xe.js} +1 -1
  79. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js → gitGraphDiagram-DS77QQ5N-D_BlXEtt.js} +1 -1
  80. package/dist/worker/console/static/assets/{index-DTZOKgAn.js → index-D5zGX6fG.js} +68 -68
  81. package/dist/worker/console/static/assets/index-DBbhESQ_.css +1 -0
  82. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DlSl4BGx.js → infoDiagram-6WML65LV-Vce2vmIe.js} +1 -1
  83. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-snnzFl_U.js → ishikawaDiagram-WSZJBQD7-DDwVBAvI.js} +1 -1
  84. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-BDE7qpMx.js → journeyDiagram-NVQOT4AX-y_Ew-wv4.js} +1 -1
  85. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-CD2Ci04G.js → kanban-definition-27J2QSJJ-CjXJFp1K.js} +1 -1
  86. package/dist/worker/console/static/assets/{linear-DdnapKIH.js → linear-1itMst1Q.js} +1 -1
  87. package/dist/worker/console/static/assets/{mermaid.core-BVjAT9b8.js → mermaid.core-C-D3ZRUa.js} +5 -5
  88. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D0yJk-zA.js → mindmap-definition-FAOFIHXS-BiYywmZc.js} +1 -1
  89. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-BVo9tFP-.js → pegDiagram-VL7TDLO6-l2fydNWA.js} +1 -1
  90. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DnOV-mZ_.js → pieDiagram-7S7Q4E2Y-Dz0hNbYW.js} +1 -1
  91. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CzUJo58i.js → quadrantDiagram-CIZ2JOQS-BujI_mLZ.js} +1 -1
  92. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-Bvikrolh.js → railroadDiagram-AXF67PYL-pWe84beb.js} +1 -1
  93. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DtaSVCap.js → requirementDiagram-LRYGKXZP-ByYu2frg.js} +1 -1
  94. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-C2c9A0wW.js → sankeyDiagram-W5VNT64P-CoPnilFP.js} +1 -1
  95. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-DmXJcU7r.js → sequenceDiagram-SI44F4Z6-1Md2LnS0.js} +1 -1
  96. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-B4GUFW92.js → sizeCapture-X5ZJPWSS-DqqqAoSH.js} +1 -1
  97. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-Dsad2MXf.js → stateDiagram-OKZ733FA-C4sGkf--.js} +1 -1
  98. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8rmop_9.js +1 -0
  99. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DGq48Fbi.js → swimlanes-SLNWSIFB-ByxJrUzW.js} +2 -2
  100. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B0ky-kBM.js +8 -0
  101. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CSSi6OFf.js → timeline-definition-Z64GVDOM-D8MLgM0O.js} +1 -1
  102. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-BqyzPrWv.js → vennDiagram-T6HMQDX7-DnBis-wZ.js} +1 -1
  103. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DlznVb6S.js → wardleyDiagram-T6FBY63Y-uuyYT7UO.js} +1 -1
  104. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-CfBcgJ_K.js → xychartDiagram-ELKLHX3M-BjgabhwK.js} +1 -1
  105. package/dist/worker/console/static/index.html +2 -2
  106. package/dist/worker/console/static-src/active-run-badge.js +17 -0
  107. package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
  108. package/dist/worker/loop-agent/loop-agent-client.js +7 -3
  109. package/dist/worker/materialize/frontend-split-task-materializer.js +72 -0
  110. package/dist/worker/observe/node-input.js +74 -3
  111. package/dist/worker/observe/node-process.js +377 -0
  112. package/dist/worker/observe/routes.js +36 -3
  113. package/dist/worker/observe/shell-handler-keys.js +34 -0
  114. package/dist/worker/observe/static/api.js +66 -0
  115. package/dist/worker/observe/static/constants.js +8 -1
  116. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  117. package/dist/worker/observe/static/format.js +23 -0
  118. package/dist/worker/observe/static/markdown-render.js +20 -1
  119. package/dist/worker/observe/static/operator-chrome.js +4 -0
  120. package/dist/worker/observe/static/state.js +13 -2
  121. package/dist/worker/observe/static/styles.css +73 -0
  122. package/dist/worker/observe/static/views/dag-inspector.js +612 -8
  123. package/dist/worker/observe/static/views/dag.js +8 -1
  124. package/dist/worker/observe/static/views/failures.js +5 -2
  125. package/dist/worker/observe/static/views/session-timeline.js +78 -9
  126. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +20 -2
  127. package/dist/workflows/dag/contract-output-registry.js +15 -0
  128. package/dist/workflows/dag/contract-validator-registrations.js +1 -2
  129. package/dist/workflows/dag/failure-category.js +12 -0
  130. package/dist/workflows/dag/frontend-closeout.js +221 -0
  131. package/dist/workflows/dag/frontend-design-policy.js +347 -0
  132. package/dist/workflows/dag/frontend-human-decision.js +182 -0
  133. package/dist/workflows/dag/frontend-implementation-contract.js +378 -160
  134. package/dist/workflows/dag/frontend-prewrite-gate.js +258 -393
  135. package/dist/workflows/dag/frontend-provider-capability-matrix.js +159 -0
  136. package/dist/workflows/dag/frontend-recovery-capsule.js +455 -0
  137. package/dist/workflows/dag/frontend-recovery-controller.js +201 -0
  138. package/dist/workflows/dag/frontend-recovery-lineage.js +178 -0
  139. package/dist/workflows/dag/frontend-recovery-plan.js +17 -10
  140. package/dist/workflows/dag/frontend-recovery-run.js +33 -20
  141. package/dist/workflows/dag/frontend-repair.js +7 -424
  142. package/dist/workflows/dag/frontend-review-context.js +195 -8
  143. package/dist/workflows/dag/frontend-risk.js +15 -2
  144. package/dist/workflows/dag/frontend-shadow-dual-write.js +665 -0
  145. package/dist/workflows/dag/frontend-shape-capsule-store.js +191 -0
  146. package/dist/workflows/dag/frontend-shape-facts.js +409 -0
  147. package/dist/workflows/dag/frontend-shape.js +425 -0
  148. package/dist/workflows/dag/frontend-split-application-service.js +203 -0
  149. package/dist/workflows/dag/frontend-split-orchestrator.js +899 -0
  150. package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
  151. package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
  152. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  153. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  154. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  155. package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
  156. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  157. package/dist/workflows/dag/frontend-typed-event-store.js +438 -0
  158. package/dist/workflows/dag/frontend-typed-event-transaction.js +180 -0
  159. package/dist/workflows/dag/frontend-verification-trace.js +52 -17
  160. package/dist/workflows/dag/frontend-worktree-diff.js +97 -18
  161. package/dist/workflows/dag/frontend-writer-admission.js +260 -0
  162. package/dist/workflows/dag/frontend-writer-status.js +244 -0
  163. package/dist/workflows/dag/init-hybrid.js +572 -500
  164. package/dist/workflows/dag/node-execution.js +307 -70
  165. package/dist/workflows/dag/prompt.js +10 -18
  166. package/dist/workflows/dag/rerun-plan.js +144 -5
  167. package/dist/workflows/dag/rerun-run.js +58 -6
  168. package/dist/workflows/dag/retry-policy.js +228 -4
  169. package/dist/workflows/dag/runner.js +314 -119
  170. package/dist/workflows/dag/scheduler.js +114 -16
  171. package/dist/workflows/dag/structured-output-repair.js +712 -0
  172. package/dist/workflows/dag/types.js +235 -23
  173. package/dist/workflows/dag/upstream-artifacts.js +0 -4
  174. package/dist/workflows/dag/validate.js +60 -10
  175. package/docs/init-surface.manifest.json +5 -0
  176. package/docs/operations/README.md +1 -1
  177. package/docs/templates/agent-dag.schema.json +44 -0
  178. package/docs/templates/frontend-test-dag.json +8 -10
  179. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  180. package/docs/templates/init-managed-agents.md +13 -3
  181. package/harness.json +3 -3
  182. package/package.json +5 -1
  183. package/scripts/next-info.mjs +356 -0
  184. package/scripts/next-publish-gate.mjs +238 -0
  185. package/scripts/release-source-binding.mjs +251 -0
  186. package/skills/codebase-scout/SKILL.md +1 -1
  187. package/skills/fe-test-ui-scout/SKILL.md +65 -0
  188. package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
  189. package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
  190. package/skills/frontend-implementation/SKILL.md +4 -4
  191. package/skills/frontend-implementation/references/design-spec.md +11 -7
  192. package/skills/frontend-implementation/references/node-contracts.md +20 -11
  193. package/skills/loop-agent/references/command-reference.md +11 -4
  194. package/skills/loop-agent/references/hybrid-dag.md +1 -1
  195. package/skills/playwright-cli/SKILL.md +22 -0
  196. package/skills/playwright-cli-case-generator/SKILL.md +20 -1
  197. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +0 -1
  198. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +0 -1
  199. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +0 -1
  200. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +0 -1
  201. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +0 -1
  202. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +0 -8
@@ -0,0 +1,347 @@
1
+ import { deterministicSha256, } from "./frontend-implementation-contract.js";
2
+ import { pathMatchesPattern } from "../../shared/git-progress.js";
3
+ const SHA256_HEX = /^[a-f0-9]{64}$/;
4
+ const NPM_PACKAGE_NAME = /^(?:@[a-zA-Z0-9_.-]+\/)?[a-zA-Z0-9][a-zA-Z0-9_.-]*$/;
5
+ function normalizeRelativePath(value) {
6
+ return value.replace(/\\/g, "/").replace(/^\.\//, "");
7
+ }
8
+ function sortFindings(findings) {
9
+ return [...findings].sort((a, b) => {
10
+ if (a.code !== b.code)
11
+ return a.code < b.code ? -1 : 1;
12
+ if (a.message !== b.message)
13
+ return a.message < b.message ? -1 : 1;
14
+ const pathA = a.path ?? "";
15
+ const pathB = b.path ?? "";
16
+ if (pathA !== pathB)
17
+ return pathA < pathB ? -1 : 1;
18
+ return 0;
19
+ });
20
+ }
21
+ /** 1. UI-visible requirements must map to a design decision. Every applicable
22
+ * UI state and every interaction must be covered by an exact `purpose` match in
23
+ * `uiComponentChoices`; a present `stylingStrategy` additionally covers the
24
+ * visual UI-state dimension (it cannot cover behavioural interactions). */
25
+ export function checkUiDesignCoverage(contract) {
26
+ const findings = [];
27
+ const purposes = new Set((contract.uiComponentChoices ?? []).map((choice) => choice.purpose));
28
+ const hasStyling = Boolean(contract.stylingStrategy?.trim());
29
+ for (const state of contract.uiStates) {
30
+ if (!state.applicable)
31
+ continue;
32
+ const covered = purposes.has(state.name) || hasStyling;
33
+ if (!covered) {
34
+ findings.push({
35
+ code: "ui-design-coverage-missing",
36
+ message: `applicable UI state "${state.name}" has no matching uiComponentChoices.purpose or stylingStrategy`,
37
+ path: state.name,
38
+ });
39
+ }
40
+ }
41
+ for (const interaction of contract.interactions) {
42
+ if (!purposes.has(interaction.name)) {
43
+ findings.push({
44
+ code: "ui-design-coverage-missing",
45
+ message: `interaction "${interaction.name}" has no matching uiComponentChoices.purpose`,
46
+ path: interaction.name,
47
+ });
48
+ }
49
+ }
50
+ return findings;
51
+ }
52
+ /** 2. Source freshness: every injected entry must be fresh with a valid hash;
53
+ * every design-evidence path must appear in the freshness set with a single,
54
+ * self-consistent sha256; any declared conflict is blocking. */
55
+ export function checkSourceFreshness(contract, sourceFreshness) {
56
+ const findings = [];
57
+ const hashesByPath = new Map();
58
+ for (const entry of sourceFreshness) {
59
+ if (!entry.fresh) {
60
+ findings.push({
61
+ code: "source-stale",
62
+ message: `source is stale: ${entry.path}`,
63
+ path: entry.path,
64
+ });
65
+ }
66
+ if (!SHA256_HEX.test(entry.sha256)) {
67
+ findings.push({
68
+ code: "source-hash-invalid",
69
+ message: `source sha256 is not a valid 64-hex digest: ${entry.path}`,
70
+ path: entry.path,
71
+ });
72
+ }
73
+ const hashes = hashesByPath.get(entry.path) ?? [];
74
+ hashes.push(entry.sha256);
75
+ hashesByPath.set(entry.path, hashes);
76
+ }
77
+ for (const [path, hashes] of hashesByPath) {
78
+ if (new Set(hashes).size > 1) {
79
+ findings.push({
80
+ code: "source-hash-conflict",
81
+ message: `source freshness entries disagree on sha256: ${path}`,
82
+ path,
83
+ });
84
+ }
85
+ }
86
+ for (const evidencePath of contract.designEvidence.paths) {
87
+ if (!hashesByPath.has(evidencePath)) {
88
+ findings.push({
89
+ code: "design-evidence-path-missing-freshness",
90
+ message: `design evidence path is absent from the freshness set: ${evidencePath}`,
91
+ path: evidencePath,
92
+ });
93
+ }
94
+ }
95
+ // `designEvidence.conflicts` is the plan's honest disclosure of design
96
+ // deviations (e.g. task text demands a test form the frozen constraints
97
+ // make infeasible). Whether a disclosed conflict is acceptable is decided
98
+ // by the design reviewer via `approve_design` / `request_design_changes`,
99
+ // which `checkDesignApproval` enforces separately. Treating any conflict
100
+ // as blocking here would strand every honest plan that reports one — the
101
+ // model cannot "fix" a conflict it correctly identified. Conflicts must be
102
+ // disclosed (schema-required array), never silently dropped, but disclosure
103
+ // itself is not a policy violation.
104
+ return findings;
105
+ }
106
+ /** 3. Mock/API strategy must be allowed; non-`not-needed` strategies require
107
+ * at least one endpoint and every endpoint must carry fixture + consumer. */
108
+ export function checkMockStrategy(contract, allowedMockStrategies) {
109
+ const findings = [];
110
+ if (!allowedMockStrategies.includes(contract.mockApi.strategy)) {
111
+ findings.push({
112
+ code: "mock-strategy-not-allowed",
113
+ message: `mockApi.strategy "${contract.mockApi.strategy}" is not in allowedMockStrategies`,
114
+ });
115
+ }
116
+ if (contract.mockApi.strategy !== "not-needed") {
117
+ if (contract.mockApi.endpoints.length === 0) {
118
+ findings.push({
119
+ code: "mock-endpoints-empty",
120
+ message: `mockApi.strategy "${contract.mockApi.strategy}" requires at least one endpoint`,
121
+ });
122
+ }
123
+ contract.mockApi.endpoints.forEach((endpoint, index) => {
124
+ if (!endpoint.fixture) {
125
+ findings.push({
126
+ code: "mock-endpoint-missing-fixture",
127
+ message: `mock endpoint ${index} is missing a fixture path`,
128
+ path: endpoint.path,
129
+ });
130
+ }
131
+ if (!endpoint.consumer) {
132
+ findings.push({
133
+ code: "mock-endpoint-missing-consumer",
134
+ message: `mock endpoint ${index} is missing a consumer path`,
135
+ path: endpoint.path,
136
+ });
137
+ }
138
+ });
139
+ }
140
+ return findings;
141
+ }
142
+ /** 4. Component choice must be precisely spec-referenced: `specified` requires
143
+ * a specReference whose normalized path is a candidate and a non-empty section;
144
+ * `new` must not carry a specReference; `reuse-existing` may omit it. */
145
+ export function checkComponentChoice(contract, componentSpecCandidatePaths) {
146
+ const findings = [];
147
+ const candidates = new Set(componentSpecCandidatePaths.map(normalizeRelativePath));
148
+ (contract.uiComponentChoices ?? []).forEach((choice, index) => {
149
+ if (choice.decision === "specified") {
150
+ if (!choice.specReference) {
151
+ findings.push({
152
+ code: "component-spec-reference-missing",
153
+ message: `specified component choice #${index} is missing specReference`,
154
+ path: choice.purpose,
155
+ });
156
+ return;
157
+ }
158
+ if (!choice.specReference.section.trim()) {
159
+ findings.push({
160
+ code: "component-spec-reference-missing",
161
+ message: `specified component choice #${index} has an empty section`,
162
+ path: choice.purpose,
163
+ });
164
+ }
165
+ const normalized = normalizeRelativePath(choice.specReference.path);
166
+ if (!candidates.has(normalized)) {
167
+ findings.push({
168
+ code: "component-spec-reference-path-outside-candidates",
169
+ message: `specified component choice #${index} specReference.path "${choice.specReference.path}" is not a candidate spec path`,
170
+ path: choice.specReference.path,
171
+ });
172
+ }
173
+ }
174
+ else if (choice.decision === "new" && choice.specReference) {
175
+ findings.push({
176
+ code: "component-new-with-spec-reference",
177
+ message: `new component choice #${index} must not carry a specReference`,
178
+ path: choice.purpose,
179
+ });
180
+ }
181
+ });
182
+ return findings;
183
+ }
184
+ /** 5. Every implementation target and every non-static verification target
185
+ * must fall inside the declared writeSet patterns. */
186
+ export function checkTargetPaths(contract, writeSetPatterns) {
187
+ const findings = [];
188
+ const matches = (file) => writeSetPatterns.some((pattern) => pathMatchesPattern(file, pattern));
189
+ for (const file of contract.targets.files) {
190
+ if (!matches(file)) {
191
+ findings.push({
192
+ code: "target-path-outside-write-set",
193
+ message: `implementation target is outside the writeSet patterns: ${file}`,
194
+ path: file,
195
+ });
196
+ }
197
+ }
198
+ for (const target of contract.verificationTargets) {
199
+ if (target.type === "static")
200
+ continue;
201
+ if (!matches(target.file)) {
202
+ findings.push({
203
+ code: "target-path-outside-write-set",
204
+ message: `verification target "${target.id}" file is outside the writeSet patterns: ${target.file}`,
205
+ path: target.file,
206
+ });
207
+ }
208
+ }
209
+ return findings;
210
+ }
211
+ /** 6. Any declared dependency identifier must be authorized. A declarative
212
+ * "no new dependencies" policy declares nothing; package-name-like tokens are
213
+ * matched exactly against `allowedDependencies`.
214
+ *
215
+ * The dependency policy is a natural-language statement, not a machine list.
216
+ * Three rules keep this check deterministic and aligned with the plan facts:
217
+ *
218
+ * 1. An empty allowedDependencies set skips the check entirely — with no
219
+ * allowlist there is no ground truth to reject against. The DAG generator
220
+ * normally fills this set from the project package.json, so an empty set
221
+ * means the manifest could not be read (no authorization is configured).
222
+ * 2. A declarative "no new dependencies" intent (零新增/不新增/无新增/不引入/
223
+ * no new…) satisfies the check: the model explicitly claims it adds
224
+ * nothing, so prose mentioning the existing toolchain ("DOM",
225
+ * "devDependencies", already-declared deps) must not be misread as new
226
+ * dependency claims.
227
+ * 3. Otherwise, every package-name-like token must match the allowlist —
228
+ * an unauthorized new dependency (a library absent from the project
229
+ * manifest) still fails closed.
230
+ */
231
+ export function checkDependencies(contract, allowedDependencies) {
232
+ const findings = [];
233
+ const policy = contract.dependencyPolicy?.trim();
234
+ if (!policy)
235
+ return findings;
236
+ // No explicit allowlist: nothing can be judged unauthorized.
237
+ if (allowedDependencies.length === 0)
238
+ return findings;
239
+ // Declarative no-new-dependencies intent: trust the claim, do not parse
240
+ // prose tokens (they mention the already-declared toolchain). The allowlist
241
+ // below already passes any already-declared dependency by exact match, so
242
+ // this exemption is deliberately narrow: only explicit negatives escape the
243
+ // token scan, never neutral descriptions that could conceal a new dep.
244
+ if (/零新增|不新增|无新增|不引入|不添加|不依赖|no\s*new\s*deps?|no\s*new\s*dependencies/i.test(policy)) {
245
+ return findings;
246
+ }
247
+ const allowed = new Set(allowedDependencies);
248
+ const tokens = policy
249
+ .split(/[,;\s]+/)
250
+ .filter((token) => token.length > 0);
251
+ for (const token of tokens) {
252
+ if (!NPM_PACKAGE_NAME.test(token))
253
+ continue;
254
+ // Already-declared dependencies are authorized by definition.
255
+ if (allowed.has(token))
256
+ continue;
257
+ // Versioned/scoped variants: pkg@1.2.3 or @scope/pkg@1 → match bare name.
258
+ const bareName = token.startsWith("@")
259
+ ? token.split("@").slice(0, 2).join("@")
260
+ : token.split("@")[0];
261
+ if (allowed.has(bareName))
262
+ continue;
263
+ findings.push({
264
+ code: "dependency-not-allowed",
265
+ message: `declared dependency "${token}" is not in allowedDependencies`,
266
+ path: token,
267
+ });
268
+ }
269
+ return findings;
270
+ }
271
+ /** 7. Every declared UI state must carry a complete attribution:
272
+ * applicable → expectedBehavior, non-applicable → notApplicableReason.
273
+ *
274
+ * The check validates the states the contract DECLARES, not a hardcoded list
275
+ * of canonical names: the plan prompts say "Do not invent UI states", so a
276
+ * contract that reasonably covers loading/empty/error/permission without a
277
+ * `disabled` entry must not be blocked for missing a state the model had no
278
+ * reason to declare — the model cannot fix that without inventing one, which
279
+ * the ladder then retries to exhaustion.
280
+ *
281
+ * Pure-logic contracts (no user-visible UI states AND no interactions) are
282
+ * exempt entirely: the schema and prompts explicitly allow `uiStates: []`.
283
+ * Once any state or interaction is declared, every declared state must carry
284
+ * complete attribution.
285
+ */
286
+ export function checkUiStateAttribution(contract) {
287
+ if (contract.uiStates.length === 0 && contract.interactions.length === 0)
288
+ return [];
289
+ const findings = [];
290
+ for (const state of contract.uiStates) {
291
+ if (state.applicable && !state.expectedBehavior) {
292
+ findings.push({
293
+ code: "ui-state-attribution-incomplete",
294
+ message: `applicable UI state "${state.name}" is missing expectedBehavior`,
295
+ path: state.name,
296
+ });
297
+ }
298
+ if (!state.applicable && !state.notApplicableReason) {
299
+ findings.push({
300
+ code: "ui-state-attribution-incomplete",
301
+ message: `non-applicable UI state "${state.name}" is missing notApplicableReason`,
302
+ path: state.name,
303
+ });
304
+ }
305
+ }
306
+ return findings;
307
+ }
308
+ /** Design approval: a `request-revision` verdict from the design reviewer is
309
+ * blocking. This is injected as structured input, never parsed from model text. */
310
+ export function checkDesignApproval(designApproval) {
311
+ if (designApproval.verdict === "request-revision") {
312
+ return [
313
+ {
314
+ code: "design-approval-request-revision",
315
+ message: "design approval verdict is request-revision",
316
+ },
317
+ ];
318
+ }
319
+ return [];
320
+ }
321
+ /** Aggregate the deterministic design-policy checks into a single
322
+ * deterministic result. Any finding blocks; an empty finding list approves. */
323
+ export function evaluateFrontendDesignPolicy(input) {
324
+ const findings = sortFindings([
325
+ ...(input.designApproval ? checkDesignApproval(input.designApproval) : []),
326
+ ...checkUiDesignCoverage(input.contract),
327
+ ...checkSourceFreshness(input.contract, input.sourceFreshness),
328
+ ...checkMockStrategy(input.contract, input.allowedMockStrategies),
329
+ ...checkComponentChoice(input.contract, input.componentSpecCandidatePaths),
330
+ ...checkTargetPaths(input.contract, input.writeSetPatterns),
331
+ ...checkDependencies(input.contract, input.allowedDependencies),
332
+ ...checkUiStateAttribution(input.contract),
333
+ ]);
334
+ const classification = findings.length > 0 ? "blocked" : "approved";
335
+ const result = {
336
+ schemaId: "frontend-design-policy-v1",
337
+ classification,
338
+ findings,
339
+ digest: "",
340
+ };
341
+ result.digest = deterministicSha256({
342
+ schemaId: result.schemaId,
343
+ classification: result.classification,
344
+ findings: result.findings,
345
+ });
346
+ return result;
347
+ }
@@ -0,0 +1,182 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ const id = z.string().trim().min(1);
4
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
5
+ const revision = z.number().int().positive();
6
+ const instant = z.string().datetime();
7
+ const factRef = z.object({ ref: id, digest }).strict();
8
+ function canonicalValue(value) {
9
+ if (Array.isArray(value))
10
+ return value.map(canonicalValue);
11
+ if (value !== null && typeof value === "object")
12
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).sort(([a], [b]) => a.localeCompare(b)).map(([key, item]) => [key, canonicalValue(item)]));
13
+ return value;
14
+ }
15
+ export function canonicalFrontendHumanDecisionJson(value) {
16
+ return JSON.stringify(canonicalValue(value));
17
+ }
18
+ export function computeFrontendHumanDecisionDigest(value) {
19
+ return createHash("sha256").update(canonicalFrontendHumanDecisionJson(value), "utf8").digest("hex");
20
+ }
21
+ const baseEnvelope = {
22
+ operationId: id,
23
+ runId: id,
24
+ operationRevision: revision,
25
+ targetDigest: digest,
26
+ staleOnTargetDigestChange: z.literal(true),
27
+ expiresAt: instant.optional(),
28
+ };
29
+ const contractEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("contract-decision"), resumeAction: z.literal("resume-contract"), payload: z.object({ questionFactIds: z.array(id).min(1) }).strict() }).strict();
30
+ const highEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("high-admission"), resumeAction: z.literal("admit-high"), payload: z.object({ admissionCandidateRef: factRef }).strict() }).strict();
31
+ const splitEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("split-materialization-approval"), resumeAction: z.literal("materialize-split"), payload: z.object({ splitManifestRef: factRef }).strict() }).strict();
32
+ const baselineEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("baseline-debt-acceptance"), resumeAction: z.literal("accept-baseline-debt"), payload: z.object({ baselineEvidenceRefs: z.array(factRef).min(1), candidateDigest: digest, commandDigest: digest, resultingPolicy: z.literal("allow-non-regression") }).strict() }).strict();
33
+ const verificationEnvelopeInput = z.object({ ...baseEnvelope, kind: z.literal("verification-config"), resumeAction: z.literal("select-verification-config"), payload: z.object({ unresolvedEntrypointRefs: z.array(factRef).min(1), allowlistedResolvers: z.array(id).min(1) }).strict() }).strict();
34
+ export const frontendHumanDecisionEnvelopeInputSchema = z.discriminatedUnion("kind", [contractEnvelopeInput, highEnvelopeInput, splitEnvelopeInput, baselineEnvelopeInput, verificationEnvelopeInput]);
35
+ const envelopeSchemas = [contractEnvelopeInput.extend({ digest }).strict(), highEnvelopeInput.extend({ digest }).strict(), splitEnvelopeInput.extend({ digest }).strict(), baselineEnvelopeInput.extend({ digest }).strict(), verificationEnvelopeInput.extend({ digest }).strict()];
36
+ const envelopeStructuralSchema = z.discriminatedUnion("kind", envelopeSchemas);
37
+ export const frontendHumanDecisionEnvelopeSchema = envelopeStructuralSchema.superRefine((value, ctx) => {
38
+ const { digest: actual, ...payload } = value;
39
+ if (computeFrontendHumanDecisionDigest(payload) !== actual)
40
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "envelope digest mismatch" });
41
+ });
42
+ export function buildFrontendHumanDecisionEnvelope(raw) {
43
+ const payload = frontendHumanDecisionEnvelopeInputSchema.parse(raw);
44
+ return envelopeStructuralSchema.parse({ ...payload, digest: computeFrontendHumanDecisionDigest(payload) });
45
+ }
46
+ export function parseFrontendHumanDecisionEnvelope(value) { return frontendHumanDecisionEnvelopeSchema.parse(value); }
47
+ const receiptBase = { operationId: id, runId: id, operationRevision: revision, targetDigest: digest, receiptId: id, envelopeDigest: digest, commitState: z.literal("committed"), consumeState: z.enum(["unconsumed", "consumed"]) };
48
+ const resolutionFact = { resolutionFactRef: factRef };
49
+ const contractReceiptInput = z.object({ ...receiptBase, kind: z.literal("contract-decision"), resumeAction: z.literal("resume-contract"), resolution: z.object({ questionFactIds: z.array(id).min(1), outcome: z.literal("resume-contract"), ...resolutionFact }).strict() }).strict();
50
+ const highReceiptInput = z.object({ ...receiptBase, kind: z.literal("high-admission"), resumeAction: z.literal("admit-high"), resolution: z.object({ admissionCandidateRef: factRef, outcome: z.literal("admitted-high"), ...resolutionFact }).strict() }).strict();
51
+ const splitReceiptInput = z.object({ ...receiptBase, kind: z.literal("split-materialization-approval"), resumeAction: z.literal("materialize-split"), resolution: z.object({ splitManifestRef: factRef, ...resolutionFact }).strict() }).strict();
52
+ const baselineReceiptInput = z.object({ ...receiptBase, kind: z.literal("baseline-debt-acceptance"), resumeAction: z.literal("accept-baseline-debt"), resolution: z.object({ baselineEvidenceRefs: z.array(factRef).min(1), candidateDigest: digest, commandDigest: digest, resultingPolicy: z.literal("allow-non-regression"), outcome: z.literal("accepted-baseline-debt"), remediated: z.literal(false), ...resolutionFact }).strict() }).strict();
53
+ const verificationReceiptInput = z.object({ ...receiptBase, kind: z.literal("verification-config"), resumeAction: z.literal("select-verification-config"), resolution: z.object({ entrypointRef: factRef, resolver: id, sourceDigest: digest, commandDigest: digest, ...resolutionFact }).strict() }).strict();
54
+ export const frontendHumanDecisionReceiptInputSchema = z.discriminatedUnion("kind", [contractReceiptInput, highReceiptInput, splitReceiptInput, baselineReceiptInput, verificationReceiptInput]);
55
+ const receiptStructuralSchema = z.discriminatedUnion("kind", [contractReceiptInput.extend({ digest }).strict(), highReceiptInput.extend({ digest }).strict(), splitReceiptInput.extend({ digest }).strict(), baselineReceiptInput.extend({ digest }).strict(), verificationReceiptInput.extend({ digest }).strict()]);
56
+ export const frontendHumanDecisionReceiptSchema = receiptStructuralSchema.superRefine((value, ctx) => {
57
+ const { digest: actual, ...payload } = value;
58
+ if (computeFrontendHumanDecisionDigest(payload) !== actual)
59
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "receipt digest mismatch" });
60
+ });
61
+ export function buildFrontendHumanDecisionReceipt(raw) {
62
+ const payload = frontendHumanDecisionReceiptInputSchema.parse(raw);
63
+ return receiptStructuralSchema.parse({ ...payload, digest: computeFrontendHumanDecisionDigest(payload) });
64
+ }
65
+ export function parseFrontendHumanDecisionReceipt(value) { return frontendHumanDecisionReceiptSchema.parse(value); }
66
+ const activeInput = z.object({ status: z.literal("active"), operationId: id, runId: id, revision }).strict();
67
+ const blockedInput = z.object({ status: z.literal("blocked-human"), operationId: id, runId: id, revision, envelope: frontendHumanDecisionEnvelopeSchema }).strict();
68
+ const resolvedInput = z.object({ status: z.literal("resolved"), operationId: id, runId: id, revision, receipt: frontendHumanDecisionReceiptSchema }).strict();
69
+ export const frontendHumanDecisionProjectionInputSchema = z.discriminatedUnion("status", [activeInput, blockedInput, resolvedInput]);
70
+ const projectionStructuralSchema = z.discriminatedUnion("status", [activeInput.extend({ digest }).strict(), blockedInput.extend({ digest }).strict(), resolvedInput.extend({ digest }).strict()]);
71
+ export const frontendHumanDecisionProjectionSchema = projectionStructuralSchema.superRefine((value, ctx) => { const { digest: actual, ...payload } = value; if (computeFrontendHumanDecisionDigest(payload) !== actual)
72
+ ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["digest"], message: "projection digest mismatch" }); });
73
+ export function buildFrontendHumanDecisionProjection(raw) { const payload = frontendHumanDecisionProjectionInputSchema.parse(raw); return projectionStructuralSchema.parse({ ...payload, digest: computeFrontendHumanDecisionDigest(payload) }); }
74
+ export function parseFrontendHumanDecisionProjection(value) { return frontendHumanDecisionProjectionSchema.parse(value); }
75
+ const reject = (projection, reason) => ({ outcome: "rejected", reason, projection });
76
+ export function materializeFrontendHumanDecision(input) {
77
+ const projection = parseFrontendHumanDecisionProjection(input.projection);
78
+ const envelope = parseFrontendHumanDecisionEnvelope(input.envelope);
79
+ if (projection.status !== "active")
80
+ return reject(projection, "projection is not active");
81
+ if (projection.revision !== input.expectedRecordRevision || projection.digest !== input.expectedProjectionDigest)
82
+ return reject(projection, "revision or projection digest conflict");
83
+ if (envelope.operationId !== projection.operationId || envelope.runId !== projection.runId || envelope.operationRevision !== projection.revision)
84
+ return reject(projection, "envelope binding mismatch");
85
+ return { outcome: "applied", projection: buildFrontendHumanDecisionProjection({ status: "blocked-human", operationId: projection.operationId, runId: projection.runId, revision: projection.revision + 1, envelope }) };
86
+ }
87
+ function receiptMatchesEnvelope(receipt, envelope) {
88
+ if (receipt.kind !== envelope.kind || receipt.resumeAction !== envelope.resumeAction || receipt.operationId !== envelope.operationId || receipt.runId !== envelope.runId || receipt.operationRevision !== envelope.operationRevision || receipt.targetDigest !== envelope.targetDigest || receipt.envelopeDigest !== envelope.digest)
89
+ return false;
90
+ switch (envelope.kind) {
91
+ case "contract-decision": return receipt.kind === envelope.kind && canonicalFrontendHumanDecisionJson(receipt.resolution.questionFactIds) === canonicalFrontendHumanDecisionJson(envelope.payload.questionFactIds);
92
+ case "high-admission": return receipt.kind === envelope.kind && canonicalFrontendHumanDecisionJson(receipt.resolution.admissionCandidateRef) === canonicalFrontendHumanDecisionJson(envelope.payload.admissionCandidateRef);
93
+ case "split-materialization-approval": return receipt.kind === envelope.kind && canonicalFrontendHumanDecisionJson(receipt.resolution.splitManifestRef) === canonicalFrontendHumanDecisionJson(envelope.payload.splitManifestRef);
94
+ case "baseline-debt-acceptance": return receipt.kind === envelope.kind && receipt.resolution.candidateDigest === envelope.payload.candidateDigest && receipt.resolution.commandDigest === envelope.payload.commandDigest && canonicalFrontendHumanDecisionJson(receipt.resolution.baselineEvidenceRefs) === canonicalFrontendHumanDecisionJson(envelope.payload.baselineEvidenceRefs);
95
+ case "verification-config": return receipt.kind === envelope.kind && envelope.payload.allowlistedResolvers.includes(receipt.resolution.resolver) && envelope.payload.unresolvedEntrypointRefs.some((item) => canonicalFrontendHumanDecisionJson(item) === canonicalFrontendHumanDecisionJson(receipt.resolution.entrypointRef));
96
+ }
97
+ }
98
+ export function consumeFrontendHumanDecision(input) {
99
+ const projection = parseFrontendHumanDecisionProjection(input.projection);
100
+ if (projection.status !== "blocked-human")
101
+ return reject(projection, "projection has no pending envelope");
102
+ const consumeInput = z.object({ expectedRecordRevision: revision, expectedProjectionDigest: digest, currentTargetDigest: digest, now: instant }).strict().safeParse({
103
+ expectedRecordRevision: input.expectedRecordRevision,
104
+ expectedProjectionDigest: input.expectedProjectionDigest,
105
+ currentTargetDigest: input.currentTargetDigest,
106
+ now: input.now,
107
+ });
108
+ if (!consumeInput.success)
109
+ return reject(projection, "invalid consume input");
110
+ const receipt = parseFrontendHumanDecisionReceipt(input.receipt);
111
+ if (projection.revision !== input.expectedRecordRevision || projection.digest !== input.expectedProjectionDigest)
112
+ return reject(projection, "revision or projection digest conflict");
113
+ if (input.currentTargetDigest !== projection.envelope.targetDigest) {
114
+ const payload = { kind: "frontend-human-decision-stale", operationId: projection.operationId, envelopeDigest: projection.envelope.digest, previousTargetDigest: projection.envelope.targetDigest, currentTargetDigest: input.currentTargetDigest };
115
+ return { outcome: "stale", reason: "target digest changed", projection, auditFact: { ...payload, digest: computeFrontendHumanDecisionDigest(payload) } };
116
+ }
117
+ if (projection.envelope.expiresAt && Date.parse(input.now) >= Date.parse(projection.envelope.expiresAt))
118
+ return reject(projection, "envelope expired");
119
+ if (receipt.commitState !== "committed" || receipt.consumeState !== "unconsumed" || !receiptMatchesEnvelope(receipt, projection.envelope))
120
+ return reject(projection, "receipt freshness or payload mismatch");
121
+ const { digest: _digest, ...receiptPayload } = receipt;
122
+ const consumed = buildFrontendHumanDecisionReceipt({ ...receiptPayload, consumeState: "consumed" });
123
+ return { outcome: "applied", projection: buildFrontendHumanDecisionProjection({ status: "resolved", operationId: projection.operationId, runId: projection.runId, revision: projection.revision + 1, receipt: consumed }) };
124
+ }
125
+ /* ------------------------------------------------------------------ *
126
+ * A+B producer/consumer wiring (no new envelope schema in the frozen *
127
+ * five-kind union). *
128
+ * ------------------------------------------------------------------ */
129
+ /**
130
+ * A+B (AC-002): contract `blocked` uniquely maps to `blocked-human`
131
+ * (contract-decision envelope) or `blocked-external` (wait-external), reusing
132
+ * the existing five-envelope schema without widening it. Any other disposition
133
+ * is `not-blocked`.
134
+ */
135
+ export function mapContractBlockedOwner(input) {
136
+ if (input.disposition !== "blocked")
137
+ return "not-blocked";
138
+ return input.blockingOwner === "blocked-external"
139
+ ? "blocked-external"
140
+ : "blocked-human";
141
+ }
142
+ /**
143
+ * A+B (AC-007): High-topology `high-admission-receipt`. The receipt is a
144
+ * runtime-materialized catalog (not a sixth human-decision envelope): a receipt
145
+ * whose candidate digest matches the admission digest resolves to `accepted`;
146
+ * any digest drift resolves to `stale`.
147
+ */
148
+ export const highAdmissionReceiptSchema = z
149
+ .object({
150
+ schemaVersion: z.literal(1),
151
+ schemaId: z.literal("high-admission-receipt-v1"),
152
+ candidateDigest: digest,
153
+ admissionDigest: digest,
154
+ status: z.enum(["accepted", "stale"]),
155
+ })
156
+ .strict();
157
+ export function evaluateHighAdmissionReceipt(input) {
158
+ return highAdmissionReceiptSchema.parse({
159
+ schemaVersion: 1,
160
+ schemaId: "high-admission-receipt-v1",
161
+ candidateDigest: input.candidateDigest,
162
+ admissionDigest: input.admissionDigest,
163
+ status: input.candidateDigest === input.admissionDigest
164
+ ? "accepted"
165
+ : "stale",
166
+ });
167
+ }
168
+ /**
169
+ * A+B (AC-008): verification-config routing. A verification-config signal
170
+ * either restarts the contract or produces a verification-config-decision
171
+ * (independent of the frozen `verification-config` envelope). Fail-closed to
172
+ * contract restart when the signal is untyped.
173
+ */
174
+ export function routeVerificationConfig(input) {
175
+ const entrypointRefs = [...new Set(input.unresolvedEntrypointRefs)].filter((item) => item.length > 0);
176
+ return {
177
+ decision: entrypointRefs.length > 0
178
+ ? "verification-config-decision"
179
+ : "contract-restart",
180
+ entrypointRefs,
181
+ };
182
+ }