feat: evidence composition — compile-time reference resolution for evidence sub-rules
An evidence may now reference another derived evidence as a sub-rule (WHEN can_read(user, doc) where can_read is itself an evidence). Resolution is a compile-time linker pass: after every evidence config is generated, each direct reference to an evidence is inlined with that evidence's own (resolved) config, so the engine evaluates a fully-resolved, acyclic config tree. - resolveEvidenceReferences(): post-generation pass over evidence configs, recursing into logical/defeasible containers (when/unless/never/always/ requires/union/intersection), always.direct nests, and comparator operands. - Forward references resolve (all configs exist before the pass runs). - Cycles and self-references are compile-time errors. - _subjectAsObject scoping is preserved through inlining. - dependsOn is recomputed after resolution, so partial-graph requirements reach transitively through composed evidence. - buildDirectRule/buildPredicateRule now apply subject-scoping to top-level PredicateCall evidence bodies (latent gap, previously missed). - validation: reject relation names shared across facts/sources/evidence/ measures (a collision silently overwrote configs and read as a false cycle). Tests: EvidenceComposition (9), DSLRuntime transitive requiredFacts, oracle campaign composition construct, illegal-mutations cycle + cross-kind cases.
This commit is contained in:
@@ -106,11 +106,28 @@ function buildProgram(kind, ps) {
|
||||
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;
|
||||
}
|
||||
default:
|
||||
throw new Error(`unknown construct: ${kind}`);
|
||||
}
|
||||
|
||||
return { dsl: FACTS + evidence, edges, oracle, relation: evidence.match(/evidence (\w+)/)[1] };
|
||||
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 }) {
|
||||
@@ -134,7 +151,7 @@ function runCheck({ kind, ps }) {
|
||||
}
|
||||
|
||||
const CONSTRUCTS = ['direct', 'chain', 'tuple_to_userset', 'fusion_min', 'fusion_max',
|
||||
'when_unless', 'never_always', 'requires_when'];
|
||||
'when_unless', 'never_always', 'requires_when', 'composition'];
|
||||
|
||||
describe('DSL generative oracle parity (rigor)', () => {
|
||||
it('generated legal DSL compiles and every check matches the oracle', async () => {
|
||||
|
||||
Reference in New Issue
Block a user