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.
This commit is contained in:
John Dvorak
2026-08-01 11:18:20 -07:00
parent 4fd4e20bd0
commit ff6e52111d
5 changed files with 229 additions and 38 deletions
+32 -14
View File
@@ -51,8 +51,8 @@ function makeModel() {
copy.check = this.check;
return copy;
},
add(src, rel, dst, p) {
this.tuples.set(this.key(src, rel, dst), p);
add(src, rel, dst, p, reli) {
this.tuples.set(this.key(src, rel, dst), { p, r: reli ?? 1.0 });
return { ok: true };
},
remove(src, rel, dst) {
@@ -61,22 +61,36 @@ function makeModel() {
},
check(src, rel, dst) {
let possibility = 0;
let reliability = 0;
if (rel === 'can_read') {
for (const [k, p] of this.tuples) {
if (k === this.key(src, 'owner', DOC)) possibility = Math.max(possibility, p);
for (const [k, v] of this.tuples) {
if (k === this.key(src, 'owner', DOC)) {
if (v.p > possibility) {
possibility = v.p;
reliability = v.r;
}
}
}
} else if (rel === 'can_access') {
// The chain traverses member_of from ANY node, then reads into the
// object from any reached node — mirror the engine exactly.
// object from any reached node — mirror the engine exactly. The
// winning path's reliability is the product of its edges.
for (let m = 0; m < NODES; m++) {
const a = this.tuples.get(this.key(src, 'member_of', m));
const b = this.tuples.get(this.key(m, 'reads', DOC));
if (a !== undefined && b !== undefined) {
possibility = Math.max(possibility, Math.min(a, b));
const combined = Math.min(a.p, b.p);
if (combined > possibility) {
possibility = combined;
reliability = a.r * b.r;
}
}
}
}
return { possibility: Math.round(possibility * 10000) / 10000 };
return {
possibility: Math.round(possibility * 10000) / 10000,
reliability: Math.round(reliability * 10000) / 10000
};
}
};
}
@@ -99,9 +113,9 @@ function makeSut() {
return {
ops,
addRelation(src, rel, dst, p) {
arbiter.addRelation(nodeKey(src), rel, nodeKey(dst), { possibility: p });
ops.push(['add', src, rel, dst, p]);
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]);
return { ok: true };
},
removeRelation(src, rel, dst) {
@@ -111,12 +125,15 @@ function makeSut() {
},
check(src, rel, dst) {
const result = arbiter.check(nodeKey(src), rel, nodeKey(dst));
return { possibility: Math.round(result.possibility * 10000) / 10000 };
return {
possibility: Math.round(result.possibility * 10000) / 10000,
reliability: Math.round((result.reliability ?? 0) * 10000) / 10000
};
},
clone() {
const fresh = makeSut();
for (const op of ops) {
if (op[0] === 'add') fresh.addRelation(op[1], op[2], op[3], op[4]);
if (op[0] === 'add') fresh.addRelation(op[1], op[2], op[3], op[4], op[5]);
else fresh.removeRelation(op[1], op[2], op[3]);
}
return fresh;
@@ -128,7 +145,8 @@ const tupleArgs = rigor.gen.tuple(
rigor.gen.int(0, NODES - 1),
rigor.gen.enum(['owner', 'member_of', 'reads']),
rigor.gen.int(0, NODES - 1),
rigor.gen.oneOf(POS)
rigor.gen.oneOf(POS),
rigor.gen.oneOf([0.3, 0.6, 0.9])
);
const removeArgs = rigor.gen.tuple(
rigor.gen.int(0, NODES - 1),
@@ -145,7 +163,7 @@ const OPERATIONS = [
{
name: 'addRelation',
args: tupleArgs,
run: (model, src, rel, dst, p) => model.add(src, rel, dst, p)
run: (model, src, rel, dst, p, reli) => model.add(src, rel, dst, p, reli)
},
{
name: 'removeRelation',