@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,376 @@
1
+ import { artifactByteDigest } from "./artifact-bytes.js";
2
+ import { assertStorageTopology } from "./topology.js";
3
+ import { validateAttachmentReference } from "./attachment-binding.js";
4
+ import { canonicalFingerprint } from "../core/artifacts.js";
5
+ import { eventHash, validateKnownEventDetails } from "../core/events.js";
6
+ import { loadStorageDriver } from "./runtime.js";
7
+ import { lstatSync, realpathSync } from "node:fs";
8
+ import path from "node:path";
9
+ import { isPathWithin } from "../core/filesystem.js";
10
+
11
+ import { PROTOCOL_VERSION } from "../core/protocol.js";
12
+ import { OPTIONAL_STORAGE_INDEXES, SCHEMA_MIGRATIONS, STORAGE_FORMAT, STORAGE_FORMAT_VERSION, STORAGE_SCHEMA_VERSION } from "./schema.js";
13
+
14
+ /**
15
+ * Canonical on-disk location of the authoritative operational store for one
16
+ * project state root. One database per state root keeps task/claim/event
17
+ * mutations inside a single transaction without cross-database coordination.
18
+ */
19
+ export { STORAGE_RELATIVE_PATH } from "./schema.js";
20
+
21
+ /**
22
+ * Durability and concurrency defaults.
23
+ *
24
+ * `synchronous=FULL` matches the durability the filesystem transaction
25
+ * implementation provided via fsync-before-rename, so migration does not
26
+ * silently trade crash safety for speed. `busy_timeout` mirrors the 5s
27
+ * transaction wait the previous write-lock loop allowed.
28
+ */
29
+ export const BUSY_TIMEOUT_MS = 5_000;
30
+
31
+ /**
32
+ * Pragma settings that must be confirmed after they are applied.
33
+ *
34
+ * `column` differs from `pragma` for busy_timeout: SQLite reports that value
35
+ * under the key `timeout`, so asserting on the pragma name would silently read
36
+ * `undefined` and reject every store.
37
+ */
38
+ function pragmaAssertions(busyTimeoutMs) {
39
+ return [
40
+ { pragma: "journal_mode", column: "journal_mode", expected: "wal" },
41
+ { pragma: "foreign_keys", column: "foreign_keys", expected: 1 },
42
+ { pragma: "synchronous", column: "synchronous", expected: 2 },
43
+ { pragma: "busy_timeout", column: "timeout", expected: busyTimeoutMs },
44
+ ];
45
+ }
46
+
47
+ /** Stable error codes surfaced to callers and mapped by the CLI layer. */
48
+ export const STORAGE_ERROR_CODES = Object.freeze({
49
+ CORRUPT: "E_STORAGE_CORRUPT",
50
+ READ_ONLY: "E_STORAGE_READ_ONLY",
51
+ BUSY: "E_STORAGE_BUSY",
52
+ FULL: "E_STORAGE_FULL",
53
+ INTERRUPTED: "E_STORAGE_INTERRUPTED",
54
+ UNSUPPORTED_VERSION: "E_STORAGE_VERSION_UNSUPPORTED",
55
+ UNSUPPORTED_RUNTIME: "E_STORAGE_UNSUPPORTED_RUNTIME",
56
+ });
57
+
58
+ /**
59
+ * SQLite primary result codes that map to an explicitly recoverable condition.
60
+ *
61
+ * `node:sqlite` reports `errcode` as the numeric SQLite result code, so the
62
+ * mapping is keyed by number. A previous implementation keyed on symbolic
63
+ * strings and therefore never matched, letting a locked database surface as an
64
+ * opaque `ERR_SQLITE_ERROR` instead of a documented busy condition.
65
+ */
66
+ const SQLITE_RESULT_CODE_MAP = Object.freeze({
67
+ 5: STORAGE_ERROR_CODES.BUSY, // SQLITE_BUSY
68
+ 6: STORAGE_ERROR_CODES.BUSY, // SQLITE_LOCKED
69
+ 8: STORAGE_ERROR_CODES.READ_ONLY, // SQLITE_READONLY
70
+ 11: STORAGE_ERROR_CODES.CORRUPT, // SQLITE_CORRUPT
71
+ 26: STORAGE_ERROR_CODES.CORRUPT, // SQLITE_NOTADB (damaged database header)
72
+ 13: STORAGE_ERROR_CODES.FULL, // SQLITE_FULL
73
+ 14: STORAGE_ERROR_CODES.INTERRUPTED, // SQLITE_CANTOPEN
74
+ 10: STORAGE_ERROR_CODES.INTERRUPTED, // SQLITE_IOERR
75
+ 9: STORAGE_ERROR_CODES.INTERRUPTED, // SQLITE_INTERRUPT
76
+ });
77
+
78
+ /** Extended result codes that carry the same primary code in their low bits. */
79
+ function primaryResultCode(errcode) {
80
+ if (typeof errcode === "number") return errcode & 0xff;
81
+ if (typeof errcode === "string" && /^\d+$/.test(errcode)) return Number(errcode) & 0xff;
82
+ return null;
83
+ }
84
+
85
+ /**
86
+ * Detect whether this runtime can host the store at all.
87
+ */
88
+ export function assertStorageRuntime() {
89
+ return loadStorageDriver();
90
+ }
91
+
92
+ /**
93
+ * Translate a raw `node:sqlite` failure into a stable, explicitly recoverable
94
+ * storage error. Anything unrecognized is rethrown untouched so real defects are
95
+ * never masked as environment conditions.
96
+ */
97
+ export function translateStorageError(error) {
98
+ const primary = primaryResultCode(error?.errcode);
99
+ const mapped = primary === null ? null : SQLITE_RESULT_CODE_MAP[primary];
100
+ if (!mapped) throw error;
101
+ const translated = new Error(
102
+ `SQLite storage operation failed (SQLITE_${primary}, ${error.errstr ?? error.message}): ${error.message}`,
103
+ );
104
+ translated.code = mapped;
105
+ translated.sqliteResultCode = primary;
106
+ translated.cause = error;
107
+ throw translated;
108
+ }
109
+ /**
110
+ * Apply the connection defaults. Extension loading is never enabled and all
111
+ * callers bind SQL parameters, so user-authored text can never be interpreted
112
+ * as schema or executable SQL.
113
+ */
114
+ function applyConnectionSettings(db, busyTimeoutMs) {
115
+ db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
116
+ db.exec("PRAGMA journal_mode = WAL");
117
+ db.exec("PRAGMA foreign_keys = ON");
118
+ db.exec("PRAGMA synchronous = FULL");
119
+ db.exec(`PRAGMA busy_timeout = ${busyTimeoutMs}`);
120
+ }
121
+
122
+ /**
123
+ * Confirm the effective settings rather than trusting that the statements
124
+ * succeeded. A filesystem that silently refuses WAL must fail loudly here
125
+ * instead of degrading durability invisibly.
126
+ */
127
+ function assertConnectionSettings(db, busyTimeoutMs) {
128
+ for (const assertion of pragmaAssertions(busyTimeoutMs)) {
129
+ const row = db.prepare(`PRAGMA ${assertion.pragma}`).get();
130
+ const actual = row?.[assertion.column];
131
+ const normalized = typeof actual === "string" ? actual.toLowerCase() : actual;
132
+ if (normalized !== assertion.expected) {
133
+ const error = new Error(
134
+ `SQLite refused required pragma ${assertion.pragma} (expected ${assertion.expected}, got ${actual})`,
135
+ );
136
+ error.code = STORAGE_ERROR_CODES.UNSUPPORTED_RUNTIME;
137
+ throw error;
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Create every table that is not yet present, then stamp the schema version.
144
+ *
145
+ * The whole migration runs inside one immediate transaction so that two
146
+ * processes opening the same fresh database cannot interleave their
147
+ * check-then-create sequence. Without that, a concurrent opener could observe
148
+ * a partially created schema and fail with a busy or "table already exists"
149
+ * error before it ever reached application code.
150
+ */
151
+ function assertCompatibleMetadata(db, { readOnly = false } = {}) {
152
+ const meta = db.prepare("SELECT * FROM storage_meta WHERE id = 1").get();
153
+ if (!meta || !Number.isInteger(meta.schema_version) || meta.schema_version < 1
154
+ || meta.schema_version > STORAGE_SCHEMA_VERSION || meta.storage_version !== STORAGE_FORMAT_VERSION
155
+ || meta.storage_format !== STORAGE_FORMAT || meta.protocol_version !== PROTOCOL_VERSION) {
156
+ const error = new Error("Storage metadata is missing or incompatible with this runtime");
157
+ error.code = STORAGE_ERROR_CODES.UNSUPPORTED_VERSION;
158
+ throw error;
159
+ }
160
+ if (readOnly && meta.schema_version !== STORAGE_SCHEMA_VERSION) {
161
+ const error = new Error("Storage schema requires an explicit writable upgrade before read-only access");
162
+ error.code = "E_STORAGE_MIGRATION_REQUIRED";
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ export function assertStorageMetadataCurrent(db) {
168
+ assertCompatibleMetadata(db, { readOnly: true });
169
+ }
170
+
171
+ function applyMigrations(db, { now = new Date().toISOString() } = {}) {
172
+ const hasMetadata = db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'storage_meta'").get();
173
+ if (hasMetadata) {
174
+ assertCompatibleMetadata(db);
175
+ if (db.prepare("SELECT schema_version FROM storage_meta WHERE id = 1").get().schema_version === STORAGE_SCHEMA_VERSION) return;
176
+ }
177
+ db.exec("BEGIN IMMEDIATE");
178
+ try {
179
+ const existing = new Set(
180
+ db.prepare("SELECT name FROM sqlite_master WHERE type = 'table'").all().map((row) => row.name),
181
+ );
182
+ if (existing.has("storage_meta")) assertCompatibleMetadata(db);
183
+ for (const migration of SCHEMA_MIGRATIONS) {
184
+ if (existing.has("storage_meta")) {
185
+ const row = db.prepare("SELECT schema_version FROM storage_meta WHERE id = 1").get();
186
+ if (row && Number(row.schema_version) >= migration.version) continue;
187
+ }
188
+ for (const statement of migration.statements) {
189
+ if (statement.startsWith("CREATE TABLE ")) {
190
+ const table = statement.slice("CREATE TABLE ".length).split(/[\s(]/)[0];
191
+ if (existing.has(table)) continue;
192
+ existing.add(table);
193
+ }
194
+ db.exec(statement);
195
+ }
196
+ }
197
+ db.prepare(
198
+ `INSERT INTO storage_meta (id, storage_version, schema_version, protocol_version, storage_format, created_at, updated_at)
199
+ VALUES (1, ?, ?, ?, ?, ?, ?)
200
+ ON CONFLICT (id) DO UPDATE SET schema_version = excluded.schema_version, updated_at = excluded.updated_at`,
201
+ ).run(STORAGE_FORMAT_VERSION, STORAGE_SCHEMA_VERSION, PROTOCOL_VERSION ?? null, STORAGE_FORMAT, now, now);
202
+ db.exec("COMMIT");
203
+ } catch (error) {
204
+ try { db.exec("ROLLBACK"); } catch { /* preserve the migration failure */ }
205
+ throw error;
206
+ }
207
+ }
208
+
209
+ function ensureOptionalIndexes(db) {
210
+ const exists = db.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'index' AND name = ?");
211
+ for (const index of OPTIONAL_STORAGE_INDEXES) {
212
+ if (!exists.get(index.name)) db.exec(index.statement);
213
+ }
214
+ }
215
+
216
+ /** Check SQLite's opened filename before configuration or reuse of a project handle. */
217
+ export function assertProjectDatabaseAdmission(db, admission) {
218
+ if (!admission) return;
219
+ const expected = path.join(admission.root, ".forgeloop/state.sqlite");
220
+ const changed = () => Object.assign(new Error("Project database path changed while opening or reusing its connection"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
221
+ try {
222
+ const root = lstatSync(admission.target, { bigint: true });
223
+ if (!root.isDirectory() || root.isSymbolicLink()) throw changed();
224
+ if (admission.rootIdentity && ["dev", "ino", "mtimeNs", "ctimeNs"].some(key => root[key] !== admission.rootIdentity[key])) throw changed();
225
+ const opened = realpathSync.native(db.location());
226
+ const resolvedExpected = realpathSync.native(expected);
227
+ const current = lstatSync(expected, { bigint: true });
228
+ if (!isPathWithin(resolvedExpected, opened) || !isPathWithin(opened, resolvedExpected)
229
+ || !current.isFile() || current.isSymbolicLink()
230
+ || current.dev !== admission.dev || current.ino !== admission.ino) throw changed();
231
+ for (const suffix of ["-wal", "-shm"]) {
232
+ try {
233
+ const sidecar = lstatSync(`${expected}${suffix}`);
234
+ if (!sidecar.isFile() || sidecar.isSymbolicLink()) throw changed();
235
+ } catch (error) { if (error.code !== "ENOENT") throw error; }
236
+ }
237
+ } catch (error) {
238
+ if (["ENOENT", "ENOTDIR", "ELOOP"].includes(error.code)) throw changed();
239
+ throw error;
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Open the operational store, apply durability defaults, and ensure the schema
245
+ * is current. The caller owns the returned handle and must `close()` it.
246
+ */
247
+ export function openStorageDatabase(databasePath, {
248
+ readOnly = false,
249
+ allowSchemaUpgrade = true,
250
+ allowOptionalIndexCreation = allowSchemaUpgrade,
251
+ now = new Date().toISOString(),
252
+ busyTimeoutMs = BUSY_TIMEOUT_MS,
253
+ projectAdmission = null,
254
+ } = {}) {
255
+ const { DatabaseSync } = assertStorageRuntime();
256
+ if (!Number.isInteger(busyTimeoutMs) || busyTimeoutMs < 0 || busyTimeoutMs > 2_147_483_647) {
257
+ const error = new Error("busyTimeoutMs must be a non-negative SQLite timeout integer");
258
+ error.code = "E_STORAGE_CONFIGURATION_INVALID";
259
+ throw error;
260
+ }
261
+ assertStorageTopology(databasePath);
262
+ let db;
263
+ try {
264
+ db = new DatabaseSync(databasePath, { readOnly, open: true, enableForeignKeyConstraints: true });
265
+ assertProjectDatabaseAdmission(db, projectAdmission);
266
+ applyConnectionSettings(db, busyTimeoutMs);
267
+ assertConnectionSettings(db, busyTimeoutMs);
268
+ if (!readOnly && allowSchemaUpgrade) applyMigrations(db, { now });
269
+ else assertCompatibleMetadata(db, { readOnly: true });
270
+ // Optional indexes preserve the marker-bound schema and exact fallback query.
271
+ // Inspect first: opening an indexed store needs no DDL writer reservation.
272
+ if (!readOnly && allowOptionalIndexCreation) ensureOptionalIndexes(db);
273
+ } catch (error) {
274
+ if (db) {
275
+ try { db.close(); } catch { /* preserve the original failure */ }
276
+ }
277
+ if (typeof error?.code === "string" && error.code.startsWith("E_STORAGE_")) throw error;
278
+ return translateStorageError(error);
279
+ }
280
+ return db;
281
+ }
282
+
283
+ /** Read the store metadata singleton, or `null` for an uninitialized database. */
284
+ export function readStorageMeta(db) {
285
+ const table = db
286
+ .prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'storage_meta'")
287
+ .get();
288
+ if (!table) return null;
289
+ return db.prepare("SELECT * FROM storage_meta WHERE id = 1").get() ?? null;
290
+ }
291
+
292
+ function validateStoredEvent(row, sequence, previousHash) {
293
+ const event = JSON.parse(row.event_json);
294
+ if (event.taskId !== row.task_id || event.seq !== row.seq || event.event !== row.event_type
295
+ || event.at !== row.at || event.hash !== row.hash || event.previousHash !== row.previous_hash
296
+ || event.seq !== sequence || event.previousHash !== previousHash || event.hash !== eventHash(event)) {
297
+ throw Object.assign(new Error("Stored ledger identity, ordering or hash mismatch"), { code: "E_LEDGER_HASH_INVALID" });
298
+ }
299
+ validateKnownEventDetails(event);
300
+ }
301
+
302
+ function collectNativeFindings(statement, project = row => row) {
303
+ const findings = [];
304
+ let total = 0;
305
+ for (const row of statement.iterate()) {
306
+ total += 1;
307
+ if (findings.length < 100) findings.push(project(row));
308
+ }
309
+ return { findings, total };
310
+ }
311
+
312
+ function nativeIntegrityFindings(db) {
313
+ const integrity = collectNativeFindings(db.prepare("PRAGMA integrity_check"), row => row.integrity_check);
314
+ const foreignKeys = collectNativeFindings(db.prepare("PRAGMA foreign_key_check"));
315
+ const truncatedFindings = {};
316
+ for (const [name, result] of Object.entries({ integrity, foreignKeys })) {
317
+ if (result.total > result.findings.length) truncatedFindings[name] = { total: result.total, reported: result.findings.length };
318
+ }
319
+ return {
320
+ integrity: integrity.findings,
321
+ foreignKeys: foreignKeys.findings,
322
+ ...(Object.keys(truncatedFindings).length ? { truncatedFindings } : {}),
323
+ };
324
+ }
325
+
326
+ /**
327
+ * Run the integrity and foreign-key checks the plan requires before a cutover
328
+ * is published. Scans all rows but retains at most 100 native findings per
329
+ * check, reporting truncation instead of silently dropping diagnostics.
330
+ */
331
+ export function checkStorageIntegrity(db) {
332
+ const native = nativeIntegrityFindings(db);
333
+ const { integrity, foreignKeys } = native;
334
+ const artifactErrors = [];
335
+ const eventErrors = [];
336
+ const attachmentErrors = [];
337
+ let taskId = null;
338
+ let sequence = 0;
339
+ let previousHash = null;
340
+ for (const row of db.prepare("SELECT * FROM events ORDER BY task_id, seq").iterate()) {
341
+ if (row.task_id !== taskId) { taskId = row.task_id; sequence = 0; previousHash = null; }
342
+ sequence += 1;
343
+ try {
344
+ validateStoredEvent(row, sequence, previousHash);
345
+ } catch (error) {
346
+ // Findings are bounded even when every event of a large ledger is bad.
347
+ if (eventErrors.length < 100) eventErrors.push({ taskId, sequence: row.seq, code: error.code ?? "E_EVENT_INVALID" });
348
+ }
349
+ previousHash = row.hash;
350
+ }
351
+ for (const row of db.prepare("SELECT task_id, kind, artifact_id, payload_json, fingerprint, source_json, byte_digest FROM task_artifacts").iterate()) {
352
+ try {
353
+ if (row.fingerprint !== canonicalFingerprint(JSON.parse(row.payload_json))) throw Object.assign(new Error("Artifact fingerprint mismatch"), { code: "E_STORAGE_ARTIFACT_INVALID" });
354
+ if (row.source_json === null && row.byte_digest === null) continue;
355
+ artifactByteDigest({ sourceText: row.source_json, byteDigest: row.byte_digest, payload: JSON.parse(row.payload_json) });
356
+ } catch (error) {
357
+ if (artifactErrors.length < 100) artifactErrors.push({ taskId: row.task_id, kind: row.kind, artifactId: row.artifact_id, code: error.code ?? "E_STORAGE_ARTIFACT_INVALID" });
358
+ }
359
+ }
360
+ for (const row of db.prepare("SELECT * FROM attachment_references ORDER BY task_id, reference_id").iterate()) {
361
+ try {
362
+ validateAttachmentReference({ taskId: row.task_id, referenceId: row.reference_id, path: row.path, size: row.size, sha256: row.sha256 });
363
+ } catch (error) {
364
+ if (attachmentErrors.length < 100) attachmentErrors.push({ taskId: row.task_id, referenceId: row.reference_id, code: error.code ?? "E_STORAGE_ATTACHMENT_INVALID" });
365
+ }
366
+ }
367
+ return {
368
+ artifactErrors,
369
+ eventErrors,
370
+ attachmentErrors,
371
+ ok: attachmentErrors.length === 0 && eventErrors.length === 0 && artifactErrors.length === 0 && integrity.length === 1 && integrity[0] === "ok" && foreignKeys.length === 0,
372
+ integrity,
373
+ foreignKeys,
374
+ ...native,
375
+ };
376
+ }
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import { assertSafePath, fileExists } from "../core/filesystem.js";
3
+ import { LEGACY_TASK_ARTIFACT_PATHS } from "../core/task-paths.js";
4
+ import { getOperationalStore } from "./operational-context.js";
5
+
6
+ /** Select existing native authority only; never bootstrap or convert legacy data. */
7
+ export async function needsExistingProjectScope(target) {
8
+ if (getOperationalStore(target)) return false;
9
+ for (const relative of [".forgeloop/state.sqlite", ".forgeloop/storage-version.json"]) {
10
+ if (await fileExists(await assertSafePath(target, relative))) return true;
11
+ }
12
+ for (const relative of [".forgeloop/state.sqlite-wal", ".forgeloop/state.sqlite-shm"]) {
13
+ if (await fileExists(await assertSafePath(target, relative))) {
14
+ throw Object.assign(new Error("Retained SQLite sidecars require explicit storage reconciliation; filesystem fallback is forbidden"), { code: "E_STORAGE_MIGRATION_REQUIRED" });
15
+ }
16
+ }
17
+ return false;
18
+ }
19
+
20
+ export async function withExistingProjectScope(target, callback, { readOnly = false } = {}) {
21
+ const { withProjectStorage } = await import("./project-boundary.js");
22
+ return withProjectStorage(target, callback, { readOnly, existingOnly: true });
23
+ }
24
+
25
+ /** Operational namespaces require canonical SQLite even before first creation. */
26
+ export function isOperationalArtifactPath(relativePath) {
27
+ const normalized = path.posix.normalize(relativePath.replaceAll("\\", "/"))
28
+ .split("/").map(segment => segment.replace(/[. ]+$/, "").split(":")[0].toLowerCase()).join("/");
29
+ return [...Object.values(LEGACY_TASK_ARTIFACT_PATHS), ".forgeloop/task-state", ".forgeloop/sessions", ".forgeloop/.txn"]
30
+ .some(root => normalized === root || normalized.startsWith(`${root}/`));
31
+ }
32
+
33
+ /** A native project cannot acquire new writable singleton operational aliases. */
34
+ export function assertNativeWritePath(target, relativePath) {
35
+ const store = getOperationalStore(target);
36
+ const normalized = path.posix.normalize(relativePath.replaceAll("\\", "/"))
37
+ .split("/").map(segment => segment.replace(/[. ]+$/, "").split(":")[0].toLowerCase()).join("/");
38
+ const reservedRoots = [...Object.values(LEGACY_TASK_ARTIFACT_PATHS), ".forgeloop/task-state", ".forgeloop/sessions", ".forgeloop/.txn"];
39
+ const databasePath = normalized === ".forgeloop/state.sqlite" || normalized.startsWith(".forgeloop/state.sqlite-")
40
+ || normalized === ".forgeloop/storage-version.json";
41
+ if (!databasePath && (!store || store.recognizes(relativePath))) return;
42
+ if (databasePath || reservedRoots.some(root => normalized === root || normalized.startsWith(`${root}/`))) {
43
+ throw Object.assign(new Error("SQLite operational writes require a canonical task-scoped path; legacy singleton persistence is retired"), { code: "E_STORAGE_OPERATION_UNSUPPORTED", artifacts: [relativePath] });
44
+ }
45
+ }
@@ -0,0 +1,44 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { realpathWithTransientWindowsRetry } from "../core/filesystem.js";
4
+ import { isOperationalArtifactPath } from "./existing-project-scope.js";
5
+
6
+ const NATIVE_EVIDENCE = ["state.sqlite", "state.sqlite-wal", "state.sqlite-shm", "storage-version.json"];
7
+
8
+ async function physicalDestination(destination) {
9
+ let ancestor = path.resolve(destination);
10
+ const missing = [];
11
+ while (true) {
12
+ try { return path.join(await realpathWithTransientWindowsRetry(ancestor), ...missing); }
13
+ catch (error) {
14
+ if (error.code !== "ENOENT") throw error;
15
+ missing.unshift(path.basename(ancestor));
16
+ const parent = path.dirname(ancestor);
17
+ if (parent === ancestor) throw error;
18
+ ancestor = parent;
19
+ }
20
+ }
21
+ }
22
+
23
+ async function exists(filename) {
24
+ try { await lstat(filename); return true; }
25
+ catch (error) { if (error.code === "ENOENT") return false; throw error; }
26
+ }
27
+
28
+ /** Portable output cannot recreate operational files alongside native authority. */
29
+ export async function assertPortableExportDestination(destination) {
30
+ const physical = await physicalDestination(destination);
31
+ let ancestor = physical;
32
+ while (true) {
33
+ const relative = path.relative(ancestor, physical).split(path.sep).join("/");
34
+ if (relative === "" || isOperationalArtifactPath(relative)) {
35
+ const evidence = await Promise.all(NATIVE_EVIDENCE.map(name => exists(path.join(ancestor, ".forgeloop", name))));
36
+ if (evidence.some(Boolean)) {
37
+ throw Object.assign(new Error("Portable export requires a separate destination outside native operational namespaces"), { code: "E_STORAGE_EXPORT_DESTINATION" });
38
+ }
39
+ }
40
+ const parent = path.dirname(ancestor);
41
+ if (parent === ancestor) return;
42
+ ancestor = parent;
43
+ }
44
+ }