feat: chain condition steps — defeasible/logical evidence as final chain hop
CI / publish (push) Successful in 9s
CI / test (push) Successful in 18s

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.
This commit is contained in:
John Dvorak
2026-08-03 12:08:54 -07:00
parent 2dc478f5a3
commit 351551af0f
9 changed files with 178 additions and 31 deletions
+17 -5
View File
@@ -155,6 +155,7 @@ export class RuleGenerator {
for (const step of rule.steps) {
if (typeof step === 'string') targetSet.add(step);
else if (step && typeof step.relation === 'string') targetSet.add(step.relation);
else if (step && step.rule) collect(step.rule, targetSet);
}
}
if (rule.type === 'relational_comparator') {
@@ -1070,13 +1071,18 @@ export class RuleGenerator {
* - 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.
* - logical / defeasible / comparator evidence → only expressible as a
* FINAL condition-gated step (the object is known, so the engine can
* verify the condition at (intermediate, object) instead of traversing
* an edge). Emitted as a `{ rule: <config> }` step the ChainRule
* evaluates as a condition hop. Non-final such steps are a compile
* error: a condition cannot discover intermediate nodes.
*/
_expandChainSteps(steps, stack) {
const out = [];
for (const step of steps) {
for (let idx = 0; idx < steps.length; idx++) {
const step = steps[idx];
const isLast = idx === steps.length - 1;
const stepName = typeof step === 'string' ? step : step.relation;
if (stepName && this.evidenceNames.has(stepName)) {
if (stack.has(stepName)) {
@@ -1099,8 +1105,14 @@ export class RuleGenerator {
out.push(...this._expandChainSteps(resolved.steps, refStack));
continue;
}
if (isLast) {
// Condition-gated final hop: inline the evidence's config as a
// rule step the engine evaluates at (intermediate, object).
out.push({ rule: this._deepCloneRule(resolved), conditionStep: true });
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.');
'Only the final chain step may reference a defeasible/logical evidence (a condition-gated hop); intermediate steps must be edge traversals.');
out.push(step);
continue;
}
+5 -1
View File
@@ -103,7 +103,11 @@ export class DSLRuntime {
if (rule.computedRelation) deps.add(rule.computedRelation);
}
if (rule.type === 'chain' && Array.isArray(rule.steps)) {
for (const s of rule.steps) deps.add(typeof s === 'string' ? s : s.relation);
for (const s of rule.steps) {
if (typeof s === 'string') deps.add(s);
else if (s && s.relation) deps.add(s.relation);
else if (s && s.rule) collect(s.rule);
}
}
if (rule.type === 'parent' && rule.parentRelation) deps.add(rule.parentRelation);
if (rule.type === 'multi_hop' && rule.relation) deps.add(rule.relation);