@sensigo/realm-testing 0.37.0 → 0.39.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/README.md +40 -0
- package/dist/assertions/evidence.d.ts.map +1 -1
- package/dist/assertions/evidence.js +8 -2
- package/dist/assertions/evidence.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runner/test-runner.d.ts.map +1 -1
- package/dist/runner/test-runner.js +4 -1
- package/dist/runner/test-runner.js.map +1 -1
- package/dist/store/in-memory-store.d.ts +7 -1
- package/dist/store/in-memory-store.d.ts.map +1 -1
- package/dist/store/in-memory-store.js +49 -1
- package/dist/store/in-memory-store.js.map +1 -1
- package/dist/store/run-store-fidelity-contract.d.ts +3 -1
- package/dist/store/run-store-fidelity-contract.d.ts.map +1 -1
- package/dist/store/run-store-fidelity-contract.js +41 -0
- package/dist/store/run-store-fidelity-contract.js.map +1 -1
- package/dist/store/settlement-contract.d.ts +39 -1
- package/dist/store/settlement-contract.d.ts.map +1 -1
- package/dist/store/settlement-contract.js +709 -2
- package/dist/store/settlement-contract.js.map +1 -1
- package/package.json +2 -2
|
@@ -405,6 +405,10 @@ function terminalRefusalCases(adapter) {
|
|
|
405
405
|
await adapter.store.update({
|
|
406
406
|
...run,
|
|
407
407
|
terminal_state: true,
|
|
408
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
409
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
410
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
411
|
+
sealed_by: { arm: 'complete' },
|
|
408
412
|
terminal_reason: 'tck-terminal',
|
|
409
413
|
});
|
|
410
414
|
const result = await settleStep(run.id, {
|
|
@@ -432,6 +436,23 @@ function terminalRefusalCases(adapter) {
|
|
|
432
436
|
// Reintroducing either trio disjunct into `isTerminal` reds this law; genuine terminal refusal is
|
|
433
437
|
// TERMINAL_REFUSAL's job (`terminal_state: true` — which abandon/abort ALWAYS set atomically,
|
|
434
438
|
// abandon-run.ts:66-71 / execution-loop.ts:1803-1811).
|
|
439
|
+
//
|
|
440
|
+
// REPINNED 2026-08-20 (issue #367) — ARM-WINS. Each case below now also asserts the DERIVED PHASE
|
|
441
|
+
// of its product record, and that phase CHANGED with #367: the run is settled `complete`, so it
|
|
442
|
+
// stamps `sealed_by: {arm: 'complete'}`, and the recorded arm outranks the seeded marker. Before
|
|
443
|
+
// #367 the same record derived `abandoned`/`aborted` from the marker alone.
|
|
444
|
+
//
|
|
445
|
+
// These two product records are the ONLY engine-constructible arm-vs-marker disagreements in the
|
|
446
|
+
// suite, which makes them the keystone: revert the sealed-wins branch in `deriveRunPhase` and the
|
|
447
|
+
// new assertions below are what reds. Nothing else observes that revert — the arm path and the
|
|
448
|
+
// legacy path agree on every honest record.
|
|
449
|
+
//
|
|
450
|
+
// The seeded foreign marker violates nothing: `SEAL_MARKERS_AGREE` is transform-scoped and
|
|
451
|
+
// ONE-DIRECTIONAL (an arm requires its own marker; it never forbids a marker it did not write).
|
|
452
|
+
// Do not "fix" these fixtures by removing the marker — the disagreement IS the test.
|
|
453
|
+
//
|
|
454
|
+
// (The law's own premise comment above — "resume never strips abandoned_at" — has been stale since
|
|
455
|
+
// #281; the honest production channel for this shape is a mixed-version fleet.)
|
|
435
456
|
// ---------------------------------------------------------------------------
|
|
436
457
|
function terminalStateOnlyCases(adapter) {
|
|
437
458
|
const { minimalDefinition } = adapter.settlementFixture;
|
|
@@ -460,6 +481,12 @@ function terminalStateOnlyCases(adapter) {
|
|
|
460
481
|
if (!result.run.completed_steps.includes('a')) {
|
|
461
482
|
throw new Error(`expected 'a' to land in completed_steps, got: ${JSON.stringify(result.run.completed_steps)}`);
|
|
462
483
|
}
|
|
484
|
+
// issue #367 KEYSTONE: the run sealed `complete`, so the recorded arm decides — even
|
|
485
|
+
// though `abandoned_at` is still on the record. Pre-#367 this derived 'abandoned'.
|
|
486
|
+
const phase = deriveRunPhase(result.run);
|
|
487
|
+
if (phase !== 'completed') {
|
|
488
|
+
throw new Error(`arm-wins: expected derived phase 'completed' from sealed_by.arm '${String(result.run.sealed_by?.arm)}' despite the seeded abandoned_at, got '${phase}'`);
|
|
489
|
+
}
|
|
463
490
|
},
|
|
464
491
|
},
|
|
465
492
|
{
|
|
@@ -486,6 +513,11 @@ function terminalStateOnlyCases(adapter) {
|
|
|
486
513
|
if (!result.run.completed_steps.includes('a')) {
|
|
487
514
|
throw new Error(`expected 'a' to land in completed_steps, got: ${JSON.stringify(result.run.completed_steps)}`);
|
|
488
515
|
}
|
|
516
|
+
// issue #367 KEYSTONE (the aborted half): same disagreement, other marker.
|
|
517
|
+
const phase = deriveRunPhase(result.run);
|
|
518
|
+
if (phase !== 'completed') {
|
|
519
|
+
throw new Error(`arm-wins: expected derived phase 'completed' from sealed_by.arm '${String(result.run.sealed_by?.arm)}' despite the seeded aborted_at, got '${phase}'`);
|
|
520
|
+
}
|
|
489
521
|
},
|
|
490
522
|
},
|
|
491
523
|
];
|
|
@@ -494,6 +526,607 @@ function terminalStateOnlyCases(adapter) {
|
|
|
494
526
|
// L7 CS-purity — structural: `options` carries VALUES only ({now}); no callback, no registry.
|
|
495
527
|
// In-repo source-text guard (the calling test file greps applySettlement's own signature).
|
|
496
528
|
// ---------------------------------------------------------------------------
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// issue #367 — the seal-integrity boundary laws. An assertion-capable store (one declaring
|
|
531
|
+
// `settleStep`) must REFUSE each of the four violations rather than persisting them: a record that
|
|
532
|
+
// lies about how it ended is permanent, and an advisory would report success on a write that broke
|
|
533
|
+
// its own invariant.
|
|
534
|
+
// ---------------------------------------------------------------------------
|
|
535
|
+
function sealIntegrityCases(adapter) {
|
|
536
|
+
const { minimalDefinition } = adapter.settlementFixture;
|
|
537
|
+
requireSettleStep(adapter.store); // assertion-capable stores only
|
|
538
|
+
/** Runs `write`, expecting it to reject with `code`. */
|
|
539
|
+
async function expectRefusedWith(code, what, write) {
|
|
540
|
+
let threw;
|
|
541
|
+
try {
|
|
542
|
+
await write();
|
|
543
|
+
}
|
|
544
|
+
catch (err) {
|
|
545
|
+
threw = err;
|
|
546
|
+
}
|
|
547
|
+
if (threw === undefined) {
|
|
548
|
+
throw new Error(`expected ${what} to be REFUSED with ${code}, but the write succeeded`);
|
|
549
|
+
}
|
|
550
|
+
const actual = threw.code;
|
|
551
|
+
if (actual !== code) {
|
|
552
|
+
throw new Error(`expected ${what} to be refused with ${code}, got '${String(actual)}'`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
async function freshRun(name) {
|
|
556
|
+
const def = minimalDefinition(['a']);
|
|
557
|
+
const { run } = await adapter.store.create({
|
|
558
|
+
workflowId: `${def.id}-${name}`,
|
|
559
|
+
workflowVersion: 1,
|
|
560
|
+
params: {},
|
|
561
|
+
});
|
|
562
|
+
return run;
|
|
563
|
+
}
|
|
564
|
+
return [
|
|
565
|
+
{
|
|
566
|
+
law: 'SEAL_FRESH_WRITE_REFUSED',
|
|
567
|
+
name: `[${adapter.storeName}] a live → terminal write carrying NO sealed_by is refused`,
|
|
568
|
+
run: async () => {
|
|
569
|
+
const run = await freshRun('unstamped');
|
|
570
|
+
await expectRefusedWith('STATE_SEAL_UNSTAMPED', 'an unstamped fresh seal', () =>
|
|
571
|
+
/* eslint-disable-next-line no-restricted-syntax --
|
|
572
|
+
* issue #367 (part 2), AUTHORIZED: this violation is the LAW. SEAL_FRESH_WRITE_REFUSED
|
|
573
|
+
* exists to prove the store refuses an unstamped fresh seal, so the fixture has to
|
|
574
|
+
* construct one. (This file is a published contract source, not a `.test.ts`, which is
|
|
575
|
+
* why the test-file scoping does not cover it.)
|
|
576
|
+
*/
|
|
577
|
+
adapter.store.update({ ...run, terminal_state: true, terminal_reason: 'tck' }));
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
law: 'SEAL_FRESH_WRITE_REFUSED',
|
|
582
|
+
name: `[${adapter.storeName}] NEGATIVE CONTROL: re-writing an ALREADY-terminal unstamped record passes (the legacy population stays usable)`,
|
|
583
|
+
run: async () => {
|
|
584
|
+
const run = await freshRun('legacy-repersist');
|
|
585
|
+
const sealed = await adapter.store.update({
|
|
586
|
+
...run,
|
|
587
|
+
terminal_state: true,
|
|
588
|
+
sealed_by: { arm: 'complete' },
|
|
589
|
+
terminal_reason: 'tck',
|
|
590
|
+
});
|
|
591
|
+
// A terminal → terminal rewrite. The forward clause is transition-scoped, so this is fine.
|
|
592
|
+
await adapter.store.update({ ...sealed, terminal_reason: 'tck (touched)' });
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
law: 'SEAL_ORPHAN_REFUSED',
|
|
597
|
+
name: `[${adapter.storeName}] a terminal → live write that RETAINS sealed_by is refused`,
|
|
598
|
+
run: async () => {
|
|
599
|
+
const run = await freshRun('orphan');
|
|
600
|
+
const sealed = await adapter.store.update({
|
|
601
|
+
...run,
|
|
602
|
+
terminal_state: true,
|
|
603
|
+
sealed_by: { arm: 'complete' },
|
|
604
|
+
terminal_reason: 'tck',
|
|
605
|
+
});
|
|
606
|
+
await expectRefusedWith('STATE_SEAL_ORPHANED', 'a resume that kept the seal', () => adapter.store.update({ ...sealed, terminal_state: false }));
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
law: 'SEAL_ORPHAN_REFUSED',
|
|
611
|
+
name: `[${adapter.storeName}] NEGATIVE CONTROL: the same transition WITH the seal stripped passes`,
|
|
612
|
+
run: async () => {
|
|
613
|
+
const run = await freshRun('orphan-control');
|
|
614
|
+
const sealed = await adapter.store.update({
|
|
615
|
+
...run,
|
|
616
|
+
terminal_state: true,
|
|
617
|
+
sealed_by: { arm: 'complete' },
|
|
618
|
+
terminal_reason: 'tck',
|
|
619
|
+
});
|
|
620
|
+
const { sealed_by: _dropped, ...base } = sealed;
|
|
621
|
+
await adapter.store.update({ ...base, terminal_state: false });
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
law: 'SEAL_ERASE_REFUSED',
|
|
626
|
+
name: `[${adapter.storeName}] a terminal rewrite that DROPS a stored sealed_by is refused`,
|
|
627
|
+
run: async () => {
|
|
628
|
+
const run = await freshRun('erase');
|
|
629
|
+
const sealed = await adapter.store.update({
|
|
630
|
+
...run,
|
|
631
|
+
terminal_state: true,
|
|
632
|
+
sealed_by: { arm: 'complete' },
|
|
633
|
+
terminal_reason: 'tck',
|
|
634
|
+
});
|
|
635
|
+
const { sealed_by: _erased, ...withoutSeal } = sealed;
|
|
636
|
+
await expectRefusedWith('STATE_SEAL_ERASED', 'a terminal rewrite dropping the seal', () => adapter.store.update({ ...withoutSeal, terminal_reason: 'tck (erased)' }));
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
law: 'SEAL_ERASE_REFUSED',
|
|
641
|
+
name: `[${adapter.storeName}] NEGATIVE CONTROL: the same rewrite KEEPING the seal passes`,
|
|
642
|
+
run: async () => {
|
|
643
|
+
const run = await freshRun('erase-control');
|
|
644
|
+
const sealed = await adapter.store.update({
|
|
645
|
+
...run,
|
|
646
|
+
terminal_state: true,
|
|
647
|
+
sealed_by: { arm: 'complete' },
|
|
648
|
+
terminal_reason: 'tck',
|
|
649
|
+
});
|
|
650
|
+
await adapter.store.update({ ...sealed, terminal_reason: 'tck (kept)' });
|
|
651
|
+
},
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
655
|
+
name: `[${adapter.storeName}] a stored arm may NOT be changed while the run stays terminal`,
|
|
656
|
+
run: async () => {
|
|
657
|
+
const run = await freshRun('rewrite');
|
|
658
|
+
const sealed = await adapter.store.update({
|
|
659
|
+
...run,
|
|
660
|
+
terminal_state: true,
|
|
661
|
+
sealed_by: { arm: 'complete' },
|
|
662
|
+
terminal_reason: 'Workflow completed.',
|
|
663
|
+
});
|
|
664
|
+
await expectRefusedWith('STATE_SEAL_REWRITTEN', 'a seal-arm rewrite', () => adapter.store.update({ ...sealed, sealed_by: { arm: 'guard_pass_complete' } }));
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
669
|
+
name: `[${adapter.storeName}] a rewrite WITH truthful adjudication provenance is ACCEPTED`,
|
|
670
|
+
run: async () => {
|
|
671
|
+
// The lawful key, published day-one so this law never has to be loosened later. The arm
|
|
672
|
+
// pair is deliberately SAME-PHASE: a cross-phase ruling on a record whose prose still says
|
|
673
|
+
// otherwise is refused by SEAL_COHERENT instead, which is a different law's job.
|
|
674
|
+
const run = await freshRun('adjudicated');
|
|
675
|
+
const sealed = await adapter.store.update({
|
|
676
|
+
...run,
|
|
677
|
+
terminal_state: true,
|
|
678
|
+
sealed_by: { arm: 'complete' },
|
|
679
|
+
terminal_reason: 'Workflow completed.',
|
|
680
|
+
});
|
|
681
|
+
const ruled = await adapter.store.update({
|
|
682
|
+
...sealed,
|
|
683
|
+
sealed_by: {
|
|
684
|
+
arm: 'guard_pass_complete',
|
|
685
|
+
adjudicated: {
|
|
686
|
+
by: 'tck-operator',
|
|
687
|
+
at: '2026-01-01T00:00:00.000Z',
|
|
688
|
+
previous_arm: 'complete',
|
|
689
|
+
reason: 'the guard closed this run',
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
if (ruled.sealed_by?.arm !== 'guard_pass_complete') {
|
|
694
|
+
throw new Error(`the adjudicated arm did not land: ${JSON.stringify(ruled.sealed_by)}`);
|
|
695
|
+
}
|
|
696
|
+
const reread = await adapter.store.get(run.id);
|
|
697
|
+
// The FULL object, field by field: a store that drops `by` or `reason` keeps the ruling's
|
|
698
|
+
// arm and loses who made it and why, which is most of what a ruling IS.
|
|
699
|
+
const expected = {
|
|
700
|
+
by: 'tck-operator',
|
|
701
|
+
at: '2026-01-01T00:00:00.000Z',
|
|
702
|
+
previous_arm: 'complete',
|
|
703
|
+
reason: 'the guard closed this run',
|
|
704
|
+
};
|
|
705
|
+
if (JSON.stringify(reread.sealed_by?.adjudicated) !== JSON.stringify(expected)) {
|
|
706
|
+
throw new Error(`the ruling did not survive the round trip in full: ` +
|
|
707
|
+
`${JSON.stringify(reread.sealed_by?.adjudicated)} — expected ${JSON.stringify(expected)}`);
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
{
|
|
712
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
713
|
+
name: `[${adapter.storeName}] a rewrite whose adjudication LIES about previous_arm is refused`,
|
|
714
|
+
run: async () => {
|
|
715
|
+
const run = await freshRun('adjudicated-lying');
|
|
716
|
+
const sealed = await adapter.store.update({
|
|
717
|
+
...run,
|
|
718
|
+
terminal_state: true,
|
|
719
|
+
sealed_by: { arm: 'complete' },
|
|
720
|
+
terminal_reason: 'Workflow completed.',
|
|
721
|
+
});
|
|
722
|
+
await expectRefusedWith('STATE_SEAL_REWRITTEN', 'a lying adjudication', () => adapter.store.update({
|
|
723
|
+
...sealed,
|
|
724
|
+
sealed_by: {
|
|
725
|
+
arm: 'guard_pass_complete',
|
|
726
|
+
adjudicated: { by: 'tck', at: 'now', previous_arm: 'step_failure' },
|
|
727
|
+
},
|
|
728
|
+
}));
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
733
|
+
name: `[${adapter.storeName}] a SAME-arm write minting lying provenance is refused; a truthful acknowledgment is accepted`,
|
|
734
|
+
run: async () => {
|
|
735
|
+
const run = await freshRun('adjudicated-same');
|
|
736
|
+
const sealed = await adapter.store.update({
|
|
737
|
+
...run,
|
|
738
|
+
terminal_state: true,
|
|
739
|
+
sealed_by: { arm: 'complete' },
|
|
740
|
+
terminal_reason: 'Workflow completed.',
|
|
741
|
+
});
|
|
742
|
+
await expectRefusedWith('STATE_SEAL_REWRITTEN', 'a same-arm lying mint', () => adapter.store.update({
|
|
743
|
+
...sealed,
|
|
744
|
+
sealed_by: {
|
|
745
|
+
arm: 'complete',
|
|
746
|
+
adjudicated: { by: 'tck', at: 'now', previous_arm: 'guard_abort' },
|
|
747
|
+
},
|
|
748
|
+
}));
|
|
749
|
+
// The acknowledgment channel: "looked at it, it stands."
|
|
750
|
+
await adapter.store.update({
|
|
751
|
+
...sealed,
|
|
752
|
+
sealed_by: {
|
|
753
|
+
arm: 'complete',
|
|
754
|
+
adjudicated: { by: 'tck', at: 'now', previous_arm: 'complete' },
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
},
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
761
|
+
name: `[${adapter.storeName}] a terminal rewrite dropping stored adjudication provenance is refused`,
|
|
762
|
+
run: async () => {
|
|
763
|
+
const run = await freshRun('adjudicated-erase');
|
|
764
|
+
const sealed = await adapter.store.update({
|
|
765
|
+
...run,
|
|
766
|
+
terminal_state: true,
|
|
767
|
+
sealed_by: { arm: 'complete' },
|
|
768
|
+
terminal_reason: 'Workflow completed.',
|
|
769
|
+
});
|
|
770
|
+
const ruled = await adapter.store.update({
|
|
771
|
+
...sealed,
|
|
772
|
+
sealed_by: {
|
|
773
|
+
arm: 'complete',
|
|
774
|
+
adjudicated: { by: 'tck', at: 'now', previous_arm: 'complete' },
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
await expectRefusedWith('STATE_SEAL_REWRITTEN', 'erasing a recorded ruling', () => adapter.store.update({ ...ruled, sealed_by: { arm: 'complete' } }));
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
// --- Direction 6: an operator's FIRST stamp of a parked record, three legs. ---
|
|
781
|
+
...(adapter.seedLegacyTerminal === undefined
|
|
782
|
+
? []
|
|
783
|
+
: [
|
|
784
|
+
{
|
|
785
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
786
|
+
name: `[${adapter.storeName}] a terminal UNSTAMPED record accepts a first stamp with previous_arm: null`,
|
|
787
|
+
run: async () => {
|
|
788
|
+
// The parked-unclassifiable population's only attributed landing channel. `null` is
|
|
789
|
+
// the truthful statement "no arm existed before this ruling".
|
|
790
|
+
const seeded = await adapter.seedLegacyTerminal('tck-adjudicate-first');
|
|
791
|
+
const ruled = await adapter.store.update({
|
|
792
|
+
...seeded,
|
|
793
|
+
sealed_by: {
|
|
794
|
+
arm: 'complete',
|
|
795
|
+
adjudicated: {
|
|
796
|
+
by: 'tck-operator',
|
|
797
|
+
at: '2026-01-01T00:00:00.000Z',
|
|
798
|
+
previous_arm: null,
|
|
799
|
+
reason: 'operator judgement',
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
});
|
|
803
|
+
if (ruled.sealed_by?.adjudicated?.previous_arm !== null) {
|
|
804
|
+
throw new Error(`the null first-stamp provenance did not land: ${JSON.stringify(ruled.sealed_by)}`);
|
|
805
|
+
}
|
|
806
|
+
const reread = await adapter.store.get(seeded.id);
|
|
807
|
+
if (reread.sealed_by?.adjudicated?.by !== 'tck-operator') {
|
|
808
|
+
throw new Error(`the ruling did not survive the round trip: ${JSON.stringify(reread.sealed_by)}`);
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
814
|
+
name: `[${adapter.storeName}] a first stamp claiming a NON-NULL previous_arm is refused`,
|
|
815
|
+
run: async () => {
|
|
816
|
+
const seeded = await adapter.seedLegacyTerminal('tck-adjudicate-first-lying');
|
|
817
|
+
await expectRefusedWith('STATE_SEAL_REWRITTEN', 'a fabricated first-stamp claim', () => adapter.store.update({
|
|
818
|
+
...seeded,
|
|
819
|
+
sealed_by: {
|
|
820
|
+
arm: 'complete',
|
|
821
|
+
adjudicated: { by: 'tck', at: 'now', previous_arm: 'step_failure' },
|
|
822
|
+
},
|
|
823
|
+
}));
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
]),
|
|
827
|
+
{
|
|
828
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
829
|
+
name: `[${adapter.storeName}] a LIVE run's first seal cannot claim adjudication, even with null`,
|
|
830
|
+
run: async () => {
|
|
831
|
+
// The terminal-scope half of the first-stamp rule: nothing had happened yet to rule on.
|
|
832
|
+
const run = await freshRun('adjudicate-live');
|
|
833
|
+
await expectRefusedWith('STATE_SEAL_REWRITTEN', 'adjudication on a live first seal', () => adapter.store.update({
|
|
834
|
+
...run,
|
|
835
|
+
terminal_state: true,
|
|
836
|
+
terminal_reason: 'Workflow completed.',
|
|
837
|
+
sealed_by: {
|
|
838
|
+
arm: 'complete',
|
|
839
|
+
adjudicated: { by: 'tck', at: 'now', previous_arm: null },
|
|
840
|
+
},
|
|
841
|
+
}));
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
846
|
+
name: `[${adapter.storeName}] a ruling SUPERSEDES the record's own prose, and keeps doing so`,
|
|
847
|
+
run: async () => {
|
|
848
|
+
// Direction 7. The scar rides the ruling write because the boundary itself refuses
|
|
849
|
+
// parked-scar creation through public channels — that population is minted by old binaries
|
|
850
|
+
// this suite cannot reproduce. The stored-scar half is pinned in the engine's own cells.
|
|
851
|
+
const run = await freshRun('adjudicate-supersedes');
|
|
852
|
+
const sealed = await adapter.store.update({
|
|
853
|
+
...run,
|
|
854
|
+
terminal_state: true,
|
|
855
|
+
sealed_by: { arm: 'complete' },
|
|
856
|
+
terminal_reason: 'Workflow completed.',
|
|
857
|
+
});
|
|
858
|
+
const ruled = await adapter.store.update({
|
|
859
|
+
...sealed,
|
|
860
|
+
failed_steps: ['x'],
|
|
861
|
+
terminal_reason: 'a scar this classifier cannot place',
|
|
862
|
+
sealed_by: {
|
|
863
|
+
arm: 'complete',
|
|
864
|
+
adjudicated: { by: 'tck-operator', at: 'now', previous_arm: 'complete' },
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
if (ruled.terminal_reason !== 'a scar this classifier cannot place') {
|
|
868
|
+
throw new Error('the prose was rewritten — a ruling must never falsify the record');
|
|
869
|
+
}
|
|
870
|
+
// Leg 2: the exemption is PERMANENT. A store exempting only the ruling write wedges the
|
|
871
|
+
// operator on every later spread of the same record.
|
|
872
|
+
await adapter.store.update({ ...ruled, updated_at: '2099-01-01T00:00:00.000Z' });
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
877
|
+
name: `[${adapter.storeName}] an arm change RIDING a prior ruling is refused — a change needs a FRESH ruling`,
|
|
878
|
+
run: async () => {
|
|
879
|
+
// Direction 8.
|
|
880
|
+
const run = await freshRun('adjudicate-riding');
|
|
881
|
+
const sealed = await adapter.store.update({
|
|
882
|
+
...run,
|
|
883
|
+
terminal_state: true,
|
|
884
|
+
sealed_by: { arm: 'complete' },
|
|
885
|
+
terminal_reason: 'Workflow completed.',
|
|
886
|
+
});
|
|
887
|
+
const ruled = await adapter.store.update({
|
|
888
|
+
...sealed,
|
|
889
|
+
sealed_by: {
|
|
890
|
+
arm: 'complete',
|
|
891
|
+
adjudicated: {
|
|
892
|
+
by: 'tck-operator',
|
|
893
|
+
at: '2026-02-02T00:00:00.000Z',
|
|
894
|
+
previous_arm: 'complete',
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
await expectRefusedWith('STATE_SEAL_REWRITTEN', 'an arm change riding a prior ruling', () => adapter.store.update({
|
|
899
|
+
...ruled,
|
|
900
|
+
sealed_by: { arm: 'guard_pass_complete', adjudicated: ruled.sealed_by.adjudicated },
|
|
901
|
+
}));
|
|
902
|
+
},
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
law: 'SEAL_REWRITE_REFUSED',
|
|
906
|
+
name: `[${adapter.storeName}] NEGATIVE CONTROL: a rewrite keeping the SAME arm passes`,
|
|
907
|
+
run: async () => {
|
|
908
|
+
const run = await freshRun('rewrite-control');
|
|
909
|
+
const sealed = await adapter.store.update({
|
|
910
|
+
...run,
|
|
911
|
+
terminal_state: true,
|
|
912
|
+
sealed_by: { arm: 'complete' },
|
|
913
|
+
terminal_reason: 'Workflow completed.',
|
|
914
|
+
});
|
|
915
|
+
// Every ordinary terminal rewrite spreads the record — without this passing, the clause
|
|
916
|
+
// would wedge the engine.
|
|
917
|
+
await adapter.store.update({ ...sealed, terminal_reason: 'Workflow completed. (touched)' });
|
|
918
|
+
},
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
law: 'SEAL_UNKNOWN_ARM_REFUSED',
|
|
922
|
+
name: `[${adapter.storeName}] an arm outside SEAL_ARMS never persists`,
|
|
923
|
+
run: async () => {
|
|
924
|
+
const run = await freshRun('unknown-arm');
|
|
925
|
+
await expectRefusedWith('STATE_SEAL_UNKNOWN_ARM', 'a foreign arm', () => adapter.store.update({
|
|
926
|
+
...run,
|
|
927
|
+
terminal_state: true,
|
|
928
|
+
// Deliberately outside the closed set — the shape a record written by a FUTURE
|
|
929
|
+
// binary would have. The cast is the point: the type system cannot stop a foreign arm
|
|
930
|
+
// arriving from disk or from another version, which is why the boundary checks it.
|
|
931
|
+
sealed_by: { arm: 'from_the_future' },
|
|
932
|
+
terminal_reason: 'tck',
|
|
933
|
+
}));
|
|
934
|
+
},
|
|
935
|
+
},
|
|
936
|
+
];
|
|
937
|
+
}
|
|
938
|
+
// ---------------------------------------------------------------------------
|
|
939
|
+
// issue #367 part 3 — the `stampSeal` laws. A store that declares the verb is promising a very
|
|
940
|
+
// specific write: the seal arm lands, `version` moves so the CAS protocol can see it, and
|
|
941
|
+
// `updated_at` does NOT move because stamping is not activity. Get that split wrong in either
|
|
942
|
+
// direction and either the stamp is silently erasable or every migrated record looks freshly
|
|
943
|
+
// touched to retention.
|
|
944
|
+
// ---------------------------------------------------------------------------
|
|
945
|
+
function stampSealCases(adapter) {
|
|
946
|
+
const stampSeal = adapter.store.stampSeal;
|
|
947
|
+
if (stampSeal === undefined)
|
|
948
|
+
return []; // dormant store — the laws bind declarers only
|
|
949
|
+
const stamp = stampSeal.bind(adapter.store);
|
|
950
|
+
/**
|
|
951
|
+
* A terminal, ALREADY-STAMPED record — the refusal legs' fixture. (An earlier version of this
|
|
952
|
+
* comment claimed the seal was "then removed by a direct write"; no such removal existed, and
|
|
953
|
+
* the boundary's ERASED clause would refuse one. Corrected rather than reworded.)
|
|
954
|
+
*/
|
|
955
|
+
async function stampedTerminal(name) {
|
|
956
|
+
const { run } = await adapter.store.create({
|
|
957
|
+
workflowId: `tck-stamp-${name}`,
|
|
958
|
+
workflowVersion: 1,
|
|
959
|
+
params: {},
|
|
960
|
+
});
|
|
961
|
+
return adapter.store.update({
|
|
962
|
+
...run,
|
|
963
|
+
completed_steps: ['a'],
|
|
964
|
+
terminal_state: true,
|
|
965
|
+
sealed_by: { arm: 'complete' },
|
|
966
|
+
terminal_reason: 'Workflow completed.',
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return [
|
|
970
|
+
{
|
|
971
|
+
law: 'STAMP_PRESERVES_UPDATED_AT',
|
|
972
|
+
name: `[${adapter.storeName}] stampSeal leaves updated_at byte-identical`,
|
|
973
|
+
run: async () => {
|
|
974
|
+
const sealed = await stampedTerminal('clock');
|
|
975
|
+
// Already stamped, so this exercises the refusal path's clock too — the record must not be
|
|
976
|
+
// touched at all.
|
|
977
|
+
const before = await adapter.store.get(sealed.id);
|
|
978
|
+
await stamp(sealed.id, { arm: 'complete' }, before.version);
|
|
979
|
+
const after = await adapter.store.get(sealed.id);
|
|
980
|
+
if (after.updated_at !== before.updated_at) {
|
|
981
|
+
throw new Error(`stampSeal moved updated_at (${before.updated_at} -> ${after.updated_at}) — stamping ` +
|
|
982
|
+
`is not activity; the retention clock must not move`);
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
// --- SUCCESS legs. These need an UNSTAMPED terminal record, which is why the seed hook
|
|
987
|
+
// exists: without them a store that NEVER WRITES conforms to all five laws.
|
|
988
|
+
...(adapter.seedLegacyTerminal === undefined
|
|
989
|
+
? [
|
|
990
|
+
{
|
|
991
|
+
law: 'STAMP_PRESERVES_UPDATED_AT',
|
|
992
|
+
name: `[${adapter.storeName}] ADAPTER_WIRING: a stampSeal-declaring store must supply seedLegacyTerminal`,
|
|
993
|
+
run: async () => {
|
|
994
|
+
throw new Error('This store declares stampSeal but the adapter has no `seedLegacyTerminal` hook, ' +
|
|
995
|
+
'so the laws cannot observe an ACTUAL stamp — only its refusals. A store that ' +
|
|
996
|
+
'never writes would pass. Supply the hook.');
|
|
997
|
+
},
|
|
998
|
+
},
|
|
999
|
+
]
|
|
1000
|
+
: [
|
|
1001
|
+
{
|
|
1002
|
+
law: 'STAMP_PRESERVES_UPDATED_AT',
|
|
1003
|
+
name: `[${adapter.storeName}] a SUCCESSFUL stamp leaves updated_at byte-identical`,
|
|
1004
|
+
run: async () => {
|
|
1005
|
+
const seeded = await adapter.seedLegacyTerminal('tck-stamp-success-clock');
|
|
1006
|
+
const result = await stamp(seeded.id, { arm: 'complete', classified: true }, seeded.version);
|
|
1007
|
+
if (!result.stamped) {
|
|
1008
|
+
throw new Error(`expected an unstamped terminal record to be STAMPED, got refusal ` +
|
|
1009
|
+
`'${result.reason}' — a store that never writes is not conformant`);
|
|
1010
|
+
}
|
|
1011
|
+
const after = await adapter.store.get(seeded.id);
|
|
1012
|
+
if (after.sealed_by?.arm !== 'complete') {
|
|
1013
|
+
throw new Error(`the arm did not land: ${JSON.stringify(after.sealed_by)}`);
|
|
1014
|
+
}
|
|
1015
|
+
if (after.updated_at !== seeded.updated_at) {
|
|
1016
|
+
throw new Error(`stamping moved updated_at (${seeded.updated_at} -> ${after.updated_at}) — ` +
|
|
1017
|
+
`stamping is not activity`);
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
law: 'STAMP_BUMPS_VERSION_ONCE',
|
|
1023
|
+
name: `[${adapter.storeName}] a SUCCESSFUL stamp bumps version exactly once`,
|
|
1024
|
+
run: async () => {
|
|
1025
|
+
const seeded = await adapter.seedLegacyTerminal('tck-stamp-success-version');
|
|
1026
|
+
await stamp(seeded.id, { arm: 'complete', classified: true }, seeded.version);
|
|
1027
|
+
const after = await adapter.store.get(seeded.id);
|
|
1028
|
+
if (after.version !== seeded.version + 1) {
|
|
1029
|
+
throw new Error(`expected version ${seeded.version + 1}, got ${after.version} — the CAS ` +
|
|
1030
|
+
`protocol is what stops a stale writer erasing the stamp`);
|
|
1031
|
+
}
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
law: 'STAMP_CLASSIFIED_ROUNDTRIP',
|
|
1036
|
+
name: `[${adapter.storeName}] a \`classified: true\` stamp survives write → read byte-for-byte`,
|
|
1037
|
+
run: async () => {
|
|
1038
|
+
// The provenance marker is what keeps a classifier-minted stamp distinguishable from
|
|
1039
|
+
// a writer's own assertion FOREVER. A store that drops it looks conformant on every
|
|
1040
|
+
// other law while quietly destroying that distinction.
|
|
1041
|
+
const seeded = await adapter.seedLegacyTerminal('tck-stamp-classified');
|
|
1042
|
+
await stamp(seeded.id, { arm: 'complete', classified: true }, seeded.version);
|
|
1043
|
+
const after = await adapter.store.get(seeded.id);
|
|
1044
|
+
if (after.sealed_by?.classified !== true) {
|
|
1045
|
+
throw new Error(`the \`classified\` provenance marker did not survive the round trip: ` +
|
|
1046
|
+
`${JSON.stringify(after.sealed_by)} — a vehicle-minted stamp must stay ` +
|
|
1047
|
+
`distinguishable from a writer-asserted one`);
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
]),
|
|
1052
|
+
{
|
|
1053
|
+
law: 'STAMP_RETURNS_NOT_THROWS_PREDICATES',
|
|
1054
|
+
name: `[${adapter.storeName}] a NON-TERMINAL record RETURNS 'not_terminal', never throws`,
|
|
1055
|
+
run: async () => {
|
|
1056
|
+
// No seed hook needed: `create()` makes a live record, which is exactly the shape.
|
|
1057
|
+
const { run } = await adapter.store.create({
|
|
1058
|
+
workflowId: 'tck-stamp-live',
|
|
1059
|
+
workflowVersion: 1,
|
|
1060
|
+
params: {},
|
|
1061
|
+
});
|
|
1062
|
+
const result = await stamp(run.id, { arm: 'complete' }, run.version);
|
|
1063
|
+
if (result.stamped !== false || result.reason !== 'not_terminal') {
|
|
1064
|
+
throw new Error(`expected a RETURNED {stamped:false, reason:'not_terminal'}, got ` +
|
|
1065
|
+
`${JSON.stringify(result)}`);
|
|
1066
|
+
}
|
|
1067
|
+
},
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
law: 'STAMP_RETURNS_NOT_THROWS_PREDICATES',
|
|
1071
|
+
name: `[${adapter.storeName}] an already-stamped record RETURNS, never throws`,
|
|
1072
|
+
run: async () => {
|
|
1073
|
+
const sealed = await stampedTerminal('already');
|
|
1074
|
+
const fresh = await adapter.store.get(sealed.id);
|
|
1075
|
+
const result = await stamp(fresh.id, { arm: 'complete' }, fresh.version);
|
|
1076
|
+
if (result.stamped !== false || result.reason !== 'already_stamped') {
|
|
1077
|
+
throw new Error(`expected a RETURNED {stamped:false, reason:'already_stamped'}, got ` +
|
|
1078
|
+
`${JSON.stringify(result)} — a predicate refusal is not an exceptional condition`);
|
|
1079
|
+
}
|
|
1080
|
+
},
|
|
1081
|
+
},
|
|
1082
|
+
{
|
|
1083
|
+
law: 'STAMP_BUMPS_VERSION_ONCE',
|
|
1084
|
+
name: `[${adapter.storeName}] a refused stamp does not bump version`,
|
|
1085
|
+
run: async () => {
|
|
1086
|
+
const sealed = await stampedTerminal('version');
|
|
1087
|
+
const before = await adapter.store.get(sealed.id);
|
|
1088
|
+
await stamp(before.id, { arm: 'complete' }, before.version);
|
|
1089
|
+
const after = await adapter.store.get(before.id);
|
|
1090
|
+
if (after.version !== before.version) {
|
|
1091
|
+
throw new Error(`a refused stamp moved version (${before.version} -> ${after.version}) — only a real ` +
|
|
1092
|
+
`write bumps it`);
|
|
1093
|
+
}
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
law: 'STAMP_REFUSES_ON_VERSION_MOVE',
|
|
1098
|
+
name: `[${adapter.storeName}] a stale expectedVersion THROWS STATE_SNAPSHOT_MISMATCH`,
|
|
1099
|
+
run: async () => {
|
|
1100
|
+
const sealed = await stampedTerminal('cas');
|
|
1101
|
+
let code;
|
|
1102
|
+
try {
|
|
1103
|
+
await stamp(sealed.id, { arm: 'complete' }, sealed.version + 99);
|
|
1104
|
+
}
|
|
1105
|
+
catch (err) {
|
|
1106
|
+
code = err.code;
|
|
1107
|
+
}
|
|
1108
|
+
if (code !== 'STATE_SNAPSHOT_MISMATCH') {
|
|
1109
|
+
throw new Error(`expected STATE_SNAPSHOT_MISMATCH on a version move, got '${String(code)}' — the ` +
|
|
1110
|
+
`sweep classified a record that has since changed, and must not write over it`);
|
|
1111
|
+
}
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
law: 'STAMP_IDEMPOTENT',
|
|
1116
|
+
name: `[${adapter.storeName}] re-stamping leaves the record byte-identical`,
|
|
1117
|
+
run: async () => {
|
|
1118
|
+
const sealed = await stampedTerminal('idem');
|
|
1119
|
+
const before = JSON.stringify(await adapter.store.get(sealed.id));
|
|
1120
|
+
await stamp(sealed.id, { arm: 'complete' }, sealed.version);
|
|
1121
|
+
await stamp(sealed.id, { arm: 'complete' }, sealed.version);
|
|
1122
|
+
const after = JSON.stringify(await adapter.store.get(sealed.id));
|
|
1123
|
+
if (after !== before) {
|
|
1124
|
+
throw new Error(`re-stamping changed the record:\n before ${before}\n after ${after}`);
|
|
1125
|
+
}
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
];
|
|
1129
|
+
}
|
|
497
1130
|
function csPurityCases(_adapter) {
|
|
498
1131
|
return [
|
|
499
1132
|
{
|
|
@@ -1003,6 +1636,10 @@ function refusalSweepCases(adapter) {
|
|
|
1003
1636
|
const terminal = await adapter.store.update({
|
|
1004
1637
|
...run,
|
|
1005
1638
|
terminal_state: true,
|
|
1639
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1640
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1641
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1642
|
+
sealed_by: { arm: 'complete' },
|
|
1006
1643
|
terminal_reason: 'tck',
|
|
1007
1644
|
});
|
|
1008
1645
|
await expectRefusalUnchanged(run.id, { kind: 'settle_step', step: 'b', outcome: 'complete', claimToken: 'x', evidence: [] }, def, 'run_terminal', terminal.version, 'run_terminal');
|
|
@@ -1071,6 +1708,10 @@ function refusalSweepCases(adapter) {
|
|
|
1071
1708
|
const terminal = await adapter.store.update({
|
|
1072
1709
|
...run,
|
|
1073
1710
|
terminal_state: true,
|
|
1711
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1712
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1713
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1714
|
+
sealed_by: { arm: 'complete' },
|
|
1074
1715
|
terminal_reason: 'tck',
|
|
1075
1716
|
});
|
|
1076
1717
|
await expectRefusalUnchanged(run.id, {
|
|
@@ -1094,6 +1735,10 @@ function refusalSweepCases(adapter) {
|
|
|
1094
1735
|
const seeded = await adapter.store.update({
|
|
1095
1736
|
...run,
|
|
1096
1737
|
terminal_state: true,
|
|
1738
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1739
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1740
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1741
|
+
sealed_by: { arm: 'complete' },
|
|
1097
1742
|
terminal_reason: 'tck',
|
|
1098
1743
|
finalizer_ledger: { fin: { status: 'completed', rank: 0 } },
|
|
1099
1744
|
});
|
|
@@ -1113,6 +1758,10 @@ function refusalSweepCases(adapter) {
|
|
|
1113
1758
|
await adapter.store.update({
|
|
1114
1759
|
...run,
|
|
1115
1760
|
terminal_state: true,
|
|
1761
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1762
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1763
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1764
|
+
sealed_by: { arm: 'complete' },
|
|
1116
1765
|
terminal_reason: 'tck',
|
|
1117
1766
|
finalizer_ledger: { fin: { status: 'pending', rank: 0 } },
|
|
1118
1767
|
});
|
|
@@ -1135,6 +1784,10 @@ function refusalSweepCases(adapter) {
|
|
|
1135
1784
|
const seeded = await adapter.store.update({
|
|
1136
1785
|
...run,
|
|
1137
1786
|
terminal_state: true,
|
|
1787
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1788
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1789
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1790
|
+
sealed_by: { arm: 'complete' },
|
|
1138
1791
|
terminal_reason: 'tck',
|
|
1139
1792
|
finalizer_ledger: {
|
|
1140
1793
|
first: { status: 'pending', rank: 0 },
|
|
@@ -1162,6 +1815,10 @@ function refusalSweepCases(adapter) {
|
|
|
1162
1815
|
await adapter.store.update({
|
|
1163
1816
|
...run,
|
|
1164
1817
|
terminal_state: true,
|
|
1818
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1819
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1820
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1821
|
+
sealed_by: { arm: 'complete' },
|
|
1165
1822
|
terminal_reason: 'tck',
|
|
1166
1823
|
finalizer_ledger: { fin: { status: 'pending', rank: 0 } },
|
|
1167
1824
|
});
|
|
@@ -1184,6 +1841,10 @@ function refusalSweepCases(adapter) {
|
|
|
1184
1841
|
const terminal = await adapter.store.update({
|
|
1185
1842
|
...run,
|
|
1186
1843
|
terminal_state: true,
|
|
1844
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1845
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1846
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1847
|
+
sealed_by: { arm: 'complete' },
|
|
1187
1848
|
terminal_reason: 'tck',
|
|
1188
1849
|
});
|
|
1189
1850
|
await expectRefusalUnchanged(run.id, {
|
|
@@ -1208,6 +1869,10 @@ function refusalSweepCases(adapter) {
|
|
|
1208
1869
|
await adapter.store.update({
|
|
1209
1870
|
...run,
|
|
1210
1871
|
terminal_state: true,
|
|
1872
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1873
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1874
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1875
|
+
sealed_by: { arm: 'complete' },
|
|
1211
1876
|
terminal_reason: 'tck',
|
|
1212
1877
|
finalizer_ledger: { fin: { status: 'pending', rank: 0 } },
|
|
1213
1878
|
});
|
|
@@ -1239,6 +1904,10 @@ function refusalSweepCases(adapter) {
|
|
|
1239
1904
|
const seeded = await adapter.store.update({
|
|
1240
1905
|
...run,
|
|
1241
1906
|
terminal_state: true,
|
|
1907
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1908
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1909
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1910
|
+
sealed_by: { arm: 'complete' },
|
|
1242
1911
|
terminal_reason: 'tck',
|
|
1243
1912
|
finalizer_ledger: { fin: { status: 'voided', rank: 0 } },
|
|
1244
1913
|
});
|
|
@@ -1264,6 +1933,10 @@ function refusalSweepCases(adapter) {
|
|
|
1264
1933
|
await adapter.store.update({
|
|
1265
1934
|
...run,
|
|
1266
1935
|
terminal_state: true,
|
|
1936
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
1937
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
1938
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
1939
|
+
sealed_by: { arm: 'complete' },
|
|
1267
1940
|
terminal_reason: 'tck',
|
|
1268
1941
|
finalizer_ledger: { fin: { status: 'pending', rank: 0 } },
|
|
1269
1942
|
});
|
|
@@ -1373,8 +2046,13 @@ function mintFreshCases(adapter) {
|
|
|
1373
2046
|
// dropped, and the STILL-PENDING... there are none pending now (onFail was already
|
|
1374
2047
|
// marked) — this fixture specifically exercises re-mint on a run with NO pending entries
|
|
1375
2048
|
// left post-void, proving mintFresh re-arms fresh regardless of prior history.
|
|
2049
|
+
// issue #367: a real `applyResume` STRIPS `sealed_by` in the same write that flips the run
|
|
2050
|
+
// live — a live run carrying a seal is an orphan, and the store boundary refuses it. This
|
|
2051
|
+
// hand-authored fixture mirrors that, or it would stop mirroring the function it stands in
|
|
2052
|
+
// for.
|
|
2053
|
+
const { sealed_by: _voidSeal, ...markedBase } = marked.run;
|
|
1376
2054
|
const postResumeVoid = await adapter.store.update({
|
|
1377
|
-
...
|
|
2055
|
+
...markedBase,
|
|
1378
2056
|
terminal_state: false,
|
|
1379
2057
|
in_progress_steps: [],
|
|
1380
2058
|
failed_steps: [],
|
|
@@ -2000,6 +2678,10 @@ function gateMismatchCases(adapter) {
|
|
|
2000
2678
|
...run,
|
|
2001
2679
|
completed_steps: ['a'],
|
|
2002
2680
|
terminal_state: true,
|
|
2681
|
+
// issue #367: a terminal record names its arm. These fixtures mean "this run is over";
|
|
2682
|
+
// their opaque `tck` prose is deliberately unclassifiable, so the boundary's coherence
|
|
2683
|
+
// check abstains and the fixture stays a pure terminal-precondition seeder.
|
|
2684
|
+
sealed_by: { arm: 'complete' },
|
|
2003
2685
|
terminal_reason: 'Workflow completed.',
|
|
2004
2686
|
pending_gate: {
|
|
2005
2687
|
gate_id: gateId,
|
|
@@ -2672,8 +3354,11 @@ function cwfsSecondEpochCases(adapter) {
|
|
|
2672
3354
|
// 'onFail' — a FINALIZER, unresumable — stays in failed_steps (the M1 scenario's own
|
|
2673
3355
|
// premise). terminal_state:false / in_progress_steps:[] / settled:{} mirror the
|
|
2674
3356
|
// mintFreshCases precedent's own void shape.
|
|
3357
|
+
// issue #367: same as the mintFreshCases precedent — the seal fact leaves in the write
|
|
3358
|
+
// that flips the run live again.
|
|
3359
|
+
const { sealed_by: _voidSeal2, ...markedFailedBase } = markedFailed.run;
|
|
2675
3360
|
const postResumeVoid = await adapter.store.update({
|
|
2676
|
-
...
|
|
3361
|
+
...markedFailedBase,
|
|
2677
3362
|
terminal_state: false,
|
|
2678
3363
|
in_progress_steps: [],
|
|
2679
3364
|
failed_steps: ['onFail'],
|
|
@@ -3227,6 +3912,26 @@ function expireDefaultResolveCases(adapter) {
|
|
|
3227
3912
|
*/
|
|
3228
3913
|
export function settlementContract(adapter) {
|
|
3229
3914
|
if (adapter.store.settleStep === undefined) {
|
|
3915
|
+
// A store that declares NEITHER verb genuinely has nothing here to conform to — the
|
|
3916
|
+
// established optional-capability idiom, and a real vacuous pass.
|
|
3917
|
+
//
|
|
3918
|
+
// But a store declaring `stampSeal` WITHOUT `settleStep` would also have received zero cases,
|
|
3919
|
+
// so its stamp and seal-integrity conformance — every law in this file that binds it — would
|
|
3920
|
+
// report green while never running. That is a wiring gap wearing a vacuous pass's clothes.
|
|
3921
|
+
if (adapter.store.stampSeal !== undefined) {
|
|
3922
|
+
return [
|
|
3923
|
+
{
|
|
3924
|
+
law: 'ADAPTER_WIRING',
|
|
3925
|
+
name: `[${adapter.storeName}] declares RunStore.stampSeal but NOT settleStep — every law in this suite would pass without running`,
|
|
3926
|
+
run: async () => {
|
|
3927
|
+
throw new Error(`[${adapter.storeName}] settlementContract: the store declares stampSeal but not ` +
|
|
3928
|
+
'settleStep, and this suite is gated on settleStep — so it would return zero ' +
|
|
3929
|
+
'cases and report green having tested nothing. Declare settleStep, or run the ' +
|
|
3930
|
+
'stamp laws through a suite that binds them on their own.');
|
|
3931
|
+
},
|
|
3932
|
+
},
|
|
3933
|
+
];
|
|
3934
|
+
}
|
|
3230
3935
|
return [];
|
|
3231
3936
|
}
|
|
3232
3937
|
if (adapter.settlementFixture === undefined) {
|
|
@@ -3251,6 +3956,8 @@ export function settlementContract(adapter) {
|
|
|
3251
3956
|
...drainMarkDedupCases(adapter),
|
|
3252
3957
|
...terminalRefusalCases(adapter),
|
|
3253
3958
|
...terminalStateOnlyCases(adapter),
|
|
3959
|
+
...sealIntegrityCases(adapter),
|
|
3960
|
+
...stampSealCases(adapter),
|
|
3254
3961
|
...csPurityCases(adapter),
|
|
3255
3962
|
...neverDowngradeCases(adapter),
|
|
3256
3963
|
...settleOutcomeIntegrityCases(adapter),
|