@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
@@ -0,0 +1,214 @@
1
+ /**
2
+ * ForgeLoop SQLite storage schema.
3
+ *
4
+ * This module owns DDL and schema versioning only. It performs no I/O against a
5
+ * project and holds no connection state, so schema definitions can be imported
6
+ * from tooling that must stay independent of the live store.
7
+ *
8
+ * Design constraints (see the SQLite migration plan):
9
+ * - Indexed relational fields carry identity, ordering, and lookup paths.
10
+ * - Nested protocol structures stay as schema-validated canonical JSON
11
+ * payloads instead of being normalized table by table.
12
+ * - Extracted columns and payloads are written in one storage operation and
13
+ * are never updated independently.
14
+ * - Foreign keys cover only relationships that are mandatory in the actual
15
+ * protocol. Audit history never cascades away from an explicit request.
16
+ */
17
+
18
+ /** Shared verbatim so the partial index and reservation query have one predicate.
19
+ * This index only identifies histories requiring conservative retention proof;
20
+ * mutation authority still requires the complete canonical ledger validation.
21
+ */
22
+ export const EVENT_RESERVATION_PROOF_PREDICATE = `CASE WHEN json_valid(event_json) = 0 THEN 1
23
+ ELSE json_type(event_json) <> 'object'
24
+ OR json_extract(event_json, '$.taskId') IS NOT task_id
25
+ OR json_extract(event_json, '$.seq') IS NOT seq
26
+ OR json_extract(event_json, '$.hash') IS NOT hash
27
+ OR json_extract(event_json, '$.previousHash') IS NOT previous_hash
28
+ OR json_extract(event_json, '$.at') IS NOT at
29
+ OR json_extract(event_json, '$.event') IS NOT event_type
30
+ OR json_extract(event_json, '$.event') IN
31
+ ('CONTRACT_BOOTSTRAP_REPAIR_RECORDED', 'CONTRACT_BOOTSTRAP_REPAIR_MIGRATION_RECORDED')
32
+ END`;
33
+
34
+ /** Bumped whenever required DDL changes incompatibly. */
35
+ export const STORAGE_SCHEMA_VERSION = 5;
36
+
37
+ /** Storage format marker recorded in `storage_meta` for cutover detection. */
38
+ export const STORAGE_FORMAT = "sqlite";
39
+ export const STORAGE_FORMAT_VERSION = 1;
40
+ export const STORAGE_RELATIVE_PATH = ".forgeloop/state.sqlite";
41
+
42
+ /**
43
+ * Declarative migration list. Each entry is applied exactly once, in order, and
44
+ * recorded in `storage_meta` so an existing database upgrades in place.
45
+ */
46
+ export const SCHEMA_MIGRATIONS = Object.freeze([
47
+ Object.freeze({
48
+ version: 1,
49
+ name: "initial-operational-store",
50
+ statements: Object.freeze([
51
+ // Singleton row describing this store. `id` is pinned to 1 so a second
52
+ // writer cannot silently create a competing metadata row.
53
+ `CREATE TABLE storage_meta (
54
+ id INTEGER PRIMARY KEY CHECK (id = 1),
55
+ storage_version INTEGER NOT NULL,
56
+ schema_version INTEGER NOT NULL,
57
+ protocol_version INTEGER,
58
+ project_identity TEXT,
59
+ storage_format TEXT NOT NULL DEFAULT '${STORAGE_FORMAT}',
60
+ created_at TEXT NOT NULL,
61
+ updated_at TEXT NOT NULL,
62
+ import_provenance TEXT
63
+ )`,
64
+
65
+ // Task identity plus the two canonical snapshots that discovery reads.
66
+ // `descriptor_json` and `state_json` are retained verbatim so the store
67
+ // never becomes a lossy re-encoding of protocol state.
68
+ `CREATE TABLE tasks (
69
+ task_id TEXT PRIMARY KEY,
70
+ task_key TEXT NOT NULL UNIQUE,
71
+ phase TEXT,
72
+ revision INTEGER,
73
+ created_at TEXT,
74
+ updated_at TEXT,
75
+ descriptor_json TEXT NOT NULL,
76
+ state_json TEXT
77
+ )`,
78
+ `CREATE INDEX tasks_phase_updated_idx ON tasks (phase, updated_at)`,
79
+
80
+ // Normalized write claims. Directory, wildcard, and ancestor/descendant
81
+ // overlap cannot be expressed as exact-path uniqueness, so callers must
82
+ // still evaluate `claimsOverlap()` over the candidate rows returned here.
83
+ `CREATE TABLE claims (
84
+ task_id TEXT NOT NULL REFERENCES tasks (task_id) ON DELETE CASCADE,
85
+ claim_norm TEXT NOT NULL,
86
+ reservation_state TEXT NOT NULL DEFAULT 'ACTIVE',
87
+ created_at TEXT,
88
+ PRIMARY KEY (task_id, claim_norm)
89
+ )`,
90
+ `CREATE INDEX claims_norm_idx ON claims (claim_norm)`,
91
+
92
+ // Canonical ledger. `event_json` stores the original event object exactly
93
+ // as imported: historical events are never rehashed or rewritten.
94
+ `CREATE TABLE events (
95
+ task_id TEXT NOT NULL REFERENCES tasks (task_id) ON DELETE CASCADE,
96
+ seq INTEGER NOT NULL,
97
+ event_type TEXT NOT NULL,
98
+ at TEXT,
99
+ previous_hash TEXT,
100
+ hash TEXT NOT NULL,
101
+ event_json TEXT NOT NULL,
102
+ PRIMARY KEY (task_id, seq)
103
+ )`,
104
+ `CREATE INDEX events_type_idx ON events (task_id, event_type, seq)`,
105
+
106
+ // Task-local idempotency is enforced by the database rather than by a
107
+ // directory scan, so a duplicate key cannot create two accepted actions.
108
+ `CREATE TABLE actions (
109
+ task_id TEXT NOT NULL REFERENCES tasks (task_id) ON DELETE CASCADE,
110
+ action_id TEXT NOT NULL,
111
+ idempotency_key TEXT,
112
+ status TEXT,
113
+ revision INTEGER,
114
+ payload_json TEXT NOT NULL,
115
+ PRIMARY KEY (task_id, action_id)
116
+ )`,
117
+ `CREATE UNIQUE INDEX actions_idempotency_idx ON actions (task_id, idempotency_key) WHERE idempotency_key IS NOT NULL`,
118
+ `CREATE INDEX actions_status_idx ON actions (task_id, status)`,
119
+
120
+ `CREATE TABLE approvals (
121
+ task_id TEXT NOT NULL REFERENCES tasks (task_id) ON DELETE CASCADE,
122
+ approval_id TEXT NOT NULL,
123
+ action_id TEXT,
124
+ decision TEXT,
125
+ payload_json TEXT NOT NULL,
126
+ PRIMARY KEY (task_id, approval_id)
127
+ )`,
128
+ `CREATE INDEX approvals_action_idx ON approvals (task_id, action_id)`,
129
+
130
+ `CREATE TABLE executions (
131
+ task_id TEXT NOT NULL REFERENCES tasks (task_id) ON DELETE CASCADE,
132
+ execution_id TEXT NOT NULL,
133
+ check_id TEXT,
134
+ verification_cycle INTEGER,
135
+ payload_json TEXT NOT NULL,
136
+ PRIMARY KEY (task_id, execution_id)
137
+ )`,
138
+ `CREATE INDEX executions_check_idx ON executions (task_id, check_id, verification_cycle)`,
139
+
140
+ // Generic typed artifact store. Replaceable current snapshots use a
141
+ // fixed artifact_id; immutable history uses distinct IDs. Optional
142
+ // artifacts stay absent until created rather than being pre-seeded.
143
+ `CREATE TABLE task_artifacts (
144
+ task_id TEXT NOT NULL REFERENCES tasks (task_id) ON DELETE CASCADE,
145
+ kind TEXT NOT NULL,
146
+ artifact_id TEXT NOT NULL,
147
+ payload_json TEXT NOT NULL,
148
+ fingerprint TEXT NOT NULL,
149
+ PRIMARY KEY (task_id, kind, artifact_id)
150
+ )`,
151
+ `CREATE INDEX task_artifacts_kind_idx ON task_artifacts (kind)`,
152
+
153
+ // Session activation markers. Kept optional so a project can adopt the
154
+ // store before every session reader is migrated.
155
+ `CREATE TABLE sessions (
156
+ session_id TEXT PRIMARY KEY,
157
+ task_id TEXT REFERENCES tasks (task_id) ON DELETE SET NULL,
158
+ activation_json TEXT NOT NULL
159
+ )`,
160
+ `CREATE INDEX sessions_task_idx ON sessions (task_id)`,
161
+ ]),
162
+ }),
163
+ Object.freeze({
164
+ version: 2,
165
+ name: "artifact-byte-evidence",
166
+ statements: Object.freeze([
167
+ "ALTER TABLE task_artifacts ADD COLUMN source_json TEXT",
168
+ "ALTER TABLE task_artifacts ADD COLUMN byte_digest TEXT",
169
+ ]),
170
+ }),
171
+ Object.freeze({
172
+ version: 3,
173
+ name: "canonical-activation-selection",
174
+ statements: Object.freeze([
175
+ "ALTER TABLE storage_meta ADD COLUMN active_session_id TEXT REFERENCES sessions(session_id)",
176
+ ]),
177
+ }),
178
+ Object.freeze({
179
+ version: 4,
180
+ name: "execution-reference-lookup",
181
+ statements: Object.freeze([
182
+ "CREATE INDEX executions_reference_idx ON executions (execution_id, task_id)",
183
+ ]),
184
+ }),
185
+ Object.freeze({
186
+ version: 5,
187
+ name: "immutable-attachment-references",
188
+ statements: Object.freeze([
189
+ `CREATE TABLE attachment_references (
190
+ task_id TEXT NOT NULL REFERENCES tasks(task_id),
191
+ reference_id TEXT NOT NULL,
192
+ path TEXT NOT NULL,
193
+ size INTEGER NOT NULL CHECK(size >= 0),
194
+ sha256 TEXT NOT NULL CHECK(length(sha256) = 64),
195
+ PRIMARY KEY(task_id, reference_id)
196
+ )`,
197
+ "CREATE INDEX attachment_references_path_idx ON attachment_references(path)",
198
+ ]),
199
+ }),
200
+
201
+ ]);
202
+
203
+ /** All DDL required to build a current store from empty, in order. */
204
+ export const SCHEMA_STATEMENTS = Object.freeze(
205
+ SCHEMA_MIGRATIONS.flatMap((migration) => [...migration.statements]),
206
+ );
207
+
208
+ /** Additive performance indexes: their absence cannot change domain outcomes.
209
+ * Writable admission installs them without changing the marker-bound format.
210
+ */
211
+ export const OPTIONAL_STORAGE_INDEXES = Object.freeze([Object.freeze({
212
+ name: "events_reservation_proof_idx",
213
+ statement: `CREATE INDEX IF NOT EXISTS events_reservation_proof_idx ON events(task_id) WHERE ${EVENT_RESERVATION_PROOF_PREDICATE}`,
214
+ })]);
@@ -0,0 +1,84 @@
1
+ import { readFile, readdir } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { assertSafePath, fileExists } from "../core/filesystem.js";
4
+ import { LEGACY_TASK_ARTIFACT_PATHS as paths, taskArtifactPath } from "../core/task-paths.js";
5
+ import { assertTaskId, taskStorageKey } from "../core/task-identity.js";
6
+ import { createTaskDescriptor } from "../core/task-descriptor.js";
7
+ import { readPortableJsonArtifact, canonicalFingerprint } from "../core/artifacts.js";
8
+ import { validateMigrationSnapshot } from "../core/task-migration-validation.js";
9
+ import { readPortableEvents } from "../core/events.js";
10
+ import { getPackageRoot } from "../core/templates.js";
11
+ import { upsertTask, appendEvent, putArtifact, putExecution } from "./repository.js";
12
+
13
+ const artifacts = Object.freeze([
14
+ ["contract", "current-contract"], ["route", "routing-result"],
15
+ ["preflight", "preflight"], ["state", "work-state"],
16
+ ["continuity", "continuity"], ["receipt", "execution-receipt"],
17
+ ]);
18
+
19
+ /** Read-only singleton conversion input. No temporary writable namespace. */
20
+ export async function readSingletonImportSource(target, packageRoot = getPackageRoot()) {
21
+ const payloads = new Map();
22
+ let taskId = null;
23
+ for (const [kind, schema] of artifacts) {
24
+ if (!(await fileExists(await assertSafePath(target, paths[kind])))) continue;
25
+ const { value } = await readPortableJsonArtifact(target, paths[kind], schema, packageRoot);
26
+ if (value.taskId) {
27
+ if (taskId !== null && taskId !== value.taskId) throw Object.assign(new Error("Singleton artifact identities disagree"), { code: "E_TASK_MIGRATION_IDENTITY_MISMATCH" });
28
+ taskId = value.taskId;
29
+ }
30
+ payloads.set(kind, { payload: value, sourceText: await readFile(await assertSafePath(target, paths[kind]), "utf8") });
31
+ }
32
+ assertTaskId(taskId);
33
+ const snapshot = await validateMigrationSnapshot(target, { taskId, packageRoot, paths });
34
+ for (const [kind, value] of payloads) {
35
+ if (createHash("sha256").update(value.sourceText).digest("hex") !== snapshot.artifactFingerprints[kind]
36
+ || canonicalFingerprint(JSON.parse(value.sourceText)) !== canonicalFingerprint(value.payload)) {
37
+ throw Object.assign(new Error("Singleton artifact changed during conversion read"), { code: "E_STORAGE_MIGRATION_SOURCE_CHANGED" });
38
+ }
39
+ }
40
+ const events = await readPortableEvents(target, packageRoot, { eventsPath: paths.events });
41
+ const directories = {};
42
+ for (const [kind, schema] of [["gates", "gate"], ["executions", "execution"]]) {
43
+ directories[kind] = [];
44
+ const directory = await assertSafePath(target, paths[kind]);
45
+ if (!(await fileExists(directory))) continue;
46
+ for (const filename of (await readdir(directory)).sort()) {
47
+ const relative = `${paths[kind]}/${filename}`;
48
+ const { value } = await readPortableJsonArtifact(target, relative, schema, packageRoot);
49
+ const id = filename.slice(0, -5);
50
+ if ((kind === "gates" ? value.gate : value.executionId) !== id) throw Object.assign(new Error(`Singleton directory artifact identity disagrees with filename: ${relative}`), { code: "E_STORAGE_PAYLOAD_MISMATCH" });
51
+ directories[kind].push({ id, payload: value, sourceText: await readFile(await assertSafePath(target, relative), "utf8") });
52
+ }
53
+ }
54
+ const after = await validateMigrationSnapshot(target, { taskId, packageRoot, paths });
55
+ if (canonicalFingerprint(snapshot) !== canonicalFingerprint(after)) throw Object.assign(new Error("Singleton source changed during conversion read"), { code: "E_STORAGE_MIGRATION_SOURCE_CHANGED" });
56
+ return { taskId, taskKey: taskStorageKey(taskId), payloads, events, directories, snapshot };
57
+ }
58
+
59
+ /** Caller must own an unpublished SQLite import transaction. */
60
+ export function importSingletonSource(db, source) {
61
+ if (!db.isTransaction) throw Object.assign(new Error("Singleton import requires an enclosing transaction"), { code: "E_STORAGE_TRANSACTION_INVALID" });
62
+ const { taskId, taskKey, payloads, events, directories } = source;
63
+ if (db.prepare("SELECT task_id FROM tasks WHERE task_id = ? OR task_key = ?").get(taskId, taskKey)) throw Object.assign(new Error("Singleton conversion collides with an existing task namespace"), { code: "E_STORAGE_IMPORT_IDENTITY_COLLISION" });
64
+ const createdAt = payloads.get("state")?.payload.createdAt ?? payloads.get("contract")?.payload.createdAt ?? events[0]?.at ?? "1970-01-01T00:00:00.000Z";
65
+ const descriptor = createTaskDescriptor({ taskId, writeClaims: [], createdAt, updatedAt: createdAt });
66
+ upsertTask(db, { taskId, taskKey, descriptor, state: payloads.get("state")?.payload ?? null });
67
+ for (const event of events) appendEvent(db, { taskId, event });
68
+ for (const [kind, value] of payloads) {
69
+ if (kind !== "state") putArtifact(db, { taskId, kind, ...value });
70
+ }
71
+ for (const gate of directories.gates) putArtifact(db, { taskId, kind: "gate", artifactId: gate.id, payload: gate.payload, sourceText: gate.sourceText });
72
+ for (const execution of directories.executions) putExecution(db, { taskId, execution: execution.payload });
73
+ return { taskId, taskKey, events: events.length, stateFingerprint: payloads.has("state") ? canonicalFingerprint(payloads.get("state").payload) : null };
74
+ }
75
+
76
+ /** Exact source-to-portable-export mapping; unrelated roots keep their paths. */
77
+ export function singletonExportPath(taskId, relativePath) {
78
+ for (const [kind, sourcePath] of Object.entries(paths)) {
79
+ if (kind === "session") continue;
80
+ if (relativePath === sourcePath) return taskArtifactPath(taskId, kind);
81
+ if (["gates", "executions"].includes(kind) && relativePath.startsWith(`${sourcePath}/`)) return `${taskArtifactPath(taskId, kind)}/${relativePath.slice(sourcePath.length + 1)}`;
82
+ }
83
+ return relativePath;
84
+ }
@@ -0,0 +1,16 @@
1
+ /** Preserve retained identities/history while permitting valid later mutations. */
2
+ export function assertSnapshotContinuity(original, current, invalid) {
3
+ const task = current.prepare("SELECT task_key FROM tasks WHERE task_id = ?");
4
+ for (const row of original.prepare("SELECT task_id, task_key FROM tasks").iterate()) {
5
+ if (task.get(row.task_id)?.task_key !== row.task_key) throw invalid("A retained task identity was lost or substituted");
6
+ }
7
+ const event = current.prepare("SELECT event_json FROM events WHERE task_id = ? AND seq = ?");
8
+ for (const row of original.prepare("SELECT task_id, seq, event_json FROM events ORDER BY task_id, seq").iterate()) {
9
+ if (event.get(row.task_id, row.seq)?.event_json !== row.event_json) throw invalid("Retained canonical event history was lost or changed");
10
+ }
11
+ const binding = current.prepare("SELECT path, size, sha256 FROM attachment_references WHERE task_id = ? AND reference_id = ?");
12
+ for (const row of original.prepare("SELECT * FROM attachment_references ORDER BY task_id, reference_id").iterate()) {
13
+ const retained = binding.get(row.task_id, row.reference_id);
14
+ if (!retained || retained.path !== row.path || retained.size !== row.size || retained.sha256 !== row.sha256) throw invalid("A retained attachment binding was lost or changed");
15
+ }
16
+ }
@@ -0,0 +1,36 @@
1
+ import { loadStorageDriver } from "./runtime.js";
2
+ import { mkdtemp, rm } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+
6
+ const ownedSnapshots = new WeakSet();
7
+ /** Only copies admitted here are immutable; callers cannot register live handles. */
8
+ export function isOwnedStorageSnapshot(db) { return ownedSnapshots.has(db); }
9
+ // Copy a committed snapshot in one native backup step. Smaller batches restart
10
+ // after writes from other connections and can starve readers under sustained work.
11
+ // This is a page-step limit, not an allocation or a JavaScript buffer size.
12
+ const SNAPSHOT_BACKUP_BATCH_PAGES = 0x7fffffff;
13
+
14
+ /** Isolate portable reads from live writers without holding a reader during I/O. */
15
+ export async function withStorageSnapshot(db, readSnapshot) {
16
+ const { backup, DatabaseSync } = loadStorageDriver();
17
+ if (db.isTransaction) {
18
+ const error = new Error("Export requires a committed storage snapshot");
19
+ error.code = "E_STORAGE_SNAPSHOT_TRANSACTION";
20
+ throw error;
21
+ }
22
+ // Only live read-only copies created here can be reused; callers cannot opt in.
23
+ if (ownedSnapshots.has(db)) return readSnapshot(db);
24
+ const directory = await mkdtemp(path.join(os.tmpdir(), "forgeloop-snapshot-"));
25
+ let snapshot;
26
+ try {
27
+ const filename = path.join(directory, "state.sqlite");
28
+ await backup(db, filename, { rate: SNAPSHOT_BACKUP_BATCH_PAGES });
29
+ snapshot = new DatabaseSync(filename, { readOnly: true, enableForeignKeyConstraints: true });
30
+ ownedSnapshots.add(snapshot);
31
+ return await readSnapshot(snapshot);
32
+ } finally {
33
+ if (snapshot) ownedSnapshots.delete(snapshot);
34
+ try { snapshot?.close(); } finally { await rm(directory, { recursive: true, force: true }); }
35
+ }
36
+ }
@@ -0,0 +1,79 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, open } from "node:fs/promises";
3
+ import { assertSafePath, assertRegularProjectFileIdentity, isPathWithin, realpathWithTransientWindowsRetry, writeFileAtomic } from "../core/filesystem.js";
4
+ import { assertOwnedStorageMaintenance } from "./maintenance.js";
5
+
6
+ const MARKER = ".forgeloop/storage-version.json";
7
+ function invalid(message) { return Object.assign(new Error(message), { code: "E_STORAGE_VERSION_MARKER_INVALID" }); }
8
+
9
+ export function validateStorageVersionMarker(value) {
10
+ if (!value || value.schemaVersion !== 1 || value.storageFormat !== "sqlite" || value.storageVersion !== 1
11
+ || !Number.isSafeInteger(value.databaseSchemaVersion) || value.databaseSchemaVersion < 1
12
+ || !["CUTOVER_PENDING", "ACTIVE"].includes(value.phase)
13
+ || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(value.operationId ?? "")
14
+ || !/^[a-f0-9]{64}$/.test(value.sourceInventoryFingerprint ?? "")) throw invalid("Storage marker is incomplete or unsupported");
15
+ return value;
16
+ }
17
+
18
+ export async function readStorageVersionMarker(target) {
19
+ const filename = await assertSafePath(target, MARKER);
20
+ const root = await realpathWithTransientWindowsRetry(target);
21
+ let bytes;
22
+ try {
23
+ const observed = await lstat(filename, { bigint: true });
24
+ if (!observed.isFile()) throw invalid("Storage marker must be a regular file");
25
+ if (!isPathWithin(root, await realpathWithTransientWindowsRetry(filename))) throw invalid("Storage marker resolves outside the project");
26
+ let file;
27
+ try { file = await open(filename, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0)); }
28
+ catch (error) { if (error.code === "ELOOP") throw invalid("Storage marker must be a regular file"); throw error; }
29
+ try {
30
+ const actual = await assertRegularProjectFileIdentity(file, filename, observed, root,
31
+ () => invalid("Storage marker must retain its admitted file identity"));
32
+ if (actual.size > 65536n) throw invalid("Storage marker exceeds its byte limit");
33
+ const buffer = Buffer.alloc(65537);
34
+ let count = 0;
35
+ while (count < buffer.length) {
36
+ const { bytesRead } = await file.read(buffer, count, buffer.length - count, count);
37
+ if (bytesRead === 0) break;
38
+ count += bytesRead;
39
+ }
40
+ bytes = buffer.subarray(0, count);
41
+ } finally { await file.close(); }
42
+ } catch (error) { if (error.code === "ENOENT") return null; throw error; }
43
+ if (bytes.length > 65536 || !Buffer.from(bytes.toString("utf8")).equals(bytes)) throw invalid("Storage marker encoding or size is invalid");
44
+ let value;
45
+ try { value = JSON.parse(bytes.toString("utf8")); } catch { throw invalid("Storage marker is not valid JSON"); }
46
+ return validateStorageVersionMarker(value);
47
+ }
48
+
49
+ /** Exclusive pending marker; activation must be a separately validated step. */
50
+ export async function writePendingStorageVersionMarker(target, marker) {
51
+ await assertOwnedStorageMaintenance(target);
52
+ const filename = await assertSafePath(target, MARKER);
53
+ const expected = validateStorageVersionMarker({ schemaVersion: 1, storageFormat: "sqlite", storageVersion: 1, phase: "CUTOVER_PENDING",
54
+ databaseSchemaVersion: marker.databaseSchemaVersion, operationId: marker.operationId, sourceInventoryFingerprint: marker.sourceInventoryFingerprint });
55
+ const existing = await readStorageVersionMarker(target);
56
+ if (existing) {
57
+ if (JSON.stringify(existing) !== JSON.stringify(expected)) throw invalid("Existing storage marker differs from this cutover operation");
58
+ return existing;
59
+ }
60
+ const file = await open(filename, "wx", 0o600);
61
+ try { await file.writeFile(`${JSON.stringify(expected)}\n`); await file.sync(); } finally { await file.close(); }
62
+ try {
63
+ const directory = await open(await assertSafePath(target, ".forgeloop"), "r");
64
+ try { await directory.sync(); } finally { await directory.close(); }
65
+ } catch (error) { if (!["EINVAL", "EPERM", "EISDIR", "ENOTSUP", "UNKNOWN"].includes(error.code)) throw error; }
66
+ return readStorageVersionMarker(target);
67
+ }
68
+
69
+ /** Called only after the cutover service validates its published database. */
70
+ export async function activateStorageVersionMarker(target, expected) {
71
+ await assertOwnedStorageMaintenance(target);
72
+ const current = await readStorageVersionMarker(target);
73
+ if (!current || current.phase !== "CUTOVER_PENDING" || current.operationId !== expected.operationId
74
+ || current.sourceInventoryFingerprint !== expected.sourceInventoryFingerprint
75
+ || current.databaseSchemaVersion !== expected.databaseSchemaVersion) throw invalid("Pending marker does not belong to this validated activation");
76
+ const active = { ...current, phase: "ACTIVE" };
77
+ await writeFileAtomic(await assertSafePath(target, MARKER), `${JSON.stringify(active)}\n`);
78
+ return active;
79
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Store-backed operational guards for the SQLite integration slice.
3
+ *
4
+ * Claim and reservation projections read the same operational records that
5
+ * native mutations use, so authority decisions are derived from one state
6
+ * history rather than a second compatibility adapter.
7
+ */
8
+ import { buildTaskClaimEvidence, classifyTaskClaimState } from "../core/task-claim-state.js";
9
+ import { resolveEffectiveContractBootstrapRepairAnchor } from "../core/contract-bootstrap-recovery.js";
10
+ import { validateLedgerEvents } from "../core/events.js";
11
+ import { listArtifacts } from "./repository.js";
12
+
13
+ import { withLedgerEventSnapshot } from "./ledger-event-snapshot.js";
14
+ import { EVENT_RESERVATION_PROOF_PREDICATE } from "./schema.js";
15
+
16
+ const ERROR_CODES = Object.freeze({
17
+ TASK_NOT_FOUND: "E_TASK_NOT_FOUND",
18
+ });
19
+
20
+ function guardError(code, message, extra = {}) {
21
+ const error = new Error(message);
22
+ error.code = code;
23
+ Object.assign(error, extra);
24
+ return error;
25
+ }
26
+
27
+ function readTask(db, taskId) {
28
+ const row = db
29
+ .prepare("SELECT task_id, task_key, phase, revision, created_at, updated_at, descriptor_json, state_json FROM tasks WHERE task_id = ?")
30
+ .get(taskId);
31
+ if (!row) return null;
32
+ return {
33
+ taskId: row.task_id,
34
+ taskKey: row.task_key,
35
+ phase: row.phase,
36
+ revision: row.revision,
37
+ createdAt: row.created_at,
38
+ updatedAt: row.updated_at,
39
+ descriptor: JSON.parse(row.descriptor_json),
40
+ state: row.state_json === null ? null : JSON.parse(row.state_json),
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Classify operational claim state from store records.
46
+ *
47
+ * Mirrors the filesystem classification order: completion release, then active
48
+ * recovery release, then active. Released claims are never treated as mutation
49
+ * authority, and a task with no recorded claim evidence is inconsistent rather
50
+ * than automatically permitted.
51
+ */
52
+ export function resolveStoreClaimState(db, taskId) {
53
+ return withLedgerEventSnapshot(db, taskId, (events) => classifyStoreClaimSnapshot(db, taskId, events));
54
+ }
55
+
56
+ function classifyStoreClaimSnapshot(db, taskId, events) {
57
+ const task = readTask(db, taskId);
58
+ if (!task) {
59
+ throw guardError(ERROR_CODES.TASK_NOT_FOUND, `Task "${taskId}" not found in project`, { taskId });
60
+ }
61
+
62
+ const ledger = validateLedgerEvents(events);
63
+ const recovery = listArtifacts(db, taskId, "recovery").at(-1)?.payload ?? null;
64
+ const repairAnchor = resolveEffectiveContractBootstrapRepairAnchor(events);
65
+ let repairArtifacts = null;
66
+ if (repairAnchor) {
67
+ const contract = listArtifacts(db, taskId, "contract").at(-1);
68
+ const route = listArtifacts(db, taskId, "route").at(-1);
69
+ const details = repairAnchor.details;
70
+ repairArtifacts = {
71
+ contract: contract ? { value: contract.payload, fingerprint: contract.fingerprint } : null,
72
+ route: route ? { value: route.payload, fingerprint: route.fingerprint } : null,
73
+ contractError: !contract ? "contract artifact is missing after a recorded repair"
74
+ : contract.fingerprint !== details.contractFingerprint ? "contract fingerprint does not match marker contract fingerprint"
75
+ : task.state?.contractFingerprint !== details.contractFingerprint ? "work-state contract fingerprint does not match marker contract fingerprint" : null,
76
+ routeError: route?.payload?.contractFingerprint !== undefined && route.payload.contractFingerprint !== details.contractFingerprint
77
+ ? "route artifact contract binding does not match repair anchor contract fingerprint" : null,
78
+ };
79
+ }
80
+ const initialErrors = [];
81
+ if (task.state && (task.phase !== task.state.phase || task.revision !== (task.state.revision ?? 0)
82
+ || task.state.taskId !== taskId) || task.descriptor.taskId !== taskId || task.descriptor.taskKey !== task.taskKey) {
83
+ initialErrors.push({ code: "E_TASK_CLAIM_OWNERSHIP_INCONSISTENT", causeCode: "E_STORAGE_PAYLOAD_MISMATCH", message: "Stored identity/phase/revision columns disagree with canonical task payloads" });
84
+ }
85
+ return classifyTaskClaimState(buildTaskClaimEvidence({ taskId, descriptor: task.descriptor, state: task.state, recovery, ledger, repairArtifacts, initialErrors }));
86
+ }
87
+
88
+ /** Reservation projection only: ACTIVE never grants mutation authority. */
89
+ export function resolveStoreReservationState(db, taskId) {
90
+ const task = readTask(db, taskId);
91
+ if (!task) throw guardError(ERROR_CODES.TASK_NOT_FOUND, `Task "${taskId}" not found in project`, { taskId });
92
+ const recovery = db.prepare("SELECT 1 FROM task_artifacts WHERE task_id = ? AND kind = 'recovery' LIMIT 1").get(taskId);
93
+ if (task.state?.phase !== "COMPLETE" && !recovery) {
94
+ // Full classification cannot release a non-completed task without a
95
+ // recovery artifact. Invalid history also retains claims. Preserve the
96
+ // previous decoder/error path for malformed/non-object historical rows
97
+ // and repair histories that may inspect contract/route artifacts. Read the
98
+ // canonical payload, not the editable event_type index. This bounded SQL
99
+ // scan is not a ledger or ownership validation result.
100
+ const requiresFullProof = db.prepare(`SELECT 1 FROM events WHERE task_id = ?
101
+ AND ${EVENT_RESERVATION_PROOF_PREDICATE} LIMIT 1`).get(taskId);
102
+ if (!requiresFullProof) return "ACTIVE";
103
+ }
104
+ const projection = resolveStoreClaimState(db, taskId);
105
+ return projection.valid && projection.effectiveWriteClaims.length === 0 && projection.claimState !== "ACTIVE"
106
+ ? "RELEASED" : "ACTIVE";
107
+ }
@@ -0,0 +1,42 @@
1
+ import { statfsSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ // Linux UAPI magic.h identifies these network/shared filesystems. This is a
5
+ // deny-list of known unsupported topology, not certification of every other FS.
6
+ const NETWORK_TYPES = new Map([
7
+ [0x6969n, "NFS"], [0x517bn, "SMB"], [0xff534d42n, "CIFS"], [0xfe534d42n, "SMB2"],
8
+ [0x5346414fn, "AFS"], [0x6b414653n, "AFS"], [0x73757245n, "Coda"],
9
+ [0x00c36400n, "Ceph"], [0x01021997n, "9P"],
10
+ ]);
11
+
12
+ function unsupported(kind) {
13
+ return Object.assign(new Error(`SQLite WAL storage requires a supported local filesystem; ${kind} topology is unsupported`), { code: "E_STORAGE_TOPOLOGY_UNSUPPORTED" });
14
+ }
15
+
16
+ export function assertStorageTopologyName(databasePath, platform, filesystemType = null) {
17
+ const filename = String(databasePath).replaceAll("\\", "/");
18
+ if (platform === "win32" && (/^\/\/[^?./]/.test(filename) || /^\/\/[?.]\/UNC\//i.test(filename))) throw unsupported("UNC network share");
19
+ if (platform === "linux" && filesystemType !== null) {
20
+ const kind = NETWORK_TYPES.get(BigInt.asUintN(32, BigInt(filesystemType)));
21
+ if (kind) throw unsupported(kind);
22
+ }
23
+ }
24
+
25
+ /** Inspect an existing ancestor before SQLite creates a database or sidecars. */
26
+ export function assertStorageTopology(databasePath) {
27
+ assertStorageTopologyName(databasePath, process.platform);
28
+ if (databasePath === ":memory:" || process.platform !== "linux") return;
29
+ let candidate = path.resolve(databasePath);
30
+ for (;;) {
31
+ try {
32
+ const filesystem = statfsSync(candidate, { bigint: true });
33
+ assertStorageTopologyName(databasePath, process.platform, filesystem.type);
34
+ return;
35
+ } catch (error) {
36
+ if (error.code !== "ENOENT" && error.code !== "ENOTDIR") throw error;
37
+ const parent = path.dirname(candidate);
38
+ if (parent === candidate) throw error;
39
+ candidate = parent;
40
+ }
41
+ }
42
+ }