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:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user