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
+20 -7
View File
@@ -18,6 +18,7 @@ import { Arbiter } from '../../src/index.js';
const KEY = 'u:0|owner|doc:0';
const VALUES = [0.2, 0.5, 0.8, 0.9];
const RELIABILITIES = [0.3, 0.6, 0.9];
function applyMirror(tuples, ops) {
for (const op of ops) {
@@ -26,7 +27,9 @@ function applyMirror(tuples, ops) {
} else if (op.operation === 'modify' && !tuples.has(KEY)) {
continue; // engine no-op on missing tuple
} else {
tuples.set(KEY, op.value);
// Last write wins for the whole tuple: reliability rides along, so
// the mirror carries it and the decision invariant can check it too.
tuples.set(KEY, { p: op.value, r: op.reliability });
}
}
}
@@ -44,7 +47,9 @@ function makeWrapper() {
tuples,
add(value) {
arbiter.addRelation('u:0', 'owner', 'doc:0', { possibility: value });
tuples.set(KEY, value);
// add-on-existing is a modify: reliability is preserved, not reset.
const existing = tuples.get(KEY);
tuples.set(KEY, { p: value, r: existing ? existing.r : 1.0 });
return { ok: true };
},
remove() {
@@ -58,7 +63,7 @@ function makeWrapper() {
srcKey: 'u:0',
relation: 'owner',
dstKey: 'doc:0',
options: op.operation === 'remove' ? {} : { possibility: op.value }
options: op.operation === 'remove' ? {} : { possibility: op.value, reliability: op.reliability }
}));
arbiter.relationManager.updateRelationsBatch(batch);
applyMirror(tuples, batchOps);
@@ -66,8 +71,10 @@ function makeWrapper() {
},
check() {
const result = arbiter.check('u:0', 'can_read', 'doc:0');
const expected = tuples.get(KEY) ?? 0;
return { engine: result.possibility, expected, reason: result.reason };
const entry = tuples.get(KEY);
const expected = entry ? entry.p : 0;
const expectedReliability = entry ? entry.r : 0;
return { engine: result.possibility, expected, expectedReliability, reliability: result.reliability, reason: result.reason };
},
clone() {
const fresh = makeWrapper();
@@ -93,7 +100,8 @@ function makeWrapper() {
const opGen = rigor.gen.record({
operation: rigor.gen.enum(['add', 'modify', 'remove']),
value: rigor.gen.oneOf(VALUES)
value: rigor.gen.oneOf(VALUES),
reliability: rigor.gen.oneOf(RELIABILITIES)
});
describe('Batch update ordering semantics (rigor)', () => {
@@ -140,6 +148,11 @@ describe('Batch update ordering semantics (rigor)', () => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return ctx.actual.engine === ctx.actual.expected;
}),
rigor.invariant('reliability parity after every step', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
if (ctx.actual.expectedReliability === 0) return true; // denied decision: reliability 0
return Math.abs((ctx.actual.reliability ?? 1.0) - ctx.actual.expectedReliability) < 1e-9;
}),
rigor.invariant('no action errors', (ctx) => ctx.error === null)
])
).run({ effort: 400, seed: 'batch-order-parity', maxTraceLength: 25 , artifacts: { dir: '', persist: 'never' }});
@@ -148,7 +161,7 @@ describe('Batch update ordering semantics (rigor)', () => {
assert.equal(inv.passed, true, [
`batch ordering violated in ${inv.failureCount} cases:`,
...result.failures.slice(0, 3).map((f) =>
` [${f.invariant}] action=${f.action} args=${JSON.stringify(f.args)} actual=${JSON.stringify(f.actual)} error=${f.error}`
` [${f.name}] action=${f.actionName} seq=${JSON.stringify((f.sequence || []).map(s => s.args).filter(a => a && a.length))} actual=${JSON.stringify(f.actual)} error=${f.error}`
)
].join('\n'));
});