feat: intermediate chain condition steps + _subjectIsObject unary scoping
CI / publish (push) Successful in 10s
CI / test (push) Successful in 19s

- _expandChainSteps: a logical/defeasible evidence referenced by a chain step
  is now a condition step ({ rule, conditionStep }) at ANY position. As the
  FINAL step the engine verifies it at (intermediate, object); as an
  INTERMEDIATE step the engine expands it from the current node (rule-based
  reachability: base edges' destinations filtered by the rule's
  defeaters/requirements) and continues traversal from each discovered node.
- buildPredicateRule / buildDirectRule: unary predicate calls whose subject
  entity IS the evidence's object parameter (trusted(other) inside
  peer_trusted(user, other)) are marked _subjectIsObject (was: only subject-var
  calls got _subjectAsObject). Requires @arbiter/core@^1.0.4.

Tests: ChainConditionStep intermediate expansion; oracle campaign gains a
chain_intermediate_condition construct (oracle = min(peer*(1-trusted), read)).
This commit is contained in:
John Dvorak
2026-08-03 13:36:16 -07:00
parent fe162251fc
commit 3ace783a59
5 changed files with 94 additions and 32 deletions
+31 -22
View File
@@ -486,14 +486,21 @@ export class RuleGenerator {
reverse: false
};
// Subject-scoped (unary) call: the predicate call's args omit the
// evidence's object parameter (user_risk(user, 1) inside a binary
// evidence) → check the relation on the subject itself.
// Unary predicate calls check the relation as a self-edge on the call's
// subject entity (the graph stores unary facts as self-edges). The subject
// entity may be the evidence's SUBJECT or its OBJECT parameter — mark the
// matching rewrite flag.
const evidenceParams = (evidence && evidence.params) || [];
const objectVar = evidenceParams[1] && evidenceParams[1].name;
const argName = a => a && (a.name !== undefined ? a.name : a.value);
if (objectVar !== undefined && !(predicate.arguments || []).some(a => argName(a) === objectVar)) {
rule._subjectAsObject = true;
const args = predicate.arguments || [];
if (objectVar !== undefined) {
const hasObjectArg = args.some(a => argName(a) === objectVar);
if (args.length === 1 && argName(args[0]) === objectVar) {
rule._subjectIsObject = true;
} else if (!hasObjectArg) {
rule._subjectAsObject = true;
}
}
return rule;
@@ -1082,7 +1089,6 @@ export class RuleGenerator {
const out = [];
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)) {
@@ -1105,15 +1111,11 @@ 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'}'. ` +
'Only the final chain step may reference a defeasible/logical evidence (a condition-gated hop); intermediate steps must be edge traversals.');
out.push(step);
// Condition step: inline the evidence's config as a rule step. As the
// FINAL step the engine verifies it at (intermediate, object); as an
// INTERMEDIATE step the engine EXPANDS it from the current node
// (rule-based reachability) and continues from each discovered node.
out.push({ rule: this._deepCloneRule(resolved), conditionStep: true });
continue;
}
}
@@ -1280,16 +1282,23 @@ export class RuleGenerator {
reverse: false
};
// Subject-scoped (unary) predicate call: the call's variable args omit the
// evidence's object parameter (banned(user) inside can_open(user, doc)).
// Mark _subjectAsObject so the engine checks the relation on the subject
// itself — the unary fact's self-edge — instead of (subject, object).
// Unary predicate calls check the relation as a self-edge on the call's
// subject entity (the graph stores unary facts as self-edges). The subject
// entity may be the evidence's SUBJECT or its OBJECT parameter:
// banned(user) in can_open(user, doc) -> self-edge on the user
// trusted(other) in peer_trusted(user, other) -> self-edge on the other
// Mark _subjectAsObject (subject-as-object on the subject entity) or
// _subjectIsObject (the subject entity IS the object parameter) so the
// engine rewrites the pair accordingly.
const evidenceParams = (evidence && evidence.params) || [];
const objectVar = evidenceParams[1] && evidenceParams[1].name;
const argName = a => a && (a.name !== undefined ? a.name : a.value);
if (objectVar !== undefined) {
const hasObjectArg = (expression.args || []).some(a =>
a && a.type === 'Variable' && a.name === objectVar);
if (!hasObjectArg) {
const args = expression.args || [];
const hasObjectArg = args.some(a => a && a.type === 'Variable' && a.name === objectVar);
if (args.length === 1 && argName(args[0]) === objectVar) {
rule._subjectIsObject = true;
} else if (!hasObjectArg) {
rule._subjectAsObject = true;
}
}