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
+22 -9
View File
@@ -51,8 +51,13 @@ function makeModel() {
copy.check = this.check;
return copy;
},
add(src, rel, dst, p, reli) {
this.tuples.set(this.key(src, rel, dst), { p, r: reli ?? 1.0 });
add(src, rel, dst, p, reli, value) {
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 };
},
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 {
possibility: Math.round(possibility * 10000) / 10000,
reliability: Math.round(reliability * 10000) / 10000
@@ -113,9 +122,11 @@ function makeSut() {
return {
ops,
addRelation(src, rel, dst, p, reli) {
arbiter.addRelation(nodeKey(src), rel, nodeKey(dst), { possibility: p, reliability: reli });
ops.push(['add', src, rel, dst, p, reli]);
addRelation(src, rel, dst, p, reli, value) {
const opts = { possibility: p, reliability: 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 };
},
removeRelation(src, rel, dst) {
@@ -133,7 +144,7 @@ function makeSut() {
clone() {
const fresh = makeSut();
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]);
}
return fresh;
@@ -146,7 +157,8 @@ const tupleArgs = rigor.gen.tuple(
rigor.gen.enum(['owner', 'member_of', 'reads']),
rigor.gen.int(0, NODES - 1),
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(
rigor.gen.int(0, NODES - 1),
@@ -163,7 +175,7 @@ const OPERATIONS = [
{
name: 'addRelation',
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',
@@ -194,7 +206,8 @@ describe('Model-based authorization graph (rigor.model.check)', () => {
assert.equal(result.passed, true, [
`engine diverged from model in ${result.failures.length} sequences:`,
...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)}`
)
].join('\n'));