@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,9 +1,10 @@
1
1
  import { E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED } from "./error-codes.js";
2
2
  import { PROTOCOL_VERSION } from "./protocol.js";
3
- import { detectLegacySingletonLayout, migrateLegacyLayout } from "./task-migration.js";
3
+ import { detectLegacySingletonLayout } from "./task-migration.js";
4
+ import { migrateLegacyTaskStorage } from "./task-storage-migration.js";
4
5
  import { getPackageRoot } from "./templates.js";
5
6
 
6
- function assertSupportedTarget(to) {
7
+ export function assertSupportedProtocolMigrationTarget(to) {
7
8
  if (to === undefined || to === null || to === "") {
8
9
  const error = new Error("migrate-protocol requires --to <protocolVersion>");
9
10
  error.code = E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED;
@@ -26,9 +27,9 @@ function assertSupportedTarget(to) {
26
27
  */
27
28
  export async function migrateProtocol(
28
29
  target,
29
- { to, dryRun = false, packageRoot = getPackageRoot() } = {},
30
+ { to, dryRun = false, packageRoot = getPackageRoot(), destination, writersQuiesced = false } = {},
30
31
  ) {
31
- assertSupportedTarget(to);
32
+ assertSupportedProtocolMigrationTarget(to);
32
33
  const legacy = await detectLegacySingletonLayout(target);
33
34
 
34
35
  if (!legacy.hasLegacy) {
@@ -43,7 +44,7 @@ export async function migrateProtocol(
43
44
  };
44
45
  }
45
46
 
46
- const result = await migrateLegacyLayout(target, { dryRun, packageRoot });
47
+ const result = await migrateLegacyTaskStorage(target, { dryRun, packageRoot, destination, writersQuiesced });
47
48
  return {
48
49
  ...result,
49
50
  fromProtocol: PROTOCOL_VERSION,
@@ -52,7 +53,7 @@ export async function migrateProtocol(
52
53
  actions: [{
53
54
  kind: "LEGACY_LAYOUT_MIGRATION",
54
55
  command: "task-migrate",
55
- receipt: ".forgeloop/task-state/<taskKey>/migration-receipt.json",
56
+ receipt: result.migrationReceipt ?? "<destination>/publication/publication-journal.json",
56
57
  artifacts: legacy.legacyFiles.map((item) => item.path),
57
58
  }],
58
59
  };
@@ -30,6 +30,9 @@ export const FAILURE_CODES = Object.freeze([
30
30
  "E_GATE_REQUIRED",
31
31
  "E_GATE_UNVERIFIED",
32
32
  "E_GATE_STALE",
33
+ "E_GATE_NOT_REQUIRED",
34
+ "E_GATE_INVALID",
35
+ "E_PHASE_FREEZE",
33
36
  "E_PHASE_TRANSITION_INVALID",
34
37
  "E_PHASE_PREREQUISITE_MISSING",
35
38
  "E_PHASE_CHRONOLOGY_INVALID",
@@ -113,6 +116,23 @@ export const FAILURE_CODES = Object.freeze([
113
116
  "E_INTERVENTION_HYPOTHESIS_MISSING",
114
117
  "E_INTERVENTION_REFERENCE_INVALID",
115
118
  "E_STRATEGY_OSCILLATION",
119
+ "E_DECISION_ENGINE_UNAVAILABLE",
120
+ "E_DECISION_ENGINE_AUTH_REQUIRED",
121
+ "E_DECISION_ENGINE_AUTH_INVALID",
122
+ "E_DECISION_MODEL_UNSUPPORTED",
123
+ "E_DECISION_REQUEST_INVALID",
124
+ "E_DECISION_RESULT_INVALID",
125
+ "E_DECISION_TIMEOUT",
126
+ "E_DECISION_RATE_LIMITED",
127
+ "E_DECISION_STATE_UNSAFE",
128
+ "E_DECISION_STATE_LIMIT",
129
+ "E_DECISION_LOW_CONFIDENCE",
130
+ "E_DECISION_STALE",
131
+ "E_DECISION_POLICY_INVALID",
132
+ "E_DECISION_CACHE_INVALID",
133
+ "E_DECISION_QUESTION_SET_UNKNOWN",
134
+ "E_DECISION_QUESTION_SET_STALE",
135
+ "E_DECISION_REQUIRED",
116
136
  "E_DECISION_CRITERION_INVALID",
117
137
  "E_DECISION_NOT_UNRESOLVED",
118
138
  ]);
@@ -1,19 +1,81 @@
1
+ import { withTaskTransaction } from "./transaction.js";
1
2
  import { readContract } from "./contract.js";
2
3
  import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
- import { appendProtocolEvent, validateEventLedger } from "./events.js";
4
+ import { appendProtocolEvent, validateCompletionRecoveryAuthorization, validateEventLedger } from "./events.js";
4
5
  import { authorizeCompletionRecoveryOrRebind } from "./completion-recovery-rebind.js";
5
6
  import { runCommandExecution } from "./execution.js";
6
7
  import { createReceipt } from "./receipt.js";
7
8
  import { currentRepositoryFingerprint } from "./repository.js";
8
9
  import { taskArtifactPath } from "./task-paths.js";
10
+ import { resolveTaskClaimState } from "./task-claim-state.js";
11
+ import { DIAGNOSTIC_SCHEMA_VERSION, diagnosticSemanticFingerprint } from "./diagnostic-model.js";
9
12
  import { classifyLoadedWorkState, readWorkState, mutateWorkState } from "./work-state.js";
10
13
  import { classifyRequirement } from "./evidence-readiness.js";
14
+ import { assertDiagnosingToCorrectingTransition } from "./phase.js";
15
+ import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
11
16
 
12
17
  export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
13
18
 
14
- const RECONCILABLE_DRIFT = new Set(["REPOSITORY_CHANGED"]);
19
+ /**
20
+ * Build the schema-valid rebound diagnostic case for a drifted DIAGNOSING task
21
+ * that entered the phase before its cycle recorded any diagnosis. The case
22
+ * binds every observation to the just-passed contract-bound evidence execution
23
+ * so the diagnosis carries explicit provenance instead of being manufactured
24
+ * by recovery: the hypothesis is the recovery statement, settled by the
25
+ * successful evidence, with the next safe action being the canonical advance.
26
+ */
27
+ function reconcileReboundDiagnosticCase({
28
+ taskId,
29
+ verificationCycle,
30
+ checkId,
31
+ requirement,
32
+ command,
33
+ executionId,
34
+ repositoryHead,
35
+ }) {
36
+ const statementLimit = 2000;
37
+ const observations = [{
38
+ id: "obs-reconcile-evidence",
39
+ kind: "CHECK_RESULT",
40
+ evidenceRef: checkId,
41
+ provenance: "FORGELOOP_EXECUTED",
42
+ statement: `Contract-bound evidence ${checkId} passed in the current repository (${repositoryHead}); execution ${executionId}: ${command}`.slice(0, statementLimit),
43
+ }];
44
+ const hypotheses = [{
45
+ id: "h-reconcile-rebind",
46
+ statement: `Repository moved after the task entered DIAGNOSING but before the cycle recorded any diagnosis; cycle ${verificationCycle} diagnosis is rebound from the passed contract-bound evidence requirement "${requirement}".`.slice(0, statementLimit),
47
+ contributorRefs: [],
48
+ evidenceRefs: [checkId],
49
+ settledBy: { type: "CHECK_STATUS", checkId, expectedStatus: "passed" },
50
+ status: "OPEN",
51
+ }];
52
+ const details = {
53
+ schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
54
+ verificationCycle,
55
+ diagnosticRevision: 1,
56
+ failureClass: "VERIFICATION_FAILURE",
57
+ observations,
58
+ contributors: [],
59
+ hypotheses,
60
+ nextSafeAction: {
61
+ statement: "Advance DIAGNOSING -> CORRECTING -> VERIFYING and continue the canonical verification pipeline in the reconciled repository.".slice(0, statementLimit),
62
+ },
63
+ };
64
+ details.diagnosticFingerprint = diagnosticSemanticFingerprint({
65
+ verificationCycle: details.verificationCycle,
66
+ failureClass: details.failureClass,
67
+ case_: details,
68
+ });
69
+ details.previousDiagnosticFingerprint = null;
70
+ return details;
71
+ }
15
72
 
16
- const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "REVIEWING"]);
73
+ // DIAGNOSING is reconcilable under the same diagnosis-boundary condition as
74
+ // CORRECTING: the active cycle must already carry an append-only diagnosis.
75
+ // Without it, drift entering DIAGNOSING before the cycle's first diagnosis is
76
+ // unrecoverable (reconcile-closure, task-recover, handoff, and every mutation
77
+ // all refuse), which stranded real drifted tasks on this exact edge.
78
+ const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "DIAGNOSING", "CORRECTING", "REVIEWING"]);
17
79
 
18
80
  function reconcileError(code, message, artifacts = []) {
19
81
  const error = new Error(message);
@@ -22,14 +84,171 @@ function reconcileError(code, message, artifacts = []) {
22
84
  return error;
23
85
  }
24
86
 
87
+ function assertRepositoryOnlyFreshness(freshness, stateRel, contractRel) {
88
+ if (freshness.status !== "REVALIDATION_REQUIRED") {
89
+ throw reconcileError(
90
+ "E_RECONCILE_NOT_STALE",
91
+ `work-state checkpoint is ${freshness.status === "FRESH" ? "fresh" : "not revalidation-required"}; no reconciliation required`,
92
+ [stateRel, contractRel],
93
+ );
94
+ }
95
+ if (freshness.reasons.length === 1 && freshness.reasons[0] === "REPOSITORY_CHANGED") return;
96
+ throw reconcileError(
97
+ "E_RECONCILE_UNSUPPORTED_DRIFT",
98
+ `reconcile-closure only reconciles repository fingerprint drift; unresolved drift: ${freshness.reasons.join(", ")}`,
99
+ [stateRel, contractRel],
100
+ );
101
+ }
102
+
103
+ async function assertFreshReviewingRecoveryIsAuthorized({
104
+ target,
105
+ packageRoot,
106
+ taskId,
107
+ state,
108
+ eventsRel,
109
+ receiptRel,
110
+ }) {
111
+ if (state.phase !== "REVIEWING" || !Object.prototype.hasOwnProperty.call(state, "lastCompletionAttempt")) return;
112
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
113
+ if (!ledger.valid) return;
114
+ let receipt = null;
115
+ try {
116
+ receipt = (await readJsonArtifact(target, receiptRel, "execution-receipt", packageRoot)).value;
117
+ } catch (error) {
118
+ if (error.code !== "ARTIFACT_MISSING") throw error;
119
+ }
120
+ const recoveryAuth = validateCompletionRecoveryAuthorization({ state, receipt, events: ledger.events });
121
+ if (!recoveryAuth.authorized) {
122
+ const first = recoveryAuth.errors?.[0] ?? {};
123
+ throw reconcileError(
124
+ first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
125
+ `REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
126
+ [eventsRel, receiptRel],
127
+ );
128
+ }
129
+ }
130
+
131
+ async function validateReconciliationCheckpoint({
132
+ target,
133
+ packageRoot,
134
+ taskId,
135
+ state,
136
+ stateRel,
137
+ contractRel,
138
+ eventsRel,
139
+ receiptRel,
140
+ authorityContext,
141
+ runtimeContext,
142
+ }) {
143
+ const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
144
+ if (freshness.status !== "REVALIDATION_REQUIRED") {
145
+ await assertFreshReviewingRecoveryIsAuthorized({
146
+ target,
147
+ packageRoot,
148
+ taskId,
149
+ state,
150
+ eventsRel,
151
+ receiptRel,
152
+ });
153
+ }
154
+ assertRepositoryOnlyFreshness(freshness, stateRel, contractRel);
155
+
156
+ const ledger = await validateEventLedger(target, packageRoot, { taskId });
157
+ if (!ledger.valid) {
158
+ const first = ledger.errors[0];
159
+ throw reconcileError(
160
+ "E_RECONCILE_LEDGER_INVALID",
161
+ `append-only event ledger must be valid before reconciliation: ${first?.message ?? "invalid ledger"}`,
162
+ [eventsRel],
163
+ );
164
+ }
165
+
166
+ await assertReconciliationClaimOwnership(target, { taskId, packageRoot, state, stateRel, eventsRel });
167
+
168
+ const reboundOrState = await rebindReconciliationState(target, {
169
+ packageRoot,
170
+ taskId,
171
+ state,
172
+ ledger,
173
+ authorityContext,
174
+ runtimeContext,
175
+ stateRel,
176
+ receiptRel,
177
+ });
178
+ return reboundOrState;
179
+ }
180
+
181
+ /** Fail closed unless the task claim is valid, mutable, and ACTIVE in this checkout. */
182
+ async function assertReconciliationClaimOwnership(target, { taskId, packageRoot, state, stateRel, eventsRel }) {
183
+ const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
184
+ if (!ownership.ownershipValid || !ownership.mutationAllowed || ownership.claimState !== "ACTIVE") {
185
+ const first = ownership.ownershipErrors?.[0] ?? ownership.errors?.[0] ?? {};
186
+ throw reconcileError(
187
+ first.code ?? "E_TASK_CLAIM_OWNERSHIP_INCONSISTENT",
188
+ `reconcile-closure requires active, valid task claim ownership: ${first.message ?? ownership.claimState}`,
189
+ [stateRel, eventsRel],
190
+ );
191
+ }
192
+ }
193
+
194
+ /** Handle the phase-bound diagnosis rebind gates for CORRECTING/DIAGNOSING and the REVIEWING recovery authorization.
195
+ * Returns the possibly-rebound state (or the original state when no rebind applies). */
196
+ async function rebindReconciliationState(target, {
197
+ packageRoot,
198
+ taskId,
199
+ state,
200
+ ledger,
201
+ authorityContext,
202
+ runtimeContext,
203
+ stateRel,
204
+ receiptRel,
205
+ }) {
206
+ if (state.phase === "CORRECTING" || state.phase === "DIAGNOSING") {
207
+ // The diagnosis boundary for CORRECTING stays absolute: recovery can never
208
+ // manufacture a diagnosis or authorize a retry the ordinary correction
209
+ // transition would refuse. A drifted DIAGNOSING task without a cycle
210
+ // diagnosis is the one legal exception: the drift struck between entering
211
+ // DIAGNOSING and recording the cycle's first diagnosis, and no other
212
+ // command can create either the diagnosis or the fresh checkpoint. Such a
213
+ // task reconciles only after contract-bound evidence passes, and the
214
+ // reconcile itself records the cycle diagnosis below (provenance bound to
215
+ // the passed evidence execution).
216
+ const cycleDiagnosis = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, state.verificationCycle ?? 1);
217
+ if (state.phase === "DIAGNOSING" && !cycleDiagnosis) {
218
+ return { ...state, undiagnosedDiagnosingRebind: true };
219
+ }
220
+ assertDiagnosingToCorrectingTransition({
221
+ state,
222
+ events: ledger.events,
223
+ resolveDiagnosis: resolveCurrentCycleDiagnostic,
224
+ });
225
+ }
226
+
227
+ if (state.phase !== "REVIEWING" || !Object.prototype.hasOwnProperty.call(state, "lastCompletionAttempt")) {
228
+ return state;
229
+ }
230
+
231
+ const recovery = await authorizeCompletionRecoveryOrRebind({ target, packageRoot, taskId, authorityContext, runtimeContext });
232
+ if (!recovery.recoveryAuth.authorized) {
233
+ const first = recovery.recoveryAuth.errors?.[0] ?? {};
234
+ throw reconcileError(
235
+ first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
236
+ `REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
237
+ [stateRel, receiptRel],
238
+ );
239
+ }
240
+ return recovery.rebound ? recovery.state : state;
241
+ }
242
+
25
243
  /**
26
- * Canonical recovery for an EXECUTING, VERIFYING, or REVIEWING task whose
244
+ * Canonical recovery for an EXECUTING, VERIFYING, CORRECTING, or REVIEWING task whose
27
245
  * objective is already satisfied in the current repository but whose
28
246
  * work-state checkpoint is stale because the repository fingerprint moved.
29
247
  *
30
248
  * The command refreshes the checkpoint repository fingerprint only after:
31
- * - the task is EXECUTING, VERIFYING, or (with authorized completion
32
- * recovery) REVIEWING,
249
+ * - the task is EXECUTING, VERIFYING, CORRECTING, or REVIEWING (with either a
250
+ * persisted completion rejection or the narrow repository-only bootstrap
251
+ * path),
33
252
  * - classification requires revalidation and the only drift is
34
253
  * REPOSITORY_CHANGED,
35
254
  * - the append-only event ledger is valid,
@@ -41,7 +260,14 @@ function reconcileError(code, message, artifacts = []) {
41
260
  * VERIFYING, prepare-completion, record-check, complete); claims are
42
261
  * released only by canonical COMPLETE.
43
262
  */
44
- export async function runReconcileClosure({
263
+ export async function runReconcileClosure(options = {}) {
264
+ if (typeof options.taskId !== "string" || !options.taskId.trim()) return reconcileSelectedClosure(options);
265
+ return withTaskTransaction({ target: options.target, packageRoot: options.packageRoot,
266
+ taskId: options.taskId, operation: "reconcile-closure", recordCommitEvent: true },
267
+ () => reconcileSelectedClosure(options));
268
+ }
269
+
270
+ async function reconcileSelectedClosure({
45
271
  target,
46
272
  packageRoot,
47
273
  taskId,
@@ -52,18 +278,7 @@ export async function runReconcileClosure({
52
278
  authorityContext,
53
279
  runtimeContext,
54
280
  } = {}) {
55
- if (typeof taskId !== "string" || !taskId.trim()) {
56
- throw reconcileError("E_TASK_REQUIRED", "reconcile-closure requires --task", []);
57
- }
58
- if (typeof checkId !== "string" || !checkId.trim()) {
59
- throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --id", []);
60
- }
61
- if (typeof requirement !== "string" || !requirement.trim()) {
62
- throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --requirement", []);
63
- }
64
- if (!Array.isArray(argv) || argv.length === 0) {
65
- throw reconcileError("E_RECONCILE_EVIDENCE_FAILED", "reconcile-closure requires -- followed by the evidence command argv", []);
66
- }
281
+ requireReconcileInputs({ taskId, checkId, requirement, argv });
67
282
 
68
283
  const stateRel = taskArtifactPath(taskId, "state");
69
284
  const contractRel = taskArtifactPath(taskId, "contract");
@@ -77,57 +292,22 @@ export async function runReconcileClosure({
77
292
  if (!RECONCILABLE_PHASES.has(state.phase)) {
78
293
  throw reconcileError(
79
294
  "E_RECONCILE_PHASE_INVALID",
80
- `reconcile-closure supports EXECUTING, VERIFYING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
295
+ `reconcile-closure supports EXECUTING, VERIFYING, DIAGNOSING, CORRECTING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
81
296
  [stateRel],
82
297
  );
83
298
  }
84
- if (state.phase === "REVIEWING") {
85
- const recovery = await authorizeCompletionRecoveryOrRebind({
86
- target,
87
- packageRoot,
88
- taskId,
89
- authorityContext,
90
- runtimeContext,
91
- });
92
- if (!recovery.recoveryAuth.authorized) {
93
- const first = recovery.recoveryAuth.errors?.[0] ?? {};
94
- throw reconcileError(
95
- first.code ?? "E_COMPLETION_RECOVERY_UNAUTHORIZED",
96
- `REVIEWING reconciliation requires authorized completion recovery: ${first.message ?? "unauthorized"}`,
97
- [stateRel, receiptRel],
98
- );
99
- }
100
- if (recovery.rebound) {
101
- state = recovery.state;
102
- }
103
- }
104
-
105
- const freshness = await classifyLoadedWorkState({ target, state, contractFile: contractRel });
106
- if (freshness.status !== "REVALIDATION_REQUIRED" || !freshness.reasons.includes("REPOSITORY_CHANGED")) {
107
- throw reconcileError(
108
- "E_RECONCILE_NOT_STALE",
109
- `work-state checkpoint is ${freshness.status === "FRESH" ? "fresh" : "not revalidation-required"}; no reconciliation required`,
110
- [stateRel, contractRel],
111
- );
112
- }
113
- const unsupported = freshness.reasons.filter((reason) => !RECONCILABLE_DRIFT.has(reason));
114
- if (unsupported.length > 0) {
115
- throw reconcileError(
116
- "E_RECONCILE_UNSUPPORTED_DRIFT",
117
- `reconcile-closure only reconciles repository fingerprint drift; unresolved drift: ${unsupported.join(", ")}`,
118
- [stateRel, contractRel],
119
- );
120
- }
121
-
122
- const ledger = await validateEventLedger(target, packageRoot, { taskId });
123
- if (!ledger.valid) {
124
- const first = ledger.errors[0];
125
- throw reconcileError(
126
- "E_RECONCILE_LEDGER_INVALID",
127
- `append-only event ledger must be valid before reconciliation: ${first?.message ?? "invalid ledger"}`,
128
- [eventsRel],
129
- );
130
- }
299
+ state = await validateReconciliationCheckpoint({
300
+ target,
301
+ packageRoot,
302
+ taskId,
303
+ state,
304
+ stateRel,
305
+ contractRel,
306
+ eventsRel,
307
+ receiptRel,
308
+ authorityContext,
309
+ runtimeContext,
310
+ });
131
311
 
132
312
  const contract = await readContract(target, packageRoot, { taskId });
133
313
  const verificationItem = (contract.value.verification ?? []).find((item) => {
@@ -169,6 +349,24 @@ export async function runReconcileClosure({
169
349
  const repository = await currentRepositoryFingerprint(target);
170
350
  const previous = state.repositoryFingerprint;
171
351
 
352
+ const rebindDiagnosis = Boolean(state.undiagnosedDiagnosingRebind);
353
+ if (rebindDiagnosis) {
354
+ delete state.undiagnosedDiagnosingRebind;
355
+ await appendProtocolEvent(target, {
356
+ taskId,
357
+ event: "DIAGNOSTIC_CASE_RECORDED",
358
+ details: reconcileReboundDiagnosticCase({
359
+ taskId,
360
+ verificationCycle: state.verificationCycle ?? 1,
361
+ checkId,
362
+ requirement,
363
+ command: argv.join(" "),
364
+ executionId: execution.execution.executionId,
365
+ repositoryHead: repository.head,
366
+ }),
367
+ }, packageRoot, { taskId });
368
+ }
369
+
172
370
  await appendProtocolEvent(target, {
173
371
  taskId,
174
372
  event: RECONCILE_EVENT,
@@ -219,3 +417,19 @@ export async function runReconcileClosure({
219
417
  event: RECONCILE_EVENT,
220
418
  };
221
419
  }
420
+
421
+ /** Validate the reconcile arguments exactly as before; shared by the phase-independent input gate. */
422
+ function requireReconcileInputs({ taskId, checkId, requirement, argv } = {}) {
423
+ if (typeof taskId !== "string" || !taskId.trim()) {
424
+ throw reconcileError("E_TASK_REQUIRED", "reconcile-closure requires --task", []);
425
+ }
426
+ if (typeof checkId !== "string" || !checkId.trim()) {
427
+ throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --id", []);
428
+ }
429
+ if (typeof requirement !== "string" || !requirement.trim()) {
430
+ throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --requirement", []);
431
+ }
432
+ if (!Array.isArray(argv) || argv.length === 0) {
433
+ throw reconcileError("E_RECONCILE_EVIDENCE_FAILED", "reconcile-closure requires -- followed by the evidence command argv", []);
434
+ }
435
+ }
@@ -1,3 +1,5 @@
1
+ import { ledgerEventsOfTypes } from "./ledger-event-collection.js";
2
+ import { ledgerRelationSet } from "./ledger-relations.js";
1
3
  import {
2
4
  LEGACY_RECOVERY_MIGRATION_EVENT,
3
5
  isLegacyRecoveryEventShape,
@@ -6,6 +8,7 @@ import {
6
8
  const RECOVERY_EVENT_TYPES = new Set([
7
9
  "TASK_RECOVERY_RECORDED",
8
10
  "OPERATOR_RECOVERY_RECORDED",
11
+ "TASK_ABANDONED",
9
12
  LEGACY_RECOVERY_MIGRATION_EVENT,
10
13
  ]);
11
14
 
@@ -22,56 +25,65 @@ function recoveryIdOf(event) {
22
25
  : null;
23
26
  }
24
27
 
25
- export function classifyRecoveryHistory(events = []) {
28
+ function recoveryCandidate(event, events, seenRecoveryIds, activeCycle, errors) {
29
+ // The known legacy defect signature is never an owning recovery cycle by
30
+ // itself. It remains non-owning historical evidence; ownership comes
31
+ // exclusively from its valid LEGACY_RECOVERY_MIGRATION_RECORDED event,
32
+ // which is validated (binding, uniqueness) by the ledger validator.
33
+ if (!event.details?.recoveryId && isLegacyRecoveryEventShape(event)) return null;
34
+ // A migration event only counts as a canonical recovery cycle when it
35
+ // binds an actual legacy event present earlier in this ledger.
36
+ if (event.event === LEGACY_RECOVERY_MIGRATION_EVENT
37
+ && !events.some((candidate) => candidate.seq === event.details?.legacyEventSeq
38
+ && isLegacyRecoveryEventShape(candidate))) {
39
+ errors.push(recoveryError(
40
+ `Legacy migration event at seq ${event?.seq ?? "unknown"} does not bind a legacy recovery event in this ledger`,
41
+ ));
42
+ return null;
43
+ }
44
+ const recoveryId = recoveryIdOf(event);
45
+ if (!recoveryId) {
46
+ errors.push(recoveryError(`Recovery event at seq ${event?.seq ?? "unknown"} has no recoveryId`));
47
+ return null;
48
+ }
49
+ if (seenRecoveryIds.has(recoveryId)) {
50
+ errors.push(recoveryError(`Recovery event at seq ${event.seq} reuses recovery id ${recoveryId}`));
51
+ return null;
52
+ }
53
+ if (activeCycle) {
54
+ errors.push(recoveryError(
55
+ `Recovery ${recoveryId} was recorded while recovery ${activeCycle.recoveryId} is unresolved`,
56
+ ));
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ recoveryId,
62
+ recoveryEventSeq: event.seq,
63
+ resumedEventSeq: null,
64
+ active: true,
65
+ event,
66
+ resumedEvent: null,
67
+ };
68
+ }
69
+
70
+ function classifyHistory(events, { retainCycles = true, onRecovery = null } = {}) {
26
71
  const recoveries = [];
27
72
  const completedRecoveries = [];
28
73
  const errors = [];
29
- const seenRecoveryIds = new Set();
74
+ const seenRecoveryIds = ledgerRelationSet();
30
75
  let activeCycle = null;
76
+ let completedRecoveryCount = 0;
31
77
 
32
- for (const event of events) {
78
+ const relevant = Array.isArray(events) ? events : ledgerEventsOfTypes(events, [...RECOVERY_EVENT_TYPES, "TASK_RECOVERY_RESUMED"]);
79
+ for (const event of relevant) {
33
80
  if (RECOVERY_EVENT_TYPES.has(event?.event)) {
34
- // The known legacy defect signature is never an owning recovery cycle by
35
- // itself. It remains non-owning historical evidence; ownership comes
36
- // exclusively from its valid LEGACY_RECOVERY_MIGRATION_RECORDED event,
37
- // which is validated (binding, uniqueness) by the ledger validator.
38
- if (!event.details?.recoveryId && isLegacyRecoveryEventShape(event)) continue;
39
- // A migration event only counts as a canonical recovery cycle when it
40
- // binds an actual legacy event present earlier in this ledger.
41
- if (event.event === LEGACY_RECOVERY_MIGRATION_EVENT
42
- && !events.some((candidate) => candidate.seq === event.details?.legacyEventSeq
43
- && isLegacyRecoveryEventShape(candidate))) {
44
- errors.push(recoveryError(
45
- `Legacy migration event at seq ${event?.seq ?? "unknown"} does not bind a legacy recovery event in this ledger`,
46
- ));
47
- continue;
48
- }
49
- const recoveryId = recoveryIdOf(event);
50
- if (!recoveryId) {
51
- errors.push(recoveryError(`Recovery event at seq ${event?.seq ?? "unknown"} has no recoveryId`));
52
- continue;
53
- }
54
- if (seenRecoveryIds.has(recoveryId)) {
55
- errors.push(recoveryError(`Recovery event at seq ${event.seq} reuses recovery id ${recoveryId}`));
56
- continue;
57
- }
58
- if (activeCycle) {
59
- errors.push(recoveryError(
60
- `Recovery ${recoveryId} was recorded while recovery ${activeCycle.recoveryId} is unresolved`,
61
- ));
62
- continue;
63
- }
64
-
65
- activeCycle = {
66
- recoveryId,
67
- recoveryEventSeq: event.seq,
68
- resumedEventSeq: null,
69
- active: true,
70
- event,
71
- resumedEvent: null,
72
- };
73
- seenRecoveryIds.add(recoveryId);
74
- recoveries.push(activeCycle);
81
+ const candidate = recoveryCandidate(event, events, seenRecoveryIds, activeCycle, errors);
82
+ if (!candidate) continue;
83
+ activeCycle = candidate;
84
+ seenRecoveryIds.add(activeCycle.recoveryId);
85
+ if (retainCycles) recoveries.push(activeCycle);
86
+ if (onRecovery) onRecovery(activeCycle);
75
87
  continue;
76
88
  }
77
89
 
@@ -94,23 +106,31 @@ export function classifyRecoveryHistory(events = []) {
94
106
  activeCycle.resumedEventSeq = event.seq;
95
107
  activeCycle.resumedEvent = event;
96
108
  activeCycle.active = false;
97
- completedRecoveries.push(activeCycle);
109
+ completedRecoveryCount += 1;
110
+ if (retainCycles) completedRecoveries.push(activeCycle);
98
111
  activeCycle = null;
99
112
  }
100
113
 
101
114
  const valid = errors.length === 0;
102
115
  return {
103
- recoveries,
116
+ ...(retainCycles ? { recoveries } : {}),
104
117
  activeRecoveryId: valid ? activeCycle?.recoveryId ?? null : null,
105
118
  activeRecovery: valid && activeCycle
106
119
  ? { recoveryId: activeCycle.recoveryId, event: activeCycle.event }
107
120
  : null,
108
- completedRecoveries,
121
+ ...(retainCycles ? { completedRecoveries } : { completedRecoveryCount }),
109
122
  valid,
110
123
  errors,
111
124
  };
112
125
  }
113
126
 
127
+ /** Public evidence keeps its historical arrays. Internal classification can stream claims. */
128
+ export function classifyRecoveryHistory(events = []) { return classifyHistory(events); }
129
+
130
+ export function summarizeRecoveryHistory(events, onRecovery) {
131
+ return classifyHistory(events, { retainCycles: false, onRecovery });
132
+ }
133
+
114
134
  export function resolveRecoveryHistory(events = []) {
115
135
  return classifyRecoveryHistory(events);
116
136
  }