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