/** * 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', async () => { // ---- 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'); } // ---- TTU multi-path fusion + reliability propagation ---- { 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, reliability: 0.9 }); a.addRelation('doc:0', 'owner', 'g:1', { possibility: 0.6 }); a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.7, reliability: 0.8 }); a.addRelation('u:0', 'member_of', 'g:1', { possibility: 0.5 }); const p = a.check('u:0', 'can_read', 'doc:0'); assert.equal(round4(p.possibility), 0.7, 'ttu multi-path fusion persistent'); assert.ok(typeof p.reliability === 'number' && p.reliability > 0, `ttu reliability flows through normal check: ${p.reliability}`); assert.ok(Math.abs(p.reliability - 0.72) < 0.01, `ttu reliability = 0.9*0.8, got ${p.reliability}`); a.removeRelation('doc:0', 'owner', 'g:0'); a.removeRelation('doc:0', 'owner', 'g:1'); a.removeRelation('u:0', 'member_of', 'g:0'); a.removeRelation('u:0', 'member_of', 'g:1'); const r = a.check('u:0', 'can_read', 'doc:0', { partialGraph: { relations: [ { src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.8, reliability: 0.9 }, { src: 'doc:0', relation: 'owner', dst: 'g:1', possibility: 0.6 }, { src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.7, reliability: 0.8 }, { src: 'u:0', relation: 'member_of', dst: 'g:1', possibility: 0.5 } ] } }); assert.equal(round4(r.possibility), 0.7, 'ttu multi-path fusion partial'); assert.ok(Math.abs(r.reliability - 0.72) < 0.01, `ttu reliability partial, got ${r.reliability}`); } // ---- TTU maxIntermediates circuit breaker through partial ---- { const a = mkArbiter(); for (let i = 0; i < 25; i++) a.addNode('gx:' + i, 'group'); a.setRelationConfig('can_read', { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of', maxIntermediates: 5 }); a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' }); const rels = []; for (let i = 0; i < 25; i++) { rels.push({ src: 'doc:0', relation: 'owner', dst: 'gx:' + i, possibility: 0.3 + (i % 10) / 20 }); rels.push({ src: 'u:0', relation: 'member_of', dst: 'gx:' + i, possibility: 0.5 }); } const r = a.check('u:0', 'can_read', 'doc:0', { partialGraph: { relations: rels } }); assert.equal(round4(r.possibility), 0.5, 'ttu circuit breaker picks best of limited intermediates'); } // ---- snapshot-restored arbiter evaluates TTU ---- { 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 }); a.enableCondensedSnapshot(); const { serializeArbiterSnapshot } = await import('../../src/core/SnapshotBinary.js'); const { ArbiterSnapshot } = await import('../../src/core/arbiter/ArbiterSnapshot.js'); const restored = ArbiterSnapshot.fromSnapshotBinary(serializeArbiterSnapshot(a), {}, () => new Arbiter()); const p = restored.check('u:0', 'can_read', 'doc:0'); assert.equal(round4(p.possibility), 0.7, 'snapshot-restored TTU'); } // ---- comparator operand as TTU (value flows via the tupleset edge) ---- { const a = mkArbiter(); a.setRelationConfig('can_access', { type: 'relational_comparator', comparator: '>=', left: { rule: { type: 'tuple_to_userset', tuplesetRelation: 'owner', computedRelation: 'member_of' }, extractValue: true }, right: { rule: { type: 'direct', relation: 'requested_level' }, extractValue: true, evaluateFrom: 'object' } }); a.setRelationConfig('member_of', { type: 'direct', relation: 'member_of' }); a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9, value: 7 }); a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 }); 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, 'ttu operand persistent allow'); a.removeRelation('doc:0', 'owner', 'g:0'); a.removeRelation('u:0', 'member_of', 'g:0'); a.removeRelation('doc:0', 'requested_level', 'doc:0'); const r = a.check('u:0', 'can_access', 'doc:0', { partialGraph: { relations: [ { src: 'doc:0', relation: 'owner', dst: 'g:0', possibility: 0.9, value: 7 }, { src: 'u:0', relation: 'member_of', dst: 'g:0', possibility: 0.8 }, { src: 'doc:0', relation: 'requested_level', dst: 'doc:0', possibility: 1.0, value: 4 } ] } }); assert.equal(r.possibility, 1, 'ttu operand partial allow'); a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9, value: 3 }); a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8 }); a.addRelation('doc:0', 'requested_level', 'doc:0', { possibility: 1.0, value: 4 }); assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 0, 'ttu operand below threshold deny'); a.removeRelation('doc:0', 'owner', 'g:0'); a.addRelation('doc:0', 'owner', 'g:0', { possibility: 0.9 }); assert.equal(a.check('u:0', 'can_access', 'doc:0').possibility, 0, 'ttu operand without value denies'); } // ---- modify changed_last_at override (value-changing modify honors the pin) ---- { const a = mkArbiter(); a.addRelation('u:0', 'balance', 'doc:0', { value: 10, changed_last_at: 1000 }); a.relationManager.updateRelationsBatch([ { operation: 'modify', srcKey: 'u:0', relation: 'balance', dstKey: 'doc:0', options: { value: 20, changed_last_at: 5000 } } ]); const after = a.relationManager.getDirectRelation(a.resolveNodeId('u:0'), 'balance', a.resolveNodeId('doc:0')); assert.equal(after.changed_last_at, 5000, 'value-changing modify honors changed_last_at override'); a.relationManager.updateRelationsBatch([ { operation: 'modify', srcKey: 'u:0', relation: 'balance', dstKey: 'doc:0', options: { value: 20, changed_last_at: 9999 } } ]); const unchanged = a.relationManager.getDirectRelation(a.resolveNodeId('u:0'), 'balance', a.resolveNodeId('doc:0')); assert.equal(unchanged.changed_last_at, 5000, 'value-unchanged modify keeps old timestamp (override not a refresh)'); } // ---- reliability propagation across kinds ---- { // chain: product of edge reliabilities (0.9 * 0.8) 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, reliability: 0.9 }); a.addRelation('g:0', 'reads', 'doc:0', { possibility: 0.7, reliability: 0.8 }); const c = a.check('u:0', 'can_access', 'doc:0'); assert.ok(Math.abs(c.reliability - 0.72) < 0.01, `chain reliability product, got ${c.reliability}`); // multi_hop: product along the path a.setRelationConfig('can_hop', { type: 'multi_hop', relation: 'member_of', maxDepth: 3 }); a.addNode('g:1', 'group'); a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.8, reliability: 0.9 }); a.addRelation('g:0', 'member_of', 'g:1', { possibility: 0.7, reliability: 0.8 }); const m = a.check('u:0', 'can_hop', 'g:1'); assert.ok(Math.abs(m.reliability - 0.72) < 0.01, `multi_hop reliability product, got ${m.reliability}`); // union: the max child's reliability (editor 0.9 / reli 0.5) a.setRelationConfig('can_union', { union: { rules: [{ relation: 'owner' }, { relation: 'editor' }] } }); a.addRelation('u:0', 'owner', 'doc:0', { possibility: 0.8, reliability: 0.9 }); a.addRelation('u:0', 'editor', 'doc:0', { possibility: 0.9, reliability: 0.5 }); const u = a.check('u:0', 'can_union', 'doc:0'); assert.equal(round4(u.reliability), 0.5, `union selected-child reliability, got ${u.reliability}`); // intersection: the min child's reliability (verified 0.6) a.setRelationConfig('can_intersect', { intersection: { rules: [{ relation: 'owner' }, { relation: 'verified' }] } }); a.addRelation('u:0', 'verified', 'doc:0', { possibility: 0.5, reliability: 0.6 }); const i = a.check('u:0', 'can_intersect', 'doc:0'); assert.equal(round4(i.reliability), 0.6, `intersection selected-child reliability, got ${i.reliability}`); // exclusion: product of both legs (0.9 * 0.7) a.setRelationConfig('can_excl', { exclusion: [{ relation: 'owner' }, { relation: 'banned' }] }); a.addRelation('u:0', 'banned', 'doc:0', { possibility: 0.5, reliability: 0.7 }); const e = a.check('u:0', 'can_excl', 'doc:0'); assert.ok(Math.abs(e.reliability - 0.63) < 0.01, `exclusion product reliability, got ${e.reliability}`); // defeasible: when reli * unless reli (0.9 * 0.7) a.setRelationConfig('can_def', { type: 'defeasible', when: { relation: 'owner' }, unless: { relation: 'banned' } }); const d = a.check('u:0', 'can_def', 'doc:0'); assert.ok(Math.abs(d.reliability - 0.63) < 0.01, `defeasible combined reliability, got ${d.reliability}`); } // ---- multi_hop value collection through partial (no crash, values flow) ---- { const a = mkArbiter(); a.addNode('g:1', 'group'); 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, value: 42 }); const p = a.check('u:0', 'can_access', 'g:1', { collectValues: true }); assert.ok(Array.isArray(p.collectedValues) && p.collectedValues.length > 0, 'multi_hop persistent values collected'); assert.ok(typeof p.collectedValues[0].value?.min === 'number', 'multi_hop value interval present'); 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', { collectValues: true, 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, value: 42 } ] } }); assert.ok(Array.isArray(r.collectedValues) && r.collectedValues.length > 0, 'multi_hop partial values collected'); } // ---- 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); });