feat: intermediate chain condition steps, graph-version cache invalidation, rolling-hash chain keys; fix vacuous rigor invariants
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.
This commit is contained in:
@@ -81,14 +81,29 @@ describe('ChainRule condition step (rule-based final hop)', () => {
|
||||
assert.equal(res.reason, 'no_chain_path_found');
|
||||
});
|
||||
|
||||
it('rejects a condition step that is not the final step', () => {
|
||||
it('expands an intermediate condition step (rule-based reachability)', () => {
|
||||
arbiter.setRelationConfig('member_of', { type: 'direct' });
|
||||
arbiter.addNode('group:g', 'group');
|
||||
arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
|
||||
arbiter.addRelation('group:g', 'can_view', 'doc:d', { possibility: 0.8 });
|
||||
// [condition(member_of unless banned), can_view] — the condition step is
|
||||
// INTERMEDIATE and discovers its reachable nodes (its base relation's
|
||||
// neighbors from the source, filtered by its defeater).
|
||||
const intermediateConfig = {
|
||||
type: 'logical',
|
||||
when: { intersection: { rules: [{ type: 'direct', relation: 'member_of' }], aggregator: 'min' } },
|
||||
unless: { union: { rules: [{ type: 'direct', relation: 'banned', _subjectIsObject: true }], aggregator: 'max' } }
|
||||
};
|
||||
const rule = {
|
||||
type: 'chain',
|
||||
steps: [{ rule: CONDITION_CONFIG, conditionStep: true }, 'can_view']
|
||||
steps: [{ rule: intermediateConfig, conditionStep: true }, 'can_view']
|
||||
};
|
||||
const res = evalRule('user:u', 'doc:d', rule);
|
||||
assert.equal(res.possibility, 0);
|
||||
assert.equal(res.reason, 'condition_step_not_final');
|
||||
// g not banned → reachable via condition → can_view → doc
|
||||
assert.ok(Math.abs(evalRule('user:u', 'doc:d', rule).possibility - 0.8) < 1e-9);
|
||||
// banning g filters it out of the intermediate expansion → no path
|
||||
arbiter.addRelation('group:g', 'banned', 'group:g', { possibility: 1.0 });
|
||||
const denied = evalRule('user:u', 'doc:d', rule);
|
||||
assert.equal(denied.possibility, 0);
|
||||
});
|
||||
|
||||
it('combines across multiple parallel intermediates (max aggregation)', () => {
|
||||
|
||||
Reference in New Issue
Block a user