Files
John Dvorak 2dc478f5a3
CI / test (push) Successful in 18s
CI / publish (push) Successful in 9s
feat: chain-step evidence composition — expand evidence steps in chains
A chain step that references a derived evidence is now expanded at compile
time, keeping the engine a flat edge-traversal evaluator:

- 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').
- CHAIN evidence step -> its steps are spliced into the parent chain
  (a sub-path flattens into the linear source->...->object traversal).
- Any other evidence type (defeasible/logical/comparator) as a step is a
  compile-time error: it is a condition, not an edge traversal.
- Cycles and self-references through chain steps are compile-time errors
  (the existing composition cycle guard now covers steps).

Rigor: oracle campaign gains a chain_step_composition construct; illegal
mutations gain a non-lowerable-chain-step case. Fixture suites updated to
retarget the self-recursive 'canRead/canAccess/...' terminals (an unsupported
recursion pattern that now fails loudly) to an any-typed 'reachable' fact,
preserving the nested-pattern parsing intent.
2026-08-03 11:34:52 -07:00

420 lines
12 KiB
JavaScript

import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { DSLCompiler } from '../src/DSLCompiler.js';
function createMockArbiter() {
const relationConfigs = new Map();
return {
relationConfigs,
setRelationConfig(relation, config) {
relationConfigs.set(relation, config);
}
};
}
const DSL_SUPPORT = `
definition Employee {
role: string
isActive: boolean
isTrusted: boolean
hasRecentActivity: boolean
lastActive: timestamp
isBlacklisted: boolean
session: string
}
definition Document {
level: string
isPublic: boolean
isEditable: boolean
}
definition Resource {
level: string
isPublic: boolean
}
fact hasRole(user: any, role: string)
fact hasClearance(user: any, level: string)
fact owns(user: any, doc: any)
fact isSuspended(user: any)
fact isActive(user: any)
fact isTrusted(user: any)
fact hasRecentActivity(user: any)
fact isBlacklisted(user: any)
fact isMember(user: any, group: any)
fact isFriend(user: any, friend: any)
fact similar(a: any, b: any)
fact reachable(user: any, doc: any)
fact parentOf(user: any, parent: any)
fact isEditable(doc: any)
fact isPublic(doc: any)
fact recentlyActive(user: any)
fact reputationScore(user: any)
fact activityScore(user: any)
fact verificationLevel(user: any)
`;
describe('Evidence Rules', () => {
const arbiter = createMockArbiter();
const compiler = new DSLCompiler(arbiter);
test('Basic evidence', () => {
const testCases = [
{
input: `evidence canRead(user: Employee, doc: Document) {
hasRole(user, 'admin')
}`,
description: 'Simple evidence with function call'
},
{
input: `evidence canAccess(user: Employee, resource: Resource) {
isActive(user)
}`,
description: 'Evidence with attribute access'
},
{
input: `evidence canModify(user: Employee, doc: Document) {
isActive(user)
hasRole(user, 'admin')
}`,
description: 'Evidence with multiple conditions'
},
{
input: `evidence canDelete(user: Employee, doc: Document) {
owns(user, doc)
isActive(user)
}`,
description: 'Evidence with ownership and status'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(DSL_SUPPORT + input, `test-basic-evidence-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
assert.ok(result.program.evidence.length > 0, 'Should have evidence');
});
});
test('Defeasible logic', () => {
const testCases = [
{
input: `evidence canAccess(user: Employee, resource: Resource) {
ALWAYS isActive(user)
}`,
description: 'ALWAYS rule - strict requirement'
},
{
input: `evidence canAccess(user: Employee, resource: Resource) {
WHEN hasRole(user, 'admin')
}`,
description: 'WHEN rule - defeasible condition'
},
{
input: `evidence canAccess(user: Employee, resource: Resource) {
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
}`,
description: 'WHEN/UNLESS rule - defeasible with defeater'
},
{
input: `evidence canAccess(user: Employee, resource: Resource) {
REQUIRES hasClearance(user, resource.level)
}`,
description: 'REQUIRES rule - inverse defeater'
},
{
input: `evidence canAccessCritical(user: Employee, resource: Resource) {
ALWAYS isActive(user)
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES hasClearance(user, resource.level)
}`,
description: 'Complex defeasible logic with all rule types'
},
{
input: `evidence canAccessSensitive(user: Employee, doc: Document) {
ALWAYS isActive(user)
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES hasClearance(user, doc.level)
fusion majority {
isTrusted(user),
hasRecentActivity(user)
}
}`,
description: 'Defeasible logic with fusion'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(DSL_SUPPORT + input, `test-defeasible-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Pattern matching', () => {
const testCases = [
{
input: `evidence canRead(user: Employee, doc: Document) {
isMember(user, *group) {
reachable(group, doc)
}
}`,
description: 'Basic pattern matching with wildcard'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
isMember(user, *group) {
reachable(group, doc)
} limit 5
}`,
description: 'Pattern matching with limit'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
similar(doc, *similar) |similarity| {
reachable(user, similar)
} with similarity > 0.7
}`,
description: 'Pattern matching with binding and condition'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
similar(doc, *similar) |similarity| {
reachable(user, similar)
} limit 5 with similarity > 0.7
}`,
description: 'Pattern matching with binding, condition, and limit'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
isMember(user, *group) {
isMember(group, *parentGroup) {
reachable(parentGroup, doc)
} limit 2
} limit 3
}`,
description: 'Nested pattern matching'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
isFriend(user, *friend) {
isMember(friend, *group) {
reachable(group, doc)
} limit 1
} limit 5
}`,
description: 'Multi-hop pattern matching'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(DSL_SUPPORT + input, `test-pattern-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Fusion', () => {
const testCases = [
{
input: `evidence canAccess(user: Employee, resource: Resource) {
fusion min {
hasClearance(user, resource.level),
isActive(user)
}
}`,
description: 'Min fusion - all conditions must be true'
},
{
input: `evidence canAccess(user: Employee, resource: Resource) {
fusion max {
hasRole(user, 'admin'),
hasRole(user, 'superuser')
}
}`,
description: 'Max fusion - any condition can be true'
},
{
input: `evidence canAccess(user: Employee, resource: Resource) {
fusion majority {
hasClearance(user, 'secret'),
isTrusted(user),
hasRecentActivity(user)
}
}`,
description: 'Majority fusion - most conditions must be true'
},
{
input: `evidence canAccessCritical(user: Employee, resource: Resource) {
fusion min {
hasClearance(user, resource.level),
isActive(user),
NOT isBlacklisted(user)
}
fusion max {
hasRole(user, 'admin')
}
fusion majority {
hasClearance(user, 'secret'),
isTrusted(user),
recentlyActive(user)
}
}`,
description: 'Nested fusion with different strategies'
},
{
input: `evidence canAccess(user: Employee, resource: Resource) {
fusion average {
reputationScore(user),
activityScore(user),
verificationLevel(user)
}
}`,
description: 'Average fusion for numeric values'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(DSL_SUPPORT + input, `test-fusion-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Complex evidence', () => {
const testCases = [
{
input: `evidence canRead(user: Employee, doc: Document) {
owns(user, doc)
isMember(user, *group) {
reachable(group, doc)
} limit 5
parentOf(user, *parent) {
reachable(parent, doc)
} limit 3
similar(doc, *similar) |similarity| {
reachable(user, similar)
} limit 5 with similarity > 0.7
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
}`,
description: 'Complex evidence with all features'
},
{
input: `evidence canAccessCritical(user: Employee, resource: Resource) {
ALWAYS isActive(user)
WHEN hasRole(user, 'admin') UNLESS isSuspended(user)
REQUIRES hasClearance(user, resource.level)
fusion min {
hasClearance(user, resource.level),
isActive(user),
NOT isBlacklisted(user)
}
fusion max {
hasRole(user, 'admin')
}
fusion majority {
hasClearance(user, 'secret'),
isTrusted(user),
recentlyActive(user)
}
}`,
description: 'Critical access with all rule types and fusion'
},
{
input: `evidence canModify(user: Employee, doc: Document) {
owns(user, doc)
isMember(user, *group) {
reachable(group, doc)
} limit 3
similar(doc, *similar) |similarity| {
reachable(user, similar)
isEditable(similar)
} limit 2 with similarity > 0.8
fusion majority {
isTrusted(user),
hasRecentActivity(user),
isPublic(doc)
}
}`,
description: 'Modification access with similarity and fusion'
}
];
testCases.forEach(({ input, description }) => {
const result = compiler.compile(DSL_SUPPORT + input, `test-complex-evidence-${Date.now()}`);
assert.ok(result.success, `${description} should parse successfully`);
});
});
test('Evidence error handling', () => {
const testCases = [
{
input: `evidence canRead(user: Employee, doc: Document) {
hasRole(user, 'admin'
}`,
description: 'Missing closing parenthesis should fail'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
WHEN hasRole(user, 'admin') UNLESS
}`,
description: 'Incomplete UNLESS condition should fail'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
fusion min {
hasRole(user, 'admin')
}`,
description: 'Incomplete fusion should fail'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
isMember(user, *group) {
reachable(group, doc)
} with
}`,
description: 'Incomplete with clause should fail'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
isMember(user, *group) {
reachable(group, doc)
} limit
}`,
description: 'Incomplete limit should fail'
},
{
input: `evidence canRead(user: Employee, doc: Document) {
invalid syntax here
}`,
description: 'Invalid syntax should fail'
}
];
testCases.forEach(({ input, description }) => {
try {
const result = compiler.compile(DSL_SUPPORT + input, `test-evidence-error-${Date.now()}`);
assert.ok(!result.success, `${description} should fail to parse`);
} catch {
// Expected to fail
}
});
});
});