/** * rigor/validity-parity.test.js — possibilistic validity metadata. * * Pins the Cella-FVN-inspired validity layer: * - every check result carries a validity block {label, operator, regime, * sources, conflictMass, validifiedPossibility, nonMaxitive} * - unlabeled relations default to heuristic; a labeled relation * propagates its label through identity/max fusion * - max (disjunctive) fusion preserves the weakest source label * - interior OWA/averaging is non-maxitive and always heuristic * - min (conjunctive) fusion is approximate at best, surfaces the * conflict mass (1 - possibility), and exposes the arbitrary-regime * validification min(1, K*possibility) * - product-style operators (exclusion, defeasible) are always heuristic * - reliability and validity are distinct: reliability stays the scalar * confidence; validity tracks the epistemic label */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { rigor } from '@rigor/core'; import { Arbiter } from '../../src/index.js'; import { buildValidity, mergeValidity, weakestValidity, DEFAULT_VALIDITY } from '../../src/core/validity.js'; const child = (r) => ({ type: 'direct', relation: r }); function mk() { const a = new Arbiter(); a.addNode('u:0', 'user'); a.addNode('d:0', 'doc'); a.addNode('g:0', 'group'); return a; } describe('Possibilistic validity metadata (rigor)', () => { it('FIXED: labels, operators, conflict mass, validification per kind', () => { // direct unlabeled -> heuristic identity; default carries the minimal // public block (label/operator), the full detail is includeMeta-only { const a = mk(); a.setRelationConfig('t', { type: 'direct', relation: 'r1' }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8 }); const r = a.check('u:0', 't', 'd:0'); assert.equal(r.validity.label, 'heuristic'); assert.equal(r.validity.operator, 'identity'); const r2 = a.check('u:0', 't', 'd:0', { includeMeta: true }); assert.deepEqual(r2.validity.sources, ['r1']); assert.equal(r2.validity.conflictMass, 0); } // labeled direct propagates its label { const a = mk(); a.setRelationConfig('t', { type: 'direct', relation: 'r1' }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' }); assert.equal(a.check('u:0', 't', 'd:0').validity.label, 'finite_sample'); } // max fusion preserves the weakest source label { const a = mk(); a.setRelationConfig('t', { union: [child('r1'), child('r2')] }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' }); a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'conformal' }); const r = a.check('u:0', 't', 'd:0'); assert.equal(r.validity.label, 'conformal', 'weakest label wins through max'); assert.equal(r.validity.operator, 'max'); assert.equal(a.check('u:0', 't', 'd:0', { includeMeta: true }).validity.nonMaxitive, false); } // interior OWA is non-maxitive heuristic { const a = mk(); a.setRelationConfig('t', { union: [child('r1'), child('r2')], aggregator: 'average' }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' }); a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'finite_sample' }); const r = a.check('u:0', 't', 'd:0'); assert.equal(r.validity.operator, 'owa'); assert.equal(a.check('u:0', 't', 'd:0', { includeMeta: true }).validity.nonMaxitive, true); assert.equal(r.validity.label, 'heuristic', 'averaging never claims validity'); } // min fusion: approximate, conflict mass, validification { const a = mk(); a.setRelationConfig('t', { intersection: [child('r1'), child('r2')] }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' }); a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'finite_sample' }); const r = a.check('u:0', 't', 'd:0'); assert.equal(r.validity.operator, 'min'); assert.equal(r.validity.label, 'approximate', 'unvalidified conjunctive is approximate at best'); const rDetail = a.check('u:0', 't', 'd:0', { includeMeta: true }); assert.ok(Math.abs(rDetail.validity.conflictMass - (1 - 0.5)) < 1e-9, 'conflict mass = 1 - possibility'); assert.equal(rDetail.validity.validifiedPossibility, 1, 'min(1, K*gamma) with K=2, gamma=0.5'); } // product operators are heuristic even with labeled sources { const a = mk(); a.setRelationConfig('t', { exclusion: [child('r1'), child('r2')] }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' }); a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'finite_sample' }); assert.equal(a.check('u:0', 't', 'd:0').validity.label, 'heuristic'); a.setRelationConfig('t2', { type: 'defeasible', when: child('r1'), unless: child('r2') }); assert.equal(a.check('u:0', 't2', 'd:0').validity.label, 'heuristic'); } // chain: conjunctive ranking with conflict surfacing { const a = mk(); a.setRelationConfig('t', { type: 'chain', steps: [{ relation: 'member_of', direction: 'out' }, { relation: 'viewer', direction: 'out' }] }); a.addRelation('u:0', 'member_of', 'g:0', { possibility: 0.9 }); a.addRelation('g:0', 'viewer', 'd:0', { possibility: 0.6 }); const r = a.check('u:0', 't', 'd:0'); assert.equal(r.validity.operator, 'min'); assert.ok(Math.abs(a.check('u:0', 't', 'd:0', { includeMeta: true }).validity.conflictMass - 0.4) < 1e-9, 'chain conflict mass'); } // reliability and validity stay distinct { const a = mk(); a.setRelationConfig('t', { type: 'direct', relation: 'r1' }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, reliability: 0.42, validity: 'finite_sample' }); const r = a.check('u:0', 't', 'd:0'); assert.equal(r.reliability, 0.42, 'reliability unchanged'); assert.equal(r.validity.label, 'finite_sample', 'validity independent of reliability'); } }); it('PROPERTY CAMPAIGN: helper semantics (weakest, merge, default identity)', async () => { const result = await rigor.campaign( [rigor.fn('helpers', (labels) => { const weakest = weakestValidity(labels); const merged = mergeValidity(labels.map(l => ({ label: l, operator: 'max', regime: 'arbitrary', sources: ['r'], nonMaxitive: false, conflictMass: 0, validifiedPossibility: null }))); const single = mergeValidity([{ label: labels[0], operator: 'max', regime: 'arbitrary', sources: ['r'], nonMaxitive: false, conflictMass: 0, validifiedPossibility: null }]); return { weakest: weakestValidity([labels[0], weakest]), mergedWeakest: merged.label === weakest, singlePass: single === undefined ? false : single.label === labels[0], defaultIsFrozen: Object.isFrozen(DEFAULT_VALIDITY), buildPositional: buildValidity('min', ['a'], ['finite_sample'], 2, 0.5).validifiedPossibility === 1 }; }, rigor.args(rigor.gen.array(rigor.gen.oneOf(['finite_sample', 'anytime', 'conformal', 'approximate', 'heuristic', 'unknown']), 1, 4)))], rigor.crucible([ rigor.invariant('helper invariants', ({ error, errorMessage, actual }) => !error && !errorMessage && Object.values(actual).every(Boolean)) ]) ).run({ effort: 200, seed: 'validity-helpers-2026', artifacts: { dir: '', persist: 'never' } }); const inv = result.crucibleVerdict?.invariants?.find(i => i.name === 'helper invariants'); assert.ok(inv && inv.passed, `validity helpers violated in ${inv?.failureCount} cases`); }); }); describe('Security affordances (rigor)', () => { it('FIXED: default results carry only the minimal validity; detail is opt-in', () => { const a = mk(); a.setRelationConfig('t', { intersection: [child('r1'), child('r2')] }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' }); a.addRelation('u:0', 'r2', 'd:0', { possibility: 0.5, validity: 'finite_sample' }); // default: minimal block — no conflict mass, no sources, no validified value const r = a.check('u:0', 't', 'd:0'); assert.equal(r.validity.label, 'approximate'); assert.equal(r.validity.operator, 'min'); assert.ok(!('conflictMass' in r.validity), 'conflict mass is not on the default result'); assert.ok(!('validifiedPossibility' in r.validity), 'validified value is not on the default result'); assert.ok(!('sources' in r.validity), 'sources are not on the default result'); // includeMeta: full debugging detail const r2 = a.check('u:0', 't', 'd:0', { includeMeta: true }); assert.equal(r2.validity.conflictMass, 0.5); assert.equal(r2.validity.validifiedPossibility, 1); assert.deepEqual(r2.validity.sources, ['r1', 'r2']); // explain (internal surface) carries the full block const e = a.explain('u:0', 't', 'd:0'); assert.equal(e.decision?.validity?.conflictMass, 0.5, 'explain carries full validity'); }); it('FIXED: audit hook emits one record per check; absent by default', () => { const records = []; const a = new Arbiter({ audit: (entry) => records.push(entry) }); a.addNode('u:0', 'user'); a.addNode('d:0', 'doc'); a.setRelationConfig('t', { type: 'direct', relation: 'r1' }); a.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8, validity: 'finite_sample' }); const r = a.check('u:0', 't', 'd:0'); assert.equal(records.length, 1, 'one audit record per check'); assert.equal(records[0].decision, 'allow'); assert.equal(records[0].possibility, 0.8); assert.equal(records[0].validityLabel, 'finite_sample'); assert.deepEqual(records[0].sources, ['r1']); assert.equal(records[0].partialGraphUsed, false); // overlay checks flag partial usage a.check('u:0', 't', 'd:0', { partialGraph: { relations: [{ src: 'u:0', relation: 'r1', dst: 'd:0', possibility: 0.9 }] } }); assert.equal(records[1].partialGraphUsed, true); // no hook -> no records, no crash const plain = new Arbiter(); plain.addNode('u:0', 'user'); plain.addNode('d:0', 'doc'); plain.setRelationConfig('t', { type: 'direct', relation: 'r1' }); plain.addRelation('u:0', 'r1', 'd:0', { possibility: 0.8 }); assert.equal(plain.check('u:0', 't', 'd:0').possibility, 0.8); }); it('FIXED: oversized partial graphs are rejected before allocation', () => { const a = new Arbiter({ partialGraphPolicy: { maxRelations: 2, maxNodes: 3 } }); a.addNode('u:0', 'user'); a.addNode('d:0', 'doc'); a.setRelationConfig('t', { type: 'direct', relation: 'owner' }); assert.throws( () => a.check('u:0', 't', 'd:0', { partialGraph: { relations: [{}, {}, {}, {}] } }), /exceeds max relations/, 'relation limit enforced pre-allocation' ); assert.throws( () => a.check('u:0', 't', 'd:0', { partialGraph: { nodes: [{}, {}, {}, {}] } }), /exceeds max nodes/, 'node limit enforced pre-allocation' ); }); }); describe('Persistence losslessness (rigor)', () => { it('FIXED: validity, decay config, and TTLs survive the snapshot round trip', async () => { const a = new Arbiter(); a.addNode('u:0', 'user'); a.addNode('d:0', 'doc'); a.setRelationConfig('can_read', { type: 'direct', relation: 'owner' }); a.addRelation('u:0', 'owner', 'd:0', { possibility: 0.8, reliability: 0.42, value: 7, validity: 'finite_sample', decayConfig: { halfLifeMs: 60000 } }); a.valueManager.setTTL('owner', 30000); 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 rel = restored.relationManager.getDirectRelation(restored.resolveNodeId('u:0'), 'owner', restored.resolveNodeId('d:0')); assert.equal(rel.validity, 'finite_sample', 'validity label survives persistence'); assert.deepEqual(rel.decayConfig, { halfLifeMs: 60000 }, 'decay config survives persistence'); assert.equal(restored.valueManager.getTTL('owner'), 30000, 'per-relation TTL survives persistence'); const r = restored.check('u:0', 'can_read', 'd:0', { includeMeta: true }); assert.equal(r.validity.label, 'finite_sample', 'restored check carries the persisted label'); // and a second restore of the same buffer is byte-stable const restored2 = ArbiterSnapshot.fromSnapshotBinary(serializeArbiterSnapshot(a), {}, () => new Arbiter()); assert.equal(restored2.relationManager.getDirectRelation(restored2.resolveNodeId('u:0'), 'owner', restored2.resolveNodeId('d:0')).validity, 'finite_sample'); }); }); describe('Temporal replay (re-entrant diagnostics) (rigor)', () => { it('FIXED: pinned-clock checks are per-time, and the explain rerun reproduces the decision', () => { const a = new Arbiter(); a.addNode('u:0', 'user'); a.addNode('d:0', 'doc'); a.setRelationConfig('premium', { type: 'relational_comparator', comparator: '>', left: { rule: { type: 'direct', relation: 'has_balance' }, extractValue: true }, right: { evaluateFrom: 'object', rule: { type: 'direct', relation: 'has_price' }, extractValue: true } }); a.valueManager.setTTL('has_balance', 60000); a.valueManager.setTTL('has_price', 60000); const t0 = 1000000; a.addRelation('u:0', 'has_balance', 'd:0', { value: 100, changed_last_at: t0 }); a.addRelation('d:0', 'has_price', 'd:0', { value: 50, changed_last_at: t0 }); // Interleaved pinned-clock checks must each reflect their own clock — // no cross-contamination through any result cache. assert.equal(a.check('u:0', 'premium', 'd:0', { now: t0 + 120000 }).possibility, 0, 'expired at +120s'); assert.equal(a.check('u:0', 'premium', 'd:0', { now: t0 + 1000 }).possibility, 1, 'fresh at +1s'); assert.equal(a.check('u:0', 'premium', 'd:0', { now: t0 + 120000 }).possibility, 0, 'expired again'); // The explain rerun reproduces the pinned decision and records the // temporal context the caller must replay. const e = a.explain('u:0', 'premium', 'd:0', { now: t0 + 120000 }); assert.equal(e.decision.possibility, 0, 'explain rerun reproduces the expired decision'); assert.deepEqual(e.request.temporal, { now: t0 + 120000 }, 'temporal context recorded for replay'); const e2 = a.explain('u:0', 'premium', 'd:0', { now: t0 + 1000 }); assert.equal(e2.decision.possibility, 1, 'explain rerun reproduces the fresh decision'); }); it('FIXED: challenge proof expiry is replayable via the pinned clock', () => { const a = new Arbiter(); a.addNode('u:0', 'user'); a.addNode('d:0', 'doc'); a.setRelationConfig('can_download', { type: 'challenge', challenge: 'mfa', subject: 'user', withinMinutes: 5 }); const issued = 500000; const proof = [{ name: 'mfa', subject: 'u:0', issuedAt: issued, expiresAt: null }]; assert.equal(a.check('u:0', 'can_download', 'd:0', { partialGraph: { challenges: proof }, now: issued + 299000 }).possibility, 1, 'within window'); assert.equal(a.check('u:0', 'can_download', 'd:0', { partialGraph: { challenges: proof }, now: issued + 301000 }).possibility, 0, 'past window'); const e = a.explain('u:0', 'can_download', 'd:0', { partialGraph: { challenges: proof }, now: issued + 301000 }); assert.equal(e.decision.possibility, 0, 'explain reproduces the expired proof'); assert.deepEqual(e.request.temporal, { now: issued + 301000 }, 'challenge temporal recorded'); }); });