@cassiomc1/forgeloop 1.14.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 (248) hide show
  1. package/CONTRIBUTING.md +11 -7
  2. package/DOCS_INDEX.md +2 -1
  3. package/ENG/c-development-eng.md +2 -2
  4. package/ENG/cpp-development-eng.md +2 -2
  5. package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
  6. package/ENG/flutter-development-eng.md +4 -2
  7. package/ENG/go-development-eng.md +2 -2
  8. package/ENG/java-development-eng.md +3 -3
  9. package/ENG/nodejs-backend-development-eng.md +8 -4
  10. package/ENG/php-development-eng.md +3 -3
  11. package/ENG/python-development-eng.md +205 -0
  12. package/ENG/rust-development-eng.md +42 -1
  13. package/ENG/sql-development-eng.md +4 -4
  14. package/ENG/swift-development-eng.md +5 -4
  15. package/ENG/test-code-eng.md +1 -1
  16. package/ENG/typescript-development-eng.md +4 -4
  17. package/GUIDE_ROUTER.md +6 -0
  18. package/LICENSE +133 -21
  19. package/LOOP_SYSTEM_DESIGN.md +10 -10
  20. package/README.md +28 -39
  21. package/THIRD_PARTY_NOTICES.md +3 -4
  22. package/completions/_forgeloop +11 -3
  23. package/completions/forgeloop.bash +19 -3
  24. package/completions/forgeloop.fish +61 -1
  25. package/docs/AGENT_PROTOCOL_SUMMARY.md +50 -40
  26. package/docs/ARTIFACT_REFERENCE.md +50 -37
  27. package/docs/CLI_REFERENCE.md +147 -3
  28. package/docs/DOCUMENTATION_GUIDE.md +6 -4
  29. package/docs/GETTING_STARTED.md +3 -3
  30. package/docs/PACKAGE_CONTENTS.md +14 -13
  31. package/docs/RELEASE_CHECKLIST.md +81 -10
  32. package/docs/SELF_HOSTED_TESTS.md +121 -0
  33. package/docs/documentation-manifest.json +25 -12
  34. package/package.json +15 -10
  35. package/schemas/task-bundle.schema.json +7 -2
  36. package/scripts/generate-agent-protocol-summary.mjs +2 -2
  37. package/scripts/run-mcp-clean-tests.mjs +30 -0
  38. package/src/adapters/agent-browser/provider.js +14 -11
  39. package/src/adapters/emulated-services/provider.js +11 -4
  40. package/src/adapters/temporary-paths.js +18 -0
  41. package/src/cli.js +146 -210
  42. package/src/commands/action-show.js +5 -2
  43. package/src/commands/advance.js +5 -1
  44. package/src/commands/baseline.js +13 -11
  45. package/src/commands/checkpoint-revalidate.js +24 -8
  46. package/src/commands/complete.js +19 -3
  47. package/src/commands/contract-create.js +3 -1
  48. package/src/commands/doctor.js +36 -13
  49. package/src/commands/gate-record.js +20 -31
  50. package/src/commands/gate-revalidate.js +11 -13
  51. package/src/commands/handoff-list.js +6 -1
  52. package/src/commands/handoff-show.js +6 -1
  53. package/src/commands/migrate-protocol.js +2 -2
  54. package/src/commands/model-route.js +9 -1
  55. package/src/commands/next.js +7 -1
  56. package/src/commands/policy-diff.js +25 -4
  57. package/src/commands/progress.js +17 -11
  58. package/src/commands/record-diagnosis.js +3 -0
  59. package/src/commands/semantic-plan.js +26 -2
  60. package/src/commands/status.js +6 -1
  61. package/src/commands/storage-backup.js +15 -0
  62. package/src/commands/storage-migrate.js +5 -0
  63. package/src/commands/storage-migration-resume.js +5 -0
  64. package/src/commands/storage-migration-status.js +44 -0
  65. package/src/commands/storage-restore-resume.js +7 -0
  66. package/src/commands/storage-restore.js +8 -0
  67. package/src/commands/task-abandon.js +1 -1
  68. package/src/commands/task-list.js +8 -43
  69. package/src/commands/task-migrate.js +3 -3
  70. package/src/commands/task-recover.js +10 -2
  71. package/src/commands/task-scope.js +18 -2
  72. package/src/commands/task-show.js +16 -2
  73. package/src/commands/validate-protocol.js +29 -14
  74. package/src/commands/validate-receipt.js +8 -1
  75. package/src/config/guides.json +4 -0
  76. package/src/core/action-ledger-projection.js +10 -3
  77. package/src/core/actions.js +38 -75
  78. package/src/core/activation.js +11 -3
  79. package/src/core/approvals.js +17 -51
  80. package/src/core/artifact-registry.js +59 -2
  81. package/src/core/artifacts.js +81 -27
  82. package/src/core/attachment-paths.js +45 -0
  83. package/src/core/attestation-coverage.js +6 -1
  84. package/src/core/attestation-verifier.js +21 -2
  85. package/src/core/attestation.js +17 -6
  86. package/src/core/audit.js +6 -1
  87. package/src/core/bundles.js +160 -66
  88. package/src/core/checkpoint-revalidation.js +12 -10
  89. package/src/core/cli-command-definitions.js +109 -7
  90. package/src/core/code-manifest.js +9 -3
  91. package/src/core/command-executors.js +182 -126
  92. package/src/core/command-input.js +21 -1
  93. package/src/core/completion-artifacts.js +71 -29
  94. package/src/core/completion-ownership.js +19 -8
  95. package/src/core/completion-recovery-rebind.js +11 -1
  96. package/src/core/completion.js +60 -9
  97. package/src/core/conformance.js +2 -1
  98. package/src/core/continuity-lint.js +13 -2
  99. package/src/core/continuity-reconciliation.js +16 -3
  100. package/src/core/continuity.js +16 -8
  101. package/src/core/contract-bootstrap-recovery.js +42 -41
  102. package/src/core/contract-revision.js +8 -9
  103. package/src/core/decision/artifact.js +2 -1
  104. package/src/core/decision/events.js +9 -6
  105. package/src/core/decision/resolver.js +19 -11
  106. package/src/core/decision/service.js +63 -38
  107. package/src/core/decision/task-bindings.js +6 -1
  108. package/src/core/diagnosis-model.js +11 -0
  109. package/src/core/diagnosis.js +10 -1
  110. package/src/core/diagnostic-projection.js +29 -12
  111. package/src/core/diagnostic-record.js +28 -3
  112. package/src/core/efficiency.js +6 -1
  113. package/src/core/events.js +316 -97
  114. package/src/core/execution-prerequisites.js +22 -13
  115. package/src/core/execution.js +26 -40
  116. package/src/core/filesystem.js +11 -0
  117. package/src/core/gate-artifact.js +105 -19
  118. package/src/core/gate-provenance.js +6 -5
  119. package/src/core/handoff.js +26 -14
  120. package/src/core/history.js +39 -35
  121. package/src/core/hypothesis-projection.js +32 -0
  122. package/src/core/information-gain-projection.js +104 -100
  123. package/src/core/inspect.js +12 -3
  124. package/src/core/integration-invocation-policy.js +10 -0
  125. package/src/core/integration-resources.js +41 -34
  126. package/src/core/ledger-event-collection.js +189 -0
  127. package/src/core/ledger-relations.js +23 -0
  128. package/src/core/native-storage.js +12 -0
  129. package/src/core/next-action-continuity.js +31 -9
  130. package/src/core/next-action-phases.js +1 -1
  131. package/src/core/next-action-review-phase.js +2 -2
  132. package/src/core/next-action-verification-phase.js +2 -1
  133. package/src/core/next-action.js +21 -2
  134. package/src/core/phase.js +138 -93
  135. package/src/core/policy-engine.js +12 -13
  136. package/src/core/preflight-consistency.js +72 -59
  137. package/src/core/preflight-loaders.js +19 -12
  138. package/src/core/preflight-model.js +6 -1
  139. package/src/core/preflight.js +29 -12
  140. package/src/core/progress.js +21 -10
  141. package/src/core/protocol-info.js +3 -0
  142. package/src/core/protocol-migration.js +7 -6
  143. package/src/core/reconcile-closure.js +162 -24
  144. package/src/core/recovery-history.js +66 -47
  145. package/src/core/reflection.js +72 -77
  146. package/src/core/responsibility.js +25 -13
  147. package/src/core/route-artifact.js +13 -3
  148. package/src/core/runtime-context.js +5 -0
  149. package/src/core/schema-validation.js +8 -3
  150. package/src/core/settlement.js +12 -1
  151. package/src/core/signing/bundle-file.js +53 -0
  152. package/src/core/signing/bundle-reference.js +15 -0
  153. package/src/core/signing/sigstore.js +60 -33
  154. package/src/core/signing/statement-file.js +27 -0
  155. package/src/core/storage-runtime-registry.js +41 -0
  156. package/src/core/structural-quality/artifacts.js +20 -14
  157. package/src/core/structural-quality/service.js +81 -56
  158. package/src/core/target-layout.js +1 -1
  159. package/src/core/task-claim-state.js +52 -18
  160. package/src/core/task-command.js +21 -1
  161. package/src/core/task-conflict-inspection.js +112 -107
  162. package/src/core/task-descriptor.js +29 -22
  163. package/src/core/task-discovery.js +288 -90
  164. package/src/core/task-lock.js +66 -306
  165. package/src/core/task-migration-validation.js +5 -5
  166. package/src/core/task-migration.js +17 -287
  167. package/src/core/task-snapshot.js +51 -8
  168. package/src/core/task-storage-migration.js +18 -0
  169. package/src/core/templates.js +20 -8
  170. package/src/core/test-intelligence/prune.js +5 -1
  171. package/src/core/trace.js +154 -99
  172. package/src/core/trajectory-evaluation.js +15 -3
  173. package/src/core/trajectory-metrics.js +47 -32
  174. package/src/core/transaction.js +16 -288
  175. package/src/core/verification-scope.js +20 -12
  176. package/src/core/work-state.js +100 -59
  177. package/src/repository-index/binary-manager.js +7 -5
  178. package/src/storage/artifact-bytes.js +22 -0
  179. package/src/storage/attachment-binding.js +11 -0
  180. package/src/storage/attachment-files.js +82 -0
  181. package/src/storage/attachment-references.js +36 -0
  182. package/src/storage/backup.js +53 -0
  183. package/src/storage/capabilities.js +31 -0
  184. package/src/storage/connection-owner.js +119 -0
  185. package/src/storage/connection.js +376 -0
  186. package/src/storage/existing-project-scope.js +45 -0
  187. package/src/storage/export-destination.js +44 -0
  188. package/src/storage/exporter.js +284 -0
  189. package/src/storage/file-durability.js +46 -0
  190. package/src/storage/importer.js +461 -0
  191. package/src/storage/index.js +74 -0
  192. package/src/storage/leases.js +63 -0
  193. package/src/storage/ledger-event-snapshot.js +150 -0
  194. package/src/storage/ledger-relations.js +220 -0
  195. package/src/storage/legacy-maintenance-boundary.js +18 -0
  196. package/src/storage/legacy-signature.js +115 -0
  197. package/src/storage/maintenance-handoff.js +129 -0
  198. package/src/storage/maintenance-owner.js +42 -0
  199. package/src/storage/maintenance.js +310 -0
  200. package/src/storage/metadata-json.js +35 -0
  201. package/src/storage/migration-activate.js +59 -0
  202. package/src/storage/migration-archive.js +57 -0
  203. package/src/storage/migration-attachment-root.js +61 -0
  204. package/src/storage/migration-candidate.js +398 -0
  205. package/src/storage/migration-publication.js +143 -0
  206. package/src/storage/migration-rollback-admission.js +54 -0
  207. package/src/storage/migration-rollback-preparation.js +132 -0
  208. package/src/storage/migration-rollback-publication.js +183 -0
  209. package/src/storage/migration-rollback-release.js +66 -0
  210. package/src/storage/migration-rollback-source-stage.js +103 -0
  211. package/src/storage/migration-rollback.js +43 -0
  212. package/src/storage/migration-source-partition.js +56 -0
  213. package/src/storage/migration-source.js +255 -0
  214. package/src/storage/migration-terminal.js +39 -0
  215. package/src/storage/migration-validation.js +75 -0
  216. package/src/storage/migration.js +43 -0
  217. package/src/storage/operational-attachments.js +49 -0
  218. package/src/storage/operational-context.js +50 -0
  219. package/src/storage/project-backup.js +98 -0
  220. package/src/storage/project-bootstrap.js +24 -0
  221. package/src/storage/project-boundary.js +79 -0
  222. package/src/storage/project-read-snapshot.js +18 -0
  223. package/src/storage/project-replacement.js +137 -0
  224. package/src/storage/project-restore.js +203 -0
  225. package/src/storage/record-definitions.js +45 -0
  226. package/src/storage/repository.js +507 -0
  227. package/src/storage/restore-journal.js +62 -0
  228. package/src/storage/restore-layout.js +30 -0
  229. package/src/storage/restore-preparation.js +84 -0
  230. package/src/storage/restore-replacement-archive.js +162 -0
  231. package/src/storage/restore-replacement-owner.js +31 -0
  232. package/src/storage/restore-replacement-preparation.js +128 -0
  233. package/src/storage/restore-replacement.js +142 -0
  234. package/src/storage/restore-terminal.js +32 -0
  235. package/src/storage/runtime.js +31 -0
  236. package/src/storage/schema.js +214 -0
  237. package/src/storage/singleton-import.js +84 -0
  238. package/src/storage/snapshot-continuity.js +16 -0
  239. package/src/storage/snapshot.js +36 -0
  240. package/src/storage/storage-marker.js +79 -0
  241. package/src/storage/task-guards.js +107 -0
  242. package/src/storage/topology.js +42 -0
  243. package/src/storage/transaction.js +179 -0
  244. package/src/storage/unit-of-work.js +663 -0
  245. package/src/storage/windows-process-incarnation.js +206 -0
  246. package/.forgeloop/forgeloop.gitignore +0 -4
  247. package/LICENSE-DOCS.md +0 -13
  248. 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";
@@ -21,6 +25,7 @@ import {
21
25
 
22
26
  import { taskArtifactPath } from "./task-paths.js";
23
27
  import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
28
+ import { getOperationalStore } from "../storage/operational-context.js";
24
29
 
25
30
  import { assertDiagnosisDetails } from "./diagnosis-model.js";
26
31
  import {
@@ -131,28 +136,12 @@ function validateExecutionGateChronology(events, index, seen, errors) {
131
136
  const latestContractRevisionSeq = events.slice(0, index).findLast((candidate) => candidate.event === CONTRACT_REVISED_EVENT)?.seq ?? 0;
132
137
  const beforeExecution = events.slice(0, index);
133
138
  for (const gate of requiredGates) {
134
- if (!hasCurrentGateEvidence(beforeExecution, events[index]?.taskId, gate, latestContractRevisionSeq)) {
139
+ if (!hasCurrentGateEvidence(beforeExecution, ledgerEventAt(events, index)?.taskId, gate, latestContractRevisionSeq)) {
135
140
  errors.push({ code: "E_PHASE_CHRONOLOGY_INVALID", message: `execution started before gate satisfaction: ${gate}` });
136
141
  }
137
142
  }
138
143
  }
139
144
 
140
- function eventIndexPath(eventsPath) {
141
- return `${eventsPath}.index.json`;
142
- }
143
-
144
- function parseEventIndex(text, relativePath) {
145
- if (typeof text !== "string") return null;
146
- try {
147
- const value = JSON.parse(text);
148
- if (!value || value.schemaVersion !== 1 || !Number.isInteger(value.seq) || value.seq < 0
149
- || (value.lastHash !== null && !/^[a-f0-9]{64}$/.test(value.lastHash))) return null;
150
- return value;
151
- } catch {
152
- return null;
153
- }
154
- }
155
-
156
145
  function checkpointFromEvents(events) {
157
146
  const last = events.at(-1) ?? null;
158
147
  return {
@@ -166,17 +155,7 @@ async function readEventCheckpoint(target, packageRoot, relPath, options, transa
166
155
  if (!transaction.eventCheckpoints) transaction.eventCheckpoints = new Map();
167
156
  const cached = transaction.eventCheckpoints.get(relPath);
168
157
  if (cached) return cached;
169
-
170
- const indexPath = eventIndexPath(relPath);
171
- const indexed = parseEventIndex(await transaction.readText(indexPath), indexPath);
172
- const tail = await readEventTail(target, packageRoot, { ...options, eventsPath: relPath, limit: 1 });
173
- const last = tail.at(-1) ?? null;
174
- let checkpoint;
175
- if (indexed && indexed.seq === (last?.seq ?? 0) && indexed.lastHash === (last?.hash ?? null)) {
176
- checkpoint = indexed;
177
- } else {
178
- checkpoint = checkpointFromEvents(await readEvents(target, packageRoot, { ...options, eventsPath: relPath }));
179
- }
158
+ const checkpoint = checkpointFromEvents(await readEventTail(target, packageRoot, { ...options, eventsPath: relPath, limit: 1 }));
180
159
  transaction.eventCheckpoints.set(relPath, checkpoint);
181
160
  return checkpoint;
182
161
  }
@@ -524,35 +503,79 @@ function protocolError(code, message, artifacts = [ARTIFACT_PATHS.events]) {
524
503
  }
525
504
 
526
505
  export async function readEvents(target, packageRoot, options = {}) {
506
+ if (await needsExistingProjectScope(target)) {
507
+ return withExistingProjectScope(target, () => readEvents(target, packageRoot, options), { readOnly: true });
508
+ }
527
509
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
528
- await assertSafePath(target, relPath);
529
- const eventsPath = ensureWithin(target, relPath);
530
- const transaction = (await getTaskTransaction(target));
531
- if (transaction) {
532
- const stagedText = await transaction.readText(relPath);
533
- 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;
534
521
  }
535
- if (!(await fileExists(eventsPath))) return [];
536
- const text = await readFile(eventsPath, "utf8");
537
- 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
+ })();
538
556
  }
539
557
 
540
558
  export async function readEventTail(target, packageRoot, options = {}) {
559
+ if (await needsExistingProjectScope(target)) {
560
+ return withExistingProjectScope(target, () => readEventTail(target, packageRoot, options), { readOnly: true });
561
+ }
541
562
  const limit = options.limit ?? 50;
542
563
  if (!Number.isInteger(limit) || limit < 1) {
543
564
  throw protocolError("E_EVENT_INVALID", "ledger tail limit must be a positive integer");
544
565
  }
545
566
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
546
- await assertSafePath(target, relPath);
547
- const eventsPath = ensureWithin(target, relPath);
548
- const transaction = (await getTaskTransaction(target));
549
- if (transaction) {
550
- const stagedText = await transaction.readText(relPath);
551
- if (stagedText !== null) {
552
- const lines = stagedText.split(/\r?\n/).filter((line) => line.trim() !== "");
553
- return parseEventsText(lines.slice(-limit).join("\n"), relPath, packageRoot);
554
- }
555
- }
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);
556
579
  if (!(await fileExists(eventsPath))) return [];
557
580
  const size = (await stat(eventsPath)).size;
558
581
  let window = Math.min(size, 64 * 1024);
@@ -600,18 +623,41 @@ async function parseEventsText(text, relPath, packageRoot) {
600
623
  });
601
624
  }
602
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
+
603
641
  export async function appendProtocolEvent(target, input, packageRoot, options = {}) {
604
642
  const activeTransaction = (await getTaskTransaction(target));
605
643
  if (typeof input?.taskId !== "string" || !input.taskId) throw protocolError("E_EVENT_INVALID", "event taskId is required");
606
644
  if (typeof input?.event !== "string" || !input.event) throw protocolError("E_EVENT_INVALID", "event type is required");
607
645
  const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
608
646
  if (!activeTransaction) {
609
- return withTaskTransaction({
610
- target,
611
- taskId: options.taskId ?? input.taskId,
612
- lockTaskId: relPath === ARTIFACT_PATHS.events ? "__legacy-events__" : (options.taskId ?? input.taskId),
613
- operation: "append-event",
614
- }, 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
+ }
615
661
  }
616
662
  const checkpoint = await readEventCheckpoint(target, packageRoot, relPath, options, activeTransaction);
617
663
  const event = buildProtocolEvent(input, { checkpoint });
@@ -619,16 +665,8 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
619
665
  assertSchema(event, schema, relPath);
620
666
  event.hash = eventHash(event);
621
667
  if (!options.dryRun) {
622
- if (activeTransaction) {
623
- await activeTransaction.appendText(relPath, `${JSON.stringify(event)}\n`);
624
- const nextCheckpoint = { schemaVersion: 1, seq: event.seq, lastHash: event.hash };
625
- await activeTransaction.stageText(eventIndexPath(relPath), `${JSON.stringify(nextCheckpoint)}\n`);
626
- activeTransaction.eventCheckpoints.set(relPath, nextCheckpoint);
627
- } else {
628
- const eventsPath = ensureWithin(target, relPath);
629
- await mkdir(path.dirname(eventsPath), { recursive: true });
630
- await appendFile(eventsPath, `${JSON.stringify(event)}\n`, { encoding: "utf8" });
631
- }
668
+ activeTransaction.appendEvent(event);
669
+ activeTransaction.eventCheckpoints.set(relPath, { schemaVersion: 1, seq: event.seq, lastHash: event.hash });
632
670
  }
633
671
  return event;
634
672
  }
@@ -640,8 +678,8 @@ export async function appendProtocolEvent(target, input, packageRoot, options =
640
678
  * before appending the migration events.
641
679
  */
642
680
  function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegacyRecoveryEvents = false } = {}) {
643
- const migrationBySeq = new Map();
644
- for (const event of events) {
681
+ const migrationBySeq = ledgerRelationMap();
682
+ for (const event of ledgerEventsOfTypes(events, [LEGACY_RECOVERY_MIGRATION_EVENT])) {
645
683
  if (event.event !== LEGACY_RECOVERY_MIGRATION_EVENT) continue;
646
684
  try {
647
685
  assertLegacyMigrationDetails(event.details);
@@ -656,11 +694,12 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
656
694
  });
657
695
  continue;
658
696
  }
659
- migrationBySeq.set(event.details.legacyEventSeq, event);
697
+ migrationBySeq.set(event.details.legacyEventSeq, events.indexOf(event));
660
698
  }
661
- for (const event of events) {
699
+ for (const event of ledgerEventsOfTypes(events, ["OPERATOR_RECOVERY_RECORDED"])) {
662
700
  if (!isLegacyRecoveryEventShape(event)) continue;
663
- const migration = migrationBySeq.get(event.seq);
701
+ const migrationPosition = migrationBySeq.get(event.seq);
702
+ const migration = migrationPosition === undefined ? undefined : ledgerEventAt(events, migrationPosition);
664
703
  if (!migration) {
665
704
  if (!allowUnmigratedLegacyRecoveryEvents) {
666
705
  errors.push({
@@ -692,7 +731,8 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
692
731
  });
693
732
  }
694
733
  }
695
- for (const [legacySeq, migration] of migrationBySeq) {
734
+ for (const [legacySeq, position] of migrationBySeq) {
735
+ const migration = ledgerEventAt(events, position);
696
736
  errors.push({
697
737
  code: "E_EVENT_INVALID",
698
738
  message: `migration event ${migration.seq} references unknown legacy recovery event seq ${legacySeq}`,
@@ -700,30 +740,37 @@ function validateLegacyRecoveryMigrations(events, errors, { allowUnmigratedLegac
700
740
  }
701
741
  }
702
742
 
703
- export async function validateEventLedger(target, packageRoot, options = {}) {
704
- const relPath = options?.eventsPath ?? options?.relativePath ?? (options?.taskId ? taskArtifactPath(options.taskId, "events") : ARTIFACT_PATHS.events);
705
- let events;
706
- try {
707
- events = await readEvents(target, packageRoot, { ...options, eventsPath: relPath });
708
- } catch (error) {
709
- return { valid: false, events: [], errors: [{ code: error.code ?? "E_EVENT_INVALID", message: error.message, artifacts: [relPath] }] };
710
- }
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 = {}) {
711
756
  const errors = [];
712
757
  let taskId = null;
758
+ let previousHash = null;
713
759
  const seen = new Set();
714
760
  let lastMilestone = -1;
715
761
  const milestoneCounts = new Map();
716
- const createdHandoffs = new Map();
717
- const acceptedHandoffs = new Set();
762
+ const createdHandoffs = ledgerRelationMap();
763
+ const acceptedHandoffs = ledgerRelationSet();
718
764
  for (const [index, event] of events.entries()) {
719
765
  if (event.seq !== index + 1) {
720
766
  errors.push({ code: "E_EVENT_INVALID", message: `event sequence must be ${index + 1}` });
721
767
  }
722
768
  if (taskId === null) taskId = event.taskId;
723
769
  if (event.taskId !== taskId) errors.push({ code: "E_EVENT_INVALID", message: "event task IDs must remain stable" });
724
- if (event.previousHash !== (index === 0 ? null : events[index - 1].hash)) {
770
+ if (event.previousHash !== previousHash) {
725
771
  errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} previousHash does not match` });
726
772
  }
773
+ previousHash = event.hash;
727
774
  if (event.hash !== eventHash(event)) {
728
775
  errors.push({ code: "E_LEDGER_HASH_INVALID", message: `event ${event.seq} hash does not match its content` });
729
776
  }
@@ -774,11 +821,172 @@ export async function validateEventLedger(target, packageRoot, options = {}) {
774
821
  errors.push(...validateContractRevisionEventBindings(events));
775
822
  errors.push(...validateGateSatisfactionBindings(events));
776
823
  errors.push(...validateSemanticDecisionEventBindings(events));
777
- errors.push(...await validateSemanticDecisionArtifactBindings(target, packageRoot, events));
778
824
  const repairedErrors = validateContractBootstrapRepairLedger(events, errors, options);
779
825
  return { valid: repairedErrors.length === 0, events, errors: repairedErrors };
780
826
  }
781
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
+
782
990
  function validateContractBootstrapRepairLedger(events, errors, options) {
783
991
  const allowUnmigrated = options?.allowUnmigratedLegacyContractBootstrapRepairMarkers === true;
784
992
  validateContractBootstrapRepairMigrations(events, errors, {
@@ -789,16 +997,36 @@ function validateContractBootstrapRepairLedger(events, errors, options) {
789
997
  });
790
998
  }
791
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 };
1021
+ }
1022
+
792
1023
  export function validateStateLedgerCoherence(state, events) {
793
1024
  const errors = [
794
1025
  ...validateCheckpointRevalidationCurrentBinding(state, events),
795
1026
  ...validateContractRevisionCurrentBinding(state, events),
796
1027
  ];
797
1028
  if (!Number.isInteger(state.verificationCycle)) return errors;
798
- const taskEvents = events.filter((event) => event.taskId === state.taskId);
799
- const observed = new Set(taskEvents.map((event) => event.event));
800
- const supportsReviewEvents = taskEvents.some((event) => event.event === "REVIEW_STARTED"
801
- || (event.event === "VERIFICATION_STARTED" && Number.isInteger(event.details?.verificationCycle)));
1029
+ const { observed, supportsReviewEvents, latestVerification, latestReview } = summarizeStateEvents(state, events);
802
1030
  const phaseRequirements = {
803
1031
  EXECUTING: ["EXECUTION_STARTED"],
804
1032
  VERIFYING: ["EXECUTION_STARTED", "VERIFICATION_STARTED"],
@@ -816,21 +1044,12 @@ export function validateStateLedgerCoherence(state, events) {
816
1044
  }
817
1045
  }
818
1046
  if (Number.isInteger(state.verificationCycle)) {
819
- const verificationEvents = taskEvents
820
- .map((event, index) => ({ event, index }))
821
- .filter(({ event }) => event.event === "VERIFICATION_STARTED");
822
- const cycles = verificationEvents.map(({ event }) => event.details?.verificationCycle ?? 1);
823
- if (cycles.at(-1) !== state.verificationCycle) {
1047
+ if ((latestVerification ? latestVerification.event.details?.verificationCycle ?? 1 : undefined) !== state.verificationCycle) {
824
1048
  errors.push({
825
1049
  code: "E_STATE_LEDGER_DIVERGENCE",
826
1050
  message: "Work-state verification cycle does not match the lifecycle ledger",
827
1051
  });
828
1052
  }
829
- const latestVerification = verificationEvents.at(-1);
830
- const latestReview = taskEvents
831
- .map((event, index) => ({ event, index }))
832
- .filter(({ event }) => event.event === "REVIEW_STARTED")
833
- .at(-1);
834
1053
  const latestReviewCycle = latestReview?.event.details?.verificationCycle;
835
1054
  if (state.phase === "VERIFYING"
836
1055
  && latestReviewCycle === state.verificationCycle
@@ -60,6 +60,7 @@ async function load(loadArtifact, code, message, artifacts, errors) {
60
60
  try {
61
61
  return await loadArtifact();
62
62
  } catch (error) {
63
+ if (error.code === "E_STATE_REVISION_CONFLICT") throw error;
63
64
  errors.push(issue(code, `${message}: ${error.message}`, artifacts));
64
65
  return null;
65
66
  }
@@ -146,6 +147,10 @@ function prerequisiteLedgerErrors(ledger, taskId, preflight, route) {
146
147
  return errors;
147
148
  }
148
149
 
150
+ function prerequisiteArtifactPath(explicitPath, taskId, kind) {
151
+ return explicitPath ?? (taskId ? taskArtifactPath(taskId, kind) : ARTIFACT_PATHS[kind]);
152
+ }
153
+
149
154
  export async function evaluateStartExecutionPrerequisites({
150
155
  target,
151
156
  state,
@@ -156,13 +161,14 @@ export async function evaluateStartExecutionPrerequisites({
156
161
  statePath = null,
157
162
  preflightPath = null,
158
163
  eventsPath = null,
164
+ readers = null,
159
165
  } = {}) {
160
166
  const effectiveTaskId = taskId ?? null;
161
- const contractRel = contractPath ?? (effectiveTaskId ? taskArtifactPath(effectiveTaskId, "contract") : ARTIFACT_PATHS.contract);
162
- const routeRel = routePath ?? (effectiveTaskId ? taskArtifactPath(effectiveTaskId, "route") : ARTIFACT_PATHS.route);
163
- const stateRel = statePath ?? (effectiveTaskId ? taskArtifactPath(effectiveTaskId, "state") : ARTIFACT_PATHS.state);
164
- const preflightRel = preflightPath ?? (effectiveTaskId ? taskArtifactPath(effectiveTaskId, "preflight") : ARTIFACT_PATHS.preflight);
165
- const eventsRel = eventsPath ?? (effectiveTaskId ? taskArtifactPath(effectiveTaskId, "events") : ARTIFACT_PATHS.events);
167
+ const contractRel = prerequisiteArtifactPath(contractPath, effectiveTaskId, "contract");
168
+ const routeRel = prerequisiteArtifactPath(routePath, effectiveTaskId, "route");
169
+ const stateRel = prerequisiteArtifactPath(statePath, effectiveTaskId, "state");
170
+ const preflightRel = prerequisiteArtifactPath(preflightPath, effectiveTaskId, "preflight");
171
+ const eventsRel = prerequisiteArtifactPath(eventsPath, effectiveTaskId, "events");
166
172
 
167
173
  const errors = [];
168
174
  const requiredArtifacts = [
@@ -180,14 +186,14 @@ export async function evaluateStartExecutionPrerequisites({
180
186
  }
181
187
 
182
188
  const contract = await load(
183
- () => readContract(target, packageRoot, { taskId: effectiveTaskId, contractPath }),
189
+ () => (readers?.readContract ?? readContract)(target, packageRoot, { taskId: effectiveTaskId, contractPath }),
184
190
  "E_PHASE_PREREQUISITE_MISSING",
185
191
  `EXECUTING requires ${contractRel}`,
186
192
  [contractRel],
187
193
  errors,
188
194
  );
189
195
  const route = await load(
190
- () => readPersistedRoute(target, packageRoot, { taskId: effectiveTaskId, routePath }),
196
+ () => (readers?.readRoute ?? readPersistedRoute)(target, packageRoot, { taskId: effectiveTaskId, routePath }),
191
197
  "E_PHASE_PREREQUISITE_MISSING",
192
198
  `EXECUTING requires ${routeRel}`,
193
199
  [routeRel],
@@ -204,21 +210,24 @@ export async function evaluateStartExecutionPrerequisites({
204
210
  }
205
211
  errors.push(...stateIdentityErrors({ contract, route, state }));
206
212
 
207
- const freshness = await classifyLoadedWorkState({
213
+ const freshness = await (readers?.classifyState ?? classifyLoadedWorkState)({
208
214
  target,
209
215
  state,
210
216
  contractFile: contractRel,
211
217
  });
212
218
  errors.push(...freshnessErrors(state, freshness));
213
219
 
214
- const preflight = await evaluatePreflight({ target, packageRoot, taskId: effectiveTaskId, contractPath, routePath, statePath });
220
+ const preflight = await evaluatePreflight({ target, packageRoot, taskId: effectiveTaskId, contractPath, routePath, statePath, readers });
215
221
  let persistedPreflight = null;
222
+ let persistedPreflightReadError = null;
216
223
  try {
217
- persistedPreflight = await readJsonArtifact(target, preflightRel, "preflight", packageRoot);
218
- } catch {
224
+ persistedPreflight = await (readers?.readPreflight ?? readJsonArtifact)(target, preflightRel, "preflight", packageRoot);
225
+ } catch (error) {
226
+ if (error.code === "E_STATE_REVISION_CONFLICT") throw error;
227
+ persistedPreflightReadError = error;
219
228
  // validatePersistedPreflight reports the stable, actionable preflight reason.
220
229
  }
221
- const persistedPreflightErrors = validatePersistedPreflight(persistedPreflight?.value, preflight);
230
+ const persistedPreflightErrors = validatePersistedPreflight(persistedPreflight?.value, preflight, persistedPreflightReadError);
222
231
  if (!sameStringSet(state.requiredGates, preflight.requiredGates)
223
232
  || !sameStringSet(state.satisfiedGates, preflight.satisfiedGates)) {
224
233
  errors.push(issue(
@@ -228,7 +237,7 @@ export async function evaluateStartExecutionPrerequisites({
228
237
  ));
229
238
  }
230
239
 
231
- const ledger = await validateEventLedger(target, packageRoot, { taskId: effectiveTaskId, eventsPath });
240
+ const ledger = await (readers?.validateLedger ?? validateEventLedger)(target, packageRoot, { taskId: effectiveTaskId, eventsPath });
232
241
  errors.push(...prerequisiteLedgerErrors(ledger, contract.value.taskId, preflight, route));
233
242
  errors.push(...validateStateLedgerCoherence(state, ledger.events).map((error) => issue(
234
243
  error.code,