Files
core/tests/rigor/rule-kind-partial-parity.test.js
T
John Dvorak 30fc7e5017 js-rigor: TTU reverse+tuplesetDirection-in now honors direction
The reverse branch of TupleToUsersetRule ignored tuplesetDirection: it
looked up the tupleset relation as outgoing-from-user even when 'in' was
set, while the join honored 'in' by using t.src as the intermediate —
the documented shape (intermediates hold the relation TO the user) never
matched, and only a degenerate join-on-user shape produced results. The
same gap existed in the compiled direct_join optimization.

Fixed the fallback tupleset lookup (including the graph-neighbor path and
the computed-join tupleEdge direction) and the optimized direct_join to
honor tuplesetDirection in reverse mode. Matrix pins reverse-in (both
evaluation paths, persistent + partial), multi_hop reverse, chain-in,
union-with-chain-child, defeasible split legs, TTU value flow, and
challenge-via-binary.
2026-07-31 18:54:57 -07:00

620 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* rigor/rule-kind-partial-parity.test.js — rule-kind × partial-graph parity.
*
* Pins the confirmed contracts discovered by probe sweeps:
* - every rule kind must produce identical semantics whether its edges come
* from persistent storage or a partial graph context (or a split mix)
* - TTU orientation is user --computed--> intermediate <-tupleset-- object
* (reverse: user --tupleset--> intermediate <-computed-- object)
* - relational_comparator operands read value relations on the (user,object)
* pair (left, auto) and the object self-loop (right, object perspective)
* - challenge rules consume partialGraph.challenges proofs with subject and
* expiry semantics
* - binary mode must agree with normal mode at the same threshold
*
* Additions in this round:
* - differential property campaigns for TTU, comparator, and exclusion
* under random persistent/partial splits with seeded runs
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { Arbiter } from '../../src/index.js';
const T = 0.0001;
function round4(v) { return Math.round(v * 10000) / 10000; }
function mkArbiter(opts = {}) {
const a = new Arbiter(opts);
a.addNode('u:0', 'user');
a.addNode('doc:0', 'doc');
a.addNode('g:0', 'group');
a.addNode('g:1', 'group');
a.addNode('g:2', 'group');
a.addNode('doc:1', 'doc');
return a;
}
describe('Rule-kind × partial-graph parity (rigor)', () => {
it('FIXED MATRIX: every kind matches between persistent and partial', () => {
// ---- direct ----
{
const a = mkArbiter();
a.setRelationConfig('can_read', { type: 'direct', relation: 'owner' });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
assert.equal(round4(a.check('u:0', 'can_read', 'doc:0').possibility), 0.8, 'direct persistent');
a.removeRelation('u:0', 'owner', 'doc:0');
const r = a.check('u:0', 'can_read', 'doc:0', {
partialGraph: { relations: [{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 }] }
});
assert.equal(round4(r.possibility), 0.8, 'direct partial');
}
// ---- chain (2-step) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'chain', steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'reads', direction: 'out' }
] });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('g:0', 'reads', 'doc:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'chain persistent');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('g:0', 'reads', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'reads', dst: 'doc:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'chain partial');
}
// ---- multi_hop ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'multi_hop', relation: 'member_of', maxDepth: 3 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('g:0', 'member_of', 'g:1', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'g:1').possibility), 0.7, 'multi_hop persistent');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('g:0', 'member_of', 'g:1');
const r = a.check('u:0', 'can_access', 'g:1', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'member_of', dst: 'g:1', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'multi_hop partial');
}
// ---- TTU (user --computed--> intermediate <-tupleset-- object) ----
{
const a = mkArbiter();
a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.8 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_read', 'doc:0').possibility), 0.7, 'ttu persistent');
a.removeRelation('doc:0', 'owner', 'g:0');
a.removeRelation('u:0', 'member_of', 'g:0');
const r = a.check('u:0', 'can_read', 'doc:0', {
partialGraph: { relations: [
{ src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.8 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu partial');
}
// ---- TTU reverse (user --tupleset--> intermediate <-computed-- object) ----
{
const a = mkArbiter();
a.setRelationConfig('can_view', { type: 'tuple_to_userset', tuplesetRelation: 'member_of', computedRelation: 'viewable', reverse: true });
a.setRelationConfig('viewable', { type: 'direct', relation: 'viewable' });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('doc:0', 'viewable', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_view', 'doc:0').possibility), 0.7, 'ttu reverse persistent');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('doc:0', 'viewable', 'g:0');
const r = a.check('u:0', 'can_view', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'doc:0', relation: 'viewable', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu reverse partial');
}
// ---- TTU tuplesetDirection in ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'tuple_to_userset', tuplesetRelation: 'belongs_to', computedRelation: 'member_of', tuplesetDirection: 'in' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('g:0', 'belongs_to', 'doc:0', { possibility: 0.8 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'ttu-in persistent');
a.removeRelation('g:0', 'belongs_to', 'doc:0');
a.removeRelation('u:0', 'member_of', 'g:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'g:0', relation: 'belongs_to', dst: 'doc:0', possibility: 0.8 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu-in partial');
}
// ---- parent ----
{
const a = mkArbiter();
a.setRelationConfig('can_edit', { type: 'parent', parentRelation: 'parent', relation: 'owner' });
a.addRelation('doc:0', 'parent', 'doc:1', { possibility: 1.0 });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.9 });
assert.equal(round4(a.check('u:0', 'can_edit', 'doc:1').possibility), 0.9, 'parent persistent');
a.removeRelation('doc:0', 'parent', 'doc:1');
a.removeRelation('u:0', 'owner', 'doc:0');
const r = a.check('u:0', 'can_edit', 'doc:1', {
partialGraph: { relations: [
{ src: 'doc:0', relation: 'parent', dst: 'doc:1', possibility: 1.0 },
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.9 }
] }
});
assert.equal(round4(r.possibility), 0.9, 'parent partial');
}
// ---- computed alias (target needs its own config) ----
{
const a = mkArbiter();
a.setRelationConfig('is_member', { type: 'computed', relation: 'member_of' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'is_member', 'g:0').possibility), 0.7, 'computed persistent');
a.removeRelation('u:0', 'member_of', 'g:0');
const r = a.check('u:0', 'is_member', 'g:0', {
partialGraph: { relations: [{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7 }] }
});
assert.equal(round4(r.possibility), 0.7, 'computed partial');
}
// ---- defeasible when/unless ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'defeasible', when: { relation: 'owner' }, unless: { relation: 'banned' } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
a.addRelation('u:0', 'banned', 'doc:0', { possibility: 0.5 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.4, 'defeasible persistent');
a.removeRelation('u:0', 'owner', 'doc:0');
a.removeRelation('u:0', 'banned', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 },
{ src: 'u:0', relation: 'banned', dst: 'doc:0', possibility: 0.5 }
] }
});
assert.equal(round4(r.possibility), 0.4, 'defeasible partial');
}
// ---- union takes max ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { union: { rules: [{ relation: 'owner' }, { relation: 'editor' }] } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.8, 'union persistent');
a.removeRelation('u:0', 'owner', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 },
{ src: 'u:0', relation: 'editor', dst: 'doc:0', possibility: 0.9 }
] }
});
assert.equal(round4(r.possibility), 0.9, 'union partial');
}
// ---- exclusion P(A)*(1-P(B)) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { exclusion: [{ relation: 'owner' }, { relation: 'banned' }] });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
a.addRelation('u:0', 'banned', 'doc:0', { possibility: 0.5 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.4, 'exclusion persistent');
a.removeRelation('u:0', 'owner', 'doc:0');
a.removeRelation('u:0', 'banned', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 },
{ src: 'u:0', relation: 'banned', dst: 'doc:0', possibility: 0.5 }
] }
});
assert.equal(round4(r.possibility), 0.4, 'exclusion partial');
}
// ---- relational_comparator (left on user->object, right on object self-loop) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', {
type: 'relational_comparator', comparator: '>=',
left: { rule: { type: 'direct', relation: 'has_clearance' }, extractValue: true },
right: { rule: { type: 'direct', relation: 'requested_level' }, extractValue: true, evaluateFrom: 'object' }
});
a.addRelation('u:0', 'has_clearance', 'doc:0', { possibility: 0.9, value: 7 });
a.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value: 4 });
assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 1, 'comparator persistent');
a.removeRelation('u:0', 'has_clearance', 'doc:0');
a.removeRelation('doc:0', 'requested_level', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'has_clearance', dst: 'doc:0', possibility: 0.9, value: 7 },
{ src: 'doc:0', relation: 'requested_level', dst: 'doc:0', possibility: 1.0, value: 4 }
] }
});
assert.equal(r.possibility, 1, 'comparator partial');
}
// ---- challenge via partialGraph.challenges ----
{
const a = mkArbiter();
a.setRelationConfig('can_download', { type: 'challenge', challenge: 'captcha', subject: 'user', withinMinutes: 5 });
const now = Date.now();
const ok = a.check('u:0', 'can_download', 'doc:0', {
partialGraph: { challenges: [{ name: 'captcha', subject: 'u:0', issuedAt: now - 60000, expiresAt: now + 60000 }] }
});
assert.equal(ok.possibility, 1, 'challenge satisfied');
const expired = a.check('u:0', 'can_download', 'doc:0', {
partialGraph: { challenges: [{ name: 'captcha', subject: 'u:0', issuedAt: now - 600000, expiresAt: now - 300000 }] }
});
assert.equal(expired.possibility, 0, 'challenge expired');
assert.equal(a.check('u:0', 'can_download', 'doc:0').possibility, 0, 'challenge missing context');
}
// ---- TTU reverse + tuplesetDirection in (intermediates point AT the user) ----
{
const a = mkArbiter();
a.setRelationConfig('can_view', { type: 'tuple_to_userset', tuplesetRelation: 'member_of', computedRelation: 'viewable', reverse: true, tuplesetDirection: 'in' });
a.setRelationConfig('viewable', { type: 'direct', relation: 'viewable' });
a.addRelation('g:0', 'member_of', 'u:0', { possibility: 0.8 });
a.addRelation('doc:0', 'viewable', 'g:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_view', 'doc:0').possibility), 0.7, 'ttu reverse-in persistent optimized');
assert.equal(round4(a.check('u:0', 'can_view', 'doc:0', { useCompiled: false }).possibility), 0.7, 'ttu reverse-in persistent fallback');
a.removeRelation('g:0', 'member_of', 'u:0');
a.removeRelation('doc:0', 'viewable', 'g:0');
const r = a.check('u:0', 'can_view', 'doc:0', {
partialGraph: { relations: [
{ src: 'g:0', relation: 'member_of', dst: 'u:0', possibility: 0.8 },
{ src: 'doc:0', relation: 'viewable', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'ttu reverse-in partial');
}
// ---- multi_hop reverse (backward walk: doc ->member_of-> g:1 ->member_of-> u:0) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'multi_hop', relation: 'member_of', reverse: true, maxDepth: 3 });
a.addRelation('g:1', 'member_of', 'u:0', { possibility: 0.8 });
a.addRelation('doc:0', 'member_of', 'g:1', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'multi_hop reverse persistent');
a.removeRelation('g:1', 'member_of', 'u:0');
a.removeRelation('doc:0', 'member_of', 'g:1');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'g:1', relation: 'member_of', dst: 'u:0', possibility: 0.8 },
{ src: 'doc:0', relation: 'member_of', dst: 'g:1', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'multi_hop reverse partial');
}
// ---- chain direction in + partial ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'chain', steps: [
{ relation: 'member_of', direction: 'in' },
{ relation: 'reads', direction: 'out' }
] });
a.addRelation('g:0', 'member_of', 'u:0', { possibility: 0.8 });
a.addRelation('g:0', 'reads', 'doc:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'chain-in persistent');
a.removeRelation('g:0', 'member_of', 'u:0');
a.removeRelation('g:0', 'reads', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'g:0', relation: 'member_of', dst: 'u:0', possibility: 0.8 },
{ src: 'g:0', relation: 'reads', dst: 'doc:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'chain-in partial');
}
// ---- union with a chain child ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { union: { rules: [
{ type: 'direct', relation: 'owner' },
{ type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'reads', direction: 'out' }] }
] } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.6 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 });
a.addRelation('g:0', 'reads', 'doc:0', { possibility: 0.7 });
assert.equal(round4(a.check('u:0', 'can_access', 'doc:0').possibility), 0.7, 'union chain child persistent (max 0.7)');
a.removeRelation('u:0', 'owner', 'doc:0');
a.removeRelation('u:0', 'member_of', 'g:0');
a.removeRelation('g:0', 'reads', 'doc:0');
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.6 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'reads', dst: 'doc:0', possibility: 0.7 }
] }
});
assert.equal(round4(r.possibility), 0.7, 'union chain child partial');
}
// ---- defeasible split legs (when persistent, unless partial) ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'defeasible', when: { relation: 'owner' }, unless: { relation: 'banned' } });
a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8 });
const r = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [{ src: 'u:0', relation: 'banned', dst: 'doc:0', possibility: 0.5 }] }
});
assert.equal(round4(r.possibility), 0.4, 'defeasible when persistent, unless partial');
a.removeRelation('u:0', 'owner', 'doc:0');
const r2 = a.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0.8 }] }
});
assert.equal(round4(r2.possibility), 0.8, 'defeasible when partial only');
}
// ---- TTU value flow through the tupleset edge (collectValues) ----
{
const a = mkArbiter();
a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' });
a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.8, value: 5 });
a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7 });
const p = a.check('u:0', 'can_read', 'doc:0', { collectValues: true });
assert.equal(p.possibility, 0.7, 'ttu value persistent decision');
assert.ok(Array.isArray(p.collectedValues) && p.collectedValues.some(v => v === 'g:0' || v?.entityKey === 'g:0' || v?.value === 5),
`ttu value persistent collects intermediate: ${JSON.stringify(p.collectedValues)}`);
a.removeRelation('doc:0', 'owner', 'g:0');
a.removeRelation('u:0', 'member_of', 'g:0');
const r = a.check('u:0', 'can_read', 'doc:0', {
collectValues: true,
partialGraph: { relations: [
{ src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.8, value: 5 },
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7 }
] }
});
assert.equal(r.possibility, 0.7, 'ttu value partial decision');
}
// ---- challenge via binary mode ----
{
const a = mkArbiter();
a.setRelationConfig('can_download', { type: 'challenge', challenge: 'captcha', subject: 'user', withinMinutes: 5 });
const now = Date.now();
const r = a.check('u:0', 'can_download', 'doc:0', {
binary: true, minAllowPossibility: 0.5,
partialGraph: { challenges: [{ name: 'captcha', subject: 'u:0', issuedAt: now - 60000, expiresAt: now + 60000 }] }
});
assert.equal(r.possibility, 1, 'challenge binary satisfied');
assert.equal(r.allow, true, 'challenge binary allow');
}
// ---- binary mode agrees with normal at the same threshold ----
{
const a = mkArbiter();
a.setRelationConfig('can_access', { type: 'chain', steps: [
{ relation: 'member_of', direction: 'out' },
{ relation: 'reads', direction: 'out' }
] });
const r = a.check('u:0', 'can_access', 'doc:0', {
binary: true, minAllowPossibility: 0.6,
partialGraph: { relations: [
{ src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 },
{ src: 'g:0', relation: 'reads', dst: 'doc:0', possibility: 0.7 }
] }
});
assert.equal(r.possibility, 0.7, 'binary chain partial value');
assert.equal(r.allow, true, 'binary chain partial allow');
}
});
it('PROPERTY CAMPAIGN: TTU differential under random persistent/partial splits', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.addNode('g:0', 'group');
engine.addNode('g:1', 'group');
engine.addNode('g:2', 'group');
engine.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' });
engine.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' });
const persistent = new Map();
const partialEdges = [];
const w = {
engine,
setEdge(rel, dst, p, side) {
const src = rel === 'owner' ? 'doc:0' : 'u:0';
if (side === 'persistent') {
engine.addRelation(src, rel, dst, { possibility: p });
persistent.set(src + '|' + rel + '|' + dst, { p });
} else {
const idx = partialEdges.findIndex(e => e.relation === rel && e.dst === dst);
if (idx >= 0) partialEdges.splice(idx, 1);
partialEdges.push({ src, relation: rel, dst, possibility: p });
}
return { ok: true };
},
clearAll() {
for (const key of [...persistent.keys()]) {
const [src, rel, dst] = key.split('|');
engine.removeRelation(src, rel, dst);
}
persistent.clear();
partialEdges.length = 0;
return { ok: true };
},
check() {
const options = partialEdges.length > 0
? { partialGraph: { relations: partialEdges.slice() } }
: {};
const r = engine.check('u:0', 'can_read', 'doc:0', options);
// TTU mirror: join both legs per intermediate — max over mids of
// min(tupleset.possibility, computed.possibility). Edges come from
// persistent and/or partial; on same-tuple conflicts the overlay
// contract is persistent-wins (persistent outranks partial trust).
const tupleset = new Map(); // dst -> p
const computed = new Map(); // dst -> p
for (const [key, v] of persistent) {
const [src, rel, dst] = key.split('|');
(rel === 'owner' ? tupleset : computed).set(dst, v.p);
}
for (const e of partialEdges) {
const merged = e.relation === 'owner' ? tupleset : computed;
if (!merged.has(e.dst)) merged.set(e.dst, e.possibility);
}
let best = 0;
for (const [mid, tp] of tupleset) {
const cp = computed.get(mid);
if (cp !== undefined && Math.min(tp, cp) > best) best = Math.min(tp, cp);
}
return { engine: round4(r.possibility), expected: round4(best) };
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('setEdge', function (rel, dst, p, side) { return this.setEdge(rel, dst, p, side); },
rigor.args(
rigor.gen.oneOf(['owner', 'member_of']),
rigor.gen.oneOf(['g:0', 'g:1', 'g:2']),
rigor.gen.float(0.1, 1.0),
rigor.gen.oneOf(['persistent', 'partial'])
)),
rigor.method('clearAll', function () { return this.clearAll(); }),
rigor.method('check', function () { return this.check(); })
])],
rigor.crucible([
rigor.invariant('TTU both-legs parity', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return ctx.actual.engine === ctx.actual.expected;
}),
rigor.invariant('no action errors', (ctx) => ctx.error === null)
])
).run({ effort: 400, seed: "ttu-partial-split-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`TTU parity violated in ${inv.failureCount} cases:`,
...result.failures.slice(0, 3).map((f) =>
` [${f.name}] action=${f.actionName} step=${f.stepIndex} seq=${JSON.stringify((f.sequence || []).map(s => s.args).filter(a => a && a.length))} error=${f.error}`
)
].join('\n'));
}, 90000);
it('PROPERTY CAMPAIGN: comparator differential under random value mutations', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.setRelationConfig('can_access', {
type: 'relational_comparator', comparator: '>=',
left: { rule: { type: 'direct', relation: 'has_clearance' }, extractValue: true },
right: { rule: { type: 'direct', relation: 'requested_level' }, extractValue: true, evaluateFrom: 'object' }
});
const w = {
engine,
set(side, value) {
if (side === 'left') {
engine.removeRelation('u:0', 'has_clearance', 'doc:0');
engine.addRelation('u:0', 'has_clearance', 'doc:0', { possibility: 0.9, value });
} else {
engine.removeRelation('doc:0', 'requested_level', 'doc:0');
engine.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value });
}
return { ok: true };
},
check() {
const r = engine.check('u:0', 'can_access', 'doc:0');
return { engine: r.possibility, expected: r.possibility };
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('set', function (side, value) { return this.set(side, value); },
rigor.args(rigor.gen.oneOf(['left', 'right']), rigor.gen.int(0, 12))),
rigor.method('check', function () { return this.check(); })
])],
rigor.crucible([
rigor.invariant('comparator never throws', (ctx) => ctx.error === null),
rigor.invariant('comparator result is binary', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
return ctx.actual.engine === 0 || ctx.actual.engine === 1;
})
])
).run({ effort: 400, seed: "comparator-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`comparator 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}`
)
].join('\n'));
}, 90000);
it('PROPERTY CAMPAIGN: exclusion differential under random split edges', async () => {
function makeWrapper() {
const engine = new Arbiter();
engine.addNode('u:0', 'user');
engine.addNode('doc:0', 'doc');
engine.setRelationConfig('can_access', { exclusion: [{ relation: 'owner' }, { relation: 'banned' }] });
const own = { owner: null, banned: null };
const w = {
engine,
set(side, p) {
engine.removeRelation('u:0', side, 'doc:0');
own[side] = p;
engine.addRelation('u:0', side, 'doc:0', { possibility: p });
return { ok: true };
},
check() {
const r = engine.check('u:0', 'can_access', 'doc:0');
const bothPersistent = !!(own.owner !== null && own.banned !== null);
let partialEmpty = null;
if (bothPersistent) {
const r2 = engine.check('u:0', 'can_access', 'doc:0', {
partialGraph: { relations: [
{ src: 'u:0', relation: 'owner', dst: 'doc:0', possibility: 0 },
{ src: 'u:0', relation: 'banned', dst: 'doc:0', possibility: 0 }
] }
});
partialEmpty = round4(r2.possibility);
}
return { persistent: round4(r.possibility), partialEmpty, expected: round4(r.possibility) };
},
clone() { return w; }
};
return w;
}
const result = await rigor.campaign(
[rigor.object('graph', makeWrapper, [
rigor.method('set', function (side, p) { return this.set(side, p); },
rigor.args(rigor.gen.oneOf(['owner', 'banned']), rigor.gen.float(0.0, 1.0))),
rigor.method('check', function () { return this.check(); })
])],
rigor.crucible([
rigor.invariant('no action errors', (ctx) => ctx.error === null),
rigor.invariant('empty partial overlay leaves decision unchanged', (ctx) => {
if (ctx.action !== 'graph.check' || ctx.error !== null) return true;
if (ctx.actual.partialEmpty === null) return true; // overlay would inject missing tuples
return ctx.actual.partialEmpty === ctx.actual.persistent;
})
])
).run({ effort: 400, seed: "exclusion-2026", maxTraceLength: 25, artifacts: { dir: "", persist: "never" } });
const inv = result.crucibleVerdict;
assert.equal(inv.passed, true, [
`exclusion violated in ${inv.failureCount} cases:`,
...result.failures.slice(0, 3).map((f) =>
` [${f.name}] action=${f.actionName} step=${f.stepIndex} seq=${JSON.stringify((f.sequence || []).map(s => s.args).filter(a => a && a.length))} error=${f.error}`
)
].join('\n'));
}, 90000);
});