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