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:
@@ -582,15 +582,21 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
|
||||
|
||||
const w = {
|
||||
engine,
|
||||
setEdge(rel, dst, p, reli, side) {
|
||||
setEdge(rel, dst, p, reli, value, side) {
|
||||
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') {
|
||||
engine.addRelation(src, rel, dst, { possibility: p, reliability: reli });
|
||||
persistent.set(src + '|' + rel + '|' + dst, { p, r: reli });
|
||||
engine.addRelation(src, rel, dst, opts);
|
||||
// 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 {
|
||||
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 });
|
||||
partialEdges.push({ src, relation: rel, dst, possibility: p, reliability: reli, value: opts.value });
|
||||
}
|
||||
return { ok: true };
|
||||
},
|
||||
@@ -607,38 +613,55 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
|
||||
const options = partialEdges.length > 0
|
||||
? { 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
|
||||
// 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 tupleset = new Map(); // dst -> { p, r, v }
|
||||
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);
|
||||
if (rel === 'owner') tupleset.set(dst, v);
|
||||
else 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 });
|
||||
if (e.relation === 'owner') {
|
||||
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 bestReliability = 0;
|
||||
const expectedValues = [];
|
||||
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 > 0) {
|
||||
if (tp.v !== undefined) expectedValues.push(round4(tp.v));
|
||||
}
|
||||
if (combinedP > best) {
|
||||
best = combinedP;
|
||||
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 {
|
||||
engine: round4(r.possibility),
|
||||
expected: round4(best),
|
||||
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; }
|
||||
@@ -648,12 +671,13 @@ describe('Rule-kind × partial-graph parity (rigor)', () => {
|
||||
|
||||
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.method('setEdge', function (rel, dst, p, reli, value, side) { return this.setEdge(rel, dst, p, reli, value, 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([null, 5, 42]),
|
||||
rigor.gen.oneOf(['persistent', 'partial'])
|
||||
)),
|
||||
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
|
||||
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)
|
||||
])
|
||||
).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'));
|
||||
}, 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 () => {
|
||||
function makeWrapper() {
|
||||
const engine = new Arbiter();
|
||||
|
||||
Reference in New Issue
Block a user