@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,3 +1,5 @@
1
+ import { ledgerRelationMap } from "./ledger-relations.js";
2
+ import { isLedgerEventCollection, ledgerEventAt, ledgerEventAtIs, ledgerEventsOfTypes, ledgerTypeSummary } from "./ledger-event-collection.js";
1
3
  import { canonicalFingerprint } from "./artifacts.js";
2
4
 
3
5
  export const CONTRACT_BOOTSTRAP_REPAIR_EVENT = "CONTRACT_BOOTSTRAP_REPAIR_RECORDED";
@@ -212,13 +214,13 @@ function isRepairTransactionCommit(event, taskId) {
212
214
  * repair transaction merely because they follow the marker.
213
215
  */
214
216
  export function resolveContractBootstrapRepairBoundary(events, marker = null) {
215
- if (!Array.isArray(events)) return null;
216
- const markers = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
217
- if (markers.length !== 1) return null;
218
- const resolvedMarker = marker ?? markers[0];
217
+ if (!isLedgerEventCollection(events)) return null;
218
+ const { count, first } = ledgerTypeSummary(events, CONTRACT_BOOTSTRAP_REPAIR_EVENT);
219
+ if (count !== 1) return null;
220
+ const resolvedMarker = marker ?? first;
219
221
  const markerIndex = events.indexOf(resolvedMarker);
220
222
  if (markerIndex < 0 || resolvedMarker.seq !== markerIndex + 1) return null;
221
- const repairCommit = events[markerIndex + 1];
223
+ const repairCommit = ledgerEventAt(events, markerIndex + 1);
222
224
  if (!isRepairTransactionCommit(repairCommit, resolvedMarker.taskId)
223
225
  || repairCommit.seq !== resolvedMarker.seq + 1) return null;
224
226
  return {
@@ -261,7 +263,7 @@ export function resolveCanonicalRouteEvolution(events, {
261
263
  targetRouteFingerprint,
262
264
  contractFingerprint,
263
265
  } = {}) {
264
- if (!Array.isArray(events)
266
+ if (!isLedgerEventCollection(events)
265
267
  || typeof taskId !== "string" || !taskId
266
268
  || !Number.isInteger(sourceSeq) || sourceSeq < 0
267
269
  || (sourceRouteFingerprint !== null && !isFingerprint(sourceRouteFingerprint))
@@ -270,9 +272,9 @@ export function resolveCanonicalRouteEvolution(events, {
270
272
  let currentFingerprint = sourceRouteFingerprint;
271
273
  const reboundEvents = [];
272
274
  for (let index = 0; index < events.length - 1; index += 1) {
273
- const reboundEvent = events[index];
275
+ const reboundEvent = ledgerEventAt(events, index);
274
276
  if (reboundEvent?.seq <= sourceSeq || reboundEvent?.event !== "ROUTE_REBOUND") continue;
275
- const routeCommit = events[index + 1];
277
+ const routeCommit = ledgerEventAt(events, index + 1);
276
278
  if (!isCanonicalRouteReboundBoundary(reboundEvent, routeCommit, {
277
279
  taskId,
278
280
  contractFingerprint,
@@ -315,8 +317,8 @@ export function resolveCanonicalPostRepairCheckpointBinding(events, marker) {
315
317
  if (checkpointEvents.length !== 1) return null;
316
318
  const checkpointEvent = checkpointEvents[0];
317
319
  const checkpointIndex = boundary.postRepairEvents.indexOf(checkpointEvent);
318
- const phaseEvent = boundary.postRepairEvents[checkpointIndex + 1];
319
- const transactionCommit = boundary.postRepairEvents[checkpointIndex + 2];
320
+ const phaseEvent = ledgerEventAt(boundary.postRepairEvents, checkpointIndex + 1);
321
+ const transactionCommit = ledgerEventAt(boundary.postRepairEvents, checkpointIndex + 2);
320
322
  const details = checkpointEvent.details;
321
323
  if (checkpointEvent.taskId !== boundary.marker.taskId
322
324
  || checkpointEvent.hash !== eventHash(checkpointEvent)
@@ -343,26 +345,25 @@ export function resolveCanonicalPostRepairCheckpointBinding(events, marker) {
343
345
  }
344
346
 
345
347
  export function isContractBootstrapRepairCandidate(events, ledgerErrors = [], taskId = null) {
346
- if (!Array.isArray(events) || events.length < 4) return null;
347
- const effectiveTaskId = taskId ?? events[0]?.taskId;
348
+ if (!isLedgerEventCollection(events) || events.length < 4) return null;
349
+ const effectiveTaskId = taskId ?? ledgerEventAt(events, 0)?.taskId;
348
350
  if (typeof effectiveTaskId !== "string" || !effectiveTaskId) return null;
349
351
  if (events.some((event) => event.taskId !== effectiveTaskId)) return null;
350
- if (events.some((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT)) return null;
351
- if (events[0]?.event !== "TASK_RECEIVED" || events[1]?.event !== "TRANSACTION_COMMITTED"
352
- || events[1]?.details?.operation !== "task-create") return null;
352
+ if (ledgerTypeSummary(events, CONTRACT_BOOTSTRAP_REPAIR_EVENT).count) return null;
353
+ if (ledgerEventAt(events, 0)?.event !== "TASK_RECEIVED" || ledgerEventAt(events, 1)?.event !== "TRANSACTION_COMMITTED"
354
+ || ledgerEventAt(events, 1)?.details?.operation !== "task-create") return null;
353
355
  if (!events.some((event) => event.event === "DISCOVERY_STARTED")) return null;
354
- const contractEvents = events.filter((event) => event.event === "CONTRACT_VALIDATED");
355
- if (contractEvents.length !== 2) return null;
356
- const [canonical, duplicate] = contractEvents;
356
+ const { count: contractCount, first: canonical, latest: duplicate } = ledgerTypeSummary(events, "CONTRACT_VALIDATED");
357
+ if (contractCount !== 2) return null;
357
358
  if (!isFingerprint(canonical.details?.contractFingerprint)
358
359
  || canonical.details.contractFingerprint !== duplicate.details?.contractFingerprint) return null;
359
360
  if (canonical.hash !== eventHash(canonical) || duplicate.hash !== eventHash(duplicate)) return null;
360
361
  const canonicalIndex = events.indexOf(canonical);
361
362
  const duplicateIndex = events.indexOf(duplicate);
362
- const canonicalCommit = events[canonicalIndex + 1];
363
- const duplicateCommit = events[duplicateIndex + 1];
363
+ const canonicalCommit = ledgerEventAt(events, canonicalIndex + 1);
364
+ const duplicateCommit = ledgerEventAt(events, duplicateIndex + 1);
364
365
  if (!isCommitFor(canonicalCommit, "contract-create") || !isCommitFor(duplicateCommit, "contract-create")) return null;
365
- if (duplicateIndex !== events.length - 2 || events.at(-1) !== duplicateCommit) return null;
366
+ if (duplicateIndex !== events.length - 2 || !ledgerEventAtIs(events, events.length - 1, duplicateCommit)) return null;
366
367
  if (events.some((event) => HISTORICAL_EVENTS.has(event.event))) return null;
367
368
  if (!Array.isArray(ledgerErrors) || ledgerErrors.length < 1) return null;
368
369
  if (ledgerErrors.some((error) => !isExactDuplicateContractChronologyError(error))) return null;
@@ -383,7 +384,7 @@ export function isContractBootstrapRepairMarkerValid(events, marker) {
383
384
  if (!resolveContractBootstrapRepairBoundary(events, marker)) return false;
384
385
  assertContractBootstrapRepairDetails(marker.details);
385
386
  if (marker.taskId !== marker.details.taskId || marker.hash !== eventHash(marker)) return false;
386
- if (marker.seq < 1 || events[marker.seq - 1] !== marker) return false;
387
+ if (marker.seq < 1 || !ledgerEventAtIs(events, marker.seq - 1, marker)) return false;
387
388
  const prefix = events.slice(0, marker.seq - 1);
388
389
  const prefixErrors = [];
389
390
  const candidate = isContractBootstrapRepairCandidate(prefix, prefixErrors, marker.taskId);
@@ -528,11 +529,11 @@ function migrationMatchesLegacyRepairBoundary({ migration, marker, repairCommit
528
529
  }
529
530
 
530
531
  function resolveContractBootstrapRepairMigration(events, migration) {
531
- if (!Array.isArray(events) || !migration || migration.event !== CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT) return null;
532
+ if (!isLedgerEventCollection(events) || !migration || migration.event !== CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT) return null;
532
533
  const marker = events.find((event) => event.seq === migration.details?.legacyMarkerSeq);
533
534
  const markerIndex = marker ? events.indexOf(marker) : -1;
534
535
  const migrationIndex = events.indexOf(migration);
535
- const commit = events[migrationIndex + 1];
536
+ const commit = ledgerEventAt(events, migrationIndex + 1);
536
537
  const boundary = marker ? resolveContractBootstrapRepairBoundary(events, marker) : null;
537
538
  if (!marker || !isLegacyContractBootstrapRepairMarkerShape(marker)
538
539
  || markerIndex < 0 || !boundary
@@ -545,9 +546,9 @@ function resolveContractBootstrapRepairMigration(events, migration) {
545
546
  }
546
547
 
547
548
  export function validateContractBootstrapRepairMigrations(events, errors, { allowUnmigratedLegacyContractBootstrapRepairMarkers = false } = {}) {
548
- const markers = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
549
- const migrations = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT);
550
- const migrationBySeq = new Map();
549
+ const markers = ledgerEventsOfTypes(events, [CONTRACT_BOOTSTRAP_REPAIR_EVENT]);
550
+ const migrations = ledgerEventsOfTypes(events, [CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT]);
551
+ const migrationBySeq = ledgerRelationMap();
551
552
  for (const migration of migrations) {
552
553
  try {
553
554
  assertContractBootstrapRepairMigrationDetails(migration.details);
@@ -569,14 +570,15 @@ export function validateContractBootstrapRepairMigrations(events, errors, { allo
569
570
  });
570
571
  continue;
571
572
  }
572
- migrationBySeq.set(migration.details.legacyMarkerSeq, migration);
573
+ migrationBySeq.set(migration.details.legacyMarkerSeq, events.indexOf(migration));
573
574
  }
574
- if (markers.length > 1) {
575
+ if (ledgerTypeSummary(events, CONTRACT_BOOTSTRAP_REPAIR_EVENT).count > 1) {
575
576
  errors.push({ code: "E_CONTRACT_BOOTSTRAP_REPAIR_INVALID", message: "contract bootstrap repair marker must occur at most once" });
576
577
  }
577
578
  for (const marker of markers) {
578
579
  if (!isLegacyContractBootstrapRepairMarkerShape(marker)) continue;
579
- const migration = migrationBySeq.get(marker.seq);
580
+ const migrationPosition = migrationBySeq.get(marker.seq);
581
+ const migration = migrationPosition === undefined ? undefined : ledgerEventAt(events, migrationPosition);
580
582
  if (!migration) {
581
583
  if (!allowUnmigratedLegacyContractBootstrapRepairMarkers) {
582
584
  errors.push({
@@ -595,7 +597,8 @@ export function validateContractBootstrapRepairMigrations(events, errors, { allo
595
597
  });
596
598
  }
597
599
  }
598
- for (const [legacySeq, migration] of migrationBySeq) {
600
+ for (const [legacySeq, position] of migrationBySeq) {
601
+ const migration = ledgerEventAt(events, position);
599
602
  errors.push({
600
603
  code: "E_EVENT_INVALID",
601
604
  message: `migration event ${migration.seq} references unknown legacy contract bootstrap repair marker seq ${legacySeq}`,
@@ -604,24 +607,22 @@ export function validateContractBootstrapRepairMigrations(events, errors, { allo
604
607
  }
605
608
 
606
609
  export function isLegacyContractBootstrapRepairMigrationCandidate(events, taskId = null) {
607
- if (!Array.isArray(events)) return null;
608
- const markers = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
609
- if (markers.length !== 1) return null;
610
- const marker = markers[0];
610
+ if (!isLedgerEventCollection(events)) return null;
611
+ const { count, first: marker } = ledgerTypeSummary(events, CONTRACT_BOOTSTRAP_REPAIR_EVENT);
612
+ if (count !== 1) return null;
611
613
  if (!isLegacyContractBootstrapRepairMarkerShape(marker)) return null;
612
614
  const effectiveTaskId = taskId ?? marker.taskId;
613
615
  if (marker.taskId !== effectiveTaskId || events.some((event) => event.taskId !== effectiveTaskId)) return null;
614
- if (events.some((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT)) return null;
616
+ if (ledgerTypeSummary(events, CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_EVENT).count) return null;
615
617
  const boundary = resolveContractBootstrapRepairBoundary(events, marker);
616
618
  if (!boundary || boundary.postRepairEvents.length > 0) return null;
617
619
  return { taskId: effectiveTaskId, marker, repairCommit: boundary.repairCommit };
618
620
  }
619
621
 
620
622
  export function resolveEffectiveContractBootstrapRepairAnchor(events) {
621
- if (!Array.isArray(events)) return null;
622
- const markers = events.filter((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
623
- if (markers.length !== 1) return null;
624
- const sourceMarker = markers[0];
623
+ if (!isLedgerEventCollection(events)) return null;
624
+ const { count, first: sourceMarker } = ledgerTypeSummary(events, CONTRACT_BOOTSTRAP_REPAIR_EVENT);
625
+ if (count !== 1) return null;
625
626
  if (isContractBootstrapRepairMarkerValid(events, sourceMarker)) {
626
627
  return { kind: "MODERN", sourceMarker, details: sourceMarker.details };
627
628
  }
@@ -643,7 +644,7 @@ export function resolveEffectiveContractBootstrapRepairAnchor(events) {
643
644
  }
644
645
 
645
646
  export function repairMarkerErrors(events, errors, { allowUnmigratedLegacyContractBootstrapRepairMarkers = false } = {}) {
646
- const marker = events.find((event) => event.event === CONTRACT_BOOTSTRAP_REPAIR_EVENT);
647
+ const marker = ledgerTypeSummary(events, CONTRACT_BOOTSTRAP_REPAIR_EVENT).first;
647
648
  const boundary = resolveContractBootstrapRepairBoundary(events, marker);
648
649
  const migratedLegacy = resolveEffectiveContractBootstrapRepairAnchor(events)?.kind === "MIGRATED_LEGACY";
649
650
  const recognized = isContractBootstrapRepairMarkerValid(events, marker)
@@ -1,3 +1,4 @@
1
+ import { isLedgerEventCollection, ledgerEventAt, ledgerEventsOfTypes, ledgerTypeSummary } from "./ledger-event-collection.js";
1
2
  import { canonicalFingerprint } from "./artifacts.js";
2
3
 
3
4
  export const CONTRACT_REVISED_EVENT = "CONTRACT_REVISED";
@@ -87,13 +88,13 @@ function validRevisionCommit(revisionEvent, commitEvent) {
87
88
  }
88
89
 
89
90
  export function resolveContractRevisionBoundary(events, revisionEvent) {
90
- if (!Array.isArray(events) || !revisionEvent || revisionEvent.event !== CONTRACT_REVISED_EVENT) return null;
91
+ if (!isLedgerEventCollection(events) || !revisionEvent || revisionEvent.event !== CONTRACT_REVISED_EVENT) return null;
91
92
  const index = events.findIndex((event) => event === revisionEvent
92
93
  || (event?.seq === revisionEvent.seq
93
94
  && event?.taskId === revisionEvent.taskId
94
95
  && event?.event === CONTRACT_REVISED_EVENT));
95
96
  if (index < 0) return null;
96
- const commitEvent = events[index + 1];
97
+ const commitEvent = ledgerEventAt(events, index + 1);
97
98
  return validRevisionCommit(revisionEvent, commitEvent)
98
99
  ? { revisionEvent, transactionCommitEvent: commitEvent }
99
100
  : null;
@@ -105,7 +106,7 @@ export function resolveCanonicalContractEvolution(events, {
105
106
  sourceContractFingerprint,
106
107
  targetContractFingerprint,
107
108
  } = {}) {
108
- if (!Array.isArray(events)
109
+ if (!isLedgerEventCollection(events)
109
110
  || typeof taskId !== "string" || !taskId
110
111
  || !Number.isInteger(sourceSeq) || sourceSeq < 0
111
112
  || !isFingerprint(sourceContractFingerprint)
@@ -138,12 +139,12 @@ export function resolveCanonicalContractEvolution(events, {
138
139
  }
139
140
 
140
141
  export function latestContractRevision(events = []) {
141
- return events.filter((event) => event.event === CONTRACT_REVISED_EVENT).at(-1) ?? null;
142
+ return ledgerTypeSummary(events, CONTRACT_REVISED_EVENT).latest ?? null;
142
143
  }
143
144
 
144
145
  export function validateContractRevisionEventBindings(events = []) {
145
146
  const errors = [];
146
- const revisions = events.filter((event) => event.event === CONTRACT_REVISED_EVENT);
147
+ const revisions = ledgerEventsOfTypes(events, [CONTRACT_REVISED_EVENT]);
147
148
  let currentContractFingerprint = null;
148
149
  let currentStateRevision = null;
149
150
  for (const event of revisions) {
@@ -184,10 +185,8 @@ export function validateContractRevisionEventBindings(events = []) {
184
185
 
185
186
  export function validateContractRevisionCurrentBinding(state, events = []) {
186
187
  const errors = [];
187
- const revisions = events.filter((event) => event.event === CONTRACT_REVISED_EVENT);
188
- if (revisions.length === 0) return errors;
189
- const first = revisions[0];
190
- const latest = revisions.at(-1);
188
+ const { count, first, latest } = ledgerTypeSummary(events, CONTRACT_REVISED_EVENT);
189
+ if (!count) return errors;
191
190
  const initial = events.find((event) => event.taskId === state?.taskId && event.event === "CONTRACT_VALIDATED");
192
191
  const evolution = resolveCanonicalContractEvolution(events, {
193
192
  taskId: state?.taskId,
@@ -5,6 +5,7 @@ import { assertSecretFree } from "../receipt.js";
5
5
  import { DECISION_AUTHORITY, DECISION_EVIDENCE_AUTHORITY, DECISION_POLICY_VERSION, PINNED_JEV_MODEL } from "./constants.js";
6
6
  import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
7
7
  import { getTaskTransaction, withTaskTransaction } from "../transaction.js";
8
+ import { operationalArtifactExists } from "../../storage/operational-context.js";
8
9
 
9
10
  export function buildDecisionArtifact(input = {}) {
10
11
  const artifact = {
@@ -58,7 +59,7 @@ export async function writeDecisionArtifact(target, taskId, decisionId, artifact
58
59
  ));
59
60
  }
60
61
  const relativePath = taskDecisionPath(taskId, decisionId);
61
- if (await fileExists(ensureWithin(target, relativePath))) {
62
+ if (operationalArtifactExists(target, relativePath) ?? await fileExists(ensureWithin(target, relativePath))) {
62
63
  throw decisionError(DECISION_ERROR_CODES.IMMUTABLE, `Semantic decision artifact already exists: ${decisionId}`);
63
64
  }
64
65
  return writeJsonArtifact(target, relativePath, artifact, "semantic-decision", packageRoot, { ...options, taskId, operation: "semantic-decision" });
@@ -1,3 +1,5 @@
1
+ import { ledgerRelationSet } from "../ledger-relations.js";
2
+ import { ledgerEventsOfTypes } from "../ledger-event-collection.js";
1
3
  import { canonicalFingerprint } from "../artifacts.js";
2
4
  import { readDecisionArtifact } from "./artifact.js";
3
5
  import { DECISION_KINDS, DECISION_STATUS, SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT } from "./constants.js";
@@ -42,8 +44,8 @@ export function decisionSupersededEventDetails(previousArtifact, supersededBy) {
42
44
 
43
45
  export function validateSemanticDecisionEventBindings(events = []) {
44
46
  const errors = [];
45
- const recorded = new Map();
46
- for (const event of events) {
47
+ const recorded = ledgerRelationSet();
48
+ for (const event of ledgerEventsOfTypes(events, [SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT])) {
47
49
  if (![SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT].includes(event.event)) continue;
48
50
  try { assertSemanticDecisionDetails(event.details, event.event); } catch (error) {
49
51
  errors.push({ code: error.code ?? DECISION_ERROR_CODES.RESULT_INVALID, message: `event ${event.seq} (${event.event}): ${error.message}` });
@@ -54,7 +56,7 @@ export function validateSemanticDecisionEventBindings(events = []) {
54
56
  if (recorded.has(key)) {
55
57
  errors.push({ code: DECISION_ERROR_CODES.RESULT_INVALID, message: `event ${event.seq} records duplicate decision ID ${event.details.decisionId}` });
56
58
  }
57
- recorded.set(key, event);
59
+ recorded.add(key);
58
60
  }
59
61
  if (event.event === SEMANTIC_DECISION_SUPERSEDED_EVENT) {
60
62
  const key = `${event.taskId}:${event.details.decisionId}`;
@@ -79,12 +81,12 @@ export function validateSemanticDecisionEventBindings(events = []) {
79
81
  return errors;
80
82
  }
81
83
 
82
- export async function validateSemanticDecisionArtifactBindings(target, packageRoot, events = []) {
84
+ export async function validateSemanticDecisionArtifactBindings(target, packageRoot, events = [], { reader = readDecisionArtifact } = {}) {
83
85
  const errors = [];
84
- for (const event of events) {
86
+ for (const event of ledgerEventsOfTypes(events, [SEMANTIC_DECISION_RECORDED_EVENT])) {
85
87
  if (event.event !== SEMANTIC_DECISION_RECORDED_EVENT) continue;
86
88
  try {
87
- const artifact = (await readDecisionArtifact(target, event.taskId, event.details.decisionId, packageRoot)).value;
89
+ const artifact = (await reader(target, event.taskId, event.details.decisionId, packageRoot)).value;
88
90
  if (artifact.taskId !== event.taskId
89
91
  || artifact.decisionId !== event.details.decisionId
90
92
  || canonicalFingerprint(artifact) !== event.details.artifactFingerprint
@@ -94,6 +96,7 @@ export async function validateSemanticDecisionArtifactBindings(target, packageRo
94
96
  errors.push({ code: DECISION_ERROR_CODES.LEDGER_INVALID, message: `event ${event.seq} does not match its immutable semantic decision artifact` });
95
97
  }
96
98
  } catch (error) {
99
+ if (error.code === "E_STATE_REVISION_CONFLICT") throw error;
97
100
  errors.push({ code: DECISION_ERROR_CODES.LEDGER_INVALID, message: `event ${event.seq} semantic decision artifact is unavailable or invalid` });
98
101
  }
99
102
  }
@@ -2,12 +2,14 @@ import { readdir } from "node:fs/promises";
2
2
 
3
3
  import { canonicalFingerprint, readJsonArtifact } from "../artifacts.js";
4
4
  import { fileExists, ensureWithin } from "../filesystem.js";
5
- import { readEvents, validateEventLedger } from "../events.js";
5
+ import { validateEventLedger } from "../events.js";
6
6
  import { taskDecisionDirectory } from "../task-paths.js";
7
7
  import { getQuestionSet } from "./question-registry.js";
8
8
  import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
9
9
  import { DECISION_ENGINE_ID, DECISION_POLICY_VERSION, PINNED_JEV_MODEL, SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT } from "./constants.js";
10
10
  import { assertDecisionFresh } from "./freshness.js";
11
+ import { listOperationalArtifactNames } from "../../storage/operational-context.js";
12
+ import { withProjectReadSnapshot } from "../../storage/project-read-snapshot.js";
11
13
 
12
14
  function invalid(message, code = DECISION_ERROR_CODES.BINDING_INVALID) {
13
15
  return decisionError(code, message);
@@ -89,14 +91,16 @@ export function assertRequiredFreshDecision({ artifact, currentBindings = {}, ex
89
91
 
90
92
  async function decisionArtifacts(target, taskId, packageRoot) {
91
93
  const directory = taskDecisionDirectory(taskId);
92
- const absolute = ensureWithin(target, directory);
93
- if (!(await fileExists(absolute))) return [];
94
- const names = (await readdir(absolute, { withFileTypes: true }))
95
- .filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
96
- .map((entry) => entry.name.slice(0, -5))
97
- .sort();
94
+ let names = listOperationalArtifactNames(target, taskId, "decisions");
95
+ if (names === null) {
96
+ const absolute = ensureWithin(target, directory);
97
+ if (!(await fileExists(absolute))) return [];
98
+ names = (await readdir(absolute, { withFileTypes: true }))
99
+ .filter(entry => entry.isFile()).map(entry => entry.name);
100
+ }
101
+ const ids = names.filter(name => name.endsWith(".json")).map(name => name.slice(0, -5)).sort();
98
102
  const artifacts = [];
99
- for (const decisionId of names) {
103
+ for (const decisionId of ids) {
100
104
  try {
101
105
  artifacts.push((await readJsonArtifact(target, `${directory}/${decisionId}.json`, "semantic-decision", packageRoot)).value);
102
106
  } catch {
@@ -106,11 +110,15 @@ async function decisionArtifacts(target, taskId, packageRoot) {
106
110
  return artifacts;
107
111
  }
108
112
 
109
- export async function resolveRequiredSemanticDecision({ target, packageRoot, taskId, decisionKind, decisionId = null, currentBindings = {} } = {}) {
110
- if (typeof taskId !== "string" || !taskId) throw decisionError(DECISION_ERROR_CODES.REQUIRED, "A task-bound semantic decision is required.");
113
+ export async function resolveRequiredSemanticDecision(options = {}) {
114
+ if (typeof options.taskId !== "string" || !options.taskId) throw decisionError(DECISION_ERROR_CODES.REQUIRED, "A task-bound semantic decision is required.");
115
+ return withProjectReadSnapshot(options.target, () => resolveSelectedSemanticDecision(options));
116
+ }
117
+
118
+ async function resolveSelectedSemanticDecision({ target, packageRoot, taskId, decisionKind, decisionId = null, currentBindings = {} }) {
111
119
  const ledgerResult = await validateEventLedger(target, packageRoot, { taskId });
112
120
  if (!ledgerResult.valid) throw decisionError(DECISION_ERROR_CODES.LEDGER_INVALID, "The task ledger is invalid for semantic decision resolution.", ledgerResult.errors);
113
- const events = await readEvents(target, packageRoot, { taskId });
121
+ const events = ledgerResult.events;
114
122
  const candidates = decisionId
115
123
  ? [(await readJsonArtifact(target, `${taskDecisionDirectory(taskId)}/${decisionId}.json`, "semantic-decision", packageRoot)).value]
116
124
  : await decisionArtifacts(target, taskId, packageRoot);
@@ -1,6 +1,6 @@
1
1
  import { canonicalFingerprint } from "../artifacts.js";
2
2
  import { readConfig } from "../config.js";
3
- import { appendProtocolEvent, readEvents } from "../events.js";
3
+ import { appendProtocolEvent, iterateEvents } from "../events.js";
4
4
  import { withTaskMutation } from "../task-command.js";
5
5
  import { DECISION_DEFAULT_POLICY, PINNED_JEV_MODEL, SEMANTIC_DECISION_RECORDED_EVENT } from "./constants.js";
6
6
  import { buildDecisionArtifact, decisionArtifactFingerprint, readDecisionArtifact, writeDecisionArtifact } from "./artifact.js";
@@ -15,6 +15,15 @@ import { resolveRequiredSemanticDecision } from "./resolver.js";
15
15
  import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
16
16
  import { getTaskTransaction } from "../transaction.js";
17
17
  import { getTestSemanticProvider } from "./test-provider.js";
18
+ import { needsExistingProjectScope, withExistingProjectScope } from "../../storage/existing-project-scope.js";
19
+ import { withProjectReadSnapshot } from "../../storage/project-read-snapshot.js";
20
+
21
+ async function withDecisionScope(options, run) {
22
+ if (options.target && options.taskId && await needsExistingProjectScope(options.target)) {
23
+ return withExistingProjectScope(options.target, () => run(options));
24
+ }
25
+ return run(options);
26
+ }
18
27
 
19
28
  function resolveProvider(provider) {
20
29
  return provider ?? getTestSemanticProvider();
@@ -50,7 +59,7 @@ function buildArtifact({ taskId, decisionId, policy, validated, result, request,
50
59
  questionSetFingerprint: validated.questionSet.fingerprint,
51
60
  questionSet: validated.questionSet,
52
61
  stateFingerprint: canonicalFingerprint(validated.state),
53
- taskStateFingerprint: canonicalFingerprint(validated.state.lifecycle),
62
+ taskStateFingerprint: taskBindings.taskStateFingerprint ?? canonicalFingerprint(validated.state.lifecycle),
54
63
  semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
55
64
  policyVersion: policy.policyVersion,
56
65
  policyFingerprint: decisionPolicyFingerprint(policy),
@@ -77,7 +86,7 @@ async function readCachedSemanticDecision({ policy, decisionId, target, taskId,
77
86
  currentBindings: {
78
87
  ...taskBindings,
79
88
  stateFingerprint: canonicalFingerprint(validated.state),
80
- taskStateFingerprint: canonicalFingerprint(validated.state.lifecycle),
89
+ taskStateFingerprint: taskBindings.taskStateFingerprint ?? canonicalFingerprint(validated.state.lifecycle),
81
90
  semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
82
91
  questionSetFingerprint: validated.questionSet.fingerprint,
83
92
  policyFingerprint: decisionPolicyFingerprint(policy),
@@ -90,13 +99,15 @@ async function readCachedSemanticDecision({ policy, decisionId, target, taskId,
90
99
 
91
100
  async function persistDecision(target, packageRoot, resolvedDecisionId, artifact, ctx) {
92
101
  await writeDecisionArtifact(target, ctx.taskId, resolvedDecisionId, artifact, packageRoot, { taskId: ctx.taskId, operation: "semantic-decision" });
93
- const priorEvents = await readEvents(target, packageRoot, { taskId: ctx.taskId });
102
+ const priorRecords = [];
103
+ const supersededIds = new Set();
104
+ for await (const candidate of iterateEvents(target, packageRoot, { taskId: ctx.taskId })) {
105
+ if (candidate.event === SEMANTIC_DECISION_RECORDED_EVENT && candidate.taskId === ctx.taskId
106
+ && candidate.details?.decisionKind === artifact.decisionKind) priorRecords.push(candidate);
107
+ if (candidate.event === "SEMANTIC_DECISION_SUPERSEDED") supersededIds.add(candidate.details?.decisionId);
108
+ }
94
109
  const event = await appendProtocolEvent(target, { taskId: ctx.taskId, event: SEMANTIC_DECISION_RECORDED_EVENT, details: decisionEventDetails(artifact) }, packageRoot, { taskId: ctx.taskId });
95
- const previous = priorEvents.findLast((candidate) => candidate.event === SEMANTIC_DECISION_RECORDED_EVENT
96
- && candidate.taskId === ctx.taskId
97
- && candidate.details?.decisionKind === artifact.decisionKind
98
- && !priorEvents.some((superseded) => superseded.event === "SEMANTIC_DECISION_SUPERSEDED"
99
- && superseded.details?.decisionId === candidate.details?.decisionId));
110
+ const previous = priorRecords.findLast(candidate => !supersededIds.has(candidate.details?.decisionId));
100
111
  if (!previous) return { artifact, artifactFingerprint: decisionArtifactFingerprint(artifact), event, supersededEvent: null };
101
112
  const previousArtifact = await readDecisionArtifact(target, ctx.taskId, previous.details.decisionId, packageRoot);
102
113
  const supersededEvent = await appendProtocolEvent(target, {
@@ -107,16 +118,23 @@ async function persistDecision(target, packageRoot, resolvedDecisionId, artifact
107
118
  return { artifact, artifactFingerprint: decisionArtifactFingerprint(artifact), event, supersededEvent };
108
119
  }
109
120
 
110
- export async function recordSemanticDecision({ target, packageRoot, taskId, decisionId, request, provider = null } = {}) {
121
+ export async function recordSemanticDecision(options = {}) {
122
+ return withDecisionScope(options, recordSelectedSemanticDecision);
123
+ }
124
+
125
+ async function recordSelectedSemanticDecision({ target, packageRoot, taskId, decisionId, request, provider = null }) {
111
126
  const resolvedDecisionId = decisionId ?? `${String(request?.decisionKind ?? "decision").toLowerCase()}-${Date.now()}-${randomUUID().slice(0, 8)}`;
112
- const policy = await loadPolicy(target, packageRoot);
113
- const taskBindings = target && taskId ? await readCurrentDecisionBindings(target, packageRoot, taskId) : {};
114
- const validated = validateDecisionRequest({
115
- ...request,
116
- taskId,
117
- state: { lifecycle: taskBindings.state ?? {}, semantic: request?.state ?? {} },
127
+ const { policy, taskBindings, validated, cached } = await withProjectReadSnapshot(target, async () => {
128
+ const policy = await loadPolicy(target, packageRoot);
129
+ const taskBindings = target && taskId ? await readCurrentDecisionBindings(target, packageRoot, taskId) : {};
130
+ const validated = validateDecisionRequest({
131
+ ...request,
132
+ taskId,
133
+ state: { lifecycle: taskBindings.state ?? {}, semantic: request?.state ?? {} },
134
+ });
135
+ const cached = await readCachedSemanticDecision({ policy, decisionId, target, taskId, packageRoot, validated, taskBindings, request });
136
+ return { policy, taskBindings, validated, cached };
118
137
  });
119
- const cached = await readCachedSemanticDecision({ policy, decisionId, target, taskId, packageRoot, validated, taskBindings, request });
120
138
  if (cached) return { artifact: cached, artifactFingerprint: decisionArtifactFingerprint(cached), event: null, supersededEvent: null, cached: true, policy };
121
139
  const engine = resolveProvider(provider) ?? createTypesafeEngine({ policy });
122
140
  if (provider && (provider.id !== "typesafe-jev" || provider.model !== PINNED_JEV_MODEL || typeof provider.evaluate !== "function")) {
@@ -128,27 +146,34 @@ export async function recordSemanticDecision({ target, packageRoot, taskId, deci
128
146
  return { ...persisted, policy };
129
147
  }
130
148
 
131
- export async function ensureSemanticDecision({ target, packageRoot, taskId, decisionId, request, provider = null, allowNetwork = true } = {}) {
132
- const policy = await loadPolicy(target, packageRoot);
133
- const taskBindings = await readCurrentDecisionBindings(target, packageRoot, taskId);
134
- const validated = validateDecisionRequest({
135
- ...request,
136
- taskId,
137
- state: { lifecycle: taskBindings.state ?? {}, semantic: request?.state ?? {} },
138
- });
139
- const currentBindings = {
140
- ...taskBindings,
141
- stateFingerprint: canonicalFingerprint(validated.state),
142
- taskStateFingerprint: canonicalFingerprint(validated.state.lifecycle),
143
- semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
144
- questionSetFingerprint: validated.questionSet.fingerprint,
145
- policyFingerprint: decisionPolicyFingerprint(policy),
146
- model: PINNED_JEV_MODEL,
147
- ...(request?.candidateSetFingerprint !== undefined ? { candidateSetFingerprint: request.candidateSetFingerprint } : {}),
148
- };
149
- const cached = await getTaskTransaction(target) ? null : await readCachedDecision({
150
- target, packageRoot, taskId, decisionKind: validated.decisionKind,
151
- currentBindings,
149
+ export async function ensureSemanticDecision(options = {}) {
150
+ return withDecisionScope(options, ensureSelectedSemanticDecision);
151
+ }
152
+
153
+ async function ensureSelectedSemanticDecision({ target, packageRoot, taskId, decisionId, request, provider = null, allowNetwork = true }) {
154
+ const { policy, validated, cached } = await withProjectReadSnapshot(target, async () => {
155
+ const policy = await loadPolicy(target, packageRoot);
156
+ const taskBindings = await readCurrentDecisionBindings(target, packageRoot, taskId);
157
+ const validated = validateDecisionRequest({
158
+ ...request,
159
+ taskId,
160
+ state: { lifecycle: taskBindings.state ?? {}, semantic: request?.state ?? {} },
161
+ });
162
+ const currentBindings = {
163
+ ...taskBindings,
164
+ stateFingerprint: canonicalFingerprint(validated.state),
165
+ taskStateFingerprint: taskBindings.taskStateFingerprint ?? canonicalFingerprint(validated.state.lifecycle),
166
+ semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
167
+ questionSetFingerprint: validated.questionSet.fingerprint,
168
+ policyFingerprint: decisionPolicyFingerprint(policy),
169
+ model: PINNED_JEV_MODEL,
170
+ ...(request?.candidateSetFingerprint !== undefined ? { candidateSetFingerprint: request.candidateSetFingerprint } : {}),
171
+ };
172
+ const cached = await getTaskTransaction(target) ? null : await readCachedDecision({
173
+ target, packageRoot, taskId, decisionKind: validated.decisionKind,
174
+ currentBindings,
175
+ });
176
+ return { policy, validated, cached };
152
177
  });
153
178
  if (cached) return { artifact: cached, artifactFingerprint: decisionArtifactFingerprint(cached), cached: true, policy };
154
179
  if (!allowNetwork && !provider) throw decisionError(DECISION_ERROR_CODES.REQUIRED, `A current ${validated.decisionKind} decision is required.`);
@@ -2,6 +2,7 @@ import { canonicalFingerprint } from "../artifacts.js";
2
2
  import { readContract } from "../contract.js";
3
3
  import { readPersistedRoute } from "../route-artifact.js";
4
4
  import { readWorkState } from "../work-state.js";
5
+ import { withProjectReadSnapshot } from "../../storage/project-read-snapshot.js";
5
6
 
6
7
  async function readOptionalArtifact(reader) {
7
8
  try {
@@ -12,7 +13,11 @@ async function readOptionalArtifact(reader) {
12
13
  }
13
14
  }
14
15
 
15
- export async function readCurrentDecisionBindings(target, packageRoot, taskId, { candidateSetFingerprint, policyFingerprint } = {}) {
16
+ export async function readCurrentDecisionBindings(target, packageRoot, taskId, options = {}) {
17
+ return withProjectReadSnapshot(target, () => readSnapshotBindings(target, packageRoot, taskId, options));
18
+ }
19
+
20
+ async function readSnapshotBindings(target, packageRoot, taskId, { candidateSetFingerprint, policyFingerprint }) {
16
21
  const state = await readWorkState(target, { packageRoot, taskId });
17
22
  const contract = await readOptionalArtifact(() => readContract(target, packageRoot, { taskId }));
18
23
  const route = await readOptionalArtifact(() => readPersistedRoute(target, packageRoot, { taskId }));
@@ -1,3 +1,5 @@
1
+ import { ledgerEventsOfTypes } from "./ledger-event-collection.js";
2
+
1
3
  import { canonicalFingerprint } from "./artifacts.js";
2
4
  import { assertFailureClass } from "./protocol.js";
3
5
 
@@ -212,3 +214,12 @@ export function currentCycleDiagnosis(events, taskId, verificationCycle) {
212
214
  }
213
215
  return null;
214
216
  }
217
+
218
+
219
+ export function latestDiagnosisForTask(events, taskId) {
220
+ let latest = null;
221
+ for (const event of ledgerEventsOfTypes(events ?? [], ["DIAGNOSIS_RECORDED"])) {
222
+ if (!taskId || event.taskId === taskId) latest = event;
223
+ }
224
+ return latest;
225
+ }
@@ -1,5 +1,6 @@
1
1
  import { appendProtocolEvent, validateEventLedger } from "./events.js";
2
2
  import { readWorkState, mutateWorkState } from "./work-state.js";
3
+ import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
3
4
  import {
4
5
  DIAGNOSIS_INFORMATION_GAIN,
5
6
  assertDiagnosisDetails,
@@ -22,7 +23,15 @@ export {
22
23
  normalizeDiagnosisText,
23
24
  };
24
25
 
25
- export async function recordDiagnosis({
26
+ export async function recordDiagnosis(options) {
27
+ if (await getTaskTransaction(options.target)) return recordDiagnosisInTransaction(options);
28
+ const state = await readWorkState(options.target, { packageRoot: options.packageRoot, taskId: options.taskId, statePath: options.statePath });
29
+ if (!state) return recordDiagnosisInTransaction(options);
30
+ return withTaskTransaction({ target: options.target, taskId: options.taskId ?? state.taskId,
31
+ packageRoot: options.packageRoot, operation: "record-diagnosis" }, () => recordDiagnosisInTransaction(options));
32
+ }
33
+
34
+ async function recordDiagnosisInTransaction({
26
35
  target,
27
36
  packageRoot,
28
37
  hypothesis,