@rulvar/store-conformance 1.10.0 → 1.12.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.
Files changed (2) hide show
  1. package/dist/index.js +102 -3
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -206,9 +206,11 @@ const GOLDEN_FOLD_STATE_SHA256 = "81e6ccff549fb3e6c1de4d34ba65b912162eba6f66403b
206
206
  * journalStoreConformance (M2-T11, DEF-4): the executable definition of
207
207
  * the JournalStore seam. Mandatory checks for the conformance tier:
208
208
  * A1 append atomicity, A2 total per-run order, A3 read-your-writes,
209
- * A4 opaque payload, meta separation, the golden fold-state fixture, the
210
- * end-to-end decide-once oracle, and the abandon fixture (zero live
211
- * classes inside a skipped subtree, zero ledger increment).
209
+ * A4 opaque payload, A5 monotonic seq (a stale or duplicate seq append
210
+ * rejects with the typed journal_order_violation and never persists),
211
+ * meta separation, the golden fold-state fixture, the end-to-end
212
+ * decide-once oracle, and the abandon fixture (zero live classes inside
213
+ * a skipped subtree, zero ledger increment).
212
214
  *
213
215
  * The oracle and the abandon fixture drive the kernel (Replayer,
214
216
  * ResolutionFold, the DEF-1 predicate) directly over the store under
@@ -335,6 +337,103 @@ function journalStoreConformance(mk) {
335
337
  ensure(loaded.hashVersion === void 0, "a4-opaque-payload", "the store injected hashVersion into a legacy entry (normalization is read-side only)");
336
338
  }
337
339
  },
340
+ {
341
+ id: "a5-monotonic-seq",
342
+ title: "append rejects a duplicate or stale seq with the typed journal_order_violation",
343
+ async run() {
344
+ const store = await mk();
345
+ await store.append(RUN$1, baseEntry(0));
346
+ await store.append(RUN$1, baseEntry(1));
347
+ for (const stale of [1, 0]) {
348
+ let thrown;
349
+ try {
350
+ await store.append(RUN$1, baseEntry(stale, { value: {
351
+ seq: stale,
352
+ second: true
353
+ } }));
354
+ } catch (error) {
355
+ thrown = error;
356
+ }
357
+ ensure(thrown?.code === "journal_order_violation", "a5-monotonic-seq", `append of stale seq ${stale} must reject with code 'journal_order_violation'`);
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");
362
+ }
363
+ },
364
+ {
365
+ id: "a5-stale-tail-race",
366
+ title: "two writers appending the same next seq: exactly one persists, the loser is typed",
367
+ async run() {
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: {
371
+ seq: 3,
372
+ writer: "a"
373
+ } })), store.append(RUN$1, baseEntry(3, { value: {
374
+ seq: 3,
375
+ writer: "b"
376
+ } }))]);
377
+ const fulfilled = settled.filter((item) => item.status === "fulfilled");
378
+ const rejected = settled.filter((item) => item.status === "rejected");
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
+ 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);
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
+ const seqs = loaded.map((item) => item.seq);
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");
385
+ }
386
+ },
387
+ {
388
+ id: "a5-stale-replayer-fencing",
389
+ title: "two kernel replayers from the same tail: one append wins, the loser gets the conflict",
390
+ async run() {
391
+ const store = await mk();
392
+ const seed = new Replayer({
393
+ runId: RUN$1,
394
+ store
395
+ });
396
+ for (let i = 0; i < 3; i += 1) await seed.appendSinglePhase({
397
+ scope: "",
398
+ key: `seed-${i}`,
399
+ kind: "step",
400
+ status: "ok",
401
+ spanId: "conf-span",
402
+ value: { i }
403
+ });
404
+ const tail = await store.load(RUN$1);
405
+ const writerA = new Replayer({
406
+ runId: RUN$1,
407
+ store,
408
+ priorEntries: tail
409
+ });
410
+ const writerB = new Replayer({
411
+ runId: RUN$1,
412
+ store,
413
+ priorEntries: tail
414
+ });
415
+ const settled = await Promise.allSettled([writerA.appendSinglePhase({
416
+ scope: "",
417
+ key: "race",
418
+ kind: "step",
419
+ status: "ok",
420
+ spanId: "conf-span",
421
+ value: "a"
422
+ }), writerB.appendSinglePhase({
423
+ scope: "",
424
+ key: "race",
425
+ kind: "step",
426
+ status: "ok",
427
+ spanId: "conf-span",
428
+ value: "b"
429
+ })]);
430
+ const rejected = settled.filter((item) => item.status === "rejected");
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
+ 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);
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
+ }
436
+ },
338
437
  {
339
438
  id: "meta-separation",
340
439
  title: "putMeta replaces the run record; listRuns filters without payload parsing; delete removes both",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rulvar/store-conformance",
3
- "version": "1.10.0",
3
+ "version": "1.12.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.10.0"
25
+ "@rulvar/core": "1.12.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/node": "^22.20.0",