/** * tests/rigor/dsl-generative-oracle.test.js — js-rigor campaign that GENERATES * legal Evidence DSL programs, compiles them to @arbiter/core configs, runs * checks, and compares every verdict against an independent ORACLE (a hand- * computed reference implementation of the DSL semantics). * * The oracle is deliberately independent of the engine: it computes the * expected possibility from the generated fact graph using the ADR-000 * semantics (direct = edge, chain = min over steps, tuple_to_userset = min of * the two legs, fusion = min/max over operands, when-unless = base×(1−defeat), * never = 0 when ≥0.5 else base, requires = base×requirement). * * Anti-vacuity: the oracle is NOT a constant — each construct maps distinct * edge possibilities, so a trivial 0-or-1 lowering would be caught. */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { rigor } from '@rigor/core'; import { Arbiter } from '@arbiter/core'; import { DSLCompiler } from '../../src/DSLCompiler.js'; const EPS = 1e-9; const P = [0, 0.25, 0.5, 0.75, 1]; const FACTS = ` definition Employee { id: string } definition Group { id: string } definition Doc { id: string } fact owns(user: Employee, doc: Doc) fact shares(user: Employee, doc: Doc) fact member_of(user: Employee, group: Group) fact can_access(group: Group, doc: Doc) fact owner(group: Group, doc: Doc) fact granted(user: Employee, doc: Doc) fact group_perm(group: Group, doc: Doc) fact group_banned(group: Group) fact banned(user: Employee) fact peer(user: Employee, other: Employee) fact trusted(other: Employee) fact doc_read(user: Employee, doc: Doc) fact mfa(user: Employee) `; // Each construct: how to build the DSL evidence + which edges to add + the oracle. function buildProgram(kind, ps) { let evidence = ''; const edges = []; let oracle = 0; switch (kind) { case 'direct': { const [pOwn] = ps; evidence = `evidence can_read(user: Employee, doc: Doc) { owns(user, doc) }`; edges.push({ src: 'u:1', relation: 'owns', dst: 'doc:9', possibility: pOwn }); oracle = pOwn; break; } case 'chain': { const [pm, pa] = ps; evidence = `evidence can_enter(user: Employee, doc: Doc) { member_of(user, *g) { can_access(g, doc) } }`; edges.push({ src: 'u:1', relation: 'member_of', dst: 'g:1', possibility: pm }); edges.push({ src: 'g:1', relation: 'can_access', dst: 'doc:9', possibility: pa }); oracle = Math.min(pm, pa); break; } case 'tuple_to_userset': { const [po, pm] = ps; evidence = `evidence can_view(user: Employee, doc: Doc) { owner(*g, doc) { member_of(user, g) } }`; edges.push({ src: 'g:1', relation: 'owner', dst: 'doc:9', possibility: po }); edges.push({ src: 'u:1', relation: 'member_of', dst: 'g:1', possibility: pm }); oracle = Math.min(pm, po); break; } case 'fusion_min': { const [p1, p2] = ps; evidence = `evidence can_fuse(user: Employee, doc: Doc) { fusion min { owns(user, doc), shares(user, doc) } }`; edges.push({ src: 'u:1', relation: 'owns', dst: 'doc:9', possibility: p1 }); edges.push({ src: 'u:1', relation: 'shares', dst: 'doc:9', possibility: p2 }); oracle = Math.min(p1, p2); break; } case 'fusion_max': { const [p1, p2] = ps; evidence = `evidence can_fuse(user: Employee, doc: Doc) { fusion max { owns(user, doc), shares(user, doc) } }`; edges.push({ src: 'u:1', relation: 'owns', dst: 'doc:9', possibility: p1 }); edges.push({ src: 'u:1', relation: 'shares', dst: 'doc:9', possibility: p2 }); oracle = Math.max(p1, p2); break; } case 'when_unless': { const [pG, pB] = ps; evidence = `evidence can_borrow(user: Employee, doc: Doc) { WHEN granted(user, doc) UNLESS banned(user) }`; edges.push({ src: 'u:1', relation: 'granted', dst: 'doc:9', possibility: pG }); edges.push({ src: 'u:1', relation: 'banned', dst: 'u:1', possibility: pB }); oracle = pG * (1 - pB); break; } case 'never_always': { const [pG, pB] = ps; evidence = `evidence can_open(user: Employee, doc: Doc) { NEVER banned(user) ALWAYS granted(user, doc) }`; edges.push({ src: 'u:1', relation: 'granted', dst: 'doc:9', possibility: pG }); edges.push({ src: 'u:1', relation: 'banned', dst: 'u:1', possibility: pB }); oracle = pB >= 0.5 ? 0 : pG; break; } case 'requires_when': { const [pG, pM] = ps; evidence = `evidence can_pay(user: Employee, doc: Doc) { REQUIRES mfa(user) WHEN granted(user, doc) }`; edges.push({ src: 'u:1', relation: 'granted', dst: 'doc:9', possibility: pG }); edges.push({ src: 'u:1', relation: 'mfa', dst: 'u:1', possibility: pM }); oracle = pG * pM; break; } case 'composition': { // can_via composes the direct evidence can_read, which reads the owns // edge — an evidence-in-evidence reference resolved at compile time. const [pOwn] = ps; evidence = `evidence can_read(user: Employee, doc: Doc) { owns(user, doc) } evidence can_via(user: Employee, doc: Doc) { can_read(user, doc) }`; edges.push({ src: 'u:1', relation: 'owns', dst: 'doc:9', possibility: pOwn }); oracle = pOwn; break; } case 'chain_step_composition': { // group_read (a direct evidence) used as a CHAIN STEP inside can_via: // the step is expanded at compile time to the underlying can_view edge. const [pm, pv] = ps; evidence = `evidence group_read(group: Group, doc: Doc) { group_perm(group, doc) } evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { group_read(g, doc) } }`; edges.push({ src: 'u:1', relation: 'member_of', dst: 'g:1', possibility: pm }); edges.push({ src: 'g:1', relation: 'group_perm', dst: 'doc:9', possibility: pv }); oracle = Math.min(pm, pv); break; } case 'chain_condition_step': { // gated (a defeasible evidence) as the FINAL chain step → a condition // step: the engine verifies gated at (intermediate, object). The oracle // is the chain's min combined with the condition's base*(1-defeat). const [pm, pv, pb] = ps; evidence = `evidence gated(group: Group, doc: Doc) { WHEN group_perm(group, doc) UNLESS group_banned(group) } evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { gated(g, doc) } }`; edges.push({ src: 'u:1', relation: 'member_of', dst: 'g:1', possibility: pm }); edges.push({ src: 'g:1', relation: 'group_perm', dst: 'doc:9', possibility: pv }); edges.push({ src: 'g:1', relation: 'group_banned', dst: 'g:1', possibility: pb }); oracle = Math.min(pm, pv * (1 - pb)); break; } case 'chain_intermediate_condition': { // peer_trusted (a defeasible evidence) as an INTERMEDIATE chain step: // the engine expands it from the source (peer edges filtered by the // trusted defeater) then continues to can_read. Oracle = min of the // surviving peer leg and the read leg. const [pp, pt, pr] = ps; evidence = `evidence peer_trusted(user: Employee, other: Employee) { WHEN peer(user, other) UNLESS trusted(other) } evidence can_via(user: Employee, doc: Doc) { peer_trusted(user, *p) { doc_read(p, doc) } }`; edges.push({ src: 'u:1', relation: 'peer', dst: 'p:1', possibility: pp }); edges.push({ src: 'p:1', relation: 'trusted', dst: 'p:1', possibility: pt }); edges.push({ src: 'p:1', relation: 'doc_read', dst: 'doc:9', possibility: pr }); oracle = Math.min(pp * (1 - pt), pr); break; } default: throw new Error(`unknown construct: ${kind}`); } return { dsl: FACTS + evidence, edges, oracle, // Check the LAST evidence declaration: the composition construct declares // two evidences (can_read + can_via), and the composed one is the target. relation: [...evidence.matchAll(/evidence\s+(\w+)/g)].at(-1)[1] }; } function runCheck({ kind, ps }) { const { dsl, edges, oracle, relation } = buildProgram(kind, ps); const arbiter = new Arbiter(); arbiter.addNode('u:1', 'Employee'); arbiter.addNode('g:1', 'Group'); arbiter.addNode('doc:9', 'Doc'); for (const e of edges) { arbiter.addNode(e.src, e.dst === 'doc:9' ? 'Doc' : 'Employee'); arbiter.addNode(e.dst, e.dst === 'doc:9' ? 'Doc' : 'Employee'); } const compiler = new DSLCompiler(arbiter); const compiled = compiler.compile(dsl, 'oracle'); if (!compiled.success) { throw new Error(`compile failed for ${kind}: ${compiled.errors.join('; ')}`); } for (const e of edges) arbiter.addRelation(e.src, e.relation, e.dst, { possibility: e.possibility }); const result = arbiter.check('u:1', relation, 'doc:9'); if (Math.abs(result.possibility - oracle) > EPS) { throw new Error(`oracle mismatch for ${kind} (edges=${JSON.stringify(edges)}): ` + `check=${result.possibility} (${result.reason}) vs oracle=${oracle}`); } return { kind, possibility: result.possibility, oracle }; } const CONSTRUCTS = ['direct', 'chain', 'tuple_to_userset', 'fusion_min', 'fusion_max', 'when_unless', 'never_always', 'requires_when', 'composition', 'chain_step_composition', 'chain_condition_step', 'chain_intermediate_condition']; describe('DSL generative oracle parity (rigor)', () => { it('generated legal DSL compiles and every check matches the oracle', async () => { const report = await rigor.campaign( [ rigor.fn('oracle-parity', runCheck, rigor.args( rigor.gen.object({ kind: rigor.gen.oneOf(CONSTRUCTS), // exactly two edge possibilities (direct uses only the first); // a shorter array would leave pB undefined and produce a NaN oracle ps: rigor.gen.tuple(rigor.gen.oneOf(P), rigor.gen.oneOf(P), rigor.gen.oneOf(P)) }) )) ], rigor.crucible([ // `actual` is the fn's return value; a thrown error (compile failure or // oracle mismatch) yields actual === undefined, failing this invariant. rigor.invariant('oracle-parity', ({ actual }) => !!actual && Math.abs(actual.possibility - actual.oracle) <= EPS) ]) ).run({ seed: 'dsl-oracle-parity', effort: 600, artifacts: { dir: '', persist: 'never' } }); const inv = report.crucibleVerdict?.invariants?.find(i => i.name === 'oracle-parity'); assert.ok(inv, 'crucible invariant missing'); assert.equal(inv.passed, true, `oracle parity violated in ${inv.failureCount} cases`); }); it('exhaustive deterministic sweep: every construct x every possibility value', () => { // Anti-vacuity complement to the campaign: sweep the full P × P × P grid // per construct without any RNG, so a construct the campaign skipped would // still be caught here. for (const kind of CONSTRUCTS) { for (const a of P) { for (const b of P) { for (const c of P) { const ps = kind === 'direct' ? [a] : [a, b, c]; const { dsl, edges, oracle, relation } = buildProgram(kind, ps); const arbiter = new Arbiter(); arbiter.addNode('u:1', 'Employee'); arbiter.addNode('g:1', 'Group'); arbiter.addNode('doc:9', 'Doc'); for (const e of edges) { arbiter.addNode(e.src, e.dst === 'doc:9' ? 'Doc' : 'Employee'); arbiter.addNode(e.dst, e.dst === 'doc:9' ? 'Doc' : 'Employee'); } const compiled = new DSLCompiler(arbiter).compile(dsl, 'sweep'); assert.ok(compiled.success, `${kind} compile failed: ${(compiled.errors || []).join('; ')}`); for (const e of edges) arbiter.addRelation(e.src, e.relation, e.dst, { possibility: e.possibility }); const result = arbiter.check('u:1', relation, 'doc:9'); assert.ok( Math.abs(result.possibility - oracle) <= EPS, `${kind} ps=[${ps}] check=${result.possibility}(${result.reason}) vs oracle=${oracle}` ); } } } } }); });