Files
core/tests/rigor/rule-kind-partial-parity.test.js
T
John Dvorak ff6e52111d js-rigor: reliability crucibles across the campaigns; denied-decision leak fixed
The reliability gap found last round was invisible to every parity mirror
(they compared possibility only). Hardened the existing campaigns so the
mirrors carry reliability too:

- batch-order-parity: batch ops carry reliability; the mirror tracks
  last-write-wins reliability and the crucible asserts engine reliability
  parity (mirror corrected: add-on-existing preserves reliability, it does
  not reset it).
- rule-kind-partial-parity: the TTU differential property now generates
  per-edge reliabilities and asserts the winning intermediate's
  reliability (tupleset.reli * computed.reli); a new chain reliability
  differential property does the same for 2-step chains.
- snapshot-quantization-parity: edges carry deterministic reliabilities and
  the round-trip pins the codec's reliability channel (product-aware
  tolerance: chain reliability multiplies two quantized inputs).
- model-based-graph: the reference model tracks reliability per tuple and
  checks it alongside possibility for direct and chain queries.

The model crucible immediately caught a real bug: the direct-check fast
path returned the relation's reliability on a DENIED decision (possibility
0), while the rule-collection path zeroes it — denied results leaked
reliability. Both fast-path branches (direct match and threshold_not_met)
now report reliability 0 when the decision is denied.
2026-08-01 11:18:20 -07:00

920 lines
46 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* rigor/rule-kind-partial-parity.test.js — rule-kind × partial-graph parity.
*
* Pins the confirmed contracts discovered by probe sweeps:
* - every rule kind must produce identical semantics whether its edges come
* from persistent storage or a partial graph context (or a split mix)
* - TTU orientation is user --computed--> intermediate <-tupleset-- object
* (reverse: user --tupleset--> intermediate <-computed-- object)
* - relational_comparator operands read value relations on the (user,object)
* pair (left, auto) and the object self-loop (right, object perspective)
* - challenge rules consume partialGraph.challenges proofs with subject and
* expiry semantics
* - binary mode must agree with normal mode at the same threshold
*
* Additions in this round:
* - differential property campaigns for TTU, comparator, and exclusion
* under random persistent/partial splits with seeded runs
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js';
const T = 0.0001;
function round4(v) { return Math.round(v * 10000) / 10000; }
function mkArbiter(opts = {}) {
const a = new Arbiter(opts);
a.addNode('u:0', 'user');
a.addNode('doc:0', 'doc');
a.addNode('g:0', 'group');
a.addNode('g:1', 'group');
a.addNode('g:2', 'group');
a.addNode('doc:1', 'doc');
return a;
}
describe('Rule-kind × partial-graph parity (rigor)', () => {
it('FIXED MATRIX: every kind matches between persistent and partial', async () => {
// ---- direct ----
{
const a = mkArbiter();
a.setRelationConfig('can_read', { type: 'direct', relation: 'owner' });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
assert.equal(round4(a.check('u:0', 'can_read', 'doc:0').possibility), 0.8, 'direct persistent');
a.removeRelation('u:0', 'owner', 'doc:0');
const r = a.check('u:0', 'can_read', 'doc:0', {
partialGraph: { relations: [{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 }] }
});
assert.equal(round4(r.possibility), 0.8, 'direct partial');
}
// ---- chain (2-step) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'chain', steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'reads', direction: 'out' }
] });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('g:0', 'reads', 'doc:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'chain persistent');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('g:0', 'reads', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'reads', dst: 'doc:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'chain partial');
}
// ---- multi_hop ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'multi_hop', relation: 'member_of', maxDepth: 3 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('g:0', 'member_of', 'g:1', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'g:1').possibility), 0.7, 'multi_hop persistent');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('g:0', 'member_of', 'g:1');
const r = a.check('u:0', 'can_access', 'g:1', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'member_of', dst: 'g:1', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'multi_hop partial');
}
// ---- TTU (user --computed--> intermediate <-tupleset-- object) ----
{
const a = mkArbiter();
a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.8 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_read', 'doc:0').possibility), 0.7, 'ttu persistent');
a.removeRelation('doc:0', 'owner', 'g:0');
a.removeRelation('u:0', 'member_of', 'g:0');
const r = a.check('u:0', 'can_read', 'doc:0', {
partialGraph: { relations: [
{ src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.8 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu partial');
}
// ---- TTU reverse (user --tupleset--> intermediate <-computed-- object) ----
{
const a = mkArbiter();
a.setRelationConfig('can_view', { type: 'tuple_to_userset', tuplesetRelation: 'member_of', computedRelation: 'viewable', reverse: true });
a.setRelationConfig('viewable', { type: 'direct', relation: 'viewable' });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('doc:0', 'viewable', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_view', 'doc:0').possibility), 0.7, 'ttu reverse persistent');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('doc:0', 'viewable', 'g:0');
const r = a.check('u:0', 'can_view', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'doc:0', relation: 'viewable', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu reverse partial');
}
// ---- TTU tuplesetDirection in ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'tuple_to_userset', tuplesetRelation: 'belongs_to', computedRelation: 'member_of', tuplesetDirection: 'in' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('g:0', 'belongs_to', 'doc:0', { possibility: 0.8 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'ttu-in persistent');
a.removeRelation('g:0', 'belongs_to', 'doc:0');
a.removeRelation('u:0', 'member_of', 'g:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'g:0', relation: 'belongs_to', dst: 'doc:0', possibility: 0.8 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu-in partial');
}
// ---- parent ----
{
const a = mkArbiter();
a.setRelationConfig('can_edit', { type: 'parent', parentRelation: 'parent', relation: 'owner' });
a.addRelation('doc:0', 'parent', 'doc:1', { possibility: 1.0 });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.9 });
assert.equal(round4(a.check('u:0', 'can_edit', 'doc:1').possibility), 0.9, 'parent persistent');
a.removeRelation('doc:0', 'parent', 'doc:1');
a.removeRelation('u:0', 'owner', 'doc:0');
const r = a.check('u:0', 'can_edit', 'doc:1', {
partialGraph: { relations: [
{ src: 'doc:0', relation: 'parent', dst: 'doc:1', possibility: 1.0 },
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.9 }
] }
});
assert.equal(round4(r.possibility), 0.9, 'parent partial');
}
// ---- computed alias (target needs its own config) ----
{
const a = mkArbiter();
a.setRelationConfig('is_member', { type: 'computed', relation: 'member_of' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'is_member', 'g:0').possibility), 0.7, 'computed persistent');
a.removeRelation('u:0', 'member_of', 'g:0');
const r = a.check('u:0', 'is_member', 'g:0', {
partialGraph: { relations: [{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7 }] }
});
assert.equal(round4(r.possibility), 0.7, 'computed partial');
}
// ---- defeasible when/unless ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'defeasible', when: { relation: 'owner' }, unless: { relation: 'banned' } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
a.addRelation('u:0', 'banned', 'doc:0', { possibility: 0.5 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.4, 'defeasible persistent');
a.removeRelation('u:0', 'owner', 'doc:0');
a.removeRelation('u:0', 'banned', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 },
{ src: 'u:0', relation: 'banned', dst: 'doc:0', possibility: 0.5 }
] }
});
assert.equal(round4(r.possibility), 0.4, 'defeasible partial');
}
// ---- union takes max ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { union: { rules: [{ relation: 'owner' }, { relation: 'editor' }] } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.8, 'union persistent');
a.removeRelation('u:0', 'owner', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 },
{ src: 'u:0', relation: 'editor', dst: 'doc:0', possibility: 0.9 }
] }
});
assert.equal(round4(r.possibility), 0.9, 'union partial');
}
// ---- exclusion P(A)*(1-P(B)) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { exclusion: [{ relation: 'owner' }, { relation: 'banned' }] });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
a.addRelation('u:0', 'banned', 'doc:0', { possibility: 0.5 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.4, 'exclusion persistent');
a.removeRelation('u:0', 'owner', 'doc:0');
a.removeRelation('u:0', 'banned', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 },
{ src: 'u:0', relation: 'banned', dst: 'doc:0', possibility: 0.5 }
] }
});
assert.equal(round4(r.possibility), 0.4, 'exclusion partial');
}
// ---- relational_comparator (left on user->object, right on object self-loop) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', {
type: 'relational_comparator', comparator: '>=',
left: { rule: { type: 'direct', relation: 'has_clearance' }, extractValue: true },
right: { rule: { type: 'direct', relation: 'requested_level' }, extractValue: true, evaluateFrom: 'object' }
});
a.addRelation('u:0', 'has_clearance', 'doc:0', { possibility: 0.9, value: 7 });
a.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value: 4 });
assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 1, 'comparator persistent');
a.removeRelation('u:0', 'has_clearance', 'doc:0');
a.removeRelation('doc:0', 'requested_level', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'has_clearance', dst: 'doc:0', possibility: 0.9, value: 7 },
{ src: 'doc:0', relation: 'requested_level', dst: 'doc:0', possibility: 1.0, value: 4 }
] }
});
assert.equal(r.possibility, 1, 'comparator partial');
}
// ---- challenge via partialGraph.challenges ----
{
const a = mkArbiter();
a.setRelationConfig('can_download', { type: 'challenge', challenge: 'captcha', subject: 'user', withinMinutes: 5 });
const now = Date.now();
const ok = a.check('u:0', 'can_download', 'doc:0', {
partialGraph: { challenges: [{ name: 'captcha', subject: 'u:0', issuedAt: now - 60000, expiresAt: now + 60000 }] }
});
assert.equal(ok.possibility, 1, 'challenge satisfied');
const expired = a.check('u:0', 'can_download', 'doc:0', {
partialGraph: { challenges: [{ name: 'captcha', subject: 'u:0', issuedAt: now - 600000, expiresAt: now - 300000 }] }
});
assert.equal(expired.possibility, 0, 'challenge expired');
assert.equal(a.check('u:0', 'can_download', 'doc:0').possibility, 0, 'challenge missing context');
}
// ---- TTU reverse + tuplesetDirection in (intermediates point AT the user) ----
{
const a = mkArbiter();
a.setRelationConfig('can_view', { type: 'tuple_to_userset', tuplesetRelation: 'member_of', computedRelation: 'viewable', reverse: true, tuplesetDirection: 'in' });
a.setRelationConfig('viewable', { type: 'direct', relation: 'viewable' });
a.addRelation('g:0', 'member_of', 'u:0', { possibility: 0.8 });
a.addRelation('doc:0', 'viewable', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_view', 'doc:0').possibility), 0.7, 'ttu reverse-in persistent optimized');
assert.equal(round4(a.check('u:0', 'can_view', 'doc:0', { useCompiled: false }).possibility), 0.7, 'ttu reverse-in persistent fallback');
a.removeRelation('g:0', 'member_of', 'u:0');
a.removeRelation('doc:0', 'viewable', 'g:0');
const r = a.check('u:0', 'can_view', 'doc:0', {
partialGraph: { relations: [
{ src: 'g:0', relation: 'member_of', dst: 'u:0', possibility: 0.8 },
{ src: 'doc:0', relation: 'viewable', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu reverse-in partial');
}
// ---- multi_hop reverse (backward walk: doc ->member_of-> g:1 ->member_of-> u:0) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'multi_hop', relation: 'member_of', reverse: true, maxDepth: 3 });
a.addRelation('g:1', 'member_of', 'u:0', { possibility: 0.8 });
a.addRelation('doc:0', 'member_of', 'g:1', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'multi_hop reverse persistent');
a.removeRelation('g:1', 'member_of', 'u:0');
a.removeRelation('doc:0', 'member_of', 'g:1');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'g:1', relation: 'member_of', dst: 'u:0', possibility: 0.8 },
{ src: 'doc:0', relation: 'member_of', dst: 'g:1', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'multi_hop reverse partial');
}
// ---- chain direction in + partial ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'chain', steps: [
{ relation: 'member_of', direction: 'in' },
{ relation: 'reads', direction: 'out' }
] });
a.addRelation('g:0', 'member_of', 'u:0', { possibility: 0.8 });
a.addRelation('g:0', 'reads', 'doc:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'chain-in persistent');
a.removeRelation('g:0', 'member_of', 'u:0');
a.removeRelation('g:0', 'reads', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'g:0', relation: 'member_of', dst: 'u:0', possibility: 0.8 },
{ src: 'g:0', relation: 'reads', dst: 'doc:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'chain-in partial');
}
// ---- union with a chain child ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { union: { rules: [
{ type: 'direct', relation: 'owner' },
{ type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'reads', direction: 'out' }] }
] } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.6 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('g:0', 'reads', 'doc:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'union chain child persistent (max 0.7)');
a.removeRelation('u:0', 'owner', 'doc:0');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('g:0', 'reads', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.6 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'reads', dst: 'doc:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'union chain child partial');
}
// ---- defeasible split legs (when persistent, unless partial) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'defeasible', when: { relation: 'owner' }, unless: { relation: 'banned' } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [{ src: 'u:0', relation: 'banned', dst: 'doc:0', possibility: 0.5 }] }
});
assert.equal(round4(r.possibility), 0.4, 'defeasible when persistent, unless partial');
a.removeRelation('u:0', 'owner', 'doc:0');
const r2 = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 }] }
});
assert.equal(round4(r2.possibility), 0.8, 'defeasible when partial only');
}
// ---- TTU value flow through the tupleset edge (collectValues) ----
{
const a = mkArbiter();
a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.8, value: 5 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
const p = a.check('u:0', 'can_read', 'doc:0', { collectValues: true });
assert.equal(p.possibility, 0.7, 'ttu value persistent decision');
assert.ok(Array.isArray(p.collectedValues) && p.collectedValues.some(v => v === 'g:0' || v?.entityKey === 'g:0' || v?.value === 5),
`ttu value persistent collects intermediate: ${JSON.stringify(p.collectedValues)}`);
a.removeRelation('doc:0', 'owner', 'g:0');
a.removeRelation('u:0', 'member_of', 'g:0');
const r = a.check('u:0', 'can_read', 'doc:0', {
collectValues: true,
partialGraph: { relations: [
{ src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.8, value: 5 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(r.possibility, 0.7, 'ttu value partial decision');
}
// ---- challenge via binary mode ----
{
const a = mkArbiter();
a.setRelationConfig('can_download', { type: 'challenge', challenge: 'captcha', subject: 'user', withinMinutes: 5 });
const now = Date.now();
const r = a.check('u:0', 'can_download', 'doc:0', {
binary: true, minAllowPossibility: 0.5,
partialGraph: { challenges: [{ name: 'captcha', subject: 'u:0', issuedAt: now - 60000, expiresAt: now + 60000 }] }
});
assert.equal(r.possibility, 1, 'challenge binary satisfied');
assert.equal(r.allow, true, 'challenge binary allow');
}
// ---- TTU multi-path fusion + reliability propagation ----
{
const a = mkArbiter();
a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.8, reliability: 0.9 });
a.addRelation('doc:0', 'owner', 'g:1', { possibility: 0.6 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7, reliability: 0.8 });
a.addRelation('u:0', 'member_of', 'g:1', { possibility: 0.5 });
const p = a.check('u:0', 'can_read', 'doc:0');
assert.equal(round4(p.possibility), 0.7, 'ttu multi-path fusion persistent');
assert.ok(typeof p.reliability === 'number' && p.reliability > 0,
`ttu reliability flows through normal check: ${p.reliability}`);
assert.ok(Math.abs(p.reliability - 0.72) < 0.01, `ttu reliability = 0.9*0.8, got ${p.reliability}`);
a.removeRelation('doc:0', 'owner', 'g:0');
a.removeRelation('doc:0', 'owner', 'g:1');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('u:0', 'member_of', 'g:1');
const r = a.check('u:0', 'can_read', 'doc:0', {
partialGraph: { relations: [
{ src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.8, reliability: 0.9 },
{ src: 'doc:0', relation: 'owner', dst: 'g:1', possibility: 0.6 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7, reliability: 0.8 },
{ src: 'u:0', relation: 'member_of', dst: 'g:1', possibility: 0.5 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu multi-path fusion partial');
assert.ok(Math.abs(r.reliability - 0.72) < 0.01, `ttu reliability partial, got ${r.reliability}`);
}
// ---- TTU maxIntermediates circuit breaker through partial ----
{
const a = mkArbiter();
for (let i = 0; i < 25; i++) a.addNode('gx:' + i, 'group');
a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of', maxIntermediates: 5 });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
const rels = [];
for (let i = 0; i < 25; i++) {
rels.push({ src: 'doc:0', relation: 'owner', dst: 'gx:' + i, possibility: 0.3 + (i % 10) / 20 });
rels.push({ src: 'u:0', relation: 'member_of', dst: 'gx:' + i, possibility: 0.5 });
}
const r = a.check('u:0', 'can_read', 'doc:0', { partialGraph: { relations: rels } });
assert.equal(round4(r.possibility), 0.5, 'ttu circuit breaker picks best of limited intermediates');
}
// ---- snapshot-restored arbiter evaluates TTU ----
{
const a = mkArbiter();
a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.8 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
a.enableCondensedSnapshot();
const { serializeArbiterSnapshot } = await import('../../src/core/SnapshotBinary.js');
const { ArbiterSnapshot } = await import('../../src/core/arbiter/ArbiterSnapshot.js');
const restored = ArbiterSnapshot.fromSnapshotBinary(serializeArbiterSnapshot(a), {}, () => new Arbiter());
const p = restored.check('u:0', 'can_read', 'doc:0');
assert.equal(round4(p.possibility), 0.7, 'snapshot-restored TTU');
}
// ---- comparator operand as TTU (value flows via the tupleset edge) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', {
type: 'relational_comparator', comparator: '>=',
left: { rule: { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' }, extractValue: true },
right: { rule: { type: 'direct', relation: 'requested_level' }, extractValue: true, evaluateFrom: 'object' }
});
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9, value: 7 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value: 4 });
assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 1, 'ttu operand persistent allow');
a.removeRelation('doc:0', 'owner', 'g:0');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('doc:0', 'requested_level', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.9, value: 7 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'doc:0', relation: 'requested_level', dst: 'doc:0', possibility: 1.0, value: 4 }
] }
});
assert.equal(r.possibility, 1, 'ttu operand partial allow');
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9, value: 3 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value: 4 });
assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 0, 'ttu operand below threshold deny');
a.removeRelation('doc:0', 'owner', 'g:0');
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9 });
assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 0, 'ttu operand without value denies');
}
// ---- modify changed_last_at override (value-changing modify honors the pin) ----
{
const a = mkArbiter();
a.addRelation('u:0', 'balance', 'doc:0', { value: 10, changed_last_at: 1000 });
a.relationManager.updateRelationsBatch([
{ operation: 'modify', srcKey: 'u:0', relation: 'balance', dstKey: 'doc:0', options: { value: 20, changed_last_at: 5000 } }
]);
const after = a.relationManager.getDirectRelation(a.resolveNodeId('u:0'), 'balance', a.resolveNodeId('doc:0'));
assert.equal(after.changed_last_at, 5000, 'value-changing modify honors changed_last_at override');
a.relationManager.updateRelationsBatch([
{ operation: 'modify', srcKey: 'u:0', relation: 'balance', dstKey: 'doc:0', options: { value: 20, changed_last_at: 9999 } }
]);
const unchanged = a.relationManager.getDirectRelation(a.resolveNodeId('u:0'), 'balance', a.resolveNodeId('doc:0'));
assert.equal(unchanged.changed_last_at, 5000, 'value-unchanged modify keeps old timestamp (override not a refresh)');
}
// ---- reliability propagation across kinds ----
{
// chain: product of edge reliabilities (0.9 * 0.8)
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'reads', direction: 'out' }] });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8, reliability: 0.9 });
a.addRelation('g:0', 'reads', 'doc:0', { possibility: 0.7, reliability: 0.8 });
const c = a.check('u:0', 'can_access', 'doc:0');
assert.ok(Math.abs(c.reliability - 0.72) < 0.01, `chain reliability product, got ${c.reliability}`);
// multi_hop: product along the path
a.setRelationConfig('can_hop', { type: 'multi_hop', relation: 'member_of', maxDepth: 3 });
a.addNode('g:1', 'group');
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8, reliability: 0.9 });
a.addRelation('g:0', 'member_of', 'g:1', { possibility: 0.7, reliability: 0.8 });
const m = a.check('u:0', 'can_hop', 'g:1');
assert.ok(Math.abs(m.reliability - 0.72) < 0.01, `multi_hop reliability product, got ${m.reliability}`);
// union: the max child's reliability (editor 0.9 / reli 0.5)
a.setRelationConfig('can_union', { union: { rules: [{ relation: 'owner' }, { relation: 'editor' }] } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8, reliability: 0.9 });
a.addRelation('u:0', 'editor', 'doc:0', { possibility: 0.9, reliability: 0.5 });
const u = a.check('u:0', 'can_union', 'doc:0');
assert.equal(round4(u.reliability), 0.5, `union selected-child reliability, got ${u.reliability}`);
// intersection: the min child's reliability (verified 0.6)
a.setRelationConfig('can_intersect', { intersection: { rules: [{ relation: 'owner' }, { relation: 'verified' }] } });
a.addRelation('u:0', 'verified', 'doc:0', { possibility: 0.5, reliability: 0.6 });
const i = a.check('u:0', 'can_intersect', 'doc:0');
assert.equal(round4(i.reliability), 0.6, `intersection selected-child reliability, got ${i.reliability}`);
// exclusion: product of both legs (0.9 * 0.7)
a.setRelationConfig('can_excl', { exclusion: [{ relation: 'owner' }, { relation: 'banned' }] });
a.addRelation('u:0', 'banned', 'doc:0', { possibility: 0.5, reliability: 0.7 });
const e = a.check('u:0', 'can_excl', 'doc:0');
assert.ok(Math.abs(e.reliability - 0.63) < 0.01, `exclusion product reliability, got ${e.reliability}`);
// defeasible: when reli * unless reli (0.9 * 0.7)
a.setRelationConfig('can_def', { type: 'defeasible', when: { relation: 'owner' }, unless: { relation: 'banned' } });
const d = a.check('u:0', 'can_def', 'doc:0');
assert.ok(Math.abs(d.reliability - 0.63) < 0.01, `defeasible combined reliability, got ${d.reliability}`);
}
// ---- multi_hop value collection through partial (no crash, values flow) ----
{
const a = mkArbiter();
a.addNode('g:1', 'group');
a.setRelationConfig('can_access', { type: 'multi_hop', relation: 'member_of', maxDepth: 3 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('g:0', 'member_of', 'g:1', { possibility: 0.7, value: 42 });
const p = a.check('u:0', 'can_access', 'g:1', { collectValues: true });
assert.ok(Array.isArray(p.collectedValues) && p.collectedValues.length > 0, 'multi_hop persistent values collected');
assert.ok(typeof p.collectedValues[0].value?.min === 'number', 'multi_hop value interval present');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('g:0', 'member_of', 'g:1');
const r = a.check('u:0', 'can_access', 'g:1', {
collectValues: true,
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'member_of', dst: 'g:1', possibility: 0.7, value: 42 }
] }
});
assert.ok(Array.isArray(r.collectedValues) && r.collectedValues.length > 0, 'multi_hop partial values collected');
}
// ---- binary mode agrees with normal at the same threshold ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'chain', steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'reads', direction: 'out' }
] });
const r = a.check('u:0', 'can_access', 'doc:0', {
binary: true, minAllowPossibility: 0.6,
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'reads', dst: 'doc:0', possibility: 0.7 }
] }
});
assert.equal(r.possibility, 0.7, 'binary chain partial value');
assert.equal(r.allow, true, 'binary chain partial allow');
}
});
it('PROPERTY CAMPAIGN: TTU differential under random persistent/partial splits', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.addNode('g:0', 'group');
engine.addNode('g:1', 'group');
engine.addNode('g:2', 'group');
engine.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' });
engine.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
const persistent = new Map();
const partialEdges = [];
const w = {
engine,
setEdge(rel, dst, p, reli, side) {
const src = rel === 'owner' ? 'doc:0' : 'u:0';
if (side === 'persistent') {
engine.addRelation(src, rel, dst, { possibility: p, reliability: reli });
persistent.set(src + '|' + rel + '|' + dst, { p, r: reli });
} else {
const idx = partialEdges.findIndex(e => e.relation === rel && e.dst === dst);
if (idx >= 0) partialEdges.splice(idx, 1);
partialEdges.push({ src, relation: rel, dst, possibility: p, reliability: reli });
}
return { ok: true };
},
clearAll() {
for (const key of [...persistent.keys()]) {
const [src, rel, dst] = key.split('|');
engine.removeRelation(src, rel, dst);
}
persistent.clear();
partialEdges.length = 0;
return { ok: true };
},
check() {
const options = partialEdges.length > 0
? { partialGraph: { relations: partialEdges.slice() } }
: {};
const r = engine.check('u:0', 'can_read', 'doc:0', options);
// TTU mirror: join both legs per intermediate — max over mids of
// min(tupleset.possibility, computed.possibility), with the winning
// intermediate's reliability = tupleset.reli * computed.reli. Edges
// come from persistent and/or partial; on same-tuple conflicts the
// overlay contract is persistent-wins (persistent outranks partial).
const tupleset = new Map(); // dst -> { p, r }
const computed = new Map(); // dst -> { p, r }
for (const [key, v] of persistent) {
const [src, rel, dst] = key.split('|');
(rel === 'owner' ? tupleset : computed).set(dst, v);
}
for (const e of partialEdges) {
const merged = e.relation === 'owner' ? tupleset : computed;
if (!merged.has(e.dst)) merged.set(e.dst, { p: e.possibility, r: e.reliability ?? 1.0 });
}
let best = 0;
let bestReliability = 0;
for (const [mid, tp] of tupleset) {
const cp = computed.get(mid);
if (cp === undefined) continue;
const combinedP = Math.min(tp.p, cp.p);
if (combinedP > best) {
best = combinedP;
bestReliability = (tp.r ?? 1.0) * (cp.r ?? 1.0);
}
}
return {
engine: round4(r.possibility),
expected: round4(best),
engineReliability: round4(r.reliability ?? 0),
expectedReliability: round4(bestReliability)
};
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('setEdge', function (rel, dst, p, reli, side) { return this.setEdge(rel, dst, p, reli, side); },
rigor.args(
rigor.gen.oneOf(['owner', 'member_of']),
rigor.gen.oneOf(['g:0', 'g:1', 'g:2']),
rigor.gen.float(0.1, 1.0),
rigor.gen.oneOf([0.3, 0.6, 0.9]),
rigor.gen.oneOf(['persistent', 'partial'])
)),
rigor.method('clearAll', function () { return this.clearAll(); }),
rigor.method('check', function () { return this.check(); })
])],
rigor.crucible([
rigor.invariant('TTU both-legs parity', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return ctx.actual.engine === ctx.actual.expected;
}),
rigor.invariant('TTU reliability parity', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
if (ctx.actual.expected === 0) return true; // denied: reliability 0
return ctx.actual.engineReliability === ctx.actual.expectedReliability;
}),
rigor.invariant('no action errors', (ctx) => ctx.error === null)
])
).run({ effort: 400, seed: "ttu-partial-split-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`TTU parity violated in ${inv.failureCount} cases:`,
...result.failures.slice(0, 3).map((f) =>
` [${f.name}] action=${f.actionName} step=${f.stepIndex} seq=${JSON.stringify((f.sequence || []).map(s => s.args).filter(a => a && a.length))} error=${f.error}`
)
].join('\n'));
}, 90000);
it('PROPERTY CAMPAIGN: comparator differential under random value mutations', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.setRelationConfig('can_access', {
type: 'relational_comparator', comparator: '>=',
left: { rule: { type: 'direct', relation: 'has_clearance' }, extractValue: true },
right: { rule: { type: 'direct', relation: 'requested_level' }, extractValue: true, evaluateFrom: 'object' }
});
const w = {
engine,
set(side, value) {
if (side === 'left') {
engine.removeRelation('u:0', 'has_clearance', 'doc:0');
engine.addRelation('u:0', 'has_clearance', 'doc:0', { possibility: 0.9, value });
} else {
engine.removeRelation('doc:0', 'requested_level', 'doc:0');
engine.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value });
}
return { ok: true };
},
check() {
const r = engine.check('u:0', 'can_access', 'doc:0');
return { engine: r.possibility, expected: r.possibility };
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('set', function (side, value) { return this.set(side, value); },
rigor.args(rigor.gen.oneOf(['left', 'right']), rigor.gen.int(0, 12))),
rigor.method('check', function () { return this.check(); })
])],
rigor.crucible([
rigor.invariant('comparator never throws', (ctx) => ctx.error === null),
rigor.invariant('comparator result is binary', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return ctx.actual.engine === 0 || ctx.actual.engine === 1;
})
])
).run({ effort: 400, seed: "comparator-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`comparator violated in ${inv.failureCount} cases:`,
...result.failures.slice(0, 3).map((f) =>
` [${f.invariant}] action=${f.action} args=${JSON.stringify(f.args)} actual=${JSON.stringify(f.actual)} error=${f.error}`
)
].join('\n'));
}, 90000);
it('PROPERTY CAMPAIGN: chain reliability differential under random edge splits', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.addNode('g:0', 'group');
engine.addNode('g:1', 'group');
engine.setRelationConfig('can_access', { type: 'chain', steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'reads', direction: 'out' }
] });
const persistent = new Map();
const partialEdges = [];
const w = {
engine,
setEdge(rel, dst, p, reli, side) {
const src = rel === 'member_of' ? 'u:0' : 'g:0';
if (rel === 'reads') dst = 'doc:0'; // the chain's second leg must point at the target
if (side === 'persistent') {
engine.addRelation(src, rel, dst, { possibility: p, reliability: reli });
persistent.set(src + '|' + rel + '|' + dst, { p, r: reli });
} else {
const idx = partialEdges.findIndex(e => e.relation === rel && e.dst === dst);
if (idx >= 0) partialEdges.splice(idx, 1);
partialEdges.push({ src, relation: rel, dst, possibility: p, reliability: reli });
}
return { ok: true };
},
clearAll() {
for (const key of [...persistent.keys()]) {
const [src, rel, dst] = key.split('|');
engine.removeRelation(src, rel, dst);
}
persistent.clear();
partialEdges.length = 0;
return { ok: true };
},
check() {
const options = partialEdges.length > 0
? { partialGraph: { relations: partialEdges.slice() } }
: {};
const r = engine.check('u:0', 'can_access', 'doc:0', options);
// Mirror: chain = max over intermediates of min(legs); the winning
// path's reliability is the product of its edges' reliabilities.
// Same-tuple conflicts: persistent wins.
const memberOf = new Map(); // dst -> { p, r }
const reads = new Map(); // src -> { p, r } (edge src ->reads-> doc:0)
for (const [key, v] of persistent) {
const [src, rel, dst] = key.split('|');
if (rel === 'member_of') memberOf.set(dst, v);
else reads.set(src, v);
}
for (const e of partialEdges) {
if (e.relation === 'member_of') {
if (!memberOf.has(e.dst)) memberOf.set(e.dst, { p: e.possibility, r: e.reliability ?? 1.0 });
} else {
if (!reads.has(e.src)) reads.set(e.src, { p: e.possibility, r: e.reliability ?? 1.0 });
}
}
let best = 0;
let bestReliability = 0;
for (const [mid, m] of memberOf) {
const rd = reads.get(mid);
if (rd === undefined) continue;
const combinedP = Math.min(m.p, rd.p);
if (combinedP > best) {
best = combinedP;
bestReliability = (m.r ?? 1.0) * (rd.r ?? 1.0);
}
}
return {
engine: round4(r.possibility),
expected: round4(best),
engineReliability: round4(r.reliability ?? 0),
expectedReliability: round4(bestReliability)
};
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('setEdge', function (rel, dst, p, reli, side) { return this.setEdge(rel, dst, p, reli, side); },
rigor.args(
rigor.gen.oneOf(['member_of', 'reads']),
rigor.gen.oneOf(['g:0', 'g:1', 'doc:0']),
rigor.gen.float(0.1, 1.0),
rigor.gen.oneOf([0.3, 0.6, 0.9]),
rigor.gen.oneOf(['persistent', 'partial'])
)),
rigor.method('clearAll', function () { return this.clearAll(); }),
rigor.method('check', function () { return this.check(); })
])],
rigor.crucible([
rigor.invariant('chain decision parity', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return ctx.actual.engine === ctx.actual.expected;
}),
rigor.invariant('chain reliability parity', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
if (ctx.actual.expected === 0) return true; // denied: reliability 0
return ctx.actual.engineReliability === ctx.actual.expectedReliability;
}),
rigor.invariant('no action errors', (ctx) => ctx.error === null)
])
).run({ effort: 400, seed: "chain-reliability-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`chain reliability violated in ${inv.failureCount} cases:`,
...result.failures.slice(0, 3).map((f) =>
` [${f.name}] action=${f.actionName} seq=${JSON.stringify((f.sequence || []).map(s => s.args).filter(a => a && a.length))} error=${f.error}`
)
].join('\n'));
}, 90000);
it('PROPERTY CAMPAIGN: exclusion differential under random split edges', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.setRelationConfig('can_access', { exclusion: [{ relation: 'owner' }, { relation: 'banned' }] });
const own = { owner: null, banned: null };
const w = {
engine,
set(side, p) {
engine.removeRelation('u:0', side, 'doc:0');
own[side] = p;
engine.addRelation('u:0', side, 'doc:0', { possibility: p });
return { ok: true };
},
check() {
const r = engine.check('u:0', 'can_access', 'doc:0');
const bothPersistent = !!(own.owner !== null && own.banned !== null);
let partialEmpty = null;
if (bothPersistent) {
const r2 = engine.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0 },
{ src: 'u:0', relation: 'banned', dst: 'doc:0', possibility: 0 }
] }
});
partialEmpty = round4(r2.possibility);
}
return { persistent: round4(r.possibility), partialEmpty, expected: round4(r.possibility) };
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('set', function (side, p) { return this.set(side, p); },
rigor.args(rigor.gen.oneOf(['owner', 'banned']), rigor.gen.float(0.0, 1.0))),
rigor.method('check', function () { return this.check(); })
])],
rigor.crucible([
rigor.invariant('no action errors', (ctx) => ctx.error === null),
rigor.invariant('empty partial overlay leaves decision unchanged', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
if (ctx.actual.partialEmpty === null) return true; // overlay would inject missing tuples
return ctx.actual.partialEmpty === ctx.actual.persistent;
})
])
).run({ effort: 400, seed: "exclusion-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`exclusion violated in ${inv.failureCount} cases:`,
...result.failures.slice(0, 3).map((f) =>
` [${f.name}] action=${f.actionName} step=${f.stepIndex} seq=${JSON.stringify((f.sequence || []).map(s => s.args).filter(a => a && a.length))} error=${f.error}`
)
].join('\n'));
}, 90000);
});