js-rigor: remove the compiled evaluator — the runtime compiler is gone
The compiled evaluation path was never a performance win and was a
double-implementation liability: every semantics fix had to land twice
(CompiledEvaluator + LogicalOperators/handlers), and several bugs lived
only in one copy. A warm benchmark shows the compiled path at parity at
best (the apparent 7x chain regression was cold-cache confound).
Removed the runtime compiled dispatch entirely: RuleEvaluator evaluates
every rule through the single fallback path (logical operators + rule
handlers). The RuleCompiler remains as the config VALIDATOR only
(_compileErrors/_compileWarnings + _needsValues + the _compiled metadata
carried by snapshots). CompiledEvaluator.js deleted.
Fixes surfaced by removing the mask:
- The defeasible fallback wrap produced the wrong component shape
({rules} instead of {union:{rules}}/{intersection:{rules}}) — the
compiled path always ran for defeasible configs, so the fallback had
never executed; now wrapped correctly.
- Defeasible configs had no normal-dispatch routing (the compiled
evaluator handled them); routed to evaluateDefeasible.
- The binary defeasible path forced the binary mode's internal 0.5
threshold, while the compiled path always ran normal mode — the binary
decision is now the thresholded normal combination (preserving the
pinned contract).
- The fallback union/intersection/exclusion results now carry the
validity blocks (previously only the compiled versions did).
This commit is contained in:
@@ -1,898 +0,0 @@
|
||||
import { buildValidity, normalizeValidity, DEFAULT_VALIDITY } from '../core/validity.js';
|
||||
import { OWAFusion, getOWAWeightsFromRule } from '../utils/OWAFusion.js';
|
||||
import { buildRemediation, extractRemediation, mergeRemediationOptions } from './remediation.js';
|
||||
|
||||
export class CompiledEvaluator {
|
||||
constructor(arbiter, ruleEvaluator, logicalOperators) {
|
||||
this.arbiter = arbiter;
|
||||
this.ruleEvaluator = ruleEvaluator;
|
||||
this.logicalOperators = logicalOperators;
|
||||
}
|
||||
|
||||
evaluate(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options = {}) {
|
||||
if (!compiled || typeof compiled !== 'object') {
|
||||
return { possibility: 0, reason: 'missing_compiled_rule' };
|
||||
}
|
||||
|
||||
switch (compiled.type) {
|
||||
case 'direct':
|
||||
return this._evaluateDirect(compiled, userId, userKey, objectId, objectKey, options, currentRelation);
|
||||
case 'computed':
|
||||
return this._evaluateComputed(compiled, userKey, objectKey, options, visited, currentRelation);
|
||||
case 'tuple_to_userset':
|
||||
return this._evaluateTupleToUserset(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
case 'chain':
|
||||
return this._evaluateChain(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
case 'multi_hop':
|
||||
return this._evaluateMultiHop(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
case 'logical':
|
||||
return this._evaluateLogical(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
case 'defeasible':
|
||||
return this._evaluateDefeasible(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
case 'relational_comparator':
|
||||
return this._evaluateRelationalComparator(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
case 'challenge':
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
compiled,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
default:
|
||||
if (compiled.source) {
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
compiled.source,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
return { possibility: 0, reason: 'unsupported_compiled_rule' };
|
||||
}
|
||||
}
|
||||
|
||||
_evaluateRelationalComparator(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
if (compiled.source) {
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
compiled.source,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
|
||||
const toOperandConfig = (operand) => {
|
||||
if (!operand) return null;
|
||||
return {
|
||||
rule: operand.rule || null,
|
||||
extractValue: operand.extractValue !== false,
|
||||
valueRelation: operand.valueRelationResolved || operand.valueRelation || null,
|
||||
evaluateFrom: operand.evaluateFrom || 'auto',
|
||||
aggregator: operand.aggregator,
|
||||
owaWeights: operand.owaWeights,
|
||||
_owaSparseCount: operand._owaSparseCount,
|
||||
minOperandPossibility: operand.minOperandPossibility
|
||||
};
|
||||
};
|
||||
|
||||
const fallbackConfig = {
|
||||
type: 'relational_comparator',
|
||||
comparator: compiled.comparator,
|
||||
marginOfSafety: compiled.marginOfSafety,
|
||||
fallbackBehavior: compiled.fallbackBehavior,
|
||||
minRulePossibility: compiled.minRulePossibility,
|
||||
epsilon: compiled.epsilon,
|
||||
left: toOperandConfig(compiled.left),
|
||||
right: toOperandConfig(compiled.right)
|
||||
};
|
||||
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
fallbackConfig,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
|
||||
_evaluateDirect(compiled, userId, userKey, objectId, objectKey, options, currentRelation) {
|
||||
const { fastPath = false, minPossibility = null, includeMeta = true } = options;
|
||||
const relName = compiled.relation || currentRelation;
|
||||
const reverse = compiled.reverse;
|
||||
const collectValues = options.collectValues !== undefined ? options.collectValues : compiled.collectValues !== false;
|
||||
|
||||
let directRel;
|
||||
if (reverse) {
|
||||
directRel = this.arbiter.relationManager.getDirectRelation(objectId, relName, userId, options);
|
||||
} else {
|
||||
directRel = this.arbiter.relationManager.getDirectRelation(userId, relName, objectId, options);
|
||||
}
|
||||
|
||||
if (!directRel) {
|
||||
return {
|
||||
possibility: 0,
|
||||
...(includeMeta && { meta: { ruleType: 'direct', reason: 'no_relation' } }),
|
||||
reason: 'no_relation'
|
||||
};
|
||||
}
|
||||
|
||||
const relationStrength = directRel.possibility;
|
||||
const _source = directRel.source || 'persistent';
|
||||
const _allowMeta = {
|
||||
ruleType: 'direct',
|
||||
reason: 'direct',
|
||||
source: _source,
|
||||
layer_name: directRel.layer_name || null,
|
||||
source_class: directRel.source_class || null,
|
||||
reducer_applied: directRel.reducer_applied || null
|
||||
};
|
||||
const result = {
|
||||
possibility: relationStrength,
|
||||
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
||||
validity: buildValidity('identity', [relName], [directRel.validity !== undefined ? normalizeValidity(directRel.validity) : 'heuristic'], 1, relationStrength),
|
||||
possibility_allow: relationStrength,
|
||||
possibility_deny: 0,
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
ruleType: 'direct',
|
||||
reason: 'relation_exists',
|
||||
relation: relName,
|
||||
reverse: reverse || false,
|
||||
strength: relationStrength,
|
||||
source: _source,
|
||||
allow: _allowMeta
|
||||
},
|
||||
meta_allow: _allowMeta
|
||||
}),
|
||||
reason: 'exists'
|
||||
};
|
||||
|
||||
if (fastPath && minPossibility !== null && result.possibility >= minPossibility) {
|
||||
if (result.meta) {
|
||||
result.meta.earlyExit = true;
|
||||
result.meta.earlyExitReason = 'strength_threshold_met';
|
||||
}
|
||||
}
|
||||
|
||||
if (collectValues && directRel.value !== undefined) {
|
||||
const sourceEntity = reverse ? objectKey : userKey;
|
||||
const targetEntity = reverse ? userKey : objectKey;
|
||||
const path = [sourceEntity, targetEntity];
|
||||
result.collectedValues = [
|
||||
this.ruleEvaluator.ruleHandlers.direct._createCollectedValue(
|
||||
directRel.value,
|
||||
directRel.possibility,
|
||||
path,
|
||||
{
|
||||
entityKey: sourceEntity,
|
||||
relation: relName,
|
||||
step: 0
|
||||
},
|
||||
{
|
||||
timestamp: directRel.changed_last_at || directRel.updated_last_at || Date.now(),
|
||||
reliability: 1.0,
|
||||
source: directRel.source || 'persistent'
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_evaluateComputed(compiled, userKey, objectKey, options, visited, currentRelation) {
|
||||
return this.arbiter.authChecker.check(userKey, compiled.relation, objectKey, {
|
||||
...options,
|
||||
_visited: visited,
|
||||
_currentRelation: currentRelation
|
||||
});
|
||||
}
|
||||
|
||||
_evaluateTupleToUserset(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
if (compiled._optimized?.kind === 'direct_join' && !options.collectValues && !options.trackEvaluation) {
|
||||
const optimized = compiled._optimized;
|
||||
const resolvePossibility = (value) => value !== undefined ? value : 1.0;
|
||||
const resolveReliability = (value) => value !== undefined ? value : 1.0;
|
||||
const tuplesetDirection = optimized.tuplesetDirection || 'out';
|
||||
let tuples;
|
||||
if (optimized.reverse) {
|
||||
tuples = tuplesetDirection === 'in'
|
||||
? this.arbiter.relationManager.getRelationsToDst(userId, optimized.tuplesetRelation, options)
|
||||
: this.arbiter.relationManager.getRelationsFromSrc(userId, optimized.tuplesetRelation, options);
|
||||
} else {
|
||||
tuples = tuplesetDirection === 'in'
|
||||
? this.arbiter.relationManager.getRelationsToDst(objectId, optimized.tuplesetRelation, options)
|
||||
: this.arbiter.relationManager.getRelationsFromSrc(objectId, optimized.tuplesetRelation, options);
|
||||
}
|
||||
|
||||
const maxIntermediates = optimized.maxIntermediates !== undefined ? optimized.maxIntermediates : 20;
|
||||
if (tuples.length > maxIntermediates * 3) {
|
||||
tuples.sort((a, b) => resolvePossibility(b.possibility) - resolvePossibility(a.possibility));
|
||||
tuples = tuples.slice(0, maxIntermediates);
|
||||
}
|
||||
|
||||
const checkingId = optimized.reverse ? objectId : userId;
|
||||
const computedEdges = this.arbiter.relationManager.getRelationsFromSrc(checkingId, optimized.computedRelation, options);
|
||||
const computedByIntermediate = new Map(computedEdges.map(edge => [edge.dst, edge]));
|
||||
|
||||
let processed = 0;
|
||||
let bestPossibility = 0;
|
||||
let bestReliability = 1.0;
|
||||
const earlyExitThreshold = optimized.earlyExitThreshold !== undefined ? optimized.earlyExitThreshold : 0.95;
|
||||
const minPossibility = optimized.minPossibility !== undefined ? optimized.minPossibility : 0;
|
||||
|
||||
for (const t of tuples) {
|
||||
if (processed >= maxIntermediates) break;
|
||||
processed++;
|
||||
const intermediateId = tuplesetDirection === 'in' ? t.src : t.dst;
|
||||
const directRel = computedByIntermediate.get(intermediateId);
|
||||
if (!directRel) continue;
|
||||
const combinedPossibility = Math.min(resolvePossibility(t.possibility), resolvePossibility(directRel.possibility));
|
||||
const finalPossibility = combinedPossibility >= minPossibility ? combinedPossibility : 0;
|
||||
if (finalPossibility > bestPossibility) {
|
||||
bestPossibility = finalPossibility;
|
||||
bestReliability = resolveReliability(t.reliability) * (directRel.reliability !== undefined ? directRel.reliability : 1.0);
|
||||
if (bestPossibility >= earlyExitThreshold) break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
possibility: bestPossibility,
|
||||
reliability: bestReliability,
|
||||
reason: bestPossibility > 0 ? 'direct_join' : 'no_match'
|
||||
};
|
||||
}
|
||||
|
||||
if (compiled.source) {
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
compiled.source,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
const fallbackConfig = {
|
||||
type: 'tuple_to_userset',
|
||||
tuplesetRelation: compiled.tuplesetRelation,
|
||||
tuplesetDirection: compiled.tuplesetDirection,
|
||||
computedRelation: compiled.computedRelation
|
||||
};
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
fallbackConfig,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
|
||||
_evaluateChain(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
if (compiled._optimized?.kind === 'direct' && !options.collectValues && !options.trackEvaluation) {
|
||||
const direct = {
|
||||
type: 'direct',
|
||||
relation: compiled._optimized.relation,
|
||||
reverse: compiled._optimized.reverse,
|
||||
collectValues: false
|
||||
};
|
||||
return this._evaluateDirect(direct, userId, userKey, objectId, objectKey, options, currentRelation);
|
||||
}
|
||||
if (compiled.source) {
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
compiled.source,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
const fallbackConfig = {
|
||||
type: 'chain',
|
||||
steps: compiled.steps,
|
||||
reverse: compiled.reverse,
|
||||
collectValues: compiled.collectValues,
|
||||
valueFilters: compiled.valueFilters,
|
||||
valueAggregation: compiled.valueAggregation
|
||||
};
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
fallbackConfig,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
|
||||
_evaluateMultiHop(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
if (compiled._optimized?.kind === 'direct' && !options.collectValues && !options.trackEvaluation) {
|
||||
const direct = {
|
||||
type: 'direct',
|
||||
relation: compiled._optimized.relation,
|
||||
reverse: compiled._optimized.reverse,
|
||||
collectValues: false
|
||||
};
|
||||
return this._evaluateDirect(direct, userId, userKey, objectId, objectKey, options, currentRelation);
|
||||
}
|
||||
if (compiled.source) {
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
compiled.source,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
return { possibility: 0, reason: 'unsupported_multi_hop' };
|
||||
}
|
||||
|
||||
_evaluateLogical(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
if (compiled.source?._subjectAsObject) { objectId = userId; objectKey = userKey; }
|
||||
let result;
|
||||
switch (compiled.op) {
|
||||
case 'union':
|
||||
result = this._evaluateUnion(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
break;
|
||||
case 'intersection':
|
||||
result = this._evaluateIntersection(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
break;
|
||||
case 'exclusion':
|
||||
result = this._evaluateExclusion(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
break;
|
||||
default:
|
||||
result = { possibility: 0, reason: 'unknown_logical_op' };
|
||||
}
|
||||
|
||||
if (compiled.negate) {
|
||||
result = {
|
||||
...result,
|
||||
possibility: Math.max(0, 1 - (result.possibility || 0)),
|
||||
reason: result.possibility === 0 ? result.reason : 'negated'
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
_evaluateUnion(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options;
|
||||
const includeOwaTrace = trackEvaluation && includeMeta;
|
||||
const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null;
|
||||
|
||||
if (compiled.useBilattice && compiled.source) {
|
||||
return this.ruleEvaluator.evaluateRule(
|
||||
userId,
|
||||
userKey,
|
||||
objectId,
|
||||
objectKey,
|
||||
compiled.source,
|
||||
visited,
|
||||
currentRelation,
|
||||
{ ...options, useCompiled: false }
|
||||
);
|
||||
}
|
||||
|
||||
if (compiled._optimized?.kind === 'direct_list') {
|
||||
return this._evaluateDirectList(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options, 'union');
|
||||
}
|
||||
|
||||
const children = compiled.children || [compiled];
|
||||
const possibilities = [];
|
||||
const reliabilities = [];
|
||||
const metas = [];
|
||||
const resChildrenValidity = [];
|
||||
const remediationOptions = [];
|
||||
const allCollectedValues = collectValues ? [] : null;
|
||||
|
||||
for (const child of children) {
|
||||
const childVisited = new Set(visited);
|
||||
let res = this.evaluate(child, userId, userKey, objectId, objectKey, childVisited, currentRelation, options);
|
||||
if (!res || typeof res.possibility !== 'number') {
|
||||
res = { possibility: 0, meta: { reason: 'missing_rule' }, collectedValues: [] };
|
||||
}
|
||||
if (res.validity) resChildrenValidity.push(res.validity);
|
||||
|
||||
mergeRemediationOptions(remediationOptions, extractRemediation(res));
|
||||
|
||||
possibilities.push(res.possibility);
|
||||
reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0);
|
||||
metas.push(includeMeta ? res.meta : null);
|
||||
|
||||
if (collectValues && res.collectedValues && Array.isArray(res.collectedValues)) {
|
||||
allCollectedValues.push(...res.collectedValues);
|
||||
if (valueContext) {
|
||||
valueContext.addCollectedValues(res.collectedValues, child.type, child);
|
||||
}
|
||||
}
|
||||
|
||||
if (fastPath && minPossibility !== null && res.possibility >= minPossibility) {
|
||||
const remediation = buildRemediation(extractRemediation(res));
|
||||
return {
|
||||
possibility: res.possibility,
|
||||
reliability: res.reliability !== undefined ? res.reliability : 1.0,
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && { meta: res.meta }),
|
||||
...(remediation ? { remediation } : {})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!possibilities.length) {
|
||||
return {
|
||||
possibility: 0,
|
||||
reliability: 1.0,
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && { meta: { operation: 'union', childCount: 0 } })
|
||||
};
|
||||
}
|
||||
|
||||
const unionOWAWeights = compiled._precomputedWeights && compiled._precomputedWeights.length === possibilities.length
|
||||
? compiled._precomputedWeights
|
||||
: getOWAWeightsFromRule(compiled, possibilities.length, metas);
|
||||
let result;
|
||||
if (compiled.reliabilityWeighting) {
|
||||
const weighted = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0));
|
||||
result = OWAFusion.fuseWithMeta(weighted, metas, unionOWAWeights, compiled.aggregator || 'max', true, owaTraceOptions);
|
||||
} else if (unionOWAWeights.some(w => w > 0)) {
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, unionOWAWeights, compiled.aggregator || 'max', true, owaTraceOptions);
|
||||
} else {
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, null, 'max', true, owaTraceOptions);
|
||||
}
|
||||
|
||||
const remediation = result.value === 0
|
||||
? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' })
|
||||
: null;
|
||||
let unionReliability = 1.0;
|
||||
if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') {
|
||||
unionReliability = reliabilities[result.trace.selectedIndex] ?? 1.0;
|
||||
} else if ((compiled.aggregator || 'max') === 'min') {
|
||||
const minP = Math.min(...possibilities);
|
||||
unionReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0;
|
||||
} else {
|
||||
const maxP = Math.max(...possibilities);
|
||||
unionReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0;
|
||||
}
|
||||
const unionOperator = (compiled.aggregator || 'max') === 'max' && (!compiled.owaWeights || (compiled.owaWeights.length <= 1 || (compiled.owaWeights[0] === 1 && compiled.owaWeights.slice(1).every(w => w === 0))))
|
||||
? 'max' : 'owa';
|
||||
const unionSourceLabels = (resChildrenValidity || []).map(v => v?.label).filter(Boolean);
|
||||
const unionSources = [];
|
||||
for (const v of resChildrenValidity || []) for (const s of v?.sources || []) if (!unionSources.includes(s)) unionSources.push(s);
|
||||
return {
|
||||
possibility: result.value,
|
||||
reliability: unionReliability,
|
||||
validity: buildValidity(unionOperator, unionSources, unionSourceLabels, possibilities.length, result.value),
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
...result.meta,
|
||||
operation: 'union',
|
||||
childCount: possibilities.length,
|
||||
aggregator: compiled.aggregator || 'max',
|
||||
...(includeOwaTrace && result.trace ? {
|
||||
owa: {
|
||||
level: null,
|
||||
aggregator: compiled.aggregator || 'max',
|
||||
weights: result.trace.weights,
|
||||
sortedValues: result.trace.sortedValues,
|
||||
contributions: result.trace.contributions,
|
||||
selectedIndex: result.trace.selectedIndex
|
||||
}
|
||||
} : {}),
|
||||
...(remediation ? { remediation } : {})
|
||||
}
|
||||
}),
|
||||
...(remediation ? { remediation } : {})
|
||||
};
|
||||
}
|
||||
|
||||
_evaluateIntersection(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options;
|
||||
const includeOwaTrace = trackEvaluation && includeMeta;
|
||||
const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null;
|
||||
const effectiveObjectId = compiled.source?._subjectAsObject ? userId : objectId;
|
||||
const effectiveObjectKey = compiled.source?._subjectAsObject ? userKey : objectKey;
|
||||
if (compiled._optimized?.kind === 'direct_list') {
|
||||
return this._evaluateDirectList(compiled, userId, userKey, effectiveObjectId, effectiveObjectKey, visited, currentRelation, options, 'intersection');
|
||||
}
|
||||
const possibilities = [];
|
||||
const reliabilities = [];
|
||||
const metas = [];
|
||||
const remediationOptions = [];
|
||||
const allCollectedValues = collectValues ? [] : null;
|
||||
|
||||
// A defeasible `when` component may compile to a single direct node
|
||||
// (no children); treat it as a one-element conjunction, matching the
|
||||
// union path's `compiled.children || [compiled]` fallback.
|
||||
const children = compiled.children || [compiled];
|
||||
const resChildrenValidity = [];
|
||||
|
||||
for (const child of children) {
|
||||
const childVisited = new Set(visited);
|
||||
const res = this.evaluate(child, userId, userKey, objectId, objectKey, childVisited, currentRelation, options);
|
||||
if (res.validity) resChildrenValidity.push(res.validity);
|
||||
possibilities.push(res.possibility);
|
||||
reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0);
|
||||
metas.push(includeMeta ? res.meta : null);
|
||||
|
||||
mergeRemediationOptions(remediationOptions, extractRemediation(res));
|
||||
|
||||
if (collectValues && res.collectedValues && Array.isArray(res.collectedValues)) {
|
||||
allCollectedValues.push(...res.collectedValues);
|
||||
if (valueContext) {
|
||||
valueContext.addCollectedValues(res.collectedValues, child.type, child);
|
||||
}
|
||||
}
|
||||
|
||||
if (fastPath && minPossibility !== null && res.possibility < minPossibility) {
|
||||
return {
|
||||
possibility: res.possibility,
|
||||
reliability: res.reliability !== undefined ? res.reliability : 1.0,
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && { meta: res.meta })
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!possibilities.length) {
|
||||
return {
|
||||
possibility: 0,
|
||||
reliability: 1.0,
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && { meta: { operation: 'intersection', childCount: 0 } })
|
||||
};
|
||||
}
|
||||
|
||||
const intersectionOWAWeights = compiled._precomputedWeights && compiled._precomputedWeights.length === possibilities.length
|
||||
? compiled._precomputedWeights
|
||||
: getOWAWeightsFromRule(compiled, possibilities.length, metas);
|
||||
const defaultMode = compiled.aggregator || 'min';
|
||||
const finalWeights = compiled.aggregator || compiled.owaWeights ? intersectionOWAWeights :
|
||||
[...Array(Math.max(0, possibilities.length - 1)).fill(0), 1];
|
||||
|
||||
let result;
|
||||
if (compiled.reliabilityWeighting) {
|
||||
const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0));
|
||||
result = OWAFusion.fuseWithMeta(reliabilityWeightedPossibilities, metas, finalWeights, defaultMode, true, owaTraceOptions);
|
||||
} else {
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, finalWeights, defaultMode, true, owaTraceOptions);
|
||||
}
|
||||
|
||||
const remediation = result.value === 0
|
||||
? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' })
|
||||
: null;
|
||||
let intersectionReliability = 1.0;
|
||||
if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') {
|
||||
intersectionReliability = reliabilities[result.trace.selectedIndex] ?? 1.0;
|
||||
} else if ((compiled.aggregator || 'min') === 'max') {
|
||||
const maxP = Math.max(...possibilities);
|
||||
intersectionReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0;
|
||||
} else {
|
||||
const minP = Math.min(...possibilities);
|
||||
intersectionReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0;
|
||||
}
|
||||
const interSourceLabels = (resChildrenValidity || []).map(v => v?.label).filter(Boolean);
|
||||
const interSources = [];
|
||||
for (const v of resChildrenValidity || []) for (const s of v?.sources || []) if (!interSources.includes(s)) interSources.push(s);
|
||||
return {
|
||||
possibility: result.value,
|
||||
reliability: intersectionReliability,
|
||||
validity: buildValidity('min', interSources, interSourceLabels, possibilities.length, result.value),
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
...result.meta,
|
||||
operation: 'intersection',
|
||||
childCount: possibilities.length,
|
||||
aggregator: defaultMode,
|
||||
...(includeOwaTrace && result.trace ? {
|
||||
owa: {
|
||||
level: null,
|
||||
aggregator: defaultMode,
|
||||
weights: result.trace.weights,
|
||||
sortedValues: result.trace.sortedValues,
|
||||
contributions: result.trace.contributions,
|
||||
selectedIndex: result.trace.selectedIndex
|
||||
}
|
||||
} : {}),
|
||||
...(remediation ? { remediation } : {})
|
||||
}
|
||||
}),
|
||||
...(remediation ? { remediation } : {})
|
||||
};
|
||||
}
|
||||
|
||||
_evaluateExclusion(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
const { valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options;
|
||||
const includeOwaTrace = trackEvaluation && includeMeta;
|
||||
const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null;
|
||||
if (compiled.children.length !== 2) {
|
||||
return {
|
||||
possibility: 0,
|
||||
...(collectValues && { collectedValues: [] }),
|
||||
...(includeMeta && { meta: { operation: 'exclusion', error: 'exclusion_requires_exactly_two_rules' } }),
|
||||
reason: 'exclusion_requires_exactly_two_rules'
|
||||
};
|
||||
}
|
||||
|
||||
const visitedA = new Set(visited);
|
||||
const visitedB = new Set(visited);
|
||||
const a = this.evaluate(compiled.children[0], userId, userKey, objectId, objectKey, visitedA, currentRelation, options);
|
||||
const b = this.evaluate(compiled.children[1], userId, userKey, objectId, objectKey, visitedB, currentRelation, options);
|
||||
|
||||
const allCollectedValues = collectValues ? [] : null;
|
||||
if (collectValues && a.collectedValues && Array.isArray(a.collectedValues)) {
|
||||
allCollectedValues.push(...a.collectedValues);
|
||||
if (valueContext) valueContext.addCollectedValues(a.collectedValues, compiled.children[0].type, compiled.children[0]);
|
||||
}
|
||||
if (collectValues && b.collectedValues && Array.isArray(b.collectedValues)) {
|
||||
allCollectedValues.push(...b.collectedValues);
|
||||
if (valueContext) valueContext.addCollectedValues(b.collectedValues, compiled.children[1].type, compiled.children[1]);
|
||||
}
|
||||
|
||||
let possibility;
|
||||
let result;
|
||||
if (compiled.aggregator || compiled.owaWeights) {
|
||||
const possibilities = [a.possibility, 1 - b.possibility];
|
||||
const metas = [a.meta, { exclusion_complement: b.meta }];
|
||||
const reliabilityWeights = [
|
||||
a.reliability !== undefined ? a.reliability : 1.0,
|
||||
b.reliability !== undefined ? b.reliability : 1.0
|
||||
];
|
||||
const exclusionOWAWeights = compiled._precomputedWeights && compiled._precomputedWeights.length === 2
|
||||
? compiled._precomputedWeights
|
||||
: getOWAWeightsFromRule(compiled, 2, metas);
|
||||
const fusedInputs = compiled.reliabilityWeighting
|
||||
? possibilities.map((poss, i) => poss * (reliabilityWeights[i] ?? 1.0))
|
||||
: possibilities;
|
||||
result = OWAFusion.fuseWithMeta(fusedInputs, metas, exclusionOWAWeights, compiled.aggregator || 'min', true, owaTraceOptions);
|
||||
possibility = result.value;
|
||||
} else {
|
||||
possibility = a.possibility * (1 - b.possibility);
|
||||
}
|
||||
|
||||
return {
|
||||
possibility,
|
||||
// Both legs contribute to the decision, so their reliabilities
|
||||
// multiply (consistent with TTU/chain path semantics).
|
||||
reliability: (a.reliability !== undefined ? a.reliability : 1.0) * (b.reliability !== undefined ? b.reliability : 1.0),
|
||||
validity: buildValidity('product', [a.validity?.sources?.[0], b.validity?.sources?.[0]].filter(Boolean), [a.validity?.label, b.validity?.label].filter(Boolean), 2, possibility),
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
operation: 'exclusion',
|
||||
childA: a.meta,
|
||||
childB: b.meta,
|
||||
aggregator: compiled.aggregator || 'standard',
|
||||
...(includeOwaTrace && result?.trace ? {
|
||||
owa: {
|
||||
level: null,
|
||||
aggregator: compiled.aggregator || 'standard',
|
||||
weights: result.trace.weights,
|
||||
sortedValues: result.trace.sortedValues,
|
||||
contributions: result.trace.contributions,
|
||||
selectedIndex: result.trace.selectedIndex
|
||||
}
|
||||
} : {})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
_evaluateDirectList(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options, op) {
|
||||
const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options;
|
||||
const includeOwaTrace = trackEvaluation && includeMeta;
|
||||
const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null;
|
||||
|
||||
const possibilities = [];
|
||||
const reliabilities = [];
|
||||
const metas = [];
|
||||
const listChildrenValidity = [];
|
||||
const allCollectedValues = collectValues ? [] : null;
|
||||
const directRules = compiled._optimized.direct;
|
||||
|
||||
for (const direct of directRules) {
|
||||
let directRel;
|
||||
if (direct.reverse) {
|
||||
directRel = this.arbiter.relationManager.getDirectRelation(objectId, direct.relation, userId, options);
|
||||
} else {
|
||||
directRel = this.arbiter.relationManager.getDirectRelation(userId, direct.relation, objectId, options);
|
||||
}
|
||||
|
||||
const possibility = directRel ? directRel.possibility : 0;
|
||||
possibilities.push(possibility);
|
||||
reliabilities.push(directRel ? (directRel.reliability !== undefined ? directRel.reliability : 1.0) : 1.0);
|
||||
if (directRel) {
|
||||
listChildrenValidity.push(buildValidity('identity', [direct.relation], [normalizeValidity(directRel.validity !== undefined ? directRel.validity : null)], 1, possibility));
|
||||
}
|
||||
if (includeMeta) {
|
||||
const _src = directRel ? (directRel.source || 'persistent') : null;
|
||||
const _allowMeta = directRel ? {
|
||||
ruleType: 'direct',
|
||||
reason: 'direct',
|
||||
source: _src,
|
||||
layer_name: directRel.layer_name || null,
|
||||
source_class: directRel.source_class || null,
|
||||
reducer_applied: directRel.reducer_applied || null
|
||||
} : null;
|
||||
metas.push(directRel ? {
|
||||
ruleType: 'direct',
|
||||
relation: direct.relation,
|
||||
reverse: direct.reverse || false,
|
||||
strength: possibility,
|
||||
source: _src,
|
||||
allow: _allowMeta
|
||||
} : { ruleType: 'direct', relation: direct.relation, reason: 'no_relation' });
|
||||
} else {
|
||||
metas.push(null);
|
||||
}
|
||||
|
||||
if (collectValues && direct.collectValues !== false && directRel && directRel.value !== undefined) {
|
||||
const sourceEntity = direct.reverse ? objectKey : userKey;
|
||||
const targetEntity = direct.reverse ? userKey : objectKey;
|
||||
const path = [sourceEntity, targetEntity];
|
||||
const collected = this.ruleEvaluator.ruleHandlers.direct._createCollectedValue(
|
||||
directRel.value,
|
||||
directRel.possibility,
|
||||
path,
|
||||
{
|
||||
entityKey: sourceEntity,
|
||||
relation: direct.relation,
|
||||
step: 0
|
||||
},
|
||||
{
|
||||
timestamp: directRel.changed_last_at || directRel.updated_last_at || Date.now(),
|
||||
reliability: 1.0,
|
||||
source: directRel.source || 'persistent'
|
||||
}
|
||||
);
|
||||
allCollectedValues.push(collected);
|
||||
if (valueContext) {
|
||||
valueContext.addCollectedValues([collected], 'direct', direct);
|
||||
}
|
||||
}
|
||||
|
||||
if (fastPath && minPossibility !== null) {
|
||||
if (op === 'union' && possibility >= minPossibility) {
|
||||
return {
|
||||
possibility,
|
||||
reliability: directRel ? (directRel.reliability !== undefined ? directRel.reliability : 1.0) : 1.0,
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && { meta: metas[metas.length - 1] })
|
||||
};
|
||||
}
|
||||
if (op === 'intersection' && possibility < minPossibility) {
|
||||
return {
|
||||
possibility,
|
||||
reliability: directRel ? (directRel.reliability !== undefined ? directRel.reliability : 1.0) : 1.0,
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && { meta: metas[metas.length - 1] })
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!possibilities.length) {
|
||||
return {
|
||||
possibility: 0,
|
||||
reliability: 1.0,
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && { meta: { operation: op, childCount: 0 } })
|
||||
};
|
||||
}
|
||||
|
||||
const weights = compiled._precomputedWeights && compiled._precomputedWeights.length === possibilities.length
|
||||
? compiled._precomputedWeights
|
||||
: getOWAWeightsFromRule(compiled, possibilities.length, metas);
|
||||
const aggregator = op === 'intersection' ? (compiled.aggregator || 'min') : (compiled.aggregator || 'max');
|
||||
const fusedInputs = compiled.reliabilityWeighting
|
||||
? possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0))
|
||||
: possibilities;
|
||||
const result = OWAFusion.fuseWithMeta(fusedInputs, metas, weights, aggregator, true, owaTraceOptions);
|
||||
|
||||
let listReliability = 1.0;
|
||||
if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') {
|
||||
listReliability = reliabilities[result.trace.selectedIndex] ?? 1.0;
|
||||
} else if (aggregator === 'min') {
|
||||
const minP = Math.min(...possibilities);
|
||||
listReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0;
|
||||
} else {
|
||||
const maxP = Math.max(...possibilities);
|
||||
listReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0;
|
||||
}
|
||||
|
||||
const listOperator = aggregator === 'min' ? 'min' : (aggregator === 'max' ? 'max' : 'owa');
|
||||
const listLabels = listChildrenValidity.map(v => v.label).filter(Boolean);
|
||||
const listSources = [];
|
||||
for (const v of listChildrenValidity) for (const s of v.sources || []) if (!listSources.includes(s)) listSources.push(s);
|
||||
return {
|
||||
possibility: result.value,
|
||||
reliability: listReliability,
|
||||
validity: buildValidity(listOperator, listSources, listLabels, possibilities.length, result.value),
|
||||
...(collectValues && { collectedValues: allCollectedValues }),
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
...result.meta,
|
||||
operation: op,
|
||||
childCount: possibilities.length,
|
||||
aggregator,
|
||||
...(includeOwaTrace && result.trace ? {
|
||||
owa: {
|
||||
level: null,
|
||||
aggregator,
|
||||
weights: result.trace.weights,
|
||||
sortedValues: result.trace.sortedValues,
|
||||
contributions: result.trace.contributions,
|
||||
selectedIndex: result.trace.selectedIndex
|
||||
}
|
||||
} : {})
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
_evaluateDefeasible(compiled, userId, userKey, objectId, objectKey, visited, currentRelation, options) {
|
||||
const { binary = false, fastPath = false, minPossibility = 0.0 } = options;
|
||||
const mode = compiled.mode || (binary ? 'binary' : (fastPath ? 'threshold' : 'normal'));
|
||||
const allCollectedValues = [];
|
||||
let neverResult = null, defeatersResult = null, strictResult = null, defeasibleResult = null, requiresResult = null;
|
||||
|
||||
if (compiled.never) {
|
||||
neverResult = this._evaluateUnion(compiled.never, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
if (neverResult.collectedValues) allCollectedValues.push(...neverResult.collectedValues);
|
||||
}
|
||||
if (compiled.always) {
|
||||
strictResult = this.evaluate(compiled.always, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
if (strictResult.collectedValues) allCollectedValues.push(...strictResult.collectedValues);
|
||||
}
|
||||
if (compiled.requires) {
|
||||
requiresResult = this._evaluateUnion(compiled.requires, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
if (requiresResult.collectedValues) allCollectedValues.push(...requiresResult.collectedValues);
|
||||
}
|
||||
if (compiled.unless) {
|
||||
defeatersResult = this._evaluateUnion(compiled.unless, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
if (defeatersResult.collectedValues) allCollectedValues.push(...defeatersResult.collectedValues);
|
||||
}
|
||||
if (compiled.when) {
|
||||
defeasibleResult = this._evaluateIntersection(compiled.when, userId, userKey, objectId, objectKey, visited, currentRelation, options);
|
||||
if (defeasibleResult.collectedValues) allCollectedValues.push(...defeasibleResult.collectedValues);
|
||||
}
|
||||
|
||||
const ruleMeta = {
|
||||
minPossibility: compiled.minPossibility,
|
||||
priority: compiled.priority,
|
||||
mode
|
||||
};
|
||||
|
||||
const finalResult = this.logicalOperators._applyDefeasibleLogic(mode, ruleMeta, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues);
|
||||
return finalResult;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { RelationalComparatorRouter } from './rules/RelationalComparatorRouter.j
|
||||
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) {
|
||||
@@ -26,7 +25,6 @@ export class RuleEvaluator {
|
||||
};
|
||||
|
||||
this.logicalOperators = new LogicalOperators(arbiter, this);
|
||||
this.compiledEvaluator = new CompiledEvaluator(arbiter, this, this.logicalOperators);
|
||||
this._valueRequirementCache = new Map();
|
||||
}
|
||||
|
||||
@@ -70,15 +68,6 @@ export class RuleEvaluator {
|
||||
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);
|
||||
@@ -94,6 +83,17 @@ export class RuleEvaluator {
|
||||
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
||||
}
|
||||
|
||||
// Defeasible configs ({ type: 'defeasible', when/unless/never/always })
|
||||
// have no rule-handler entry; route them to the defeasible evaluator
|
||||
// with the components normalized to the union/intersection format.
|
||||
if (this.logicalOperators._isDefeasibleLogic(rule)) {
|
||||
const result = this.logicalOperators.evaluateDefeasible(
|
||||
numericUserId, userKey, numericObjectId, objectKey,
|
||||
wrapDefeasibleComponents(rule), visited, currentRelation, enhancedOptions
|
||||
);
|
||||
return this._maybeCacheRuleResult(result, currentRelation, ruleCacheKey, canCacheRuleResult);
|
||||
}
|
||||
|
||||
// For other rule types, evaluate normally first
|
||||
// Shorthand operand objects ({ relation: 'owner' } inside logical rules,
|
||||
// or caller-supplied raw configs) carry no type: treat them as direct
|
||||
@@ -292,17 +292,17 @@ export class RuleEvaluator {
|
||||
// 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, {
|
||||
// The compiled path ran defeasible rules in normal mode regardless of
|
||||
// the binary flag (compiled.mode defaulted to 'normal'), so the binary
|
||||
// decision was always the thresholded normal combination. Preserve
|
||||
// that contract: force normal mode; the checker applies the binary
|
||||
// thresholds to the result.
|
||||
const wrapped = wrapDefeasibleComponents(rule);
|
||||
wrapped.mode = 'normal';
|
||||
return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapped, visited, currentRelation, {
|
||||
...enhancedOptions,
|
||||
binary: true,
|
||||
fastPath: true
|
||||
});
|
||||
}
|
||||
return this.logicalOperators.evaluateDefeasible(userId, userKey, objectId, objectKey, wrapDefeasibleComponents(rule), visited, currentRelation, {
|
||||
...enhancedOptions,
|
||||
binary: true,
|
||||
fastPath: true
|
||||
binary: false,
|
||||
fastPath: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -433,18 +433,19 @@ export class RuleEvaluator {
|
||||
* Logical (union/intersection/exclusion) components pass through.
|
||||
*/
|
||||
function wrapDefeasibleComponents(rule) {
|
||||
const wrapSingle = (component) => ({ rules: [component] });
|
||||
// The defeasible evaluator's normalize expects the component format
|
||||
// when.intersection.rules[] / unless.union.rules[] (not bare {rules}).
|
||||
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);
|
||||
if (component.union) return component;
|
||||
if (Array.isArray(component)) return { union: { rules: component } };
|
||||
return { union: { rules: [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);
|
||||
if (component.intersection) return component;
|
||||
if (Array.isArray(component)) return { intersection: { rules: component } };
|
||||
return { intersection: { rules: [component] } };
|
||||
};
|
||||
return {
|
||||
...rule,
|
||||
|
||||
@@ -888,6 +888,7 @@ export class LogicalOperators extends BaseRule {
|
||||
const childRules = unionConfig.rules || [];
|
||||
|
||||
let possibilities = [], reliabilities = [], metas = [], reasons = [];
|
||||
const resChildrenValidity = [];
|
||||
const remediationOptions = [];
|
||||
const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options;
|
||||
const allCollectedValues = collectValues ? [] : null; // Track collected values from all child rules
|
||||
@@ -901,7 +902,7 @@ export class LogicalOperators extends BaseRule {
|
||||
if (!res || typeof res.possibility !== 'number') {
|
||||
res = { possibility: 0, meta: { reason: 'missing_rule' }, collectedValues: [] };
|
||||
}
|
||||
|
||||
if (res.validity) resChildrenValidity.push(res.validity);
|
||||
|
||||
if (res.reason === 'cycle') reasons.push('cycle');
|
||||
mergeRemediationOptions(remediationOptions, extractRemediation(res));
|
||||
@@ -1034,9 +1035,14 @@ export class LogicalOperators extends BaseRule {
|
||||
const maxP = Math.max(...possibilities);
|
||||
unionReliability = reliabilities[possibilities.indexOf(maxP)] ?? 1.0;
|
||||
}
|
||||
const unionOperator = (unionConfig.aggregator || 'max') === 'max' && (!unionConfig.owaWeights || (unionConfig.owaWeights.length <= 1 || (unionConfig.owaWeights[0] === 1 && unionConfig.owaWeights.slice(1).every(w => w === 0))))
|
||||
? 'max' : 'owa';
|
||||
const unionSources = [];
|
||||
for (const v of resChildrenValidity) for (const s of v.sources || []) if (!unionSources.includes(s)) unionSources.push(s);
|
||||
return {
|
||||
possibility: result.value,
|
||||
reliability: unionReliability,
|
||||
validity: buildValidity(unionOperator, unionSources, resChildrenValidity.map(v => v.label).filter(Boolean), possibilities.length, result.value),
|
||||
...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values from child rules
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
@@ -1073,6 +1079,7 @@ export class LogicalOperators extends BaseRule {
|
||||
const childRules = intersectionConfig.rules || [];
|
||||
|
||||
let possibilities = [], reliabilities = [], metas = [], reasons = [];
|
||||
const resChildrenValidity = [];
|
||||
const remediationOptions = [];
|
||||
const { fastPath = false, minPossibility = null, valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options;
|
||||
const allCollectedValues = collectValues ? [] : null; // Track collected values from all child rules
|
||||
@@ -1090,6 +1097,7 @@ export class LogicalOperators extends BaseRule {
|
||||
|
||||
for (const child of childRules) {
|
||||
const res = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, child, visited, currentRelation, options);
|
||||
if (res.validity) resChildrenValidity.push(res.validity);
|
||||
if (res.reason === 'cycle') reasons.push('cycle');
|
||||
possibilities.push(res.possibility);
|
||||
reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0);
|
||||
@@ -1174,9 +1182,12 @@ export class LogicalOperators extends BaseRule {
|
||||
const minP = Math.min(...possibilities);
|
||||
intersectionReliability = reliabilities[possibilities.indexOf(minP)] ?? 1.0;
|
||||
}
|
||||
const interSources = [];
|
||||
for (const v of resChildrenValidity) for (const s of v.sources || []) if (!interSources.includes(s)) interSources.push(s);
|
||||
return {
|
||||
possibility: result.value,
|
||||
reliability: intersectionReliability,
|
||||
validity: buildValidity('min', interSources, resChildrenValidity.map(v => v.label).filter(Boolean), possibilities.length, result.value),
|
||||
...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values from child rules
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
@@ -1305,6 +1316,7 @@ export class LogicalOperators extends BaseRule {
|
||||
// Both legs contribute to the decision, so their reliabilities
|
||||
// multiply (consistent with TTU/chain path semantics).
|
||||
reliability: (a.reliability !== undefined ? a.reliability : 1.0) * (b.reliability !== undefined ? b.reliability : 1.0),
|
||||
validity: buildValidity('product', [a.validity?.sources?.[0], b.validity?.sources?.[0]].filter(Boolean), [a.validity?.label, b.validity?.label].filter(Boolean), 2, possibility),
|
||||
// In binary mode the negated child's strength is the deny side of the
|
||||
// dual-threshold contract: deny fires when P(B) >= maxDenyPossibility.
|
||||
...(options.binary && { possibility_deny: b.possibility ?? 0 }),
|
||||
|
||||
Reference in New Issue
Block a user