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.
Three improvements over the complex-graph crucibles:
1. Anti-vacuity guards (assertRealVerdict): rigor's complexity verdict
PASSES on zero observations — a broken action (missing import, wrong
args shape) silently goes green. Every complexity verdict now asserts
observationCount >= 50, costSource == expected, and calibrated ==
true, so a vacuous verdict is a test failure.
2. Snapshot complexity: serialized snapshot BYTE SIZE is O(n) in graph
size, verified deterministically (build and restore round-trip).
Wall-clock timing at sub-ms scale is pure jitter for the e-process
spread check (verified empirically — buildTime O(n) failed on spread
while buildBytes passed); latency stays covered by benchmark
percentiles.
3. complex-graph-mutation-crucible: MUTATION-FRESHNESS — random edge
removals/additions on community + scale-free graphs, normal/binary
agreement re-checked after EVERY mutation, stale grants and missing
fresh grants are failures. Two real findings during bring-up, both
fixture bugs rather than engine bugs:
- the scale-free generator returned a raw edge COUNT as 'relations'
while other generators returned edge arrays (now null, consistent
with dense-adversarial; the crucible walks the arbiter's store)
- arbiter.relations stores NUMERIC ids, so removals must resolve
string keys to ids before matching (string-key comparison silently
no-oped, looking like a stale grant)
Snapshot read-only semantics documented in the crucible: enableCondensed-
Snapshot flips the engine to read-only permanently, so mutation crucibles
exercise the writable path, and frozen-snapshot properties stay with the
snapshot-parity suites. Rigor 239/239, full suite 841/779/0.