@rulvar/store-conformance 1.44.0 → 1.45.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.
package/dist/index.d.ts CHANGED
@@ -47,6 +47,9 @@ declare function leasableStoreConformance(mk: StoreFactory<LeasableStore>, optio
47
47
  ttlMs?: number;
48
48
  }): ConformanceSuite;
49
49
  //#endregion
50
+ //#region src/fenced-writes.d.ts
51
+ declare function fencedWritesConformance(mk: StoreFactory<LeasableStore>): ConformanceSuite;
52
+ //#endregion
50
53
  //#region src/fixtures/golden-fold.d.ts
51
54
  /**
52
55
  * seq 0 agent spawn (running; abandoned by seq 6)
@@ -71,4 +74,4 @@ declare function foldStateSha256(entries: readonly JournalEntry[]): string;
71
74
  /** The reference hash; computed once from the kernel fold and frozen. */
72
75
  declare const GOLDEN_FOLD_STATE_SHA256 = "81e6ccff549fb3e6c1de4d34ba65b912162eba6f66403b5d5f23a3e1ec69243c";
73
76
  //#endregion
74
- export { type ConformanceCheck, type ConformanceSuite, GOLDEN_FOLD_JOURNAL, GOLDEN_FOLD_STATE_SHA256, type StoreFactory, type TestRegistrar, foldStateSha256, journalStoreConformance, leasableStoreConformance, makeSuite, materializeFoldState, registerConformance, stableStringify };
77
+ export { type ConformanceCheck, type ConformanceSuite, GOLDEN_FOLD_JOURNAL, GOLDEN_FOLD_STATE_SHA256, type StoreFactory, type TestRegistrar, fencedWritesConformance, foldStateSha256, journalStoreConformance, leasableStoreConformance, makeSuite, materializeFoldState, registerConformance, stableStringify };
package/dist/index.js CHANGED
@@ -219,7 +219,7 @@ const GOLDEN_FOLD_STATE_SHA256 = "81e6ccff549fb3e6c1de4d34ba65b912162eba6f66403b
219
219
  * (FakeAdapter call counters) live in the M2 cassette suite
220
220
  * (@rulvar/testing).
221
221
  */
222
- const RUN$1 = "conformance-run";
222
+ const RUN$2 = "conformance-run";
223
223
  function baseEntry(seq, overrides) {
224
224
  return {
225
225
  hashVersion: 2,
@@ -238,7 +238,7 @@ function baseEntry(seq, overrides) {
238
238
  function sameEntry(a, b) {
239
239
  return stableStringify(a) === stableStringify(b);
240
240
  }
241
- function meta(runId, overrides) {
241
+ function meta$1(runId, overrides) {
242
242
  return {
243
243
  runId,
244
244
  status: "running",
@@ -259,14 +259,14 @@ function journalStoreConformance(mk) {
259
259
  blob: `${big}-${i}`,
260
260
  sentinel: "END-OF-ENTRY"
261
261
  } }));
262
- const appends = submitted.map((item) => store.append(RUN$1, item));
263
- const midLoads = [store.load(RUN$1), store.load(RUN$1)];
262
+ const appends = submitted.map((item) => store.append(RUN$2, item));
263
+ const midLoads = [store.load(RUN$2), store.load(RUN$2)];
264
264
  await Promise.all([...appends, ...midLoads]);
265
265
  for (const seen of await Promise.all(midLoads)) for (const item of seen) {
266
266
  const original = submitted.find((candidate) => candidate.seq === item.seq);
267
267
  ensure(original !== void 0 && sameEntry(item, original), "a1-append-atomicity", `a load observed a torn or foreign entry at seq ${item.seq}`);
268
268
  }
269
- const loaded = await store.load(RUN$1);
269
+ const loaded = await store.load(RUN$2);
270
270
  ensure(loaded.length === submitted.length, "a1-append-atomicity", `expected ${submitted.length} whole entries, loaded ${loaded.length}`);
271
271
  for (const item of loaded) {
272
272
  const original = submitted.find((candidate) => candidate.seq === item.seq);
@@ -280,12 +280,12 @@ function journalStoreConformance(mk) {
280
280
  async run() {
281
281
  const store = await mk();
282
282
  const entries = Array.from({ length: 10 }, (_, i) => baseEntry(i));
283
- for (const item of entries) await store.append(RUN$1, item);
284
- const first = await store.load(RUN$1);
283
+ for (const item of entries) await store.append(RUN$2, item);
284
+ const first = await store.load(RUN$2);
285
285
  ensure(first.map((item) => item.seq).join(",") === entries.map((item) => item.seq).join(","), "a2-total-order", `sequential appends loaded out of order: ${first.map((item) => item.seq).join(",")}`);
286
- await Promise.all(Array.from({ length: 8 }, (_, i) => store.append(RUN$1, baseEntry(10 + i))));
287
- const second = await store.load(RUN$1);
288
- const third = await store.load(RUN$1);
286
+ await Promise.all(Array.from({ length: 8 }, (_, i) => store.append(RUN$2, baseEntry(10 + i))));
287
+ const second = await store.load(RUN$2);
288
+ const third = await store.load(RUN$2);
289
289
  ensure(second.map((item) => item.seq).join(",") === third.map((item) => item.seq).join(","), "a2-total-order", "two loads of the same run disagree on entry order");
290
290
  ensure(second.length === 18 && new Set(second.map((item) => item.seq)).size === 18, "a2-total-order", "concurrent appends were lost or duplicated");
291
291
  ensure(second.slice(0, 10).map((item) => item.seq).join(",") === entries.map((item) => item.seq).join(","), "a2-total-order", "later appends reordered the existing prefix");
@@ -298,8 +298,8 @@ function journalStoreConformance(mk) {
298
298
  const store = await mk();
299
299
  for (let i = 0; i < 5; i += 1) {
300
300
  const item = baseEntry(i);
301
- await store.append(RUN$1, item);
302
- const loaded = await store.load(RUN$1);
301
+ await store.append(RUN$2, item);
302
+ const loaded = await store.load(RUN$2);
303
303
  const last = loaded[loaded.length - 1];
304
304
  ensure(loaded.length === i + 1 && last !== void 0 && sameEntry(last, item), "a3-read-your-writes", `append of seq ${i} was acknowledged but not visible to load`);
305
305
  }
@@ -331,8 +331,8 @@ function journalStoreConformance(mk) {
331
331
  spanId: "s",
332
332
  startedAt: "2026-01-01T00:00:00.000Z"
333
333
  };
334
- await store.append(RUN$1, exotic);
335
- const [loaded] = await store.load(RUN$1);
334
+ await store.append(RUN$2, exotic);
335
+ const [loaded] = await store.load(RUN$2);
336
336
  ensure(loaded !== void 0 && sameEntry(loaded, exotic), "a4-opaque-payload", "the exotic entry did not round-trip byte-exactly (normalized, stripped, or rewritten)");
337
337
  ensure(loaded.hashVersion === void 0, "a4-opaque-payload", "the store injected hashVersion into a legacy entry (normalization is read-side only)");
338
338
  }
@@ -342,12 +342,12 @@ function journalStoreConformance(mk) {
342
342
  title: "append rejects a duplicate or stale seq with the typed journal_order_violation",
343
343
  async run() {
344
344
  const store = await mk();
345
- await store.append(RUN$1, baseEntry(0));
346
- await store.append(RUN$1, baseEntry(1));
345
+ await store.append(RUN$2, baseEntry(0));
346
+ await store.append(RUN$2, baseEntry(1));
347
347
  for (const stale of [1, 0]) {
348
348
  let thrown;
349
349
  try {
350
- await store.append(RUN$1, baseEntry(stale, { value: {
350
+ await store.append(RUN$2, baseEntry(stale, { value: {
351
351
  seq: stale,
352
352
  second: true
353
353
  } }));
@@ -356,9 +356,9 @@ function journalStoreConformance(mk) {
356
356
  }
357
357
  ensure(thrown?.code === "journal_order_violation", "a5-monotonic-seq", `append of stale seq ${stale} must reject with code 'journal_order_violation'`);
358
358
  }
359
- ensure((await store.load(RUN$1)).map((item) => item.seq).join(",") === "0,1", "a5-monotonic-seq", "a rejected stale append must never become visible");
360
- await store.append(RUN$1, baseEntry(2));
361
- ensure((await store.load(RUN$1)).length === 3, "a5-monotonic-seq", "the append of the true next seq must succeed after rejections");
359
+ ensure((await store.load(RUN$2)).map((item) => item.seq).join(",") === "0,1", "a5-monotonic-seq", "a rejected stale append must never become visible");
360
+ await store.append(RUN$2, baseEntry(2));
361
+ ensure((await store.load(RUN$2)).length === 3, "a5-monotonic-seq", "the append of the true next seq must succeed after rejections");
362
362
  }
363
363
  },
364
364
  {
@@ -366,11 +366,11 @@ function journalStoreConformance(mk) {
366
366
  title: "two writers appending the same next seq: exactly one persists, the loser is typed",
367
367
  async run() {
368
368
  const store = await mk();
369
- for (let i = 0; i < 3; i += 1) await store.append(RUN$1, baseEntry(i));
370
- const settled = await Promise.allSettled([store.append(RUN$1, baseEntry(3, { value: {
369
+ for (let i = 0; i < 3; i += 1) await store.append(RUN$2, baseEntry(i));
370
+ const settled = await Promise.allSettled([store.append(RUN$2, baseEntry(3, { value: {
371
371
  seq: 3,
372
372
  writer: "a"
373
- } })), store.append(RUN$1, baseEntry(3, { value: {
373
+ } })), store.append(RUN$2, baseEntry(3, { value: {
374
374
  seq: 3,
375
375
  writer: "b"
376
376
  } }))]);
@@ -378,7 +378,7 @@ function journalStoreConformance(mk) {
378
378
  const rejected = settled.filter((item) => item.status === "rejected");
379
379
  ensure(fulfilled.length === 1 && rejected.length === 1, "a5-stale-tail-race", `exactly one of two same-seq appends may persist (got ${fulfilled.length} fulfilled)`);
380
380
  ensure((rejected[0]?.reason)?.code === "journal_order_violation", "a5-stale-tail-race", "the losing writer must observe the typed 'journal_order_violation' conflict");
381
- const loaded = await store.load(RUN$1);
381
+ const loaded = await store.load(RUN$2);
382
382
  ensure(loaded.length === 4 && loaded.filter((item) => item.seq === 3).length === 1, "a5-stale-tail-race", "the journal must hold exactly one entry at the raced seq");
383
383
  const seqs = loaded.map((item) => item.seq);
384
384
  ensure(seqs.every((seq, index) => index === 0 || seq > (seqs[index - 1] ?? NaN)), "a5-stale-tail-race", "reloading after the race must show a strictly increasing total order");
@@ -390,7 +390,7 @@ function journalStoreConformance(mk) {
390
390
  async run() {
391
391
  const store = await mk();
392
392
  const seed = new Replayer({
393
- runId: RUN$1,
393
+ runId: RUN$2,
394
394
  store
395
395
  });
396
396
  for (let i = 0; i < 3; i += 1) await seed.appendSinglePhase({
@@ -401,14 +401,14 @@ function journalStoreConformance(mk) {
401
401
  spanId: "conf-span",
402
402
  value: { i }
403
403
  });
404
- const tail = await store.load(RUN$1);
404
+ const tail = await store.load(RUN$2);
405
405
  const writerA = new Replayer({
406
- runId: RUN$1,
406
+ runId: RUN$2,
407
407
  store,
408
408
  priorEntries: tail
409
409
  });
410
410
  const writerB = new Replayer({
411
- runId: RUN$1,
411
+ runId: RUN$2,
412
412
  store,
413
413
  priorEntries: tail
414
414
  });
@@ -430,7 +430,7 @@ function journalStoreConformance(mk) {
430
430
  const rejected = settled.filter((item) => item.status === "rejected");
431
431
  ensure(settled.filter((item) => item.status === "fulfilled").length === 1 && rejected.length === 1, "a5-stale-replayer-fencing", "exactly one stale-tail replayer append may persist");
432
432
  ensure((rejected[0]?.reason)?.code === "journal_order_violation", "a5-stale-replayer-fencing", "the losing replayer must observe the typed 'journal_order_violation' conflict");
433
- const seqs = (await store.load(RUN$1)).map((item) => item.seq);
433
+ const seqs = (await store.load(RUN$2)).map((item) => item.seq);
434
434
  ensure(new Set(seqs).size === seqs.length && seqs.every((seq, index) => index === 0 || seq > (seqs[index - 1] ?? NaN)), "a5-stale-replayer-fencing", "reopening the store must show a unique, strictly increasing seq order");
435
435
  }
436
436
  },
@@ -440,28 +440,28 @@ function journalStoreConformance(mk) {
440
440
  async run() {
441
441
  const store = await mk();
442
442
  ensure((await store.load("never-written")).length === 0, "meta-separation", "load of an unknown run must return an empty journal");
443
- await store.append(RUN$1, baseEntry(0));
444
- await store.putMeta(meta(RUN$1, {
443
+ await store.append(RUN$2, baseEntry(0));
444
+ await store.putMeta(meta$1(RUN$2, {
445
445
  name: "wf-a",
446
446
  tags: ["team:core"]
447
447
  }));
448
- await store.putMeta(meta("other-run", {
448
+ await store.putMeta(meta$1("other-run", {
449
449
  status: "suspended",
450
450
  name: "wf-b"
451
451
  }));
452
- ensure((await store.listRuns()).filter((candidate) => candidate.runId === RUN$1).length === 1, "meta-separation", "putMeta must upsert one record per run");
453
- await store.putMeta(meta(RUN$1, {
452
+ ensure((await store.listRuns()).filter((candidate) => candidate.runId === RUN$2).length === 1, "meta-separation", "putMeta must upsert one record per run");
453
+ await store.putMeta(meta$1(RUN$2, {
454
454
  status: "suspended",
455
455
  name: "wf-a",
456
456
  tags: ["team:core"]
457
457
  }));
458
458
  const suspended = await store.listRuns({ status: "suspended" });
459
- ensure(suspended.some((candidate) => candidate.runId === RUN$1) && suspended.every((candidate) => candidate.status === "suspended"), "meta-separation", "listRuns must reflect the replaced meta and honor the status filter");
459
+ ensure(suspended.some((candidate) => candidate.runId === RUN$2) && suspended.every((candidate) => candidate.status === "suspended"), "meta-separation", "listRuns must reflect the replaced meta and honor the status filter");
460
460
  const byName = await store.listRuns({ name: "wf-b" });
461
461
  ensure(byName.length === 1 && byName[0]?.runId === "other-run", "meta-separation", "listRuns must honor the name filter");
462
462
  const byTag = await store.listRuns({ tags: ["team:core"] });
463
- ensure(byTag.length === 1 && byTag[0]?.runId === RUN$1, "meta-separation", "listRuns must honor the tags filter");
464
- await store.putMeta(meta(RUN$1, {
463
+ ensure(byTag.length === 1 && byTag[0]?.runId === RUN$2, "meta-separation", "listRuns must honor the tags filter");
464
+ await store.putMeta(meta$1(RUN$2, {
465
465
  status: "suspended",
466
466
  name: "wf-a",
467
467
  tags: ["team:core"],
@@ -471,20 +471,20 @@ function journalStoreConformance(mk) {
471
471
  argsHash: "a".repeat(64),
472
472
  genesis: "g".repeat(26)
473
473
  }));
474
- const roundTripped = (await store.listRuns()).find((candidate) => candidate.runId === RUN$1);
474
+ const roundTripped = (await store.listRuns()).find((candidate) => candidate.runId === RUN$2);
475
475
  ensure(roundTripped?.budgetUsd === 12.5, "meta-separation", "putMeta/listRuns must round-trip optional RunMeta fields (budgetUsd)");
476
476
  ensure(roundTripped?.segments === 3, "meta-separation", "putMeta/listRuns must round-trip optional RunMeta fields (segments)");
477
477
  ensure(roundTripped?.argsProvided === true && roundTripped.argsHash === "a".repeat(64), "meta-separation", "putMeta/listRuns must round-trip optional RunMeta fields (argsProvided, argsHash)");
478
478
  ensure(roundTripped?.genesis === "g".repeat(26), "meta-separation", "putMeta/listRuns must round-trip optional RunMeta fields (genesis)");
479
- ensure((await store.listRuns({ statuses: ["suspended", "running"] })).some((candidate) => candidate.runId === RUN$1), "meta-separation", "listRuns({ statuses }) must include every meta whose status matches (supersets are allowed, drops are not)");
479
+ ensure((await store.listRuns({ statuses: ["suspended", "running"] })).some((candidate) => candidate.runId === RUN$2), "meta-separation", "listRuns({ statuses }) must include every meta whose status matches (supersets are allowed, drops are not)");
480
480
  const lookup = store.getMeta;
481
481
  if (typeof lookup === "function") {
482
- const fetched = await lookup.call(store, RUN$1);
483
- ensure(fetched?.runId === RUN$1 && fetched.genesis === "g".repeat(26), "meta-separation", "getMeta must return the stored meta for an existing run");
482
+ const fetched = await lookup.call(store, RUN$2);
483
+ ensure(fetched?.runId === RUN$2 && fetched.genesis === "g".repeat(26), "meta-separation", "getMeta must return the stored meta for an existing run");
484
484
  ensure(await lookup.call(store, "conformance-missing-run") === void 0, "meta-separation", "getMeta must resolve undefined for a missing run, never reject");
485
485
  }
486
- await store.delete(RUN$1);
487
- ensure((await store.load(RUN$1)).length === 0 && !(await store.listRuns()).some((candidate) => candidate.runId === RUN$1), "meta-separation", "delete must remove the journal and the meta record");
486
+ await store.delete(RUN$2);
487
+ ensure((await store.load(RUN$2)).length === 0 && !(await store.listRuns()).some((candidate) => candidate.runId === RUN$2), "meta-separation", "delete must remove the journal and the meta record");
488
488
  }
489
489
  },
490
490
  {
@@ -492,8 +492,8 @@ function journalStoreConformance(mk) {
492
492
  title: "the kernel fold over the stored golden journal hashes to the reference",
493
493
  async run() {
494
494
  const store = await mk();
495
- for (const item of GOLDEN_FOLD_JOURNAL) await store.append(RUN$1, item);
496
- const hash = foldStateSha256(await store.load(RUN$1));
495
+ for (const item of GOLDEN_FOLD_JOURNAL) await store.append(RUN$2, item);
496
+ const hash = foldStateSha256(await store.load(RUN$2));
497
497
  ensure(hash === GOLDEN_FOLD_STATE_SHA256, "golden-fold-state", `fold state hash ${hash} differs from the reference ${GOLDEN_FOLD_STATE_SHA256}: the store influenced replay semantics`);
498
498
  }
499
499
  },
@@ -503,7 +503,7 @@ function journalStoreConformance(mk) {
503
503
  async run() {
504
504
  const store = await mk();
505
505
  const writer = new Replayer({
506
- runId: RUN$1,
506
+ runId: RUN$2,
507
507
  store
508
508
  });
509
509
  const suspended = await writer.appendSuspended({
@@ -525,17 +525,17 @@ function journalStoreConformance(mk) {
525
525
  })]);
526
526
  const outcomes = [first, second];
527
527
  ensure(outcomes.filter((outcome) => outcome.applied).length === 1, "decide-once-oracle", "a race of two resolution attempts must settle exactly one applied");
528
- const journaled = await store.load(RUN$1);
528
+ const journaled = await store.load(RUN$2);
529
529
  ensure(journaled.filter((item) => item.kind === "resolution").length === 2, "decide-once-oracle", "both attempts must be journaled (the loser is a journaled noop)");
530
530
  const state = new Replayer({
531
- runId: RUN$1,
531
+ runId: RUN$2,
532
532
  store,
533
533
  priorEntries: journaled,
534
534
  strict: true
535
535
  }).suspensionState(suspended.seq);
536
536
  const winnerSeq = outcomes.find((outcome) => outcome.applied)?.seq;
537
537
  ensure(state.state === "resolved" && state.by === winnerSeq, "decide-once-oracle", "the replayed fold disagrees with the live winner");
538
- const reload = await store.load(RUN$1);
538
+ const reload = await store.load(RUN$2);
539
539
  ensure(foldStateSha256(journaled) === foldStateSha256(reload), "decide-once-oracle", "two loads of the raced journal fold to different states");
540
540
  }
541
541
  },
@@ -545,7 +545,7 @@ function journalStoreConformance(mk) {
545
545
  async run() {
546
546
  const store = await mk();
547
547
  const writer = new Replayer({
548
- runId: RUN$1,
548
+ runId: RUN$2,
549
549
  store
550
550
  });
551
551
  const branchIdentity = {
@@ -581,9 +581,9 @@ function journalStoreConformance(mk) {
581
581
  reason: "conformance abandon"
582
582
  })).applied, "abandon-derived-skip", "the covering abandon must apply");
583
583
  const replayed = new Replayer({
584
- runId: RUN$1,
584
+ runId: RUN$2,
585
585
  store,
586
- priorEntries: await store.load(RUN$1),
586
+ priorEntries: await store.load(RUN$2),
587
587
  strict: true
588
588
  });
589
589
  replayed.setDisposition(dispositionHook(replayed.fold.abandonFold, buildDeriverRegistry()));
@@ -613,7 +613,7 @@ function journalStoreConformance(mk) {
613
613
  * ships in M5 (@rulvar/store-sqlite); the multi-process soak re-runs
614
614
  * these sections under real concurrency in M8.
615
615
  */
616
- const RUN = "lease-run";
616
+ const RUN$1 = "lease-run";
617
617
  function leaseEntry(seq) {
618
618
  return {
619
619
  hashVersion: 2,
@@ -645,11 +645,11 @@ function leasableStoreConformance(mk, options) {
645
645
  title: "acquire on a held lease rejects with the typed LeaseHeldError",
646
646
  async run() {
647
647
  const store = await mk();
648
- const held = await store.acquire(RUN, "owner-a");
649
- ensure(held.runId === RUN && held.owner === "owner-a" && typeof held.epoch === "number", "lease-exclusive-acquire", "acquire must return a Lease with runId, owner, and a numeric fencing epoch");
650
- ensure(await mustReject(() => store.acquire(RUN, "owner-b")) instanceof LeaseHeldError, "lease-exclusive-acquire", "a second acquire while held must reject with LeaseHeldError (typed, code lease_held)");
648
+ const held = await store.acquire(RUN$1, "owner-a");
649
+ ensure(held.runId === RUN$1 && held.owner === "owner-a" && typeof held.epoch === "number", "lease-exclusive-acquire", "acquire must return a Lease with runId, owner, and a numeric fencing epoch");
650
+ ensure(await mustReject(() => store.acquire(RUN$1, "owner-b")) instanceof LeaseHeldError, "lease-exclusive-acquire", "a second acquire while held must reject with LeaseHeldError (typed, code lease_held)");
651
651
  await store.release(held);
652
- ensure((await store.acquire(RUN, "owner-b")).owner === "owner-b", "lease-exclusive-acquire", "after release, acquire must succeed for a new owner");
652
+ ensure((await store.acquire(RUN$1, "owner-b")).owner === "owner-b", "lease-exclusive-acquire", "after release, acquire must succeed for a new owner");
653
653
  }
654
654
  },
655
655
  {
@@ -657,14 +657,14 @@ function leasableStoreConformance(mk, options) {
657
657
  title: "the epoch is monotonic; a stale-epoch append is rejected and invisible",
658
658
  async run() {
659
659
  const store = await mk();
660
- const first = await store.acquire(RUN, "owner-a");
661
- await store.append(RUN, leaseEntry(0), first);
660
+ const first = await store.acquire(RUN$1, "owner-a");
661
+ await store.append(RUN$1, leaseEntry(0), first);
662
662
  await store.release(first);
663
- const second = await store.acquire(RUN, "owner-b");
663
+ const second = await store.acquire(RUN$1, "owner-b");
664
664
  ensure(second.epoch > first.epoch, "fencing-stale-epoch", `the fencing epoch must be monotonic per run (got ${second.epoch} after ${first.epoch})`);
665
- await store.append(RUN, leaseEntry(1), second);
666
- ensure(await mustReject(() => store.append(RUN, leaseEntry(2), first)) !== void 0, "fencing-stale-epoch", "an append carrying a stale epoch must be rejected");
667
- const loaded = await store.load(RUN);
665
+ await store.append(RUN$1, leaseEntry(1), second);
666
+ ensure(await mustReject(() => store.append(RUN$1, leaseEntry(2), first)) !== void 0, "fencing-stale-epoch", "an append carrying a stale epoch must be rejected");
667
+ const loaded = await store.load(RUN$1);
668
668
  ensure(loaded.length === 2 && !loaded.some((item) => item.seq === 2), "fencing-stale-epoch", "a rejected stale-epoch append must never appear in load");
669
669
  }
670
670
  },
@@ -673,12 +673,12 @@ function leasableStoreConformance(mk, options) {
673
673
  title: "a released lease can neither renew nor append",
674
674
  async run() {
675
675
  const store = await mk();
676
- const lease = await store.acquire(RUN, "owner-a");
676
+ const lease = await store.acquire(RUN$1, "owner-a");
677
677
  await store.renew(lease);
678
678
  await store.release(lease);
679
679
  ensure(await mustReject(() => store.renew(lease)) !== void 0, "lease-release-fences", "renew of a released lease must reject");
680
- ensure(await mustReject(() => store.append(RUN, leaseEntry(0), lease)) !== void 0, "lease-release-fences", "append under a released lease must reject");
681
- ensure((await store.load(RUN)).length === 0, "lease-release-fences", "nothing may be visible after only-rejected appends");
680
+ ensure(await mustReject(() => store.append(RUN$1, leaseEntry(0), lease)) !== void 0, "lease-release-fences", "append under a released lease must reject");
681
+ ensure((await store.load(RUN$1)).length === 0, "lease-release-fences", "nothing may be visible after only-rejected appends");
682
682
  }
683
683
  }
684
684
  ];
@@ -688,13 +688,13 @@ function leasableStoreConformance(mk, options) {
688
688
  title: "renew at ttl/3 keeps the lease held past ttl; an unrenewed lease expires",
689
689
  async run() {
690
690
  const store = await mk();
691
- const held = await store.acquire(RUN, "owner-a");
691
+ const held = await store.acquire(RUN$1, "owner-a");
692
692
  const cadence = Math.floor(ttlMs / 3);
693
693
  const deadline = Date.now() + Math.ceil(ttlMs * 1.5);
694
694
  while (Date.now() < deadline) {
695
695
  await sleep(cadence);
696
696
  await store.renew(held);
697
- ensure(await mustReject(() => store.acquire(RUN, "owner-b")) instanceof LeaseHeldError, "lease-ttl-and-renew-cadence", "a lease renewed at ttl/3 must stay held past the original ttl");
697
+ ensure(await mustReject(() => store.acquire(RUN$1, "owner-b")) instanceof LeaseHeldError, "lease-ttl-and-renew-cadence", "a lease renewed at ttl/3 must stay held past the original ttl");
698
698
  }
699
699
  await store.release(held);
700
700
  const abandoned = await store.acquire("expiring-run", "owner-a");
@@ -705,4 +705,130 @@ function leasableStoreConformance(mk, options) {
705
705
  return makeSuite("leasableStoreConformance", checks);
706
706
  }
707
707
  //#endregion
708
- export { GOLDEN_FOLD_JOURNAL, GOLDEN_FOLD_STATE_SHA256, foldStateSha256, journalStoreConformance, leasableStoreConformance, makeSuite, materializeFoldState, registerConformance, stableStringify };
708
+ //#region src/fenced-writes.ts
709
+ /**
710
+ * fencedWritesConformance (the fenced run state RFC, phase 2): the
711
+ * executable definition of the `fencedWrites` capability. Run it ONLY
712
+ * against stores that declare the marker; the checks are the promise
713
+ * the marker makes:
714
+ * - a mutation carrying a stale or released lease (putMeta, delete,
715
+ * append) rejects with the typed LeaseHeldError and mutates NOTHING;
716
+ * - a mutation guarded by a live lease for a DIFFERENT run rejects the
717
+ * same way (a lease fences exactly the run it names);
718
+ * - a mutation carrying no lease keeps single-writer semantics.
719
+ *
720
+ * Staleness is produced with release+reacquire (the epoch is monotonic
721
+ * per the leasable contract), so the suite needs no wall-clock sleeps.
722
+ */
723
+ const RUN = "fenced-run";
724
+ const OTHER = "fenced-other-run";
725
+ function fencedEntry(seq) {
726
+ return {
727
+ hashVersion: 2,
728
+ seq,
729
+ scope: "",
730
+ key: `fenced-key-${seq}`,
731
+ ordinal: 0,
732
+ kind: "step",
733
+ status: "ok",
734
+ value: { seq },
735
+ spanId: "fenced-span",
736
+ startedAt: (/* @__PURE__ */ new Date(17e11 + seq * 1e3)).toISOString()
737
+ };
738
+ }
739
+ function meta(runId, status, segments) {
740
+ return {
741
+ runId,
742
+ status,
743
+ segments,
744
+ updatedAt: `at-${status}-${String(segments)}`
745
+ };
746
+ }
747
+ async function readMeta(store, runId) {
748
+ const lookup = store;
749
+ if (typeof lookup.getMeta === "function") return lookup.getMeta(runId);
750
+ return (await store.listRuns()).find((m) => m.runId === runId);
751
+ }
752
+ async function mustRejectFenced(checkId, what, operation) {
753
+ try {
754
+ await operation();
755
+ } catch (thrown) {
756
+ ensure(thrown instanceof LeaseHeldError, checkId, `${what} must reject with the typed LeaseHeldError, got ${String(thrown)}`);
757
+ return;
758
+ }
759
+ ensure(false, checkId, `${what} must reject`);
760
+ }
761
+ function fencedWritesConformance(mk) {
762
+ return makeSuite("fencedWritesConformance", [
763
+ {
764
+ id: "fenced-marker-declared",
765
+ title: "the store declares the fencedWrites promise",
766
+ async run() {
767
+ ensure((await mk()).fencedWrites === true, "fenced-marker-declared", "fencedWritesConformance targets stores declaring `fencedWrites: true`; the marker is a promise, not an inference");
768
+ }
769
+ },
770
+ {
771
+ id: "fenced-meta-stale-rejected",
772
+ title: "a stale putMeta rejects typed and the successor meta is intact",
773
+ async run() {
774
+ const store = await mk();
775
+ const first = await store.acquire(RUN, "owner-a");
776
+ await store.putMeta(meta(RUN, "running", 2), first);
777
+ await store.release(first);
778
+ const second = await store.acquire(RUN, "owner-b");
779
+ await store.putMeta(meta(RUN, "running", 3), second);
780
+ await mustRejectFenced("fenced-meta-stale-rejected", "a stale putMeta", () => store.putMeta(meta(RUN, "cancelled", 2), first));
781
+ const current = await readMeta(store, RUN);
782
+ ensure(current !== void 0 && current.status === "running" && current.segments === 3, "fenced-meta-stale-rejected", `the successor meta (status running, segments 3) must survive a stale putMeta unchanged, got ${JSON.stringify(current)}`);
783
+ }
784
+ },
785
+ {
786
+ id: "fenced-delete-stale-rejected",
787
+ title: "a stale delete rejects typed and journal plus meta are intact",
788
+ async run() {
789
+ const store = await mk();
790
+ const first = await store.acquire(RUN, "owner-a");
791
+ await store.append(RUN, fencedEntry(0), first);
792
+ await store.putMeta(meta(RUN, "running", 1), first);
793
+ await store.release(first);
794
+ const second = await store.acquire(RUN, "owner-b");
795
+ await mustRejectFenced("fenced-delete-stale-rejected", "a stale delete", () => store.delete(RUN, first));
796
+ ensure((await store.load(RUN)).length === 1, "fenced-delete-stale-rejected", "the journal must survive a stale delete unchanged");
797
+ ensure(await readMeta(store, RUN) !== void 0, "fenced-delete-stale-rejected", "the meta row must survive a stale delete unchanged");
798
+ await store.delete(RUN, second);
799
+ ensure((await store.load(RUN)).length === 0 && await readMeta(store, RUN) === void 0, "fenced-delete-stale-rejected", "a delete under the current lease must remove the journal and the meta row");
800
+ }
801
+ },
802
+ {
803
+ id: "fenced-run-match",
804
+ title: "a live lease for a different run guards nothing",
805
+ async run() {
806
+ const store = await mk();
807
+ const foreign = await store.acquire(OTHER, "owner-a");
808
+ await store.putMeta(meta(RUN, "running", 1));
809
+ await store.append(RUN, fencedEntry(0));
810
+ await mustRejectFenced("fenced-run-match", "a cross-run putMeta", () => store.putMeta(meta(RUN, "cancelled", 1), foreign));
811
+ await mustRejectFenced("fenced-run-match", "a cross-run append", () => store.append(RUN, fencedEntry(1), foreign));
812
+ await mustRejectFenced("fenced-run-match", "a cross-run delete", () => store.delete(RUN, foreign));
813
+ const current = await readMeta(store, RUN);
814
+ ensure(current !== void 0 && current.status === "running", "fenced-run-match", "the run guarded by nobody must survive every cross-run mutation attempt");
815
+ ensure((await store.load(RUN)).length === 1, "fenced-run-match", "a cross-run append must never become visible");
816
+ }
817
+ },
818
+ {
819
+ id: "fenced-unleased-passthrough",
820
+ title: "mutations carrying no lease keep single-writer semantics",
821
+ async run() {
822
+ const store = await mk();
823
+ await store.putMeta(meta(RUN, "running", 1));
824
+ await store.append(RUN, fencedEntry(0));
825
+ const current = await readMeta(store, RUN);
826
+ ensure(current !== void 0 && current.status === "running", "fenced-unleased-passthrough", "an unleased putMeta must keep working");
827
+ await store.delete(RUN);
828
+ ensure((await store.load(RUN)).length === 0 && await readMeta(store, RUN) === void 0, "fenced-unleased-passthrough", "an unleased delete must keep working");
829
+ }
830
+ }
831
+ ]);
832
+ }
833
+ //#endregion
834
+ export { GOLDEN_FOLD_JOURNAL, GOLDEN_FOLD_STATE_SHA256, fencedWritesConformance, foldStateSha256, journalStoreConformance, leasableStoreConformance, makeSuite, materializeFoldState, registerConformance, stableStringify };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/store-conformance",
3
- "version": "1.44.0",
3
+ "version": "1.45.0",
4
4
  "description": "Rulvar executable store conformance kit (DEF-4).",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -22,7 +22,7 @@
22
22
  "access": "public"
23
23
  },
24
24
  "dependencies": {
25
- "@rulvar/core": "1.44.0"
25
+ "@rulvar/core": "1.45.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.20.0",