@astrosheep/keiyaku 2.9.11 → 2.9.13
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.
- package/README.md +79 -85
- package/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms.js +1 -0
- package/build/agents/providers/claude-agent-sdk/adapter.js +17 -4
- package/build/cli/commands/akuma/akuma/handler.js +2 -1
- package/build/cli/commands/akuma/akuma/meta.js +4 -3
- package/build/cli/commands/akuma/catalog.js +2 -0
- package/build/cli/commands/akuma/list/handler.js +1 -1
- package/build/cli/commands/akuma/list/meta-list.js +12 -0
- package/build/cli/commands/akuma/list/meta-ls.js +2 -2
- package/build/cli/commands/akuma.js +5 -0
- package/build/cli/commands/contract/amend/meta.js +4 -3
- package/build/cli/commands/contract/arc/meta.js +2 -0
- package/build/cli/commands/contract/audit/handler.js +28 -2
- package/build/cli/commands/contract/audit/meta.js +4 -3
- package/build/cli/commands/contract/bind/meta.js +4 -4
- package/build/cli/commands/contract/petition/handler.js +2 -1
- package/build/cli/commands/contract/petition/meta.js +2 -2
- package/build/cli/commands/contract/renew/handler.js +44 -3
- package/build/cli/commands/contract/renew/meta.js +5 -4
- package/build/cli/commands/metadata.js +14 -10
- package/build/cli/commands/projection/call/handler.js +1 -1
- package/build/cli/commands/projection/call/meta.js +1 -1
- package/build/cli/commands/projection/catalog.js +2 -2
- package/build/cli/commands/projection/{revive → fork}/handler.js +10 -5
- package/build/cli/commands/projection/fork/meta.js +12 -0
- package/build/cli/commands/projection/history/handler.js +46 -2
- package/build/cli/commands/projection/history/meta.js +3 -3
- package/build/cli/commands/projection/kill/handler.js +4 -5
- package/build/cli/commands/projection/kill/meta.js +2 -2
- package/build/cli/commands/projection/status/handler.js +29 -13
- package/build/cli/commands/projection/status/meta.js +14 -3
- package/build/cli/commands/projection/tell/handler.js +8 -4
- package/build/cli/commands/projection/tell/meta.js +1 -1
- package/build/cli/commands/projection/wait/handler.js +45 -20
- package/build/cli/commands/projection/wait/meta.js +1 -1
- package/build/cli/commands/shared.js +2 -2
- package/build/cli/commands/system/catalog.js +2 -0
- package/build/cli/commands/task/add/handler.js +28 -12
- package/build/cli/commands/task/add/meta.js +18 -6
- package/build/cli/commands/task/catalog.js +4 -0
- package/build/cli/commands/task/compose/handler.js +18 -0
- package/build/cli/commands/task/compose/meta.js +10 -0
- package/build/cli/commands/task/doctor/handler.js +2 -3
- package/build/cli/commands/task/doctor/meta.js +2 -2
- package/build/cli/commands/task/done/meta.js +2 -2
- package/build/cli/commands/task/drop/handler.js +7 -2
- package/build/cli/commands/task/drop/meta.js +6 -3
- package/build/cli/commands/task/hold/meta.js +2 -2
- package/build/cli/commands/task/log/meta.js +2 -2
- package/build/cli/commands/task/ls/meta.js +2 -2
- package/build/cli/commands/task/note/handler.js +6 -0
- package/build/cli/commands/task/note/meta.js +11 -0
- package/build/cli/commands/task/resume/meta.js +2 -2
- package/build/cli/commands/task/shared.js +29 -15
- package/build/cli/commands/task/show/meta.js +2 -2
- package/build/cli/commands/task/start/meta.js +2 -2
- package/build/cli/commands/task/stop/meta.js +2 -2
- package/build/cli/commands/task/update/meta.js +3 -2
- package/build/cli/commands/verification/handler.js +43 -0
- package/build/cli/commands/verification/meta.js +13 -0
- package/build/cli/completion.js +16 -13
- package/build/cli/flags.js +48 -12
- package/build/cli/help.js +29 -30
- package/build/cli/index.js +14 -8
- package/build/cli/parse-flags.js +60 -21
- package/build/cli/parse-metadata.js +10 -4
- package/build/cli/parse-selectors.js +22 -4
- package/build/cli/parse.js +43 -30
- package/build/cli/projection-address.js +7 -7
- package/build/cli/render/arc.js +12 -3
- package/build/cli/render/audit.js +2 -2
- package/build/cli/render/call.js +4 -4
- package/build/cli/render/kanshi.js +122 -85
- package/build/cli/render/misc.js +2 -2
- package/build/cli/render/petition.js +16 -1
- package/build/cli/render/projection-history.js +17 -4
- package/build/cli/render/shared.js +5 -5
- package/build/cli/render/status-indicator.js +47 -0
- package/build/cli/render/status.js +106 -76
- package/build/cli/render/success-response.js +20 -5
- package/build/cli/render/task.js +33 -5
- package/build/cli/render/tell.js +10 -4
- package/build/cli/render/tool-command-normalization.js +137 -0
- package/build/cli/render/tool-presentation.js +2 -1
- package/build/cli/render/verification.js +23 -0
- package/build/cli/render/wait.js +101 -33
- package/build/cli/types.js +2 -1
- package/build/config/akuma-loader.js +21 -1
- package/build/config/settings/knobs.js +7 -1
- package/build/config/settings/schema.js +10 -5
- package/build/core/addressing.js +3 -3
- package/build/core/amend.js +102 -17
- package/build/core/arc.js +4 -4
- package/build/core/audit/candidate.js +10 -18
- package/build/core/audit/coordinates.js +12 -17
- package/build/core/audit/evidence.js +6 -4
- package/build/core/audit/facade.js +3 -3
- package/build/core/audit/report.js +1 -1
- package/build/core/bind-reconciliation.js +42 -23
- package/build/core/bind.js +182 -81
- package/build/core/call/call.js +6 -7
- package/build/core/call/context.js +46 -33
- package/build/core/call/execution.js +21 -6
- package/build/core/call/prompt.js +11 -42
- package/build/core/contract-carrier-runtime.js +209 -105
- package/build/core/contract-carrier.js +156 -73
- package/build/core/contract-view.js +43 -6
- package/build/core/contract.js +2 -1
- package/build/core/derived-replay.js +168 -35
- package/build/core/draft.js +60 -27
- package/build/core/forfeit.js +1 -1
- package/build/core/lifecycle-history.js +13 -0
- package/build/core/lifecycle-recovery.js +32 -27
- package/build/core/lifecycle-runner.js +14 -14
- package/build/core/log.js +1 -1
- package/build/core/outcome-base.js +2 -2
- package/build/core/projection/akuma-name.js +43 -0
- package/build/core/projection/generation/database.js +141 -79
- package/build/core/projection/generation/ledger.js +91 -0
- package/build/core/projection/generation/model.js +19 -14
- package/build/core/projection/generation/projection-generation-continuation.js +1 -1
- package/build/core/projection/generation/projection-generation-doorbell.js +88 -0
- package/build/core/projection/generation/projection-generation-execution.js +24 -0
- package/build/core/projection/generation/projection-generation-identity.js +4 -0
- package/build/core/projection/generation/projection-generation-launcher.js +72 -138
- package/build/core/projection/generation/projection-generation-process.js +16 -15
- package/build/core/projection/generation/projection-generation-runner.js +132 -76
- package/build/core/projection/generation/projection-generation-runtime.js +10 -61
- package/build/core/projection/generation/protocol.js +68 -0
- package/build/core/projection/generation/store.js +17 -7
- package/build/core/projection/generation/transitions.js +24 -14
- package/build/core/projection/index.js +5 -5
- package/build/core/projection/projection-core.js +6 -2
- package/build/core/projection/projection-execution-observer.js +1 -1
- package/build/core/projection/projection-history.js +8 -8
- package/build/core/projection/projection-kill.js +20 -10
- package/build/core/projection/projection-life-observer.js +8 -8
- package/build/core/projection/projection-mint.js +6 -8
- package/build/core/projection/projection-status-observation.js +77 -8
- package/build/core/projection/projection-status.js +151 -4
- package/build/core/projection/projection-wait.js +48 -29
- package/build/core/projection/projection-wake.js +28 -88
- package/build/core/projection/tell/launch-store.js +1 -1
- package/build/core/projection/tell/store.js +1 -1
- package/build/core/renew-build.js +204 -74
- package/build/core/renew-plan.js +34 -9
- package/build/core/renew-rewrite.js +56 -15
- package/build/core/renew.js +39 -35
- package/build/core/repository-ledger/accepted-fold-read.js +6 -3
- package/build/core/repository-ledger/claim-evidence.js +1 -1
- package/build/core/repository-ledger/codec.js +11 -5
- package/build/core/repository-ledger/current-state-store.js +464 -349
- package/build/core/repository-ledger/fold-repository.js +5 -6
- package/build/core/repository-ledger/identity.js +60 -0
- package/build/core/repository-ledger/inventory.js +3 -3
- package/build/core/repository-ledger/read-model.js +57 -9
- package/build/core/repository-ledger/write-transaction.js +24 -17
- package/build/core/run-control/connection-bound-close.js +41 -0
- package/build/core/run-control/detached-lease-bootstrap.js +208 -0
- package/build/core/run-control/detached-process.js +14 -0
- package/build/core/run-control/index.js +6 -0
- package/build/core/{process-group.js → run-control/process-tree.js} +1 -1
- package/build/core/run-control/runner-lease.js +172 -0
- package/build/core/run-control/sqlite-process-lifetime-lock.js +174 -0
- package/build/core/seal.js +44 -32
- package/build/core/settlement/claim-delivery.js +22 -9
- package/build/core/settlement/claim.js +8 -6
- package/build/core/settlement/petition-claim-gates.js +9 -8
- package/build/core/settlement/petition-head-guard.js +5 -6
- package/build/core/settlement/petition-preview.js +21 -5
- package/build/core/settlement/petition.js +51 -17
- package/build/core/settlement/settlement.js +20 -15
- package/build/core/settlement/verdict.js +2 -2
- package/build/core/settlement/verification.js +492 -364
- package/build/core/status/board.js +184 -75
- package/build/core/status/drift.js +8 -10
- package/build/core/task/board.js +1 -0
- package/build/core/task/commands.js +25 -0
- package/build/core/task/compose.js +328 -0
- package/build/core/task/document.js +102 -39
- package/build/core/task/index.js +4 -3
- package/build/core/task/settlement-git.js +70 -30
- package/build/core/task/settlement-policy.js +27 -14
- package/build/core/task/source-board.js +1 -0
- package/build/core/task/task-bind-preparation.js +59 -9
- package/build/core/task/task-contract.js +2 -2
- package/build/core/task/task-git-store.js +1 -0
- package/build/core/task/task-store-repository.js +2 -0
- package/build/core/task/task.js +2 -2
- package/build/core/transcripts.js +4 -4
- package/build/core/verification-declaration.js +95 -0
- package/build/core/worktree-bootstrap.js +1 -1
- package/build/core/worktree-path.js +7 -11
- package/build/flow-error.js +15 -2
- package/build/generated/version.js +2 -2
- package/build/git/branches.js +41 -70
- package/build/git/commits.js +92 -21
- package/build/git/core.js +9 -161
- package/build/git/diff/preview.js +4 -4
- package/build/git/diff/read.js +4 -4
- package/build/git/diff/structured.js +5 -5
- package/build/git/process.js +229 -0
- package/build/git/refs.js +30 -5
- package/build/git/staging.js +39 -115
- package/build/git/streaming-batch.js +9 -16
- package/build/git/worktree.js +104 -91
- package/build/index.js +7 -0
- package/package.json +2 -2
- package/skills/keiyaku-akuma/SKILL.md +27 -21
- package/skills/keiyaku-task/SKILL.md +61 -5
- package/skills/keiyaku-workflow/SKILL.md +81 -26
- package/build/cli/commands/projection/revive/meta.js +0 -12
- package/build/core/projection/projection-life-protocol.js +0 -115
- package/build/core/projection/projection-runner-lock.js +0 -382
- package/build/core/projection/tell/database.js +0 -127
- package/build/core/settlement/verification-coordination.js +0 -305
- package/build/core/settlement/verification-supervisor.js +0 -275
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { createHash } from "node:crypto";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { DatabaseSync } from "node:sqlite";
|
|
5
5
|
import { runGitProcess } from "../../git/core.js";
|
|
6
|
+
import { acquireSqliteProcessLifetimeLock } from "../run-control/sqlite-process-lifetime-lock.js";
|
|
6
7
|
import { applyFoldEntry } from "./fold.js";
|
|
7
8
|
import { createLifecycleEntrySchema, evidenceObjectId, repositoryDeliveryRef } from "./codec.js";
|
|
8
|
-
|
|
9
|
-
export const
|
|
10
|
-
const
|
|
9
|
+
import { repositoryLedgerHead } from "./identity.js";
|
|
10
|
+
export const REPOSITORY_CURRENT_STATE_VERSION = 4;
|
|
11
|
+
export const REPOSITORY_CURRENT_STATE_PREFIX = "keiyaku-current-state-";
|
|
12
|
+
export const REPOSITORY_CURRENT_STATE_SUFFIX = ".sqlite";
|
|
13
|
+
const SNAPSHOT_READER_BUSY_MS = 5_000;
|
|
14
|
+
const SNAPSHOT_WRITER_LOCK_FILE = "keiyaku-current-state-writer-coordination.sqlite";
|
|
11
15
|
const SCHEMA = `
|
|
12
16
|
CREATE TABLE snapshot_schema (
|
|
13
17
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
@@ -64,7 +68,7 @@ function exactRows(database, sql, ...params) {
|
|
|
64
68
|
return database.prepare(sql).all(...params);
|
|
65
69
|
}
|
|
66
70
|
function pinDigest(pin) {
|
|
67
|
-
return digest(["keiyaku-current-state-
|
|
71
|
+
return digest(["keiyaku-current-state-v4", REPOSITORY_CURRENT_STATE_VERSION, pin.head, pin.objectFormat, pin.emptyTree]);
|
|
68
72
|
}
|
|
69
73
|
function repositoryDigest(database) {
|
|
70
74
|
return digest(["repository", exactRows(database, "SELECT singleton, diagnostics_json FROM snapshot_repository ORDER BY singleton")]);
|
|
@@ -126,13 +130,16 @@ function validateRepositoryReceipts(database, contracts, summaryOnly, includeTop
|
|
|
126
130
|
if (includeTopology)
|
|
127
131
|
assertReceipt(database, "topology", topologyDigest(database));
|
|
128
132
|
}
|
|
129
|
-
|
|
133
|
+
function snapshotFileName(head) {
|
|
134
|
+
return `${REPOSITORY_CURRENT_STATE_PREFIX}${head ?? "empty"}${REPOSITORY_CURRENT_STATE_SUFFIX}`;
|
|
135
|
+
}
|
|
136
|
+
export async function repositoryCurrentStateSnapshotPath(cwd, head, signal) {
|
|
130
137
|
// Addressing callers already pass the resolved primary worktree root. Reuse
|
|
131
138
|
// that physical coordinate within this request instead of probing Git again.
|
|
132
139
|
const localGitDirectory = path.join(cwd, ".git");
|
|
133
140
|
try {
|
|
134
141
|
if ((await fs.stat(localGitDirectory)).isDirectory()) {
|
|
135
|
-
return path.join(await fs.realpath(localGitDirectory),
|
|
142
|
+
return path.join(await fs.realpath(localGitDirectory), snapshotFileName(head));
|
|
136
143
|
}
|
|
137
144
|
}
|
|
138
145
|
catch {
|
|
@@ -145,38 +152,163 @@ async function snapshotPath(cwd, signal) {
|
|
|
145
152
|
if (result.status !== 0 || result.error || !path.isAbsolute(commonDir)) {
|
|
146
153
|
throw new Error(`cannot resolve repository common directory: ${result.stderr.trim() || result.error?.message || `exit ${String(result.status)}`}`);
|
|
147
154
|
}
|
|
148
|
-
return path.join(commonDir,
|
|
155
|
+
return path.join(commonDir, snapshotFileName(head));
|
|
156
|
+
}
|
|
157
|
+
function classifySnapshotFailure(error) {
|
|
158
|
+
const code = error.code;
|
|
159
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
160
|
+
if (code === "ENOENT" || message.includes("no such file or directory"))
|
|
161
|
+
return "missing";
|
|
162
|
+
if (code === "ERR_SQLITE_BUSY" || code === "ERR_SQLITE_LOCKED" || message.includes("database is locked") || message.includes("database is busy"))
|
|
163
|
+
return "transient";
|
|
164
|
+
if (code === "ERR_SQLITE_CORRUPT" || code === "ERR_SQLITE_NOTADB" || message.includes("database disk image is malformed") || message.includes("file is not a database"))
|
|
165
|
+
return "corrupt";
|
|
166
|
+
return "validation-failure";
|
|
167
|
+
}
|
|
168
|
+
function isCantOpen(error) {
|
|
169
|
+
const code = error.code;
|
|
170
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
171
|
+
return code === "ERR_SQLITE_CANTOPEN" || message.includes("unable to open database file");
|
|
172
|
+
}
|
|
173
|
+
async function snapshotExists(databasePath) {
|
|
174
|
+
try {
|
|
175
|
+
await fs.lstat(databasePath);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (error.code === "ENOENT")
|
|
180
|
+
return false;
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
149
183
|
}
|
|
150
|
-
function openValidated(databasePath) {
|
|
184
|
+
async function openValidated(databasePath) {
|
|
185
|
+
const existedBeforeOpen = await snapshotExists(databasePath);
|
|
151
186
|
let database;
|
|
152
187
|
try {
|
|
153
|
-
database = new DatabaseSync(databasePath);
|
|
154
|
-
database.exec(
|
|
188
|
+
database = new DatabaseSync(databasePath, { readOnly: true });
|
|
189
|
+
database.exec(`PRAGMA busy_timeout = ${SNAPSHOT_READER_BUSY_MS}; PRAGMA query_only = ON`);
|
|
155
190
|
const rows = database.prepare("SELECT version FROM snapshot_schema WHERE singleton = 1").all();
|
|
156
191
|
if (rows.length !== 1 || rows[0]?.version !== REPOSITORY_CURRENT_STATE_VERSION) {
|
|
157
192
|
database.close();
|
|
158
|
-
return
|
|
193
|
+
return { kind: "validation-failure" };
|
|
159
194
|
}
|
|
160
|
-
return database;
|
|
195
|
+
return { kind: "opened", database };
|
|
161
196
|
}
|
|
162
|
-
catch {
|
|
197
|
+
catch (error) {
|
|
163
198
|
try {
|
|
164
199
|
database.close();
|
|
165
200
|
}
|
|
166
|
-
catch { /*
|
|
167
|
-
|
|
201
|
+
catch { /* open did not yield a usable connection */ }
|
|
202
|
+
if (isCantOpen(error)) {
|
|
203
|
+
const existsAfterFailure = await snapshotExists(databasePath);
|
|
204
|
+
if (existedBeforeOpen === false || existsAfterFailure === false)
|
|
205
|
+
return { kind: "missing", error };
|
|
206
|
+
}
|
|
207
|
+
return { kind: classifySnapshotFailure(error), error };
|
|
168
208
|
}
|
|
169
209
|
}
|
|
170
|
-
async function
|
|
171
|
-
|
|
210
|
+
async function readSnapshot(databasePath, read) {
|
|
211
|
+
const opened = await openValidated(databasePath);
|
|
212
|
+
if (opened.kind !== "opened")
|
|
213
|
+
return opened;
|
|
214
|
+
try {
|
|
215
|
+
return { kind: "read", value: read(opened.database) };
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
return { kind: classifySnapshotFailure(error), error };
|
|
219
|
+
}
|
|
220
|
+
finally {
|
|
172
221
|
try {
|
|
173
|
-
|
|
222
|
+
opened.database.close();
|
|
174
223
|
}
|
|
175
|
-
catch
|
|
176
|
-
|
|
177
|
-
|
|
224
|
+
catch { /* validation failure already has its outcome */ }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export function currentStateWriterCoordination(databasePath) {
|
|
228
|
+
const lockPath = path.join(path.dirname(databasePath), SNAPSHOT_WRITER_LOCK_FILE);
|
|
229
|
+
return {
|
|
230
|
+
path: lockPath,
|
|
231
|
+
async acquire(signal) {
|
|
232
|
+
const lease = await acquireSqliteProcessLifetimeLock({ path: lockPath, signal });
|
|
233
|
+
return { release: () => lease.close() };
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
async function withSnapshotWriter(databasePath, signal, operation) {
|
|
238
|
+
const lock = await currentStateWriterCoordination(databasePath).acquire(signal);
|
|
239
|
+
try {
|
|
240
|
+
return await operation();
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
lock.release();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function temporarySnapshotPath(databasePath) {
|
|
247
|
+
return `${databasePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
248
|
+
}
|
|
249
|
+
async function discardTemporarySnapshot(databasePath) {
|
|
250
|
+
await fs.unlink(databasePath).catch((error) => {
|
|
251
|
+
if (error.code !== "ENOENT")
|
|
252
|
+
throw error;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/** Writers alone publish or retire cache bytes; readers have no filesystem mutation path. */
|
|
256
|
+
async function publishImmutableSnapshot(buildPath, databasePath) {
|
|
257
|
+
if (await snapshotExists(databasePath))
|
|
258
|
+
return false;
|
|
259
|
+
await fs.rename(buildPath, databasePath);
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* A filename becomes published only after its complete signed contents validate.
|
|
264
|
+
* This runs under writer coordination; readers merely report an unusable cache.
|
|
265
|
+
*/
|
|
266
|
+
async function preparePublicationTarget(databasePath, expectedHead) {
|
|
267
|
+
const snapshot = await readSnapshot(databasePath, (database) => {
|
|
268
|
+
const pin = metadata(database);
|
|
269
|
+
if (!pin || pin.head !== expectedHead)
|
|
270
|
+
throw new Error("snapshot pin does not match its immutable filename");
|
|
271
|
+
validatePinReceipt(database, pin);
|
|
272
|
+
const rows = database.prepare(`
|
|
273
|
+
SELECT contract, status, current_json AS currentJson, fold_json AS foldJson, diagnostics_json AS diagnosticsJson
|
|
274
|
+
FROM snapshot_contracts ORDER BY contract
|
|
275
|
+
`).all();
|
|
276
|
+
const repository = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_repository WHERE singleton = 1").get();
|
|
277
|
+
if (!repository)
|
|
278
|
+
throw new Error("snapshot repository row is missing");
|
|
279
|
+
validateRepositoryReceipts(database, rows.map((row) => row.contract), false, true);
|
|
280
|
+
parseDiagnostics(repository.diagnosticsJson);
|
|
281
|
+
for (const row of rows) {
|
|
282
|
+
parseDiagnostics(row.diagnosticsJson, row.contract);
|
|
283
|
+
validateContractConsistency(database, row, pin);
|
|
178
284
|
}
|
|
179
|
-
})
|
|
285
|
+
});
|
|
286
|
+
if (snapshot.kind === "read")
|
|
287
|
+
return "published";
|
|
288
|
+
if (snapshot.kind === "missing")
|
|
289
|
+
return "missing";
|
|
290
|
+
if (snapshot.kind === "transient")
|
|
291
|
+
return "transient";
|
|
292
|
+
await fs.unlink(databasePath).catch((error) => {
|
|
293
|
+
if (error.code !== "ENOENT")
|
|
294
|
+
throw error;
|
|
295
|
+
});
|
|
296
|
+
return "missing";
|
|
297
|
+
}
|
|
298
|
+
async function garbageCollectSnapshots(databasePath) {
|
|
299
|
+
const directory = path.dirname(databasePath);
|
|
300
|
+
const retained = path.basename(databasePath);
|
|
301
|
+
let entries;
|
|
302
|
+
try {
|
|
303
|
+
entries = await fs.readdir(directory);
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const cacheFile = /^keiyaku-current-state-(?:[0-9a-f]{40}|[0-9a-f]{64}|empty)\.sqlite(?:\..+\.tmp)?$/;
|
|
309
|
+
await Promise.all(entries
|
|
310
|
+
.filter((entry) => entry !== retained && cacheFile.test(entry))
|
|
311
|
+
.map((entry) => fs.unlink(path.join(directory, entry)).catch(() => undefined)));
|
|
180
312
|
}
|
|
181
313
|
function parseJson(text) {
|
|
182
314
|
return JSON.parse(text);
|
|
@@ -204,8 +336,8 @@ function parseCurrent(text, contract) {
|
|
|
204
336
|
const value = JSON.parse(text);
|
|
205
337
|
if (!isRecord(value))
|
|
206
338
|
throw new Error("snapshot current facts are not an object");
|
|
207
|
-
assertClosedKeys(value, ["
|
|
208
|
-
if (value.contract !== contract || typeof value.
|
|
339
|
+
assertClosedKeys(value, ["contractHead", "contract", "phase", "intent", "receiptSegments", "seal", "bootstrapReplay", "petition", "verdict", "pins", "effectiveScope", "causal", "resources"], "snapshot current facts");
|
|
340
|
+
if (value.contract !== contract || typeof value.contractHead !== "string" || !isRecord(value.resources)) {
|
|
209
341
|
throw new Error("snapshot current facts disagree with their row key");
|
|
210
342
|
}
|
|
211
343
|
return value;
|
|
@@ -256,11 +388,8 @@ function validateContractConsistency(database, input, pin, summaryOnly = false)
|
|
|
256
388
|
const diagnostics = parseDiagnostics(diagnosticRow.diagnosticsJson, input.contract);
|
|
257
389
|
if ((input.status === "healthy") !== (diagnostics.length === 0))
|
|
258
390
|
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");
|
|
391
|
+
if (summaryOnly)
|
|
262
392
|
return model;
|
|
263
|
-
}
|
|
264
393
|
const fold = JSON.parse(input.foldJson);
|
|
265
394
|
if (!isRecord(fold) || fold.contract !== input.contract || !Array.isArray(fold.entryObjectIds))
|
|
266
395
|
throw new Error("snapshot fold is invalid");
|
|
@@ -268,7 +397,7 @@ function validateContractConsistency(database, input, pin, summaryOnly = false)
|
|
|
268
397
|
if (fold.entryObjectIds.length !== objectIds.length || fold.entryObjectIds.some((oid, index) => oid !== objectIds[index])) {
|
|
269
398
|
throw new Error("snapshot fold and lifecycle history disagree");
|
|
270
399
|
}
|
|
271
|
-
if (model.
|
|
400
|
+
if (model.contractHead !== objectIds.at(-1) || model.phase !== fold.phase || model.pins.base !== fold.bind?.data?.base) {
|
|
272
401
|
throw new Error("snapshot current facts disagree with fold or pin");
|
|
273
402
|
}
|
|
274
403
|
const typedFold = fold;
|
|
@@ -341,17 +470,12 @@ function storedContract(database, contract) {
|
|
|
341
470
|
diagnostics: parseDiagnostics(row.diagnosticsJson, contract),
|
|
342
471
|
};
|
|
343
472
|
}
|
|
344
|
-
function atPin(model,
|
|
345
|
-
return model
|
|
473
|
+
function atPin(model, _head) {
|
|
474
|
+
return model;
|
|
346
475
|
}
|
|
347
476
|
export async function readPinnedCurrentContract(cwd, observedHead, contract, signal) {
|
|
348
|
-
const databasePath = await
|
|
349
|
-
const
|
|
350
|
-
if (!database) {
|
|
351
|
-
await discard(databasePath);
|
|
352
|
-
return null;
|
|
353
|
-
}
|
|
354
|
-
try {
|
|
477
|
+
const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
|
|
478
|
+
const snapshot = await readSnapshot(databasePath, (database) => {
|
|
355
479
|
const pin = metadata(database);
|
|
356
480
|
if (!pin || pin.head !== observedHead)
|
|
357
481
|
return null;
|
|
@@ -371,53 +495,36 @@ export async function readPinnedCurrentContract(cwd, observedHead, contract, sig
|
|
|
371
495
|
return unattributed.length > 0
|
|
372
496
|
? { status: "indeterminate", contract, diagnostics: unattributed }
|
|
373
497
|
: { status: "absent", contract, diagnostics: [] };
|
|
374
|
-
}
|
|
375
|
-
|
|
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
|
-
}
|
|
498
|
+
});
|
|
499
|
+
return snapshot.kind === "read" ? snapshot.value : null;
|
|
386
500
|
}
|
|
387
|
-
export async function readRepositoryCurrentStatePin(cwd, signal) {
|
|
388
|
-
const databasePath = await
|
|
389
|
-
const
|
|
390
|
-
if (!database) {
|
|
391
|
-
await discard(databasePath);
|
|
392
|
-
return undefined;
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
501
|
+
export async function readRepositoryCurrentStatePin(cwd, observedHead, signal) {
|
|
502
|
+
const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
|
|
503
|
+
const snapshot = await readSnapshot(databasePath, (database) => {
|
|
395
504
|
const pin = metadata(database);
|
|
396
505
|
if (!pin)
|
|
397
506
|
return undefined;
|
|
398
507
|
validatePinReceipt(database, pin);
|
|
399
508
|
return pin.head;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
509
|
+
});
|
|
510
|
+
return snapshot.kind === "read" ? snapshot.value : undefined;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* The first commit parent is the only inexpensive candidate for a linear
|
|
514
|
+
* incremental predecessor. It is not a current pointer: the caller still
|
|
515
|
+
* validates the cache addressed by that exact OID before using it.
|
|
516
|
+
*/
|
|
517
|
+
export async function readRepositoryCurrentStatePredecessor(cwd, observedHead, signal) {
|
|
518
|
+
const result = await runGitProcess(cwd, ["show", "-s", "--format=%P", observedHead], { signal });
|
|
519
|
+
if (result.error?.name === "AbortError")
|
|
520
|
+
throw result.error;
|
|
521
|
+
if (result.status !== 0 || result.error)
|
|
522
|
+
return null;
|
|
523
|
+
return result.stdout.trim().split(/\s+/)[0] || null;
|
|
412
524
|
}
|
|
413
525
|
export async function readRepositoryCurrentStateNodeIds(cwd, observedHead, signal) {
|
|
414
|
-
const databasePath = await
|
|
415
|
-
const
|
|
416
|
-
if (!database) {
|
|
417
|
-
await discard(databasePath);
|
|
418
|
-
return null;
|
|
419
|
-
}
|
|
420
|
-
try {
|
|
526
|
+
const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
|
|
527
|
+
const snapshot = await readSnapshot(databasePath, (database) => {
|
|
421
528
|
const pin = metadata(database);
|
|
422
529
|
if (!pin || pin.head !== observedHead)
|
|
423
530
|
return null;
|
|
@@ -442,28 +549,13 @@ export async function readRepositoryCurrentStateNodeIds(cwd, observedHead, signa
|
|
|
442
549
|
}
|
|
443
550
|
}
|
|
444
551
|
return { ledgerParents, entryIntroductions };
|
|
445
|
-
}
|
|
446
|
-
|
|
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
|
-
}
|
|
552
|
+
});
|
|
553
|
+
return snapshot.kind === "read" ? snapshot.value : null;
|
|
457
554
|
}
|
|
458
555
|
/** Rehydrate the accepted typed topology for publication admission without Git history traversal. */
|
|
459
556
|
export async function readPinnedAcceptedTraversal(cwd, observedHead, signal) {
|
|
460
|
-
const databasePath = await
|
|
461
|
-
const
|
|
462
|
-
if (!database) {
|
|
463
|
-
await discard(databasePath);
|
|
464
|
-
return null;
|
|
465
|
-
}
|
|
466
|
-
try {
|
|
557
|
+
const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
|
|
558
|
+
const snapshot = await readSnapshot(databasePath, (database) => {
|
|
467
559
|
const pin = metadata(database);
|
|
468
560
|
if (!pin || pin.head !== observedHead || !pin.objectFormat || !pin.emptyTree)
|
|
469
561
|
return null;
|
|
@@ -520,27 +612,12 @@ export async function readPinnedAcceptedTraversal(cwd, observedHead, signal) {
|
|
|
520
612
|
};
|
|
521
613
|
visit(observedHead);
|
|
522
614
|
return { head: observedHead, coordinates: { objectFormat: pin.objectFormat, objectIdLength: pin.objectFormat === "sha1" ? 40 : 64, emptyTree: pin.emptyTree }, nodes, order, diseases: [] };
|
|
523
|
-
}
|
|
524
|
-
|
|
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
|
-
}
|
|
615
|
+
});
|
|
616
|
+
return snapshot.kind === "read" ? snapshot.value : null;
|
|
535
617
|
}
|
|
536
618
|
export async function readPinnedRepositoryLifecycle(cwd, observedHead, signal, options = {}) {
|
|
537
|
-
const databasePath = await
|
|
538
|
-
const
|
|
539
|
-
if (!database) {
|
|
540
|
-
await discard(databasePath);
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
try {
|
|
619
|
+
const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, signal);
|
|
620
|
+
const snapshot = await readSnapshot(databasePath, (database) => {
|
|
544
621
|
const pin = metadata(database);
|
|
545
622
|
if (!pin || pin.head !== observedHead)
|
|
546
623
|
return null;
|
|
@@ -554,7 +631,7 @@ export async function readPinnedRepositoryLifecycle(cwd, observedHead, signal, o
|
|
|
554
631
|
validatePinReceipt(database, pin);
|
|
555
632
|
validateRepositoryReceipts(database, rows.map((row) => row.contract), options.summaryOnly === true, options.includeTopology !== false);
|
|
556
633
|
return {
|
|
557
|
-
observedLedgerHead: pin.head,
|
|
634
|
+
observedLedgerHead: pin.head === null ? null : repositoryLedgerHead(pin.head),
|
|
558
635
|
objectFormat: pin.objectFormat,
|
|
559
636
|
emptyTree: pin.emptyTree,
|
|
560
637
|
contracts: rows.flatMap((row) => {
|
|
@@ -575,279 +652,317 @@ export async function readPinnedRepositoryLifecycle(cwd, observedHead, signal, o
|
|
|
575
652
|
: database.prepare("SELECT node_json AS nodeJson FROM snapshot_topology ORDER BY rowid").all().map((row) => parseJson(row.nodeJson)),
|
|
576
653
|
},
|
|
577
654
|
};
|
|
578
|
-
}
|
|
579
|
-
|
|
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
|
-
}
|
|
655
|
+
});
|
|
656
|
+
return snapshot.kind === "read" ? snapshot.value : null;
|
|
590
657
|
}
|
|
591
658
|
export async function publishRepositoryCurrentState(cwd, read, options) {
|
|
592
|
-
const databasePath = await
|
|
659
|
+
const databasePath = await repositoryCurrentStateSnapshotPath(cwd, read.observedLedgerHead, options.signal);
|
|
593
660
|
await fs.mkdir(path.dirname(databasePath), { recursive: true });
|
|
594
661
|
if (await options.confirmHead() !== read.observedLedgerHead)
|
|
595
662
|
return false;
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
|
|
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 */ }
|
|
663
|
+
return withSnapshotWriter(databasePath, options.signal, async () => {
|
|
664
|
+
if (await options.confirmHead() !== read.observedLedgerHead)
|
|
665
|
+
return false;
|
|
666
|
+
const existing = await preparePublicationTarget(databasePath, read.observedLedgerHead);
|
|
667
|
+
if (existing === "published")
|
|
668
|
+
return true;
|
|
669
|
+
if (existing === "transient")
|
|
670
|
+
return false;
|
|
671
|
+
const buildPath = temporarySnapshotPath(databasePath);
|
|
672
|
+
let database;
|
|
643
673
|
try {
|
|
674
|
+
await fs.unlink(buildPath).catch((error) => {
|
|
675
|
+
if (error.code !== "ENOENT")
|
|
676
|
+
throw error;
|
|
677
|
+
});
|
|
678
|
+
database = new DatabaseSync(buildPath);
|
|
679
|
+
database.exec("PRAGMA synchronous = FULL; PRAGMA journal_mode = DELETE; BEGIN IMMEDIATE");
|
|
680
|
+
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");
|
|
681
|
+
database.exec(SCHEMA);
|
|
682
|
+
database.prepare("INSERT INTO snapshot_schema(singleton, version) VALUES (1, ?)").run(REPOSITORY_CURRENT_STATE_VERSION);
|
|
683
|
+
database.prepare("INSERT INTO snapshot_pin(singleton, ledger_head, object_format, empty_tree) VALUES (1, ?, ?, ?)").run(read.observedLedgerHead, read.objectFormat, read.emptyTree);
|
|
684
|
+
const insert = database.prepare("INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json) VALUES (?, ?, ?, ?, ?)");
|
|
685
|
+
const insertHistory = database.prepare("INSERT INTO snapshot_history(contract, position, object_id, entry_json) VALUES (?, ?, ?, ?)");
|
|
686
|
+
const insertSummary = database.prepare("INSERT INTO snapshot_summaries(contract, history_json) VALUES (?, ?)");
|
|
687
|
+
const isolated = new Set(read.isolatedContracts);
|
|
688
|
+
const models = new Map(read.contracts.map((model) => [model.contract, model]));
|
|
689
|
+
for (const contract of new Set([...models.keys(), ...isolated])) {
|
|
690
|
+
const model = models.get(contract) ?? null;
|
|
691
|
+
const diagnostics = read.diagnostics.filter((disease) => disease.contract === contract);
|
|
692
|
+
const status = isolated.has(contract) ? "isolated" : diagnostics.length > 0 ? "diseased" : "healthy";
|
|
693
|
+
const folded = options.fold.contracts.get(contract) ?? null;
|
|
694
|
+
const current = model === null ? null : Object.fromEntries(Object.entries(model).filter(([key]) => key !== "history"));
|
|
695
|
+
insert.run(contract, status, current === null ? null : JSON.stringify(current), folded === null ? null : JSON.stringify(folded), JSON.stringify(diagnostics));
|
|
696
|
+
model?.history.forEach((row, position) => insertHistory.run(contract, position, row.objectId, JSON.stringify(row.entry)));
|
|
697
|
+
if (model)
|
|
698
|
+
insertSummary.run(contract, JSON.stringify(summaryHistory(model.history)));
|
|
699
|
+
}
|
|
700
|
+
database.prepare("INSERT INTO snapshot_repository(singleton, diagnostics_json) VALUES (1, ?)").run(JSON.stringify(read.diagnostics.filter((disease) => disease.contract === undefined)));
|
|
701
|
+
const insertTopology = database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)");
|
|
702
|
+
for (const node of read.topology.nodes)
|
|
703
|
+
insertTopology.run(node.objectId, JSON.stringify(node));
|
|
704
|
+
const insertResource = database.prepare("INSERT INTO snapshot_resources(kind, resource_key, contract) VALUES (?, ?, ?)");
|
|
705
|
+
for (const model of read.contracts) {
|
|
706
|
+
if (model.resources.worktree?.state === "held")
|
|
707
|
+
insertResource.run("worktree", model.resources.worktree.place, model.contract);
|
|
708
|
+
for (const task of model.resources.tasks)
|
|
709
|
+
if (task.state === "held")
|
|
710
|
+
insertResource.run("task", task.taskId, model.contract);
|
|
711
|
+
}
|
|
712
|
+
writeIntegrityReceipts(database);
|
|
713
|
+
await options.beforeCommit?.();
|
|
714
|
+
database.exec("COMMIT");
|
|
644
715
|
database.close();
|
|
716
|
+
// No reader can be holding this new name. An existing same-head file is
|
|
717
|
+
// convergence, never a target for replacement.
|
|
718
|
+
await publishImmutableSnapshot(buildPath, databasePath);
|
|
719
|
+
await garbageCollectSnapshots(databasePath);
|
|
720
|
+
return true;
|
|
645
721
|
}
|
|
646
|
-
catch {
|
|
647
|
-
|
|
648
|
-
|
|
722
|
+
catch (error) {
|
|
723
|
+
try {
|
|
724
|
+
database.exec("ROLLBACK");
|
|
725
|
+
}
|
|
726
|
+
catch { /* database may not have opened */ }
|
|
727
|
+
try {
|
|
728
|
+
database.close();
|
|
729
|
+
}
|
|
730
|
+
catch { /* database may not have opened */ }
|
|
731
|
+
await discardTemporarySnapshot(buildPath);
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
});
|
|
649
735
|
}
|
|
650
736
|
export async function advanceRepositoryCurrentState(cwd, pinnedHead, observedHead, segment, options) {
|
|
651
|
-
const
|
|
737
|
+
const pinnedDatabasePath = await repositoryCurrentStateSnapshotPath(cwd, pinnedHead, options.signal);
|
|
738
|
+
const databasePath = await repositoryCurrentStateSnapshotPath(cwd, observedHead, options.signal);
|
|
652
739
|
if (await options.confirmHead() !== observedHead)
|
|
653
740
|
return false;
|
|
654
|
-
|
|
655
|
-
|
|
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");
|
|
741
|
+
return withSnapshotWriter(databasePath, options.signal, async () => {
|
|
742
|
+
if (await options.confirmHead() !== observedHead)
|
|
662
743
|
return false;
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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) {
|
|
744
|
+
const existing = await preparePublicationTarget(databasePath, observedHead);
|
|
745
|
+
if (existing === "published")
|
|
746
|
+
return true;
|
|
747
|
+
if (existing === "transient")
|
|
748
|
+
return false;
|
|
749
|
+
const buildPath = temporarySnapshotPath(databasePath);
|
|
750
|
+
let database;
|
|
751
|
+
try {
|
|
752
|
+
await fs.copyFile(pinnedDatabasePath, buildPath);
|
|
753
|
+
database = new DatabaseSync(buildPath);
|
|
754
|
+
database.exec("PRAGMA synchronous = FULL; BEGIN IMMEDIATE");
|
|
755
|
+
const versions = database.prepare("SELECT version FROM snapshot_schema WHERE singleton = 1").all();
|
|
756
|
+
if (versions.length !== 1 || versions[0]?.version !== REPOSITORY_CURRENT_STATE_VERSION) {
|
|
694
757
|
database.exec("ROLLBACK");
|
|
758
|
+
database.close();
|
|
695
759
|
return false;
|
|
696
760
|
}
|
|
697
|
-
|
|
761
|
+
}
|
|
762
|
+
catch {
|
|
763
|
+
try {
|
|
764
|
+
database.close();
|
|
765
|
+
}
|
|
766
|
+
catch { /* replacement recovery owns unusable bytes */ }
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
const pin = metadata(database);
|
|
771
|
+
if (!pin || pin.head !== pinnedHead) {
|
|
698
772
|
database.exec("ROLLBACK");
|
|
699
773
|
return false;
|
|
700
774
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
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);
|
|
775
|
+
validatePinReceipt(database, pin);
|
|
776
|
+
assertReceipt(database, "topology", topologyDigest(database));
|
|
777
|
+
assertReceipt(database, "resources", resourcesDigest(database));
|
|
778
|
+
for (const contract of new Set(segment.transactions.map((transaction) => transaction.entries[0]?.entry.contract).filter((contract) => contract !== undefined))) {
|
|
779
|
+
if (database.prepare("SELECT 1 FROM snapshot_contracts WHERE contract = ?").get(contract))
|
|
780
|
+
validateContractReceipt(database, contract, false);
|
|
714
781
|
}
|
|
715
|
-
|
|
716
|
-
|
|
782
|
+
const acceptedTopology = new Set(database.prepare("SELECT object_id AS objectId FROM snapshot_topology").all().map((row) => row.objectId));
|
|
783
|
+
const pendingJoins = new Map(segment.joins.map((join) => [join.objectId, join]));
|
|
784
|
+
const promoteReadyJoins = () => {
|
|
785
|
+
let promoted;
|
|
786
|
+
do {
|
|
787
|
+
promoted = false;
|
|
788
|
+
for (const join of pendingJoins.values()) {
|
|
789
|
+
if (!join.parents.every((parent) => acceptedTopology.has(parent)))
|
|
790
|
+
continue;
|
|
791
|
+
database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(join.objectId, JSON.stringify({
|
|
792
|
+
objectId: join.objectId,
|
|
793
|
+
type: "join",
|
|
794
|
+
ledgerParents: join.parents,
|
|
795
|
+
}));
|
|
796
|
+
acceptedTopology.add(join.objectId);
|
|
797
|
+
pendingJoins.delete(join.objectId);
|
|
798
|
+
promoted = true;
|
|
799
|
+
}
|
|
800
|
+
} while (promoted);
|
|
801
|
+
};
|
|
802
|
+
for (const transaction of segment.transactions) {
|
|
803
|
+
promoteReadyJoins();
|
|
804
|
+
if ((transaction.predecessor !== null && !acceptedTopology.has(transaction.predecessor)) || transaction.entries.length === 0) {
|
|
717
805
|
database.exec("ROLLBACK");
|
|
718
806
|
return false;
|
|
719
807
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
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
|
-
}
|
|
808
|
+
if (transaction.entries.some((entry) => database.prepare("SELECT 1 FROM snapshot_history WHERE object_id = ?").get(entry.objectId) !== undefined)) {
|
|
809
|
+
database.exec("ROLLBACK");
|
|
810
|
+
return false;
|
|
731
811
|
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
812
|
+
const contract = transaction.entries[0].entry.contract;
|
|
813
|
+
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);
|
|
814
|
+
let state = row?.foldJson ? parseJson(row.foldJson) : undefined;
|
|
815
|
+
const priorModel = row?.currentJson ? currentModel(database, contract, row.currentJson) : null;
|
|
816
|
+
const first = transaction.entries[0];
|
|
817
|
+
if (first.entry.kind === "bind" && (state || row)) {
|
|
818
|
+
const diagnostics = [...(row ? parseJson(row.diagnosticsJson) : []), {
|
|
819
|
+
objectId: first.objectId,
|
|
820
|
+
code: "same-contract-divergence",
|
|
821
|
+
contract,
|
|
822
|
+
detail: "incremental bind diverges from an already admitted contract",
|
|
823
|
+
}];
|
|
824
|
+
database.prepare("UPDATE snapshot_contracts SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = ? WHERE contract = ?").run(JSON.stringify(diagnostics), contract);
|
|
738
825
|
}
|
|
739
826
|
else {
|
|
740
|
-
if (first.entry.kind
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
827
|
+
if (first.entry.kind !== "bind" && (!state || first.causalParent !== state.entryObjectIds.at(-1))) {
|
|
828
|
+
database.exec("ROLLBACK");
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
let staged = state === undefined ? undefined : structuredClone(state);
|
|
832
|
+
let disease;
|
|
833
|
+
const basePosition = staged?.entryObjectIds.length ?? 0;
|
|
834
|
+
for (let index = 0; index < transaction.entries.length; index += 1) {
|
|
835
|
+
const entry = transaction.entries[index];
|
|
836
|
+
const applied = applyFoldEntry(staged, { objectId: entry.objectId, entry: entry.entry, position: basePosition + index });
|
|
837
|
+
staged = applied.state;
|
|
838
|
+
if (applied.disease) {
|
|
839
|
+
disease = { ...applied.disease, transactionObjectId: transaction.objectId };
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (disease || !staged) {
|
|
844
|
+
const diagnostics = [...(row ? parseJson(row.diagnosticsJson) : []), ...(disease ? [disease] : [])];
|
|
845
|
+
if (row)
|
|
846
|
+
database.prepare("UPDATE snapshot_contracts SET status = 'diseased', diagnostics_json = ? WHERE contract = ?").run(JSON.stringify(diagnostics), contract);
|
|
847
|
+
else
|
|
848
|
+
database.prepare("INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json) VALUES (?, 'isolated', NULL, NULL, ?)").run(contract, JSON.stringify(diagnostics));
|
|
849
|
+
}
|
|
850
|
+
else {
|
|
851
|
+
if (first.entry.kind === "bind" && (first.entry.data.after?.length ?? 0) > 0) {
|
|
852
|
+
const unavailable = first.entry.data.after.filter((dependency) => {
|
|
853
|
+
const prerequisite = database.prepare("SELECT status, fold_json AS foldJson FROM snapshot_contracts WHERE contract = ?").get(dependency);
|
|
854
|
+
if (!prerequisite || prerequisite.status !== "healthy" || prerequisite.foldJson === null)
|
|
855
|
+
return true;
|
|
856
|
+
return false;
|
|
857
|
+
});
|
|
858
|
+
if (unavailable.length > 0) {
|
|
859
|
+
const diagnostic = {
|
|
860
|
+
objectId: first.objectId,
|
|
861
|
+
code: "causal-prerequisite",
|
|
862
|
+
contract,
|
|
863
|
+
detail: `after prerequisites are not admitted healthy contracts: ${unavailable.join(", ")}`,
|
|
864
|
+
};
|
|
865
|
+
database.prepare(`
|
|
755
866
|
INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json)
|
|
756
867
|
VALUES (?, 'isolated', NULL, NULL, ?)
|
|
757
868
|
ON CONFLICT(contract) DO UPDATE SET status = 'isolated', current_json = NULL, fold_json = NULL, diagnostics_json = excluded.diagnostics_json
|
|
758
869
|
`).run(contract, JSON.stringify([diagnostic]));
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
870
|
+
acceptedTopology.add(transaction.objectId);
|
|
871
|
+
database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(transaction.objectId, JSON.stringify({
|
|
872
|
+
objectId: transaction.objectId,
|
|
873
|
+
type: "transaction",
|
|
874
|
+
ledgerParents: transaction.predecessor === null ? [] : [transaction.predecessor],
|
|
875
|
+
verb: transaction.transaction.verb,
|
|
876
|
+
contract,
|
|
877
|
+
entryObjectIds: transaction.entries.map((entry) => entry.objectId),
|
|
878
|
+
finalEntryObjectId: transaction.entries.at(-1).objectId,
|
|
879
|
+
}));
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
770
882
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
database.prepare(`
|
|
883
|
+
const history = [...(priorModel?.history ?? []), ...transaction.entries.map((entry) => ({ objectId: entry.objectId, entry: entry.entry }))];
|
|
884
|
+
const model = await options.materialize(staged, history);
|
|
885
|
+
database.prepare(`
|
|
775
886
|
INSERT INTO snapshot_contracts(contract, status, current_json, fold_json, diagnostics_json)
|
|
776
887
|
VALUES (?, 'healthy', ?, ?, '[]')
|
|
777
888
|
ON CONFLICT(contract) DO UPDATE SET status = 'healthy', current_json = excluded.current_json, fold_json = excluded.fold_json, diagnostics_json = '[]'
|
|
778
889
|
`).run(contract, JSON.stringify(Object.fromEntries(Object.entries(model).filter(([key]) => key !== "history"))), JSON.stringify(staged));
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
890
|
+
database.prepare("DELETE FROM snapshot_history WHERE contract = ?").run(contract);
|
|
891
|
+
const insertHistory = database.prepare("INSERT INTO snapshot_history(contract, position, object_id, entry_json) VALUES (?, ?, ?, ?)");
|
|
892
|
+
model.history.forEach((historyRow, position) => insertHistory.run(contract, position, historyRow.objectId, JSON.stringify(historyRow.entry)));
|
|
893
|
+
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)));
|
|
894
|
+
database.prepare("DELETE FROM snapshot_resources WHERE contract = ?").run(contract);
|
|
895
|
+
const insertResource = database.prepare("INSERT INTO snapshot_resources(kind, resource_key, contract) VALUES (?, ?, ?)");
|
|
896
|
+
if (model.resources.worktree?.state === "held")
|
|
897
|
+
insertResource.run("worktree", model.resources.worktree.place, contract);
|
|
898
|
+
for (const task of model.resources.tasks)
|
|
899
|
+
if (task.state === "held")
|
|
900
|
+
insertResource.run("task", task.taskId, contract);
|
|
901
|
+
const collisions = [
|
|
902
|
+
...(model.resources.worktree?.state === "held" ? [{ kind: "worktree", key: model.resources.worktree.place }] : []),
|
|
903
|
+
...model.resources.tasks.filter((task) => task.state === "held").map((task) => ({ kind: "task", key: task.taskId })),
|
|
904
|
+
];
|
|
905
|
+
for (const resource of collisions) {
|
|
906
|
+
const owners = database.prepare("SELECT contract FROM snapshot_resources WHERE kind = ? AND resource_key = ?").all(resource.kind, resource.key);
|
|
907
|
+
if (owners.length < 2)
|
|
908
|
+
continue;
|
|
909
|
+
for (const owner of owners) {
|
|
910
|
+
const ownerRow = database.prepare("SELECT diagnostics_json AS diagnosticsJson FROM snapshot_contracts WHERE contract = ?").get(owner.contract);
|
|
911
|
+
const diagnostic = {
|
|
912
|
+
objectId: first.objectId,
|
|
913
|
+
code: resource.kind === "worktree" ? "worktree-place-collision" : "task-ownership-collision",
|
|
914
|
+
contract: owner.contract,
|
|
915
|
+
detail: `${resource.kind} ${resource.key} is held by ${owners.map((candidate) => candidate.contract).sort().join(", ")}`,
|
|
916
|
+
};
|
|
917
|
+
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);
|
|
918
|
+
}
|
|
807
919
|
}
|
|
808
920
|
}
|
|
809
921
|
}
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
922
|
+
const insertLifecycleTopology = database.prepare("INSERT OR IGNORE INTO snapshot_topology(object_id, node_json) VALUES (?, ?)");
|
|
923
|
+
for (const entry of transaction.entries)
|
|
924
|
+
insertLifecycleTopology.run(entry.objectId, JSON.stringify({
|
|
925
|
+
objectId: entry.objectId,
|
|
926
|
+
type: "lifecycle",
|
|
927
|
+
ledgerParents: entry.causalParent === null ? [] : [entry.causalParent],
|
|
928
|
+
}));
|
|
929
|
+
database.prepare("INSERT INTO snapshot_topology(object_id, node_json) VALUES (?, ?)").run(transaction.objectId, JSON.stringify({
|
|
930
|
+
objectId: transaction.objectId,
|
|
931
|
+
type: "transaction",
|
|
932
|
+
ledgerParents: transaction.predecessor === null ? [] : [transaction.predecessor],
|
|
933
|
+
verb: transaction.transaction.verb,
|
|
934
|
+
contract,
|
|
935
|
+
entryObjectIds: transaction.entries.map((entry) => entry.objectId),
|
|
936
|
+
finalEntryObjectId: transaction.entries.at(-1).objectId,
|
|
817
937
|
}));
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
938
|
+
acceptedTopology.add(transaction.objectId);
|
|
939
|
+
}
|
|
940
|
+
promoteReadyJoins();
|
|
941
|
+
if (pendingJoins.size > 0 || !acceptedTopology.has(observedHead)) {
|
|
942
|
+
database.exec("ROLLBACK");
|
|
943
|
+
return false;
|
|
944
|
+
}
|
|
945
|
+
database.prepare("UPDATE snapshot_pin SET ledger_head = ? WHERE singleton = 1").run(observedHead);
|
|
946
|
+
writeIntegrityReceipts(database);
|
|
947
|
+
database.exec("COMMIT");
|
|
948
|
+
database.close();
|
|
949
|
+
await publishImmutableSnapshot(buildPath, databasePath);
|
|
950
|
+
await garbageCollectSnapshots(databasePath);
|
|
951
|
+
return true;
|
|
828
952
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
953
|
+
catch {
|
|
954
|
+
try {
|
|
955
|
+
database.exec("ROLLBACK");
|
|
956
|
+
}
|
|
957
|
+
catch { /* handled by recovery */ }
|
|
832
958
|
return false;
|
|
833
959
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
catch {
|
|
841
|
-
try {
|
|
842
|
-
database.exec("ROLLBACK");
|
|
960
|
+
finally {
|
|
961
|
+
try {
|
|
962
|
+
database.close();
|
|
963
|
+
}
|
|
964
|
+
catch { /* database may not have opened */ }
|
|
965
|
+
await discardTemporarySnapshot(buildPath);
|
|
843
966
|
}
|
|
844
|
-
|
|
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));
|
|
967
|
+
});
|
|
853
968
|
}
|