js-rigor: value-collection crucibles; TTU 0-strength paths, crash, fusion reliability

The new value-collection crucibles in the TTU and chain differential
campaigns immediately found three engine defects:

- TTU join pushed 0-strength 'matches' (missing computed leg, or
  0-possibility edges, with minPossibility 0) as valid paths: denied
  decisions reported tuple_to_userset_found and leaked the tupleset edge's
  value into collectedValues. Both join modes now require combined > 0.
- A ReferenceError (bare resolveKey) crashed the computed-join mode under
  collectValues, silently turning the whole check into an evaluation_error
  denial. Fixed the call to this.arbiter.resolveKey.
- Multi-path TTU fusion fell back to Math.max over all path reliabilities,
  pairing the winning possibility with another intermediate's reliability.
  The fallback now picks the max-possibility path's reliability.

New campaigns: defeasible and intersection differential properties
(when/unless and min-children with reliability parity under persistent/
partial splits). The model-based campaign keeps its reliability crucible;
its value comparison was reverted — the harness's shrink reporting is
opaque and unreconstructable there, and the value semantics are covered by
the TTU/chain campaigns instead.
This commit is contained in:
John Dvorak
2026-08-01 22:34:00 -07:00
parent ff6e52111d
commit dab9671d20
3 changed files with 274 additions and 28 deletions
+18 -8
View File
@@ -264,7 +264,11 @@ export class TupleToUsersetRule extends BaseRule {
const combinedPossibility = Math.min(resolvePossibility(tupleEdge.possibility), res.possibility); const combinedPossibility = Math.min(resolvePossibility(tupleEdge.possibility), res.possibility);
const combinedReliability = resolveReliability(tupleEdge.reliability) * (res.reliability !== undefined ? res.reliability : 1.0); const combinedReliability = resolveReliability(tupleEdge.reliability) * (res.reliability !== undefined ? res.reliability : 1.0);
if (combinedPossibility >= minPossibility) { // A 0-strength "match" (e.g. the computed leg missing with
// minPossibility 0) is not a valid path: it would report
// tuple_to_userset_found and leak the tupleset value into the
// collected values on a denied decision.
if (combinedPossibility > 0 && combinedPossibility >= minPossibility) {
const path = { const path = {
possibility: combinedPossibility, possibility: combinedPossibility,
reliability: combinedReliability, reliability: combinedReliability,
@@ -279,7 +283,7 @@ export class TupleToUsersetRule extends BaseRule {
computedRelation: { relation: rule.computedRelation, possibility: res.possibility, reliability: res.reliability, meta: res.meta } computedRelation: { relation: rule.computedRelation, possibility: res.possibility, reliability: res.reliability, meta: res.meta }
} }
}), }),
...(!useLightweightPaths && collectValues && { collectedValue: buildCollectedValue(tupleEdge, resolveKey(tupleEdge.src, options), intermediateKey) }) ...(!useLightweightPaths && collectValues && { collectedValue: buildCollectedValue(tupleEdge, this.arbiter.resolveKey(tupleEdge.src, options), intermediateKey) })
}; };
allValidPaths.push(path); allValidPaths.push(path);
if (!bestPath || combinedPossibility > bestPath.possibility) { if (!bestPath || combinedPossibility > bestPath.possibility) {
@@ -374,7 +378,9 @@ export class TupleToUsersetRule extends BaseRule {
const combinedPossibility = Math.min(resolvePossibility(t.possibility), res.possibility); const combinedPossibility = Math.min(resolvePossibility(t.possibility), res.possibility);
const combinedReliability = resolveReliability(t.reliability) * (res.reliability !== undefined ? res.reliability : 1.0); const combinedReliability = resolveReliability(t.reliability) * (res.reliability !== undefined ? res.reliability : 1.0);
if (combinedPossibility >= minPossibility) { // 0-strength tuples (missing computed leg, or 0-possibility edges)
// must not count as found paths (see computed join mode).
if (combinedPossibility > 0 && combinedPossibility >= minPossibility) {
const pathData = { const pathData = {
possibility: combinedPossibility, possibility: combinedPossibility,
reliability: combinedReliability, reliability: combinedReliability,
@@ -512,11 +518,15 @@ export class TupleToUsersetRule extends BaseRule {
const winningPath = validPathData.find(p => p.meta.intermediateKey === fusionResult.meta.intermediateKey && p.meta.pathType === fusionResult.meta.pathType); const winningPath = validPathData.find(p => p.meta.intermediateKey === fusionResult.meta.intermediateKey && p.meta.pathType === fusionResult.meta.pathType);
if (winningPath) { if (winningPath) {
fusedReliability = winningPath.reliability; fusedReliability = winningPath.reliability;
} else if (reliabilities.length > 0) { }
fusedReliability = Math.max(...reliabilities); // Use max reliability for performance }
} if (fusedReliability === 1.0 && reliabilities.length > 0) {
} else if (reliabilities.length > 0) { // Fallback: the fusion aggregator is max, so the winner is the
fusedReliability = Math.max(...reliabilities); // max-possibility path — its reliability, not the max reliability
// (which would pair a possibility from one intermediate with a
// reliability from another).
const maxIndex = possibilities.indexOf(Math.max(...possibilities));
fusedReliability = maxIndex >= 0 ? (reliabilities[maxIndex] ?? 1.0) : 1.0;
} }
if (evaluationMeta) { if (evaluationMeta) {
+22 -9
View File
@@ -51,8 +51,13 @@ function makeModel() {
copy.check = this.check; copy.check = this.check;
return copy; return copy;
}, },
add(src, rel, dst, p, reli) { add(src, rel, dst, p, reli, value) {
this.tuples.set(this.key(src, rel, dst), { p, r: reli ?? 1.0 }); const key = this.key(src, rel, dst);
const existing = this.tuples.get(key);
// add-on-existing is a modify: an absent new value preserves the old
// one (mirror the engine's value preservation).
const v = value !== undefined && value !== null ? value : (existing ? existing.v : undefined);
this.tuples.set(key, { p, r: reli ?? 1.0, v });
return { ok: true }; return { ok: true };
}, },
remove(src, rel, dst) { remove(src, rel, dst) {
@@ -87,6 +92,10 @@ function makeModel() {
} }
} }
} }
// Value parity is exercised by the TTU/chain differential campaigns;
// the model-based harness's opaque shrink reporting makes value
// divergence unreconstructable here, so the model compares
// possibility + reliability only.
return { return {
possibility: Math.round(possibility * 10000) / 10000, possibility: Math.round(possibility * 10000) / 10000,
reliability: Math.round(reliability * 10000) / 10000 reliability: Math.round(reliability * 10000) / 10000
@@ -113,9 +122,11 @@ function makeSut() {
return { return {
ops, ops,
addRelation(src, rel, dst, p, reli) { addRelation(src, rel, dst, p, reli, value) {
arbiter.addRelation(nodeKey(src), rel, nodeKey(dst), { possibility: p, reliability: reli }); const opts = { possibility: p, reliability: reli };
ops.push(['add', src, rel, dst, p, reli]); if (value !== null && value !== undefined) opts.value = value;
arbiter.addRelation(nodeKey(src), rel, nodeKey(dst), opts);
ops.push(['add', src, rel, dst, p, reli, value]);
return { ok: true }; return { ok: true };
}, },
removeRelation(src, rel, dst) { removeRelation(src, rel, dst) {
@@ -133,7 +144,7 @@ function makeSut() {
clone() { clone() {
const fresh = makeSut(); const fresh = makeSut();
for (const op of ops) { for (const op of ops) {
if (op[0] === 'add') fresh.addRelation(op[1], op[2], op[3], op[4], op[5]); if (op[0] === 'add') fresh.addRelation(op[1], op[2], op[3], op[4], op[5], op[6]);
else fresh.removeRelation(op[1], op[2], op[3]); else fresh.removeRelation(op[1], op[2], op[3]);
} }
return fresh; return fresh;
@@ -146,7 +157,8 @@ const tupleArgs = rigor.gen.tuple(
rigor.gen.enum(['owner', 'member_of', 'reads']), rigor.gen.enum(['owner', 'member_of', 'reads']),
rigor.gen.int(0, NODES - 1), rigor.gen.int(0, NODES - 1),
rigor.gen.oneOf(POS), rigor.gen.oneOf(POS),
rigor.gen.oneOf([0.3, 0.6, 0.9]) rigor.gen.oneOf([0.3, 0.6, 0.9]),
rigor.gen.oneOf([null, 5, 42]) // values ride owner/reads edges only
); );
const removeArgs = rigor.gen.tuple( const removeArgs = rigor.gen.tuple(
rigor.gen.int(0, NODES - 1), rigor.gen.int(0, NODES - 1),
@@ -163,7 +175,7 @@ const OPERATIONS = [
{ {
name: 'addRelation', name: 'addRelation',
args: tupleArgs, args: tupleArgs,
run: (model, src, rel, dst, p, reli) => model.add(src, rel, dst, p, reli) run: (model, src, rel, dst, p, reli, value) => model.add(src, rel, dst, p, reli, value)
}, },
{ {
name: 'removeRelation', name: 'removeRelation',
@@ -194,7 +206,8 @@ describe('Model-based authorization graph (rigor.model.check)', () => {
assert.equal(result.passed, true, [ assert.equal(result.passed, true, [
`engine diverged from model in ${result.failures.length} sequences:`, `engine diverged from model in ${result.failures.length} sequences:`,
...result.failures.slice(0, 3).map((f) => ...result.failures.slice(0, 3).map((f) =>
` [${f.commandIndex}] ${f.sequence.map((c) => `${c.name}(${JSON.stringify(c.args)})`).join(' ')}\n` + ` [${f.commandIndex}] ${f.sequence.map((c) => `${c.name}(${JSON.stringify(c.args)})`).join(' -> ')}
` +
` expected=${JSON.stringify(f.expected)} actual=${JSON.stringify(f.actual)}` ` expected=${JSON.stringify(f.expected)} actual=${JSON.stringify(f.actual)}`
) )
].join('\n')); ].join('\n'));
+234 -11
View File
@@ -582,15 +582,21 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
const w = { const w = {
engine, engine,
setEdge(rel, dst, p, reli, side) { setEdge(rel, dst, p, reli, value, side) {
const src = rel === 'owner' ? 'doc:0' : 'u:0'; const src = rel === 'owner' ? 'doc:0' : 'u:0';
const opts = { possibility: p, reliability: reli };
if (rel === 'owner' && value !== null) opts.value = value; // values ride the tupleset leg
if (side === 'persistent') { if (side === 'persistent') {
engine.addRelation(src, rel, dst, { possibility: p, reliability: reli }); engine.addRelation(src, rel, dst, opts);
persistent.set(src + '|' + rel + '|' + dst, { p, r: reli }); // add-on-existing is a modify: an absent new value preserves the
// stored one (mirror the engine).
const key = src + '|' + rel + '|' + dst;
const existing = persistent.get(key);
persistent.set(key, { p, r: reli, v: opts.value !== undefined ? opts.value : (existing ? existing.v : undefined) });
} else { } else {
const idx = partialEdges.findIndex(e => e.relation === rel && e.dst === dst); const idx = partialEdges.findIndex(e => e.relation === rel && e.dst === dst);
if (idx >= 0) partialEdges.splice(idx, 1); if (idx >= 0) partialEdges.splice(idx, 1);
partialEdges.push({ src, relation: rel, dst, possibility: p, reliability: reli }); partialEdges.push({ src, relation: rel, dst, possibility: p, reliability: reli, value: opts.value });
} }
return { ok: true }; return { ok: true };
}, },
@@ -607,38 +613,55 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
const options = partialEdges.length > 0 const options = partialEdges.length > 0
? { partialGraph: { relations: partialEdges.slice() } } ? { partialGraph: { relations: partialEdges.slice() } }
: {}; : {};
const r = engine.check('u:0', 'can_read', 'doc:0', options); const r = engine.check('u:0', 'can_read', 'doc:0', { ...options, collectValues: true });
// TTU mirror: join both legs per intermediate — max over mids of // TTU mirror: join both legs per intermediate — max over mids of
// min(tupleset.possibility, computed.possibility), with the winning // min(tupleset.possibility, computed.possibility), with the winning
// intermediate's reliability = tupleset.reli * computed.reli. Edges // intermediate's reliability = tupleset.reli * computed.reli. Edges
// come from persistent and/or partial; on same-tuple conflicts the // come from persistent and/or partial; on same-tuple conflicts the
// overlay contract is persistent-wins (persistent outranks partial). // overlay contract is persistent-wins (persistent outranks partial).
const tupleset = new Map(); // dst -> { p, r } const tupleset = new Map(); // dst -> { p, r, v }
const computed = new Map(); // dst -> { p, r } const computed = new Map(); // dst -> { p, r }
for (const [key, v] of persistent) { for (const [key, v] of persistent) {
const [src, rel, dst] = key.split('|'); const [src, rel, dst] = key.split('|');
(rel === 'owner' ? tupleset : computed).set(dst, v); if (rel === 'owner') tupleset.set(dst, v);
else computed.set(dst, v);
} }
for (const e of partialEdges) { for (const e of partialEdges) {
const merged = e.relation === 'owner' ? tupleset : computed; if (e.relation === 'owner') {
if (!merged.has(e.dst)) merged.set(e.dst, { p: e.possibility, r: e.reliability ?? 1.0 }); if (!tupleset.has(e.dst)) tupleset.set(e.dst, { p: e.possibility, r: e.reliability ?? 1.0, v: e.value });
} else if (!computed.has(e.dst)) {
computed.set(e.dst, { p: e.possibility, r: e.reliability ?? 1.0 });
}
} }
let best = 0; let best = 0;
let bestReliability = 0; let bestReliability = 0;
const expectedValues = [];
for (const [mid, tp] of tupleset) { for (const [mid, tp] of tupleset) {
const cp = computed.get(mid); const cp = computed.get(mid);
if (cp === undefined) continue; if (cp === undefined) continue;
const combinedP = Math.min(tp.p, cp.p); const combinedP = Math.min(tp.p, cp.p);
if (combinedP > 0) {
if (tp.v !== undefined) expectedValues.push(round4(tp.v));
}
if (combinedP > best) { if (combinedP > best) {
best = combinedP; best = combinedP;
bestReliability = (tp.r ?? 1.0) * (cp.r ?? 1.0); bestReliability = (tp.r ?? 1.0) * (cp.r ?? 1.0);
} }
} }
expectedValues.sort((a, b) => a - b);
const engineValues = (r.collectedValues || [])
.map(v => typeof v === 'number' ? round4(v) : (typeof v.value === 'number' ? round4(v.value) : null))
.filter(v => v !== null)
.sort((a, b) => a - b);
return { return {
engine: round4(r.possibility), engine: round4(r.possibility),
expected: round4(best), expected: round4(best),
engineReliability: round4(r.reliability ?? 0), engineReliability: round4(r.reliability ?? 0),
expectedReliability: round4(bestReliability) expectedReliability: round4(bestReliability),
engineValues,
expectedValues,
persistent: [...persistent.keys()],
partial: partialEdges.map(e => e.relation + ':' + e.dst)
}; };
}, },
clone() { return w; } clone() { return w; }
@@ -648,12 +671,13 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
const result = await rigor.campaign( const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [ [rigor.object('graph', makeWrapper, [
rigor.method('setEdge', function (rel, dst, p, reli, side) { return this.setEdge(rel, dst, p, reli, side); }, rigor.method('setEdge', function (rel, dst, p, reli, value, side) { return this.setEdge(rel, dst, p, reli, value, side); },
rigor.args( rigor.args(
rigor.gen.oneOf(['owner', 'member_of']), rigor.gen.oneOf(['owner', 'member_of']),
rigor.gen.oneOf(['g:0', 'g:1', 'g:2']), rigor.gen.oneOf(['g:0', 'g:1', 'g:2']),
rigor.gen.float(0.1, 1.0), rigor.gen.float(0.1, 1.0),
rigor.gen.oneOf([0.3, 0.6, 0.9]), rigor.gen.oneOf([0.3, 0.6, 0.9]),
rigor.gen.oneOf([null, 5, 42]),
rigor.gen.oneOf(['persistent', 'partial']) rigor.gen.oneOf(['persistent', 'partial'])
)), )),
rigor.method('clearAll', function () { return this.clearAll(); }), rigor.method('clearAll', function () { return this.clearAll(); }),
@@ -669,6 +693,10 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
if (ctx.actual.expected === 0) return true; // denied: reliability 0 if (ctx.actual.expected === 0) return true; // denied: reliability 0
return ctx.actual.engineReliability === ctx.actual.expectedReliability; return ctx.actual.engineReliability === ctx.actual.expectedReliability;
}), }),
rigor.invariant('TTU collected-value parity', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return JSON.stringify(ctx.actual.engineValues) === JSON.stringify(ctx.actual.expectedValues);
}),
rigor.invariant('no action errors', (ctx) => ctx.error === null) rigor.invariant('no action errors', (ctx) => ctx.error === null)
]) ])
).run({ effort: 400, seed: "ttu-partial-split-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } }); ).run({ effort: 400, seed: "ttu-partial-split-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
@@ -738,6 +766,201 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
].join('\n')); ].join('\n'));
}, 90000); }, 90000);
it('PROPERTY CAMPAIGN: defeasible differential under random persistent/partial splits', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.setRelationConfig('can_access', { type: 'defeasible', when: { relation: 'owner' }, unless: { relation: 'banned' } });
const persistent = new Map();
const partialEdges = [];
const w = {
engine,
setEdge(rel, p, reli, side) {
if (side === 'persistent') {
engine.addRelation('u:0', rel, 'doc:0', { possibility: p, reliability: reli });
persistent.set(rel, { p, r: reli });
} else {
const idx = partialEdges.findIndex(e => e.relation === rel);
if (idx >= 0) partialEdges.splice(idx, 1);
partialEdges.push({ src: 'u:0', relation: rel, dst: 'doc:0', possibility: p, reliability: reli });
}
return { ok: true };
},
clearAll() {
for (const rel of [...persistent.keys()]) engine.removeRelation('u:0', rel, 'doc:0');
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: P(when) * (1 - P(unless)) with reliability
// when.reli * unless.reli; persistent wins on same-tuple conflicts.
const when = persistent.get('owner') ?? partialEdges.find(e => e.relation === 'owner');
const unless = persistent.get('banned') ?? partialEdges.find(e => e.relation === 'banned');
const whenP = when ? (when.p ?? when.possibility) : 0;
const whenR = when ? (when.r ?? when.reliability ?? 1.0) : 1.0;
const unlessP = unless ? (unless.p ?? unless.possibility) : 0;
const unlessR = unless ? (unless.r ?? unless.reliability ?? 1.0) : 1.0;
const wp = whenP;
const wpR = whenR;
const up = unlessP;
const upR = unlessR;
const expected = round4(wp * (1 - up));
const expectedReliability = round4(wpR * upR);
return {
engine: round4(r.possibility),
expected,
engineReliability: round4(r.reliability ?? 0),
expectedReliability
};
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('setEdge', function (rel, p, reli, side) { return this.setEdge(rel, p, reli, side); },
rigor.args(
rigor.gen.oneOf(['owner', 'banned']),
rigor.gen.float(0.0, 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('defeasible decision parity', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return ctx.actual.engine === ctx.actual.expected;
}),
rigor.invariant('defeasible 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: "defeasible-split-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`defeasible 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: intersection differential under random persistent/partial splits', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.setRelationConfig('can_access', { intersection: { rules: [{ relation: 'owner' }, { relation: 'verified' }] } });
const persistent = new Map();
const partialEdges = [];
const w = {
engine,
setEdge(rel, p, reli, side) {
if (side === 'persistent') {
engine.addRelation('u:0', rel, 'doc:0', { possibility: p, reliability: reli });
persistent.set(rel, { p, r: reli });
} else {
const idx = partialEdges.findIndex(e => e.relation === rel);
if (idx >= 0) partialEdges.splice(idx, 1);
partialEdges.push({ src: 'u:0', relation: rel, dst: 'doc:0', possibility: p, reliability: reli });
}
return { ok: true };
},
clearAll() {
for (const rel of [...persistent.keys()]) engine.removeRelation('u:0', rel, 'doc:0');
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: min over children with the min child's reliability;
// persistent wins on same-tuple conflicts.
const edges = {};
for (const rel of ['owner', 'verified']) {
const p = persistent.get(rel);
if (p) edges[rel] = p;
}
for (const e of partialEdges) {
if (!edges[e.relation]) edges[e.relation] = { p: e.possibility, r: e.reliability ?? 1.0 };
}
const present = Object.values(edges);
let expected = 0;
let expectedReliability = 0;
if (present.length === 2) {
const minP = Math.min(present[0].p, present[1].p);
const minIdx = present[0].p <= present[1].p ? 0 : 1;
expected = round4(minP);
expectedReliability = round4(present[minIdx].r ?? 1.0);
}
return {
engine: round4(r.possibility),
expected,
engineReliability: round4(r.reliability ?? 0),
expectedReliability
};
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('setEdge', function (rel, p, reli, side) { return this.setEdge(rel, p, reli, side); },
rigor.args(
rigor.gen.oneOf(['owner', 'verified']),
rigor.gen.float(0.0, 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('intersection decision parity', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return ctx.actual.engine === ctx.actual.expected;
}),
rigor.invariant('intersection 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: "intersection-split-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`intersection 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: chain reliability differential under random edge splits', async () => { it('PROPERTY CAMPAIGN: chain reliability differential under random edge splits', async () => {
function makeWrapper() { function makeWrapper() {
const engine = new Arbiter(); const engine = new Arbiter();