@rulvar/store-conformance 1.9.0 → 1.11.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.js +102 -3
- 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,
|
|
210
|
-
*
|
|
211
|
-
*
|
|
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.
|
|
3
|
+
"version": "1.11.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.
|
|
25
|
+
"@rulvar/core": "1.11.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^22.20.0",
|