@deksden-com/dd-flow-cli 0.9.0-beta.144 → 0.9.0-beta.149

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 (254) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +13 -8
  3. package/dist/build-info.json +7 -7
  4. package/dist/cli/command-inputs.js +13 -3
  5. package/dist/cli/command-metadata.js +51 -16
  6. package/dist/cli/diagnostic-invocation.js +46 -0
  7. package/dist/cli/diagnostic.js +120 -0
  8. package/dist/cli/help.js +67 -1
  9. package/dist/cli/hook-ingress.js +27 -3
  10. package/dist/cli/memory-flow-admission.js +9 -3
  11. package/dist/cli/memory-init-invocation.js +10 -3
  12. package/dist/cli/memory-init.js +77 -68
  13. package/dist/cli/memory-storage-admission.js +3 -2
  14. package/dist/cli/memory-storage-qualification.js +105 -0
  15. package/dist/cli/memory-storage-transition.js +123 -0
  16. package/dist/cli/memory-upgrade.js +189 -0
  17. package/dist/cli/native-hook-ingress.js +14 -4
  18. package/dist/cli/native-hook-preparation.js +55 -0
  19. package/dist/cli/response-file.js +2 -1
  20. package/dist/cli/run-cli.js +79 -40
  21. package/dist/cli/setup.js +2 -1
  22. package/dist/cli.js +18 -0
  23. package/dist/domain/diagnostic-aspects.js +103 -0
  24. package/dist/domain/diagnostic-flows.js +8 -0
  25. package/dist/domain/stage-handler-identities.js +8 -0
  26. package/dist/harness-runtime/bin/dd-agy.mjs +3 -3
  27. package/dist/harness-runtime/bin/dd-grok.mjs +3 -3
  28. package/dist/harness-runtime/bin/dd-zcode.mjs +5 -5
  29. package/dist/harness-runtime/lib/agy-error-boundary.mjs +58 -0
  30. package/dist/harness-runtime/lib/agy-native-inspection.mjs +216 -0
  31. package/dist/harness-runtime/lib/agy-native_pb.js +117 -0
  32. package/dist/harness-runtime/lib/daemon-operations.d.mts +1 -0
  33. package/dist/harness-runtime/lib/daemon-operations.mjs +84 -10
  34. package/dist/harness-runtime/lib/dd-agy-daemon.mjs +366 -52
  35. package/dist/harness-runtime/lib/dd-agy.mjs +17 -3
  36. package/dist/harness-runtime/lib/dd-codex-daemon.mjs +27 -13
  37. package/dist/harness-runtime/lib/dd-codex.mjs +4 -0
  38. package/dist/harness-runtime/lib/dd-droid-daemon.mjs +4 -3
  39. package/dist/harness-runtime/lib/dd-droid.mjs +71 -29
  40. package/dist/harness-runtime/lib/dd-grok-daemon.mjs +73 -16
  41. package/dist/harness-runtime/lib/dd-grok.mjs +6 -3
  42. package/dist/harness-runtime/lib/dd-opencode-daemon.mjs +66 -20
  43. package/dist/harness-runtime/lib/dd-opencode.mjs +30 -3
  44. package/dist/harness-runtime/lib/dd-zcode-daemon.mjs +7 -6
  45. package/dist/harness-runtime/lib/dd-zcode.mjs +44 -14
  46. package/dist/harness-runtime/lib/delegation-instructions.mjs +1 -1
  47. package/dist/harness-runtime/lib/driver-recovery.mjs +9 -8
  48. package/dist/harness-runtime/lib/entity-references.d.mts +2 -0
  49. package/dist/harness-runtime/lib/entity-references.mjs +9 -0
  50. package/dist/harness-runtime/lib/managed-daemon.d.mts +1 -1
  51. package/dist/harness-runtime/lib/managed-daemon.mjs +24 -9
  52. package/dist/harness-runtime/lib/native-children.d.mts +8 -1
  53. package/dist/harness-runtime/lib/native-children.mjs +114 -8
  54. package/dist/harness-runtime/lib/native-hook-command.mjs +4 -4
  55. package/dist/harness-runtime/lib/native-request-proof.mjs +1 -1
  56. package/dist/harness-runtime/lib/observation-clock.d.mts +2 -2
  57. package/dist/harness-runtime/lib/observation-clock.mjs +7 -4
  58. package/dist/harness-runtime/lib/opencode-plugin.mjs +3 -3
  59. package/dist/harness-runtime/lib/operation-errors.mjs +1 -1
  60. package/dist/harness-runtime/lib/provider-overload-policy.d.mts +5 -3
  61. package/dist/harness-runtime/lib/provider-overload-policy.mjs +20 -7
  62. package/dist/harness-runtime/lib/runner-events.mjs +3 -1
  63. package/dist/harness-runtime/lib/setup-progress.mjs +8 -6
  64. package/dist/harness-runtime/lib/tool-observations.mjs +1 -1
  65. package/dist/schemas/diagnostic-stage-result.schema.json +354 -0
  66. package/dist/schemas/diagnostic-work-result.schema.json +296 -0
  67. package/dist/schemas/execution-bundle.schema.json +7 -1
  68. package/dist/schemas/mb-upgrade-stage-result.schema.json +863 -0
  69. package/dist/schemas/memory-repair-work-result.schema.json +9 -2
  70. package/dist/schemas/project-flow-pack-manifest.schema.json +1 -1
  71. package/dist/schemas/release-impact.schema.json +3 -3
  72. package/dist/schemas/stage-start-response.schema.json +4 -0
  73. package/dist/services/accepted-intent.js +13 -1
  74. package/dist/services/canon.js +43 -7
  75. package/dist/services/check-environment.js +21 -0
  76. package/dist/services/check-runner-evidence.js +267 -15
  77. package/dist/services/cli-operation-classifier.js +1 -1
  78. package/dist/services/cli-upgrade-allowlist.js +1 -1
  79. package/dist/services/code-checks.js +34 -19
  80. package/dist/services/compatibility-preflight.js +5 -2
  81. package/dist/services/controller-execution.js +10 -4
  82. package/dist/services/controller-fanout.js +34 -10
  83. package/dist/services/controller-memory-task.js +45 -0
  84. package/dist/services/controller-memory-worker.js +49 -0
  85. package/dist/services/dashboard.js +2 -2
  86. package/dist/services/descriptor-run-settlement.js +5 -3
  87. package/dist/services/descriptor-run-start.js +46 -20
  88. package/dist/services/descriptor-stage-coordinator.js +66 -13
  89. package/dist/services/descriptor-stage-settlement.js +2 -1
  90. package/dist/services/descriptor-stage-start.js +40 -4
  91. package/dist/services/diagnostic-evidence.js +73 -0
  92. package/dist/services/diagnostic-report-publication.js +72 -0
  93. package/dist/services/diagnostic-report.js +67 -0
  94. package/dist/services/diagnostic-run-bundle.js +74 -0
  95. package/dist/services/diagnostic-snapshot.js +159 -0
  96. package/dist/services/diagnostic-stage-handlers.js +108 -0
  97. package/dist/services/diagnostic-stage-results.js +132 -0
  98. package/dist/services/diagnostic-start.js +84 -0
  99. package/dist/services/diagnostic-work-batch.js +34 -0
  100. package/dist/services/diagnostic-work-collection.js +97 -0
  101. package/dist/services/diagnostic-work-registration.js +101 -0
  102. package/dist/services/diagnostic-work-results.js +93 -0
  103. package/dist/services/engines.js +64 -14
  104. package/dist/services/eval-snapshots.js +186 -119
  105. package/dist/services/execution-bundles.js +19 -7
  106. package/dist/services/external-work-launch.js +5 -3
  107. package/dist/services/flow-catalog.js +13 -4
  108. package/dist/services/frozen-engine.js +148 -11
  109. package/dist/services/hook-memory-routing.js +188 -0
  110. package/dist/services/hook-recovery-guard.js +14 -0
  111. package/dist/services/hook-session-observation.js +24 -0
  112. package/dist/services/hooks.js +27 -8
  113. package/dist/services/hybrid-contracts.js +4 -13
  114. package/dist/services/hybrid-digest.js +14 -0
  115. package/dist/services/installed-flow-source.js +47 -4
  116. package/dist/services/lifecycle-invocations.js +179 -38
  117. package/dist/services/memory-accepted-composition.js +5 -4
  118. package/dist/services/memory-completed-correction-wave.js +23 -15
  119. package/dist/services/memory-compose-handler.js +163 -21
  120. package/dist/services/memory-composition-acceptance.js +186 -10
  121. package/dist/services/memory-composition-graph.js +42 -11
  122. package/dist/services/memory-composition-registration.js +23 -8
  123. package/dist/services/memory-composition-work-custody.js +14 -9
  124. package/dist/services/memory-composition-work-results.js +15 -3
  125. package/dist/services/memory-corrected-review-batch.js +10 -3
  126. package/dist/services/memory-corrected-review-findings.js +4 -2
  127. package/dist/services/memory-corrected-review-inputs.js +10 -5
  128. package/dist/services/memory-corrected-review-receipt.js +17 -9
  129. package/dist/services/memory-corrected-review-registration.js +3 -2
  130. package/dist/services/memory-correction-check-receipt.js +20 -4
  131. package/dist/services/memory-custody-snapshot.js +36 -0
  132. package/dist/services/memory-finish-evidence.js +44 -22
  133. package/dist/services/memory-finish-handler.js +37 -23
  134. package/dist/services/memory-finish-integration.js +218 -48
  135. package/dist/services/memory-finish-readiness.js +66 -19
  136. package/dist/services/memory-git-integration.js +87 -18
  137. package/dist/services/memory-historical-corrected-review.js +21 -15
  138. package/dist/services/memory-historical-original-review.js +22 -9
  139. package/dist/services/memory-historical-repair-registration.js +14 -8
  140. package/dist/services/memory-historical-second-review.js +30 -11
  141. package/dist/services/memory-init-contexts.js +44 -2
  142. package/dist/services/memory-init-controller-start.js +27 -8
  143. package/dist/services/memory-init-git-policy.js +50 -21
  144. package/dist/services/memory-init-lint-capability.js +24 -6
  145. package/dist/services/memory-init-native-capacity.js +17 -4
  146. package/dist/services/memory-init-preflight.js +21 -17
  147. package/dist/services/memory-init-request-candidate.js +73 -17
  148. package/dist/services/memory-init-request-input.js +23 -7
  149. package/dist/services/memory-init-run-bundle.js +49 -12
  150. package/dist/services/memory-init-storage.js +17 -7
  151. package/dist/services/memory-init-worktrunk-capability.js +3 -7
  152. package/dist/services/memory-layer.js +71 -6
  153. package/dist/services/memory-lint-config.js +111 -2
  154. package/dist/services/memory-native-review-copy.js +5 -67
  155. package/dist/services/memory-normal-flow-entry.js +12 -0
  156. package/dist/services/memory-ordinary-defaults.js +22 -5
  157. package/dist/services/memory-preflight-handler.js +15 -0
  158. package/dist/services/memory-repair-current-view.js +1 -1
  159. package/dist/services/memory-repair-document-delta.js +70 -33
  160. package/dist/services/memory-repair-history.js +12 -7
  161. package/dist/services/memory-repair-physical-binding.js +1 -119
  162. package/dist/services/memory-repair-publication-plan.js +5 -3
  163. package/dist/services/memory-repair-publication-source.js +1 -1
  164. package/dist/services/memory-repair-publication.js +3 -2
  165. package/dist/services/memory-repair-registration.js +2 -1
  166. package/dist/services/memory-repair-work-baseline.js +4 -2
  167. package/dist/services/memory-repair-work-completion.js +32 -12
  168. package/dist/services/memory-repair-work-results.js +21 -2
  169. package/dist/services/memory-repair-workspace.js +11 -2
  170. package/dist/services/memory-repair-writer-copy.js +2 -1
  171. package/dist/services/memory-result-binding.js +78 -12
  172. package/dist/services/memory-retained-review-authority.js +17 -8
  173. package/dist/services/memory-retained-second-baseline.js +1 -1
  174. package/dist/services/memory-retained-second-repairs.js +12 -6
  175. package/dist/services/memory-review-correction-plan.js +47 -12
  176. package/dist/services/memory-review-handler.js +17 -17
  177. package/dist/services/memory-review-inputs.js +55 -9
  178. package/dist/services/memory-review-limits.js +10 -0
  179. package/dist/services/memory-review-receipt.js +2 -2
  180. package/dist/services/memory-review-registration.js +13 -4
  181. package/dist/services/memory-review-work-custody.js +3 -1
  182. package/dist/services/memory-review-work-input.js +26 -7
  183. package/dist/services/memory-review-work-results.js +26 -4
  184. package/dist/services/memory-second-corrected-review-inputs.js +5 -3
  185. package/dist/services/memory-second-correction-plan.js +2 -1
  186. package/dist/services/memory-second-repair-launch.js +4 -4
  187. package/dist/services/memory-second-repair-source.js +1 -1
  188. package/dist/services/memory-stage-dispatch.js +38 -1
  189. package/dist/services/memory-stage-results.js +113 -22
  190. package/dist/services/memory-tool-probe.js +25 -0
  191. package/dist/services/memory-upgrade-analysis-handler.js +243 -0
  192. package/dist/services/memory-upgrade-bootstrap-readiness.js +251 -0
  193. package/dist/services/memory-upgrade-evidence.js +100 -0
  194. package/dist/services/memory-upgrade-inventory.js +442 -0
  195. package/dist/services/memory-upgrade-publication-custody.js +73 -0
  196. package/dist/services/memory-upgrade-publication.js +413 -0
  197. package/dist/services/memory-upgrade-selection.js +430 -0
  198. package/dist/services/memory-upgrade-start.js +60 -0
  199. package/dist/services/memory-upgrade-storage-readiness.js +340 -0
  200. package/dist/services/memory-verification-acceptance.js +38 -17
  201. package/dist/services/memory-verification-checks.js +92 -27
  202. package/dist/services/memory-verification-receipt.js +6 -5
  203. package/dist/services/memory-verify-handler.js +15 -13
  204. package/dist/services/memory-work-result-json.js +12 -0
  205. package/dist/services/memory-work-result-preparation.js +5 -4
  206. package/dist/services/merge-server.js +4 -2
  207. package/dist/services/migrations.js +127 -98
  208. package/dist/services/native-daemon-history.js +2 -1
  209. package/dist/services/physical-startup.js +45 -0
  210. package/dist/services/project-registry.js +97 -0
  211. package/dist/services/project-summary.js +1 -1
  212. package/dist/services/projects.js +3 -93
  213. package/dist/services/prompt-delivery.js +71 -0
  214. package/dist/services/provider-capacity.js +112 -46
  215. package/dist/services/recovery-snapshot-database.js +32 -4
  216. package/dist/services/release-cache.js +8 -0
  217. package/dist/services/repair-intents.js +1 -12
  218. package/dist/services/repair-receipt.js +13 -0
  219. package/dist/services/run-control-worker.js +35 -18
  220. package/dist/services/run-control.js +2 -1
  221. package/dist/services/run-controller-adapter.js +2 -1
  222. package/dist/services/run-controller-process.js +4 -7
  223. package/dist/services/run-controller-recovery.js +8 -2
  224. package/dist/services/run-controller.js +215 -94
  225. package/dist/services/run-fork.js +1 -1
  226. package/dist/services/run-recovery-guard.js +30 -0
  227. package/dist/services/run-recovery.js +11 -44
  228. package/dist/services/run-start-custody.js +17 -5
  229. package/dist/services/runs.js +6 -1
  230. package/dist/services/runtime-scope-resume.js +12 -6
  231. package/dist/services/runtime-scope-stop.js +3 -2
  232. package/dist/services/runtime-scope-worker.js +46 -19
  233. package/dist/services/runtime-service.js +2 -1
  234. package/dist/services/sessions.js +3 -24
  235. package/dist/services/setup.js +63 -13
  236. package/dist/services/stage-handlers.js +22 -7
  237. package/dist/services/stage-pause.js +111 -44
  238. package/dist/services/version-status.js +1 -1
  239. package/dist/services/vnext-code.js +2 -2
  240. package/dist/services/vnext-fanout.js +51 -9
  241. package/dist/services/vnext-plan-review.js +3 -5
  242. package/dist/services/work-registry.js +117 -39
  243. package/dist/services/workspace-bootstrap.js +2 -1
  244. package/dist/services/workspace-files.js +1 -1
  245. package/dist/shared/entity-references.js +1 -8
  246. package/dist/storage/data-preservation.js +65 -0
  247. package/dist/storage/database.js +53 -19
  248. package/dist/storage/import-guard.js +84 -27
  249. package/dist/storage/memory-upgrade-schema-bootstrap.js +38 -0
  250. package/dist/storage/paired-writer-migration.js +424 -0
  251. package/dist/storage/writer-contract.js +11 -4
  252. package/dist/storage/writer-migration.js +313 -52
  253. package/dist/storage/writer-restore.js +217 -0
  254. package/package.json +13 -6
@@ -3,16 +3,21 @@ import path from "node:path";
3
3
  import crypto from "node:crypto";
4
4
  import { DatabaseSync } from "node:sqlite";
5
5
  import { AppError } from "../shared/errors.js";
6
- import { installStoreWriterContract, readStoreWriterContract, registerStoreWriter, STORE_WRITER_VERSION } from "./writer-contract.js";
6
+ import { installStoreWriterContract, readStoreWriterContract, registerReconciliationWriter, STORE_WRITER_VERSION } from "./writer-contract.js";
7
7
  import { rollbackStoreTransaction, withStoreCleanup } from "./native-cleanup.js";
8
+ import { captureStoreData, verifyStoreData } from "./data-preservation.js";
9
+ import { publishImmutableInput } from "./immutable-input.js";
8
10
  /** Explicit one-store cutover. The caller must separately drain external owners. */
9
- export function migrateStoreWriter(file, backup) {
11
+ export function migrateStoreWriter(file, backup, request) {
12
+ if (request !== undefined && !(request instanceof PreparedStoreWriterMigration))
13
+ throw new AppError("store_migration_request_invalid", "Migration request must be prepared", 1);
14
+ request?.assertBinding(file, backup);
10
15
  if (!path.isAbsolute(file) || !path.isAbsolute(backup) || file === backup || !fs.existsSync(file) || !fs.statSync(file).isFile())
11
16
  throw new AppError("store_migration_path_invalid", "Migration requires an existing database and a new absolute backup path", 1);
12
17
  const source = fs.realpathSync(file);
13
18
  const destination = path.join(fs.realpathSync(path.dirname(backup)), path.basename(backup));
14
- if (fs.lstatSync(file).isSymbolicLink() || [source, `${source}-wal`, `${source}-shm`, `${source}-journal`].includes(destination))
15
- throw new AppError("store_migration_path_invalid", "Backup cannot alias the source database or its SQLite sidecars", 1);
19
+ if (fs.lstatSync(file).isSymbolicLink() || fs.lstatSync(file).nlink !== 1 || [source, `${source}-wal`, `${source}-shm`, `${source}-journal`].includes(destination))
20
+ throw new AppError("store_migration_path_invalid", "Source must have one physical owner and backup cannot alias it or its SQLite sidecars", 1);
16
21
  let existing;
17
22
  try {
18
23
  existing = fs.lstatSync(backup);
@@ -26,60 +31,316 @@ export function migrateStoreWriter(file, backup) {
26
31
  throw new AppError("store_migration_path_invalid", "Backup path already exists", 1);
27
32
  const reader = new DatabaseSync(file, { readOnly: true });
28
33
  return withStoreCleanup(() => {
29
- const recorded = reader.prepare("SELECT 1 FROM sqlite_schema WHERE type = 'table' AND name = 'dd_flow_writer_migrations'").get()
30
- ? reader.prepare("SELECT receipt_json FROM dd_flow_writer_migrations WHERE backup_path = ?").get(destination) : null;
31
- const receipt = recorded ? JSON.parse(String(recorded.receipt_json)) : null;
34
+ const receipt = recordedMigration(reader, destination);
35
+ if (!receipt && request)
36
+ return null;
32
37
  if (!receipt || receipt.database !== file || receipt.backup !== backup || receipt.writer_version !== readStoreWriterContract(reader) || receipt.backup_sha256 !== crypto.createHash("sha256").update(fs.readFileSync(backup)).digest("hex")) {
33
38
  throw new AppError("store_migration_path_invalid", "Existing backup has no matching committed migration receipt", 1);
34
39
  }
40
+ if (request && (receipt.request_sha256 !== request.sha256 || receipt.request_id !== request.requestId))
41
+ unknownOutcome("foreign_committed_receipt");
35
42
  return receipt;
36
- }, () => reader.close());
37
- }
38
- const db = new DatabaseSync(file);
39
- let committed = false;
40
- return withStoreCleanup(() => {
41
- db.exec("PRAGMA busy_timeout = 1000");
42
- registerStoreWriter(db);
43
- db.exec("BEGIN IMMEDIATE");
44
- try {
45
- const previous = readStoreWriterContract(db);
46
- const tables = new Set(db.prepare("SELECT name FROM sqlite_schema WHERE type = 'table'").all().map(row => row.name));
47
- const gates = [
48
- ["managed_processes", "state IS NULL OR state NOT IN ('stopped','failed')"],
49
- ["run_controllers", "status IS NULL OR status NOT IN ('completed','stop_target_reached','cancelled','superseded')"],
50
- ["run_control_workers", "status IS NULL OR status NOT IN ('completed','failed','superseded')"],
51
- ["work_sessions", "status IS NULL OR status NOT IN ('completed','failed','cancelled')"]
52
- ];
53
- for (const [table, predicate] of gates) {
54
- if (tables.has(table) && db.prepare(`SELECT 1 FROM ${table} WHERE ${predicate} LIMIT 1`).get())
55
- throw new AppError("store_migration_not_drained", "Store still contains unresolved writer ownership", 1, { table });
43
+ }, () => reader.close()) ?? resumePrepared();
44
+ }
45
+ return resumePrepared();
46
+ function resumePrepared() {
47
+ const db = new DatabaseSync(file);
48
+ let committed = false;
49
+ return withStoreCleanup(() => {
50
+ db.exec("PRAGMA busy_timeout = 1000");
51
+ registerReconciliationWriter(db);
52
+ db.exec("BEGIN IMMEDIATE");
53
+ try {
54
+ const original = request?.assertOriginal(db);
55
+ const previous = readStoreWriterContract(db);
56
+ assertStoreWriterDrain(db);
57
+ const preservedData = original ?? captureStoreData(db);
58
+ // Explicit drained cutover, not a live-runtime writer path: keep backup, durability
59
+ // and hash inside this barrier so the receipt describes exactly the fenced source.
60
+ // A separate reader sees committed WAL while this connection prevents new writes.
61
+ const backupSha256 = verifiedBackup(file, backup, !existing, previous, preservedData, request);
62
+ const receipt = { database: file, backup, backup_sha256: backupSha256, previous_version: previous, writer_version: STORE_WRITER_VERSION,
63
+ ...(request ? { request_id: request.requestId, request_sha256: request.sha256 } : {}),
64
+ data_preservation: { schema_id: "dd-flow/store-data-preservation@1", tables: preservedData } };
65
+ db.exec("CREATE TABLE IF NOT EXISTS dd_flow_writer_migrations (backup_path TEXT PRIMARY KEY, receipt_json TEXT NOT NULL)");
66
+ // Upgrade only the known historical writer contract. Its existing fence
67
+ // admits this connection as v1 until the row changes in this transaction;
68
+ // install then registers v2 and publishes replacement fences atomically.
69
+ if (previous === 1)
70
+ db.prepare("UPDATE dd_flow_writer_contract SET version=? WHERE id=1 AND version=1").run(STORE_WRITER_VERSION);
71
+ installStoreWriterContract(db);
72
+ db.prepare("INSERT INTO dd_flow_writer_migrations VALUES (?, ?)").run(destination, JSON.stringify(receipt));
73
+ verifyStoreData(db, preservedData);
74
+ db.exec("COMMIT");
75
+ committed = true;
76
+ return receipt;
56
77
  }
57
- // Explicit drained cutover, not a live-runtime writer path: keep backup, durability
58
- // and hash inside this barrier so the receipt describes exactly the fenced source.
59
- // A separate reader sees committed WAL while this connection prevents new writes.
60
- const reader = new DatabaseSync(file, { readOnly: true });
61
- withStoreCleanup(() => reader.exec(`VACUUM INTO '${backup.replaceAll("'", "''")}'`), () => reader.close());
62
- const descriptor = fs.openSync(backup, "r");
63
- withStoreCleanup(() => fs.fsyncSync(descriptor), () => fs.closeSync(descriptor));
64
- const directory = fs.openSync(path.dirname(backup), "r");
65
- withStoreCleanup(() => fs.fsyncSync(directory), () => fs.closeSync(directory));
66
- const verification = new DatabaseSync(backup, { readOnly: true });
67
- withStoreCleanup(() => {
68
- const check = verification.prepare("PRAGMA integrity_check").get();
69
- if (Object.values(check)[0] !== "ok" || readStoreWriterContract(verification) !== previous)
70
- throw new AppError("store_migration_backup_invalid", "Backup failed integrity or contract verification", 1);
71
- }, () => verification.close());
72
- const receipt = { database: file, backup, backup_sha256: crypto.createHash("sha256").update(fs.readFileSync(backup)).digest("hex"), previous_version: previous, writer_version: STORE_WRITER_VERSION };
73
- db.exec("CREATE TABLE IF NOT EXISTS dd_flow_writer_migrations (backup_path TEXT PRIMARY KEY, receipt_json TEXT NOT NULL)");
74
- db.prepare("INSERT INTO dd_flow_writer_migrations VALUES (?, ?)").run(destination, JSON.stringify(receipt));
75
- installStoreWriterContract(db);
76
- db.exec("COMMIT");
77
- committed = true;
78
- return receipt;
78
+ catch (error) {
79
+ rollbackStoreTransaction(db, "ROLLBACK", error);
80
+ }
81
+ }, () => { if (db.isOpen)
82
+ db.close(); }, () => ({ effect: committed ? "committed" : "uncommitted" }));
83
+ }
84
+ }
85
+ /** Caller holds the source writer barrier; a separate reader includes committed
86
+ * WAL in the durable copy. Shared by backup-only preparation and cutover. */
87
+ function verifiedBackup(file, backup, create, previous, preservedData, request) {
88
+ const reader = new DatabaseSync(file, { readOnly: true });
89
+ withStoreCleanup(() => { if (create)
90
+ reader.exec(`VACUUM INTO '${backup.replaceAll("'", "''")}'`); }, () => reader.close());
91
+ const descriptor = fs.openSync(backup, "r");
92
+ withStoreCleanup(() => fs.fsyncSync(descriptor), () => fs.closeSync(descriptor));
93
+ const directory = fs.openSync(path.dirname(backup), "r");
94
+ withStoreCleanup(() => fs.fsyncSync(directory), () => fs.closeSync(directory));
95
+ const verification = new DatabaseSync(backup, { readOnly: true });
96
+ withStoreCleanup(() => {
97
+ const check = verification.prepare("PRAGMA integrity_check").get();
98
+ if (Object.values(check)[0] !== "ok" || readStoreWriterContract(verification) !== previous)
99
+ throw new AppError("store_migration_backup_invalid", "Backup failed integrity or contract verification", 1);
100
+ verifyStoreData(verification, preservedData);
101
+ request?.assertBackup(verification);
102
+ }, () => verification.close());
103
+ return crypto.createHash("sha256").update(fs.readFileSync(backup)).digest("hex");
104
+ }
105
+ function assertStoreWriterDrain(db) {
106
+ const tables = new Set(db.prepare("SELECT name FROM sqlite_schema WHERE type='table'").all().map(row => String(row.name)));
107
+ const gates = [
108
+ ["managed_processes", "state IS NULL OR state NOT IN ('stopped','failed')"],
109
+ ["run_controllers", "status IS NULL OR status NOT IN ('completed','stop_target_reached','cancelled','superseded')"],
110
+ ["run_control_workers", "status IS NULL OR status NOT IN ('completed','failed','superseded')"],
111
+ ["merge_requests", "status IS NULL OR status NOT IN ('completed','failed','cancelled','superseded')"],
112
+ ["work_sessions", "status IS NULL OR status NOT IN ('completed','failed','cancelled')"]
113
+ ];
114
+ for (const [table, predicate] of gates)
115
+ if (tables.has(table) && db.prepare(`SELECT 1 FROM ${table} WHERE ${predicate} LIMIT 1`).get())
116
+ throw new AppError("store_migration_not_drained", "Store still contains unresolved writer ownership", 1, { table });
117
+ }
118
+ function recordedMigration(db, backup) {
119
+ const row = db.prepare("SELECT 1 FROM sqlite_schema WHERE type='table' AND name='dd_flow_writer_migrations'").get()
120
+ ? db.prepare("SELECT receipt_json FROM dd_flow_writer_migrations WHERE backup_path=?").get(backup) : null;
121
+ return row ? JSON.parse(String(row.receipt_json)) : null;
122
+ }
123
+ function unknownOutcome(reason) {
124
+ throw new AppError("store_migration_outcome_unknown", "Reconcile this exact migration request before another apply or explicit recovery", 1, { reason, effect: "unknown", next_action: "reconcile_exact_request" });
125
+ }
126
+ const requestHash = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
127
+ function footprintExists(file) {
128
+ try {
129
+ fs.lstatSync(file);
130
+ return true;
131
+ }
132
+ catch (error) {
133
+ if (error.code === "ENOENT")
134
+ return false;
135
+ throw error;
136
+ }
137
+ }
138
+ function sourceState(db) {
139
+ return { writer_version: readStoreWriterContract(db), tables: captureStoreData(db),
140
+ metadata: { user_version: db.prepare("PRAGMA user_version").get(), application_id: db.prepare("PRAGMA application_id").get(), encoding: db.prepare("PRAGMA encoding").get() },
141
+ schema: db.prepare("SELECT type,name,tbl_name,sql FROM sqlite_schema ORDER BY type,name").all(),
142
+ migration_receipts: db.prepare("SELECT 1 FROM sqlite_schema WHERE name='dd_flow_writer_migrations' AND type='table'").get()
143
+ ? db.prepare("SELECT backup_path,receipt_json FROM dd_flow_writer_migrations ORDER BY backup_path").all() : [] };
144
+ }
145
+ /** One-store request dependency, not shared physical drain or engine-schema
146
+ * migration. Its immutable intent lives outside the migrated DB. Restore needs
147
+ * the independently retained digest, never a freshly selected replacement plan.
148
+ */
149
+ export class PreparedStoreWriterMigration {
150
+ sha256;
151
+ requestId;
152
+ #file;
153
+ #intent;
154
+ #bytes;
155
+ constructor(file, intent) {
156
+ this.#file = file;
157
+ this.#intent = structuredClone(intent);
158
+ this.#bytes = JSON.stringify(intent);
159
+ if (Buffer.byteLength(this.#bytes) > 4 * 1024 * 1024)
160
+ throw new AppError("store_migration_request_invalid", "Migration request exceeds its supported bound", 1);
161
+ this.sha256 = requestHash(this.#bytes);
162
+ this.requestId = intent.request_id;
163
+ Object.freeze(this);
164
+ }
165
+ static prepare(input) {
166
+ if (typeof input.requestId !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(input.requestId))
167
+ throw new AppError("store_migration_request_invalid", "Invalid migration request ID", 2);
168
+ for (const file of [input.database, input.backup, input.requestFile]) {
169
+ if (!path.isAbsolute(file) || path.join(fs.realpathSync(path.dirname(file)), path.basename(file)) !== file)
170
+ throw new AppError("store_migration_request_invalid", "Migration requires canonical absolute paths", 2);
171
+ }
172
+ if (new Set([input.database, input.backup, input.requestFile]).size !== 3 || footprintExists(input.backup) || footprintExists(input.requestFile))
173
+ throw new AppError("store_migration_request_invalid", "Restore an existing request; do not replace its plan or backup", 1);
174
+ if ([input.database, input.backup].some(file => ["-wal", "-shm", "-journal"].some(suffix => [input.backup, input.requestFile].includes(file + suffix))))
175
+ throw new AppError("store_migration_request_invalid", "Request and backup must not alias SQLite sidecars", 1);
176
+ const owner = fs.lstatSync(input.database);
177
+ if (!owner.isFile() || owner.nlink !== 1)
178
+ throw new AppError("store_migration_path_invalid", "Migration requires one regular source owner", 1);
179
+ const db = new DatabaseSync(input.database, { readOnly: true });
180
+ return withStoreCleanup(() => {
181
+ db.exec("BEGIN");
182
+ assertStoreWriterDrain(db);
183
+ const source = sourceState(db);
184
+ if (source.writer_version !== null && ![1, STORE_WRITER_VERSION].includes(source.writer_version))
185
+ throw new AppError("store_writer_incompatible", "Unsupported source writer contract", 1);
186
+ return new PreparedStoreWriterMigration(input.requestFile, { schema_id: "dd-flow/store-writer-migration-request@1", request_id: input.requestId,
187
+ database: input.database, backup: input.backup, source_owner: { dev: owner.dev, ino: owner.ino }, source, writer_version: STORE_WRITER_VERSION });
188
+ }, () => db.close());
189
+ }
190
+ static restore(requestFile, expectedSha256) {
191
+ const stat = fs.lstatSync(requestFile);
192
+ if (!stat.isFile() || stat.size > 4 * 1024 * 1024)
193
+ unknownOutcome("invalid_request_file");
194
+ const bytes = fs.readFileSync(requestFile, "utf8");
195
+ return this.restoreInput(requestFile, bytes, expectedSha256);
196
+ }
197
+ /** A paired request retains these exact child bytes before any cutover. */
198
+ static restoreInput(requestFile, bytes, expectedSha256) {
199
+ if (!path.isAbsolute(requestFile) || Buffer.byteLength(bytes) > 4 * 1024 * 1024)
200
+ unknownOutcome("invalid_request_file");
201
+ if (!/^[a-f0-9]{64}$/.test(expectedSha256) || requestHash(bytes) !== expectedSha256)
202
+ unknownOutcome("request_changed");
203
+ const intent = JSON.parse(bytes);
204
+ if (!intent || Object.keys(intent).sort().join() !== "backup,database,request_id,schema_id,source,source_owner,writer_version"
205
+ || intent.schema_id !== "dd-flow/store-writer-migration-request@1" || intent.writer_version !== STORE_WRITER_VERSION
206
+ || typeof intent.request_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(intent.request_id)
207
+ || !intent.source || !Array.isArray(intent.source.tables) || !Array.isArray(intent.source.schema) || !Array.isArray(intent.source.migration_receipts)
208
+ || !intent.source_owner || !Number.isSafeInteger(intent.source_owner.dev) || !Number.isSafeInteger(intent.source_owner.ino)
209
+ || !path.isAbsolute(intent.database) || !path.isAbsolute(intent.backup))
210
+ unknownOutcome("invalid_request");
211
+ const prepared = new PreparedStoreWriterMigration(requestFile, intent);
212
+ if (prepared.sha256 !== expectedSha256)
213
+ unknownOutcome("noncanonical_request");
214
+ return prepared;
215
+ }
216
+ inputArtifact() { return { path: this.#file, sha256: this.sha256, base64: Buffer.from(this.#bytes).toString("base64") }; }
217
+ assertBinding(file, backup) {
218
+ if (file !== this.#intent.database || backup !== this.#intent.backup)
219
+ unknownOutcome("foreign_store_scope");
220
+ const owner = fs.lstatSync(file);
221
+ if (!owner.isFile() || owner.nlink !== 1 || owner.dev !== this.#intent.source_owner.dev || owner.ino !== this.#intent.source_owner.ino)
222
+ unknownOutcome("source_owner_changed");
223
+ if (footprintExists(backup) && !fs.lstatSync(backup).isFile())
224
+ unknownOutcome("invalid_backup_path");
225
+ if (requestHash(fs.readFileSync(this.#file)) !== this.sha256)
226
+ unknownOutcome("request_changed");
227
+ }
228
+ assertOriginal(db) {
229
+ const owner = fs.lstatSync(this.#intent.database);
230
+ const current = sourceState(db);
231
+ if (owner.dev !== this.#intent.source_owner.dev || owner.ino !== this.#intent.source_owner.ino || owner.nlink !== 1
232
+ || requestHash(JSON.stringify(current)) !== requestHash(JSON.stringify(this.#intent.source)))
233
+ unknownOutcome("source_changed");
234
+ return current.tables;
235
+ }
236
+ assertBackup(db) {
237
+ if (requestHash(JSON.stringify(sourceState(db))) !== requestHash(JSON.stringify(this.#intent.source)))
238
+ unknownOutcome("backup_not_original");
239
+ }
240
+ /** Read-only outcome inspection; unknown never authorizes another apply. */
241
+ reconcile() {
242
+ try {
243
+ this.assertBinding(this.#intent.database, this.#intent.backup);
244
+ const db = new DatabaseSync(this.#intent.database, { readOnly: true });
245
+ return withStoreCleanup(() => {
246
+ db.exec("BEGIN");
247
+ const receipt = recordedMigration(db, this.#intent.backup);
248
+ if (receipt) {
249
+ if (receipt.request_sha256 !== this.sha256 || receipt.request_id !== this.requestId || receipt.database !== this.#intent.database
250
+ || receipt.backup !== this.#intent.backup || receipt.writer_version !== readStoreWriterContract(db)
251
+ || receipt.backup_sha256 !== requestHash(fs.readFileSync(this.#intent.backup)))
252
+ unknownOutcome("committed_receipt_changed");
253
+ return { status: "applied", receipt };
254
+ }
255
+ this.assertOriginal(db);
256
+ if (fs.existsSync(this.#intent.backup)) {
257
+ const backup = new DatabaseSync(this.#intent.backup, { readOnly: true });
258
+ withStoreCleanup(() => {
259
+ const check = backup.prepare("PRAGMA integrity_check").get();
260
+ if (!check || Object.values(check)[0] !== "ok")
261
+ unknownOutcome("backup_invalid");
262
+ this.assertBackup(backup);
263
+ }, () => backup.close());
264
+ }
265
+ return { status: "not_applied" };
266
+ }, () => db.close());
79
267
  }
80
268
  catch (error) {
81
- rollbackStoreTransaction(db, "ROLLBACK", error);
269
+ return { status: "unknown", reason: error instanceof AppError ? String(error.details.reason ?? error.code) : "outcome_unavailable" };
82
270
  }
83
- }, () => { if (db.isOpen)
84
- db.close(); }, () => ({ effect: committed ? "committed" : "uncommitted" }));
271
+ }
272
+ /** Publish a plan without cutover. Retention is not current drain/readiness. */
273
+ retain() {
274
+ publishImmutableInput(this.#file, Buffer.from(this.#bytes), "store_migration_request_conflict");
275
+ return { request_id: this.requestId, request_file: this.#file, sha256: this.sha256 };
276
+ }
277
+ recoveryReference() {
278
+ this.assertBinding(this.#intent.database, this.#intent.backup);
279
+ return { database: this.#intent.database, backup: this.#intent.backup, migration_sha256: this.sha256 };
280
+ }
281
+ /** Read back the actual original backup, not caller-proposed evidence. */
282
+ backupFacts() {
283
+ this.assertBinding(this.#intent.database, this.#intent.backup);
284
+ const db = new DatabaseSync(this.#intent.backup, { readOnly: true });
285
+ withStoreCleanup(() => {
286
+ const check = db.prepare("PRAGMA integrity_check").get();
287
+ if (!check || Object.values(check)[0] !== "ok")
288
+ unknownOutcome("backup_invalid");
289
+ this.assertBackup(db);
290
+ }, () => db.close());
291
+ return { database: this.#intent.database, backup: this.#intent.backup, backup_sha256: requestHash(fs.readFileSync(this.#intent.backup)),
292
+ source_request_sha256: this.sha256, source_writer_version: this.#intent.source.writer_version,
293
+ source_data_sha256: requestHash(JSON.stringify(this.#intent.source.tables)) };
294
+ }
295
+ /** Called after cutover and before admitting other runtime writes. */
296
+ verifyAppliedData() {
297
+ const observed = this.reconcile();
298
+ if (observed.status !== "applied")
299
+ unknownOutcome("applied_receipt_missing");
300
+ const db = new DatabaseSync(this.#intent.database, { readOnly: true });
301
+ withStoreCleanup(() => {
302
+ db.exec("BEGIN");
303
+ if (db.prepare("PRAGMA quick_check").get()?.quick_check !== "ok" || db.prepare("PRAGMA foreign_key_check").get()
304
+ || readStoreWriterContract(db) !== STORE_WRITER_VERSION)
305
+ unknownOutcome("applied_integrity_failed");
306
+ verifyStoreData(db, this.#intent.source.tables);
307
+ }, () => db.close());
308
+ return { database: this.#intent.database, source_request_sha256: this.sha256, writer_version: STORE_WRITER_VERSION,
309
+ source_data_sha256: requestHash(JSON.stringify(this.#intent.source.tables)), integrity: "ok", foreign_key_violations: 0 };
310
+ }
311
+ /** Durable verified copy without changing schema or writer contract. */
312
+ backup() {
313
+ this.retain();
314
+ const outcome = this.reconcile();
315
+ if (outcome.status === "unknown")
316
+ unknownOutcome(outcome.reason ?? "outcome_unavailable");
317
+ if (outcome.status === "applied")
318
+ return;
319
+ const db = new DatabaseSync(this.#intent.database);
320
+ withStoreCleanup(() => {
321
+ db.exec("PRAGMA busy_timeout=1000");
322
+ registerReconciliationWriter(db);
323
+ db.exec("BEGIN IMMEDIATE");
324
+ try {
325
+ this.assertBinding(this.#intent.database, this.#intent.backup);
326
+ const data = this.assertOriginal(db);
327
+ assertStoreWriterDrain(db);
328
+ verifiedBackup(this.#intent.database, this.#intent.backup, !fs.existsSync(this.#intent.backup), readStoreWriterContract(db), data, this);
329
+ db.exec("COMMIT");
330
+ }
331
+ catch (error) {
332
+ rollbackStoreTransaction(db, "ROLLBACK", error);
333
+ }
334
+ }, () => db.close());
335
+ }
336
+ apply() {
337
+ this.retain();
338
+ const outcome = this.reconcile();
339
+ if (outcome.status === "unknown")
340
+ unknownOutcome(outcome.reason ?? "outcome_unavailable");
341
+ if (outcome.status === "applied")
342
+ return outcome.receipt;
343
+ return migrateStoreWriter(this.#intent.database, this.#intent.backup, this);
344
+ }
85
345
  }
346
+ export { sourceState as captureStoreMigrationState, assertStoreWriterDrain };
@@ -0,0 +1,217 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { AppError } from "../shared/errors.js";
6
+ import { PreparedStoreWriterMigration, captureStoreMigrationState, assertStoreWriterDrain } from "./writer-migration.js";
7
+ import { registerReconciliationWriter } from "./writer-contract.js";
8
+ import { publishImmutableInput } from "./immutable-input.js";
9
+ import { rollbackStoreTransaction, withStoreCleanup } from "./native-cleanup.js";
10
+ const hash = (bytes) => createHash("sha256").update(bytes).digest("hex");
11
+ const quote = (name) => `"${name.replaceAll('"', '""')}"`;
12
+ function refuse(reason) {
13
+ throw new AppError("store_restore_outcome_unknown", "Restore requires its exact recorded recovery scope and verified old backup", 1, { reason, effect: "unknown", next_action: "reconcile_exact_recovery" });
14
+ }
15
+ function state(db) {
16
+ const snapshot = captureStoreMigrationState(db);
17
+ // Empty SQLite housekeeping left by a dropped AUTOINCREMENT/statistics table
18
+ // is not product state. Nonempty sequence values remain preservation facts.
19
+ return { ...snapshot, schema: snapshot.schema.filter(row => !String(row.name).startsWith("sqlite_")),
20
+ tables: snapshot.tables.filter(table => !table.table.startsWith("sqlite_") || table.row_count > 0) };
21
+ }
22
+ function supported(db) {
23
+ if (db.prepare("SELECT 1 FROM sqlite_schema WHERE type='table' AND upper(sql) LIKE 'CREATE VIRTUAL TABLE%' LIMIT 1").get())
24
+ throw new AppError("store_restore_schema_unsupported", "Transactional runtime restoration does not execute virtual-table modules", 1);
25
+ }
26
+ /** Explicit single-store rollback, never an apply retry. External/shared
27
+ * physical drain is the bootstrap owner's dependency; logical store ownership
28
+ * is checked here before preparation and again inside the exclusive writer.
29
+ * File identity stays unchanged; all schema/data replacement is one transaction.
30
+ */
31
+ export class PreparedStoreWriterRestore {
32
+ sha256;
33
+ #migration;
34
+ #file;
35
+ #intent;
36
+ #bytes;
37
+ constructor(migration, file, intent) {
38
+ this.#migration = migration;
39
+ this.#file = file;
40
+ this.#intent = structuredClone(intent);
41
+ this.#bytes = JSON.stringify(intent);
42
+ this.sha256 = hash(this.#bytes);
43
+ Object.freeze(this);
44
+ }
45
+ static prepare(migration, input) {
46
+ if (!(migration instanceof PreparedStoreWriterMigration) || typeof input.recoveryId !== "string"
47
+ || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(input.recoveryId))
48
+ refuse("unprepared_recovery_scope");
49
+ const reference = migration.recoveryReference();
50
+ if (!path.isAbsolute(input.requestFile) || path.join(fs.realpathSync(path.dirname(input.requestFile)), path.basename(input.requestFile)) !== input.requestFile)
51
+ refuse("noncanonical_recovery_path");
52
+ const paths = [reference.database, reference.backup].flatMap(file => [file, file + "-wal", file + "-shm", file + "-journal"]);
53
+ if (paths.includes(input.requestFile) || paths.includes(input.requestFile + ".result.json"))
54
+ refuse("recovery_path_alias");
55
+ for (const file of [input.requestFile, input.requestFile + ".result.json"]) {
56
+ try {
57
+ fs.lstatSync(file);
58
+ }
59
+ catch (error) {
60
+ if (error.code === "ENOENT")
61
+ continue;
62
+ throw error;
63
+ }
64
+ refuse("restore_existing_recovery_request");
65
+ }
66
+ const backup = new DatabaseSync(reference.backup, { readOnly: true });
67
+ const restored = withStoreCleanup(() => {
68
+ backup.exec("BEGIN");
69
+ migration.assertBackup(backup);
70
+ supported(backup);
71
+ if (backup.prepare("PRAGMA integrity_check").get()?.integrity_check !== "ok" || backup.prepare("PRAGMA foreign_key_check").get())
72
+ refuse("backup_invalid");
73
+ return hash(JSON.stringify(state(backup)));
74
+ }, () => backup.close());
75
+ const current = new DatabaseSync(reference.database, { readOnly: true });
76
+ const before = withStoreCleanup(() => {
77
+ current.exec("BEGIN");
78
+ assertStoreWriterDrain(current);
79
+ supported(current);
80
+ return hash(JSON.stringify(state(current)));
81
+ }, () => current.close());
82
+ return new PreparedStoreWriterRestore(migration, input.requestFile, { schema_id: "dd-flow/store-writer-restore-request@1", recovery_id: input.recoveryId,
83
+ ...reference, backup_sha256: hash(fs.readFileSync(reference.backup)), before_sha256: before, restored_sha256: restored });
84
+ }
85
+ static restore(migration, file, expectedSha256) {
86
+ const stat = fs.lstatSync(file);
87
+ if (!stat.isFile() || stat.size > 16 * 1024)
88
+ refuse("invalid_recovery_request");
89
+ const bytes = fs.readFileSync(file, "utf8");
90
+ migration.recoveryReference();
91
+ return this.restoreInput(migration, file, bytes, expectedSha256);
92
+ }
93
+ static restoreInput(migration, file, bytes, expectedSha256) {
94
+ if (!(migration instanceof PreparedStoreWriterMigration) || !path.isAbsolute(file) || Buffer.byteLength(bytes) > 16 * 1024)
95
+ refuse("invalid_recovery_request");
96
+ if (!/^[a-f0-9]{64}$/.test(expectedSha256) || hash(bytes) !== expectedSha256)
97
+ refuse("recovery_request_changed");
98
+ const original = JSON.parse(Buffer.from(migration.inputArtifact().base64, "base64").toString("utf8"));
99
+ const value = JSON.parse(bytes), reference = { database: original.database, backup: original.backup, migration_sha256: migration.sha256 };
100
+ if (!value || Object.keys(value).sort().join() !== "backup,backup_sha256,before_sha256,database,migration_sha256,recovery_id,restored_sha256,schema_id"
101
+ || value.schema_id !== "dd-flow/store-writer-restore-request@1" || value.database !== reference.database || value.backup !== reference.backup
102
+ || value.migration_sha256 !== reference.migration_sha256 || typeof value.recovery_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,95}$/.test(value.recovery_id)
103
+ || [value.backup_sha256, value.before_sha256, value.restored_sha256].some(digest => typeof digest !== "string" || !/^[a-f0-9]{64}$/.test(digest)))
104
+ refuse("foreign_recovery_request");
105
+ const prepared = new PreparedStoreWriterRestore(migration, file, value);
106
+ if (prepared.sha256 !== expectedSha256)
107
+ refuse("noncanonical_recovery_request");
108
+ return prepared;
109
+ }
110
+ inputArtifact() { return { path: this.#file, sha256: this.sha256, base64: Buffer.from(this.#bytes).toString("base64") }; }
111
+ retain() {
112
+ publishImmutableInput(this.#file, Buffer.from(this.#bytes), "store_restore_request_conflict");
113
+ return { request_file: this.#file, sha256: this.sha256, recovery_id: this.#intent.recovery_id };
114
+ }
115
+ assertInputs(retained = true) {
116
+ this.#migration.recoveryReference();
117
+ if (["-wal", "-shm", "-journal"].some(suffix => fs.existsSync(this.#intent.backup + suffix)))
118
+ refuse("backup_has_live_sidecars");
119
+ if (retained && hash(fs.readFileSync(this.#file)) !== this.sha256 || hash(fs.readFileSync(this.#intent.backup)) !== this.#intent.backup_sha256)
120
+ refuse("recovery_inputs_changed");
121
+ }
122
+ observe(retained) {
123
+ try {
124
+ this.assertInputs(retained);
125
+ const db = new DatabaseSync(this.#intent.database, { readOnly: true });
126
+ return withStoreCleanup(() => {
127
+ db.exec("BEGIN");
128
+ const actual = hash(JSON.stringify(state(db)));
129
+ if (actual === this.#intent.restored_sha256)
130
+ return { status: "restored" };
131
+ return actual === this.#intent.before_sha256 ? { status: "not_restored" } : { status: "unknown", reason: "target_changed" };
132
+ }, () => db.close());
133
+ }
134
+ catch (error) {
135
+ return { status: "unknown", reason: error instanceof AppError ? String(error.details.reason ?? error.code) : "outcome_unavailable" };
136
+ }
137
+ }
138
+ reconcile() { return this.observe(true); }
139
+ /** Check a prepared child before a paired parent publishes recovery custody. */
140
+ revalidatePrepared() { const observed = this.observe(false); if (observed.status === "unknown")
141
+ refuse(observed.reason ?? "target_changed"); }
142
+ finish() {
143
+ const receipt = { schema_id: "dd-flow/store-writer-restore-receipt@1", recovery_id: this.#intent.recovery_id, request_sha256: this.sha256,
144
+ migration_sha256: this.#intent.migration_sha256, database: this.#intent.database, backup: this.#intent.backup, backup_sha256: this.#intent.backup_sha256,
145
+ restored_sha256: this.#intent.restored_sha256, status: "restored", changed: this.#intent.before_sha256 !== this.#intent.restored_sha256 };
146
+ try {
147
+ publishImmutableInput(this.#file + ".result.json", Buffer.from(JSON.stringify(receipt)), "store_restore_receipt_conflict");
148
+ }
149
+ catch (error) {
150
+ throw new AppError("store_restore_publication_pending", "Database restoration is committed; reconcile the same recovery to publish its receipt", 1, { effect: "committed", reason: error instanceof AppError ? error.code : "receipt_publication_failed", next_action: "reconcile_exact_recovery" });
151
+ }
152
+ return receipt;
153
+ }
154
+ apply() {
155
+ this.retain();
156
+ const observed = this.reconcile();
157
+ if (observed.status === "unknown")
158
+ refuse(observed.reason ?? "outcome_unavailable");
159
+ if (observed.status === "restored")
160
+ return this.finish();
161
+ let committed = false;
162
+ const backup = new DatabaseSync(this.#intent.backup, { readOnly: true });
163
+ withStoreCleanup(() => {
164
+ backup.exec("BEGIN");
165
+ this.#migration.assertBackup(backup);
166
+ supported(backup);
167
+ const db = new DatabaseSync(this.#intent.database);
168
+ withStoreCleanup(() => {
169
+ db.exec("PRAGMA busy_timeout=1000; PRAGMA foreign_keys=OFF");
170
+ registerReconciliationWriter(db);
171
+ db.exec("BEGIN EXCLUSIVE");
172
+ try {
173
+ this.assertInputs();
174
+ assertStoreWriterDrain(db);
175
+ if (hash(JSON.stringify(state(db))) !== this.#intent.before_sha256)
176
+ refuse("target_changed_before_restore");
177
+ supported(db);
178
+ for (const row of db.prepare("SELECT name FROM sqlite_schema WHERE type='view'").all())
179
+ db.exec(`DROP VIEW ${quote(String(row.name))}`);
180
+ for (const row of db.prepare("SELECT name FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%'").all())
181
+ db.exec(`DROP TABLE ${quote(String(row.name))}`);
182
+ for (const row of backup.prepare("SELECT name,sql FROM sqlite_schema WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all()) {
183
+ const name = String(row.name);
184
+ db.exec(String(row.sql));
185
+ const columns = backup.prepare(`PRAGMA table_info(${quote(name)})`).all().map(column => String(column.name));
186
+ const read = backup.prepare(`SELECT ${columns.map(quote).join(",")} FROM ${quote(name)}`);
187
+ read.setReadBigInts(true);
188
+ const insert = db.prepare(`INSERT INTO ${quote(name)} (${columns.map(quote).join(",")}) VALUES (${columns.map(() => "?").join(",")})`);
189
+ for (const values of read.iterate())
190
+ insert.run(...columns.map(column => values[column]));
191
+ }
192
+ if (db.prepare("SELECT 1 FROM sqlite_schema WHERE name='sqlite_sequence'").get()) {
193
+ db.exec("DELETE FROM sqlite_sequence");
194
+ if (backup.prepare("SELECT 1 FROM sqlite_schema WHERE name='sqlite_sequence'").get()) {
195
+ const insert = db.prepare("INSERT INTO sqlite_sequence(name,seq) VALUES (?,?)"), read = backup.prepare("SELECT name,seq FROM sqlite_sequence");
196
+ read.setReadBigInts(true);
197
+ for (const row of read.iterate())
198
+ insert.run(row.name, row.seq);
199
+ }
200
+ }
201
+ for (const row of backup.prepare("SELECT sql FROM sqlite_schema WHERE type IN ('index','view','trigger') AND sql IS NOT NULL ORDER BY CASE type WHEN 'index' THEN 0 WHEN 'view' THEN 1 ELSE 2 END,name").all())
202
+ db.exec(String(row.sql));
203
+ for (const pragma of ["user_version", "application_id"])
204
+ db.exec(`PRAGMA ${pragma}=${Number(backup.prepare(`PRAGMA ${pragma}`).get()[pragma])}`);
205
+ if (hash(JSON.stringify(state(db))) !== this.#intent.restored_sha256 || db.prepare("PRAGMA foreign_key_check").get())
206
+ refuse("restored_readback_failed");
207
+ db.exec("COMMIT");
208
+ committed = true;
209
+ }
210
+ catch (error) {
211
+ rollbackStoreTransaction(db, "ROLLBACK", error);
212
+ }
213
+ }, () => db.close(), () => ({ effect: committed ? "committed" : "uncommitted", database: this.#intent.database }));
214
+ }, () => backup.close(), () => ({ effect: committed ? "committed" : "uncommitted", database: this.#intent.database }));
215
+ return this.finish();
216
+ }
217
+ }