@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,438 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { z } from "zod";
3
+ import { sha256OfCanonicalJson } from "../../task/contract/hash.js";
4
+ import { appendJsonlLineAtomic, writeJsonlAtomic, } from "../../infrastructure/harness/atomic-write.js";
5
+ /**
6
+ * M1 typed event store (AC-001).
7
+ *
8
+ * A pure, side-effect-free event store for the frontend DAG refactor. The store
9
+ * distinguishes three phases — `staged`, `quarantined`, `committed` — and
10
+ * partitions records by `attemptId` so that sibling attempts cannot observe
11
+ * each other's facts. `committed` is the single terminal phase.
12
+ *
13
+ * Phase transition rules (enforced by the helpers below, not by loose field
14
+ * assignment):
15
+ * staged --adopt success--> committed (terminal)
16
+ * staged --validation fail--> quarantined
17
+ * quarantined --(no transition)--> (never visible to committed readers)
18
+ *
19
+ * This module only defines data + transitions. Persistence is delegated to the
20
+ * existing atomic-write boundary (`appendJsonlLineAtomic` / `writeJsonlAtomic`),
21
+ * and hashing reuses `sha256OfCanonicalJson` from the task-contract boundary so
22
+ * the fact identity is deterministic and reusable.
23
+ */
24
+ export const TYPED_EVENT_SCHEMA_VERSION = 1;
25
+ export const TYPED_EVENT_FACT_KINDS = [
26
+ "terminal-commit",
27
+ "disconnect",
28
+ "duplicate-tool-call",
29
+ "length-before-terminal",
30
+ "reasoning-timeout",
31
+ "provider-ended-without-terminal",
32
+ "read-only-fact-channel",
33
+ ];
34
+ /**
35
+ * M2 shadow-only fact catalog (AC-001 shadow dual write). These facts are
36
+ * audit-only observations derived from the authoritative text/JSON outputs of
37
+ * the frontend-contract-pi / frontend-scout-pi / frontend-plan-pi nodes. They
38
+ * are never branch-control signals and never participate in writer admission.
39
+ *
40
+ * Kept deliberately separate from `TYPED_EVENT_FACT_KINDS`: widening the shadow
41
+ * catalog must never widen the provider capability matrix (which stays frozen
42
+ * at its seven kinds). `target-surface` is shared by the scout and plan
43
+ * origins, so the union of the two catalogs is 12 unique kinds.
44
+ */
45
+ export const SHADOW_FACT_KINDS = [
46
+ "requirement",
47
+ "constraint",
48
+ "open-question",
49
+ "split-proposal",
50
+ "target-surface",
51
+ "design-evidence",
52
+ "component-choice",
53
+ "state-flow",
54
+ "data-flow",
55
+ "mock-api",
56
+ "design-deviation",
57
+ "dependency",
58
+ ];
59
+ /**
60
+ * M5 review terminal fact catalog (AC-001). These two committed typed tools are
61
+ * the authoritative terminal verdict of `frontend-review-pi`. They are kept in
62
+ * a dedicated catalog so widening the review terminal surface never widens the
63
+ * frozen 7/7/12 provider/shadow/event catalogs.
64
+ */
65
+ export const REVIEW_TERMINAL_FACT_KINDS = [
66
+ "approve_review",
67
+ "request_review_changes",
68
+ ];
69
+ /**
70
+ * M8 design terminal fact catalog. `frontend-design-review-pi` emits its
71
+ * authoritative terminal verdict through the committed typed tools
72
+ * `approve_design` / `request_design_changes` instead of the legacy first-line
73
+ * `VERDICT: pass|request-revision` text protocol. Kept in a dedicated catalog
74
+ * so widening the design terminal surface never widens the frozen provider /
75
+ * shadow / event / review catalogs.
76
+ */
77
+ export const DESIGN_TERMINAL_FACT_KINDS = [
78
+ "approve_design",
79
+ "request_design_changes",
80
+ ];
81
+ /**
82
+ * A+B: frontend-contract-pi incremental typed fact catalog. The contract node
83
+ * submits requirement/constraint/evidence-expectation/handoff-intent/
84
+ * open-question/split-proposal facts plus the `contract-finalized` terminal
85
+ * fact carrying the terminal disposition (`ready | ready-with-assumptions |
86
+ * blocked` + optional `blockingOwner`). Kept in a dedicated catalog so
87
+ * widening the contract surface never widens the frozen provider / shadow /
88
+ * event / review / design catalogs.
89
+ */
90
+ export const CONTRACT_FACT_KINDS = [
91
+ "requirement",
92
+ "constraint",
93
+ "evidence-expectation",
94
+ "handoff-intent",
95
+ "open-question",
96
+ "split-proposal",
97
+ "contract-finalized",
98
+ ];
99
+ export const CONTRACT_TERMINAL_FACT_KINDS = ["contract-finalized"];
100
+ export const contractDispositionSchema = z.enum([
101
+ "ready",
102
+ "ready-with-assumptions",
103
+ "blocked",
104
+ ]);
105
+ export const contractBlockingOwnerSchema = z.enum([
106
+ "blocked-human",
107
+ "blocked-external",
108
+ ]);
109
+ /**
110
+ * A+B: `frontend-plan-pi` terminal fact. The plan node now records its decision
111
+ * ledger through seven incremental `record_*` tools and closes with exactly one
112
+ * `finalize_plan` terminal fact.
113
+ */
114
+ export const PLAN_TERMINAL_FACT_KINDS = ["finalize_plan"];
115
+ /**
116
+ * A+B (AC-009): typed issue category shared by review and design change
117
+ * requests. The five-value enum replaces free-form issueCategory strings.
118
+ */
119
+ export const FRONTEND_ISSUE_CATEGORY_VALUES = [
120
+ "implementation-mismatch",
121
+ "approved-design-defect",
122
+ "target-surface-defect",
123
+ "contract-requirement-gap",
124
+ "unknown",
125
+ ];
126
+ export const frontendIssueCategorySchema = z.enum(FRONTEND_ISSUE_CATEGORY_VALUES);
127
+ /** Runtime control facts committed at frontend shape boundaries. Kept outside
128
+ * the frozen provider/shadow catalogs so adding control authority cannot widen
129
+ * either legacy capability surface. */
130
+ export const FRONTEND_SHAPE_FACT_KINDS = ["frontend-shape"];
131
+ export const typedEventPhaseSchema = z.enum([
132
+ "staged",
133
+ "quarantined",
134
+ "committed",
135
+ ]);
136
+ const ALL_TYPED_EVENT_FACT_KIND_VALUES = [
137
+ ...new Set([
138
+ ...TYPED_EVENT_FACT_KINDS,
139
+ ...SHADOW_FACT_KINDS,
140
+ ...REVIEW_TERMINAL_FACT_KINDS,
141
+ ...DESIGN_TERMINAL_FACT_KINDS,
142
+ ...FRONTEND_SHAPE_FACT_KINDS,
143
+ ...CONTRACT_FACT_KINDS,
144
+ ...PLAN_TERMINAL_FACT_KINDS,
145
+ ]),
146
+ ];
147
+ export const typedEventFactKindSchema = z.enum(ALL_TYPED_EVENT_FACT_KIND_VALUES);
148
+ /** A typed event fact is a `kind`-discriminated record. The schema stays
149
+ * permissive beyond `kind` (arbitrary JSON-safe payload) so provider/model
150
+ * shadow facts can carry their own fields, but `kind` is always validated. */
151
+ export const typedEventFactSchema = z
152
+ .record(z.unknown())
153
+ .superRefine((value, ctx) => {
154
+ if (!typedEventFactKindSchema.safeParse(value.kind).success) {
155
+ ctx.addIssue({
156
+ code: z.ZodIssueCode.custom,
157
+ message: "fact.kind must be a valid typed event fact kind",
158
+ path: ["kind"],
159
+ });
160
+ }
161
+ });
162
+ /** A single review finding shared by both typed terminal verdicts. Mirrors the
163
+ * legacy JSON review finding shape so typed↔JSON equivalence stays total. */
164
+ export const reviewFindingSchema = z
165
+ .object({
166
+ severity: z.enum(["Critical", "Important", "Minor", "Info"]),
167
+ file: z.string().min(1).optional(),
168
+ line: z.number().int().positive().optional(),
169
+ issue: z.string().min(1),
170
+ requiredChange: z.string().min(1).optional(),
171
+ })
172
+ .strict();
173
+ /** `approve_review` terminal fact payload. Approval may carry informational
174
+ * findings (Minor/Info) but never Critical/Important — the same rule the legacy
175
+ * JSON verdict applies to `verdict: "pass"`. */
176
+ export const approveReviewFactSchema = z
177
+ .object({
178
+ kind: z.literal("approve_review"),
179
+ verdict: z.literal("approve_review"),
180
+ findings: z.array(reviewFindingSchema),
181
+ })
182
+ .strict()
183
+ .superRefine((value, ctx) => {
184
+ if (value.findings.some((finding) => finding.severity === "Critical" ||
185
+ finding.severity === "Important")) {
186
+ ctx.addIssue({
187
+ code: z.ZodIssueCode.custom,
188
+ message: "approve_review cannot carry Critical or Important findings",
189
+ path: ["findings"],
190
+ });
191
+ }
192
+ });
193
+ /** `request_review_changes` terminal fact payload. Requires a typed issue
194
+ * category, at least one evidence ref, and at least one finding. */
195
+ export const requestReviewChangesFactSchema = z
196
+ .object({
197
+ kind: z.literal("request_review_changes"),
198
+ verdict: z.literal("request_review_changes"),
199
+ issueCategory: frontendIssueCategorySchema,
200
+ evidenceRefs: z.array(z.string().min(1)).min(1),
201
+ findings: z.array(reviewFindingSchema).min(1),
202
+ })
203
+ .strict();
204
+ /** `approve_design` terminal fact payload. Approval may carry informational
205
+ * findings (Minor/Info) but never Critical/Important — the same rule the legacy
206
+ * text protocol applies to `VERDICT: pass`. */
207
+ export const approveDesignFactSchema = z
208
+ .object({
209
+ kind: z.literal("approve_design"),
210
+ verdict: z.literal("approve_design"),
211
+ findings: z.array(reviewFindingSchema),
212
+ })
213
+ .strict()
214
+ .superRefine((value, ctx) => {
215
+ if (value.findings.some((finding) => finding.severity === "Critical" ||
216
+ finding.severity === "Important")) {
217
+ ctx.addIssue({
218
+ code: z.ZodIssueCode.custom,
219
+ message: "approve_design cannot carry Critical or Important findings",
220
+ path: ["findings"],
221
+ });
222
+ }
223
+ });
224
+ /** `request_design_changes` terminal fact payload. Requires a typed issue
225
+ * category, at least one evidence ref, and at least one finding. */
226
+ export const requestDesignChangesFactSchema = z
227
+ .object({
228
+ kind: z.literal("request_design_changes"),
229
+ verdict: z.literal("request_design_changes"),
230
+ issueCategory: frontendIssueCategorySchema,
231
+ evidenceRefs: z.array(z.string().min(1)).min(1),
232
+ findings: z.array(reviewFindingSchema).min(1),
233
+ })
234
+ .strict();
235
+ export const typedEventRecordSchema = z
236
+ .object({
237
+ schemaVersion: z.literal(TYPED_EVENT_SCHEMA_VERSION),
238
+ eventId: z.string().min(1),
239
+ requestId: z.string().min(1),
240
+ attemptId: z.string().min(1),
241
+ phase: typedEventPhaseSchema,
242
+ payloadSha256: z.string().regex(/^[a-f0-9]{64}$/),
243
+ fact: typedEventFactSchema,
244
+ revision: z.number().int().nonnegative(),
245
+ recordedAt: z.string().min(1),
246
+ quarantineReason: z.string().min(1).optional(),
247
+ })
248
+ .strict()
249
+ .superRefine((value, ctx) => {
250
+ if (value.phase === "quarantined" && !value.quarantineReason) {
251
+ ctx.addIssue({
252
+ code: z.ZodIssueCode.custom,
253
+ message: "quarantined event requires a quarantineReason",
254
+ path: ["quarantineReason"],
255
+ });
256
+ }
257
+ });
258
+ /** A fact kind that closes the attempt. Only one such fact may commit per
259
+ * attempt; the transaction layer's terminal conflict gate depends on this. */
260
+ export function isTerminalFactKind(kind) {
261
+ return (kind === "terminal-commit" ||
262
+ kind === "provider-ended-without-terminal" ||
263
+ REVIEW_TERMINAL_FACT_KINDS.includes(kind) ||
264
+ DESIGN_TERMINAL_FACT_KINDS.includes(kind) ||
265
+ PLAN_TERMINAL_FACT_KINDS.includes(kind) ||
266
+ CONTRACT_TERMINAL_FACT_KINDS.includes(kind));
267
+ }
268
+ /** Deterministic fact identity shared by staging and adoption. */
269
+ export function typedEventPayloadSha256(fact) {
270
+ return sha256OfCanonicalJson(fact);
271
+ }
272
+ export class TypedEventStoreError extends Error {
273
+ code;
274
+ constructor(code, message) {
275
+ super(message);
276
+ this.name = "TypedEventStoreError";
277
+ this.code = code;
278
+ }
279
+ }
280
+ export function createTypedEventStore(initial) {
281
+ return {
282
+ revision: initial?.revision ?? 0,
283
+ records: [...(initial?.records ?? [])],
284
+ };
285
+ }
286
+ function replaceRecord(store, eventId, next) {
287
+ const index = store.records.findIndex((record) => record.eventId === eventId);
288
+ if (index < 0) {
289
+ throw new TypedEventStoreError("EVENT_NOT_FOUND", `no typed event with eventId ${eventId}`);
290
+ }
291
+ store.records[index] = next;
292
+ }
293
+ /** Stage a fact as `staged` without any commit decision. */
294
+ export function stageTypedEventRecord(store, input) {
295
+ const existing = store.records.find((record) => record.eventId === input.eventId);
296
+ if (existing) {
297
+ throw new TypedEventStoreError("EVENT_ID_CONFLICT", `eventId ${input.eventId} already exists with phase ${existing.phase}`);
298
+ }
299
+ const record = typedEventRecordSchema.parse({
300
+ schemaVersion: TYPED_EVENT_SCHEMA_VERSION,
301
+ eventId: input.eventId,
302
+ requestId: input.requestId,
303
+ attemptId: input.attemptId,
304
+ phase: "staged",
305
+ payloadSha256: typedEventPayloadSha256(input.fact),
306
+ fact: input.fact,
307
+ revision: 0,
308
+ recordedAt: input.recordedAt ?? new Date().toISOString(),
309
+ });
310
+ store.records.push(record);
311
+ return record;
312
+ }
313
+ /** Quarantine a staged event after validation failure. */
314
+ export function quarantineTypedEventRecord(store, eventId, reason) {
315
+ const record = store.records.find((candidate) => candidate.eventId === eventId);
316
+ if (!record) {
317
+ throw new TypedEventStoreError("EVENT_NOT_FOUND", `no typed event with eventId ${eventId}`);
318
+ }
319
+ if (record.phase !== "staged") {
320
+ throw new TypedEventStoreError("INVALID_PHASE_TRANSITION", `event ${eventId} is ${record.phase}, not staged`);
321
+ }
322
+ const next = typedEventRecordSchema.parse({
323
+ ...record,
324
+ phase: "quarantined",
325
+ quarantineReason: reason,
326
+ });
327
+ replaceRecord(store, eventId, next);
328
+ return next;
329
+ }
330
+ /** Commit a staged event at `revision`. Committed is terminal. */
331
+ export function commitTypedEventRecord(store, eventId, revision) {
332
+ const record = store.records.find((candidate) => candidate.eventId === eventId);
333
+ if (!record) {
334
+ throw new TypedEventStoreError("EVENT_NOT_FOUND", `no typed event with eventId ${eventId}`);
335
+ }
336
+ if (record.phase !== "staged") {
337
+ throw new TypedEventStoreError("INVALID_PHASE_TRANSITION", `event ${eventId} is ${record.phase}, not staged`);
338
+ }
339
+ const { quarantineReason: _reason, ...rest } = record;
340
+ void _reason;
341
+ const next = typedEventRecordSchema.parse({
342
+ ...rest,
343
+ phase: "committed",
344
+ revision,
345
+ });
346
+ replaceRecord(store, eventId, next);
347
+ return next;
348
+ }
349
+ /** Explicitly adopt a quarantined event as committed at `revision`. This is the
350
+ * only quarantined → committed transition and is reserved for the explicit
351
+ * `adopt_staged_fact` path (a later attempt adopts a fact a prior attempt
352
+ * quarantined). Committed is terminal; the quarantine reason is dropped. */
353
+ export function commitQuarantinedTypedEventRecord(store, eventId, revision) {
354
+ const record = store.records.find((candidate) => candidate.eventId === eventId);
355
+ if (!record) {
356
+ throw new TypedEventStoreError("EVENT_NOT_FOUND", `no typed event with eventId ${eventId}`);
357
+ }
358
+ if (record.phase !== "quarantined") {
359
+ throw new TypedEventStoreError("INVALID_PHASE_TRANSITION", `event ${eventId} is ${record.phase}, not quarantined`);
360
+ }
361
+ const { quarantineReason: _reason, ...rest } = record;
362
+ void _reason;
363
+ const next = typedEventRecordSchema.parse({
364
+ ...rest,
365
+ phase: "committed",
366
+ revision,
367
+ });
368
+ replaceRecord(store, eventId, next);
369
+ return next;
370
+ }
371
+ /** Read committed events only. Staged/quarantined events are never returned
372
+ * here (AC-001: quarantine facts must not leak downstream). */
373
+ export function readCommittedEvents(store, attemptId) {
374
+ return store.records.filter((record) => record.phase === "committed" &&
375
+ (attemptId === undefined || record.attemptId === attemptId));
376
+ }
377
+ /** Read events for a single attempt, optionally filtered by phase. */
378
+ export function readEventsByAttempt(store, attemptId, options) {
379
+ const phases = options?.phases;
380
+ return store.records.filter((record) => record.attemptId === attemptId &&
381
+ (!phases || phases.includes(record.phase)));
382
+ }
383
+ /** Find a record by requestId, optionally scoped to phase/attempt. */
384
+ export function findEventByRequestId(store, requestId, options) {
385
+ return store.records.find((record) => record.requestId === requestId &&
386
+ (options?.phase === undefined || record.phase === options.phase) &&
387
+ (options?.attemptId === undefined ||
388
+ record.attemptId === options.attemptId));
389
+ }
390
+ export function findCommittedEventByRequestId(store, requestId) {
391
+ return findEventByRequestId(store, requestId, { phase: "committed" });
392
+ }
393
+ /** Whether the attempt already has a committed terminal fact. */
394
+ export function hasCommittedTerminalEvent(store, attemptId) {
395
+ return store.records.some((record) => record.attemptId === attemptId &&
396
+ record.phase === "committed" &&
397
+ isTerminalFactKind(record.fact.kind));
398
+ }
399
+ /* ------------------------------------------------------------------ *
400
+ * Persistence (JSONL). Reuses the atomic-write boundary so every write *
401
+ * is either fully present or absent. *
402
+ * ------------------------------------------------------------------ */
403
+ function parseTypedEventRecordLine(line) {
404
+ return typedEventRecordSchema.parse(JSON.parse(line));
405
+ }
406
+ export async function readTypedEventStoreFromJsonl(filePath) {
407
+ let raw;
408
+ try {
409
+ raw = await readFile(filePath, "utf8");
410
+ }
411
+ catch (error) {
412
+ if (error &&
413
+ typeof error === "object" &&
414
+ "code" in error &&
415
+ error.code === "ENOENT") {
416
+ return [];
417
+ }
418
+ throw error;
419
+ }
420
+ return raw
421
+ .split("\n")
422
+ .map((line) => line.trim())
423
+ .filter(Boolean)
424
+ .map((line) => parseTypedEventRecordLine(line));
425
+ }
426
+ /** Load a store from disk, deriving `revision` from the highest committed
427
+ * record revision. */
428
+ export async function loadTypedEventStore(filePath) {
429
+ const records = await readTypedEventStoreFromJsonl(filePath);
430
+ const revision = records.reduce((max, record) => Math.max(max, record.revision), 0);
431
+ return { revision, records };
432
+ }
433
+ export async function appendTypedEventRecordJsonl(filePath, record) {
434
+ await appendJsonlLineAtomic(filePath, record);
435
+ }
436
+ export async function writeTypedEventStoreJsonl(filePath, records) {
437
+ await writeJsonlAtomic(filePath, records);
438
+ }
@@ -0,0 +1,180 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { commitQuarantinedTypedEventRecord, commitTypedEventRecord, findCommittedEventByRequestId, hasCommittedTerminalEvent, stageTypedEventRecord, typedEventPayloadSha256, } from "./frontend-typed-event-store.js";
3
+ export class TypedEventTransactionError extends Error {
4
+ code;
5
+ details;
6
+ constructor(code, message, details) {
7
+ super(message);
8
+ this.name = "TypedEventTransactionError";
9
+ this.code = code;
10
+ this.details = details;
11
+ }
12
+ }
13
+ /** Stage a fact without committing. Returns the eventId for later adoption. */
14
+ export function stageTypedEventFact(options) {
15
+ const eventId = options.eventId ?? randomUUID();
16
+ const record = stageTypedEventRecord(options.store, {
17
+ eventId,
18
+ requestId: options.requestId,
19
+ attemptId: options.attemptId,
20
+ fact: options.fact,
21
+ recordedAt: (options.now ?? new Date()).toISOString(),
22
+ });
23
+ return { eventId, record };
24
+ }
25
+ function resolveReplayOrConflict(options, existing) {
26
+ const expectedPayload = typedEventPayloadSha256(options.fact);
27
+ if (existing.payloadSha256 !== expectedPayload) {
28
+ throw new TypedEventTransactionError("REQUEST_ID_REUSE_CONFLICT", `requestId ${options.requestId} was used with a different payload hash`, { existingPayloadSha256: existing.payloadSha256, expectedPayload });
29
+ }
30
+ return {
31
+ ok: true,
32
+ eventId: existing.eventId,
33
+ record: existing,
34
+ idempotentReplay: true,
35
+ revision: existing.revision,
36
+ };
37
+ }
38
+ /**
39
+ * Explicitly adopt a staged fact as committed.
40
+ *
41
+ * Order of checks matters and is deterministic:
42
+ * 1. requestId idempotency (pre-check + re-check inside the critical section);
43
+ * 2. terminal conflict gate;
44
+ * 3. revision drift;
45
+ * 4. staged-record integrity;
46
+ * 5. commit (single logical commit point) + optional post-commit hook.
47
+ */
48
+ export async function adoptTypedEventFact(options) {
49
+ const defaultRunExclusive = async (fn) => {
50
+ return await fn();
51
+ };
52
+ const runExclusive = options.runExclusive ?? defaultRunExclusive;
53
+ const preExisting = findCommittedEventByRequestId(options.store, options.requestId);
54
+ if (preExisting)
55
+ return resolveReplayOrConflict(options, preExisting);
56
+ return runExclusive(async () => {
57
+ const underLock = findCommittedEventByRequestId(options.store, options.requestId);
58
+ if (underLock)
59
+ return resolveReplayOrConflict(options, underLock);
60
+ if (hasCommittedTerminalEvent(options.store, options.attemptId)) {
61
+ throw new TypedEventTransactionError("TERMINAL_CONFLICT", `attempt ${options.attemptId} already has a committed terminal fact; refusing late commit of requestId ${options.requestId}`);
62
+ }
63
+ if (options.store.revision !== options.expectedRevision) {
64
+ throw new TypedEventTransactionError("REVISION_CONFLICT", `expectedRevision ${options.expectedRevision} does not match store revision ${options.store.revision}`, {
65
+ expectedRevision: options.expectedRevision,
66
+ currentRevision: options.store.revision,
67
+ });
68
+ }
69
+ const staged = options.store.records.find((record) => record.eventId === options.eventId);
70
+ if (!staged) {
71
+ throw new TypedEventTransactionError("EVENT_NOT_FOUND", `no typed event with eventId ${options.eventId}`);
72
+ }
73
+ if (staged.phase !== "staged") {
74
+ throw new TypedEventTransactionError("EVENT_NOT_STAGED", `event ${options.eventId} is ${staged.phase}, not staged`);
75
+ }
76
+ if (staged.requestId !== options.requestId ||
77
+ staged.attemptId !== options.attemptId) {
78
+ throw new TypedEventTransactionError("EVENT_MISMATCH", `staged event ${options.eventId} does not match requestId/attemptId`);
79
+ }
80
+ if (staged.payloadSha256 !== typedEventPayloadSha256(options.fact)) {
81
+ throw new TypedEventTransactionError("EVENT_PAYLOAD_MISMATCH", `staged event ${options.eventId} payload hash does not match the adoption fact`);
82
+ }
83
+ const nextRevision = options.expectedRevision + 1;
84
+ const record = commitTypedEventRecord(options.store, options.eventId, nextRevision);
85
+ options.store.revision = nextRevision;
86
+ if (options.afterCommit) {
87
+ try {
88
+ await options.afterCommit({ record, revision: nextRevision });
89
+ }
90
+ catch (error) {
91
+ throw new TypedEventTransactionError("POST_COMMIT_ERROR", `post-commit step failed after requestId ${options.requestId} was committed: ${error instanceof Error ? error.message : String(error)}`);
92
+ }
93
+ }
94
+ return {
95
+ ok: true,
96
+ eventId: options.eventId,
97
+ record,
98
+ idempotentReplay: false,
99
+ revision: nextRevision,
100
+ };
101
+ });
102
+ }
103
+ /**
104
+ * A+B (AC-005): explicitly adopt a quarantined (or still-staged) fact from a
105
+ * previous attempt into the committed ledger. This is the `adopt_staged_fact`
106
+ * semantics: a fact that failed validation and was quarantined becomes visible
107
+ * to committed readers only through an explicit adopt by a later attempt.
108
+ *
109
+ * Reuses the existing adoption double-check (requestId idempotency, terminal
110
+ * conflict gate, revision CAS) without re-staging: the fact payload is the
111
+ * already-recorded staged/quarantined payload, so adoption cannot mutate it.
112
+ */
113
+ export async function adoptStagedFact(options) {
114
+ const defaultRunExclusive = async (fn) => {
115
+ return await fn();
116
+ };
117
+ const runExclusive = options.runExclusive ?? defaultRunExclusive;
118
+ const preExisting = findCommittedEventByRequestId(options.store, options.requestId);
119
+ if (preExisting) {
120
+ return {
121
+ ok: true,
122
+ eventId: preExisting.eventId,
123
+ record: preExisting,
124
+ idempotentReplay: true,
125
+ revision: preExisting.revision,
126
+ };
127
+ }
128
+ return runExclusive(async () => {
129
+ const underLock = findCommittedEventByRequestId(options.store, options.requestId);
130
+ if (underLock) {
131
+ return {
132
+ ok: true,
133
+ eventId: underLock.eventId,
134
+ record: underLock,
135
+ idempotentReplay: true,
136
+ revision: underLock.revision,
137
+ };
138
+ }
139
+ if (hasCommittedTerminalEvent(options.store, options.attemptId)) {
140
+ throw new TypedEventTransactionError("TERMINAL_CONFLICT", `attempt ${options.attemptId} already has a committed terminal fact; refusing late adoption of requestId ${options.requestId}`);
141
+ }
142
+ if (options.store.revision !== options.expectedRevision) {
143
+ throw new TypedEventTransactionError("REVISION_CONFLICT", `expectedRevision ${options.expectedRevision} does not match store revision ${options.store.revision}`, {
144
+ expectedRevision: options.expectedRevision,
145
+ currentRevision: options.store.revision,
146
+ });
147
+ }
148
+ const existing = options.store.records.find((record) => record.eventId === options.eventId);
149
+ if (!existing) {
150
+ throw new TypedEventTransactionError("EVENT_NOT_FOUND", `no typed event with eventId ${options.eventId}`);
151
+ }
152
+ if (existing.phase !== "staged" && existing.phase !== "quarantined") {
153
+ throw new TypedEventTransactionError("EVENT_NOT_STAGED", `event ${options.eventId} is ${existing.phase}, not staged/quarantined`);
154
+ }
155
+ if (existing.requestId !== options.requestId ||
156
+ existing.attemptId !== options.attemptId) {
157
+ throw new TypedEventTransactionError("EVENT_MISMATCH", `staged event ${options.eventId} does not match requestId/attemptId`);
158
+ }
159
+ const nextRevision = options.expectedRevision + 1;
160
+ const record = existing.phase === "quarantined"
161
+ ? commitQuarantinedTypedEventRecord(options.store, options.eventId, nextRevision)
162
+ : commitTypedEventRecord(options.store, options.eventId, nextRevision);
163
+ options.store.revision = nextRevision;
164
+ if (options.afterCommit) {
165
+ try {
166
+ await options.afterCommit({ record, revision: nextRevision });
167
+ }
168
+ catch (error) {
169
+ throw new TypedEventTransactionError("POST_COMMIT_ERROR", `post-commit step failed after requestId ${options.requestId} was committed: ${error instanceof Error ? error.message : String(error)}`);
170
+ }
171
+ }
172
+ return {
173
+ ok: true,
174
+ eventId: options.eventId,
175
+ record,
176
+ idempotentReplay: false,
177
+ revision: nextRevision,
178
+ };
179
+ });
180
+ }