2026-07-31 13:44:06 -07:00
|
|
|
import { DirectRule } from './rules/DirectRule.js';
|
|
|
|
|
import { ComputedRule } from './rules/ComputedRule.js';
|
|
|
|
|
import { ParentRule } from './rules/ParentRule.js';
|
|
|
|
|
import { TupleToUsersetRule } from './rules/TupleToUsersetRule.js';
|
|
|
|
|
import { MultiHopRule } from './rules/MultiHopRule.js';
|
|
|
|
|
import { LogicalOperators } from './rules/LogicalOperators.js';
|
|
|
|
|
import { RelationalComparatorRouter } from './rules/RelationalComparatorRouter.js';
|
|
|
|
|
import { ChainRule } from './rules/ChainRule.js';
|
|
|
|
|
import { ChallengeRule } from './rules/ChallengeRule.js';
|
|
|
|
|
import { ValueContext } from './ValueContext.js';
|
|
|
|
|
import { CompiledEvaluator } from './CompiledEvaluator.js';
|
|
|
|
|
|
|
|
|
|
export class RuleEvaluator {
|
|
|
|
|
constructor(arbiter) {
|
|
|
|
|
this.arbiter = arbiter;
|
|
|
|
|
|
|
|
|
|
this.ruleHandlers = {
|
|
|
|
|
direct: new DirectRule(arbiter),
|
|
|
|
|
computed: new ComputedRule(arbiter),
|
|
|
|
|
parent: new ParentRule(arbiter),
|
|
|
|
|
tuple_to_userset: new TupleToUsersetRule(arbiter),
|
|
|
|
|
multi_hop: new MultiHopRule(arbiter),
|
|
|
|
|
relational_comparator: new RelationalComparatorRouter(arbiter, this),
|
|
|
|
|
chain: new ChainRule(arbiter),
|
|
|
|
|
challenge: new ChallengeRule(arbiter)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.logicalOperators = new LogicalOperators(arbiter, this);
|
|
|
|
|
this.compiledEvaluator = new CompiledEvaluator(arbiter, this, this.logicalOperators);
|
|
|
|
|
this._valueRequirementCache = new Map();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
|
|
|
|
|
const { binary = false, valueContext = null, includeMeta = true } = options;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Convert string keys to numeric IDs if needed
|
|
|
|
|
const numericUserId = typeof userId === 'string' ? this.arbiter.resolveNodeId(userId, options) : userId;
|
|
|
|
|
const numericObjectId = typeof objectId === 'string' ? this.arbiter.resolveNodeId(objectId, options) : objectId;
|
|
|
|
|
|
|
|
|
|
const needsValueContext = valueContext !== null && valueContext !== undefined
|
|
|
|
|
? true
|
|
|
|
|
: (options.collectValues === true || this._ruleRequiresValues(rule, new Set()));
|
|
|
|
|
const finalValueContext = needsValueContext ? (valueContext || new ValueContext(this.arbiter, options)) : null;
|
|
|
|
|
const collectValues = options.collectValues !== undefined ? options.collectValues : (rule._needsValues ?? needsValueContext);
|
|
|
|
|
const enhancedOptions = { ...options, minPossibility: options.minPossibility ?? options.minAllowPossibility, valueContext: finalValueContext, collectValues, includeMeta };
|
|
|
|
|
|
|
|
|
|
const canCacheRuleResult = !!this.arbiter.ruleResultCache && currentRelation &&
|
|
|
|
|
!binary && !options.partialGraphContext && !includeMeta &&
|
|
|
|
|
options.cacheRuleResult !== false;
|
|
|
|
|
const ruleCacheKey = canCacheRuleResult
|
|
|
|
|
? this._getRuleResultCacheKey(numericUserId, currentRelation, numericObjectId, rule)
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
if (canCacheRuleResult) {
|
|
|
|
|
const cached = this.arbiter.ruleResultCache.get(ruleCacheKey);
|
|
|
|
|
if (cached && Date.now() - cached.timestamp < this.arbiter.ruleResultCacheTTL) {
|
|
|
|
|
this.arbiter.ruleResultCacheStats.hits++;
|
|
|
|
|
if (collectValues && finalValueContext && cached.result?.collectedValues?.length) {
|
|
|
|
|
const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
|
|
|
|
|
finalValueContext.addCollectedValues(cached.result.collectedValues, ruleType, rule);
|
|
|
|
|
}
|
|
|
|
|
return cached.result;
|
|
|
|
|
}
|
|
|
|
|
this.arbiter.ruleResultCacheStats.misses++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BINARY MODE: Fast evaluation with early termination
|
|
|
|
|
if (binary) {
|
|
|
|
|
return this._evaluateRuleBinary(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rule && rule._compiled && options.useCompiled !== false && !(rule._compileErrors && rule._compileErrors.length)) {
|
|
|
|
|
const result = this.compiledEvaluator.evaluate(rule._compiled, userId, userKey, objectId, objectKey, visited, currentRelation, enhancedOptions);
|
|
|
|
|
if (finalValueContext && result.collectedValues && result.collectedValues.length > 0) {
|
|
|
|
|
const ruleType = rule?.type || (rule?.union ? 'union' : rule?.intersection ? 'intersection' : rule?.exclusion ? 'exclusion' : 'rule');
|
|
|
|
|
finalValueContext.addCollectedValues(result.collectedValues, ruleType, rule);
|
|
|
|
|
}
|
|
|
|
|
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rule.union) {
|
|
|
|
|
const result = this.logicalOperators.evaluateUnion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
|
|
|
|
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rule.intersection) {
|
|
|
|
|
const result = this.logicalOperators.evaluateIntersection(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
|
|
|
|
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rule.exclusion) {
|
|
|
|
|
const result = this.logicalOperators.evaluateExclusion(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
|
|
|
|
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For other rule types, evaluate normally first
|
2026-08-02 08:14:39 -07:00
|
|
|
// Shorthand operand objects ({ relation: 'owner' } inside logical rules,
|
|
|
|
|
// or caller-supplied raw configs) carry no type: treat them as direct
|
|
|
|
|
// rules instead of failing with unknown_rule_type.
|
|
|
|
|
const handler = this.ruleHandlers[rule.type || (rule.relation || rule.rel || rule.label || rule.name ? 'direct' : null)];
|
2026-07-31 13:44:06 -07:00
|
|
|
if (!handler) {
|
|
|
|
|
return {
|
|
|
|
|
possibility_allow: 0,
|
|
|
|
|
possibility_deny: 0,
|
|
|
|
|
...(collectValues && { collectedValues: [] }),
|
|
|
|
|
...(includeMeta && { meta_allow: null, meta_deny: null }),
|
|
|
|
|
reason: 'unknown_rule_type'
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const result = handler.evaluate(numericUserId, userKey, numericObjectId, objectKey, rule, visited, currentRelation, enhancedOptions);
|
|
|
|
|
|
|
|
|
|
// Add collected values to value context
|
|
|
|
|
if (finalValueContext && result.collectedValues && result.collectedValues.length > 0) {
|
|
|
|
|
finalValueContext.addCollectedValues(result.collectedValues, rule.type, rule);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Return original result if inference didn't help or values weren't sufficient
|
|
|
|
|
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_getRuleResultCacheKey(userId, relation, objectId, rule) {
|
|
|
|
|
const base = this.arbiter.keyManager.createCompositeKey(userId, relation, objectId);
|
|
|
|
|
let suffix = 'rule';
|
|
|
|
|
if (rule) {
|
|
|
|
|
if (rule.union || rule.intersection || rule.exclusion) {
|
|
|
|
|
suffix = 'logical';
|
2026-08-02 08:14:39 -07:00
|
|
|
} else if (rule.type === 'direct' || (!rule.type && (rule.relation || rule.rel || rule.label || rule.name) && !rule.union && !rule.intersection && !rule.exclusion)) {
|
|
|
|
|
// Shorthand operands ({ relation: 'editor' }) dispatch to the direct
|
|
|
|
|
// handler but carry no type; without this, every shorthand child of
|
|
|
|
|
// a logical rule shares one cache key and the first child's result
|
|
|
|
|
// is served for all of them.
|
|
|
|
|
suffix = `direct:${rule.relation || rule.rel || rule.label || rule.name || 'unknown'}`;
|
2026-07-31 13:44:06 -07:00
|
|
|
} else if (rule.type === 'tuple_to_userset') {
|
|
|
|
|
suffix = `tupleset:${rule.tuplesetRelation || 'unknown'}:${rule.computedRelation || 'unknown'}`;
|
|
|
|
|
} else if (rule.type === 'chain' && Array.isArray(rule.steps)) {
|
|
|
|
|
const stepSig = rule.steps.map(step => {
|
|
|
|
|
if (typeof step === 'string') return step;
|
|
|
|
|
return `${step.relation || 'unknown'}:${step.direction || 'out'}`;
|
|
|
|
|
}).join('>');
|
|
|
|
|
suffix = `chain:${stepSig}`;
|
|
|
|
|
} else if (rule.type === 'multi_hop') {
|
|
|
|
|
suffix = `multihop:${rule.relation || 'unknown'}:${rule.maxDepth || 'auto'}`;
|
|
|
|
|
} else if (rule.type === 'parent') {
|
|
|
|
|
suffix = `parent:${rule.parentRelation || 'unknown'}`;
|
|
|
|
|
} else if (rule.type === 'relational_comparator') {
|
|
|
|
|
const signature = this._getComparatorCacheSignature(rule);
|
|
|
|
|
suffix = `comparator:${rule.comparator || 'unknown'}:${signature}`;
|
|
|
|
|
} else {
|
|
|
|
|
suffix = rule.type || 'rule';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return `${base}|${suffix}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_maybeCacheRuleResult(result, relation, cacheKey, enabled) {
|
|
|
|
|
if (!enabled || !cacheKey || !relation) return result;
|
|
|
|
|
this.arbiter.ruleResultCache.set(cacheKey, {
|
|
|
|
|
result,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
});
|
|
|
|
|
this.arbiter._cacheRuleResult(relation, cacheKey);
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_getComparatorCacheSignature(rule) {
|
|
|
|
|
const leftSig = this._getComparatorOperandSignature(rule.left || rule.leftOperand);
|
|
|
|
|
const rightSig = this._getComparatorOperandSignature(rule.right || rule.rightOperand);
|
|
|
|
|
return `L(${leftSig})|R(${rightSig})`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_getComparatorOperandSignature(operand) {
|
|
|
|
|
if (!operand || typeof operand !== 'object') return 'none';
|
|
|
|
|
const valueRelation = operand.valueRelation || operand.valueRelationResolved;
|
|
|
|
|
if (valueRelation) return `relation:${valueRelation}`;
|
|
|
|
|
|
|
|
|
|
const nestedRule = operand.rule || null;
|
|
|
|
|
if (!nestedRule || typeof nestedRule !== 'object') return 'value';
|
|
|
|
|
|
|
|
|
|
if (nestedRule.type === 'direct') {
|
|
|
|
|
const rel = nestedRule.relation || nestedRule.rel || nestedRule.label || nestedRule.name || 'unknown';
|
|
|
|
|
return `${nestedRule.reverse ? 'rev:' : ''}${rel}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nestedRule.type === 'logical') {
|
|
|
|
|
if (nestedRule._optimized?.kind === 'direct_list') {
|
|
|
|
|
const rels = nestedRule._optimized.direct
|
|
|
|
|
.map((direct) => `${direct.reverse ? 'rev:' : ''}${direct.relation}`)
|
|
|
|
|
.join(',');
|
|
|
|
|
return `direct_list:${nestedRule.op || 'logical'}:${rels}`;
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(nestedRule.children)) {
|
|
|
|
|
const childKeys = nestedRule.children.map((child) => {
|
|
|
|
|
if (child?.type === 'direct') {
|
|
|
|
|
return `${child.reverse ? 'rev:' : ''}${child.relation}`;
|
|
|
|
|
}
|
|
|
|
|
return child?.type || 'unknown';
|
|
|
|
|
}).join(',');
|
|
|
|
|
return `logical:${nestedRule.op || 'logical'}:${childKeys}`;
|
|
|
|
|
}
|
|
|
|
|
return `logical:${nestedRule.op || 'logical'}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nestedRule.union) {
|
|
|
|
|
const rules = Array.isArray(nestedRule.union.rules) ? nestedRule.union.rules : (Array.isArray(nestedRule.union) ? nestedRule.union : []);
|
|
|
|
|
const rels = rules.map((child) => {
|
|
|
|
|
if (child?.type === 'direct') {
|
|
|
|
|
const rel = child.relation || child.rel || child.label || child.name || 'unknown';
|
|
|
|
|
return `${child.reverse ? 'rev:' : ''}${rel}`;
|
|
|
|
|
}
|
|
|
|
|
return child?.type || 'unknown';
|
|
|
|
|
}).join(',');
|
|
|
|
|
return `logical:union:${rels}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nestedRule.intersection) {
|
|
|
|
|
const rules = Array.isArray(nestedRule.intersection.rules) ? nestedRule.intersection.rules : (Array.isArray(nestedRule.intersection) ? nestedRule.intersection : []);
|
|
|
|
|
const rels = rules.map((child) => {
|
|
|
|
|
if (child?.type === 'direct') {
|
|
|
|
|
const rel = child.relation || child.rel || child.label || child.name || 'unknown';
|
|
|
|
|
return `${child.reverse ? 'rev:' : ''}${rel}`;
|
|
|
|
|
}
|
|
|
|
|
return child?.type || 'unknown';
|
|
|
|
|
}).join(',');
|
|
|
|
|
return `logical:intersection:${rels}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (nestedRule.exclusion) {
|
|
|
|
|
const rules = Array.isArray(nestedRule.exclusion.rules) ? nestedRule.exclusion.rules : (Array.isArray(nestedRule.exclusion) ? nestedRule.exclusion : []);
|
|
|
|
|
const rels = rules.map((child) => {
|
|
|
|
|
if (child?.type === 'direct') {
|
|
|
|
|
const rel = child.relation || child.rel || child.label || child.name || 'unknown';
|
|
|
|
|
return `${child.reverse ? 'rev:' : ''}${rel}`;
|
|
|
|
|
}
|
|
|
|
|
return child?.type || 'unknown';
|
|
|
|
|
}).join(',');
|
|
|
|
|
return `logical:exclusion:${rels}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nestedRule.type || 'value';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Binary mode rule evaluation: Ultra-fast with strict thresholds and value context
|
|
|
|
|
*/
|
|
|
|
|
_evaluateRuleBinary(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
|
|
|
|
|
const {
|
|
|
|
|
minAllowPossibility = 0.8,
|
|
|
|
|
maxDenyPossibility = 0.8,
|
|
|
|
|
fastPath = true,
|
|
|
|
|
valueContext = null,
|
|
|
|
|
includeMeta = true
|
|
|
|
|
} = options;
|
|
|
|
|
|
|
|
|
|
const needsValueContext = valueContext !== null && valueContext !== undefined
|
|
|
|
|
? true
|
|
|
|
|
: (options.collectValues === true || this._ruleRequiresValues(rule, new Set()));
|
|
|
|
|
const finalValueContext = needsValueContext ? (valueContext || new ValueContext(this.arbiter, options)) : null;
|
|
|
|
|
const collectValues = options.collectValues !== undefined ? options.collectValues : (rule._needsValues ?? needsValueContext);
|
|
|
|
|
const enhancedOptions = { ...options, minPossibility: options.minPossibility ?? options.minAllowPossibility, valueContext: finalValueContext, collectValues, includeMeta };
|
|
|
|
|
|
|
|
|
|
// Handle logical operators in binary mode
|
|
|
|
|
if (rule.union) {
|
|
|
|
|
return this.logicalOperators.evaluateUnion(userId, userKey, objectId, objectKey, rule, visited, currentRelation, {
|
|
|
|
|
...enhancedOptions,
|
|
|
|
|
binary: true,
|
|
|
|
|
fastPath: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rule.intersection) {
|
|
|
|
|
return this.logicalOperators.evaluateIntersection(userId, userKey, objectId, objectKey, rule, visited, currentRelation, {
|
|
|
|
|
...enhancedOptions,
|
|
|
|
|
binary: true,
|
|
|
|
|
fastPath: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rule.exclusion) {
|
|
|
|
|
return this.logicalOperators.evaluateExclusion(userId, userKey, objectId, objectKey, rule, visited, currentRelation, {
|
|
|
|
|
...enhancedOptions,
|
|
|
|
|
binary: true,
|
|
|
|
|
fastPath: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Defeasible rules (when/unless/never/always/requires) have no entry in
|
|
|
|
|
// ruleHandlers — normal mode reaches them via the compiled evaluator,
|
|
|
|
|
// but binary mode short-circuits to this method, where a bare handler
|
|
|
|
|
// lookup returned unknown_rule_type (a hard binary deny for every
|
|
|
|
|
// defeasible config). Route them through the defeasible evaluator,
|
|
|
|
|
// which implements binary/threshold modes natively.
|
|
|
|
|
if (this.logicalOperators._isDefeasibleLogic(rule)) {
|
|
|
|
|
if (rule._compiled && !(rule._compileErrors && rule._compileErrors.length)) {
|
|
|
|
|
return this.compiledEvaluator.evaluate(rule._compiled, userId, userKey, objectId, objectKey, visited, currentRelation, {
|
|
|
|
|
...enhancedOptions,
|
|
|
|
|
binary: true,
|
|
|
|
|
fastPath: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapDefeasibleComponents(rule), visited, currentRelation, {
|
|
|
|
|
...enhancedOptions,
|
|
|
|
|
binary: true,
|
|
|
|
|
fastPath: true
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get rule handler
|
|
|
|
|
const handler = this.ruleHandlers[rule.type];
|
|
|
|
|
if (!handler) {
|
|
|
|
|
return {
|
|
|
|
|
possibility_allow: 0,
|
|
|
|
|
possibility_deny: 0,
|
|
|
|
|
...(collectValues && { collectedValues: [] }),
|
|
|
|
|
...(includeMeta && { meta_allow: null, meta_deny: null }),
|
|
|
|
|
reason: 'unknown_rule_type',
|
|
|
|
|
binary: true
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Evaluate rule with binary options
|
|
|
|
|
const result = handler.evaluate(userId, userKey, objectId, objectKey, rule, visited, currentRelation, {
|
|
|
|
|
...enhancedOptions,
|
|
|
|
|
binary: true,
|
|
|
|
|
fastPath: true
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add collected values to value context in binary mode too
|
|
|
|
|
// This is important if downstream rules in a logical operator need them
|
|
|
|
|
if (finalValueContext && result.collectedValues && result.collectedValues.length > 0) {
|
|
|
|
|
finalValueContext.addCollectedValues(result.collectedValues, rule.type, rule);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Binary mode means we only care about allow/deny based on thresholds
|
|
|
|
|
const allow = result.possibility_allow >= minAllowPossibility;
|
|
|
|
|
const deny = result.possibility_deny >= maxDenyPossibility;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
possibility_allow: allow ? 1 : 0,
|
|
|
|
|
possibility_deny: deny ? 1 : 0,
|
|
|
|
|
// Preserve the real continuous possibility so logical operators
|
|
|
|
|
// (union/intersection/exclusion) can aggregate child strengths —
|
|
|
|
|
// binarized 0/1 values alone corrupt max/min/product aggregation.
|
|
|
|
|
possibility: result.possibility ?? result.possibility_allow ?? (allow ? 1 : 0),
|
|
|
|
|
...(includeMeta && { meta_allow: allow ? (result.meta_allow || result.meta) : null, meta_deny: deny ? result.meta_deny : null }),
|
|
|
|
|
reason: result.reason || (allow ? 'binary_allow_threshold_met' : (deny ? 'binary_deny_threshold_met' : 'binary_threshold_not_met')),
|
|
|
|
|
binary: true,
|
|
|
|
|
collectedValues: result.collectedValues || [] // Preserve collected values
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get value context statistics for debugging
|
|
|
|
|
*/
|
|
|
|
|
getValueContextStats(options = {}) {
|
|
|
|
|
const { valueContext } = options;
|
|
|
|
|
return valueContext ? valueContext.getStats() : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_ruleRequiresValues(rule, visitedRelations) {
|
|
|
|
|
if (!rule || typeof rule !== 'object') return false;
|
|
|
|
|
|
|
|
|
|
if (rule.type === 'relational_comparator' || rule.qualitative === true) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rule.type === 'computed') {
|
|
|
|
|
const relationName = rule.relation;
|
|
|
|
|
if (relationName) {
|
|
|
|
|
if (visitedRelations.has(relationName)) return false;
|
|
|
|
|
visitedRelations.add(relationName);
|
|
|
|
|
if (this._valueRequirementCache.has(relationName)) {
|
|
|
|
|
return this._valueRequirementCache.get(relationName);
|
|
|
|
|
}
|
|
|
|
|
const config = this.arbiter.relationConfigs.get(relationName);
|
|
|
|
|
const requiresValues = this._ruleRequiresValues(config, visitedRelations);
|
|
|
|
|
this._valueRequirementCache.set(relationName, requiresValues);
|
|
|
|
|
if (config && typeof config === 'object') {
|
|
|
|
|
config._needsValues = requiresValues;
|
|
|
|
|
}
|
|
|
|
|
return requiresValues;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const traverse = (child) => this._ruleRequiresValues(child, visitedRelations);
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(rule.union) && rule.union.some(traverse)) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (rule.union && Array.isArray(rule.union.rules) && rule.union.rules.some(traverse)) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(rule.intersection) && rule.intersection.some(traverse)) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (rule.intersection && Array.isArray(rule.intersection.rules) && rule.intersection.rules.some(traverse)) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (Array.isArray(rule.exclusion) && rule.exclusion.some(traverse)) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (rule.when && this._ruleRequiresValues(rule.when, visitedRelations)) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (rule.unless && this._ruleRequiresValues(rule.unless, visitedRelations)) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
if (rule.requires && this._ruleRequiresValues(rule.requires, visitedRelations)) {
|
|
|
|
|
rule._needsValues = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rule._needsValues = false;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Wrap raw defeasible components (single rules like
|
|
|
|
|
* `{ when: { type: 'direct', relation: 'x' } }`) into the object shape
|
|
|
|
|
* LogicalOperators.evaluateDefeasible expects
|
|
|
|
|
* (`when.intersection.rules[]`, `unless.union.rules[]`, `always.direct`).
|
|
|
|
|
* Logical (union/intersection/exclusion) components pass through.
|
|
|
|
|
*/
|
|
|
|
|
function wrapDefeasibleComponents(rule) {
|
|
|
|
|
const wrapSingle = (component) => ({ rules: [component] });
|
|
|
|
|
const wrapUnion = (component) => {
|
|
|
|
|
if (!component) return component;
|
|
|
|
|
if (Array.isArray(component)) return { rules: component };
|
|
|
|
|
if (component.union || component.intersection || component.exclusion) return component;
|
|
|
|
|
return wrapSingle(component);
|
|
|
|
|
};
|
|
|
|
|
const wrapIntersection = (component) => {
|
|
|
|
|
if (!component) return component;
|
|
|
|
|
if (Array.isArray(component)) return { rules: component };
|
|
|
|
|
if (component.union || component.intersection || component.exclusion) return component;
|
|
|
|
|
return wrapSingle(component);
|
|
|
|
|
};
|
|
|
|
|
return {
|
|
|
|
|
...rule,
|
|
|
|
|
when: rule.when ? wrapIntersection(rule.when) : rule.when,
|
|
|
|
|
unless: rule.unless ? wrapUnion(rule.unless) : rule.unless,
|
|
|
|
|
never: rule.never ? wrapUnion(rule.never) : rule.never,
|
|
|
|
|
requires: rule.requires ? wrapUnion(rule.requires) : rule.requires,
|
|
|
|
|
always: rule.always ? { direct: rule.always.direct || rule.always } : rule.always
|
|
|
|
|
};
|
|
|
|
|
}
|