@astrosheep/keiyaku 2.9.9 → 2.9.11

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 (230) hide show
  1. package/README.md +93 -174
  2. package/build/.tsbuildinfo +1 -1
  3. package/build/agents/harness/control-types.js +18 -1
  4. package/build/agents/harness/index.js +2 -0
  5. package/build/agents/harness/outcome-error.js +81 -0
  6. package/build/agents/harness/projection.js +2 -1
  7. package/build/agents/harness/router.js +5 -1
  8. package/build/agents/providers/claude-agent-sdk/adapter.js +18 -4
  9. package/build/agents/providers/claude-agent-sdk/registration.js +11 -1
  10. package/build/agents/providers/claude-agent-sdk/session.js +19 -5
  11. package/build/agents/providers/codex-app-server/adapter.js +11 -6
  12. package/build/agents/providers/codex-app-server/events.js +67 -6
  13. package/build/agents/providers/codex-app-server/registration.js +11 -1
  14. package/build/agents/providers/opencode-sdk/adapter.js +8 -1
  15. package/build/agents/providers/opencode-sdk/events.js +24 -2
  16. package/build/agents/providers/opencode-sdk/registration.js +11 -1
  17. package/build/agents/providers/pi/checkpoint.js +9 -1
  18. package/build/agents/providers/pi/events.js +2 -0
  19. package/build/agents/providers/pi/registration.js +14 -1
  20. package/build/cli/commands/akuma/akuma/handler.js +1 -1
  21. package/build/cli/commands/akuma/akuma/meta.js +3 -3
  22. package/build/cli/commands/akuma/catalog.js +2 -2
  23. package/build/cli/commands/akuma/{view → show}/handler.js +5 -5
  24. package/build/cli/commands/akuma/{view → show}/meta.js +3 -3
  25. package/build/cli/commands/akuma.js +1 -1
  26. package/build/cli/commands/contract/amend/handler.js +5 -4
  27. package/build/cli/commands/contract/arc/handler.js +5 -4
  28. package/build/cli/commands/contract/audit/handler.js +2 -2
  29. package/build/cli/commands/contract/bind/handler.js +12 -3
  30. package/build/cli/commands/contract/bind/meta.js +45 -15
  31. package/build/cli/commands/contract/forfeit/handler.js +9 -5
  32. package/build/cli/commands/contract/log/handler.js +2 -2
  33. package/build/cli/commands/contract/petition/handler.js +34 -7
  34. package/build/cli/commands/contract/renew/handler.js +7 -7
  35. package/build/cli/commands/contract/renew/meta.js +3 -3
  36. package/build/cli/commands/metadata.js +10 -6
  37. package/build/cli/commands/projection/call/meta.js +1 -1
  38. package/build/cli/commands/projection/catalog.js +2 -0
  39. package/build/cli/commands/projection/history/handler.js +21 -0
  40. package/build/cli/commands/projection/history/meta.js +9 -0
  41. package/build/cli/commands/projection/shared.js +3 -4
  42. package/build/cli/commands/projection/status/handler.js +3 -3
  43. package/build/cli/commands/projection/wait/handler.js +20 -11
  44. package/build/cli/commands/task/add/handler.js +14 -5
  45. package/build/cli/commands/task/add/meta.js +2 -2
  46. package/build/cli/commands/task/catalog.js +4 -0
  47. package/build/cli/commands/task/done/handler.js +1 -4
  48. package/build/cli/commands/task/done/meta.js +1 -1
  49. package/build/cli/commands/task/drop/meta.js +1 -1
  50. package/build/cli/commands/task/hold/handler.js +3 -0
  51. package/build/cli/commands/task/hold/meta.js +8 -0
  52. package/build/cli/commands/task/log/handler.js +1 -1
  53. package/build/cli/commands/task/log/meta.js +2 -2
  54. package/build/cli/commands/task/resume/handler.js +3 -0
  55. package/build/cli/commands/task/resume/meta.js +8 -0
  56. package/build/cli/commands/task/shared.js +39 -14
  57. package/build/cli/commands/task/show/handler.js +1 -1
  58. package/build/cli/commands/task/show/meta.js +1 -1
  59. package/build/cli/commands/task/start/meta.js +1 -1
  60. package/build/cli/commands/task/stop/handler.js +1 -4
  61. package/build/cli/commands/task/stop/meta.js +1 -1
  62. package/build/cli/commands/task/update/handler.js +11 -3
  63. package/build/cli/commands/task/update/meta.js +3 -3
  64. package/build/cli/completion.js +3 -2
  65. package/build/cli/flags.js +51 -14
  66. package/build/cli/help.js +4 -1
  67. package/build/cli/index.js +40 -25
  68. package/build/cli/parse-flags.js +40 -7
  69. package/build/cli/parse-metadata.js +5 -2
  70. package/build/cli/parse-selectors.js +2 -3
  71. package/build/cli/parse.js +40 -6
  72. package/build/cli/projection-address.js +12 -10
  73. package/build/cli/render/amendment-warning.js +8 -0
  74. package/build/cli/render/arc.js +6 -2
  75. package/build/cli/render/audit.js +400 -81
  76. package/build/cli/render/kanshi.js +6 -1
  77. package/build/cli/render/lifecycle.js +56 -0
  78. package/build/cli/render/misc.js +24 -28
  79. package/build/cli/render/petition.js +5 -0
  80. package/build/cli/render/projection-activity.js +3 -2
  81. package/build/cli/render/projection-history.js +64 -0
  82. package/build/cli/render/shared.js +23 -9
  83. package/build/cli/render/status.js +9 -16
  84. package/build/cli/render/task.js +51 -26
  85. package/build/cli/render/terminal-failure.js +3 -1
  86. package/build/cli/render/wait.js +3 -5
  87. package/build/cli/subagent-guard.js +1 -1
  88. package/build/cli/types.js +1 -1
  89. package/build/config/env-keys.js +2 -0
  90. package/build/config/env.js +23 -0
  91. package/build/config/settings/schema.js +3 -1
  92. package/build/core/address-carrier.js +89 -0
  93. package/build/core/addressing.js +255 -196
  94. package/build/core/amend.js +115 -38
  95. package/build/core/amendment-criteria.js +17 -0
  96. package/build/core/amendment-warning.js +10 -0
  97. package/build/core/arc.js +184 -83
  98. package/build/core/audit/candidate.js +110 -9
  99. package/build/core/audit/coordinates.js +120 -65
  100. package/build/core/audit/evidence.js +19 -23
  101. package/build/core/audit/facade.js +34 -11
  102. package/build/core/audit/report.js +5 -5
  103. package/build/core/bind-reconciliation.js +390 -0
  104. package/build/core/bind-workspace.js +12 -20
  105. package/build/core/bind.js +242 -273
  106. package/build/core/call/call.js +1 -0
  107. package/build/core/call/context.js +46 -71
  108. package/build/core/call/prompt.js +6 -3
  109. package/build/core/context.js +49 -35
  110. package/build/core/contract-carrier-runtime.js +408 -0
  111. package/build/core/contract-carrier.js +270 -0
  112. package/build/core/contract-view.js +33 -27
  113. package/build/core/contract.js +5 -1
  114. package/build/core/derived-replay.js +207 -0
  115. package/build/core/draft.js +125 -57
  116. package/build/core/forfeit.js +113 -56
  117. package/build/core/identifier-slug.js +63 -13
  118. package/build/core/ids.js +4 -4
  119. package/build/core/lifecycle-history.js +35 -0
  120. package/build/core/lifecycle-recovery.js +117 -0
  121. package/build/core/lifecycle-runner.js +377 -0
  122. package/build/core/log.js +37 -11
  123. package/build/core/outcome-base.js +181 -0
  124. package/build/core/process-group.js +139 -0
  125. package/build/core/projection/generation/database.js +82 -7
  126. package/build/core/projection/generation/model.js +9 -36
  127. package/build/core/projection/generation/projection-generation-continuation.js +10 -22
  128. package/build/core/projection/generation/projection-generation-execution.js +6 -5
  129. package/build/core/projection/index.js +5 -4
  130. package/build/core/projection/projection-activity.js +27 -8
  131. package/build/core/projection/projection-alias.js +23 -10
  132. package/build/core/projection/projection-core.js +21 -7
  133. package/build/core/projection/projection-execution-observer.js +91 -39
  134. package/build/core/projection/projection-history.js +474 -0
  135. package/build/core/projection/projection-runner-lock.js +12 -0
  136. package/build/core/projection/projection-status-observation.js +365 -0
  137. package/build/core/projection/projection-status.js +21 -358
  138. package/build/core/projection/projection-terminal-failure.js +13 -8
  139. package/build/core/projection/projection-wait.js +7 -2
  140. package/build/core/projection/tell/launch-store.js +244 -0
  141. package/build/core/projection/tell/model.js +3 -3
  142. package/build/core/projection/tell/store.js +70 -118
  143. package/build/core/registry.js +22 -87
  144. package/build/core/renew-build.js +424 -0
  145. package/build/core/renew-plan.js +239 -0
  146. package/build/core/renew-rewrite.js +77 -0
  147. package/build/core/renew.js +295 -520
  148. package/build/core/repository-ledger/accepted-fold-read.js +19 -0
  149. package/build/core/repository-ledger/accepted-tail.js +116 -0
  150. package/build/core/repository-ledger/admission.js +38 -0
  151. package/build/core/repository-ledger/authoritative-transaction.js +1 -0
  152. package/build/core/repository-ledger/claim-evidence.js +147 -0
  153. package/build/core/repository-ledger/codec.js +297 -0
  154. package/build/core/repository-ledger/current-state-store.js +853 -0
  155. package/build/core/repository-ledger/fold-repository.js +375 -0
  156. package/build/core/repository-ledger/fold.js +248 -0
  157. package/build/core/repository-ledger/git-object-store.js +243 -0
  158. package/build/core/repository-ledger/history-read.js +15 -0
  159. package/build/core/repository-ledger/incremental-read.js +153 -0
  160. package/build/core/repository-ledger/index.js +15 -0
  161. package/build/core/repository-ledger/inventory-plan.js +75 -0
  162. package/build/core/repository-ledger/inventory.js +310 -0
  163. package/build/core/repository-ledger/publication-admission.js +125 -0
  164. package/build/core/repository-ledger/publication.js +353 -0
  165. package/build/core/repository-ledger/read-model.js +469 -0
  166. package/build/core/repository-ledger/transaction-membership.js +78 -0
  167. package/build/core/repository-ledger/traversal.js +404 -0
  168. package/build/core/repository-ledger/write-transaction.js +339 -0
  169. package/build/core/seal.js +168 -161
  170. package/build/core/settlement/claim-delivery.js +42 -215
  171. package/build/core/settlement/claim.js +235 -153
  172. package/build/core/settlement/index.js +4 -4
  173. package/build/core/settlement/petition-claim-gates.js +52 -33
  174. package/build/core/settlement/petition-forfeit.js +12 -5
  175. package/build/core/settlement/petition-head-guard.js +8 -5
  176. package/build/core/settlement/petition-preview.js +68 -56
  177. package/build/core/settlement/petition-window.js +50 -0
  178. package/build/core/settlement/petition.js +252 -131
  179. package/build/core/settlement/queue-read-model.js +72 -95
  180. package/build/core/settlement/queue-seat-allocation.js +35 -126
  181. package/build/core/settlement/queue.js +1 -1
  182. package/build/core/settlement/settlement.js +172 -80
  183. package/build/core/settlement/verdict.js +112 -66
  184. package/build/core/settlement/verification-coordination.js +305 -0
  185. package/build/core/settlement/verification-supervisor.js +275 -0
  186. package/build/core/settlement/verification.js +398 -31
  187. package/build/core/status/board.js +114 -119
  188. package/build/core/status/lifecycle.js +7 -80
  189. package/build/core/structured-query.js +23 -0
  190. package/build/core/target-ref.js +1 -1
  191. package/build/core/task/board.js +59 -24
  192. package/build/core/task/commands.js +181 -33
  193. package/build/core/task/coordinate.js +56 -0
  194. package/build/core/task/document.js +25 -9
  195. package/build/core/task/index.js +7 -4
  196. package/build/core/task/query.js +17 -27
  197. package/build/core/task/settlement-git.js +164 -179
  198. package/build/core/task/settlement-policy.js +15 -11
  199. package/build/core/task/source-board.js +31 -13
  200. package/build/core/task/task-bind-preparation.js +193 -0
  201. package/build/core/task/task-contract.js +102 -193
  202. package/build/core/task/task-file-transaction.js +171 -0
  203. package/build/core/task/task-git-runtime.js +76 -14
  204. package/build/core/task/task-git-store.js +68 -169
  205. package/build/core/task/task-store-repository.js +13 -6
  206. package/build/core/task/task-worktree.js +6 -33
  207. package/build/core/task/task.js +1 -1
  208. package/build/core/task/tree.js +3 -3
  209. package/build/core/task/validation.js +4 -3
  210. package/build/core/transcripts.js +93 -1
  211. package/build/core/worktree-bootstrap.js +44 -36
  212. package/build/core/worktree-path.js +210 -77
  213. package/build/flow-error.js +7 -7
  214. package/build/generated/version.js +2 -2
  215. package/build/git/core.js +76 -13
  216. package/build/git/refs.js +168 -46
  217. package/build/git/streaming-batch.js +73 -13
  218. package/package.json +1 -1
  219. package/skills/keiyaku/SKILL.md +1 -1
  220. package/skills/keiyaku-akuma/SKILL.md +3 -3
  221. package/skills/keiyaku-task/SKILL.md +4 -2
  222. package/skills/keiyaku-workflow/SKILL.md +88 -203
  223. package/build/core/entry.js +0 -305
  224. package/build/core/ledger-batch.js +0 -194
  225. package/build/core/ledger.js +0 -84
  226. package/build/core/projection/leash.js +0 -87
  227. package/build/core/ref-log.js +0 -119
  228. package/build/core/renew-session.js +0 -128
  229. package/build/core/status/ledger-batch.js +0 -1
  230. package/build/core/status/reconciliation.js +0 -137
@@ -0,0 +1,853 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import { runGitProcess } from "../../git/core.js";
6
+ import { applyFoldEntry } from "./fold.js";
7
+ import { createLifecycleEntrySchema, evidenceObjectId, repositoryDeliveryRef } from "./codec.js";
8
+ export const REPOSITORY_CURRENT_STATE_VERSION = 3;
9
+ export const REPOSITORY_CURRENT_STATE_FILE = "keiyaku-current-state.sqlite";
10
+ const SNAPSHOT_WRITE_BUSY_MS = 5_000;
11
+ const SCHEMA = `
12
+ CREATE TABLE snapshot_schema (
13
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
14
+ version INTEGER NOT NULL CHECK (version = ${REPOSITORY_CURRENT_STATE_VERSION})
15
+ ) STRICT;
16
+ CREATE TABLE snapshot_pin (
17
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
18
+ ledger_head TEXT,
19
+ object_format TEXT CHECK (object_format IS NULL OR object_format IN ('sha1', 'sha256')),
20
+ empty_tree TEXT
21
+ ) STRICT;
22
+ CREATE TABLE snapshot_contracts (
23
+ contract TEXT PRIMARY KEY CHECK (length(contract) > 0),
24
+ status TEXT NOT NULL CHECK (status IN ('healthy', 'diseased', 'isolated')),
25
+ current_json TEXT CHECK (current_json IS NULL OR json_valid(current_json)),
26
+ fold_json TEXT CHECK (fold_json IS NULL OR json_valid(fold_json)),
27
+ diagnostics_json TEXT NOT NULL CHECK (json_valid(diagnostics_json))
28
+ ) STRICT;
29
+ CREATE TABLE snapshot_history (
30
+ contract TEXT NOT NULL,
31
+ position INTEGER NOT NULL CHECK (position >= 0),
32
+ object_id TEXT NOT NULL,
33
+ entry_json TEXT NOT NULL CHECK (json_valid(entry_json)),
34
+ PRIMARY KEY(contract, position),
35
+ UNIQUE(object_id)
36
+ ) STRICT;
37
+ CREATE TABLE snapshot_summaries (
38
+ contract TEXT PRIMARY KEY,
39
+ history_json TEXT NOT NULL CHECK (json_valid(history_json))
40
+ ) STRICT;
41
+ CREATE TABLE snapshot_repository (
42
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
43
+ diagnostics_json TEXT NOT NULL CHECK (json_valid(diagnostics_json))
44
+ ) STRICT;
45
+ CREATE TABLE snapshot_topology (
46
+ object_id TEXT PRIMARY KEY,
47
+ node_json TEXT NOT NULL CHECK (json_valid(node_json))
48
+ ) STRICT;
49
+ CREATE TABLE snapshot_resources (
50
+ kind TEXT NOT NULL CHECK (kind IN ('worktree', 'task')),
51
+ resource_key TEXT NOT NULL,
52
+ contract TEXT NOT NULL,
53
+ PRIMARY KEY(kind, resource_key, contract)
54
+ ) STRICT;
55
+ CREATE TABLE snapshot_integrity (
56
+ receipt_key TEXT PRIMARY KEY,
57
+ digest TEXT NOT NULL CHECK (length(digest) = 64)
58
+ ) STRICT;
59
+ `;
60
+ function digest(value) {
61
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
62
+ }
63
+ function exactRows(database, sql, ...params) {
64
+ return database.prepare(sql).all(...params);
65
+ }
66
+ function pinDigest(pin) {
67
+ return digest(["keiyaku-current-state-v3", REPOSITORY_CURRENT_STATE_VERSION, pin.head, pin.objectFormat, pin.emptyTree]);
68
+ }
69
+ function repositoryDigest(database) {
70
+ return digest(["repository", exactRows(database, "SELECT singleton, diagnostics_json FROM snapshot_repository ORDER BY singleton")]);
71
+ }
72
+ function topologyDigest(database) {
73
+ return digest(["topology", exactRows(database, "SELECT object_id, node_json FROM snapshot_topology ORDER BY object_id")]);
74
+ }
75
+ function resourcesDigest(database, contract) {
76
+ return digest(["resources", contract ?? null, contract === undefined
77
+ ? exactRows(database, "SELECT kind, resource_key, contract FROM snapshot_resources ORDER BY kind, resource_key, contract")
78
+ : exactRows(database, "SELECT kind, resource_key, contract FROM snapshot_resources WHERE contract = ? ORDER BY kind, resource_key", contract)]);
79
+ }
80
+ function contractDigest(database, contract, summaryOnly) {
81
+ const contractRows = summaryOnly
82
+ ? exactRows(database, "SELECT contract, status, current_json, diagnostics_json FROM snapshot_contracts WHERE contract = ?", contract)
83
+ : exactRows(database, "SELECT contract, status, current_json, fold_json, diagnostics_json FROM snapshot_contracts WHERE contract = ?", contract);
84
+ const historyRows = summaryOnly
85
+ ? exactRows(database, "SELECT contract, history_json FROM snapshot_summaries WHERE contract = ?", contract)
86
+ : exactRows(database, "SELECT contract, position, object_id, entry_json FROM snapshot_history WHERE contract = ? ORDER BY position", contract);
87
+ return digest([summaryOnly ? "contract-summary" : "contract-full", contract, contractRows, historyRows, resourcesDigest(database, contract)]);
88
+ }
89
+ function receipt(database, key) {
90
+ return database.prepare("SELECT digest FROM snapshot_integrity WHERE receipt_key = ?").get(key)?.digest ?? null;
91
+ }
92
+ function assertReceipt(database, key, actual) {
93
+ if (receipt(database, key) !== actual)
94
+ throw new Error(`snapshot integrity receipt ${key} is missing or mismatched`);
95
+ }
96
+ function writeIntegrityReceipts(database) {
97
+ database.exec("DELETE FROM snapshot_integrity");
98
+ const insert = database.prepare("INSERT INTO snapshot_integrity(receipt_key, digest) VALUES (?, ?)");
99
+ const pin = metadata(database);
100
+ if (!pin)
101
+ throw new Error("snapshot pin is missing while signing current state");
102
+ insert.run("pin", pinDigest(pin));
103
+ insert.run("repository", repositoryDigest(database));
104
+ insert.run("topology", topologyDigest(database));
105
+ insert.run("resources", resourcesDigest(database));
106
+ const contracts = database.prepare("SELECT contract FROM snapshot_contracts ORDER BY contract").all().map((row) => row.contract);
107
+ for (const contract of contracts) {
108
+ insert.run(`contract:${contract}:full`, contractDigest(database, contract, false));
109
+ insert.run(`contract:${contract}:summary`, contractDigest(database, contract, true));
110
+ }
111
+ insert.run("contracts", digest(["contracts", contracts.map((contract) => [contract, receipt(database, `contract:${contract}:full`), receipt(database, `contract:${contract}:summary`)])]));
112
+ }
113
+ function validatePinReceipt(database, pin) {
114
+ assertReceipt(database, "pin", pinDigest(pin));
115
+ }
116
+ function validateContractReceipt(database, contract, summaryOnly) {
117
+ assertReceipt(database, `contract:${contract}:${summaryOnly ? "summary" : "full"}`, contractDigest(database, contract, summaryOnly));
118
+ }
119
+ function validateRepositoryReceipts(database, contracts, summaryOnly, includeTopology) {
120
+ assertReceipt(database, "repository", repositoryDigest(database));
121
+ assertReceipt(database, "resources", resourcesDigest(database));
122
+ for (const contract of contracts)
123
+ validateContractReceipt(database, contract, summaryOnly);
124
+ const signedContracts = contracts.map((contract) => [contract, receipt(database, `contract:${contract}:full`), receipt(database, `contract:${contract}:summary`)]);
125
+ assertReceipt(database, "contracts", digest(["contracts", signedContracts]));
126
+ if (includeTopology)
127
+ assertReceipt(database, "topology", topologyDigest(database));
128
+ }
129
+ async function snapshotPath(cwd, signal) {
130
+ // Addressing callers already pass the resolved primary worktree root. Reuse
131
+ // that physical coordinate within this request instead of probing Git again.
132
+ const localGitDirectory = path.join(cwd, ".git");
133
+ try {
134
+ if ((await fs.stat(localGitDirectory)).isDirectory()) {
135
+ return path.join(await fs.realpath(localGitDirectory), REPOSITORY_CURRENT_STATE_FILE);
136
+ }
137
+ }
138
+ catch {
139
+ // A subdirectory or linked-worktree .git file still needs Git's common-dir witness.
140
+ }
141
+ const result = await runGitProcess(cwd, ["rev-parse", "--path-format=absolute", "--git-common-dir"], { signal });
142
+ if (result.error?.name === "AbortError")
143
+ throw result.error;
144
+ const commonDir = result.stdout.trim();
145
+ if (result.status !== 0 || result.error || !path.isAbsolute(commonDir)) {
146
+ throw new Error(`cannot resolve repository common directory: ${result.stderr.trim() || result.error?.message || `exit ${String(result.status)}`}`);
147
+ }
148
+ return path.join(commonDir, REPOSITORY_CURRENT_STATE_FILE);
149
+ }
150
+ function openValidated(databasePath) {
151
+ let database;
152
+ try {
153
+ database = new DatabaseSync(databasePath);
154
+ database.exec("PRAGMA query_only = ON");
155
+ const rows = database.prepare("SELECT version FROM snapshot_schema WHERE singleton = 1").all();
156
+ if (rows.length !== 1 || rows[0]?.version !== REPOSITORY_CURRENT_STATE_VERSION) {
157
+ database.close();
158
+ return null;
159
+ }
160
+ return database;
161
+ }
162
+ catch {
163
+ try {
164
+ database.close();
165
+ }
166
+ catch { /* discarded below */ }
167
+ return null;
168
+ }
169
+ }
170
+ async function discard(databasePath) {
171
+ await Promise.all(["", "-wal", "-shm"].map(async (suffix) => {
172
+ try {
173
+ await fs.unlink(`${databasePath}${suffix}`);
174
+ }
175
+ catch (error) {
176
+ if (error.code !== "ENOENT")
177
+ throw error;
178
+ }
179
+ }));
180
+ }
181
+ function parseJson(text) {
182
+ return JSON.parse(text);
183
+ }
184
+ function parseDiagnostics(text, contract) {
185
+ const value = JSON.parse(text);
186
+ if (!Array.isArray(value) || value.some((item) => !isRecord(item)
187
+ || typeof item.objectId !== "string" || typeof item.code !== "string" || typeof item.detail !== "string"
188
+ || (contract !== undefined && item.contract !== contract))) {
189
+ throw new Error("snapshot diagnostics are invalid or disagree with their owner");
190
+ }
191
+ return value;
192
+ }
193
+ function isRecord(value) {
194
+ return typeof value === "object" && value !== null && !Array.isArray(value);
195
+ }
196
+ function assertClosedKeys(value, keys, label) {
197
+ const actual = Object.keys(value).sort();
198
+ const expected = [...keys].sort();
199
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) {
200
+ throw new Error(`${label} has an unknown or missing field`);
201
+ }
202
+ }
203
+ function parseCurrent(text, contract) {
204
+ const value = JSON.parse(text);
205
+ if (!isRecord(value))
206
+ throw new Error("snapshot current facts are not an object");
207
+ assertClosedKeys(value, ["observedLedgerHead", "contract", "phase", "intent", "receiptSegments", "seal", "bootstrapReplay", "petition", "verdict", "pins", "effectiveScope", "causal", "resources"], "snapshot current facts");
208
+ if (value.contract !== contract || typeof value.observedLedgerHead !== "string" || !isRecord(value.resources)) {
209
+ throw new Error("snapshot current facts disagree with their row key");
210
+ }
211
+ return value;
212
+ }
213
+ function readHistory(database, contract, summaryOnly = false, objectFormat = "sha1") {
214
+ if (summaryOnly) {
215
+ const row = database.prepare("SELECT history_json AS historyJson FROM snapshot_summaries WHERE contract = ?").get(contract);
216
+ if (!row)
217
+ throw new Error("snapshot current summary is missing");
218
+ const value = JSON.parse(row.historyJson);
219
+ if (!Array.isArray(value) || value.some((item) => !isRecord(item) || typeof item.objectId !== "string" || !createLifecycleEntrySchema(objectFormat).safeParse(item.entry).success || item.entry.contract !== contract)) {
220
+ throw new Error("snapshot current summary is invalid");
221
+ }
222
+ return value;
223
+ }
224
+ const rows = database.prepare("SELECT position, object_id AS objectId, entry_json AS entryJson FROM snapshot_history WHERE contract = ? ORDER BY position").all(contract);
225
+ if (!summaryOnly && rows.some((row, index) => row.position !== index))
226
+ throw new Error("snapshot lifecycle history positions are not contiguous");
227
+ const history = rows.map((row) => {
228
+ const parsed = createLifecycleEntrySchema(objectFormat).safeParse(JSON.parse(row.entryJson));
229
+ if (!parsed.success || parsed.data.contract !== contract)
230
+ throw new Error("snapshot lifecycle history row is invalid");
231
+ return { objectId: row.objectId, entry: parsed.data };
232
+ });
233
+ return history;
234
+ }
235
+ function summaryHistory(history) {
236
+ return history.filter(({ entry }, index) => entry.kind === "bind" || entry.kind === "petition" || index === history.length - 1)
237
+ .filter((row, index, rows) => rows.findIndex((candidate) => candidate.objectId === row.objectId) === index);
238
+ }
239
+ function currentModel(database, contract, currentJson, summaryOnly = false, objectFormat = "sha1") {
240
+ return { ...parseCurrent(currentJson, contract), history: readHistory(database, contract, summaryOnly, objectFormat) };
241
+ }
242
+ function validateContractConsistency(database, input, pin, summaryOnly = false) {
243
+ if ((input.currentJson === null) !== (input.foldJson === null))
244
+ throw new Error("snapshot current/fold presence disagrees");
245
+ if (input.status !== "isolated" && input.currentJson === null)
246
+ throw new Error("non-isolated snapshot row has no current facts");
247
+ if (input.currentJson === null) {
248
+ if (readHistory(database, input.contract).length !== 0)
249
+ throw new Error("isolated snapshot row retains lifecycle history");
250
+ return null;
251
+ }
252
+ if (!pin.objectFormat)
253
+ throw new Error("snapshot contract row has no object format");
254
+ const model = currentModel(database, input.contract, input.currentJson, summaryOnly, pin.objectFormat);
255
+ const diagnosticRow = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_contracts WHERE contract = ?").get(input.contract);
256
+ const diagnostics = parseDiagnostics(diagnosticRow.diagnosticsJson, input.contract);
257
+ if ((input.status === "healthy") !== (diagnostics.length === 0))
258
+ throw new Error("snapshot status disagrees with diagnostics");
259
+ if (summaryOnly) {
260
+ if (model.observedLedgerHead !== pin.head)
261
+ throw new Error("snapshot current summary disagrees with pin");
262
+ return model;
263
+ }
264
+ const fold = JSON.parse(input.foldJson);
265
+ if (!isRecord(fold) || fold.contract !== input.contract || !Array.isArray(fold.entryObjectIds))
266
+ throw new Error("snapshot fold is invalid");
267
+ const objectIds = model.history.map((row) => row.objectId);
268
+ if (fold.entryObjectIds.length !== objectIds.length || fold.entryObjectIds.some((oid, index) => oid !== objectIds[index])) {
269
+ throw new Error("snapshot fold and lifecycle history disagree");
270
+ }
271
+ if (model.observedLedgerHead !== pin.head || model.phase !== fold.phase || model.pins.base !== fold.bind?.data?.base) {
272
+ throw new Error("snapshot current facts disagree with fold or pin");
273
+ }
274
+ const typedFold = fold;
275
+ const expectedWorktree = typedFold.bind.data.workspace === "worktree"
276
+ ? { place: typedFold.bind.data.place, state: typedFold.terminalObjectId === null ? "held" : "released" }
277
+ : null;
278
+ const expectedTasks = (typedFold.bind.data.taskIds ?? []).map((taskId) => ({ taskId, state: typedFold.terminal?.kind === "forfeit" ? "released" : "held" }));
279
+ const petitionRow = [...model.history].reverse().find(({ entry }) => entry.kind === "petition");
280
+ const verdictRow = [...model.history].reverse().find(({ entry }) => entry.kind === "verdict");
281
+ const arcRow = [...model.history].reverse().find(({ entry }) => entry.kind === "arc");
282
+ const expectedIntent = arcRow?.entry.kind === "arc"
283
+ ? { arc: arcRow.entry.data.arc, openedBy: arcRow.objectId, title: arcRow.entry.data.title, objective: arcRow.entry.data.objective, brief: arcRow.entry.data.brief }
284
+ : { arc: 0, openedBy: typedFold.entryObjectIds[0], title: typedFold.bind.data.title, objective: typedFold.bind.data.objective, brief: null };
285
+ const expectedPetition = typedFold.terminalObjectId !== null || !typedFold.petition || !petitionRow || petitionRow.entry.kind !== "petition"
286
+ ? { state: "none" }
287
+ : {
288
+ state: typedFold.phase === "approved" && typedFold.verdict ? "approved" : "awaiting-verdict",
289
+ objectId: petitionRow.objectId,
290
+ intent: typedFold.petition.data.intent,
291
+ seat: typedFold.petition.data.seat,
292
+ target: typedFold.petition.data.target,
293
+ targetHead: typedFold.petition.data.intent === "claim" ? typedFold.petition.data.targetHead : null,
294
+ ...(typedFold.phase === "approved" && typedFold.verdict && verdictRow ? { verdictObjectId: verdictRow.objectId, summary: typedFold.verdict.data.summary } : {}),
295
+ };
296
+ const expectedVerdict = verdictRow?.entry.kind === "verdict"
297
+ ? { objectId: verdictRow.objectId, result: verdictRow.entry.data.result, summary: verdictRow.entry.data.summary }
298
+ : null;
299
+ if (model.pins.delivery !== typedFold.deliveryHead
300
+ || model.pins.target.ref !== typedFold.target
301
+ || model.pins.target.approvedHead !== (typedFold.approvedTargetSnapshot?.targetHead ?? null)
302
+ || JSON.stringify(model.effectiveScope) !== JSON.stringify(typedFold.effectiveScope)
303
+ || JSON.stringify(model.receiptSegments) !== JSON.stringify(typedFold.segments)
304
+ || JSON.stringify(model.seal) !== JSON.stringify(typedFold.segments.at(-1)?.seal ?? null)
305
+ || JSON.stringify(model.intent) !== JSON.stringify(expectedIntent)
306
+ || JSON.stringify(model.petition) !== JSON.stringify(expectedPetition)
307
+ || JSON.stringify(model.verdict) !== JSON.stringify(expectedVerdict)
308
+ || JSON.stringify(model.causal.after) !== JSON.stringify(typedFold.bind.data.after ?? [])
309
+ || model.resources.deliveryRef !== repositoryDeliveryRef(input.contract)
310
+ || JSON.stringify(model.resources.worktree) !== JSON.stringify(expectedWorktree)
311
+ || JSON.stringify(model.resources.tasks) !== JSON.stringify(expectedTasks)) {
312
+ throw new Error("snapshot nested current facts disagree with the accepted fold");
313
+ }
314
+ const actualResources = database.prepare("SELECT kind, resource_key AS resourceKey FROM snapshot_resources WHERE contract = ? ORDER BY kind, resource_key").all(input.contract);
315
+ const expectedResources = [
316
+ ...(model.resources.worktree?.state === "held" ? [{ kind: "worktree", resourceKey: model.resources.worktree.place }] : []),
317
+ ...model.resources.tasks.filter((task) => task.state === "held").map((task) => ({ kind: "task", resourceKey: task.taskId })),
318
+ ].sort((left, right) => left.kind.localeCompare(right.kind) || left.resourceKey.localeCompare(right.resourceKey));
319
+ if (JSON.stringify(actualResources) !== JSON.stringify(expectedResources))
320
+ throw new Error("snapshot resource rows disagree with current facts");
321
+ return model;
322
+ }
323
+ function metadata(database) {
324
+ const rows = database.prepare("SELECT ledger_head AS head, object_format AS objectFormat, empty_tree AS emptyTree FROM snapshot_pin WHERE singleton = 1").all();
325
+ return rows.length === 1 ? rows[0] : null;
326
+ }
327
+ function storedContract(database, contract) {
328
+ const row = database.prepare(`
329
+ SELECT contract, status, current_json AS currentJson, fold_json AS foldJson, diagnostics_json AS diagnosticsJson
330
+ FROM snapshot_contracts WHERE contract = ?
331
+ `).get(contract);
332
+ if (!row)
333
+ return null;
334
+ const pin = metadata(database);
335
+ if (!pin)
336
+ throw new Error("snapshot pin is missing");
337
+ return {
338
+ contract: row.contract,
339
+ status: row.status,
340
+ model: validateContractConsistency(database, row, pin),
341
+ diagnostics: parseDiagnostics(row.diagnosticsJson, contract),
342
+ };
343
+ }
344
+ function atPin(model, head) {
345
+ return model.observedLedgerHead === head ? model : { ...model, observedLedgerHead: head };
346
+ }
347
+ export async function readPinnedCurrentContract(cwd, observedHead, contract, signal) {
348
+ const databasePath = await snapshotPath(cwd, signal);
349
+ const database = openValidated(databasePath);
350
+ if (!database) {
351
+ await discard(databasePath);
352
+ return null;
353
+ }
354
+ try {
355
+ const pin = metadata(database);
356
+ if (!pin || pin.head !== observedHead)
357
+ return null;
358
+ validatePinReceipt(database, pin);
359
+ const row = storedContract(database, contract);
360
+ if (row)
361
+ validateContractReceipt(database, contract, false);
362
+ if (row?.status === "healthy" && row.model)
363
+ return { status: "healthy", model: atPin(row.model, observedHead), diagnostics: [] };
364
+ if (row?.status === "diseased" && row.model)
365
+ return { status: "diseased", model: atPin(row.model, observedHead), diagnostics: row.diagnostics };
366
+ if (row?.status === "isolated")
367
+ return { status: "isolated", contract, diagnostics: row.diagnostics };
368
+ assertReceipt(database, "repository", repositoryDigest(database));
369
+ const repository = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_repository WHERE singleton = 1").get();
370
+ const unattributed = repository ? parseDiagnostics(repository.diagnosticsJson).filter((disease) => disease.contract === undefined) : [];
371
+ return unattributed.length > 0
372
+ ? { status: "indeterminate", contract, diagnostics: unattributed }
373
+ : { status: "absent", contract, diagnostics: [] };
374
+ }
375
+ catch {
376
+ database.close();
377
+ await discard(databasePath);
378
+ return null;
379
+ }
380
+ finally {
381
+ try {
382
+ database.close();
383
+ }
384
+ catch { /* corruption path already discarded */ }
385
+ }
386
+ }
387
+ export async function readRepositoryCurrentStatePin(cwd, signal) {
388
+ const databasePath = await snapshotPath(cwd, signal);
389
+ const database = openValidated(databasePath);
390
+ if (!database) {
391
+ await discard(databasePath);
392
+ return undefined;
393
+ }
394
+ try {
395
+ const pin = metadata(database);
396
+ if (!pin)
397
+ return undefined;
398
+ validatePinReceipt(database, pin);
399
+ return pin.head;
400
+ }
401
+ catch {
402
+ database.close();
403
+ await discard(databasePath);
404
+ return undefined;
405
+ }
406
+ finally {
407
+ try {
408
+ database.close();
409
+ }
410
+ catch { /* corruption path already discarded */ }
411
+ }
412
+ }
413
+ export async function readRepositoryCurrentStateNodeIds(cwd, observedHead, signal) {
414
+ const databasePath = await snapshotPath(cwd, signal);
415
+ const database = openValidated(databasePath);
416
+ if (!database) {
417
+ await discard(databasePath);
418
+ return null;
419
+ }
420
+ try {
421
+ const pin = metadata(database);
422
+ if (!pin || pin.head !== observedHead)
423
+ return null;
424
+ validatePinReceipt(database, pin);
425
+ assertReceipt(database, "topology", topologyDigest(database));
426
+ const rows = database.prepare("SELECT object_id AS objectId, node_json AS nodeJson FROM snapshot_topology").all();
427
+ const ledgerParents = new Map();
428
+ const entryIntroductions = new Map();
429
+ for (const row of rows) {
430
+ const node = JSON.parse(row.nodeJson);
431
+ if (node.objectId !== row.objectId || !Array.isArray(node.ledgerParents) || node.ledgerParents.some((parent) => typeof parent !== "string"))
432
+ throw new Error("snapshot topology row is invalid");
433
+ ledgerParents.set(row.objectId, node.ledgerParents);
434
+ if (node.type === "transaction") {
435
+ if (!Array.isArray(node.entryObjectIds) || node.entryObjectIds.some((entry) => typeof entry !== "string"))
436
+ throw new Error("snapshot transaction topology is invalid");
437
+ for (const entry of node.entryObjectIds) {
438
+ if (entryIntroductions.has(entry))
439
+ throw new Error("snapshot topology repeats an entry introduction");
440
+ entryIntroductions.set(entry, row.objectId);
441
+ }
442
+ }
443
+ }
444
+ return { ledgerParents, entryIntroductions };
445
+ }
446
+ catch {
447
+ database.close();
448
+ await discard(databasePath);
449
+ return null;
450
+ }
451
+ finally {
452
+ try {
453
+ database.close();
454
+ }
455
+ catch { /* corruption path already discarded */ }
456
+ }
457
+ }
458
+ /** Rehydrate the accepted typed topology for publication admission without Git history traversal. */
459
+ export async function readPinnedAcceptedTraversal(cwd, observedHead, signal) {
460
+ const databasePath = await snapshotPath(cwd, signal);
461
+ const database = openValidated(databasePath);
462
+ if (!database) {
463
+ await discard(databasePath);
464
+ return null;
465
+ }
466
+ try {
467
+ const pin = metadata(database);
468
+ if (!pin || pin.head !== observedHead || !pin.objectFormat || !pin.emptyTree)
469
+ return null;
470
+ validatePinReceipt(database, pin);
471
+ assertReceipt(database, "repository", repositoryDigest(database));
472
+ assertReceipt(database, "topology", topologyDigest(database));
473
+ assertReceipt(database, "resources", resourcesDigest(database));
474
+ const repository = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_repository WHERE singleton = 1").get();
475
+ const contractRows = database.prepare("SELECT contract, status, diagnostics_json AS diagnosticsJson FROM snapshot_contracts").all();
476
+ validateRepositoryReceipts(database, contractRows.map((row) => row.contract).sort(), false, true);
477
+ if (!repository || parseDiagnostics(repository.diagnosticsJson).length > 0 || contractRows.some((row) => row.status !== "healthy" || parseDiagnostics(row.diagnosticsJson, row.contract).length > 0))
478
+ return null;
479
+ const historyRows = database.prepare("SELECT object_id AS objectId, entry_json AS entryJson FROM snapshot_history").all();
480
+ const entries = new Map(historyRows.map((row) => {
481
+ const parsed = createLifecycleEntrySchema(pin.objectFormat).parse(JSON.parse(row.entryJson));
482
+ return [row.objectId, parsed];
483
+ }));
484
+ const topologyRows = database.prepare("SELECT object_id AS objectId, node_json AS nodeJson FROM snapshot_topology ORDER BY rowid").all();
485
+ const nodes = new Map();
486
+ const order = [];
487
+ for (const row of topologyRows) {
488
+ const node = JSON.parse(row.nodeJson);
489
+ if (node.objectId !== row.objectId || !Array.isArray(node.ledgerParents))
490
+ return null;
491
+ let typed;
492
+ if (node.type === "lifecycle") {
493
+ const entry = entries.get(row.objectId);
494
+ if (!entry || node.ledgerParents.length > 1)
495
+ return null;
496
+ typed = { type: "lifecycle", objectId: row.objectId, entry, ledgerParents: node.ledgerParents, evidenceParent: evidenceObjectId(entry) };
497
+ }
498
+ else if (node.type === "transaction" && node.verb && node.contract && node.entryObjectIds && node.finalEntryObjectId) {
499
+ typed = { type: "transaction", objectId: row.objectId, transaction: { v: 1, kind: "transaction", verb: node.verb, predecessor: node.ledgerParents.length === 0 ? "none" : "p1" }, ledgerParents: node.ledgerParents, entryObjectIds: node.entryObjectIds, contract: node.contract, finalEntryObjectId: node.finalEntryObjectId, evidenceParent: null };
500
+ }
501
+ else if (node.type === "join" && node.ledgerParents.length === 2) {
502
+ typed = { type: "join", objectId: row.objectId, join: { v: 1, kind: "join", at: "1970-01-01T00:00:00.000Z", actor: "snapshot" }, ledgerParents: node.ledgerParents, evidenceParent: null };
503
+ }
504
+ else
505
+ return null;
506
+ nodes.set(row.objectId, typed);
507
+ }
508
+ const visited = new Set();
509
+ const visit = (objectId) => {
510
+ if (visited.has(objectId))
511
+ return;
512
+ visited.add(objectId);
513
+ const node = nodes.get(objectId);
514
+ if (!node)
515
+ return;
516
+ const dependencies = node.type === "transaction" ? [...node.ledgerParents, ...node.entryObjectIds] : [...node.ledgerParents];
517
+ for (const dependency of dependencies)
518
+ visit(dependency);
519
+ order.push(objectId);
520
+ };
521
+ visit(observedHead);
522
+ return { head: observedHead, coordinates: { objectFormat: pin.objectFormat, objectIdLength: pin.objectFormat === "sha1" ? 40 : 64, emptyTree: pin.emptyTree }, nodes, order, diseases: [] };
523
+ }
524
+ catch {
525
+ database.close();
526
+ await discard(databasePath);
527
+ return null;
528
+ }
529
+ finally {
530
+ try {
531
+ database.close();
532
+ }
533
+ catch { /* corruption path already discarded */ }
534
+ }
535
+ }
536
+ export async function readPinnedRepositoryLifecycle(cwd, observedHead, signal, options = {}) {
537
+ const databasePath = await snapshotPath(cwd, signal);
538
+ const database = openValidated(databasePath);
539
+ if (!database) {
540
+ await discard(databasePath);
541
+ return null;
542
+ }
543
+ try {
544
+ const pin = metadata(database);
545
+ if (!pin || pin.head !== observedHead)
546
+ return null;
547
+ const rows = database.prepare(`
548
+ SELECT contract, status, current_json AS currentJson, fold_json AS foldJson, diagnostics_json AS diagnosticsJson
549
+ FROM snapshot_contracts ORDER BY contract
550
+ `).all();
551
+ const repository = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_repository WHERE singleton = 1").get();
552
+ if (!repository)
553
+ throw new Error("snapshot repository row is missing");
554
+ validatePinReceipt(database, pin);
555
+ validateRepositoryReceipts(database, rows.map((row) => row.contract), options.summaryOnly === true, options.includeTopology !== false);
556
+ return {
557
+ observedLedgerHead: pin.head,
558
+ objectFormat: pin.objectFormat,
559
+ emptyTree: pin.emptyTree,
560
+ contracts: rows.flatMap((row) => {
561
+ const model = validateContractConsistency(database, row, pin, options.summaryOnly);
562
+ if (!model)
563
+ return [];
564
+ return [atPin(model, pin.head)];
565
+ }),
566
+ isolatedContracts: rows.filter((row) => row.status === "isolated").map((row) => row.contract),
567
+ diagnostics: [
568
+ ...parseDiagnostics(repository.diagnosticsJson),
569
+ ...rows.flatMap((row) => parseDiagnostics(row.diagnosticsJson, row.contract)),
570
+ ],
571
+ topology: {
572
+ head: pin.head,
573
+ nodes: options.includeTopology === false
574
+ ? []
575
+ : database.prepare("SELECT node_json AS nodeJson FROM snapshot_topology ORDER BY rowid").all().map((row) => parseJson(row.nodeJson)),
576
+ },
577
+ };
578
+ }
579
+ catch {
580
+ database.close();
581
+ await discard(databasePath);
582
+ return null;
583
+ }
584
+ finally {
585
+ try {
586
+ database.close();
587
+ }
588
+ catch { /* corruption path already discarded */ }
589
+ }
590
+ }
591
+ export async function publishRepositoryCurrentState(cwd, read, options) {
592
+ const databasePath = await snapshotPath(cwd, options.signal);
593
+ await fs.mkdir(path.dirname(databasePath), { recursive: true });
594
+ if (await options.confirmHead() !== read.observedLedgerHead)
595
+ return false;
596
+ let database;
597
+ try {
598
+ database = new DatabaseSync(databasePath);
599
+ database.exec(`PRAGMA busy_timeout = ${SNAPSHOT_WRITE_BUSY_MS}; PRAGMA synchronous = FULL; PRAGMA journal_mode = DELETE; BEGIN IMMEDIATE`);
600
+ database.exec("DROP TABLE IF EXISTS snapshot_integrity; DROP TABLE IF EXISTS snapshot_resources; DROP TABLE IF EXISTS snapshot_topology; DROP TABLE IF EXISTS snapshot_summaries; DROP TABLE IF EXISTS snapshot_history; DROP TABLE IF EXISTS snapshot_contracts; DROP TABLE IF EXISTS snapshot_repository; DROP TABLE IF EXISTS snapshot_pin; DROP TABLE IF EXISTS snapshot_schema");
601
+ database.exec(SCHEMA);
602
+ database.prepare("INSERT INTO snapshot_schema(singleton, version) VALUES (1, ?)").run(REPOSITORY_CURRENT_STATE_VERSION);
603
+ database.prepare("INSERT INTO snapshot_pin(singleton, ledger_head, object_format, empty_tree) VALUES (1, ?, ?, ?)").run(read.observedLedgerHead, read.objectFormat, read.emptyTree);
604
+ const insert = database.prepare("INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json) VALUES (?, ?, ?, ?, ?)");
605
+ const insertHistory = database.prepare("INSERT INTO snapshot_history(contract, position, object_id, entry_json) VALUES (?, ?, ?, ?)");
606
+ const insertSummary = database.prepare("INSERT INTO snapshot_summaries(contract, history_json) VALUES (?, ?)");
607
+ const isolated = new Set(read.isolatedContracts);
608
+ const models = new Map(read.contracts.map((model) => [model.contract, model]));
609
+ for (const contract of new Set([...models.keys(), ...isolated])) {
610
+ const model = models.get(contract) ?? null;
611
+ const diagnostics = read.diagnostics.filter((disease) => disease.contract === contract);
612
+ const status = isolated.has(contract) ? "isolated" : diagnostics.length > 0 ? "diseased" : "healthy";
613
+ const folded = options.fold.contracts.get(contract) ?? null;
614
+ const current = model === null ? null : Object.fromEntries(Object.entries(model).filter(([key]) => key !== "history"));
615
+ insert.run(contract, status, current === null ? null : JSON.stringify(current), folded === null ? null : JSON.stringify(folded), JSON.stringify(diagnostics));
616
+ model?.history.forEach((row, position) => insertHistory.run(contract, position, row.objectId, JSON.stringify(row.entry)));
617
+ if (model)
618
+ insertSummary.run(contract, JSON.stringify(summaryHistory(model.history)));
619
+ }
620
+ database.prepare("INSERT INTO snapshot_repository(singleton, diagnostics_json) VALUES (1, ?)").run(JSON.stringify(read.diagnostics.filter((disease) => disease.contract === undefined)));
621
+ const insertTopology = database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)");
622
+ for (const node of read.topology.nodes)
623
+ insertTopology.run(node.objectId, JSON.stringify(node));
624
+ const insertResource = database.prepare("INSERT INTO snapshot_resources(kind, resource_key, contract) VALUES (?, ?, ?)");
625
+ for (const model of read.contracts) {
626
+ if (model.resources.worktree?.state === "held")
627
+ insertResource.run("worktree", model.resources.worktree.place, model.contract);
628
+ for (const task of model.resources.tasks)
629
+ if (task.state === "held")
630
+ insertResource.run("task", task.taskId, model.contract);
631
+ }
632
+ writeIntegrityReceipts(database);
633
+ options.beforeCommit?.();
634
+ database.exec("COMMIT");
635
+ database.close();
636
+ return true;
637
+ }
638
+ catch (error) {
639
+ try {
640
+ database.exec("ROLLBACK");
641
+ }
642
+ catch { /* database may not have opened */ }
643
+ try {
644
+ database.close();
645
+ }
646
+ catch { /* database may not have opened */ }
647
+ throw error;
648
+ }
649
+ }
650
+ export async function advanceRepositoryCurrentState(cwd, pinnedHead, observedHead, segment, options) {
651
+ const databasePath = await snapshotPath(cwd, options.signal);
652
+ if (await options.confirmHead() !== observedHead)
653
+ return false;
654
+ const database = openValidated(databasePath);
655
+ if (!database)
656
+ return false;
657
+ try {
658
+ database.exec(`PRAGMA query_only = OFF; PRAGMA busy_timeout = ${SNAPSHOT_WRITE_BUSY_MS}; PRAGMA synchronous = FULL; BEGIN IMMEDIATE`);
659
+ const pin = metadata(database);
660
+ if (!pin || pin.head !== pinnedHead) {
661
+ database.exec("ROLLBACK");
662
+ return false;
663
+ }
664
+ validatePinReceipt(database, pin);
665
+ assertReceipt(database, "topology", topologyDigest(database));
666
+ assertReceipt(database, "resources", resourcesDigest(database));
667
+ for (const contract of new Set(segment.transactions.map((transaction) => transaction.entries[0]?.entry.contract).filter((contract) => contract !== undefined))) {
668
+ if (database.prepare("SELECT 1 FROM snapshot_contracts WHERE contract = ?").get(contract))
669
+ validateContractReceipt(database, contract, false);
670
+ }
671
+ const acceptedTopology = new Set(database.prepare("SELECT object_id AS objectId FROM snapshot_topology").all().map((row) => row.objectId));
672
+ const pendingJoins = new Map(segment.joins.map((join) => [join.objectId, join]));
673
+ const promoteReadyJoins = () => {
674
+ let promoted;
675
+ do {
676
+ promoted = false;
677
+ for (const join of pendingJoins.values()) {
678
+ if (!join.parents.every((parent) => acceptedTopology.has(parent)))
679
+ continue;
680
+ database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(join.objectId, JSON.stringify({
681
+ objectId: join.objectId,
682
+ type: "join",
683
+ ledgerParents: join.parents,
684
+ }));
685
+ acceptedTopology.add(join.objectId);
686
+ pendingJoins.delete(join.objectId);
687
+ promoted = true;
688
+ }
689
+ } while (promoted);
690
+ };
691
+ for (const transaction of segment.transactions) {
692
+ promoteReadyJoins();
693
+ if ((transaction.predecessor !== null && !acceptedTopology.has(transaction.predecessor)) || transaction.entries.length === 0) {
694
+ database.exec("ROLLBACK");
695
+ return false;
696
+ }
697
+ if (transaction.entries.some((entry) => database.prepare("SELECT 1 FROM snapshot_history WHERE object_id = ?").get(entry.objectId) !== undefined)) {
698
+ database.exec("ROLLBACK");
699
+ return false;
700
+ }
701
+ const contract = transaction.entries[0].entry.contract;
702
+ const row = database.prepare("SELECT status, current_json AS currentJson, fold_json AS foldJson, diagnostics_json AS diagnosticsJson FROM snapshot_contracts WHERE contract = ?").get(contract);
703
+ let state = row?.foldJson ? parseJson(row.foldJson) : undefined;
704
+ const priorModel = row?.currentJson ? currentModel(database, contract, row.currentJson) : null;
705
+ const first = transaction.entries[0];
706
+ if (first.entry.kind === "bind" && (state || row)) {
707
+ const diagnostics = [...(row ? parseJson(row.diagnosticsJson) : []), {
708
+ objectId: first.objectId,
709
+ code: "same-contract-divergence",
710
+ contract,
711
+ detail: "incremental bind diverges from an already admitted contract",
712
+ }];
713
+ database.prepare("UPDATE snapshot_contracts SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = ? WHERE contract = ?").run(JSON.stringify(diagnostics), contract);
714
+ }
715
+ else {
716
+ if (first.entry.kind !== "bind" && (!state || first.causalParent !== state.entryObjectIds.at(-1))) {
717
+ database.exec("ROLLBACK");
718
+ return false;
719
+ }
720
+ let staged = state === undefined ? undefined : structuredClone(state);
721
+ let disease;
722
+ const basePosition = staged?.entryObjectIds.length ?? 0;
723
+ for (let index = 0; index < transaction.entries.length; index += 1) {
724
+ const entry = transaction.entries[index];
725
+ const applied = applyFoldEntry(staged, { objectId: entry.objectId, entry: entry.entry, position: basePosition + index });
726
+ staged = applied.state;
727
+ if (applied.disease) {
728
+ disease = { ...applied.disease, transactionObjectId: transaction.objectId };
729
+ break;
730
+ }
731
+ }
732
+ if (disease || !staged) {
733
+ const diagnostics = [...(row ? parseJson(row.diagnosticsJson) : []), ...(disease ? [disease] : [])];
734
+ if (row)
735
+ database.prepare("UPDATE snapshot_contracts SET status = 'diseased', diagnostics_json = ? WHERE contract = ?").run(JSON.stringify(diagnostics), contract);
736
+ else
737
+ database.prepare("INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json) VALUES (?, 'isolated', NULL, NULL, ?)").run(contract, JSON.stringify(diagnostics));
738
+ }
739
+ else {
740
+ if (first.entry.kind === "bind" && (first.entry.data.after?.length ?? 0) > 0) {
741
+ const unavailable = first.entry.data.after.filter((dependency) => {
742
+ const prerequisite = database.prepare("SELECT status, fold_json AS foldJson FROM snapshot_contracts WHERE contract = ?").get(dependency);
743
+ if (!prerequisite || prerequisite.status !== "healthy" || prerequisite.foldJson === null)
744
+ return true;
745
+ return parseJson(prerequisite.foldJson).terminalObjectId === null;
746
+ });
747
+ if (unavailable.length > 0) {
748
+ const diagnostic = {
749
+ objectId: first.objectId,
750
+ code: "causal-prerequisite",
751
+ contract,
752
+ detail: `after prerequisites are not admitted terminal contracts: ${unavailable.join(", ")}`,
753
+ };
754
+ database.prepare(`
755
+ INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json)
756
+ VALUES (?, 'isolated', NULL, NULL, ?)
757
+ ON CONFLICT(contract) DO UPDATE SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = excluded.diagnostics_json
758
+ `).run(contract, JSON.stringify([diagnostic]));
759
+ acceptedTopology.add(transaction.objectId);
760
+ database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(transaction.objectId, JSON.stringify({
761
+ objectId: transaction.objectId,
762
+ type: "transaction",
763
+ ledgerParents: transaction.predecessor === null ? [] : [transaction.predecessor],
764
+ verb: transaction.transaction.verb,
765
+ contract,
766
+ entryObjectIds: transaction.entries.map((entry) => entry.objectId),
767
+ finalEntryObjectId: transaction.entries.at(-1).objectId,
768
+ }));
769
+ continue;
770
+ }
771
+ }
772
+ const history = [...(priorModel?.history ?? []), ...transaction.entries.map((entry) => ({ objectId: entry.objectId, entry: entry.entry }))];
773
+ const model = await options.materialize(staged, history);
774
+ database.prepare(`
775
+ INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json)
776
+ VALUES (?, 'healthy', ?, ?, '[]')
777
+ ON CONFLICT(contract) DO UPDATE SET status = 'healthy', current_json = excluded.current_json, fold_json = excluded.fold_json, diagnostics_json = '[]'
778
+ `).run(contract, JSON.stringify(Object.fromEntries(Object.entries(model).filter(([key]) => key !== "history"))), JSON.stringify(staged));
779
+ database.prepare("DELETE FROM snapshot_history WHERE contract = ?").run(contract);
780
+ const insertHistory = database.prepare("INSERT INTO snapshot_history(contract, position, object_id, entry_json) VALUES (?, ?, ?, ?)");
781
+ model.history.forEach((historyRow, position) => insertHistory.run(contract, position, historyRow.objectId, JSON.stringify(historyRow.entry)));
782
+ database.prepare("INSERT INTO snapshot_summaries(contract, history_json) VALUES (?, ?) ON CONFLICT(contract) DO UPDATE SET history_json = excluded.history_json").run(contract, JSON.stringify(summaryHistory(model.history)));
783
+ database.prepare("DELETE FROM snapshot_resources WHERE contract = ?").run(contract);
784
+ const insertResource = database.prepare("INSERT INTO snapshot_resources(kind, resource_key, contract) VALUES (?, ?, ?)");
785
+ if (model.resources.worktree?.state === "held")
786
+ insertResource.run("worktree", model.resources.worktree.place, contract);
787
+ for (const task of model.resources.tasks)
788
+ if (task.state === "held")
789
+ insertResource.run("task", task.taskId, contract);
790
+ const collisions = [
791
+ ...(model.resources.worktree?.state === "held" ? [{ kind: "worktree", key: model.resources.worktree.place }] : []),
792
+ ...model.resources.tasks.filter((task) => task.state === "held").map((task) => ({ kind: "task", key: task.taskId })),
793
+ ];
794
+ for (const resource of collisions) {
795
+ const owners = database.prepare("SELECT contract FROM snapshot_resources WHERE kind = ? AND resource_key = ?").all(resource.kind, resource.key);
796
+ if (owners.length < 2)
797
+ continue;
798
+ for (const owner of owners) {
799
+ const ownerRow = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_contracts WHERE contract = ?").get(owner.contract);
800
+ const diagnostic = {
801
+ objectId: first.objectId,
802
+ code: resource.kind === "worktree" ? "worktree-place-collision" : "task-ownership-collision",
803
+ contract: owner.contract,
804
+ detail: `${resource.kind} ${resource.key} is held by ${owners.map((candidate) => candidate.contract).sort().join(", ")}`,
805
+ };
806
+ database.prepare("UPDATE snapshot_contracts SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = ? WHERE contract = ?").run(JSON.stringify([...parseJson(ownerRow.diagnosticsJson), diagnostic]), owner.contract);
807
+ }
808
+ }
809
+ }
810
+ }
811
+ const insertLifecycleTopology = database.prepare("INSERT OR IGNORE INTO snapshot_topology(object_id, node_json) VALUES (?, ?)");
812
+ for (const entry of transaction.entries)
813
+ insertLifecycleTopology.run(entry.objectId, JSON.stringify({
814
+ objectId: entry.objectId,
815
+ type: "lifecycle",
816
+ ledgerParents: entry.causalParent === null ? [] : [entry.causalParent],
817
+ }));
818
+ database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(transaction.objectId, JSON.stringify({
819
+ objectId: transaction.objectId,
820
+ type: "transaction",
821
+ ledgerParents: transaction.predecessor === null ? [] : [transaction.predecessor],
822
+ verb: transaction.transaction.verb,
823
+ contract,
824
+ entryObjectIds: transaction.entries.map((entry) => entry.objectId),
825
+ finalEntryObjectId: transaction.entries.at(-1).objectId,
826
+ }));
827
+ acceptedTopology.add(transaction.objectId);
828
+ }
829
+ promoteReadyJoins();
830
+ if (pendingJoins.size > 0 || !acceptedTopology.has(observedHead)) {
831
+ database.exec("ROLLBACK");
832
+ return false;
833
+ }
834
+ database.prepare("UPDATE snapshot_contracts SET current_json = json_set(current_json, '$.observedLedgerHead', ?) WHERE current_json IS NOT NULL").run(observedHead);
835
+ database.prepare("UPDATE snapshot_pin SET ledger_head = ? WHERE singleton = 1").run(observedHead);
836
+ writeIntegrityReceipts(database);
837
+ database.exec("COMMIT");
838
+ return true;
839
+ }
840
+ catch {
841
+ try {
842
+ database.exec("ROLLBACK");
843
+ }
844
+ catch { /* handled by recovery */ }
845
+ return false;
846
+ }
847
+ finally {
848
+ database.close();
849
+ }
850
+ }
851
+ export async function discardRepositoryCurrentState(cwd, signal) {
852
+ await discard(await snapshotPath(cwd, signal));
853
+ }