BEHAVES AS transitive now emits bounded multi_hop configs (direct checks and
evidence references), fixing a silent no-op. NOT builds keep _subjectAsObject
scoping so unary predicates negate the right node, and value-typed evidence
objects gate by exact edge value. Recompiling a scope uninstalls its stale
relation configs (compileMultiple coexistence preserved). Sources become
injectable relations honored by requiredFacts with a within-X recency gate.
Duplicate definition fields and three common declaration mistakes (within on a
fact, two BEHAVES clauses, limit on a non-pattern body) now produce targeted
errors. Provider edges referencing unknown nodes are warned and dropped.
An evidence whose config contains a chain step referencing ITSELF is now
unrolled at compile time into a bounded transitive closure: a union of paths
— base, hop+base, hop²+base, …, hop^N+base — where `hop` is the recursive
chain's steps before the self-reference and the depth N comes from the
pattern's `limit N` (or the compiler's maxRecursionDepth default, 3). The
base (the evidence's non-recursive statements) is verified as a condition
step at each path's terminal node, so the engine needs no new machinery.
- Chain configs carry the pattern's `limit` as maxDepth.
- resolveEvidenceReferences detects a self-reference (_findSelfReference),
extracts the base (_extractBase), and unrolls (_unrollRecursiveEvidence).
- Pure recursion with no base case is a compile-time error; mutual cycles
between distinct evidence remain a compile-time error.
Example: can_access_via = can_access OR (reports_to + can_access_via) up to
the declared limit grants access inherited up a reporting chain.
Tests: Recursion (unroll shape, base + multi-hop grants, depth-limit
enforcement, default depth, pure-recursion error, mutual-cycle guard).
- check() gains cacheProviderResults (per-check option + policy default): the
provider cache is a STORE-RETRIEVAL cache (wall-clock), independent of the
caller's decision { now }; callers who pin time or want fresh retrieval can
opt out per-check or globally.
- The generator now rejects a comparator evidence referenced at a NON-final
chain position (a comparator compares values at (src, candidate) but
provides no candidate set, so it cannot enumerate intermediate nodes).
Final-position comparators still lower to condition steps.
Tests: cache bypass (per-check + policy), comparator final OK / intermediate
error.
- _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)).
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.
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.
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.
Lowering fixes (validate/lower/compile into known-correct core structures):
- tuple_to_userset: structural classification by object-side predicate
(owner(*g, doc) { member_of(user, g) } -> tuple_to_userset with direction
'in'/'out'); the old heuristic routed every outer-wildcard to chain.
- relational_comparator: operands now lower to real direct-rule configs
(evaluateFrom derived from evidence param positions; expectedValue for
literal args) instead of raw AST nodes the engine could not evaluate.
- defeasible: multi-level bodies (NEVER/REQUIRES/ALWAYS/WHEN/UNLESS) merge
into one five-level rule instead of ANDed level-only rules that always
resolved 0; nested PatternMatches flatten to N-step chains; unary predicate
calls mark _subjectAsObject (subject-as-object semantics).
- validation: reject duplicate fact/evidence definitions.
DSLRuntime (higher-order DSL+Core wrapper):
- typed addNode/updateNodeData/addRelation/updateRelation against the DSL
schema (known types, relation params, field types, value-carrying facts);
- check() derives the evidence's injectable partial-graph requirements,
retrieves missing facts through caller data callbacks, injects them, and
delegates, returning requiredFacts/providedFacts/missingFacts.
js-rigor campaigns:
- generative oracle: generate legal DSL per construct and compare every
verdict against an independent hand-computed oracle (8 constructs x P grid)
plus an exhaustive deterministic sweep;
- illegal mutations: one-flaw perturbations of a valid program must be
reliably rejected (duplicate evidence/fact, arity/type mismatches, reserved
built-ins, malformed syntax), with a control that must compile.
Depends on @arbiter/core@^1.0.2 (reason codes + _subjectAsObject).
The Evidence DSL (ADR-000) is a thin declarative layer that compiles to
engine rule types. It has zero runtime coupling to the core engine
(DSLCompiler takes an arbiter as a duck-typed argument; the only shared
code was the ip-utils helpers, now local). Extracting it into its own
package keeps the core artifact free of the DSL surface.
- @arbiter/evidence-dsl depends on @arbiter/core (config formats are the
compilation target)
- deep-path exports for the compiler, parser, generator, validation,
and built-in functions (the surface the core's DSL tests consume)
- tests moved alongside; generate-parser script + peggy devDep local
- CI: test on push, publish on v* tags