3ace783a59
- _expandChainSteps: a logical/defeasible evidence referenced by a chain step
is now a condition step ({ rule, conditionStep }) at ANY position. As the
FINAL step the engine verifies it at (intermediate, object); as an
INTERMEDIATE step the engine expands it from the current node (rule-based
reachability: base edges' destinations filtered by the rule's
defeaters/requirements) and continues traversal from each discovered node.
- buildPredicateRule / buildDirectRule: unary predicate calls whose subject
entity IS the evidence's object parameter (trusted(other) inside
peer_trusted(user, other)) are marked _subjectIsObject (was: only subject-var
calls got _subjectAsObject). Requires @arbiter/core@^1.0.4.
Tests: ChainConditionStep intermediate expansion; oracle campaign gains a
chain_intermediate_condition construct (oracle = min(peer*(1-trusted), read)).
130 lines
6.3 KiB
JavaScript
130 lines
6.3 KiB
JavaScript
/**
|
|
* 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: <config>, 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);
|
|
});
|
|
});
|