@cassiomc1/forgeloop 1.13.0 → 2.0.0-rc.2

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 (395) hide show
  1. package/AGENT_COMPATIBILITY.md +8 -0
  2. package/CONTRIBUTING.md +11 -7
  3. package/DOCS_INDEX.md +37 -4
  4. package/ENG/c-development-eng.md +2 -2
  5. package/ENG/cpp-development-eng.md +2 -2
  6. package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
  7. package/ENG/flutter-development-eng.md +4 -2
  8. package/ENG/go-development-eng.md +2 -2
  9. package/ENG/java-development-eng.md +3 -3
  10. package/ENG/nodejs-backend-development-eng.md +10 -6
  11. package/ENG/php-development-eng.md +3 -3
  12. package/ENG/python-development-eng.md +205 -0
  13. package/ENG/rust-development-eng.md +42 -1
  14. package/ENG/sec-code-eng.md +7 -7
  15. package/ENG/sql-development-eng.md +4 -4
  16. package/ENG/swift-development-eng.md +5 -4
  17. package/ENG/test-code-eng.md +1 -1
  18. package/ENG/typescript-development-eng.md +4 -4
  19. package/EXECUTION_STATE.md +12 -0
  20. package/GUIDE_ROUTER.md +6 -0
  21. package/LICENSE +133 -21
  22. package/LOOP_ENGINEERING.md +28 -2
  23. package/LOOP_SYSTEM_DESIGN.md +10 -10
  24. package/ORCHESTRATOR_INTEGRATION.md +9 -5
  25. package/PROTOCOL_INTEGRATION.md +55 -2
  26. package/README.md +63 -59
  27. package/TERMINOLOGY.md +2 -0
  28. package/THIRD_PARTY_NOTICES.md +3 -4
  29. package/THREAT_MODEL.md +140 -1
  30. package/completions/_forgeloop +29 -3
  31. package/completions/forgeloop.bash +55 -3
  32. package/completions/forgeloop.fish +183 -1
  33. package/docs/ADVISORY_CONTEXT.md +25 -0
  34. package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
  35. package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
  36. package/docs/AGENT_PROTOCOL_SUMMARY.md +74 -39
  37. package/docs/AGENT_SKILL.md +66 -0
  38. package/docs/ARTIFACT_REFERENCE.md +171 -35
  39. package/docs/AUDIT_UX.md +46 -0
  40. package/docs/BROWSER_VERIFICATION.md +136 -0
  41. package/docs/CLI_REFERENCE.md +512 -8
  42. package/docs/CODE_ATTESTATION.md +2 -2
  43. package/docs/DOCUMENTATION_GUIDE.md +34 -11
  44. package/docs/GETTING_STARTED.md +3 -3
  45. package/docs/JEV_BENCHMARKS.md +31 -0
  46. package/docs/MODEL_ROUTING.md +37 -0
  47. package/docs/OPENSRC_ADAPTER.md +241 -0
  48. package/docs/PACKAGE_CONTENTS.md +46 -18
  49. package/docs/PROVIDERS.md +126 -0
  50. package/docs/PROVIDER_ARCHITECTURE.md +199 -0
  51. package/docs/RECIPES.md +9 -0
  52. package/docs/RELEASE_CHECKLIST.md +116 -12
  53. package/docs/SECURITY_REVIEW.md +71 -0
  54. package/docs/SELF_HOSTED_TESTS.md +121 -0
  55. package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
  56. package/docs/TEST_INTELLIGENCE.md +29 -0
  57. package/docs/TEST_PRUNING.md +14 -0
  58. package/docs/TROUBLESHOOTING.md +198 -1
  59. package/docs/UNIVERSAL_INTEGRATION.md +31 -0
  60. package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
  61. package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
  62. package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
  63. package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
  64. package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
  65. package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
  66. package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
  67. package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
  68. package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
  69. package/docs/diagrams/README.md +13 -9
  70. package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
  71. package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
  72. package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
  73. package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
  74. package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
  75. package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
  76. package/docs/documentation-manifest.json +774 -16
  77. package/docs/protocol-requirements.json +24 -0
  78. package/package.json +43 -11
  79. package/schemas/config.schema.json +14 -0
  80. package/schemas/context-plan.schema.json +18 -0
  81. package/schemas/semantic-decision.schema.json +46 -0
  82. package/schemas/task-bundle.schema.json +7 -2
  83. package/schemas/test-utility.schema.json +44 -0
  84. package/scripts/CI_VALIDATORS.md +6 -6
  85. package/scripts/benchmark-jev.mjs +5 -0
  86. package/scripts/benchmark-test-intelligence.mjs +4 -0
  87. package/scripts/generate-agent-protocol-summary.mjs +6 -3
  88. package/scripts/generate-forgeloop-skill.mjs +133 -0
  89. package/scripts/jev-smoke.mjs +19 -0
  90. package/scripts/run-mcp-clean-tests.mjs +30 -0
  91. package/skills/forgeloop/README.md +9 -0
  92. package/skills/forgeloop/SKILL.md +77 -0
  93. package/skills/forgeloop/references/lifecycle.md +9 -0
  94. package/skills/forgeloop/references/recovery.md +7 -0
  95. package/skills/forgeloop/references/verification.md +7 -0
  96. package/src/adapters/agent-browser/assertions.js +47 -0
  97. package/src/adapters/agent-browser/commands.js +54 -0
  98. package/src/adapters/agent-browser/index.js +3 -0
  99. package/src/adapters/agent-browser/locator.js +40 -0
  100. package/src/adapters/agent-browser/process.js +215 -0
  101. package/src/adapters/agent-browser/provider.js +316 -0
  102. package/src/adapters/emulated-services/constants.js +24 -0
  103. package/src/adapters/emulated-services/index.js +7 -0
  104. package/src/adapters/emulated-services/process.js +162 -0
  105. package/src/adapters/emulated-services/provider.js +289 -0
  106. package/src/adapters/opensrc/normalize.js +90 -0
  107. package/src/adapters/opensrc/process.js +248 -0
  108. package/src/adapters/opensrc/provider.js +338 -0
  109. package/src/adapters/opensrc/search.js +264 -0
  110. package/src/adapters/temporary-paths.js +18 -0
  111. package/src/adapters/typesafe/client.js +28 -0
  112. package/src/adapters/typesafe/engine.js +63 -0
  113. package/src/adapters/typesafe/normalize.js +41 -0
  114. package/src/cli.js +218 -174
  115. package/src/commands/action-show.js +5 -2
  116. package/src/commands/advance.js +5 -1
  117. package/src/commands/baseline.js +13 -11
  118. package/src/commands/checkpoint-revalidate.js +192 -0
  119. package/src/commands/complete.js +19 -3
  120. package/src/commands/context-plan.js +38 -0
  121. package/src/commands/contract-create.js +266 -0
  122. package/src/commands/contract-revise.js +236 -0
  123. package/src/commands/decision-show.js +14 -0
  124. package/src/commands/decision-status.js +22 -0
  125. package/src/commands/discover.js +41 -0
  126. package/src/commands/doctor.js +51 -13
  127. package/src/commands/gate-record.js +194 -0
  128. package/src/commands/gate-revalidate.js +135 -0
  129. package/src/commands/handoff-list.js +6 -1
  130. package/src/commands/handoff-show.js +6 -1
  131. package/src/commands/migrate-protocol.js +2 -2
  132. package/src/commands/model-route.js +40 -0
  133. package/src/commands/next.js +7 -1
  134. package/src/commands/policy-diff.js +25 -4
  135. package/src/commands/progress.js +17 -11
  136. package/src/commands/record-diagnosis.js +3 -0
  137. package/src/commands/route.js +146 -18
  138. package/src/commands/semantic-plan.js +41 -0
  139. package/src/commands/status.js +6 -1
  140. package/src/commands/storage-backup.js +15 -0
  141. package/src/commands/storage-migrate.js +5 -0
  142. package/src/commands/storage-migration-resume.js +5 -0
  143. package/src/commands/storage-migration-status.js +44 -0
  144. package/src/commands/storage-restore-resume.js +7 -0
  145. package/src/commands/storage-restore.js +8 -0
  146. package/src/commands/task-abandon.js +224 -0
  147. package/src/commands/task-list.js +8 -43
  148. package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
  149. package/src/commands/task-migrate.js +3 -3
  150. package/src/commands/task-recover.js +10 -2
  151. package/src/commands/task-repair-contract-bootstrap.js +263 -0
  152. package/src/commands/task-scope.js +18 -2
  153. package/src/commands/task-show.js +16 -2
  154. package/src/commands/test-inventory.js +5 -0
  155. package/src/commands/test-prune-plan.js +5 -0
  156. package/src/commands/test-prune-probe.js +5 -0
  157. package/src/commands/test-utility.js +5 -0
  158. package/src/commands/validate-protocol.js +39 -15
  159. package/src/commands/validate-receipt.js +8 -1
  160. package/src/config/guides.json +4 -0
  161. package/src/core/action-ledger-projection.js +10 -3
  162. package/src/core/actions.js +38 -75
  163. package/src/core/activation.js +11 -3
  164. package/src/core/approvals.js +17 -51
  165. package/src/core/artifact-registry.js +83 -2
  166. package/src/core/artifacts.js +81 -27
  167. package/src/core/attachment-paths.js +45 -0
  168. package/src/core/attestation-coverage.js +6 -1
  169. package/src/core/attestation-verifier.js +21 -2
  170. package/src/core/attestation.js +17 -6
  171. package/src/core/audit-ux.js +514 -0
  172. package/src/core/audit.js +6 -1
  173. package/src/core/browser-verification/constants.js +149 -0
  174. package/src/core/browser-verification/normalize.js +254 -0
  175. package/src/core/browser-verification/provider.js +519 -0
  176. package/src/core/browser-verification/service.js +115 -0
  177. package/src/core/bundles.js +160 -66
  178. package/src/core/checkpoint-revalidation.js +321 -0
  179. package/src/core/cli-command-definitions.js +350 -7
  180. package/src/core/code-manifest.js +9 -3
  181. package/src/core/command-executors.js +274 -108
  182. package/src/core/command-input.js +136 -44
  183. package/src/core/completion-artifacts.js +85 -34
  184. package/src/core/completion-ownership.js +19 -8
  185. package/src/core/completion-recovery-rebind.js +11 -1
  186. package/src/core/completion.js +64 -15
  187. package/src/core/config.js +3 -0
  188. package/src/core/conformance.js +2 -1
  189. package/src/core/context-compiler/budget.js +9 -0
  190. package/src/core/context-compiler/candidates.js +39 -0
  191. package/src/core/context-compiler/compiler.js +63 -0
  192. package/src/core/context-compiler/fingerprint.js +11 -0
  193. package/src/core/context-compiler/policy.js +13 -0
  194. package/src/core/context-compiler/result.js +23 -0
  195. package/src/core/continuity-lint.js +13 -2
  196. package/src/core/continuity-reconciliation.js +16 -3
  197. package/src/core/continuity.js +16 -8
  198. package/src/core/contract-bootstrap-recovery.js +656 -0
  199. package/src/core/contract-revision.js +209 -0
  200. package/src/core/decision/artifact.js +70 -0
  201. package/src/core/decision/benchmarks.js +103 -0
  202. package/src/core/decision/cache.js +27 -0
  203. package/src/core/decision/constants.js +58 -0
  204. package/src/core/decision/cutover.js +34 -0
  205. package/src/core/decision/engine.js +22 -0
  206. package/src/core/decision/errors.js +68 -0
  207. package/src/core/decision/events.js +104 -0
  208. package/src/core/decision/freshness.js +19 -0
  209. package/src/core/decision/normalizers/index.js +115 -0
  210. package/src/core/decision/policy.js +18 -0
  211. package/src/core/decision/projection.js +16 -0
  212. package/src/core/decision/question-registry.js +201 -0
  213. package/src/core/decision/request.js +26 -0
  214. package/src/core/decision/resolver.js +138 -0
  215. package/src/core/decision/result.js +58 -0
  216. package/src/core/decision/service.js +181 -0
  217. package/src/core/decision/state-builder.js +65 -0
  218. package/src/core/decision/task-bindings.js +35 -0
  219. package/src/core/decision/test-provider.js +32 -0
  220. package/src/core/decision/thresholds.js +15 -0
  221. package/src/core/diagnosis-model.js +11 -0
  222. package/src/core/diagnosis.js +10 -1
  223. package/src/core/diagnostic-projection.js +29 -12
  224. package/src/core/diagnostic-record.js +28 -3
  225. package/src/core/efficiency.js +6 -1
  226. package/src/core/error-codes.js +278 -0
  227. package/src/core/events.js +535 -147
  228. package/src/core/evidence-readiness.js +9 -0
  229. package/src/core/execution-prerequisites.js +36 -13
  230. package/src/core/execution-profile.js +63 -38
  231. package/src/core/execution.js +26 -40
  232. package/src/core/filesystem.js +11 -0
  233. package/src/core/gate-artifact.js +105 -19
  234. package/src/core/gate-provenance.js +125 -0
  235. package/src/core/handoff.js +26 -14
  236. package/src/core/history.js +39 -35
  237. package/src/core/hypothesis-projection.js +32 -0
  238. package/src/core/information-gain-projection.js +104 -100
  239. package/src/core/inspect.js +12 -3
  240. package/src/core/integration-invocation-policy.js +37 -4
  241. package/src/core/integration-resources.js +119 -87
  242. package/src/core/ledger-event-collection.js +189 -0
  243. package/src/core/ledger-relations.js +23 -0
  244. package/src/core/model-router/constants.js +10 -0
  245. package/src/core/model-router/policy.js +103 -0
  246. package/src/core/model-router/router.js +37 -0
  247. package/src/core/native-storage.js +12 -0
  248. package/src/core/next-action-continuity.js +31 -9
  249. package/src/core/next-action-model.js +58 -0
  250. package/src/core/next-action-phases.js +131 -43
  251. package/src/core/next-action-refresh.js +43 -9
  252. package/src/core/next-action-review-phase.js +9 -4
  253. package/src/core/next-action-verification-phase.js +2 -1
  254. package/src/core/next-action.js +56 -9
  255. package/src/core/phase.js +266 -103
  256. package/src/core/policy-engine.js +12 -13
  257. package/src/core/preflight-consistency.js +89 -62
  258. package/src/core/preflight-loaders.js +48 -9
  259. package/src/core/preflight-model.js +6 -1
  260. package/src/core/preflight.js +29 -12
  261. package/src/core/progress.js +21 -10
  262. package/src/core/protocol-info.js +68 -0
  263. package/src/core/protocol-migration.js +7 -6
  264. package/src/core/protocol.js +20 -0
  265. package/src/core/reconcile-closure.js +281 -67
  266. package/src/core/recovery-history.js +67 -47
  267. package/src/core/reflection.js +72 -77
  268. package/src/core/responsibility.js +25 -13
  269. package/src/core/resumability.js +154 -44
  270. package/src/core/route-artifact.js +28 -4
  271. package/src/core/router.js +67 -1
  272. package/src/core/runtime-context.js +123 -61
  273. package/src/core/schema-validation.js +11 -3
  274. package/src/core/security-review/constants.js +64 -0
  275. package/src/core/security-review/normalize.js +245 -0
  276. package/src/core/security-review/provider.js +204 -0
  277. package/src/core/security-review/service.js +134 -0
  278. package/src/core/semantic-planning/constants.js +19 -0
  279. package/src/core/semantic-planning/projection.js +94 -0
  280. package/src/core/semantic-planning/service.js +15 -0
  281. package/src/core/settlement.js +12 -1
  282. package/src/core/signing/bundle-file.js +53 -0
  283. package/src/core/signing/bundle-reference.js +15 -0
  284. package/src/core/signing/sigstore.js +60 -33
  285. package/src/core/signing/statement-file.js +27 -0
  286. package/src/core/sources.js +37 -0
  287. package/src/core/storage-runtime-registry.js +41 -0
  288. package/src/core/structural-quality/artifacts.js +20 -14
  289. package/src/core/structural-quality/service.js +81 -56
  290. package/src/core/target-layout.js +1 -1
  291. package/src/core/task-claim-state.js +249 -15
  292. package/src/core/task-command.js +21 -1
  293. package/src/core/task-conflict-inspection.js +133 -102
  294. package/src/core/task-descriptor.js +29 -22
  295. package/src/core/task-discovery.js +288 -90
  296. package/src/core/task-lock.js +66 -306
  297. package/src/core/task-migration-validation.js +5 -5
  298. package/src/core/task-migration.js +17 -287
  299. package/src/core/task-paths.js +13 -0
  300. package/src/core/task-recovery.js +1 -0
  301. package/src/core/task-snapshot.js +51 -8
  302. package/src/core/task-storage-migration.js +18 -0
  303. package/src/core/templates.js +23 -8
  304. package/src/core/test-intelligence/benchmarks.js +68 -0
  305. package/src/core/test-intelligence/inventory.js +73 -0
  306. package/src/core/test-intelligence/prune.js +94 -0
  307. package/src/core/test-intelligence/semantic-state.js +15 -0
  308. package/src/core/test-intelligence/service.js +40 -0
  309. package/src/core/test-intelligence/utility.js +50 -0
  310. package/src/core/trace.js +164 -105
  311. package/src/core/trajectory-evaluation.js +15 -3
  312. package/src/core/trajectory-metrics.js +47 -32
  313. package/src/core/transaction.js +16 -287
  314. package/src/core/verification-scope.js +20 -12
  315. package/src/core/work-state.js +100 -59
  316. package/src/integration.d.ts +492 -0
  317. package/src/integration.js +54 -0
  318. package/src/providers/README.md +47 -0
  319. package/src/providers/capabilities.js +46 -0
  320. package/src/providers/errors.js +15 -0
  321. package/src/providers/index.js +29 -0
  322. package/src/providers/json-snapshot.js +105 -0
  323. package/src/providers/registry.js +152 -0
  324. package/src/repository-index/binary-manager.js +7 -5
  325. package/src/storage/artifact-bytes.js +22 -0
  326. package/src/storage/attachment-binding.js +11 -0
  327. package/src/storage/attachment-files.js +82 -0
  328. package/src/storage/attachment-references.js +36 -0
  329. package/src/storage/backup.js +53 -0
  330. package/src/storage/capabilities.js +31 -0
  331. package/src/storage/connection-owner.js +119 -0
  332. package/src/storage/connection.js +376 -0
  333. package/src/storage/existing-project-scope.js +45 -0
  334. package/src/storage/export-destination.js +44 -0
  335. package/src/storage/exporter.js +284 -0
  336. package/src/storage/file-durability.js +46 -0
  337. package/src/storage/importer.js +461 -0
  338. package/src/storage/index.js +74 -0
  339. package/src/storage/leases.js +63 -0
  340. package/src/storage/ledger-event-snapshot.js +150 -0
  341. package/src/storage/ledger-relations.js +220 -0
  342. package/src/storage/legacy-maintenance-boundary.js +18 -0
  343. package/src/storage/legacy-signature.js +115 -0
  344. package/src/storage/maintenance-handoff.js +129 -0
  345. package/src/storage/maintenance-owner.js +42 -0
  346. package/src/storage/maintenance.js +310 -0
  347. package/src/storage/metadata-json.js +35 -0
  348. package/src/storage/migration-activate.js +59 -0
  349. package/src/storage/migration-archive.js +57 -0
  350. package/src/storage/migration-attachment-root.js +61 -0
  351. package/src/storage/migration-candidate.js +398 -0
  352. package/src/storage/migration-publication.js +143 -0
  353. package/src/storage/migration-rollback-admission.js +54 -0
  354. package/src/storage/migration-rollback-preparation.js +132 -0
  355. package/src/storage/migration-rollback-publication.js +183 -0
  356. package/src/storage/migration-rollback-release.js +66 -0
  357. package/src/storage/migration-rollback-source-stage.js +103 -0
  358. package/src/storage/migration-rollback.js +43 -0
  359. package/src/storage/migration-source-partition.js +56 -0
  360. package/src/storage/migration-source.js +255 -0
  361. package/src/storage/migration-terminal.js +39 -0
  362. package/src/storage/migration-validation.js +75 -0
  363. package/src/storage/migration.js +43 -0
  364. package/src/storage/operational-attachments.js +49 -0
  365. package/src/storage/operational-context.js +50 -0
  366. package/src/storage/project-backup.js +98 -0
  367. package/src/storage/project-bootstrap.js +24 -0
  368. package/src/storage/project-boundary.js +79 -0
  369. package/src/storage/project-read-snapshot.js +18 -0
  370. package/src/storage/project-replacement.js +137 -0
  371. package/src/storage/project-restore.js +203 -0
  372. package/src/storage/record-definitions.js +45 -0
  373. package/src/storage/repository.js +507 -0
  374. package/src/storage/restore-journal.js +62 -0
  375. package/src/storage/restore-layout.js +30 -0
  376. package/src/storage/restore-preparation.js +84 -0
  377. package/src/storage/restore-replacement-archive.js +162 -0
  378. package/src/storage/restore-replacement-owner.js +31 -0
  379. package/src/storage/restore-replacement-preparation.js +128 -0
  380. package/src/storage/restore-replacement.js +142 -0
  381. package/src/storage/restore-terminal.js +32 -0
  382. package/src/storage/runtime.js +31 -0
  383. package/src/storage/schema.js +214 -0
  384. package/src/storage/singleton-import.js +84 -0
  385. package/src/storage/snapshot-continuity.js +16 -0
  386. package/src/storage/snapshot.js +36 -0
  387. package/src/storage/storage-marker.js +79 -0
  388. package/src/storage/task-guards.js +107 -0
  389. package/src/storage/topology.js +42 -0
  390. package/src/storage/transaction.js +179 -0
  391. package/src/storage/unit-of-work.js +663 -0
  392. package/src/storage/windows-process-incarnation.js +206 -0
  393. package/.forgeloop/forgeloop.gitignore +0 -4
  394. package/LICENSE-DOCS.md +0 -13
  395. package/src/core/transaction-maintenance.js +0 -70
@@ -1,6 +1,10 @@
1
+ import { ledgerRelationMap, ledgerRelationSet } from "./ledger-relations.js";
2
+ import { ledgerEventAt, ledgerEventsOfTypes } from "./ledger-event-collection.js";
3
+ import { needsExistingProjectScope, withExistingProjectScope, isOperationalArtifactPath } from "../storage/existing-project-scope.js";
1
4
 
2
- import { appendFile, mkdir, open, readFile, stat } from "node:fs/promises";
3
- import path from "node:path";
5
+ import { open, readFile, stat } from "node:fs/promises";
6
+ import { closeSync, lstatSync, openSync, readSync } from "node:fs";
7
+ import { createHash } from "node:crypto";
4
8
 
5
9
  import { assertSafePath, ensureWithin, fileExists } from "./filesystem.js";
6
10
  import { ARTIFACT_PATHS, canonicalFingerprint } from "./artifacts.js";
@@ -9,9 +13,19 @@ import { assertSchema, readSchema } from "./schema-validation.js";
9
13
  import { assertSecretFree } from "./receipt.js";
10
14
  import { PROTOCOL_VERSION } from "./protocol.js";
11
15
  import { isRecoverableCompletionEvidenceCode } from "./completion-recovery.js";
16
+ import {
17
+ CONTRACT_BOOTSTRAP_REPAIR_EVENT,
18
+ ROUTE_CHECKPOINT_BOUND_EVENT,
19
+ assertContractBootstrapRepairDetails,
20
+ assertContractBootstrapRepairMigrationDetails,
21
+ isLegacyContractBootstrapRepairMarkerShape,
22
+ validateContractBootstrapRepairMigrations,
23
+ repairMarkerErrors,
24
+ } from "./contract-bootstrap-recovery.js";
12
25
 
13
26
  import { taskArtifactPath } from "./task-paths.js";
14
27
  import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
28
+ import { getOperationalStore } from "../storage/operational-context.js";
15
29
 
16
30
  import { assertDiagnosisDetails } from "./diagnosis-model.js";
17
31
  import {
@@ -33,6 +47,32 @@ import {
33
47
  isLegacyRecoveryEventShape,
34
48
  legacyRecoveryMigrationId,
35
49
  } from "./task-recovery-migration.js";
50
+ import {
51
+ CHECKPOINT_REVALIDATED_EVENT,
52
+ assertCheckpointRevalidatedDetails,
53
+ validateCheckpointRevalidationCurrentBinding,
54
+ validateCheckpointRevalidationEventBindings,
55
+ } from "./checkpoint-revalidation.js";
56
+ import {
57
+ CONTRACT_REVISED_EVENT,
58
+ assertContractRevisedDetails,
59
+ validateContractRevisionCurrentBinding,
60
+ validateContractRevisionEventBindings,
61
+ } from "./contract-revision.js";
62
+ import {
63
+ GATE_SATISFIED_EVENT,
64
+ GATE_REVALIDATED_EVENT,
65
+ assertGateSatisfiedDetails,
66
+ assertGateRevalidatedDetails,
67
+ hasCurrentGateEvidence,
68
+ validateGateSatisfactionBindings,
69
+ } from "./gate-provenance.js";
70
+ import {
71
+ assertSemanticDecisionDetails,
72
+ validateSemanticDecisionArtifactBindings,
73
+ validateSemanticDecisionEventBindings,
74
+ } from "./decision/events.js";
75
+ import { SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT } from "./decision/constants.js";
36
76
 
37
77
  const EVENT_SCHEMA_VERSION = 1;
38
78
  export const LIFECYCLE_MILESTONES = Object.freeze([
@@ -60,19 +100,45 @@ const REPEATABLE_MILESTONES = new Set([
60
100
  "PREFLIGHT_READY",
61
101
  ]);
62
102
 
63
- function eventIndexPath(eventsPath) {
64
- return `${eventsPath}.index.json`;
103
+ export function isRevisionEpochPlanRepeat(events = [], index = 0, event = null) {
104
+ if (event?.event !== "PLAN_RECORDED") return false;
105
+ const previousPlan = events.slice(0, index).findLast((candidate) => candidate.event === "PLAN_RECORDED");
106
+ const revision = events.slice(0, index).findLast((candidate) => candidate.event === CONTRACT_REVISED_EVENT);
107
+ return Boolean(previousPlan && revision && previousPlan.seq < revision.seq);
65
108
  }
66
109
 
67
- function parseEventIndex(text, relativePath) {
68
- if (typeof text !== "string") return null;
69
- try {
70
- const value = JSON.parse(text);
71
- if (!value || value.schemaVersion !== 1 || !Number.isInteger(value.seq) || value.seq < 0
72
- || (value.lastHash !== null && !/^[a-f0-9]{64}$/.test(value.lastHash))) return null;
73
- return value;
74
- } catch {
75
- return null;
110
+ function validateMilestoneChronology(events, index, event, milestoneCounts, lastMilestone, errors) {
111
+ const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(event.event);
112
+ if (milestoneIndex < 0) return lastMilestone;
113
+ const count = (milestoneCounts.get(event.event) ?? 0) + 1;
114
+ milestoneCounts.set(event.event, count);
115
+ const revisionPlanRepeat = isRevisionEpochPlanRepeat(events, index, event);
116
+ if (count > 1 && !REPEATABLE_MILESTONES.has(event.event) && !revisionPlanRepeat) {
117
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
118
+ }
119
+ if (milestoneIndex > lastMilestone + 1) {
120
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is missing prerequisite milestone: ${LIFECYCLE_MILESTONES[lastMilestone + 1]}` });
121
+ } else if (milestoneIndex < lastMilestone && event.event !== "VERIFICATION_STARTED" && event.event !== "PREFLIGHT_READY") {
122
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is out of lifecycle order` });
123
+ } else if (milestoneIndex === lastMilestone && !REPEATABLE_MILESTONES.has(event.event) && !revisionPlanRepeat) {
124
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
125
+ }
126
+ if (milestoneIndex > lastMilestone) return milestoneIndex;
127
+ return lastMilestone;
128
+ }
129
+
130
+ function validateExecutionGateChronology(events, index, seen, errors) {
131
+ if (!seen.has("ROUTE_VALIDATED")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before route validation" });
132
+ if (!seen.has("CONTRACT_VALIDATED")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before contract validation" });
133
+ if (!seen.has("PREFLIGHT_READY")) errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before preflight readiness" });
134
+ const preflight = events.slice(0, index).findLast((candidate) => candidate.event === "PREFLIGHT_READY");
135
+ const requiredGates = preflight?.details?.requiredGates ?? [];
136
+ const latestContractRevisionSeq = events.slice(0, index).findLast((candidate) => candidate.event === CONTRACT_REVISED_EVENT)?.seq ?? 0;
137
+ const beforeExecution = events.slice(0, index);
138
+ for (const gate of requiredGates) {
139
+ if (!hasCurrentGateEvidence(beforeExecution, ledgerEventAt(events, index)?.taskId, gate, latestContractRevisionSeq)) {
140
+ errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `execution started before gate satisfaction: ${gate}` });
141
+ }
76
142
  }
77
143
  }
78
144
 
@@ -89,17 +155,7 @@ async function readEventCheckpoint(target, packageRoot, relPath, options, transa
89
155
  if (!transaction.eventCheckpoints) transaction.eventCheckpoints = new Map();
90
156
  const cached = transaction.eventCheckpoints.get(relPath);
91
157
  if (cached) return cached;
92
-
93
- const indexPath = eventIndexPath(relPath);
94
- const indexed = parseEventIndex(await transaction.readText(indexPath), indexPath);
95
- const tail = await readEventTail(target, packageRoot, { ...options, eventsPath: relPath, limit: 1 });
96
- const last = tail.at(-1) ?? null;
97
- let checkpoint;
98
- if (indexed && indexed.seq === (last?.seq ?? 0) && indexed.lastHash === (last?.hash ?? null)) {
99
- checkpoint = indexed;
100
- } else {
101
- checkpoint = checkpointFromEvents(await readEvents(target, packageRoot, { ...options, eventsPath: relPath }));
102
- }
158
+ const checkpoint = checkpointFromEvents(await readEventTail(target, packageRoot, { ...options, eventsPath: relPath, limit: 1 }));
103
159
  transaction.eventCheckpoints.set(relPath, checkpoint);
104
160
  return checkpoint;
105
161
  }
@@ -125,6 +181,22 @@ export function validateKnownEventDetails(event) {
125
181
  case "CHECKPOINT_RECONCILED":
126
182
  assertReconcileClosureDetails(event.details);
127
183
  return;
184
+ case CHECKPOINT_REVALIDATED_EVENT:
185
+ assertCheckpointRevalidatedDetails(event.details);
186
+ return;
187
+ case CONTRACT_REVISED_EVENT:
188
+ assertContractRevisedDetails(event.details);
189
+ return;
190
+ case GATE_SATISFIED_EVENT:
191
+ assertGateSatisfiedDetails(event.details);
192
+ return;
193
+ case GATE_REVALIDATED_EVENT:
194
+ assertGateRevalidatedDetails(event.details);
195
+ return;
196
+ case SEMANTIC_DECISION_RECORDED_EVENT:
197
+ case SEMANTIC_DECISION_SUPERSEDED_EVENT:
198
+ assertSemanticDecisionDetails(event.details, event.event);
199
+ return;
128
200
  case "TASK_RECOVERY_RECORDED":
129
201
  assertRecoveryRecordedDetails(event.details);
130
202
  return;
@@ -135,18 +207,27 @@ export function validateKnownEventDetails(event) {
135
207
  if (!event.details?.recoveryId && isLegacyRecoveryDetailsShape(event.details)) return;
136
208
  assertRecoveryRecordedDetails(event.details);
137
209
  return;
210
+ case "TASK_ABANDONED":
211
+ assertTaskAbandonedDetails(event.details);
212
+ return;
138
213
  case "LEGACY_RECOVERY_MIGRATION_RECORDED":
139
214
  assertLegacyMigrationDetails(event.details);
140
215
  return;
216
+ case CONTRACT_BOOTSTRAP_REPAIR_EVENT:
217
+ case "CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_RECORDED":
218
+ validateKnownContractBootstrapEvent(event);
219
+ return;
220
+ case "ROUTE_REBOUND":
221
+ assertRouteReboundDetails(event.details);
222
+ return;
223
+ case ROUTE_CHECKPOINT_BOUND_EVENT:
224
+ assertRouteCheckpointBoundDetails(event.details);
225
+ return;
141
226
  case "TASK_RECOVERY_RESUMED":
142
227
  assertRecoveryResumedDetails(event.details);
143
228
  return;
144
229
  case "TRAJECTORY_EVALUATED":
145
- if (!event.details || !/^eval-[A-Za-z0-9_-]+$/.test(event.details.evaluationId ?? "")
146
- || typeof event.details.scenarioId !== "string" || !/^[a-f0-9]{64}$/.test(event.details.evaluationFingerprint ?? "")
147
- || event.fingerprint !== event.details.evaluationFingerprint) {
148
- throw protocolError("E_EVENT_INVALID", "TRAJECTORY_EVALUATED requires a bound evaluationId, scenarioId, and fingerprint");
149
- }
230
+ assertTrajectoryEvaluatedDetails(event);
150
231
  return;
151
232
  case "WORKSPACE_BOUND":
152
233
  assertStructuredArtifactEvent(event, ["workspaceFingerprint"], "WORKSPACE_BOUND");
@@ -161,14 +242,7 @@ export function validateKnownEventDetails(event) {
161
242
  assertFingerprint(event.details.digest, "HANDOFF_CREATED details.digest");
162
243
  return;
163
244
  case "HANDOFF_ACCEPTED":
164
- if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)
165
- || typeof event.details.handoffId !== "string" || !/^handoff-[A-Za-z0-9_-]+$/.test(event.details.handoffId)
166
- || typeof event.details.handoffDigest !== "string"
167
- || typeof event.details.consumerId !== "string" || !event.details.consumerId.trim()
168
- || (event.details.harness !== undefined && (typeof event.details.harness !== "string" || !event.details.harness.trim()))) {
169
- throw protocolError("E_EVENT_INVALID", "HANDOFF_ACCEPTED requires a valid handoffId, handoffDigest, and consumerId");
170
- }
171
- assertFingerprint(event.details.handoffDigest, "HANDOFF_ACCEPTED details.handoffDigest");
245
+ assertHandoffAcceptedDetails(event);
172
246
  return;
173
247
  case "RESPONSIBILITY_SET":
174
248
  assertStructuredArtifactEvent(event, ["responsibilityFingerprint"], "RESPONSIBILITY_SET");
@@ -207,6 +281,34 @@ export function validateKnownEventDetails(event) {
207
281
  }
208
282
  }
209
283
 
284
+ function validateKnownContractBootstrapEvent(event) {
285
+ if (event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT) {
286
+ if (isLegacyContractBootstrapRepairMarkerShape(event)) return;
287
+ assertContractBootstrapRepairDetails(event.details);
288
+ return;
289
+ }
290
+ assertContractBootstrapRepairMigrationDetails(event.details);
291
+ }
292
+
293
+ function assertTrajectoryEvaluatedDetails(event) {
294
+ if (!event.details || !/^eval-[A-Za-z0-9_-]+$/.test(event.details.evaluationId ?? "")
295
+ || typeof event.details.scenarioId !== "string" || !/^[a-f0-9]{64}$/.test(event.details.evaluationFingerprint ?? "")
296
+ || event.fingerprint !== event.details.evaluationFingerprint) {
297
+ throw protocolError("E_EVENT_INVALID", "TRAJECTORY_EVALUATED requires a bound evaluationId, scenarioId, and fingerprint");
298
+ }
299
+ }
300
+
301
+ function assertHandoffAcceptedDetails(event) {
302
+ if (!event.details || typeof event.details !== "object" || Array.isArray(event.details)
303
+ || typeof event.details.handoffId !== "string" || !/^handoff-[A-Za-z0-9_-]+$/.test(event.details.handoffId)
304
+ || typeof event.details.handoffDigest !== "string"
305
+ || typeof event.details.consumerId !== "string" || !event.details.consumerId.trim()
306
+ || (event.details.harness !== undefined && (typeof event.details.harness !== "string" || !event.details.harness.trim()))) {
307
+ throw protocolError("E_EVENT_INVALID", "HANDOFF_ACCEPTED requires a valid handoffId, handoffDigest, and consumerId");
308
+ }
309
+ assertFingerprint(event.details.handoffDigest, "HANDOFF_ACCEPTED details.handoffDigest");
310
+ }
311
+
210
312
  function assertStringList(value, label) {
211
313
  if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || !item)) {
212
314
  throw protocolError("E_EVENT_INVALID", `${label} must be an array of non-empty strings`);
@@ -234,6 +336,35 @@ function assertStructuredArtifactEvent(event, requiredDetails, label) {
234
336
  }
235
337
  }
236
338
 
339
+ function assertRouteReboundDetails(details) {
340
+ const keys = ["routeFingerprint", "contractFingerprint", "previousRouteFingerprint"];
341
+ if (!details || typeof details !== "object" || Array.isArray(details)
342
+ || Object.keys(details).length !== keys.length
343
+ || keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
344
+ throw protocolError("E_EVENT_INVALID", "ROUTE_REBOUND requires exactly route, contract, and previous route fingerprints");
345
+ }
346
+ assertFingerprint(details.routeFingerprint, "ROUTE_REBOUND details.routeFingerprint");
347
+ assertFingerprint(details.contractFingerprint, "ROUTE_REBOUND details.contractFingerprint");
348
+ if (details.previousRouteFingerprint !== null) {
349
+ assertFingerprint(details.previousRouteFingerprint, "ROUTE_REBOUND details.previousRouteFingerprint");
350
+ }
351
+ }
352
+
353
+ function assertRouteCheckpointBoundDetails(details) {
354
+ const keys = ["routeFingerprint", "contractFingerprint", "selectedGuides"];
355
+ if (!details || typeof details !== "object" || Array.isArray(details)
356
+ || Object.keys(details).length !== keys.length
357
+ || keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
358
+ throw protocolError("E_EVENT_INVALID", "ROUTE_CHECKPOINT_BOUND requires exactly route, contract, and selected guide identity");
359
+ }
360
+ assertFingerprint(details.routeFingerprint, "ROUTE_CHECKPOINT_BOUND details.routeFingerprint");
361
+ assertFingerprint(details.contractFingerprint, "ROUTE_CHECKPOINT_BOUND details.contractFingerprint");
362
+ assertStringList(details.selectedGuides, "ROUTE_CHECKPOINT_BOUND details.selectedGuides");
363
+ if (new Set(details.selectedGuides).size !== details.selectedGuides.length) {
364
+ throw protocolError("E_EVENT_INVALID", "ROUTE_CHECKPOINT_BOUND details.selectedGuides must not contain duplicates");
365
+ }
366
+ }
367
+
237
368
  function assertRecoveryRecordedDetails(details) {
238
369
  if (!details || typeof details !== "object" || Array.isArray(details)) {
239
370
  throw protocolError("E_EVENT_INVALID", "recovery event requires structured details");
@@ -256,6 +387,44 @@ function assertRecoveryRecordedDetails(details) {
256
387
  assertStringList(details.releasedClaims, "recovery event details.releasedClaims");
257
388
  }
258
389
 
390
+ function assertTaskAbandonedDetails(details) {
391
+ const keys = [
392
+ "recoveryId", "classification", "reasonCodes", "previousPhase", "previousRevision",
393
+ "previousHead", "previousBranch", "currentHead", "currentBranch",
394
+ "releasedClaims", "authorityKind",
395
+ ];
396
+ if (!details || typeof details !== "object" || Array.isArray(details)
397
+ || Object.keys(details).length !== keys.length
398
+ || keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))) {
399
+ throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED requires the exact abandonment boundary details");
400
+ }
401
+ if (typeof details.recoveryId !== "string" || details.recoveryId.length === 0) {
402
+ throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.recoveryId must be a non-empty string");
403
+ }
404
+ if (details.classification !== "ABANDONED") {
405
+ throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.classification must be ABANDONED");
406
+ }
407
+ assertStringList(details.reasonCodes, "TASK_ABANDONED details.reasonCodes");
408
+ if (details.reasonCodes.length !== 1 || details.reasonCodes[0] !== "CALLER_ABANDONED") {
409
+ throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.reasonCodes must be [CALLER_ABANDONED]");
410
+ }
411
+ if (typeof details.previousPhase !== "string" || details.previousPhase.length === 0) {
412
+ throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.previousPhase must be a non-empty string");
413
+ }
414
+ if (!Number.isInteger(details.previousRevision) || details.previousRevision < 0) {
415
+ throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.previousRevision must be a non-negative integer");
416
+ }
417
+ for (const key of ["previousHead", "previousBranch", "currentHead", "currentBranch"]) {
418
+ if (typeof details[key] !== "string" && details[key] !== null) {
419
+ throw protocolError("E_EVENT_INVALID", `TASK_ABANDONED details.${key} must be a string or null`);
420
+ }
421
+ }
422
+ assertStringList(details.releasedClaims, "TASK_ABANDONED details.releasedClaims");
423
+ if (details.authorityKind !== "CALLER_ACKNOWLEDGED") {
424
+ throw protocolError("E_EVENT_INVALID", "TASK_ABANDONED details.authorityKind must be CALLER_ACKNOWLEDGED");
425
+ }
426
+ }
427
+
259
428
  function assertRecoveryResumedDetails(details) {
260
429
  if (!details || typeof details !== "object" || Array.isArray(details)
261
430
  || typeof details.recoveryId !== "string" || !details.recoveryId) {
@@ -334,35 +503,79 @@ function protocolError(code, message, artifacts = [ARTIFACT_PATHS.events]) {
334
503
  }
335
504
 
336
505
  export async function readEvents(target, packageRoot, options = {}) {
506
+ if (await needsExistingProjectScope(target)) {
507
+ return withExistingProjectScope(target, () => readEvents(target, packageRoot, options), { readOnly: true });
508
+ }
337
509
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
338
- await assertSafePath(target, relPath);
339
- const eventsPath = ensureWithin(target, relPath);
340
- const transaction = (await getTaskTransaction(target));
341
- if (transaction) {
342
- const stagedText = await transaction.readText(relPath);
343
- if (stagedText !== null) return parseEventsText(stagedText, relPath, packageRoot);
510
+ const store = getOperationalStore(target);
511
+ if (store?.recognizes(relPath)) {
512
+ const schema = await readSchema("event", packageRoot);
513
+ const events = [];
514
+ // Array callers consume the synchronous cursor directly. Streaming callers
515
+ // retain iterateEvents; both paths validate and fully bind observed rows.
516
+ for (const event of store.iterateEvents(relPath)) {
517
+ validateStoredEvent(event, schema, `${relPath}[${events.length}]`);
518
+ events.push(event);
519
+ }
520
+ return events;
344
521
  }
345
- if (!(await fileExists(eventsPath))) return [];
346
- const text = await readFile(eventsPath, "utf8");
347
- return parseEventsText(text, relPath, packageRoot);
522
+ if (await assertOperationalLedgerAbsent(target, relPath)) return [];
523
+ return readPortableEvents(target, packageRoot, { ...options, eventsPath: relPath });
524
+ }
525
+
526
+ async function assertOperationalLedgerAbsent(target, relativePath) {
527
+ await assertSafePath(target, relativePath);
528
+ if (isOperationalArtifactPath(relativePath) && await fileExists(ensureWithin(target, relativePath))) {
529
+ throw protocolError("E_STORAGE_MIGRATION_REQUIRED", "Operational ledger reads require canonical SQLite; inspect legacy exports explicitly", [relativePath]);
530
+ }
531
+ return isOperationalArtifactPath(relativePath);
532
+ }
533
+
534
+ /** Explicit read-only inspection of an import/export ledger; never selects native authority. */
535
+ export async function readPortableEvents(target, packageRoot, options = {}) {
536
+ const relPath = options.eventsPath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
537
+ const filename = await assertSafePath(target, relPath);
538
+ if (!(await fileExists(filename))) return [];
539
+ return parseEventsText(await readFile(filename, "utf8"), relPath, packageRoot);
540
+ }
541
+
542
+ /** Stream schema-validated canonical rows within the captured operational scope. */
543
+ export function iterateEvents(target, packageRoot, options = {}) {
544
+ const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
545
+ const store = getOperationalStore(target);
546
+ return (async function* validatedEvents() {
547
+ if (!store?.recognizes(relPath)) { yield* await readEvents(target, packageRoot, options); return; }
548
+ const schema = await readSchema("event", packageRoot);
549
+ let index = 0;
550
+ for (const event of store.iterateEvents(relPath)) {
551
+ validateStoredEvent(event, schema, `${relPath}[${index}]`);
552
+ index += 1;
553
+ yield event;
554
+ }
555
+ })();
348
556
  }
349
557
 
350
558
  export async function readEventTail(target, packageRoot, options = {}) {
559
+ if (await needsExistingProjectScope(target)) {
560
+ return withExistingProjectScope(target, () => readEventTail(target, packageRoot, options), { readOnly: true });
561
+ }
351
562
  const limit = options.limit ?? 50;
352
563
  if (!Number.isInteger(limit) || limit < 1) {
353
564
  throw protocolError("E_EVENT_INVALID", "ledger tail limit must be a positive integer");
354
565
  }
355
566
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
356
- await assertSafePath(target, relPath);
357
- const eventsPath = ensureWithin(target, relPath);
358
- const transaction = (await getTaskTransaction(target));
359
- if (transaction) {
360
- const stagedText = await transaction.readText(relPath);
361
- if (stagedText !== null) {
362
- const lines = stagedText.split(/\r?\n/).filter((line) => line.trim() !== "");
363
- return parseEventsText(lines.slice(-limit).join("\n"), relPath, packageRoot);
364
- }
365
- }
567
+ const store = getOperationalStore(target);
568
+ if (store?.recognizes(relPath)) return validateStoredEvents(store.readEvents(relPath, limit), relPath, packageRoot);
569
+ if (await assertOperationalLedgerAbsent(target, relPath)) return [];
570
+ return readPortableEventTail(target, packageRoot, { ...options, eventsPath: relPath });
571
+ }
572
+
573
+ /** Bounded tail inspection for explicit legacy/import/export tooling. */
574
+ export async function readPortableEventTail(target, packageRoot, options = {}) {
575
+ const limit = options.limit ?? 50;
576
+ if (!Number.isInteger(limit) || limit < 1) throw protocolError("E_EVENT_INVALID", "ledger tail limit must be a positive integer");
577
+ const relPath = options.eventsPath ?? options.relativePath ?? (options.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
578
+ const eventsPath = await assertSafePath(target, relPath);
366
579
  if (!(await fileExists(eventsPath))) return [];
367
580
  const size = (await stat(eventsPath)).size;
368
581
  let window = Math.min(size, 64 * 1024);
@@ -410,18 +623,41 @@ async function parseEventsText(text, relPath, packageRoot) {
410
623
  });
411
624
  }
412
625
 
626
+ async function validateStoredEvents(events, relativePath, packageRoot) {
627
+ const schema = await readSchema("event", packageRoot);
628
+ for (const [index, event] of events.entries()) {
629
+ validateStoredEvent(event, schema, `${relativePath}[${index}]`);
630
+ }
631
+ return events;
632
+ }
633
+
634
+ function validateStoredEvent(event, schema, label) {
635
+ assertJsonBytes(JSON.stringify(event), label);
636
+ assertJsonLimits(event, label);
637
+ assertSchema(event, schema, label);
638
+ validateKnownEventDetails(event);
639
+ }
640
+
413
641
  export async function appendProtocolEvent(target, input, packageRoot, options = {}) {
414
642
  const activeTransaction = (await getTaskTransaction(target));
415
643
  if (typeof input?.taskId !== "string" || !input.taskId) throw protocolError("E_EVENT_INVALID", "event taskId is required");
416
644
  if (typeof input?.event !== "string" || !input.event) throw protocolError("E_EVENT_INVALID", "event type is required");
417
645
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
418
646
  if (!activeTransaction) {
419
- return withTaskTransaction({
420
- target,
421
- taskId: options.taskId ?? input.taskId,
422
- lockTaskId: relPath === ARTIFACT_PATHS.events ? "__legacy-events__" : (options.taskId ?? input.taskId),
423
- operation: "append-event",
424
- }, async () => appendProtocolEvent(target, input, packageRoot, options));
647
+ // Only a standalone append is replayable: nested domain mutations and
648
+ // external observations never enter this retry boundary.
649
+ const capturedInput = structuredClone(input);
650
+ for (let attempt = 0; ; attempt += 1) {
651
+ try {
652
+ return await withTaskTransaction({
653
+ target,
654
+ taskId: options.taskId ?? capturedInput.taskId,
655
+ operation: "append-event",
656
+ }, () => appendProtocolEvent(target, capturedInput, packageRoot, options));
657
+ } catch (error) {
658
+ if (error.code !== "E_STATE_REVISION_CONFLICT" || attempt >= 2) throw error;
659
+ }
660
+ }
425
661
  }
426
662
  const checkpoint = await readEventCheckpoint(target, packageRoot, relPath, options, activeTransaction);
427
663
  const event = buildProtocolEvent(input, { checkpoint });
@@ -429,16 +665,8 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
429
665
  assertSchema(event, schema, relPath);
430
666
  event.hash = eventHash(event);
431
667
  if (!options.dryRun) {
432
- if (activeTransaction) {
433
- await activeTransaction.appendText(relPath, `${JSON.stringify(event)}\n`);
434
- const nextCheckpoint = { schemaVersion: 1, seq: event.seq, lastHash: event.hash };
435
- await activeTransaction.stageText(eventIndexPath(relPath), `${JSON.stringify(nextCheckpoint)}\n`);
436
- activeTransaction.eventCheckpoints.set(relPath, nextCheckpoint);
437
- } else {
438
- const eventsPath = ensureWithin(target, relPath);
439
- await mkdir(path.dirname(eventsPath), { recursive: true });
440
- await appendFile(eventsPath, `${JSON.stringify(event)}\n`, { encoding: "utf8" });
441
- }
668
+ activeTransaction.appendEvent(event);
669
+ activeTransaction.eventCheckpoints.set(relPath, { schemaVersion: 1, seq: event.seq, lastHash: event.hash });
442
670
  }
443
671
  return event;
444
672
  }
@@ -450,8 +678,8 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
450
678
  * before appending the migration events.
451
679
  */
452
680
  function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegacyRecoveryEvents = false } = {}) {
453
- const migrationBySeq = new Map();
454
- for (const event of events) {
681
+ const migrationBySeq = ledgerRelationMap();
682
+ for (const event of ledgerEventsOfTypes(events, [LEGACY_RECOVERY_MIGRATION_EVENT])) {
455
683
  if (event.event !== LEGACY_RECOVERY_MIGRATION_EVENT) continue;
456
684
  try {
457
685
  assertLegacyMigrationDetails(event.details);
@@ -466,11 +694,12 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
466
694
  });
467
695
  continue;
468
696
  }
469
- migrationBySeq.set(event.details.legacyEventSeq, event);
697
+ migrationBySeq.set(event.details.legacyEventSeq, events.indexOf(event));
470
698
  }
471
- for (const event of events) {
699
+ for (const event of ledgerEventsOfTypes(events, ["OPERATOR_RECOVERY_RECORDED"])) {
472
700
  if (!isLegacyRecoveryEventShape(event)) continue;
473
- const migration = migrationBySeq.get(event.seq);
701
+ const migrationPosition = migrationBySeq.get(event.seq);
702
+ const migration = migrationPosition === undefined ? undefined : ledgerEventAt(events, migrationPosition);
474
703
  if (!migration) {
475
704
  if (!allowUnmigratedLegacyRecoveryEvents) {
476
705
  errors.push({
@@ -502,7 +731,8 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
502
731
  });
503
732
  }
504
733
  }
505
- for (const [legacySeq, migration] of migrationBySeq) {
734
+ for (const [legacySeq, position] of migrationBySeq) {
735
+ const migration = ledgerEventAt(events, position);
506
736
  errors.push({
507
737
  code: "E_EVENT_INVALID",
508
738
  message: `migration event ${migration.seq} references unknown legacy recovery event seq ${legacySeq}`,
@@ -510,30 +740,37 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
510
740
  }
511
741
  }
512
742
 
513
- export async function validateEventLedger(target, packageRoot, options = {}) {
514
- const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
515
- let events;
516
- try {
517
- events = await readEvents(target, packageRoot, { ...options, eventsPath: relPath });
518
- } catch (error) {
519
- return { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
520
- }
743
+ /**
744
+ * Pure ledger validation over an already-loaded event array.
745
+ *
746
+ * This is the smallest reusable seam between the filesystem path and a
747
+ * database-backed reader: every rule below is computed from the events alone,
748
+ * with no filesystem access. `validateEventLedger` keeps its existing signature
749
+ * and delegates here after reading events, so behavior is unchanged for every
750
+ * existing caller.
751
+ *
752
+ * Callers that already hold events (for example a database reader) can obtain
753
+ * exactly the same guarantees without materializing files first.
754
+ */
755
+ export function validateLedgerEvents(events, options = {}) {
521
756
  const errors = [];
522
757
  let taskId = null;
758
+ let previousHash = null;
523
759
  const seen = new Set();
524
760
  let lastMilestone = -1;
525
761
  const milestoneCounts = new Map();
526
- const createdHandoffs = new Map();
527
- const acceptedHandoffs = new Set();
762
+ const createdHandoffs = ledgerRelationMap();
763
+ const acceptedHandoffs = ledgerRelationSet();
528
764
  for (const [index, event] of events.entries()) {
529
765
  if (event.seq !== index + 1) {
530
766
  errors.push({ code: "E_EVENT_INVALID", message: `event sequence must be ${index + 1}` });
531
767
  }
532
768
  if (taskId === null) taskId = event.taskId;
533
769
  if (event.taskId !== taskId) errors.push({ code: "E_EVENT_INVALID", message: "event task IDs must remain stable" });
534
- if (event.previousHash !== (index === 0 ? null : events[index - 1].hash)) {
770
+ if (event.previousHash !== previousHash) {
535
771
  errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} previousHash does not match` });
536
772
  }
773
+ previousHash = event.hash;
537
774
  if (event.hash !== eventHash(event)) {
538
775
  errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} hash does not match its content` });
539
776
  }
@@ -542,50 +779,10 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
542
779
  } catch (err) {
543
780
  errors.push({ code: err.code ?? "E_EVENT_INVALID", message: `event ${event.seq} (${event.event}): ${err.message}` });
544
781
  }
545
- const milestoneIndex = LIFECYCLE_MILESTONES.indexOf(event.event);
546
- if (milestoneIndex >= 0) {
547
- const count = (milestoneCounts.get(event.event) ?? 0) + 1;
548
- milestoneCounts.set(event.event, count);
549
- if (count > 1 && !REPEATABLE_MILESTONES.has(event.event)) {
550
- errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
551
- }
552
- if (milestoneIndex > lastMilestone + 1) {
553
- errors.push({
554
- code: "E_PHASE_CHRONOLOGY_INVALID",
555
- message: `${event.event} is missing prerequisite milestone: ${LIFECYCLE_MILESTONES[lastMilestone + 1]}`,
556
- });
557
- } else if (milestoneIndex < lastMilestone && event.event !== "VERIFICATION_STARTED"
558
- && event.event !== "PREFLIGHT_READY") {
559
- // VERIFICATION_STARTED re-enters per verification cycle; PREFLIGHT_READY
560
- // may be refreshed mid-lifecycle (policy/contract evolution) after its
561
- // prerequisites were already satisfied by the earlier occurrence.
562
- errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `${event.event} is out of lifecycle order` });
563
- } else if (milestoneIndex === lastMilestone && !REPEATABLE_MILESTONES.has(event.event)) {
564
- errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `lifecycle milestone must not repeat: ${event.event}` });
565
- }
566
- if (milestoneIndex > lastMilestone) lastMilestone = milestoneIndex;
567
- }
782
+ lastMilestone = validateMilestoneChronology(events, index, event, milestoneCounts, lastMilestone, errors);
568
783
  seen.add(event.event);
569
- if (event.event === "EXECUTION_STARTED" && !seen.has("ROUTE_VALIDATED")) {
570
- errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before route validation" });
571
- }
572
- if (event.event === "EXECUTION_STARTED" && !seen.has("CONTRACT_VALIDATED")) {
573
- errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before contract validation" });
574
- }
575
- if (event.event === "EXECUTION_STARTED" && !seen.has("PREFLIGHT_READY")) {
576
- errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "execution started before preflight readiness" });
577
- }
578
784
  if (event.event === "EXECUTION_STARTED") {
579
- const preflight = events.slice(0, index).findLast((candidate) => candidate.event === "PREFLIGHT_READY");
580
- const requiredGates = preflight?.details?.requiredGates ?? [];
581
- const satisfiedGates = new Set(events.slice(0, index)
582
- .filter((candidate) => candidate.event === "GATE_SATISFIED")
583
- .map((candidate) => candidate.details?.gate));
584
- for (const gate of requiredGates) {
585
- if (!satisfiedGates.has(gate)) {
586
- errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `execution started before gate satisfaction: ${gate}` });
587
- }
588
- }
785
+ validateExecutionGateChronology(events, index, seen, errors);
589
786
  }
590
787
  if (event.event === "VERIFICATION_RECORDED" && !seen.has("VERIFICATION_STARTED")) {
591
788
  errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: "verification evidence recorded before verification started" });
@@ -620,16 +817,216 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
620
817
  validateLegacyRecoveryMigrations(events, errors, {
621
818
  allowUnmigratedLegacyRecoveryEvents: options?.allowUnmigratedLegacyRecoveryEvents === true,
622
819
  });
623
- return { valid: errors.length === 0, events, errors };
820
+ errors.push(...validateCheckpointRevalidationEventBindings(events));
821
+ errors.push(...validateContractRevisionEventBindings(events));
822
+ errors.push(...validateGateSatisfactionBindings(events));
823
+ errors.push(...validateSemanticDecisionEventBindings(events));
824
+ const repairedErrors = validateContractBootstrapRepairLedger(events, errors, options);
825
+ return { valid: repairedErrors.length === 0, events, errors: repairedErrors };
826
+ }
827
+
828
+ /** Validate canonical native or non-operational interchange ledger with all domain bindings. */
829
+ export async function validateEventLedger(target, packageRoot, options = {}) {
830
+ if (await needsExistingProjectScope(target)) {
831
+ return withExistingProjectScope(target, () => validateEventLedger(target, packageRoot, options), { readOnly: true });
832
+ }
833
+ return validateReadLedger(target, packageRoot, options, readEvents);
834
+ }
835
+
836
+ /** Explicit legacy source validation; never makes the source writable authority. */
837
+ export async function validatePortableEventLedger(target, packageRoot, options = {}) {
838
+ return validateReadLedger(target, packageRoot, options, readPortableEvents);
839
+ }
840
+
841
+ async function validateReadLedger(target, packageRoot, options, reader) {
842
+ const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
843
+ let events;
844
+ try {
845
+ events = await reader(target, packageRoot, { ...options, eventsPath: relPath });
846
+ } catch (error) {
847
+ // A concurrent commit can invalidate a preparation read between statements;
848
+ // that is the standard retryable revision conflict, not ledger evidence.
849
+ // Converting it into an invalid-ledger result would let classification
850
+ // surface a transient contention loser as permanent ownership
851
+ // inconsistency, so a conflict keeps its own domain error here.
852
+ if (error?.code === "E_STATE_REVISION_CONFLICT") throw error;
853
+ return { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
854
+ }
855
+ const result = validateLedgerEvents(events, options);
856
+ const bindingErrors = await validateSemanticDecisionArtifactBindings(target, packageRoot, events);
857
+ if (bindingErrors.length === 0) return result;
858
+ return { valid: false, events, errors: [...result.errors, ...bindingErrors] };
859
+ }
860
+
861
+ // Proof reuse is minted only for one owned read-only backup callback. Public
862
+ // collections and caller-supplied auditSource metadata cannot opt into it.
863
+ const ownedLedgerProofs = new WeakMap();
864
+
865
+ function ownedSnapshotFileIdentity(filename) {
866
+ // The WAL is durable database content that can change while a read cursor
867
+ // pins the main file. The SHM sidecar only carries transient lock/index
868
+ // state, so it is intentionally excluded from the byte identity.
869
+ const paths = [filename, `${filename}-wal`];
870
+ const files = paths.map(current => {
871
+ try {
872
+ const file = lstatSync(current, { bigint: true });
873
+ if (!file.isFile()) throw protocolError("E_STATE_REVISION_CONFLICT", "Owned ledger snapshot file changed during its audit");
874
+ return file;
875
+ } catch (error) {
876
+ if (error?.code === "ENOENT") return null;
877
+ throw error;
878
+ }
879
+ });
880
+ const identity = files.map(file => file
881
+ ? [file.dev, file.ino, file.size, file.mtimeNs, file.ctimeNs].join(":")
882
+ : "missing").join("|");
883
+ if (process.platform !== "win32") return identity;
884
+ // Windows may defer file timestamps until SQLite's open handle closes.
885
+ // Hash readable bytes with bounded memory so raw writes cannot reuse a proof.
886
+ const hash = createHash("sha256");
887
+ const buffer = Buffer.allocUnsafe(64 * 1024);
888
+ for (const [index, current] of paths.entries()) {
889
+ hash.update(`${index}:`);
890
+ if (!files[index]) continue;
891
+ const fd = openSync(current, "r");
892
+ try {
893
+ let count;
894
+ while ((count = readSync(fd, buffer, 0, buffer.length, null)) !== 0) hash.update(buffer.subarray(0, count));
895
+ } finally { closeSync(fd); }
896
+ }
897
+ return `${identity}:${hash.digest("hex")}`;
898
+ }
899
+
900
+ function assertOwnedLedgerUnchanged(events) {
901
+ const owned = ownedLedgerProofs.get(events);
902
+ if (owned && (owned.versionStatement.get().data_version !== owned.version
903
+ || ownedSnapshotFileIdentity(owned.filename) !== owned.fileIdentity)) {
904
+ throw protocolError("E_STATE_REVISION_CONFLICT", "Owned ledger snapshot changed during its audit");
905
+ }
906
+ return owned;
907
+ }
908
+
909
+ function validateOwnedLedgerEvents(events, options) {
910
+ const owned = assertOwnedLedgerUnchanged(events);
911
+ if (!owned) return validateLedgerEvents(events, options);
912
+ const flags = {
913
+ allowUnmigratedLegacyRecoveryEvents: options?.allowUnmigratedLegacyRecoveryEvents === true,
914
+ allowUnmigratedLegacyContractBootstrapRepairMarkers: options?.allowUnmigratedLegacyContractBootstrapRepairMarkers === true,
915
+ };
916
+ const key = JSON.stringify(flags);
917
+ if (owned.successes.has(key)) return { valid: true, events, errors: [] };
918
+ const result = validateLedgerEvents(events, flags);
919
+ assertOwnedLedgerUnchanged(events);
920
+ if (result.valid) owned.successes.add(key);
921
+ return result;
922
+ }
923
+
924
+ async function evaluateCallbackLedgerAudit(target, packageRoot, options, callback, events, relPath) {
925
+ let result;
926
+ try {
927
+ result = validateOwnedLedgerEvents(events, options);
928
+ const owned = assertOwnedLedgerUnchanged(events);
929
+ const sameScope = owned && owned.store === getOperationalStore(target)
930
+ && owned.db === owned.store.db && owned.packageRoot === packageRoot
931
+ && owned.relPath === relPath && owned.taskId === options?.taskId;
932
+ const bindingErrors = sameScope && owned.semanticValidated
933
+ ? [] : await validateSemanticDecisionArtifactBindings(target, packageRoot, events);
934
+ assertOwnedLedgerUnchanged(events);
935
+ if (sameScope && result.valid && bindingErrors.length === 0) owned.semanticValidated = true;
936
+ if (bindingErrors.length) result = { valid: false, events, errors: [...result.errors, ...bindingErrors] };
937
+ } catch (error) {
938
+ result = { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
939
+ }
940
+ const initiallyValid = result.valid;
941
+ const callbackResult = await callback(result);
942
+ // The caller may await other work or mutate its result object. Keep the
943
+ // private snapshot proof valid for the entire successful observation.
944
+ if (initiallyValid) assertOwnedLedgerUnchanged(events);
945
+ return callbackResult;
946
+ }
947
+
948
+ /** Internal callback audit: events remain owned by one immutable native snapshot. */
949
+ export async function withEventLedgerAudit(target, packageRoot, options, callback) {
950
+ if (await needsExistingProjectScope(target)) {
951
+ return withExistingProjectScope(target, () => withEventLedgerAudit(target, packageRoot, options, callback), { readOnly: true });
952
+ }
953
+ const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
954
+ const store = getOperationalStore(target);
955
+ // Prepared mutations retain their existing read-set/CAS and staged-overlay semantics.
956
+ if (!store?.recognizes(relPath) || !relPath.endsWith("/events.ndjson") || store.hasPreparedMutations()) {
957
+ return callback(await validateEventLedger(target, packageRoot, options));
958
+ }
959
+ const taskId = store.taskId({ taskKey: relPath.split("/")[2] });
960
+ if (!taskId) return callback(await validateEventLedger(target, packageRoot, options));
961
+ if (store.auditSource?.taskId === taskId && store.auditSource.packageRoot === packageRoot && store.auditSource.relPath === relPath) {
962
+ return evaluateCallbackLedgerAudit(target, packageRoot, options, callback, store.auditSource.events, relPath);
963
+ }
964
+ const schema = await readSchema("event", packageRoot);
965
+ const { withDetachedLedgerSnapshot } = await import("../storage/ledger-event-snapshot.js");
966
+ const { withOperationalReadSnapshot } = await import("../storage/unit-of-work.js");
967
+ let completeObservation;
968
+ return withDetachedLedgerSnapshot(store.db, taskId, (events, db) => withOperationalReadSnapshot({ db, target }, async snapshotStore => {
969
+ completeObservation = snapshotStore.beginEventSnapshotObservation(relPath);
970
+ snapshotStore.auditSource = { taskId, packageRoot, relPath, events };
971
+ const filename = db.prepare("PRAGMA database_list").all().find(row => row.name === "main").file;
972
+ const versionStatement = db.prepare("PRAGMA data_version");
973
+ ownedLedgerProofs.set(events, { db, filename, store: snapshotStore, taskId, packageRoot, relPath, semanticValidated: false, fileIdentity: ownedSnapshotFileIdentity(filename),
974
+ versionStatement, version: versionStatement.get().data_version, successes: new Set() });
975
+ try { return await evaluateCallbackLedgerAudit(target, packageRoot, options, callback, events, relPath); }
976
+ finally { ownedLedgerProofs.delete(events); }
977
+ }), {
978
+ validate(event, index) { validateStoredEvent(event, schema, `${relPath}[${index}]`); return event; },
979
+ onFullScan: store.captureObservations ? digest => completeObservation(digest) : null,
980
+ // Keep the private schema/index proof valid for every collection read,
981
+ // including a cursor resumed after the caller yielded an event. The guard
982
+ // deliberately uses the full owned snapshot identity check on all
983
+ // platforms; Windows may defer filesystem timestamps, so a cheap-only
984
+ // resume check would expose a raw-tampered payload before the outer audit
985
+ // boundary noticed it.
986
+ guard(_events, _phase) { assertOwnedLedgerUnchanged(_events); },
987
+ });
988
+ }
989
+
990
+ function validateContractBootstrapRepairLedger(events, errors, options) {
991
+ const allowUnmigrated = options?.allowUnmigratedLegacyContractBootstrapRepairMarkers === true;
992
+ validateContractBootstrapRepairMigrations(events, errors, {
993
+ allowUnmigratedLegacyContractBootstrapRepairMarkers: allowUnmigrated,
994
+ });
995
+ return repairMarkerErrors(events, errors, {
996
+ allowUnmigratedLegacyContractBootstrapRepairMarkers: allowUnmigrated,
997
+ });
998
+ }
999
+
1000
+ function summarizeStateEvents(state, events) {
1001
+ const milestones = ["EXECUTION_STARTED", "VERIFICATION_STARTED", "REVIEW_STARTED", "COMPLETION_VALIDATED"];
1002
+ const observed = new Set();
1003
+ let supportsReviewEvents = false;
1004
+ let latestVerification;
1005
+ let latestReview;
1006
+ let index = 0;
1007
+ for (const event of ledgerEventsOfTypes(events, milestones)) {
1008
+ if (event.taskId !== state.taskId) continue;
1009
+ observed.add(event.event);
1010
+ if (event.event === "VERIFICATION_STARTED") {
1011
+ latestVerification = { event, index };
1012
+ if (Number.isInteger(event.details?.verificationCycle)) supportsReviewEvents = true;
1013
+ }
1014
+ if (event.event === "REVIEW_STARTED") {
1015
+ supportsReviewEvents = true;
1016
+ latestReview = { event, index };
1017
+ }
1018
+ index += 1;
1019
+ }
1020
+ return { observed, supportsReviewEvents, latestVerification, latestReview };
624
1021
  }
625
1022
 
626
1023
  export function validateStateLedgerCoherence(state, events) {
627
- const errors = [];
1024
+ const errors = [
1025
+ ...validateCheckpointRevalidationCurrentBinding(state, events),
1026
+ ...validateContractRevisionCurrentBinding(state, events),
1027
+ ];
628
1028
  if (!Number.isInteger(state.verificationCycle)) return errors;
629
- const taskEvents = events.filter((event) => event.taskId === state.taskId);
630
- const observed = new Set(taskEvents.map((event) => event.event));
631
- const supportsReviewEvents = taskEvents.some((event) => event.event === "REVIEW_STARTED"
632
- || (event.event === "VERIFICATION_STARTED" && Number.isInteger(event.details?.verificationCycle)));
1029
+ const { observed, supportsReviewEvents, latestVerification, latestReview } = summarizeStateEvents(state, events);
633
1030
  const phaseRequirements = {
634
1031
  EXECUTING: ["EXECUTION_STARTED"],
635
1032
  VERIFYING: ["EXECUTION_STARTED", "VERIFICATION_STARTED"],
@@ -647,21 +1044,12 @@ export function validateStateLedgerCoherence(state, events) {
647
1044
  }
648
1045
  }
649
1046
  if (Number.isInteger(state.verificationCycle)) {
650
- const verificationEvents = taskEvents
651
- .map((event, index) => ({ event, index }))
652
- .filter(({ event }) => event.event === "VERIFICATION_STARTED");
653
- const cycles = verificationEvents.map(({ event }) => event.details?.verificationCycle ?? 1);
654
- if (cycles.at(-1) !== state.verificationCycle) {
1047
+ if ((latestVerification ? latestVerification.event.details?.verificationCycle ?? 1 : undefined) !== state.verificationCycle) {
655
1048
  errors.push({
656
1049
  code: "E_STATE_LEDGER_DIVERGENCE",
657
1050
  message: "Work-state verification cycle does not match the lifecycle ledger",
658
1051
  });
659
1052
  }
660
- const latestVerification = verificationEvents.at(-1);
661
- const latestReview = taskEvents
662
- .map((event, index) => ({ event, index }))
663
- .filter(({ event }) => event.event === "REVIEW_STARTED")
664
- .at(-1);
665
1053
  const latestReviewCycle = latestReview?.event.details?.verificationCycle;
666
1054
  if (state.phase === "VERIFYING"
667
1055
  && latestReviewCycle === state.verificationCycle