skill-family-harness-node 0.1.2 → 0.2.0

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.
@@ -0,0 +1,1050 @@
1
+ import { constants as FS_CONSTANTS } from "node:fs";
2
+ import {
3
+ link,
4
+ lstat,
5
+ mkdir,
6
+ open,
7
+ readdir,
8
+ realpath,
9
+ rename,
10
+ unlink,
11
+ } from "node:fs/promises";
12
+ import { randomBytes } from "node:crypto";
13
+ import path from "node:path";
14
+ import process from "node:process";
15
+ import {
16
+ canonicalJson,
17
+ compileSchema,
18
+ detectDialect,
19
+ digestDocument,
20
+ findSchemaByObject,
21
+ } from "skill-family-contracts";
22
+ import { HARNESS_ERROR_KINDS, mechanismError } from "./errors.mjs";
23
+ import { validateContractDocument } from "./validation.mjs";
24
+
25
+ export const STATE_GENESIS_DIGEST = "0".repeat(64);
26
+
27
+ const EVENT_SCHEMA_ID = findSchemaByObject("state-event-envelope").$id;
28
+ const SNAPSHOT_SCHEMA_ID = findSchemaByObject("state-snapshot-metadata").$id;
29
+ const EVENT_KIND = "skill-family.state-event-envelope";
30
+ const SNAPSHOT_KIND = "skill-family.state-snapshot-metadata";
31
+ const EVENTS_DIR = "events";
32
+ const SNAPSHOTS_DIR = "snapshots";
33
+ const LOCK_FILE = "writer.lock";
34
+ const RECOVERY_FILE = "writer-recovery.lock";
35
+ const MUTATION_FILE = "writer-mutation.lock";
36
+ const FENCING_FILE = "fencing-counter.json";
37
+ const HEAD_FILE = "chain-head.json";
38
+ const EVENT_NAME = /^(\d{6})\.json$/;
39
+ const SNAPSHOT_NAME = /^(\d{6})\.json$/;
40
+ const TEMP_NAME = /^\.(event|snapshot|control)-[0-9a-f]+\.tmp$/;
41
+ const ROOT_NAMES = new Set([
42
+ EVENTS_DIR,
43
+ SNAPSHOTS_DIR,
44
+ LOCK_FILE,
45
+ RECOVERY_FILE,
46
+ MUTATION_FILE,
47
+ FENCING_FILE,
48
+ HEAD_FILE,
49
+ ]);
50
+
51
+ const TEST_HOOKS = new WeakMap();
52
+
53
+ function failure(kind, message, details) {
54
+ return mechanismError(kind, message, details);
55
+ }
56
+
57
+ function randomId() {
58
+ return randomBytes(12).toString("hex");
59
+ }
60
+
61
+ function clockMillis(clock) {
62
+ const raw = clock?.now?.() ?? Date.now();
63
+ const value = raw instanceof Date ? raw.getTime() : raw;
64
+ if (!Number.isFinite(value)) throw new TypeError("state store clock must return a finite time");
65
+ return value;
66
+ }
67
+
68
+ function assertJsonValue(value, seen = new Set(), location = "value") {
69
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
70
+ if (typeof value === "number") {
71
+ if (!Number.isFinite(value)) throw new TypeError(`${location} contains a non-finite number`);
72
+ return;
73
+ }
74
+ if (typeof value !== "object") throw new TypeError(`${location} is not JSON data`);
75
+ if (seen.has(value)) throw new TypeError(`${location} contains a cycle`);
76
+ seen.add(value);
77
+ try {
78
+ if (Array.isArray(value)) {
79
+ for (let index = 0; index < value.length; index += 1) {
80
+ if (!Object.hasOwn(value, index)) throw new TypeError(`${location} contains a sparse array`);
81
+ assertJsonValue(value[index], seen, `${location}[${index}]`);
82
+ }
83
+ return;
84
+ }
85
+ const prototype = Object.getPrototypeOf(value);
86
+ if (prototype !== Object.prototype && prototype !== null) {
87
+ throw new TypeError(`${location} must contain only plain JSON objects`);
88
+ }
89
+ for (const [key, child] of Object.entries(value)) {
90
+ assertJsonValue(child, seen, `${location}.${key}`);
91
+ }
92
+ } finally {
93
+ seen.delete(value);
94
+ }
95
+ }
96
+
97
+ function assertSafeRegular(stats, label) {
98
+ if (stats.isSymbolicLink() || !stats.isFile() || stats.nlink !== 1) {
99
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `${label} must be one ordinary, unlinked file`);
100
+ }
101
+ }
102
+
103
+ function assertSafeDirectory(stats, label) {
104
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
105
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `${label} must be one real directory`);
106
+ }
107
+ }
108
+
109
+ function sameIdentity(actual, expected) {
110
+ return actual.dev === expected.dev && actual.ino === expected.ino;
111
+ }
112
+
113
+ async function assertNoSymlinkAncestors(absolute, { allowMissingLeaf = false } = {}) {
114
+ const parsed = path.parse(absolute);
115
+ const parts = absolute.slice(parsed.root.length).split(path.sep).filter(Boolean);
116
+ let current = parsed.root;
117
+ for (let index = 0; index < parts.length; index += 1) {
118
+ current = path.join(current, parts[index]);
119
+ let stats;
120
+ try {
121
+ stats = await lstat(current);
122
+ } catch (cause) {
123
+ if (allowMissingLeaf && cause?.code === "ENOENT" && index === parts.length - 1) return;
124
+ throw failure(HARNESS_ERROR_KINDS.INVALID_ROOT, "state store path ancestry cannot be inspected", {
125
+ path: current,
126
+ code: cause?.code,
127
+ });
128
+ }
129
+ if (stats.isSymbolicLink()) {
130
+ throw failure(HARNESS_ERROR_KINDS.INVALID_ROOT, "state store root cannot have a symbolic-link ancestor", {
131
+ path: current,
132
+ });
133
+ }
134
+ if (index < parts.length - 1 && !stats.isDirectory()) {
135
+ throw failure(HARNESS_ERROR_KINDS.INVALID_ROOT, "state store ancestor must be a directory", { path: current });
136
+ }
137
+ }
138
+ }
139
+
140
+ async function readSafeFile(file, { optional = false, label = "state entry" } = {}) {
141
+ let before;
142
+ try {
143
+ before = await lstat(file);
144
+ } catch (cause) {
145
+ if (optional && cause?.code === "ENOENT") return null;
146
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `${label} cannot be inspected`, { code: cause?.code });
147
+ }
148
+ assertSafeRegular(before, label);
149
+ const noFollow = FS_CONSTANTS.O_NOFOLLOW ?? 0;
150
+ let handle;
151
+ try {
152
+ handle = await open(file, FS_CONSTANTS.O_RDONLY | noFollow);
153
+ const opened = await handle.stat();
154
+ assertSafeRegular(opened, label);
155
+ if (opened.dev !== before.dev || opened.ino !== before.ino) {
156
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `${label} changed during inspection`);
157
+ }
158
+ return { text: await handle.readFile("utf8"), stats: opened };
159
+ } finally {
160
+ await handle?.close();
161
+ }
162
+ }
163
+
164
+ async function readSafeJson(file, options) {
165
+ const result = await readSafeFile(file, options);
166
+ if (result === null) return null;
167
+ try {
168
+ return { value: JSON.parse(result.text), stats: result.stats };
169
+ } catch {
170
+ throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `${options?.label ?? "state entry"} is not valid JSON`);
171
+ }
172
+ }
173
+
174
+ async function syncDirectory(dir) {
175
+ try {
176
+ const handle = await open(dir, FS_CONSTANTS.O_RDONLY);
177
+ try {
178
+ await handle.sync();
179
+ } finally {
180
+ await handle.close();
181
+ }
182
+ } catch {
183
+ // Some platforms do not support directory fsync. File fsync and atomic
184
+ // namespace operations remain the portable contract.
185
+ }
186
+ }
187
+
188
+ async function writeExclusive(file, bytes, mode = 0o600) {
189
+ const noFollow = FS_CONSTANTS.O_NOFOLLOW ?? 0;
190
+ let handle;
191
+ try {
192
+ handle = await open(
193
+ file,
194
+ FS_CONSTANTS.O_WRONLY | FS_CONSTANTS.O_CREAT | FS_CONSTANTS.O_EXCL | noFollow,
195
+ mode,
196
+ );
197
+ await handle.writeFile(bytes);
198
+ await handle.sync();
199
+ const stats = await handle.stat();
200
+ assertSafeRegular(stats, path.basename(file));
201
+ return stats;
202
+ } finally {
203
+ await handle?.close();
204
+ }
205
+ }
206
+
207
+ async function atomicReplace(file, bytes, prefix = "control") {
208
+ const dir = path.dirname(file);
209
+ const existing = await readSafeFile(file, { optional: true, label: path.basename(file) });
210
+ void existing;
211
+ const temp = path.join(dir, `.${prefix}-${randomId()}.tmp`);
212
+ try {
213
+ await writeExclusive(temp, bytes);
214
+ await rename(temp, file);
215
+ await syncDirectory(dir);
216
+ } catch (cause) {
217
+ await unlink(temp).catch(() => {});
218
+ throw cause;
219
+ }
220
+ }
221
+
222
+ async function appendExclusiveEvent(dir, file, bytes) {
223
+ const temp = path.join(dir, `.event-${randomId()}.tmp`);
224
+ let linked = false;
225
+ try {
226
+ await writeExclusive(temp, bytes);
227
+ await link(temp, file); // atomic create-if-absent; never replaces history
228
+ linked = true;
229
+ await unlink(temp);
230
+ await syncDirectory(dir);
231
+ } catch (cause) {
232
+ if (!linked) await unlink(temp).catch(() => {});
233
+ if (cause?.code === "EEXIST") {
234
+ throw failure(HARNESS_ERROR_KINDS.DUPLICATE_SEQUENCE, "event sequence already exists");
235
+ }
236
+ throw cause;
237
+ }
238
+ }
239
+
240
+ async function prepareRoot(root, { create }) {
241
+ if (typeof root !== "string" || root.length === 0) {
242
+ throw new TypeError("state store root must be a non-empty path");
243
+ }
244
+ const absolute = path.resolve(root);
245
+ await assertNoSymlinkAncestors(absolute, { allowMissingLeaf: create });
246
+ let stats;
247
+ try {
248
+ stats = await lstat(absolute);
249
+ } catch (cause) {
250
+ if (!create || cause?.code !== "ENOENT") {
251
+ throw failure(HARNESS_ERROR_KINDS.INVALID_ROOT, "state store root does not exist");
252
+ }
253
+ try {
254
+ await mkdir(absolute);
255
+ stats = await lstat(absolute);
256
+ } catch (mkdirCause) {
257
+ throw failure(HARNESS_ERROR_KINDS.INVALID_ROOT, "state store root cannot be created", { code: mkdirCause?.code });
258
+ }
259
+ }
260
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
261
+ throw failure(HARNESS_ERROR_KINDS.INVALID_ROOT, "state store root must be a real directory, not a link or special entry");
262
+ }
263
+ const resolved = await realpath(absolute);
264
+ if (resolved !== absolute) {
265
+ throw failure(HARNESS_ERROR_KINDS.INVALID_ROOT, "state store root must use its canonical symlink-free path");
266
+ }
267
+ return absolute;
268
+ }
269
+
270
+ async function ensureDirectory(root, name) {
271
+ const target = path.join(root, name);
272
+ let stats;
273
+ try {
274
+ stats = await lstat(target);
275
+ } catch (cause) {
276
+ if (cause?.code !== "ENOENT") throw cause;
277
+ await mkdir(target);
278
+ stats = await lstat(target);
279
+ }
280
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
281
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `${name} must be a real directory`);
282
+ }
283
+ return { path: target, stats };
284
+ }
285
+
286
+ async function ensureLayout(root) {
287
+ const rootStats = await lstat(root);
288
+ assertSafeDirectory(rootStats, "state root");
289
+ const events = await ensureDirectory(root, EVENTS_DIR);
290
+ const snapshots = await ensureDirectory(root, SNAPSHOTS_DIR);
291
+ for (const entry of await readdir(root, { withFileTypes: true })) {
292
+ if (ROOT_NAMES.has(entry.name) || TEMP_NAME.test(entry.name)) continue;
293
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `unexpected entry in state root: ${entry.name}`);
294
+ }
295
+ return {
296
+ eventsDir: events.path,
297
+ snapshotsDir: snapshots.path,
298
+ identities: {
299
+ root: { dev: rootStats.dev, ino: rootStats.ino },
300
+ events: { dev: events.stats.dev, ino: events.stats.ino },
301
+ snapshots: { dev: snapshots.stats.dev, ino: snapshots.stats.ino },
302
+ },
303
+ };
304
+ }
305
+
306
+ async function assertLayoutIdentity(store) {
307
+ assertStore(store);
308
+ await assertNoSymlinkAncestors(store.root);
309
+ const entries = [
310
+ [store.root, store.identities.root, "state root"],
311
+ [path.join(store.root, EVENTS_DIR), store.identities.events, EVENTS_DIR],
312
+ [path.join(store.root, SNAPSHOTS_DIR), store.identities.snapshots, SNAPSHOTS_DIR],
313
+ ];
314
+ for (const [target, expected, label] of entries) {
315
+ let stats;
316
+ try {
317
+ stats = await lstat(target);
318
+ } catch (cause) {
319
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `${label} cannot be revalidated`, { code: cause?.code });
320
+ }
321
+ assertSafeDirectory(stats, label);
322
+ if (!sameIdentity(stats, expected)) {
323
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `${label} was replaced after the state store opened`);
324
+ }
325
+ }
326
+ for (const entry of await readdir(store.root, { withFileTypes: true })) {
327
+ if (ROOT_NAMES.has(entry.name) || TEMP_NAME.test(entry.name)) continue;
328
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `unexpected entry in state root: ${entry.name}`);
329
+ }
330
+ }
331
+
332
+ async function inspectLayoutReadOnly(root) {
333
+ await assertNoSymlinkAncestors(root);
334
+ for (const name of [EVENTS_DIR, SNAPSHOTS_DIR]) {
335
+ const target = path.join(root, name);
336
+ const stats = await lstat(target).catch((cause) => {
337
+ if (cause?.code === "ENOENT") return null;
338
+ throw cause;
339
+ });
340
+ if (stats && (stats.isSymbolicLink() || !stats.isDirectory())) {
341
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `${name} must be a real directory`);
342
+ }
343
+ }
344
+ for (const entry of await readdir(root, { withFileTypes: true })) {
345
+ if (ROOT_NAMES.has(entry.name) || TEMP_NAME.test(entry.name)) continue;
346
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, `unexpected entry in state root: ${entry.name}`);
347
+ }
348
+ }
349
+
350
+ function normalizePayloadSchemas(payloadSchemas) {
351
+ if (!payloadSchemas || typeof payloadSchemas !== "object" || Array.isArray(payloadSchemas)) {
352
+ throw new TypeError("payloadSchemas must map eventType to a version-to-schema map");
353
+ }
354
+ const validators = new Map();
355
+ for (const [eventType, versions] of Object.entries(payloadSchemas)) {
356
+ if (!versions || typeof versions !== "object" || Array.isArray(versions)) {
357
+ throw new TypeError(`payloadSchemas.${eventType} must be a version map`);
358
+ }
359
+ for (const [versionText, schema] of Object.entries(versions)) {
360
+ if (!/^[1-9]\d*$/.test(versionText) || !schema || typeof schema !== "object" || Array.isArray(schema)) {
361
+ throw new TypeError(`payloadSchemas.${eventType}.${versionText} is not a frozen JSON Schema pair`);
362
+ }
363
+ const dialect = detectDialect(schema) ?? "2020-12";
364
+ validators.set(`${eventType}\u0000${Number(versionText)}`, compileSchema({ schema }, { dialect, policy: "strict" }));
365
+ }
366
+ }
367
+ if (validators.size === 0) throw new TypeError("payloadSchemas must register at least one eventType/version pair");
368
+ return validators;
369
+ }
370
+
371
+ function validatePayload(validators, eventType, version, payload, position) {
372
+ const validator = validators.get(`${eventType}\u0000${version}`);
373
+ if (!validator || !validator(payload)) {
374
+ throw failure(HARNESS_ERROR_KINDS.EVENT_SCHEMA_INVALID, "eventType/payloadSchemaVersion is unknown or payload validation failed", {
375
+ position,
376
+ eventType,
377
+ payloadSchemaVersion: version,
378
+ errors: validator?.errors?.map((entry) => entry.message) ?? [],
379
+ });
380
+ }
381
+ }
382
+
383
+ function recordDigest(record) {
384
+ const { recordDigest: _ignored, ...unsigned } = record;
385
+ return digestDocument(unsigned);
386
+ }
387
+
388
+ async function readEvent(file, sequence, validators) {
389
+ let parsed;
390
+ try {
391
+ parsed = await readSafeJson(file, { label: `event ${sequence}` });
392
+ } catch (cause) {
393
+ if (cause?.details?.kind === HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY) throw cause;
394
+ throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `event ${sequence} is unreadable`, { position: sequence });
395
+ }
396
+ const record = parsed.value;
397
+ const checked = validateContractDocument(record, { schemaId: EVENT_SCHEMA_ID });
398
+ if (!checked.valid) {
399
+ throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `event ${sequence} violates its envelope`, { position: sequence });
400
+ }
401
+ validatePayload(validators, record.eventType, record.payloadSchemaVersion, record.payload, sequence);
402
+ return record;
403
+ }
404
+
405
+ async function scanEvents(store) {
406
+ await assertLayoutIdentity(store);
407
+ const entries = await readdir(store.eventsDir, { withFileTypes: true });
408
+ const sequences = [];
409
+ for (const entry of entries) {
410
+ if (TEMP_NAME.test(entry.name)) {
411
+ await readSafeFile(path.join(store.eventsDir, entry.name), { label: "event staging file" });
412
+ continue;
413
+ }
414
+ const match = EVENT_NAME.exec(entry.name);
415
+ if (!match) throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `unexpected event entry: ${entry.name}`, { position: 0 });
416
+ sequences.push(Number(match[1]));
417
+ }
418
+ sequences.sort((a, b) => a - b);
419
+ let previousDigest = STATE_GENESIS_DIGEST;
420
+ let previousFencing = 0;
421
+ const eventIds = new Set();
422
+ const idempotencyKeys = new Set();
423
+ const fencingOwners = new Map();
424
+ const records = [];
425
+ for (let index = 0; index < sequences.length; index += 1) {
426
+ const expected = index + 1;
427
+ if (sequences[index] !== expected) {
428
+ throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `event sequence gap at ${expected}`, { position: expected });
429
+ }
430
+ const record = await readEvent(path.join(store.eventsDir, `${String(expected).padStart(6, "0")}.json`), expected, store.validators);
431
+ if (record.sequence !== expected || record.prevDigest !== previousDigest || record.recordDigest !== recordDigest(record)) {
432
+ throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `event chain breaks at ${expected}`, { position: expected });
433
+ }
434
+ if (record.writerFencing < previousFencing) {
435
+ throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `writer fencing regresses at ${expected}`, { position: expected });
436
+ }
437
+ const knownOwner = fencingOwners.get(record.writerFencing);
438
+ if (knownOwner && knownOwner !== record.writerOwner) {
439
+ throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `one fencing token has multiple owners at ${expected}`, { position: expected });
440
+ }
441
+ if (eventIds.has(record.eventId) || idempotencyKeys.has(record.idempotencyKey)) {
442
+ throw failure(HARNESS_ERROR_KINDS.CHAIN_BROKEN, `duplicate event identity at ${expected}`, { position: expected });
443
+ }
444
+ fencingOwners.set(record.writerFencing, record.writerOwner);
445
+ eventIds.add(record.eventId);
446
+ idempotencyKeys.add(record.idempotencyKey);
447
+ records.push(record);
448
+ previousDigest = record.recordDigest;
449
+ previousFencing = record.writerFencing;
450
+ }
451
+ return { records, eventCount: records.length, headDigest: previousDigest, maxFencing: previousFencing };
452
+ }
453
+
454
+ function lockRecord({ owner, fencing, acquisitionId, acquiredAt }) {
455
+ return { schemaVersion: 1, owner, fencing, acquisitionId, acquiredAt };
456
+ }
457
+
458
+ function validateLock(record) {
459
+ if (
460
+ !record || record.schemaVersion !== 1 || typeof record.owner !== "string" || record.owner.length === 0 ||
461
+ !Number.isSafeInteger(record.fencing) || record.fencing < 0 ||
462
+ typeof record.acquisitionId !== "string" || !/^[0-9a-f]{24}$/.test(record.acquisitionId) ||
463
+ !Number.isFinite(Date.parse(record.acquiredAt))
464
+ ) {
465
+ throw failure(HARNESS_ERROR_KINDS.LOCK_CORRUPT, "writer lock is malformed");
466
+ }
467
+ return record;
468
+ }
469
+
470
+ async function readLock(root, { optional = false } = {}) {
471
+ const parsed = await readSafeJson(path.join(root, LOCK_FILE), { optional, label: LOCK_FILE });
472
+ return parsed === null ? null : { record: validateLock(parsed.value), stats: parsed.stats };
473
+ }
474
+
475
+ async function removeSameFile(file, expectedStats) {
476
+ const current = await lstat(file).catch(() => null);
477
+ if (current && current.dev === expectedStats.dev && current.ino === expectedStats.ino) await unlink(file);
478
+ }
479
+
480
+ async function allocateFencing(store, lowerBound = 0) {
481
+ const counterPath = path.join(store.root, FENCING_FILE);
482
+ const parsed = await readSafeJson(counterPath, { optional: true, label: FENCING_FILE });
483
+ let previous = 0;
484
+ if (parsed !== null) {
485
+ const value = parsed.value;
486
+ if (value?.schemaVersion !== 1 || !Number.isSafeInteger(value.lastFencing) || value.lastFencing < 0) {
487
+ throw failure(HARNESS_ERROR_KINDS.LOCK_CORRUPT, "fencing counter is malformed");
488
+ }
489
+ previous = value.lastFencing;
490
+ }
491
+ const fencing = Math.max(previous, lowerBound) + 1;
492
+ await atomicReplace(counterPath, `${canonicalJson({ schemaVersion: 1, lastFencing: fencing })}\n`);
493
+ return fencing;
494
+ }
495
+
496
+ function assertStore(store) {
497
+ if (!store || store.__stateStore !== true) throw new TypeError("expected a state store handle");
498
+ if (store.closed) throw failure(HARNESS_ERROR_KINDS.STORE_CLOSED, "state store is closed");
499
+ }
500
+
501
+ async function assertWriter(store) {
502
+ assertStore(store);
503
+ await assertLayoutIdentity(store);
504
+ const lock = await readLock(store.root);
505
+ if (
506
+ lock.record.owner !== store.owner || lock.record.fencing !== store.fencing ||
507
+ lock.record.acquisitionId !== store.acquisitionId
508
+ ) {
509
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "writer fencing no longer matches the active lock", {
510
+ owner: store.owner,
511
+ fencing: store.fencing,
512
+ });
513
+ }
514
+ }
515
+
516
+ async function buildStore(root, owner, fencing, acquisitionId, validators, clock) {
517
+ const { eventsDir, snapshotsDir, identities } = await ensureLayout(root);
518
+ return {
519
+ __stateStore: true,
520
+ root,
521
+ owner,
522
+ fencing,
523
+ acquisitionId,
524
+ validators,
525
+ clock,
526
+ eventsDir,
527
+ snapshotsDir,
528
+ identities,
529
+ closed: false,
530
+ };
531
+ }
532
+
533
+ function mutationRecord(store, operation) {
534
+ return {
535
+ schemaVersion: 1,
536
+ owner: store.owner,
537
+ fencing: store.fencing,
538
+ acquisitionId: store.acquisitionId,
539
+ operation,
540
+ mutationId: randomId(),
541
+ };
542
+ }
543
+
544
+ async function acquireMutation(root, record) {
545
+ const file = path.join(root, MUTATION_FILE);
546
+ let stats;
547
+ try {
548
+ stats = await writeExclusive(file, `${canonicalJson(record)}\n`);
549
+ } catch (cause) {
550
+ if (cause?.code === "EEXIST") {
551
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "another state-store mutation is in progress", {
552
+ operation: record.operation,
553
+ });
554
+ }
555
+ throw cause;
556
+ }
557
+ return { file, stats };
558
+ }
559
+
560
+ async function reclaimMutationForConfirmedRecovery(root, expectedOwner, expectedFencing) {
561
+ const file = path.join(root, MUTATION_FILE);
562
+ const parsed = await readSafeJson(file, { label: MUTATION_FILE });
563
+ if (
564
+ parsed.value?.schemaVersion !== 1 || parsed.value.owner !== expectedOwner ||
565
+ parsed.value.fencing !== expectedFencing || typeof parsed.value.mutationId !== "string"
566
+ ) {
567
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "active mutation does not match the explicitly confirmed writer");
568
+ }
569
+ await removeSameFile(file, parsed.stats);
570
+ const remaining = await readSafeFile(file, { optional: true, label: MUTATION_FILE });
571
+ if (remaining !== null) {
572
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "active mutation changed during explicit recovery");
573
+ }
574
+ }
575
+
576
+ async function acquireRecoveryMutation(root, record) {
577
+ try {
578
+ return await acquireMutation(root, record);
579
+ } catch (cause) {
580
+ if (cause?.details?.kind !== HARNESS_ERROR_KINDS.STORE_LOCKED) throw cause;
581
+ // `confirmOwnerTerminated` was checked before this function is reached.
582
+ // Reclaiming a matching guard therefore does not add a second recovery
583
+ // boundary or infer liveness from time/PID. A falsely confirmed live
584
+ // writer is still fenced by guard + writer-lock revalidation before write.
585
+ await reclaimMutationForConfirmedRecovery(root, record.owner, record.fencing);
586
+ return acquireMutation(root, record);
587
+ }
588
+ }
589
+
590
+ async function releaseMutation(guard) {
591
+ const current = await lstat(guard.file).catch((cause) => {
592
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, "state-store mutation guard disappeared before release", {
593
+ code: cause?.code,
594
+ });
595
+ });
596
+ assertSafeRegular(current, MUTATION_FILE);
597
+ if (!sameIdentity(current, guard.stats)) {
598
+ throw failure(HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY, "state-store mutation guard was replaced before release");
599
+ }
600
+ await unlink(guard.file);
601
+ await syncDirectory(path.dirname(guard.file));
602
+ }
603
+
604
+ async function assertMutationGuard(guard) {
605
+ const current = await lstat(guard.file).catch((cause) => {
606
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "state-store mutation ownership was lost", { code: cause?.code });
607
+ });
608
+ assertSafeRegular(current, MUTATION_FILE);
609
+ if (!sameIdentity(current, guard.stats)) {
610
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "state-store mutation ownership was replaced");
611
+ }
612
+ }
613
+
614
+ async function withWriterMutation(store, operation, action) {
615
+ await assertWriter(store);
616
+ const guard = await acquireMutation(store.root, mutationRecord(store, operation));
617
+ let actionFailed = false;
618
+ try {
619
+ await assertWriter(store);
620
+ const recovery = await readSafeFile(path.join(store.root, RECOVERY_FILE), {
621
+ optional: true,
622
+ label: RECOVERY_FILE,
623
+ });
624
+ if (recovery !== null) {
625
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "explicit lock recovery is in progress");
626
+ }
627
+ return await action(guard);
628
+ } catch (cause) {
629
+ actionFailed = true;
630
+ throw cause;
631
+ } finally {
632
+ try {
633
+ await releaseMutation(guard);
634
+ } catch (releaseCause) {
635
+ if (!actionFailed) throw releaseCause;
636
+ }
637
+ }
638
+ }
639
+
640
+ /** Test-only hook registry. It is deliberately not re-exported by index.mjs. */
641
+ export function __setStateStoreTestHooks(store, hooks) {
642
+ assertStore(store);
643
+ if (hooks === null) {
644
+ TEST_HOOKS.delete(store);
645
+ return;
646
+ }
647
+ if (!hooks || typeof hooks !== "object" || Array.isArray(hooks)) {
648
+ throw new TypeError("state-store test hooks must be an object or null");
649
+ }
650
+ TEST_HOOKS.set(store, hooks);
651
+ }
652
+
653
+ async function runTestHook(store, name) {
654
+ const hook = TEST_HOOKS.get(store)?.[name];
655
+ if (hook !== undefined) await hook();
656
+ }
657
+
658
+ export async function inspectStateStoreLock(root, { clock } = {}) {
659
+ if (typeof root !== "string" || root.length === 0) {
660
+ throw new TypeError("state store root must be a non-empty path");
661
+ }
662
+ const rootStats = await lstat(path.resolve(root)).catch((cause) => {
663
+ if (cause?.code === "ENOENT") return null;
664
+ throw cause;
665
+ });
666
+ if (rootStats === null) return { locked: false, rootExists: false, recoveryInProgress: false };
667
+ const safeRoot = await prepareRoot(root, { create: false });
668
+ await inspectLayoutReadOnly(safeRoot);
669
+ const recovery = await readSafeFile(path.join(safeRoot, RECOVERY_FILE), { optional: true, label: RECOVERY_FILE });
670
+ const lock = await readLock(safeRoot, { optional: true });
671
+ if (lock === null) return { locked: false, rootExists: true, recoveryInProgress: recovery !== null };
672
+ const acquiredAt = Date.parse(lock.record.acquiredAt);
673
+ return {
674
+ locked: true,
675
+ rootExists: true,
676
+ owner: lock.record.owner,
677
+ fencing: lock.record.fencing,
678
+ acquiredAt: lock.record.acquiredAt,
679
+ ageMs: Math.max(0, clockMillis(clock) - acquiredAt),
680
+ recoveryInProgress: recovery !== null,
681
+ };
682
+ }
683
+
684
+ export async function openStateStore(root, { owner, payloadSchemas, clock } = {}) {
685
+ const safeRoot = await prepareRoot(root, { create: true });
686
+ const validators = normalizePayloadSchemas(payloadSchemas);
687
+ const writerOwner = typeof owner === "string" && owner.length > 0 ? owner : `writer-${process.pid}-${randomId()}`;
688
+ const acquisitionId = randomId();
689
+ await ensureLayout(safeRoot);
690
+ if (await readSafeFile(path.join(safeRoot, RECOVERY_FILE), { optional: true, label: RECOVERY_FILE })) {
691
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "explicit lock recovery is in progress");
692
+ }
693
+ if (await readSafeFile(path.join(safeRoot, MUTATION_FILE), { optional: true, label: MUTATION_FILE })) {
694
+ throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "an unfinished state-store mutation requires explicit recovery");
695
+ }
696
+ const placeholder = lockRecord({ owner: writerOwner, fencing: 0, acquisitionId, acquiredAt: new Date(clockMillis(clock)).toISOString() });
697
+ let lockStats;
698
+ try {
699
+ lockStats = await writeExclusive(path.join(safeRoot, LOCK_FILE), `${canonicalJson(placeholder)}\n`);
700
+ } catch (cause) {
701
+ if (cause?.code === "EEXIST") throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "state store already has a writer");
702
+ throw cause;
703
+ }
704
+ try {
705
+ const store = await buildStore(safeRoot, writerOwner, 0, acquisitionId, validators, clock);
706
+ const scan = await scanEvents(store);
707
+ const fencing = await allocateFencing(store, scan.maxFencing);
708
+ const active = lockRecord({ ...placeholder, fencing });
709
+ await atomicReplace(path.join(safeRoot, HEAD_FILE), `${canonicalJson({ lastSequence: scan.eventCount, headDigest: scan.headDigest })}\n`);
710
+ // Publish the active fencing token last. Every prior initialization
711
+ // failure still owns the original exclusive-create inode and can release
712
+ // it without risking another writer's lock.
713
+ await atomicReplace(path.join(safeRoot, LOCK_FILE), `${canonicalJson(active)}\n`);
714
+ store.fencing = fencing;
715
+ return store;
716
+ } catch (cause) {
717
+ await removeSameFile(path.join(safeRoot, LOCK_FILE), lockStats).catch(() => {});
718
+ throw cause;
719
+ }
720
+ }
721
+
722
+ export async function recoverStateStoreLock(root, {
723
+ expectedOwner,
724
+ expectedFencing,
725
+ confirmOwnerTerminated,
726
+ newOwner,
727
+ payloadSchemas,
728
+ clock,
729
+ } = {}) {
730
+ if (confirmOwnerTerminated !== true) {
731
+ throw failure(HARNESS_ERROR_KINDS.LOCK_RECOVERY_REFUSED, "lock recovery requires explicit confirmation that the observed writer has terminated");
732
+ }
733
+ const safeRoot = await prepareRoot(root, { create: false });
734
+ const validators = normalizePayloadSchemas(payloadSchemas);
735
+ await ensureLayout(safeRoot);
736
+ const mutationGuard = await acquireRecoveryMutation(safeRoot, {
737
+ schemaVersion: 1,
738
+ owner: expectedOwner,
739
+ fencing: expectedFencing,
740
+ acquisitionId: "recovery-claim",
741
+ operation: "recover",
742
+ mutationId: randomId(),
743
+ });
744
+ const guardPath = path.join(safeRoot, RECOVERY_FILE);
745
+ let guardStats;
746
+ try {
747
+ guardStats = await writeExclusive(guardPath, `${canonicalJson({ schemaVersion: 1, recoveryId: randomId() })}\n`);
748
+ } catch (cause) {
749
+ await releaseMutation(mutationGuard).catch(() => {});
750
+ if (cause?.code === "EEXIST") throw failure(HARNESS_ERROR_KINDS.STORE_LOCKED, "another explicit recovery is in progress");
751
+ throw cause;
752
+ }
753
+ let recoveryFailed = false;
754
+ try {
755
+ const observed = await readLock(safeRoot);
756
+ if (observed.record.owner !== expectedOwner || observed.record.fencing !== expectedFencing) {
757
+ throw failure(HARNESS_ERROR_KINDS.LOCK_RECOVERY_REFUSED, "observed owner/fencing no longer matches the recovery request", {
758
+ observedOwner: observed.record.owner,
759
+ observedFencing: observed.record.fencing,
760
+ });
761
+ }
762
+ const owner = typeof newOwner === "string" && newOwner.length > 0 ? newOwner : `recovery-${process.pid}-${randomId()}`;
763
+ const acquisitionId = randomId();
764
+ const store = await buildStore(safeRoot, owner, 0, acquisitionId, validators, clock);
765
+ const scan = await scanEvents(store);
766
+ const fencing = await allocateFencing(store, Math.max(expectedFencing, scan.maxFencing));
767
+ const active = lockRecord({ owner, fencing, acquisitionId, acquiredAt: new Date(clockMillis(clock)).toISOString() });
768
+ await atomicReplace(path.join(safeRoot, LOCK_FILE), `${canonicalJson(active)}\n`);
769
+ store.fencing = fencing;
770
+ await removeSameFile(guardPath, guardStats);
771
+ return store;
772
+ } catch (cause) {
773
+ recoveryFailed = true;
774
+ await removeSameFile(guardPath, guardStats).catch(() => {});
775
+ throw cause;
776
+ } finally {
777
+ try {
778
+ await releaseMutation(mutationGuard);
779
+ } catch (releaseCause) {
780
+ if (!recoveryFailed) throw releaseCause;
781
+ }
782
+ }
783
+ }
784
+
785
+ export async function appendEvent(store, event) {
786
+ if (!event || typeof event !== "object" || Array.isArray(event)) throw new TypeError("event must be a plain object");
787
+ assertJsonValue(event, new Set(), "event");
788
+ return withWriterMutation(store, "append-event", async (guard) => {
789
+ const scan = await scanEvents(store);
790
+ const prior = scan.records.find((record) => record.idempotencyKey === event.idempotencyKey);
791
+ if (prior) {
792
+ const same = canonicalJson({
793
+ eventId: prior.eventId,
794
+ eventType: prior.eventType,
795
+ payloadSchemaVersion: prior.payloadSchemaVersion,
796
+ producer: prior.producer,
797
+ payload: prior.payload,
798
+ }) === canonicalJson({
799
+ eventId: event.eventId,
800
+ eventType: event.eventType,
801
+ payloadSchemaVersion: event.payloadSchemaVersion,
802
+ producer: event.producer,
803
+ payload: event.payload,
804
+ });
805
+ if (same) return { appended: false, record: prior };
806
+ throw failure(HARNESS_ERROR_KINDS.IDEMPOTENCY_CONFLICT, "idempotency key was reused with different event content");
807
+ }
808
+ if (scan.records.some((record) => record.eventId === event.eventId)) {
809
+ throw failure(HARNESS_ERROR_KINDS.IDEMPOTENCY_CONFLICT, "eventId was reused with a different idempotency key", {
810
+ eventId: event.eventId,
811
+ identity: "eventId",
812
+ });
813
+ }
814
+ const sequence = scan.eventCount + 1;
815
+ validatePayload(store.validators, event.eventType, event.payloadSchemaVersion, event.payload, sequence);
816
+ const record = {
817
+ schemaVersion: 1,
818
+ kind: EVENT_KIND,
819
+ eventId: event.eventId,
820
+ eventType: event.eventType,
821
+ payloadSchemaVersion: event.payloadSchemaVersion,
822
+ producer: event.producer,
823
+ idempotencyKey: event.idempotencyKey,
824
+ writerOwner: store.owner,
825
+ writerFencing: store.fencing,
826
+ sequence,
827
+ prevDigest: scan.headDigest,
828
+ payload: event.payload,
829
+ };
830
+ record.recordDigest = recordDigest(record);
831
+ const checked = validateContractDocument(record, { schemaId: EVENT_SCHEMA_ID });
832
+ if (!checked.valid) throw failure(HARNESS_ERROR_KINDS.EVENT_SCHEMA_INVALID, "event envelope is invalid", { errors: checked.errors });
833
+ await runTestHook(store, "beforeAuthoritativeEventWrite");
834
+ await assertMutationGuard(guard);
835
+ await assertWriter(store);
836
+ await appendExclusiveEvent(store.eventsDir, path.join(store.eventsDir, `${String(sequence).padStart(6, "0")}.json`), `${canonicalJson(record)}\n`);
837
+ await assertWriter(store);
838
+ await atomicReplace(path.join(store.root, HEAD_FILE), `${canonicalJson({ lastSequence: sequence, headDigest: record.recordDigest })}\n`);
839
+ return { appended: true, record };
840
+ });
841
+ }
842
+
843
+ export async function readEvents(store, { afterSequence = 0 } = {}) {
844
+ assertStore(store);
845
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0) throw new TypeError("afterSequence must be a non-negative integer");
846
+ const scan = await scanEvents(store);
847
+ return scan.records.filter((record) => record.sequence > afterSequence);
848
+ }
849
+
850
+ async function cacheStatus(store, scan) {
851
+ let parsed;
852
+ try {
853
+ parsed = await readSafeJson(path.join(store.root, HEAD_FILE), { optional: true, label: HEAD_FILE });
854
+ } catch (cause) {
855
+ if (cause?.details?.kind === HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY) throw cause;
856
+ return "corrupt";
857
+ }
858
+ if (parsed === null) return "missing";
859
+ return parsed.value?.lastSequence === scan.eventCount && parsed.value?.headDigest === scan.headDigest ? "current" : "stale";
860
+ }
861
+
862
+ export async function verifyStateStore(store, { reducer, initial = null } = {}) {
863
+ assertStore(store);
864
+ const scan = await scanEvents(store);
865
+ const result = {
866
+ valid: true,
867
+ eventCount: scan.eventCount,
868
+ headDigest: scan.headDigest,
869
+ chainHeadCache: await cacheStatus(store, scan),
870
+ snapshotStatus: "missing",
871
+ };
872
+ let snapshot;
873
+ try {
874
+ snapshot = await newestSnapshot(store, scan);
875
+ } catch (cause) {
876
+ if (cause?.details?.kind !== HARNESS_ERROR_KINDS.SNAPSHOT_MISMATCH) throw cause;
877
+ return { ...result, valid: false, snapshotStatus: "corrupt", snapshotError: cause.message };
878
+ }
879
+ if (snapshot === null) return result;
880
+ if (reducer === undefined) {
881
+ return { ...result, valid: false, snapshotStatus: "unverified" };
882
+ }
883
+ if (typeof reducer !== "function") throw new TypeError("reducer must be a function");
884
+ assertJsonValue(initial, new Set(), "initial state");
885
+ const prefix = reduceEventPrefix(scan.records, reducer, initial, snapshot.lastSequence);
886
+ if (digestDocument(prefix) !== snapshot.stateDigest) {
887
+ return { ...result, valid: false, snapshotStatus: "semantic-mismatch" };
888
+ }
889
+ return { ...result, snapshotStatus: "verified" };
890
+ }
891
+
892
+ async function newestSnapshot(store, scan) {
893
+ await assertLayoutIdentity(store);
894
+ const entries = await readdir(store.snapshotsDir, { withFileTypes: true });
895
+ const sequences = [];
896
+ for (const entry of entries) {
897
+ if (TEMP_NAME.test(entry.name)) {
898
+ await readSafeFile(path.join(store.snapshotsDir, entry.name), { label: "snapshot staging file" });
899
+ continue;
900
+ }
901
+ const match = SNAPSHOT_NAME.exec(entry.name);
902
+ if (!match) throw failure(HARNESS_ERROR_KINDS.SNAPSHOT_MISMATCH, `unexpected snapshot entry: ${entry.name}`);
903
+ sequences.push(Number(match[1]));
904
+ }
905
+ if (sequences.length === 0) return null;
906
+ sequences.sort((left, right) => left - right);
907
+ let newest = null;
908
+ for (const sequence of sequences) {
909
+ let parsed;
910
+ try {
911
+ parsed = await readSafeJson(path.join(store.snapshotsDir, `${String(sequence).padStart(6, "0")}.json`), { label: `snapshot ${sequence}` });
912
+ } catch (cause) {
913
+ if (cause?.details?.kind === HARNESS_ERROR_KINDS.UNSAFE_STATE_ENTRY) throw cause;
914
+ throw failure(HARNESS_ERROR_KINDS.SNAPSHOT_MISMATCH, `snapshot ${sequence} is unreadable`, { position: sequence });
915
+ }
916
+ const snapshot = parsed.value;
917
+ const checked = validateContractDocument(snapshot, { schemaId: SNAPSHOT_SCHEMA_ID });
918
+ const expectedDigest = sequence === 0 ? STATE_GENESIS_DIGEST : scan.records[sequence - 1]?.recordDigest;
919
+ if (!checked.valid || snapshot.lastSequence !== sequence || snapshot.chainHeadDigest !== expectedDigest || snapshot.stateDigest !== digestDocument(snapshot.state)) {
920
+ throw failure(HARNESS_ERROR_KINDS.SNAPSHOT_MISMATCH, `snapshot ${sequence} does not bind to the authoritative event prefix`, { position: sequence });
921
+ }
922
+ newest = snapshot;
923
+ }
924
+ return newest;
925
+ }
926
+
927
+ export async function readSnapshot(store, { reducer, initial = null } = {}) {
928
+ assertStore(store);
929
+ const scan = await scanEvents(store); // bad events always win over old snapshots
930
+ const snapshot = await newestSnapshot(store, scan);
931
+ if (snapshot === null) return null;
932
+ if (typeof reducer !== "function") {
933
+ throw new TypeError("readSnapshot requires the consumer reducer used to verify the authoritative event prefix");
934
+ }
935
+ assertJsonValue(initial, new Set(), "initial state");
936
+ const prefix = reduceEventPrefix(scan.records, reducer, initial, snapshot.lastSequence);
937
+ if (digestDocument(prefix) !== snapshot.stateDigest) {
938
+ throw failure(HARNESS_ERROR_KINDS.SNAPSHOT_MISMATCH, "snapshot state does not equal the reducer result for the authoritative event prefix", {
939
+ position: snapshot.lastSequence,
940
+ });
941
+ }
942
+ return snapshot;
943
+ }
944
+
945
+ export async function writeSnapshot(store, state, { hostSessionRef, reducer, initial = null } = {}) {
946
+ if (typeof reducer !== "function") {
947
+ throw new TypeError("writeSnapshot requires the consumer reducer used to derive state from authoritative events");
948
+ }
949
+ assertJsonValue(state, new Set(), "snapshot state");
950
+ assertJsonValue(initial, new Set(), "initial state");
951
+ return withWriterMutation(store, "write-snapshot", async () => {
952
+ const scan = await scanEvents(store);
953
+ const authoritativeState = reduceEventPrefix(scan.records, reducer, initial, scan.eventCount);
954
+ if (digestDocument(authoritativeState) !== digestDocument(state)) {
955
+ throw failure(HARNESS_ERROR_KINDS.SNAPSHOT_MISMATCH, "snapshot state does not equal the reducer result for the authoritative event prefix", {
956
+ position: scan.eventCount,
957
+ });
958
+ }
959
+ const snapshot = {
960
+ schemaVersion: 1,
961
+ kind: SNAPSHOT_KIND,
962
+ lastSequence: scan.eventCount,
963
+ chainHeadDigest: scan.headDigest,
964
+ stateDigest: digestDocument(authoritativeState),
965
+ state: authoritativeState,
966
+ };
967
+ if (hostSessionRef !== undefined) snapshot.hostSessionRef = hostSessionRef;
968
+ const checked = validateContractDocument(snapshot, { schemaId: SNAPSHOT_SCHEMA_ID });
969
+ if (!checked.valid) throw failure(HARNESS_ERROR_KINDS.SNAPSHOT_MISMATCH, "snapshot metadata is invalid", { errors: checked.errors });
970
+ await assertWriter(store);
971
+ await atomicReplace(path.join(store.snapshotsDir, `${String(scan.eventCount).padStart(6, "0")}.json`), `${canonicalJson(snapshot)}\n`, "snapshot");
972
+ return snapshot;
973
+ });
974
+ }
975
+
976
+ function reduceEventPrefix(records, reducer, initial, length) {
977
+ let state = structuredClone(initial);
978
+ for (const record of records.slice(0, length)) {
979
+ state = reducer(state, structuredClone(record));
980
+ assertJsonValue(state, new Set(), "reducer result");
981
+ }
982
+ return state;
983
+ }
984
+
985
+ export async function rebuildSnapshot(store, reducer, { initial = null } = {}) {
986
+ assertStore(store);
987
+ if (typeof reducer !== "function") throw new TypeError("reducer must be a function");
988
+ assertJsonValue(initial, new Set(), "initial state");
989
+ const scan = await scanEvents(store); // full authority check before snapshot use
990
+ let snapshot = null;
991
+ let snapshotStatus = "missing";
992
+ try {
993
+ snapshot = await newestSnapshot(store, scan);
994
+ snapshotStatus = snapshot === null ? "missing" : "structurally-valid";
995
+ } catch (cause) {
996
+ if (cause?.details?.kind !== HARNESS_ERROR_KINDS.SNAPSHOT_MISMATCH) throw cause;
997
+ snapshotStatus = "ignored-corrupt";
998
+ }
999
+ const state = reduceEventPrefix(scan.records, reducer, initial, scan.eventCount);
1000
+ if (snapshot !== null) {
1001
+ const prefix = reduceEventPrefix(scan.records, reducer, initial, snapshot.lastSequence);
1002
+ if (digestDocument(prefix) !== snapshot.stateDigest) {
1003
+ snapshotStatus = "ignored-semantic-mismatch";
1004
+ } else {
1005
+ snapshotStatus = "verified";
1006
+ }
1007
+ }
1008
+ return {
1009
+ state,
1010
+ lastSequence: scan.eventCount,
1011
+ chainHeadDigest: scan.headDigest,
1012
+ replayed: scan.eventCount,
1013
+ snapshotSequence: snapshot?.lastSequence ?? 0,
1014
+ snapshotStatus,
1015
+ };
1016
+ }
1017
+
1018
+ export async function closeStateStore(store) {
1019
+ if (!store || store.__stateStore !== true) throw new TypeError("expected a state store handle");
1020
+ if (store.closed) return;
1021
+ await assertLayoutIdentity(store);
1022
+ const lock = await readLock(store.root, { optional: true }).catch(() => null);
1023
+ const ownsLock = lock && lock.record.owner === store.owner && lock.record.fencing === store.fencing &&
1024
+ lock.record.acquisitionId === store.acquisitionId;
1025
+ if (!ownsLock) {
1026
+ store.closed = true;
1027
+ TEST_HOOKS.delete(store);
1028
+ return;
1029
+ }
1030
+ const guard = await acquireMutation(store.root, mutationRecord(store, "close"));
1031
+ let closeFailed = false;
1032
+ try {
1033
+ await assertWriter(store);
1034
+ store.closed = true;
1035
+ TEST_HOOKS.delete(store);
1036
+ await removeSameFile(path.join(store.root, LOCK_FILE), lock.stats);
1037
+ await syncDirectory(store.root);
1038
+ } catch (cause) {
1039
+ closeFailed = true;
1040
+ throw cause;
1041
+ } finally {
1042
+ try {
1043
+ await releaseMutation(guard);
1044
+ } catch (releaseCause) {
1045
+ if (!closeFailed) throw releaseCause;
1046
+ }
1047
+ }
1048
+ }
1049
+
1050
+ export const close = closeStateStore;