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.
This commit is contained in:
@@ -1054,6 +1054,59 @@ export class RuleGenerator {
|
||||
if (out.left?.rule) out.left = { ...out.left, rule: this._resolveRule(out.left.rule, stack) };
|
||||
if (out.right?.rule) out.right = { ...out.right, rule: this._resolveRule(out.right.rule, stack) };
|
||||
}
|
||||
// Chain steps may reference a derived evidence; expand those steps
|
||||
// (direct evidence → underlying relation, chain evidence → spliced steps).
|
||||
if (rule.type === 'chain' && Array.isArray(out.steps)) {
|
||||
out.steps = this._expandChainSteps(out.steps, stack);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand chain steps that reference a derived evidence:
|
||||
* - direct evidence → rename the step to the underlying relation
|
||||
* (member_of(user,*g){ group_read(g,doc) } where group_read = can_view
|
||||
* becomes step 'can_view');
|
||||
* - chain evidence → splice its steps into this chain (flattening)
|
||||
* (a step that is itself a sub-path becomes its steps, preserving the
|
||||
* linear source→…→object traversal);
|
||||
* - anything else (defeasible/logical/comparator) → compile error: such a
|
||||
* step is a condition, not an edge traversal, and cannot lower to a flat
|
||||
* chain step.
|
||||
*/
|
||||
_expandChainSteps(steps, stack) {
|
||||
const out = [];
|
||||
for (const step of steps) {
|
||||
const stepName = typeof step === 'string' ? step : step.relation;
|
||||
if (stepName && this.evidenceNames.has(stepName)) {
|
||||
if (stack.has(stepName)) {
|
||||
this.errors.push(`Cyclic evidence reference involving '${stepName}'. Evidence composition must be acyclic.`);
|
||||
out.push(step);
|
||||
continue;
|
||||
}
|
||||
const referencedConfig = this.generatedRules.get(stepName);
|
||||
if (referencedConfig) {
|
||||
const refStack = new Set(stack);
|
||||
refStack.add(stepName);
|
||||
const resolved = this._resolveRule(referencedConfig, refStack);
|
||||
if (resolved.type === 'direct' && resolved.relation && resolved.relation !== stepName) {
|
||||
out.push(typeof step === 'string'
|
||||
? resolved.relation
|
||||
: { ...step, relation: resolved.relation });
|
||||
continue;
|
||||
}
|
||||
if (resolved.type === 'chain' && Array.isArray(resolved.steps)) {
|
||||
out.push(...this._expandChainSteps(resolved.steps, refStack));
|
||||
continue;
|
||||
}
|
||||
this.errors.push(`Chain step '${stepName}' references an evidence with type '${resolved.type || 'logical'}'. ` +
|
||||
'Chain steps can only reference facts, direct evidence, or chain evidence.');
|
||||
out.push(step);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(step);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user