Completes the rolling-hash rollout (previously only createChainKey hashed):
- createCompositeKey, createSrcRelKey, createDstRelKey, and the
valueRelationsBySrc/Dst keys now produce 53-bit rolling hashes instead of
`src|rel|dst` string concatenation. Direction markers keep srcRel vs dstRel
distinct; the composite/chain keys are exact integers usable as Map keys.
- Direct-check cache invalidation is now key-TRACKED instead of pattern-
matched: every direct-check result is registered under the checked relation,
its base relations (reverse dependency index), and the subject/object node
ids (Arbiter._trackDirectCheckKey). Relation-level invalidation deletes the
tracked keys for each affected relation (covering config-override checks);
node-level invalidation (node removal / updateNodeData) deletes by node id.
This replaces the pipe-delimited-string regex matching that required the old
key format.
- DecisionCache.invalidateByNodeKey and invalidateAll route through the tracked
indexes; tracking maps are cleared on full flush.
Tests updated to the tracked contract (register injected keys via
_trackDirectCheckKey); full suite green.
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.
The principle: time is caller-provided (options.now / partialGraph.now);
the wall clock is only the fallback for unpinned callers, never a hidden
decision input. Remaining clock leaks:
- getBlurredValue gained an optional now param threaded to _isValueExpired;
ChainRule (2 sites), MultiHopRule (2 sites), RelationManager (3 sites)
now pass the caller clock. Previously a pinned-clock caller's chain/
multi-hop value TTL used the WALL clock (wall in 2026, pinned T0 in
2001 -> values wrongly expired).
- MultiHopRule's TTL gate used valueFilters.ttl || 24h instead of the
valueManager's per-relation TTL (inconsistent with chain/comparator);
now valueManager.getTTL is the authority, valueFilters.ttl the override.
- QualitativeRelationalComparatorRule decay (_calculatePeriodsElapsed)
and value timestamps used the wall clock, so qualitative possibility
decay ignored the pinned clock; now threaded through _evaluateOperand.
- ValueManager decay internals (getDecayedRelation, _calculateSeparated
Decay, _calculateBlurredValue) accept a now param (background worker
still passes none -> wall clock is correct there).
- PartialGraphContext._addChallengeProof/_addRelation used Date.now()
instead of the context's own this.now (the partial graph's time).
- Arbiter gained an injectable clock (options.clock) driving unpinned
cache-entry freshness in DecisionCache, RuleEvaluator, ChainRule, and
RelationalComparatorRule; DecisionCache explicit clock still wins.
- Collected-value timestamps in DirectRule and RelationalComparatorRule
honor the caller clock.
Pinned-clock chain probe: values fresh at T0, expired at T0+61s, with the
wall clock in 2026. Rigor 251/251, full suite 853/791/0.