@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,425 @@
1
+ /**
2
+ * Frontend task shape routing (M7) and shape-transition capsules.
3
+ *
4
+ * Pure and independent of the M4 risk classifier and the runtime branch
5
+ * control. The shape functions here are deterministic (no I/O, no model, no
6
+ * side effects). This module is the generation-time authority for frontend
7
+ * task-shape routing; `init-hybrid.ts` must consume it directly and must not
8
+ * import the closeout renderer.
9
+ */
10
+ import { createHash } from "node:crypto";
11
+ import { z } from "zod";
12
+ /** Fixture-only node counts. They are NOT the authoritative 15/13/11
13
+ * topology: split-required has no count. */
14
+ export const FRONTEND_SHAPE_NODE_COUNTS = {
15
+ micro: 7,
16
+ small: 10,
17
+ standard: 11,
18
+ high: 11,
19
+ };
20
+ const SHAPE_ESCALATION_PATTERNS = [
21
+ { id: "auth-permission", re: /\bauth\b|\bpermission\b|\brbac\b|登录|权限|\bjwt\b|\bsession\b/i },
22
+ { id: "sensitive-action", re: /\bpayment\b|delete\s+account|支付|删除用户|\bdestructive\b/i },
23
+ { id: "ssr-boundary", re: /\bssr\b|\brsc\b|server\s+component|\bhydration\b|server-client/i },
24
+ { id: "new-route", re: /new\s+route|新增路由|createBrowserRouter|add\s+route/i },
25
+ { id: "shared-public-api", re: /public\s+api|shared\s+component|公共组件|破坏性|breaking\s+change/i },
26
+ { id: "schema-api-change", re: /\bopenapi\b|graphql\s+schema|api\s+contract|接口变更|schema\s+change/i },
27
+ { id: "new-dependency", re: /new\s+dependency|add\s+dependency|npm\s+install|新增依赖/i },
28
+ { id: "cross-domain-paths", re: /多个领域|cross-module|multiple\s+packages/i },
29
+ ];
30
+ const SHAPE_SMALL_PATTERNS = [
31
+ { id: "single-component", re: /single\s+component|单一组件|局部样式|style\s+tweak|css\s+only|文案|copy\s+change/i },
32
+ { id: "no-remote", re: /no\s+api|无接口|本地状态|local\s+state\s+only|pure\s+ui/i },
33
+ ];
34
+ const SHAPE_SPLIT_REQUIRED_PATTERN = /拆分|split\s+(?:requirement|task|需求)|large\s+requirement|大型需求|多步骤需求/i;
35
+ const EXECUTABLE_SHAPE_RANK = {
36
+ micro: 0,
37
+ small: 1,
38
+ standard: 2,
39
+ high: 3,
40
+ };
41
+ function pathSpreadScore(paths) {
42
+ const tops = new Set(paths
43
+ .map((p) => p.replace(/\\/g, "/").split("/").filter(Boolean)[0] ?? "")
44
+ .filter(Boolean));
45
+ return tops.size;
46
+ }
47
+ function collectEscalationSignals(blob, supervised) {
48
+ const signals = [];
49
+ for (const pattern of SHAPE_ESCALATION_PATTERNS) {
50
+ if (pattern.re.test(blob))
51
+ signals.push(pattern.id);
52
+ }
53
+ if (supervised)
54
+ signals.push("supervised-profile");
55
+ if (SHAPE_SPLIT_REQUIRED_PATTERN.test(blob)) {
56
+ signals.push("large-requirement-split");
57
+ }
58
+ return [...new Set(signals)];
59
+ }
60
+ function resolveBaselineFrontendTaskShape(input) {
61
+ const blob = [
62
+ input.requirementMarkdown ?? "",
63
+ input.constraintMarkdown ?? "",
64
+ (input.allowedPaths ?? []).join("\n"),
65
+ ].join("\n");
66
+ const profile = (input.governanceProfile ?? "").toLowerCase();
67
+ const supervised = profile === "supervised" ||
68
+ profile.includes("supervised") ||
69
+ /\bsupervised\b/i.test(blob);
70
+ const escalationSignals = collectEscalationSignals(blob, supervised);
71
+ const smallHits = [];
72
+ for (const pattern of SHAPE_SMALL_PATTERNS) {
73
+ if (pattern.re.test(blob))
74
+ smallHits.push(pattern.id);
75
+ }
76
+ const paths = input.allowedPaths ?? [];
77
+ const spread = pathSpreadScore(paths);
78
+ const concentrated = spread <= 1 && paths.length <= 4;
79
+ const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
80
+ !/\b(no\s+api|无接口|not-needed)\b/i.test(blob);
81
+ const complexity = (input.complexity ?? "").toLowerCase();
82
+ // split-required wins over everything: it is a termination/escalation
83
+ // signal, not an executable topology.
84
+ if (input.splitSignal.splitRequired) {
85
+ return {
86
+ shape: "split-required",
87
+ signals: [...new Set([...escalationSignals, "split-required"])],
88
+ rejectedSignals: smallHits.map((id) => `small:${id}:blocked-by-split-required`),
89
+ reason: "split-required signal present: terminate/escalate before planning or writing",
90
+ };
91
+ }
92
+ if (supervised) {
93
+ const shape = escalationSignals.filter((s) => s !== "supervised-profile").length > 0
94
+ ? "high"
95
+ : "standard";
96
+ return {
97
+ shape,
98
+ signals: escalationSignals,
99
+ rejectedSignals: smallHits.map((id) => `small:${id}:blocked-by-supervised`),
100
+ reason: shape === "high"
101
+ ? `high escalation under supervised: ${escalationSignals.join(", ")}`
102
+ : "supervised profile forces full gates (standard minimum)",
103
+ };
104
+ }
105
+ if (escalationSignals.length > 0) {
106
+ return {
107
+ shape: "high",
108
+ signals: escalationSignals,
109
+ rejectedSignals: smallHits.map((id) => `small:${id}:blocked-by-escalation`),
110
+ reason: `escalation signals present: ${escalationSignals.join(", ")}`,
111
+ };
112
+ }
113
+ // micro: the smallest possible change — explicit micro complexity, no API
114
+ // signal, no escalation, concentrated paths.
115
+ if ((complexity === "micro" || complexity === "trivial") &&
116
+ concentrated &&
117
+ !hasApiSignal) {
118
+ return {
119
+ shape: "micro",
120
+ signals: smallHits.length > 0 ? smallHits : ["micro-scope"],
121
+ rejectedSignals: [],
122
+ reason: "micro scope: trivial single-file change without remote API",
123
+ };
124
+ }
125
+ // small: no escalation, no supervised, small keywords, concentrated paths,
126
+ // no remote API signal.
127
+ if (smallHits.length > 0 &&
128
+ concentrated &&
129
+ !hasApiSignal &&
130
+ (complexity === "small" || complexity === "low")) {
131
+ return {
132
+ shape: "small",
133
+ signals: smallHits,
134
+ rejectedSignals: [],
135
+ reason: `small signals without escalation: ${smallHits.join(", ")}`,
136
+ };
137
+ }
138
+ const rejected = smallHits.map((id) => `small:${id}:blocked-by-${hasApiSignal ? "api-signal" : "path-spread"}`);
139
+ return {
140
+ shape: "standard",
141
+ signals: ["default-standard"],
142
+ rejectedSignals: rejected,
143
+ reason: smallHits.length > 0
144
+ ? hasApiSignal
145
+ ? "small keywords present but API/remote signals require standard topology"
146
+ : "small keywords present but path spread requires standard topology"
147
+ : "ambiguous or standard frontend work; default standard topology",
148
+ };
149
+ }
150
+ function reshapeFloorShape(capsule) {
151
+ if (capsule.requestedMinimumRisk === "high")
152
+ return "high";
153
+ if (capsule.requestedMinimumSize === "standard")
154
+ return "standard";
155
+ if (capsule.requestedMinimumRisk === "standard")
156
+ return "standard";
157
+ return "small";
158
+ }
159
+ function raiseExecutableShape(baseline, floor) {
160
+ if (baseline === "split-required")
161
+ return "split-required";
162
+ return EXECUTABLE_SHAPE_RANK[baseline] >= EXECUTABLE_SHAPE_RANK[floor]
163
+ ? baseline
164
+ : floor;
165
+ }
166
+ /**
167
+ * Route a frontend task into one of five shapes. Pure and independent of the
168
+ * M4 risk classifier and the runtime branch control. Escalation signals
169
+ * (high-risk keywords, supervised profile, cross-domain, large requirement
170
+ * split) always raise the shape above `small` or produce `split-required` —
171
+ * they are never silently swallowed. A verified shape-transition capsule, when
172
+ * present, outranks natural-language keywords and complexity but never lowers
173
+ * an already-higher executable baseline.
174
+ */
175
+ export function resolveFrontendTaskShape(input) {
176
+ const capsule = input.shapeTransitionCapsule != null
177
+ ? parseFrontendShapeTransitionCapsule(input.shapeTransitionCapsule)
178
+ : undefined;
179
+ if (capsule) {
180
+ if (!input.taskId || !input.sourceDigest) {
181
+ throw new Error("shape transition capsule requires matching taskId and sourceDigest");
182
+ }
183
+ if (capsule.taskId !== input.taskId ||
184
+ capsule.sourceDigest !== input.sourceDigest) {
185
+ throw new Error("shape transition capsule task/source binding mismatch");
186
+ }
187
+ }
188
+ const baseline = resolveBaselineFrontendTaskShape(input);
189
+ if (!capsule)
190
+ return baseline;
191
+ if (capsule.kind === "split") {
192
+ return {
193
+ shape: "split-required",
194
+ signals: [...new Set([...baseline.signals, "shape-transition-split"])],
195
+ rejectedSignals: baseline.rejectedSignals,
196
+ reason: `shape transition capsule split: ${capsule.splitProposalRef}`,
197
+ };
198
+ }
199
+ const floor = reshapeFloorShape(capsule);
200
+ const shape = raiseExecutableShape(baseline.shape, floor);
201
+ return {
202
+ shape,
203
+ signals: [
204
+ ...new Set([
205
+ ...baseline.signals,
206
+ ...capsule.escalationSignals,
207
+ "shape-transition-reshape",
208
+ ]),
209
+ ],
210
+ rejectedSignals: baseline.rejectedSignals,
211
+ reason: shape === baseline.shape
212
+ ? baseline.reason
213
+ : `shape transition capsule reshape floor ${floor} raised ${baseline.shape}`,
214
+ };
215
+ }
216
+ /* ------------------------------------------------------------------ *
217
+ * Frozen topology binding and runtime escalation evaluation *
218
+ * ------------------------------------------------------------------ */
219
+ export const FRONTEND_TOPOLOGY_BINDING_SCHEMA_VERSION = 1;
220
+ /** Freeze capability from the generated canonical task set. Node complexity
221
+ * is deliberately excluded: Standard and High share the same full topology. */
222
+ export function buildFrontendTopologyBinding(taskIds) {
223
+ const canonicalTaskIds = [...new Set(taskIds)].sort();
224
+ const capability = canonicalTaskIds.includes("frontend-design-review-pi")
225
+ ? "full"
226
+ : "small";
227
+ const descriptor = {
228
+ schemaVersion: FRONTEND_TOPOLOGY_BINDING_SCHEMA_VERSION,
229
+ capability,
230
+ taskIds: canonicalTaskIds,
231
+ };
232
+ return {
233
+ ...descriptor,
234
+ canonicalDigest: sha256Utf8(JSON.stringify(sortKeysDeep(descriptor))),
235
+ };
236
+ }
237
+ export function assertFrontendTopologyBinding(binding, taskIds) {
238
+ if (!binding)
239
+ throw new Error("frontend topology binding is missing");
240
+ const expected = buildFrontendTopologyBinding(taskIds);
241
+ if (binding.schemaVersion !== expected.schemaVersion ||
242
+ binding.capability !== expected.capability ||
243
+ binding.canonicalDigest !== expected.canonicalDigest ||
244
+ !stringArraysEqual(binding.taskIds, expected.taskIds)) {
245
+ throw new Error("frontend topology binding drifted from immutable DagSpec tasks");
246
+ }
247
+ return binding;
248
+ }
249
+ /** Runtime-only fact derivation. This intentionally does not call the
250
+ * generation-time `resolveFrontendTaskShape` router. */
251
+ export function deriveFrontendRuntimeShapeFacts(input) {
252
+ const blob = [
253
+ input.requirementMarkdown ?? "",
254
+ input.constraintMarkdown ?? "",
255
+ (input.allowedPaths ?? []).join("\n"),
256
+ ].join("\n");
257
+ const supervised = /\bsupervised\b/i.test(blob);
258
+ const escalationSignals = collectEscalationSignals(blob, supervised);
259
+ const paths = input.allowedPaths ?? [];
260
+ const hasApiSignal = /\b(api|fetch|axios|graphql|endpoint|远程|接口)\b/i.test(blob) &&
261
+ !/\b(no\s+api|无接口|not-needed)\b/i.test(blob);
262
+ const requiresFull = escalationSignals.length > 0 || pathSpreadScore(paths) > 1 || hasApiSignal;
263
+ return {
264
+ splitRequired: input.splitRequired,
265
+ escalationSignals,
266
+ minimumTopologyCapability: requiresFull ? "full" : "small",
267
+ };
268
+ }
269
+ /** Compare committed runtime facts with the generation-frozen topology. */
270
+ export function evaluateFrontendShapeEscalation(input) {
271
+ if (input.splitRequired) {
272
+ return {
273
+ outcome: "split-required",
274
+ reason: "runtime facts require splitting before downstream dispatch",
275
+ };
276
+ }
277
+ if (input.minimumTopologyCapability === "full" &&
278
+ input.currentTopologyCapability === "small") {
279
+ return {
280
+ outcome: "reshape-required",
281
+ reason: `runtime facts require full frontend topology${input.escalationSignals.length > 0 ? `: ${input.escalationSignals.join(", ")}` : ""}`,
282
+ };
283
+ }
284
+ return {
285
+ outcome: "continue",
286
+ reason: input.escalationSignals.length > 0
287
+ ? `frozen topology already covers runtime risk signals: ${input.escalationSignals.join(", ")}`
288
+ : "frozen topology satisfies committed runtime facts",
289
+ };
290
+ }
291
+ /* ------------------------------------------------------------------ *
292
+ * Shape transition capsule (§7.1 fields; runtime five-tier mapping) *
293
+ * ------------------------------------------------------------------ */
294
+ const hash64 = z.string().regex(/^[a-f0-9]{64}$/);
295
+ const committedIdList = z.array(z.string().min(1)).min(1);
296
+ const frontendShapeTransitionCapsuleBaseFields = {
297
+ schemaVersion: z.literal(1),
298
+ taskId: z.string().min(1),
299
+ parentRunId: z.string().min(1),
300
+ sourceDigest: hash64,
301
+ fromShapeDigest: hash64,
302
+ shapeFactsDigest: hash64,
303
+ committedFactIds: committedIdList,
304
+ evidenceRefs: committedIdList,
305
+ committedAt: z.string().datetime(),
306
+ capsuleDigest: hash64,
307
+ };
308
+ export const frontendShapeTransitionReshapeCapsuleSchema = z
309
+ .object({
310
+ ...frontendShapeTransitionCapsuleBaseFields,
311
+ kind: z.literal("reshape"),
312
+ requestedMinimumSize: z.enum(["small", "standard"]),
313
+ requestedMinimumRisk: z.enum(["low", "standard", "high"]),
314
+ escalationSignals: z.array(z.string().min(1)),
315
+ })
316
+ .strict();
317
+ export const frontendShapeTransitionSplitCapsuleSchema = z
318
+ .object({
319
+ ...frontendShapeTransitionCapsuleBaseFields,
320
+ kind: z.literal("split"),
321
+ splitProposalRef: z.string().min(1),
322
+ splitProposalDigest: hash64,
323
+ requirementCoverageDigest: hash64,
324
+ })
325
+ .strict();
326
+ export const frontendShapeTransitionCapsuleSchema = z.discriminatedUnion("kind", [
327
+ frontendShapeTransitionReshapeCapsuleSchema,
328
+ frontendShapeTransitionSplitCapsuleSchema,
329
+ ]);
330
+ function isPlainObject(value) {
331
+ return typeof value === "object" && value !== null && !Array.isArray(value);
332
+ }
333
+ function dedupeSortStrings(values) {
334
+ return [...new Set(values)].sort();
335
+ }
336
+ function stringArraysEqual(left, right) {
337
+ return left.length === right.length && left.every((value, index) => value === right[index]);
338
+ }
339
+ function sortKeysDeep(value) {
340
+ if (Array.isArray(value)) {
341
+ return value.map((item) => sortKeysDeep(item));
342
+ }
343
+ if (isPlainObject(value)) {
344
+ const keys = Object.keys(value).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
345
+ const out = {};
346
+ for (const key of keys) {
347
+ out[key] = sortKeysDeep(value[key]);
348
+ }
349
+ return out;
350
+ }
351
+ return value;
352
+ }
353
+ function sha256Utf8(value) {
354
+ return createHash("sha256").update(value, "utf8").digest("hex");
355
+ }
356
+ function computeCapsuleDigest(fields) {
357
+ const { capsuleDigest: _ignored, ...rest } = fields;
358
+ return sha256Utf8(JSON.stringify(sortKeysDeep(rest)));
359
+ }
360
+ function requireNonEmptyStringArray(value, field) {
361
+ if (!Array.isArray(value) || value.length === 0) {
362
+ throw new Error(`${field} must be a non-empty array of committed facts`);
363
+ }
364
+ if (!value.every((entry) => typeof entry === "string" && entry.length > 0)) {
365
+ throw new Error(`${field} must contain only non-empty strings`);
366
+ }
367
+ return value;
368
+ }
369
+ function normalizeCapsuleRecord(input) {
370
+ const next = {
371
+ ...input,
372
+ committedFactIds: dedupeSortStrings(requireNonEmptyStringArray(input.committedFactIds, "committedFactIds")),
373
+ evidenceRefs: dedupeSortStrings(requireNonEmptyStringArray(input.evidenceRefs, "evidenceRefs")),
374
+ };
375
+ if (next.kind === "reshape") {
376
+ const signals = next.escalationSignals;
377
+ if (signals === undefined) {
378
+ next.escalationSignals = [];
379
+ }
380
+ else if (!Array.isArray(signals)) {
381
+ throw new Error("escalationSignals must be a string array");
382
+ }
383
+ else {
384
+ next.escalationSignals = dedupeSortStrings(signals.map((entry) => {
385
+ if (typeof entry !== "string") {
386
+ throw new Error("escalationSignals must contain only strings");
387
+ }
388
+ return entry;
389
+ }));
390
+ }
391
+ }
392
+ return next;
393
+ }
394
+ export function computeFrontendShapeSourceDigest(input) {
395
+ return sha256Utf8(JSON.stringify(sortKeysDeep({
396
+ constraintMarkdown: input.constraintMarkdown ?? "",
397
+ requirementMarkdown: input.requirementMarkdown,
398
+ })));
399
+ }
400
+ export function buildFrontendShapeTransitionCapsule(input) {
401
+ if (!isPlainObject(input)) {
402
+ throw new Error("FrontendShapeTransitionCapsule input must be an object");
403
+ }
404
+ const { capsuleDigest: _ignored, ...rest } = input;
405
+ const normalized = normalizeCapsuleRecord(rest);
406
+ return frontendShapeTransitionCapsuleSchema.parse({
407
+ ...normalized,
408
+ capsuleDigest: computeCapsuleDigest(normalized),
409
+ });
410
+ }
411
+ export function parseFrontendShapeTransitionCapsule(value) {
412
+ const parsed = frontendShapeTransitionCapsuleSchema.parse(value);
413
+ const normalized = normalizeCapsuleRecord(parsed);
414
+ if (!stringArraysEqual(parsed.committedFactIds, normalized.committedFactIds) ||
415
+ !stringArraysEqual(parsed.evidenceRefs, normalized.evidenceRefs) ||
416
+ (parsed.kind === "reshape" &&
417
+ !stringArraysEqual(parsed.escalationSignals, normalized.escalationSignals))) {
418
+ throw new Error("FrontendShapeTransitionCapsule arrays must use canonical sorted unique representation");
419
+ }
420
+ const expected = computeCapsuleDigest(normalized);
421
+ if (parsed.capsuleDigest !== expected) {
422
+ throw new Error("capsuleDigest does not match canonical fields");
423
+ }
424
+ return parsed;
425
+ }
@@ -0,0 +1,203 @@
1
+ import { z } from "zod";
2
+ import { assertFrontendSplitBindingConsistency, buildFrontendOperationExecution, buildFrontendSplitAggregateCloseout, completeFrontendSplitIntegration, completeFrontendSplitIntegrationInputSchema, finalizeFrontendSplitAggregate, frontendCommittedFactRefSchema, frontendOperationExecutionSchema, frontendSplitAggregateCloseoutSchema, frontendSplitCandidateBindingSchema, frontendSplitChildObservationSchema, frontendSplitIntegrationBindingSchema, frontendSplitManifestSchema, frontendSplitParentRecordSchema, isFrontendSplitChildReady, observeFrontendSplitChild, prepareFrontendSplitJoin, } from "./frontend-split-orchestrator.js";
3
+ const idSchema = z.string().trim().min(1);
4
+ const digestSchema = z.string().regex(/^[a-f0-9]{64}$/);
5
+ const positiveIntegerSchema = z.number().int().positive();
6
+ export const frontendSplitProjectionSchema = z.object({
7
+ revision: positiveIntegerSchema,
8
+ execution: frontendOperationExecutionSchema,
9
+ manifest: frontendSplitManifestSchema,
10
+ parent: frontendSplitParentRecordSchema,
11
+ candidateBinding: frontendSplitCandidateBindingSchema.optional(),
12
+ integrationBinding: frontendSplitIntegrationBindingSchema.optional(),
13
+ aggregateCloseout: z.object({ value: frontendSplitAggregateCloseoutSchema, committedRef: frontendCommittedFactRefSchema }).strict().optional(),
14
+ }).strict().superRefine((value, ctx) => {
15
+ if (value.revision !== value.parent.operationRevision || value.execution.kind !== "split-parent" || value.execution.operationId !== value.parent.operationId || value.execution.operationRevision !== value.parent.operationRevision || value.execution.parentRecordDigest !== value.parent.digest)
16
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "revision/execution/parent projection mismatch" });
17
+ if (value.manifest.parentOperationId !== value.parent.operationId || value.manifest.digest !== value.parent.manifestDigest)
18
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "manifest/parent projection mismatch" });
19
+ try {
20
+ assertFrontendSplitBindingConsistency(value);
21
+ }
22
+ catch (error) {
23
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: error instanceof Error ? error.message : "split projection binding mismatch" });
24
+ }
25
+ });
26
+ export function assertFrontendSplitProjectionSuccessor(currentValue, nextValue) {
27
+ const current = frontendSplitProjectionSchema.parse(currentValue);
28
+ const next = frontendSplitProjectionSchema.parse(nextValue);
29
+ if (next.revision !== current.revision + 1 || next.parent.operationRevision !== current.parent.operationRevision + 1)
30
+ throw new Error("split successor revision mismatch");
31
+ if (JSON.stringify(next.manifest) !== JSON.stringify(current.manifest) || next.parent.operationId !== current.parent.operationId || next.parent.generation !== current.parent.generation || next.parent.sourceRunId !== current.parent.sourceRunId || next.parent.sourceDigest !== current.parent.sourceDigest || next.parent.manifestDigest !== current.parent.manifestDigest)
32
+ throw new Error("split successor immutable binding mismatch");
33
+ if (next.parent.operationState === "success" || next.parent.operationTerminal === "success" || next.parent.phase === "closed") {
34
+ if (!next.aggregateCloseout)
35
+ throw new Error("split success requires committed aggregate closeout");
36
+ const finalized = finalizeFrontendSplitAggregate(current.parent, next.aggregateCloseout.value, next.aggregateCloseout.committedRef);
37
+ const expected = frontendSplitProjectionSchema.parse({ ...current, revision: current.revision + 1, parent: finalized.parent, execution: finalized.execution, aggregateCloseout: next.aggregateCloseout });
38
+ if (JSON.stringify(next) !== JSON.stringify(expected))
39
+ throw new Error("split success is not the canonical aggregate successor");
40
+ }
41
+ else if (next.aggregateCloseout)
42
+ throw new Error("non-success successor may not carry aggregate closeout");
43
+ return next;
44
+ }
45
+ const commandBase = { operationId: idSchema, expectedRevision: positiveIntegerSchema, expectedParentDigest: digestSchema, expectedManifestDigest: digestSchema };
46
+ export const frontendSplitWakeupCommandSchema = z.object(commandBase).strict();
47
+ export const frontendSplitChildCallbackCommandSchema = z.object({ ...commandBase, observation: frontendSplitChildObservationSchema }).strict();
48
+ export const frontendSplitIntegrationCallbackCommandSchema = z.object({ ...commandBase, completion: completeFrontendSplitIntegrationInputSchema }).strict();
49
+ export const frontendSplitFinalizeCommandSchema = z.object(commandBase).strict();
50
+ function commandFreshnessMismatch(projection, command) {
51
+ if (projection.parent.operationId !== command.operationId)
52
+ return "operationId";
53
+ if (projection.revision !== command.expectedRevision)
54
+ return "expectedRevision";
55
+ if (projection.parent.digest !== command.expectedParentDigest)
56
+ return "expectedParentDigest";
57
+ if (projection.manifest.digest !== command.expectedManifestDigest)
58
+ return "expectedManifestDigest";
59
+ return undefined;
60
+ }
61
+ function assertCurrent(raw, command) {
62
+ const projection = frontendSplitProjectionSchema.parse(raw);
63
+ if (commandFreshnessMismatch(projection, command))
64
+ throw new Error("frontend split command freshness mismatch");
65
+ return projection;
66
+ }
67
+ function executionForParent(parent) {
68
+ return buildFrontendOperationExecution({ kind: "split-parent", operationId: parent.operationId, operationRevision: parent.operationRevision, sourceRunId: parent.sourceRunId, sourceDigest: parent.sourceDigest, generation: parent.generation, parentRecordDigest: parent.digest });
69
+ }
70
+ function nextProjection(current, patch) {
71
+ return frontendSplitProjectionSchema.parse({ ...current, ...patch, revision: current.revision + 1 });
72
+ }
73
+ export class FrontendSplitOrchestrator {
74
+ ports;
75
+ constructor(ports) {
76
+ this.ports = ports;
77
+ }
78
+ async loadProjection(operationId) {
79
+ const loaded = await this.ports.load(operationId);
80
+ if (!loaded)
81
+ throw new Error(`frontend split operation not found: ${operationId}`);
82
+ return frontendSplitProjectionSchema.parse(loaded);
83
+ }
84
+ async loadCurrent(command) {
85
+ return assertCurrent(await this.loadProjection(command.operationId), command);
86
+ }
87
+ async loadCallbackCurrent(command, fact) {
88
+ const projection = await this.loadProjection(command.operationId);
89
+ const mismatch = commandFreshnessMismatch(projection, command);
90
+ if (!mismatch)
91
+ return projection;
92
+ await this.ports.auditStale({
93
+ operationId: command.operationId,
94
+ reason: "frontend split command freshness mismatch",
95
+ fact: { mismatch, callbackFact: fact },
96
+ });
97
+ return undefined;
98
+ }
99
+ cas(current, next) {
100
+ return this.ports.compareAndSwap({ operationId: current.parent.operationId, expectedRevision: current.revision, expectedExecutionDigest: current.execution.digest, expectedParentDigest: current.parent.digest, next });
101
+ }
102
+ async wakeupChildren(raw) {
103
+ const command = frontendSplitWakeupCommandSchema.parse(raw);
104
+ const current = await this.loadCurrent(command);
105
+ const materialized = [];
106
+ const scheduled = [];
107
+ for (const childId of current.manifest.topologicalOrder) {
108
+ const child = current.manifest.children.find((item) => item.childId === childId);
109
+ const result = await this.ports.materialize({ kind: "child", parentOperationId: current.parent.operationId, manifestDigest: current.manifest.digest, generation: current.parent.generation, taskId: child.taskId, operationId: child.operationId, payloadDigest: current.manifest.digest, allowedPaths: child.allowedPaths, writeSet: child.writeSet, verifyRefs: current.manifest.verifyRefs });
110
+ if (result.taskId !== child.taskId || result.operationId !== child.operationId)
111
+ throw new Error("child materialization binding drift");
112
+ materialized.push(child.operationId);
113
+ const childControllerOwnsScheduling = current.parent.observations.some((observation) => observation.childId === childId && observation.operationId === child.operationId);
114
+ if (!childControllerOwnsScheduling && isFrontendSplitChildReady(current.parent, current.manifest, childId)) {
115
+ await this.ports.schedule({ operationId: child.operationId, generation: current.parent.generation, bindingDigest: current.parent.digest });
116
+ scheduled.push(child.operationId);
117
+ }
118
+ }
119
+ return { materialized, scheduled };
120
+ }
121
+ async childCallback(raw) {
122
+ const command = frontendSplitChildCallbackCommandSchema.parse(raw);
123
+ const current = await this.loadCallbackCurrent(command, command.observation);
124
+ if (!current)
125
+ return { outcome: "ignored-stale" };
126
+ const reduced = observeFrontendSplitChild(current.parent, current.manifest, command.observation);
127
+ if (reduced.outcome === "ignored-stale") {
128
+ await this.ports.auditStale({ operationId: command.operationId, reason: reduced.auditFact.reason, fact: reduced.auditFact });
129
+ return { outcome: "ignored-stale" };
130
+ }
131
+ return this.cas(current, nextProjection(current, { parent: reduced.parent, execution: executionForParent(reduced.parent) }));
132
+ }
133
+ async wakeupIntegration(raw) {
134
+ const command = frontendSplitWakeupCommandSchema.parse(raw);
135
+ const current = await this.loadCurrent(command);
136
+ const integration = current.manifest.integration;
137
+ const materializeAndSchedule = async (bindingDigest) => {
138
+ if (!integration.required || !integration.operationId)
139
+ return;
140
+ const taskId = `${current.parent.operationId}:integration`;
141
+ const result = await this.ports.materialize({ kind: "integration", parentOperationId: current.parent.operationId, manifestDigest: current.manifest.digest, generation: current.parent.generation, taskId, operationId: integration.operationId, payloadDigest: bindingDigest, allowedPaths: integration.allowedPaths, writeSet: integration.writeSet, verifyRefs: integration.verifyRefs });
142
+ if (result.taskId !== taskId || result.operationId !== integration.operationId)
143
+ throw new Error("integration materialization binding drift");
144
+ await this.ports.schedule({ operationId: integration.operationId, generation: current.parent.generation, bindingDigest });
145
+ };
146
+ if (current.parent.phase === "awaiting-integration") {
147
+ if (!integration.required || !current.candidateBinding)
148
+ return { outcome: "not-ready" };
149
+ await materializeAndSchedule(current.candidateBinding.digest);
150
+ return { outcome: "scheduled-existing" };
151
+ }
152
+ const joined = prepareFrontendSplitJoin(current.parent, current.manifest);
153
+ if (joined.outcome === "not-ready")
154
+ return { outcome: "not-ready" };
155
+ if (integration.required) {
156
+ const taskId = `${current.parent.operationId}:integration`;
157
+ const result = await this.ports.materialize({ kind: "integration", parentOperationId: current.parent.operationId, manifestDigest: current.manifest.digest, generation: current.parent.generation, taskId, operationId: integration.operationId, payloadDigest: joined.binding.digest, allowedPaths: integration.allowedPaths, writeSet: integration.writeSet, verifyRefs: integration.verifyRefs });
158
+ if (result.taskId !== taskId || result.operationId !== integration.operationId)
159
+ throw new Error("integration materialization binding drift");
160
+ }
161
+ const next = nextProjection(current, { parent: joined.parent, execution: executionForParent(joined.parent), candidateBinding: joined.binding });
162
+ const applied = await this.cas(current, next);
163
+ if (applied.outcome === "applied" && integration.required)
164
+ await this.ports.schedule({ operationId: integration.operationId, generation: current.parent.generation, bindingDigest: joined.binding.digest });
165
+ return applied;
166
+ }
167
+ async integrationCallback(raw) {
168
+ const command = frontendSplitIntegrationCallbackCommandSchema.parse(raw);
169
+ const current = await this.loadCallbackCurrent(command, command.completion);
170
+ if (!current)
171
+ return { outcome: "ignored-stale" };
172
+ if (!current.candidateBinding)
173
+ throw new Error("candidate binding is missing");
174
+ const completed = completeFrontendSplitIntegration(current.parent, current.manifest, current.candidateBinding, command.completion);
175
+ if (completed.outcome === "ignored-stale") {
176
+ await this.ports.auditStale({ operationId: command.operationId, reason: completed.reason, fact: command.completion });
177
+ return { outcome: "ignored-stale" };
178
+ }
179
+ return this.cas(current, nextProjection(current, { parent: completed.parent, execution: executionForParent(completed.parent), integrationBinding: completed.binding }));
180
+ }
181
+ async finalize(raw) {
182
+ const command = frontendSplitFinalizeCommandSchema.parse(raw);
183
+ const current = await this.loadCurrent(command);
184
+ if (!current.candidateBinding)
185
+ throw new Error("candidate binding is missing");
186
+ const childCloseoutFacts = [];
187
+ for (const child of current.candidateBinding.children) {
188
+ childCloseoutFacts.push(await this.ports.loadChildCloseoutFacts({ childId: child.childId, closeoutRef: child.closeoutRef }));
189
+ }
190
+ const aggregate = buildFrontendSplitAggregateCloseout(current.parent, current.manifest, current.candidateBinding, current.integrationBinding, childCloseoutFacts);
191
+ return this.ports.settleAggregate({
192
+ operationId: current.parent.operationId,
193
+ expectedRevision: current.revision,
194
+ expectedExecutionDigest: current.execution.digest,
195
+ expectedParentDigest: current.parent.digest,
196
+ expectedManifestDigest: current.manifest.digest,
197
+ expectedCandidateBindingDigest: current.candidateBinding.digest,
198
+ expectedIntegrationBindingDigest: current.integrationBinding?.digest,
199
+ childCloseoutFacts,
200
+ aggregate,
201
+ });
202
+ }
203
+ }