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:
@@ -41,6 +41,7 @@ export function validateDslText(dslText, options = {}) {
|
||||
validateSources(program, tables, errors, warnings, dslText);
|
||||
validateMeasures(program, tables, errors, warnings, dslText);
|
||||
validateEvidence(program, tables, errors, warnings, dslText);
|
||||
validateCrossKindRelationNames(program, errors, warnings, dslText);
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
@@ -335,6 +336,37 @@ function validateEvidence(program, tables, errors, warnings, source) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Relation names must be unique across facts, sources, evidence, and measures.
|
||||
* A fact and an evidence sharing a name would silently overwrite each other's
|
||||
* relation config during generation (and read as a false cyclic reference).
|
||||
*/
|
||||
function validateCrossKindRelationNames(program, errors, warnings, source) {
|
||||
const seen = new Map();
|
||||
const kinds = [
|
||||
['fact', program.facts],
|
||||
['source', program.sources],
|
||||
['evidence', program.evidence],
|
||||
['measure', program.measures]
|
||||
];
|
||||
for (const [kind, items] of kinds) {
|
||||
for (const item of items || []) {
|
||||
const prev = seen.get(item.name);
|
||||
if (prev) {
|
||||
errors.push(createError({
|
||||
message: `Name '${item.name}' is already used by a ${prev} declaration.`,
|
||||
rule: 'Relation names must be unique across facts, sources, evidence, and measures.',
|
||||
fix: `Rename the ${kind} or the ${prev} to a unique name.`,
|
||||
location: findLocation(source, `${kind} ${item.name}`),
|
||||
context: formatContext(source, findLocation(source, item.name))
|
||||
}));
|
||||
} else {
|
||||
seen.set(item.name, kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateEvidenceBody(body, scope, tables, errors, warnings, source, parent) {
|
||||
for (const stmt of body.statements || []) {
|
||||
switch (stmt.type) {
|
||||
|
||||
Reference in New Issue
Block a user