/** * tests/ChainConditionStep.test.js — a chain whose FINAL (object-side) hop * references a defeasible/logical evidence. The compiler lowers it to a * condition step: `{ rule: , conditionStep: true }`, which the engine * verifies at (intermediate, object) rather than traversing an edge. * * Only the final step may be a condition (the object is known); an * intermediate condition cannot discover nodes and is a compile error. */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { Arbiter } from '@arbiter/core'; import { DSLCompiler } from '../src/DSLCompiler.js'; const DEFS = ` definition Employee { id: string } definition Group { id: string } definition Doc { id: string } fact member_of(user: Employee, group: Group) fact can_view(group: Group, doc: Doc) fact banned(group: Group) fact can_edit(group: Group, doc: Doc) `; function compile(dsl, name = 'chain-cond') { const arb = new Arbiter(); const compiler = new DSLCompiler(arb); const result = compiler.compile(dsl, name); return { arb, result }; } describe('Chain condition step (logical evidence as final hop)', () => { it('lowers a defeasible final step to a condition step and grants', () => { const { arb, result } = compile(` ${DEFS} evidence gated(group: Group, doc: Doc) { WHEN can_view(group, doc) UNLESS banned(group) } evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { gated(g, doc) } } `); assert.ok(result.success, JSON.stringify(result.errors)); const steps = arb.relationConfigs.get('can_via').steps; assert.equal(steps[0], 'member_of'); assert.equal(steps[1].conditionStep, true); assert.equal(steps[1].rule.type, 'logical'); // transitive dependency collection through the condition step assert.deepEqual(arb.relationConfigs.get('can_via').dependsOn, ['member_of', 'can_view', 'banned']); arb.addNode('u:1', 'Employee'); arb.addNode('g:1', 'Group'); arb.addNode('doc:9', 'Doc'); arb.addRelation('u:1', 'member_of', 'g:1', { possibility: 1.0 }); arb.addRelation('g:1', 'can_view', 'doc:9', { possibility: 0.7 }); assert.equal(arb.check('u:1', 'can_via', 'doc:9').possibility, 0.7); // banning the intermediate defeats the condition hop arb.addRelation('g:1', 'banned', 'g:1', { possibility: 1.0 }); assert.equal(arb.check('u:1', 'can_via', 'doc:9').possibility, 0); }); it('supports ALWAYS/NEVER evidence as a condition step', () => { const { arb, result } = compile(` ${DEFS} evidence gated(group: Group, doc: Doc) { ALWAYS can_edit(group, doc) } evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { gated(g, doc) } } `); assert.ok(result.success, JSON.stringify(result.errors)); const steps = arb.relationConfigs.get('can_via').steps; assert.equal(steps[1].conditionStep, true); arb.addNode('u:1', 'Employee'); arb.addNode('g:1', 'Group'); arb.addNode('doc:9', 'Doc'); arb.addRelation('u:1', 'member_of', 'g:1', { possibility: 1.0 }); arb.addRelation('g:1', 'can_edit', 'doc:9', { possibility: 0.6 }); assert.equal(arb.check('u:1', 'can_via', 'doc:9').possibility, 0.6); arb.removeRelation('g:1', 'can_edit', 'doc:9'); assert.equal(arb.check('u:1', 'can_via', 'doc:9').possibility, 0); }); it('keeps the condition evidence checkable in its own right', () => { const { arb, result } = compile(` ${DEFS} evidence gated(group: Group, doc: Doc) { WHEN can_view(group, doc) UNLESS banned(group) } evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { gated(g, doc) } } `); assert.ok(result.success, JSON.stringify(result.errors)); arb.addNode('g:1', 'Group'); arb.addNode('doc:9', 'Doc'); arb.addRelation('g:1', 'can_view', 'doc:9', { possibility: 0.8 }); assert.equal(arb.check('g:1', 'gated', 'doc:9').possibility, 0.8); }); it('parallel intermediates aggregate through the condition step', () => { const { arb, result } = compile(` ${DEFS} evidence gated(group: Group, doc: Doc) { can_view(group, doc) } evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { gated(g, doc) } } `); assert.ok(result.success, JSON.stringify(result.errors)); arb.addNode('u:1', 'Employee'); arb.addNode('g:1', 'Group'); arb.addNode('g2:2', 'Group'); arb.addNode('doc:9', 'Doc'); arb.addRelation('u:1', 'member_of', 'g:1', { possibility: 0.5 }); arb.addRelation('g:1', 'can_view', 'doc:9', { possibility: 0.7 }); arb.addRelation('u:1', 'member_of', 'g2:2', { possibility: 1.0 }); arb.addRelation('g2:2', 'can_view', 'doc:9', { possibility: 0.8 }); // max over paths: min(0.5,0.7)=0.5, min(1.0,0.8)=0.8 -> 0.8 assert.equal(arb.check('u:1', 'can_via', 'doc:9').possibility, 0.8); }); it('expands an INTERMEDIATE condition step via rule-based reachability', () => { const { arb, result } = compile(` definition Employee { id: string } definition Doc { id: string } fact peer(user: Employee, other: Employee) fact trusted(other: Employee) fact can_read(user: Employee, doc: Doc) evidence peer_trusted(user: Employee, other: Employee) { WHEN peer(user, other) UNLESS trusted(other) } evidence can_access(user: Employee, doc: Doc) { peer_trusted(user, *p) { can_read(p, doc) } } `); assert.ok(result.success, JSON.stringify(result.errors)); const steps = arb.relationConfigs.get('can_access').steps; assert.equal(steps[0].conditionStep, true); assert.equal(steps[0].rule.type, 'logical'); arb.addNode('u:1', 'Employee'); arb.addNode('p:1', 'Employee'); arb.addNode('p:2', 'Employee'); arb.addNode('doc:9', 'Doc'); arb.addRelation('u:1', 'peer', 'p:1', { possibility: 1.0 }); arb.addRelation('u:1', 'peer', 'p:2', { possibility: 1.0 }); arb.addRelation('p:1', 'trusted', 'p:1', { possibility: 1.0 }); // p:1 filtered arb.addRelation('p:1', 'can_read', 'doc:9', { possibility: 0.9 }); arb.addRelation('p:2', 'can_read', 'doc:9', { possibility: 0.7 }); // only untrusted peer p:2 survives the intermediate condition -> 0.7 assert.equal(arb.check('u:1', 'can_access', 'doc:9').possibility, 0.7); // trusting p:2 too removes all intermediates -> 0 arb.addRelation('p:2', 'trusted', 'p:2', { possibility: 1.0 }); assert.equal(arb.check('u:1', 'can_access', 'doc:9').possibility, 0); }); });