- ChainRule._expandRuleFromSrc now expands tuple_to_userset configs: from a
source node the reachable set is the objects sharing an intermediate with
the source (src ->computed-> intermediate ->tupleset-> object, direction
aware, weakest-link combined). Lets a TTU evidence serve as an intermediate
condition step in a chain.
- scripts/benchmark.js: ratio-based self-calibration. Comparing each action's
RATIO to a cheap reference action (default check[direct-hit]) cancels
machine-load swings that scale all actions proportionally, so the gate only
fails on code regressions that shift a single action's ratio. The reference
is still checked absolutely with a loose bound. Verified: stable across
runs, and a simulated union-ttu slowdown is caught (+76.8% ratio).
Tests: chain-condition-step intermediate TTU expansion.
Chain intermediates (rule-based reachability):
- ChainRule: a condition step ({ rule, conditionStep }) at an INTERMEDIATE
position is now EXPANDED from the current node — the rule's base edges'
destinations, filtered by its defeaters/requirements — and traversal
continues from each discovered node. Adds _expandRuleFromSrc / direct /
logical(union/intersection) / defeasible / nested-chain expansion.
- RuleEvaluator: _subjectIsObject flag for unary predicate calls whose subject
entity IS the object parameter (trusted(other) inside peer_trusted(user,
other)); previously only subject-var unary calls (_subjectAsObject) were
handled, so object-var unary defeaters never fired.
Graph-version cache invalidation:
- Arbiter gains a monotonic _graphVersion, incremented on every relation
mutation. ChainRule result cache, RuleEvaluator rule-result cache, and
DecisionCache rule cache now stamp entries with the graph version and treat
any mismatch as a miss — graph mutations can no longer serve stale
chain/authorization results.
Rolling-hash cache keys:
- UnifiedKeyManager.createChainKey now builds a 53-bit rolling hash (dual
FNV-1a lanes, exact for ints/floats/strings/nested configs) instead of
JSON.stringify — no string allocation or serialization on the chain-cache
hot path. Composite keys stay structured strings because the direct-check
cache pattern-invalidates by relation ID.
Rigor invariant migration (correctness):
- All 43 rigor test files' throw-based invariants ({ error, errorMessage } =>
!error && !errorMessage) never saw fn throws — vacuous. Migrated to
({ actual }) => actual !== undefined, which fails on any thrown violation
while passing legitimate null-skips. The migration immediately surfaced
two latent bugs, now fixed:
* node-manager/graph-indices skip paths returned bare undefined (falsy
sentinel) — return { skipped: true }.
* complex-graph-values-crucible expiry section rewrote values equal to the
mutation loop's last write; the engine (by design) keeps the old
timestamp on same-value rewrites so the pre-expiry grant never
materialized. Now writes guaranteed-different values.
A chain step of the form { rule: <config>, conditionStep: true } is a
condition-gated hop instead of an edge traversal. It is valid only as the
FINAL step: the object is known, so the engine verifies the referenced rule
at (intermediate, object) for each current path. Enables a chain's object-side
hop to reference a defeasible/logical evidence (e.g.
member_of(user,*g){ gated(g,doc) } where gated is WHEN/UNLESS).
- RuleEvaluator wires the ChainRule with itself so condition steps can
evaluate sub-rules recursively.
- ChainRule constructor accepts the ruleEvaluator; the traversal loop handles
a final condition step (min-combined possibility, reliability product,
threshold respect). Non-final condition steps return condition_step_not_final.
- The rule config is part of the chain cache key (JSON.stringify of steps).
Tests: chain-condition-step (grant/deny/missing-edge/non-final/parallel-max).