351551af0f
A chain's FINAL (object-side) step may now reference a defeasible/logical
evidence. The compiler lowers it to a condition step
({ rule: <config>, conditionStep: true }) that the engine verifies at
(intermediate, object) instead of traversing an edge. Requires
@arbiter/core@^1.0.3 (ChainRule condition-step support).
- _expandChainSteps: a logical/defeasible/comparator evidence is expressible
as a final condition step; non-final such steps remain a compile error
(a condition cannot discover intermediate nodes).
- Dependency collection (generator + DSLRuntime) descends into condition-step
rule configs, so partial-graph requirements reach through them.
Tests: ChainConditionStep (defeasible + ALWAYS steps, independent checkability,
parallel aggregation), oracle campaign chain_condition_step construct
(oracle = min(pm, pv*(1-pb))), DSLRuntime transitive required facts through a
condition step.
128 lines
5.9 KiB
JavaScript
128 lines
5.9 KiB
JavaScript
/**
|
|
* tests/ChainStepComposition.test.js — evidence composition inside CHAIN
|
|
* steps. A chain step that references a derived evidence is expanded at
|
|
* compile time:
|
|
* - a DIRECT evidence step → renamed to its underlying relation
|
|
* (member_of(user,*g){ group_read(g,doc) } where group_read = can_view
|
|
* becomes step 'can_view');
|
|
* - a CHAIN evidence step → its steps are spliced into the parent chain
|
|
* (a sub-path flattens into the linear source→…→object traversal);
|
|
* - a DEFEASIBLE / LOGICAL / COMPARATOR evidence step is not an edge
|
|
* traversal and is rejected at compile time;
|
|
* - cycles and self-references through chain steps are compile errors.
|
|
*/
|
|
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 group_has(group: Group, sub: Group)
|
|
fact can_view(group: Group, doc: Doc)
|
|
fact can_access(group: Group, doc: Doc)
|
|
fact banned(group: Group)
|
|
`;
|
|
|
|
function compile(dsl, name = 'chain-compose') {
|
|
const arb = new Arbiter();
|
|
const compiler = new DSLCompiler(arb);
|
|
const result = compiler.compile(dsl, name);
|
|
return { arb, result };
|
|
}
|
|
|
|
describe('Chain step composition', () => {
|
|
it('renames a direct-evidence chain step to its underlying relation', () => {
|
|
const { arb, result } = compile(`
|
|
${DEFS}
|
|
evidence group_read(group: Group, doc: Doc) { can_view(group, doc) }
|
|
evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { group_read(g, doc) } }
|
|
`);
|
|
assert.ok(result.success, JSON.stringify(result.errors));
|
|
// step 'group_read' → 'can_view'
|
|
assert.deepEqual(arb.relationConfigs.get('can_via').steps, ['member_of', 'can_view']);
|
|
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);
|
|
});
|
|
|
|
it('splices a chain-evidence step into the parent chain', () => {
|
|
const { arb, result } = compile(`
|
|
${DEFS}
|
|
evidence group_enter(group: Group, doc: Doc) { group_has(group, *s) { can_access(s, doc) } }
|
|
evidence can_deep(user: Employee, doc: Doc) { member_of(user, *g) { group_enter(g, doc) } }
|
|
`);
|
|
assert.ok(result.success, JSON.stringify(result.errors));
|
|
// step 'group_enter' → its steps [group_has, can_access]
|
|
assert.deepEqual(arb.relationConfigs.get('can_deep').steps, ['member_of', 'group_has', 'can_access']);
|
|
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: 1.0 });
|
|
arb.addRelation('g:1', 'group_has', 'g2:2', { possibility: 0.9 });
|
|
arb.addRelation('g2:2', 'can_access', 'doc:9', { possibility: 0.8 });
|
|
assert.equal(arb.check('u:1', 'can_deep', 'doc:9').possibility, 0.8);
|
|
});
|
|
|
|
it('expands a chain step whose direct evidence is itself composed', () => {
|
|
const { arb, result } = compile(`
|
|
${DEFS}
|
|
evidence group_view(group: Group, doc: Doc) { can_view(group, doc) }
|
|
evidence group_read(group: Group, doc: Doc) { group_view(group, doc) }
|
|
evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { group_read(g, doc) } }
|
|
`);
|
|
assert.ok(result.success, JSON.stringify(result.errors));
|
|
assert.deepEqual(arb.relationConfigs.get('can_via').steps, ['member_of', 'can_view']);
|
|
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.6 });
|
|
assert.equal(arb.check('u:1', 'can_via', 'doc:9').possibility, 0.6);
|
|
});
|
|
|
|
it('lowers a logical evidence FINAL step to a condition step', () => {
|
|
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));
|
|
// final-step logical evidence → condition step (verified at the object)
|
|
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');
|
|
});
|
|
|
|
it('rejects a mutual cycle through chain steps', () => {
|
|
const { result } = compile(`
|
|
${DEFS}
|
|
evidence cyc_a(group: Group, doc: Doc) { group_has(group, *g) { cyc_b(g, doc) } }
|
|
evidence cyc_b(group: Group, doc: Doc) { cyc_a(group, doc) }
|
|
`);
|
|
assert.equal(result.success, false);
|
|
assert.ok(result.errors.some(e => /[Cc]yclic/.test(e)), JSON.stringify(result.errors));
|
|
});
|
|
|
|
it('rejects a self-reference through its own chain step', () => {
|
|
const { result } = compile(`
|
|
${DEFS}
|
|
evidence cyc_c(group: Group, doc: Doc) { group_has(group, *g) { cyc_c(g, doc) } }
|
|
`);
|
|
assert.equal(result.success, false);
|
|
assert.ok(result.errors.some(e => /[Cc]yclic/.test(e)), JSON.stringify(result.errors));
|
|
});
|
|
|
|
it('re-derives transitive dependencies through expanded chain steps', () => {
|
|
const { arb, result } = compile(`
|
|
${DEFS}
|
|
evidence group_read(group: Group, doc: Doc) { can_view(group, doc) }
|
|
evidence can_via(user: Employee, doc: Doc) { member_of(user, *g) { group_read(g, doc) } }
|
|
`);
|
|
assert.ok(result.success, JSON.stringify(result.errors));
|
|
// dependsOn reflects the expanded step, not the evidence reference
|
|
assert.deepEqual(arb.relationConfigs.get('can_via').dependsOn, ['member_of', 'can_view']);
|
|
});
|
|
});
|