@cassiomc1/forgeloop 1.14.0 → 2.0.0-rc.4

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 (250) 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 +30 -40
  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 +110 -10
  32. package/docs/SELF_HOSTED_TESTS.md +121 -0
  33. package/docs/TROUBLESHOOTING.md +33 -0
  34. package/docs/documentation-manifest.json +25 -12
  35. package/package.json +15 -10
  36. package/schemas/task-bundle.schema.json +7 -2
  37. package/scripts/generate-agent-protocol-summary.mjs +2 -2
  38. package/scripts/run-mcp-clean-tests.mjs +30 -0
  39. package/src/adapters/agent-browser/provider.js +14 -11
  40. package/src/adapters/emulated-services/provider.js +11 -4
  41. package/src/adapters/temporary-paths.js +18 -0
  42. package/src/cli.js +146 -210
  43. package/src/commands/action-show.js +5 -2
  44. package/src/commands/advance.js +5 -1
  45. package/src/commands/baseline.js +13 -11
  46. package/src/commands/checkpoint-revalidate.js +24 -8
  47. package/src/commands/complete.js +19 -3
  48. package/src/commands/contract-create.js +3 -1
  49. package/src/commands/doctor.js +36 -13
  50. package/src/commands/gate-record.js +20 -31
  51. package/src/commands/gate-revalidate.js +11 -13
  52. package/src/commands/handoff-list.js +6 -1
  53. package/src/commands/handoff-show.js +6 -1
  54. package/src/commands/migrate-protocol.js +2 -2
  55. package/src/commands/model-route.js +9 -1
  56. package/src/commands/next.js +7 -1
  57. package/src/commands/policy-diff.js +25 -4
  58. package/src/commands/progress.js +17 -11
  59. package/src/commands/record-diagnosis.js +3 -0
  60. package/src/commands/semantic-plan.js +26 -2
  61. package/src/commands/status.js +6 -1
  62. package/src/commands/storage-backup.js +15 -0
  63. package/src/commands/storage-migrate.js +5 -0
  64. package/src/commands/storage-migration-resume.js +5 -0
  65. package/src/commands/storage-migration-status.js +44 -0
  66. package/src/commands/storage-restore-resume.js +7 -0
  67. package/src/commands/storage-restore.js +8 -0
  68. package/src/commands/task-abandon.js +1 -1
  69. package/src/commands/task-list.js +8 -43
  70. package/src/commands/task-migrate.js +3 -3
  71. package/src/commands/task-recover.js +10 -2
  72. package/src/commands/task-scope.js +18 -2
  73. package/src/commands/task-show.js +16 -2
  74. package/src/commands/validate-protocol.js +29 -14
  75. package/src/commands/validate-receipt.js +8 -1
  76. package/src/config/guides.json +4 -0
  77. package/src/core/action-ledger-projection.js +10 -3
  78. package/src/core/actions.js +38 -75
  79. package/src/core/activation.js +11 -3
  80. package/src/core/approvals.js +17 -51
  81. package/src/core/artifact-registry.js +59 -2
  82. package/src/core/artifacts.js +81 -27
  83. package/src/core/attachment-paths.js +45 -0
  84. package/src/core/attestation-coverage.js +6 -1
  85. package/src/core/attestation-verifier.js +21 -2
  86. package/src/core/attestation.js +17 -6
  87. package/src/core/audit.js +6 -1
  88. package/src/core/bundles.js +160 -66
  89. package/src/core/checkpoint-revalidation.js +12 -10
  90. package/src/core/cli-command-definitions.js +109 -7
  91. package/src/core/code-manifest.js +9 -3
  92. package/src/core/command-executors.js +182 -126
  93. package/src/core/command-input.js +21 -1
  94. package/src/core/completion-artifacts.js +85 -36
  95. package/src/core/completion-ownership.js +19 -8
  96. package/src/core/completion-recovery-rebind.js +11 -1
  97. package/src/core/completion.js +60 -9
  98. package/src/core/conformance.js +2 -1
  99. package/src/core/continuity-lint.js +13 -2
  100. package/src/core/continuity-reconciliation.js +16 -3
  101. package/src/core/continuity.js +16 -8
  102. package/src/core/contract-bootstrap-recovery.js +42 -41
  103. package/src/core/contract-revision.js +8 -9
  104. package/src/core/decision/artifact.js +2 -1
  105. package/src/core/decision/events.js +9 -6
  106. package/src/core/decision/resolver.js +19 -11
  107. package/src/core/decision/service.js +63 -38
  108. package/src/core/decision/task-bindings.js +6 -1
  109. package/src/core/diagnosis-model.js +11 -0
  110. package/src/core/diagnosis.js +10 -1
  111. package/src/core/diagnostic-projection.js +29 -12
  112. package/src/core/diagnostic-record.js +28 -3
  113. package/src/core/efficiency.js +6 -1
  114. package/src/core/events.js +316 -97
  115. package/src/core/execution-prerequisites.js +22 -13
  116. package/src/core/execution.js +26 -40
  117. package/src/core/filesystem.js +11 -0
  118. package/src/core/gate-artifact.js +105 -19
  119. package/src/core/gate-provenance.js +6 -5
  120. package/src/core/handoff.js +26 -14
  121. package/src/core/history.js +39 -35
  122. package/src/core/hypothesis-projection.js +32 -0
  123. package/src/core/information-gain-projection.js +104 -100
  124. package/src/core/inspect.js +12 -3
  125. package/src/core/integration-invocation-policy.js +10 -0
  126. package/src/core/integration-resources.js +41 -34
  127. package/src/core/ledger-event-collection.js +189 -0
  128. package/src/core/ledger-relations.js +23 -0
  129. package/src/core/native-storage.js +12 -0
  130. package/src/core/next-action-continuity.js +31 -9
  131. package/src/core/next-action-phases.js +1 -1
  132. package/src/core/next-action-review-phase.js +2 -2
  133. package/src/core/next-action-verification-phase.js +2 -1
  134. package/src/core/next-action.js +21 -2
  135. package/src/core/phase.js +138 -93
  136. package/src/core/policy-engine.js +12 -13
  137. package/src/core/preflight-consistency.js +72 -59
  138. package/src/core/preflight-loaders.js +19 -12
  139. package/src/core/preflight-model.js +6 -1
  140. package/src/core/preflight.js +29 -12
  141. package/src/core/progress.js +21 -10
  142. package/src/core/protocol-info.js +3 -0
  143. package/src/core/protocol-migration.js +7 -6
  144. package/src/core/reconcile-closure.js +162 -24
  145. package/src/core/recovery-history.js +66 -47
  146. package/src/core/reflection.js +72 -77
  147. package/src/core/responsibility.js +25 -13
  148. package/src/core/route-artifact.js +13 -3
  149. package/src/core/runtime-context.js +5 -0
  150. package/src/core/schema-validation.js +8 -3
  151. package/src/core/settlement.js +12 -1
  152. package/src/core/signing/bundle-file.js +53 -0
  153. package/src/core/signing/bundle-reference.js +15 -0
  154. package/src/core/signing/sigstore.js +60 -33
  155. package/src/core/signing/statement-file.js +27 -0
  156. package/src/core/storage-runtime-registry.js +41 -0
  157. package/src/core/structural-quality/artifacts.js +20 -14
  158. package/src/core/structural-quality/service.js +81 -56
  159. package/src/core/target-layout.js +1 -1
  160. package/src/core/task-claim-state.js +52 -18
  161. package/src/core/task-command.js +21 -1
  162. package/src/core/task-conflict-inspection.js +112 -107
  163. package/src/core/task-descriptor.js +29 -22
  164. package/src/core/task-discovery.js +288 -90
  165. package/src/core/task-lock.js +66 -306
  166. package/src/core/task-migration-validation.js +5 -5
  167. package/src/core/task-migration.js +17 -287
  168. package/src/core/task-snapshot.js +51 -8
  169. package/src/core/task-storage-migration.js +18 -0
  170. package/src/core/templates.js +20 -8
  171. package/src/core/test-intelligence/prune.js +5 -1
  172. package/src/core/trace.js +154 -99
  173. package/src/core/trajectory-evaluation.js +15 -3
  174. package/src/core/trajectory-metrics.js +47 -32
  175. package/src/core/transaction.js +16 -288
  176. package/src/core/verification-scope.js +20 -12
  177. package/src/core/work-state.js +100 -59
  178. package/src/repository-index/binary-manager.js +15 -7
  179. package/src/storage/artifact-bytes.js +22 -0
  180. package/src/storage/attachment-binding.js +11 -0
  181. package/src/storage/attachment-files.js +82 -0
  182. package/src/storage/attachment-references.js +36 -0
  183. package/src/storage/backup.js +53 -0
  184. package/src/storage/capabilities.js +31 -0
  185. package/src/storage/connection-owner.js +119 -0
  186. package/src/storage/connection.js +376 -0
  187. package/src/storage/existing-project-scope.js +45 -0
  188. package/src/storage/export-destination.js +44 -0
  189. package/src/storage/exporter.js +284 -0
  190. package/src/storage/file-durability.js +46 -0
  191. package/src/storage/importer.js +461 -0
  192. package/src/storage/index.js +74 -0
  193. package/src/storage/leases.js +63 -0
  194. package/src/storage/ledger-event-snapshot.js +150 -0
  195. package/src/storage/ledger-relations.js +220 -0
  196. package/src/storage/legacy-maintenance-boundary.js +18 -0
  197. package/src/storage/legacy-signature.js +115 -0
  198. package/src/storage/maintenance-handoff.js +129 -0
  199. package/src/storage/maintenance-owner.js +42 -0
  200. package/src/storage/maintenance.js +310 -0
  201. package/src/storage/metadata-json.js +35 -0
  202. package/src/storage/migration-activate.js +59 -0
  203. package/src/storage/migration-archive.js +57 -0
  204. package/src/storage/migration-attachment-root.js +61 -0
  205. package/src/storage/migration-candidate.js +398 -0
  206. package/src/storage/migration-publication.js +143 -0
  207. package/src/storage/migration-rollback-admission.js +54 -0
  208. package/src/storage/migration-rollback-preparation.js +132 -0
  209. package/src/storage/migration-rollback-publication.js +183 -0
  210. package/src/storage/migration-rollback-release.js +66 -0
  211. package/src/storage/migration-rollback-source-stage.js +103 -0
  212. package/src/storage/migration-rollback.js +43 -0
  213. package/src/storage/migration-source-partition.js +56 -0
  214. package/src/storage/migration-source.js +255 -0
  215. package/src/storage/migration-terminal.js +39 -0
  216. package/src/storage/migration-validation.js +75 -0
  217. package/src/storage/migration.js +43 -0
  218. package/src/storage/operational-attachments.js +49 -0
  219. package/src/storage/operational-context.js +50 -0
  220. package/src/storage/project-backup.js +98 -0
  221. package/src/storage/project-bootstrap.js +24 -0
  222. package/src/storage/project-boundary.js +79 -0
  223. package/src/storage/project-read-snapshot.js +18 -0
  224. package/src/storage/project-replacement.js +137 -0
  225. package/src/storage/project-restore.js +203 -0
  226. package/src/storage/record-definitions.js +45 -0
  227. package/src/storage/repository.js +507 -0
  228. package/src/storage/restore-execution-provenance.js +26 -0
  229. package/src/storage/restore-journal.js +62 -0
  230. package/src/storage/restore-layout.js +30 -0
  231. package/src/storage/restore-preparation.js +84 -0
  232. package/src/storage/restore-replacement-archive.js +162 -0
  233. package/src/storage/restore-replacement-owner.js +31 -0
  234. package/src/storage/restore-replacement-preparation.js +128 -0
  235. package/src/storage/restore-replacement.js +142 -0
  236. package/src/storage/restore-terminal.js +32 -0
  237. package/src/storage/runtime.js +31 -0
  238. package/src/storage/schema.js +214 -0
  239. package/src/storage/singleton-import.js +84 -0
  240. package/src/storage/snapshot-continuity.js +16 -0
  241. package/src/storage/snapshot.js +36 -0
  242. package/src/storage/storage-marker.js +79 -0
  243. package/src/storage/task-guards.js +107 -0
  244. package/src/storage/topology.js +42 -0
  245. package/src/storage/transaction.js +179 -0
  246. package/src/storage/unit-of-work.js +667 -0
  247. package/src/storage/windows-process-incarnation.js +206 -0
  248. package/.forgeloop/forgeloop.gitignore +0 -4
  249. package/LICENSE-DOCS.md +0 -13
  250. package/src/core/transaction-maintenance.js +0 -70
@@ -1,8 +1,5 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
-
4
1
  import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
5
- import { appendProtocolEvent, readEvents } from "./events.js";
2
+ import { appendProtocolEvent, iterateEvents } from "./events.js";
6
3
  import {
7
4
  canonicalActionFingerprint,
8
5
  actionRequiresIdempotency,
@@ -22,8 +19,7 @@ import {
22
19
  E_ACTION_STATE_MISMATCH,
23
20
  E_ACTION_VERIFICATION_REQUIRED,
24
21
  } from "./error-codes.js";
25
- import { assertSafePath, ensureWithin } from "./filesystem.js";
26
- import { taskActionPath, taskDirectory, TASK_ARTIFACT_FILES } from "./task-paths.js";
22
+ import { requireNativeStore, withNativeReadScope } from "./native-storage.js";
27
23
 
28
24
  const STATE_EVENT_NAMES = Object.freeze({
29
25
  AUTHORIZED: "ACTION_AUTHORIZED",
@@ -41,61 +37,26 @@ function actionError(code, message) {
41
37
  return error;
42
38
  }
43
39
 
44
- async function readActionFile(target, packageRoot, taskId, actionId) {
45
- const relPath = taskActionPath(taskId, actionId);
46
- await assertSafePath(target, relPath);
47
- const absolute = ensureWithin(target, relPath);
48
- let text;
49
- try {
50
- text = await readFile(absolute, "utf8");
51
- } catch (error) {
52
- if (error?.code === "ENOENT") return null;
53
- throw error;
54
- }
55
- try {
56
- return JSON.parse(text);
57
- } catch (error) {
58
- throw actionError(E_ACTION_INVALID, `durable action artifact is not valid JSON: ${relPath}`);
59
- }
60
- }
61
-
62
- async function writeActionFile(target, packageRoot, taskId, action) {
63
- const relPath = taskActionPath(taskId, action.actionId);
64
- await assertSafePath(target, relPath);
65
- const serialized = `${JSON.stringify(action, null, 2)}\n`;
66
- const activeTransaction = (await getTaskTransaction(target));
67
- if (activeTransaction) {
68
- await activeTransaction.stageText(relPath, serialized);
69
- } else {
70
- const absolute = ensureWithin(target, relPath);
71
- await mkdir(path.dirname(absolute), { recursive: true });
72
- await writeFile(absolute, serialized, "utf8");
73
- }
40
+ async function readActionFile(target, taskId, actionId) {
41
+ return withNativeReadScope(target, () => requireNativeStore(
42
+ target,
43
+ actionError,
44
+ "Actions require canonical SQLite storage; migrate legacy operational state explicitly",
45
+ ).readAction(taskId, actionId));
74
46
  }
75
47
 
76
- function taskActionsDirectory(taskId) {
77
- return `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.actions}`;
48
+ async function writeActionFile(target, action) {
49
+ const transaction = await getTaskTransaction(target);
50
+ if (!transaction) throw actionError("E_STORAGE_TRANSACTION_INVALID", "Action persistence requires an active task transaction");
51
+ return transaction.stageAction(action);
78
52
  }
79
53
 
80
- async function listActionFiles(target, packageRoot, taskId) {
81
- const relDir = taskActionsDirectory(taskId);
82
- await assertSafePath(target, relDir);
83
- const absoluteDir = ensureWithin(target, relDir);
84
- let entries;
85
- try {
86
- entries = await readdir(absoluteDir);
87
- } catch (error) {
88
- if (error?.code === "ENOENT") return [];
89
- throw error;
90
- }
91
- const actions = [];
92
- for (const entry of entries) {
93
- if (!entry.endsWith(".json")) continue;
94
- const parsed = await readActionFile(target, packageRoot, taskId, entry.replace(/\.json$/, ""));
95
- if (parsed) actions.push(parsed);
96
- }
97
- actions.sort((left, right) => String(left.createdAt).localeCompare(String(right.createdAt)));
98
- return actions;
54
+ async function listActionFiles(target, taskId) {
55
+ return withNativeReadScope(target, () => requireNativeStore(
56
+ target,
57
+ actionError,
58
+ "Action listing requires canonical SQLite storage; migrate legacy operational state explicitly",
59
+ ).listActions(taskId).sort((left, right) => String(left.createdAt).localeCompare(String(right.createdAt))));
99
60
  }
100
61
 
101
62
  function assertProposeInput(input) {
@@ -152,7 +113,7 @@ export async function proposeAction(target, { packageRoot, taskId, input }) {
152
113
  );
153
114
  }
154
115
 
155
- const existingById = await readActionFile(target, packageRoot, taskId, input.actionId);
116
+ const existingById = await readActionFile(target, taskId, input.actionId);
156
117
  if (existingById) {
157
118
  if (existingById.actionFingerprint === identityFingerprint) {
158
119
  validateActionArtifact(existingById);
@@ -185,7 +146,7 @@ export async function proposeAction(target, { packageRoot, taskId, input }) {
185
146
  };
186
147
  validateActionArtifact(action);
187
148
 
188
- await writeActionFile(target, packageRoot, taskId, action);
149
+ await writeActionFile(target, action);
189
150
  await appendProtocolEvent(target, {
190
151
  taskId,
191
152
  event: "ACTION_PROPOSED",
@@ -207,25 +168,28 @@ export async function proposeAction(target, { packageRoot, taskId, input }) {
207
168
  );
208
169
  }
209
170
 
210
- export async function readAction(target, { packageRoot, taskId, actionId }) {
211
- const action = await readActionFile(target, packageRoot, taskId, actionId);
171
+ export async function readAction(target, { taskId, actionId }) {
172
+ const action = await readActionFile(target, taskId, actionId);
212
173
  if (!action) {
213
174
  throw actionError(E_ACTION_NOT_FOUND, `durable action ${actionId} does not exist for task ${taskId}`);
214
175
  }
215
176
  return validateActionArtifact(action);
216
177
  }
217
178
 
218
- export async function listActions(target, { packageRoot, taskId }) {
219
- const actions = await listActionFiles(target, packageRoot, taskId);
179
+ export async function listActions(target, { taskId }) {
180
+ const actions = await listActionFiles(target, taskId);
220
181
  return actions.map((action) => validateActionArtifact(action));
221
182
  }
222
183
 
223
- export async function findActionByIdempotencyKey(target, { packageRoot, taskId, idempotencyKey }) {
184
+ export async function findActionByIdempotencyKey(target, { taskId, idempotencyKey }) {
224
185
  if (typeof idempotencyKey !== "string" || !idempotencyKey) {
225
186
  throw actionError(E_ACTION_INVALID, "idempotencyKey must be a non-empty string");
226
187
  }
227
- const actions = await listActionFiles(target, packageRoot, taskId);
228
- const found = actions.find((action) => action.idempotencyKey === idempotencyKey);
188
+ const found = await withNativeReadScope(target, () => requireNativeStore(
189
+ target,
190
+ actionError,
191
+ "Action listing requires canonical SQLite storage; migrate legacy operational state explicitly",
192
+ ).findActionByIdempotencyKey(taskId, idempotencyKey));
229
193
  return found ? validateActionArtifact(found) : null;
230
194
  }
231
195
 
@@ -289,7 +253,7 @@ async function applyTransition(target, {
289
253
  next.commitResultCode = details.commitResultCode;
290
254
  }
291
255
  validateActionArtifact(next);
292
- await writeActionFile(target, packageRoot, taskId, next);
256
+ await writeActionFile(target, next);
293
257
 
294
258
  const baseDetails = {
295
259
  actionId: next.actionId,
@@ -413,13 +377,12 @@ export async function transitionVerifiedAction(target, {
413
377
  }
414
378
 
415
379
  export async function detectOrphanActions(target, { packageRoot, taskId }) {
416
- const actions = await listActionFiles(target, packageRoot, taskId);
417
- const events = await readEvents(target, packageRoot, { taskId });
418
- const proposedFingerprints = new Set(
419
- events
420
- .filter((event) => event.event === "ACTION_PROPOSED")
421
- .map((event) => event.details?.actionFingerprint),
422
- );
380
+ const actions = await listActionFiles(target, taskId);
381
+ const candidates = new Set(actions.map(action => action.actionFingerprint));
382
+ const proposedFingerprints = new Set();
383
+ for await (const event of iterateEvents(target, packageRoot, { taskId })) {
384
+ if (event.event === "ACTION_PROPOSED" && candidates.has(event.details?.actionFingerprint)) proposedFingerprints.add(event.details?.actionFingerprint);
385
+ }
423
386
  return actions
424
387
  .filter((action) => !proposedFingerprints.has(action.actionFingerprint))
425
388
  .map((action) => action.actionId);
@@ -431,7 +394,7 @@ export async function detectOrphanActions(target, { packageRoot, taskId }) {
431
394
  * authorization, reconciliation, verification) and compared to its artifact.
432
395
  */
433
396
  export async function validateActionLedgerConsistency(target, { packageRoot, taskId }) {
434
- const actions = await listActionFiles(target, packageRoot, taskId);
397
+ const actions = await listActionFiles(target, taskId);
435
398
  const { projectActionLedger } = await import("./action-ledger-projection.js");
436
399
  const issues = [];
437
400
  for (const action of actions) {
@@ -3,8 +3,14 @@ import { randomUUID } from "node:crypto";
3
3
  import { ARTIFACT_PATHS, writeJsonArtifact } from "./artifacts.js";
4
4
  import { PROTOCOL_VERSION } from "./protocol.js";
5
5
  import { sessionArtifactPath } from "./task-paths.js";
6
+ import { getOperationalStore } from "../storage/operational-context.js";
7
+ import { withTaskTransaction } from "./transaction.js";
6
8
 
7
9
  export async function activateSession(target, packageRoot, options = {}) {
10
+ if (!getOperationalStore(target)) {
11
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
12
+ return withProjectStorage(target, () => activateSession(target, packageRoot, options));
13
+ }
8
14
  const sessionId = options.sessionId ?? randomUUID();
9
15
  const value = {
10
16
  schemaVersion: 1,
@@ -14,7 +20,9 @@ export async function activateSession(target, packageRoot, options = {}) {
14
20
  createdAt: options.createdAt ?? new Date().toISOString(),
15
21
  };
16
22
  const relativePath = sessionArtifactPath(sessionId);
17
- const written = await writeJsonArtifact(target, relativePath, value, "activation", packageRoot, options);
18
- await writeJsonArtifact(target, ARTIFACT_PATHS.session, value, "activation", packageRoot, options).catch(() => {});
19
- return { ...written.value, path: relativePath };
23
+ return withTaskTransaction({ target, taskId: sessionId, operation: "activate-session", packageRoot }, async () => {
24
+ const written = await writeJsonArtifact(target, relativePath, value, "activation", packageRoot);
25
+ await writeJsonArtifact(target, ARTIFACT_PATHS.session, value, "activation", packageRoot);
26
+ return { ...written.value, path: relativePath };
27
+ });
20
28
  }
@@ -1,6 +1,3 @@
1
- import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
-
4
1
  import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
5
2
  import { appendProtocolEvent } from "./events.js";
6
3
  import { canonicalFingerprint } from "./artifacts.js";
@@ -16,11 +13,10 @@ import {
16
13
  E_APPROVAL_INVALID,
17
14
  E_APPROVAL_STALE,
18
15
  } from "./error-codes.js";
19
- import { assertSafePath, ensureWithin } from "./filesystem.js";
20
16
  import { isTrustedHostAuthorityContext } from "./capability-policy.js";
21
- import { taskApprovalPath, taskDirectory, TASK_ARTIFACT_FILES } from "./task-paths.js";
22
17
  import { readAction } from "./actions.js";
23
18
  import { readWorkState } from "./work-state.js";
19
+ import { requireNativeStore, withNativeReadScope } from "./native-storage.js";
24
20
 
25
21
  function approvalError(code, message) {
26
22
  const error = new Error(message);
@@ -29,55 +25,25 @@ function approvalError(code, message) {
29
25
  }
30
26
 
31
27
  async function readApprovalFile(target, taskId, approvalId) {
32
- const relPath = taskApprovalPath(taskId, approvalId);
33
- await assertSafePath(target, relPath);
34
- let text;
35
- try {
36
- text = await readFile(ensureWithin(target, relPath), "utf8");
37
- } catch (error) {
38
- if (error?.code === "ENOENT") return null;
39
- throw error;
40
- }
41
- try {
42
- return JSON.parse(text);
43
- } catch {
44
- throw approvalError(E_APPROVAL_INVALID, `approval artifact is not valid JSON: ${relPath}`);
45
- }
28
+ return withNativeReadScope(target, () => requireNativeStore(
29
+ target,
30
+ approvalError,
31
+ "Approvals require canonical SQLite storage; migrate legacy operational state explicitly",
32
+ ).readApproval(taskId, approvalId));
46
33
  }
47
34
 
48
- async function writeApprovalFile(target, taskId, approval) {
49
- const relPath = taskApprovalPath(taskId, approval.approvalId);
50
- await assertSafePath(target, relPath);
51
- const serialized = `${JSON.stringify(approval, null, 2)}\n`;
52
- const activeTransaction = (await getTaskTransaction(target));
53
- if (activeTransaction) {
54
- await activeTransaction.stageText(relPath, serialized);
55
- } else {
56
- const absolute = ensureWithin(target, relPath);
57
- await mkdir(path.dirname(absolute), { recursive: true });
58
- await writeFile(absolute, serialized, "utf8");
59
- }
35
+ async function writeApprovalFile(target, approval) {
36
+ const transaction = await getTaskTransaction(target);
37
+ if (!transaction) throw approvalError("E_STORAGE_TRANSACTION_INVALID", "Approval persistence requires an active task transaction");
38
+ return transaction.stageApproval(approval);
60
39
  }
61
40
 
62
41
  async function listApprovalFiles(target, taskId) {
63
- const relDir = `${taskDirectory(taskId)}/${TASK_ARTIFACT_FILES.approvals}`;
64
- await assertSafePath(target, relDir);
65
- const absoluteDir = ensureWithin(target, relDir);
66
- let entries;
67
- try {
68
- entries = await readdir(absoluteDir);
69
- } catch (error) {
70
- if (error?.code === "ENOENT") return [];
71
- throw error;
72
- }
73
- const approvals = [];
74
- for (const entry of entries) {
75
- if (!entry.endsWith(".json")) continue;
76
- const parsed = await readApprovalFile(target, taskId, entry.replace(/\.json$/, ""));
77
- if (parsed) approvals.push(parsed);
78
- }
79
- approvals.sort((left, right) => String(left.requestedAt).localeCompare(String(right.requestedAt)));
80
- return approvals;
42
+ return withNativeReadScope(target, () => requireNativeStore(
43
+ target,
44
+ approvalError,
45
+ "Approval listing requires canonical SQLite storage; migrate legacy operational state explicitly",
46
+ ).listApprovals(taskId).sort((left, right) => String(left.requestedAt).localeCompare(String(right.requestedAt))));
81
47
  }
82
48
 
83
49
  function approvalBindingFields(approval) {
@@ -207,7 +173,7 @@ export async function requestApproval(target, { packageRoot, taskId, input }) {
207
173
  };
208
174
  validateApprovalArtifact(approval);
209
175
 
210
- await writeApprovalFile(target, taskId, approval);
176
+ await writeApprovalFile(target, approval);
211
177
  await appendProtocolEvent(target, {
212
178
  taskId,
213
179
  event: "APPROVAL_REQUESTED",
@@ -295,7 +261,7 @@ export async function resolveApproval(target, {
295
261
  reason: reason ?? current.reason ?? null,
296
262
  };
297
263
  validateApprovalArtifact(resolved);
298
- await writeApprovalFile(target, taskId, resolved);
264
+ await writeApprovalFile(target, resolved);
299
265
 
300
266
  const details = {
301
267
  approvalId: resolved.approvalId,
@@ -1,12 +1,12 @@
1
1
  import { TASK_STATE_ROOT, TASK_ARTIFACT_FILES, PROJECT_ARTIFACT_PATHS, SESSIONS_ROOT } from "./task-paths.js";
2
2
 
3
3
  /**
4
- * Canonical registry of ForgeLoop artifacts, defining filesystem paths,
4
+ * Canonical registry of ForgeLoop artifacts, defining logical identities,
5
5
  * schema bindings, ownership, mutability, trust classifications, and scopes.
6
6
  *
7
7
  * Schema field shapes remain strictly owned by JSON schemas in `schemas/`.
8
8
  */
9
- export const ARTIFACT_REGISTRY = Object.freeze({
9
+ const ARTIFACT_DEFINITIONS = Object.freeze({
10
10
  descriptor: Object.freeze({
11
11
  key: "descriptor",
12
12
  scope: "TASK",
@@ -416,3 +416,60 @@ export const ARTIFACT_REGISTRY = Object.freeze({
416
416
  description: "Deterministic test inventory and bounded utility analysis; never completion evidence or deletion authority.",
417
417
  }),
418
418
  });
419
+
420
+ const DATABASE_PATH = ".forgeloop/state.sqlite";
421
+ const RECORD_TABLES = Object.freeze({ descriptor: "tasks", state: "tasks", events: "events", actions: "actions", approvals: "approvals", executions: "executions", session: "sessions" });
422
+ const ARTIFACT_KINDS = Object.freeze({ gates: "gate", evaluations: "evaluation", handoffs: "handoff", semanticDecisions: "decision", codeManifest: "attestation", attestationStatement: "attestation" });
423
+
424
+ function canonicalStorage(artifact) {
425
+ if (artifact.scope === "PROJECT") return Object.freeze({ backend: "FILE", path: artifact.path });
426
+ if (artifact.key === "attestationBundle") {
427
+ return Object.freeze({ backend: "SQLITE_REFERENCED_FILE", path: DATABASE_PATH, table: "attachment_references", attachmentPath: ".forgeloop/attachments/objects/<sha256>" });
428
+ }
429
+ const table = RECORD_TABLES[artifact.key] ?? "task_artifacts";
430
+ return Object.freeze({ backend: "SQLITE", path: DATABASE_PATH, table,
431
+ ...(table === "task_artifacts" ? { kind: ARTIFACT_KINDS[artifact.key] ?? artifact.key } : {}),
432
+ ...(artifact.key === "descriptor" ? { column: "descriptor_json" } : {}),
433
+ ...(artifact.key === "state" ? { column: "state_json" } : {}),
434
+ });
435
+ }
436
+
437
+ /** `path` stays a historical logical alias; it is never a live-file guarantee. */
438
+ export const ARTIFACT_REGISTRY = Object.freeze(Object.fromEntries(
439
+ Object.entries(ARTIFACT_DEFINITIONS).map(([key, artifact]) => [key, Object.freeze({
440
+ ...artifact,
441
+ logicalPath: artifact.path,
442
+ canonicalStorage: canonicalStorage(artifact),
443
+ // Configuration and policy inputs are copied separately from database export.
444
+ exportPath: artifact.scope === "PROJECT" ? null : artifact.key === "attestationBundle" ? ".forgeloop/attachments/objects/<sha256>" : artifact.path,
445
+ ...(artifact.key === "attestationBundle" ? { exportBindingPath: "export-index.json#attachments" } : {}),
446
+ })]),
447
+ ));
448
+
449
+ function portableSingletonMapping(key) {
450
+ return Object.freeze({
451
+ key, kind: ARTIFACT_REGISTRY[key].canonicalStorage.kind, filename: TASK_ARTIFACT_FILES[key],
452
+ });
453
+ }
454
+
455
+ function portableCollectionMapping([key, registryKey]) {
456
+ return Object.freeze({
457
+ kind: ARTIFACT_REGISTRY[registryKey].canonicalStorage.kind, directory: TASK_ARTIFACT_FILES[key],
458
+ });
459
+ }
460
+
461
+ /** Ordered interchange mappings; bundle-specific filenames remain in bundles.js. */
462
+ export const PORTABLE_ARTIFACT_MAPPINGS = Object.freeze({
463
+ singletons: Object.freeze([
464
+ "contract", "route", "preflight", "continuity", "receipt", "policySnapshot",
465
+ "recovery", "workspaceBinding", "responsibility", "verificationScope", "usage", "testUtility",
466
+ ].map(portableSingletonMapping)),
467
+ collections: Object.freeze([
468
+ ["gates", "gates"],
469
+ ["handoffs", "handoffs"],
470
+ ["decisions", "semanticDecisions"],
471
+ ["attestations", "codeManifest"],
472
+ ["evaluations", "evaluations"],
473
+ ["structuralQuality", "structuralQuality"],
474
+ ].map(portableCollectionMapping)),
475
+ });
@@ -1,3 +1,5 @@
1
+ import { ARTIFACT_REGISTRY } from "./artifact-registry.js";
2
+ import { needsExistingProjectScope, withExistingProjectScope, assertNativeWritePath, isOperationalArtifactPath } from "../storage/existing-project-scope.js";
1
3
  import { createHash } from "node:crypto";
2
4
 
3
5
  import { assertSafePath, ensureWithin, fileExists, readBytes, writeFileAtomic } from "./filesystem.js";
@@ -6,6 +8,12 @@ import { assertJsonBytes, assertJsonLimits } from "./json-safety.js";
6
8
  import { assertSchema, readSchema } from "./schema-validation.js";
7
9
  import { getPackageRoot } from "./templates.js";
8
10
  import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
11
+ import { getOperationalStore, readOperationalText } from "../storage/operational-context.js";
12
+ import { withNativeReadScope } from "./native-storage.js";
13
+
14
+ const OPERATIONAL_SCHEMAS = new Set(Object.values(ARTIFACT_REGISTRY)
15
+ .filter(artifact => artifact.scope !== "PROJECT" && artifact.schema)
16
+ .map(artifact => artifact.schema));
9
17
 
10
18
  export const ARTIFACT_PATHS = Object.freeze({
11
19
  contract: ".forgeloop/current-contract.json",
@@ -63,12 +71,43 @@ function artifactError(code, relativePath, error) {
63
71
  );
64
72
  }
65
73
 
74
+ /** Validate already captured canonical bytes after their native read scope closes. */
75
+ export async function parseCapturedJsonArtifact(text, relativePath, schemaName, packageRoot = getPackageRoot()) {
76
+ if (text === null) throw new ArtifactError("ARTIFACT_MISSING", `Artifact is missing: ${relativePath}`, [relativePath]);
77
+ try {
78
+ assertJsonBytes(text, relativePath);
79
+ const value = JSON.parse(text);
80
+ assertJsonLimits(value, relativePath);
81
+ assertSchema(value, await readSchema(schemaName, packageRoot), relativePath);
82
+ return { value, path: relativePath, fingerprint: canonicalFingerprint(value) };
83
+ } catch (error) { throw artifactError(error.code === "JSON_LIMIT_EXCEEDED" ? error.code : "ARTIFACT_INVALID", relativePath, error); }
84
+ }
85
+
66
86
  export async function readJsonArtifact(
67
87
  target,
68
88
  relativePath,
69
89
  schemaName,
70
90
  packageRoot = getPackageRoot(),
71
91
  ) {
92
+ return withNativeReadScope(target, async () => {
93
+ const operational = readOperationalText(target, relativePath);
94
+ if (operational.selected) {
95
+ return parseCapturedJsonArtifact(operational.text, relativePath, schemaName, packageRoot);
96
+ }
97
+ if (isOperationalArtifactPath(relativePath)) {
98
+ try { await assertSafePath(target, relativePath); }
99
+ catch (error) { throw artifactError("ARTIFACT_PATH_INVALID", relativePath, error); }
100
+ if (!(await fileExists(ensureWithin(target, relativePath)))) {
101
+ throw new ArtifactError("ARTIFACT_MISSING", `Artifact is missing: ${relativePath}`, [relativePath]);
102
+ }
103
+ throw new ArtifactError("E_STORAGE_MIGRATION_REQUIRED", "Operational JSON reads require canonical SQLite storage; migrate legacy state explicitly", [relativePath]);
104
+ }
105
+ return readPortableJsonArtifact(target, relativePath, schemaName, packageRoot);
106
+ });
107
+ }
108
+
109
+ /** Explicit file inspection for migration inputs, portable exports and configuration. */
110
+ export async function readPortableJsonArtifact(target, relativePath, schemaName, packageRoot = getPackageRoot()) {
72
111
  try {
73
112
  await assertSafePath(target, relativePath);
74
113
  } catch (error) {
@@ -76,9 +115,7 @@ export async function readJsonArtifact(
76
115
  }
77
116
 
78
117
  const artifactPath = ensureWithin(target, relativePath);
79
- const transaction = (await getTaskTransaction(target));
80
- const stagedText = transaction ? await transaction.readText(relativePath) : null;
81
- if (stagedText === null && !(await fileExists(artifactPath))) {
118
+ if (!(await fileExists(artifactPath))) {
82
119
  throw new ArtifactError(
83
120
  "ARTIFACT_MISSING",
84
121
  `Artifact is missing: ${relativePath}`,
@@ -86,14 +123,9 @@ export async function readJsonArtifact(
86
123
  );
87
124
  }
88
125
 
89
- let value;
90
126
  try {
91
- const bytes = stagedText === null ? await readBytes(artifactPath) : Buffer.from(stagedText, "utf8");
92
- assertJsonBytes(bytes, relativePath);
93
- value = JSON.parse(bytes.toString("utf8"));
94
- assertJsonLimits(value, relativePath);
95
- const schema = await readSchema(schemaName, packageRoot);
96
- assertSchema(value, schema, relativePath);
127
+ const bytes = await readBytes(artifactPath);
128
+ return await parseCapturedJsonArtifact(bytes, relativePath, schemaName, packageRoot);
97
129
  } catch (error) {
98
130
  throw artifactError(
99
131
  ["JSON_LIMIT_EXCEEDED", "ARTIFACT_PATH_INVALID"].includes(error.code)
@@ -103,12 +135,6 @@ export async function readJsonArtifact(
103
135
  error,
104
136
  );
105
137
  }
106
-
107
- return {
108
- value,
109
- path: relativePath,
110
- fingerprint: canonicalFingerprint(value),
111
- };
112
138
  }
113
139
 
114
140
  export async function writeJsonArtifact(
@@ -119,17 +145,49 @@ export async function writeJsonArtifact(
119
145
  packageRoot = getPackageRoot(),
120
146
  { dryRun = false, taskId = null, operation = "write-artifact" } = {},
121
147
  ) {
148
+ if (OPERATIONAL_SCHEMAS.has(schemaName) && !isOperationalArtifactPath(relativePath)) {
149
+ throw new ArtifactError("E_STORAGE_OPERATION_UNSUPPORTED", "Operational artifacts require canonical SQLite identities; use explicit portable export", [relativePath]);
150
+ }
151
+ if (!getOperationalStore(target) && isOperationalArtifactPath(relativePath)) {
152
+ await assertSafePath(target, relativePath);
153
+ assertSecretFree(value);
154
+ assertSchema(value, await readSchema(schemaName, packageRoot), relativePath);
155
+ assertJsonLimits(value, relativePath);
156
+ assertJsonBytes(`${JSON.stringify(value, null, 2)}\n`, relativePath);
157
+ const { withProjectStorage } = await import("../storage/project-boundary.js");
158
+ return withProjectStorage(target, () => writeJsonArtifact(target, relativePath, value, schemaName, packageRoot, { dryRun, taskId, operation }), { readOnly: dryRun });
159
+ }
160
+ if (await needsExistingProjectScope(target)) {
161
+ return withExistingProjectScope(target, () => writeJsonArtifact(target, relativePath, value, schemaName, packageRoot, { dryRun, taskId, operation }), { readOnly: dryRun });
162
+ }
163
+ assertNativeWritePath(target, relativePath);
164
+ const store = getOperationalStore(target);
165
+ if (store?.recognizes(relativePath)) {
166
+ assertSecretFree(value);
167
+ assertSchema(value, await readSchema(schemaName, packageRoot), relativePath);
168
+ assertJsonLimits(value, relativePath);
169
+ const text = `${JSON.stringify(value, null, 2)}\n`;
170
+ assertJsonBytes(text, relativePath);
171
+ if (!dryRun) {
172
+ const scopedTaskId = taskId ?? value.taskId ?? store.transaction?.taskId;
173
+ if (!scopedTaskId) throw new ArtifactError("E_TASK_REQUIRED", "Operational artifact requires a task identity", [relativePath]);
174
+ await withTaskTransaction({ target, taskId: scopedTaskId, operation, packageRoot }, tx => tx.stageJsonRecord(relativePath, text, schemaName));
175
+ }
176
+ return { path: relativePath, fingerprint: canonicalFingerprint(value), value };
177
+ }
178
+ return writePortableJsonArtifact(target, relativePath, value, schemaName, packageRoot, { dryRun });
179
+ }
180
+
181
+ /** Explicit interchange/configuration output; never canonical operational persistence. */
182
+ export async function writePortableJsonArtifact(target, relativePath, value, schemaName, packageRoot = getPackageRoot(), { dryRun = false } = {}) {
183
+ if (isOperationalArtifactPath(relativePath)) throw new ArtifactError("E_STORAGE_OPERATION_UNSUPPORTED", "Portable output cannot replace a canonical operational namespace", [relativePath]);
184
+ assertNativeWritePath(target, relativePath);
185
+ if (!dryRun && await getTaskTransaction(target)) throw new ArtifactError("E_STORAGE_TRANSACTION_INVALID", "Portable output requires no active operational transaction", [relativePath]);
122
186
  try {
123
187
  await assertSafePath(target, relativePath);
124
188
  } catch (error) {
125
189
  throw artifactError("ARTIFACT_PATH_INVALID", relativePath, error);
126
190
  }
127
- const activeTransaction = (await getTaskTransaction(target));
128
- if (!activeTransaction && taskId && !dryRun) {
129
- return withTaskTransaction({ target, taskId, operation, packageRoot }, async () => (
130
- writeJsonArtifact(target, relativePath, value, schemaName, packageRoot, { dryRun, taskId, operation })
131
- ));
132
- }
133
191
  try {
134
192
  const artifactPath = ensureWithin(target, relativePath);
135
193
  assertSecretFree(value);
@@ -138,11 +196,7 @@ export async function writeJsonArtifact(
138
196
  assertJsonLimits(value, relativePath);
139
197
  const serialized = `${JSON.stringify(value, null, 2)}\n`;
140
198
  assertJsonBytes(serialized, relativePath);
141
- if (activeTransaction && !dryRun) {
142
- await activeTransaction.stageText(relativePath, serialized);
143
- } else {
144
- await writeFileAtomic(artifactPath, serialized, { dryRun });
145
- }
199
+ await writeFileAtomic(artifactPath, serialized, { dryRun });
146
200
  return { path: relativePath, fingerprint: canonicalFingerprint(value), value };
147
201
  } catch (error) {
148
202
  throw artifactError(
@@ -0,0 +1,45 @@
1
+ import { getOperationalStore, readOperationalText } from "../storage/operational-context.js";
2
+ import { validateAttachmentReference } from "../storage/attachment-binding.js";
3
+ import { taskStorageKey } from "./task-identity.js";
4
+ import { taskAttestationBundlePath, taskAttestationStatementPath } from "./task-paths.js";
5
+ import { legacyAttachmentReferenceId, signatureReferencePrefix } from "./signing/bundle-reference.js";
6
+
7
+ function resolveCanonicalSignature(target, taskId, store) {
8
+ const source = readOperationalText(target, taskAttestationStatementPath(taskId));
9
+ if (source.text === null) return null;
10
+ const prefix = signatureReferencePrefix(source.text);
11
+ // At most two distinct objects are enough to detect ambiguity. Repeated
12
+ // bindings to identical bytes do not make selection ambiguous.
13
+ const rows = store.observe(`signature:${taskId}:${prefix}`, () => store.db.prepare(
14
+ "SELECT MIN(reference_id) AS reference_id, path, size, sha256 FROM attachment_references WHERE task_id = ? AND reference_id >= ? AND reference_id < ? GROUP BY path, size, sha256 ORDER BY path LIMIT 2",
15
+ ).all(taskId, prefix, `${prefix}~`));
16
+ const paths = new Set(rows.map(row => validateAttachmentReference({ taskId, referenceId: row.reference_id, path: row.path, size: row.size, sha256: row.sha256 }).path));
17
+ for (const reference of store.attachments.values()) {
18
+ if (reference.taskId === taskId && reference.referenceId.startsWith(prefix)) paths.add(validateAttachmentReference(reference).path);
19
+ }
20
+ if (paths.size > 1) {
21
+ throw Object.assign(new Error("Multiple signature bundles bind this statement; supply an explicit bundle path"), { code: "E_ATTESTATION_SIGNATURE_INVALID" });
22
+ }
23
+ return paths.values().next().value ?? null;
24
+ }
25
+
26
+ /** Preserve legacy logical references while locating external SQLite assets. */
27
+ export function resolveAttestationBundlePath(target, taskId, suppliedPath = null) {
28
+ const legacy = taskAttestationBundlePath(taskId);
29
+ const store = getOperationalStore(target);
30
+ if (store && suppliedPath) {
31
+ const referenceId = legacyAttachmentReferenceId(suppliedPath);
32
+ const row = store.observe(`attachment-alias:${JSON.stringify([taskId, referenceId])}`, () => store.db.prepare(
33
+ "SELECT * FROM attachment_references WHERE task_id = ? AND reference_id = ?",
34
+ ).get(taskId, referenceId) ?? null);
35
+ const staged = store.attachments.get(JSON.stringify([taskId, referenceId]));
36
+ const reference = staged ?? (row ? { taskId: row.task_id, referenceId: row.reference_id, path: row.path, size: row.size, sha256: row.sha256 } : null);
37
+ if (reference) return validateAttachmentReference(reference).path;
38
+ }
39
+ if (store && (!suppliedPath || suppliedPath === legacy)) {
40
+ const canonical = resolveCanonicalSignature(target, taskId, store);
41
+ if (canonical) return canonical;
42
+ return `.forgeloop/attachments/${taskStorageKey(taskId)}/attestations/statement.sigstore.json`;
43
+ }
44
+ return suppliedPath ?? legacy;
45
+ }
@@ -6,6 +6,7 @@ import { resolveRevisionProvider } from "./revision/provider.js";
6
6
  import { REVISION_PROVIDERS } from "./revision/registry.js";
7
7
  import { assertAttestationStatementBindings, verifyCodeManifestContent } from "./attestation-verifier.js";
8
8
  import { taskAttestationBundlePath } from "./task-paths.js";
9
+ import { withProjectReadSnapshot } from "../storage/project-read-snapshot.js";
9
10
 
10
11
  function pathMatchesExclusion(pathValue, exclusions) {
11
12
  return exclusions.some((pattern) => {
@@ -24,7 +25,11 @@ function isProtocolMetadataPath(value) {
24
25
  return normalized === ".forgeloop" || normalized.startsWith(".forgeloop/");
25
26
  }
26
27
 
27
- export async function evaluateAttestationCoverage({
28
+ export async function evaluateAttestationCoverage(options = {}) {
29
+ return withProjectReadSnapshot(options.target, () => evaluateSelectedAttestationCoverage(options));
30
+ }
31
+
32
+ async function evaluateSelectedAttestationCoverage({
28
33
  target,
29
34
  packageRoot = getPackageRoot(),
30
35
  revisionProvider = "git",