fb258035f9
Probe sweep of the OWA surfaces found three real defects:
- reliabilityWeighting was a silent no-op everywhere: every implementation
scaled possibilities by metas[i].reliability, but no child meta ever
carried a reliability field (the compiled direct omitted it and the
DirectRule handler omitted it too), so the weighting was always x1.0.
All weighting branches now use the tracked child reliabilities, and the
DirectRule handler + its meta now carry the relation's reliability.
- The compiled union and the direct_list fast path had no
reliabilityWeighting branch at all; both now apply it.
- Shorthand children ({ relation: 'editor' }) dispatch to the direct
handler but carry no type, so _getRuleResultCacheKey derived the generic
'rule' suffix for every shorthand child of a logical rule — the first
child's cached result was served for all of them (the fallback path
returned the owner's 0.8 for the editor). The key derivation now matches
the shorthand dispatch. The RuleEvaluator also treats shorthand operands
as direct rules instead of unknown_rule_type on the non-compiled path.
New pins: an OWA differential property (custom weights, max/min/average
aggregators, reliabilityWeighting, compiled path) and a multi_hop
pathAggregation=owa fixed pin with reliability propagation.
1332 lines
54 KiB
JavaScript
1332 lines
54 KiB
JavaScript
import { BaseRule } from './BaseRule.js';
|
|
import { OWAFusion, getOWAWeightsFromRule } from '../../utils/OWAFusion.js';
|
|
import { Arbiter } from '../../core/Arbiter.js';
|
|
import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js';
|
|
import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js';
|
|
import { QualitativeScale } from '../../qualitative/QualitativeScale.js';
|
|
import { buildRemediation, extractRemediation, mergeRemediationOptions } from '../remediation.js';
|
|
|
|
/**
|
|
* LogicalOperators - Handles logical combinations of rules and defeasible logic with sophisticated evidential fusion
|
|
*
|
|
* This rule implements both traditional logical operations and defeasible logic patterns with full OWA (Ordered Weighted Averaging) support
|
|
* for nuanced evidential reasoning beyond simple max/min operations.
|
|
*
|
|
* === EVIDENTIAL FUSION WITH OWA ===
|
|
* Every logical operation supports sophisticated aggregation through OWAFusion:
|
|
*
|
|
* Basic Aggregators:
|
|
* - max: Maximum value (optimistic OR)
|
|
* - min: Minimum value (pessimistic AND)
|
|
* - average: Equal weight average
|
|
* - sum: Additive evidence (each contributes full weight)
|
|
*
|
|
* Advanced Aggregators:
|
|
* - majority: Focus on consensus (median or top 60% for larger sets)
|
|
* - median: Pure median value
|
|
* - optimistic: Exponential decay favoring higher values
|
|
* - pessimistic: Exponential decay favoring lower values
|
|
* - top2/top3: Equal weight on top N values
|
|
* - priority: Weight by rule priority values
|
|
* - custom: User-defined OWA weights
|
|
*
|
|
* Example sophisticated union with evidential fusion:
|
|
* {
|
|
* type: 'logical',
|
|
* union: {
|
|
* rules: [rule1, rule2, rule3],
|
|
* aggregator: 'majority', // Use consensus-based fusion
|
|
* reliabilityWeighting: true, // Weight by reliability scores
|
|
* owaWeights: [0.5, 0.3, 0.2] // Custom evidential weights (optional)
|
|
* }
|
|
* }
|
|
*
|
|
* === DEFEASIBLE LOGIC STRUCTURE ===
|
|
* {
|
|
* type: 'logical',
|
|
* never: { // ABSOLUTE DENIAL - If true, overrides everything
|
|
* union: {
|
|
* rules: Array<Rule>,
|
|
* aggregator?: string, // OWA aggregation for absolute denials
|
|
* owaWeights?: Array<number>,
|
|
* reliabilityWeighting?: boolean
|
|
* }
|
|
* },
|
|
* unless: { // DEFEATERS - If true, defeats the rule
|
|
* union: {
|
|
* rules: Array<Rule>,
|
|
* aggregator?: string, // OWA aggregation for defeaters
|
|
* owaWeights?: Array<number>,
|
|
* reliabilityWeighting?: boolean
|
|
* }
|
|
* },
|
|
* always: { // STRICT RULES - If true, wins against all (unless NEVER is true)
|
|
* direct: Rule, // Single rule that is strict
|
|
* aggregator?: string,
|
|
* owaWeights?: Array<number>,
|
|
* reliabilityWeighting?: boolean
|
|
* },
|
|
* when: { // DEFEASIBLE RULES - Potentially defeasible
|
|
* intersection: {
|
|
* rules: Array<Rule>,
|
|
* aggregator?: string, // OWA aggregation for defeasible conditions
|
|
* owaWeights?: Array<number>,
|
|
* reliabilityWeighting?: boolean
|
|
* }
|
|
* },
|
|
* requires: { // INVERSE DEFEATERS - If not true, defeats
|
|
* union: {
|
|
* rules: Array<Rule>,
|
|
* aggregator?: string, // OWA aggregation for requirements
|
|
* owaWeights?: Array<number>,
|
|
* reliabilityWeighting?: boolean
|
|
* }
|
|
* },
|
|
*
|
|
* // Mode control
|
|
* mode?: 'binary' | 'normal' | 'threshold', // Evaluation mode
|
|
* priority?: number, // For tie-breaking in binary mode
|
|
* }
|
|
*
|
|
* === TRADITIONAL LOGICAL OPERATIONS WITH OWA ===
|
|
* {
|
|
* type: 'logical',
|
|
* union: {
|
|
* rules: Array<Rule>, // OR operation on child rules
|
|
* aggregator?: string, // OWA aggregation method: max, average, majority, optimistic, etc.
|
|
* owaWeights?: Array<number>, // Custom OWA weights for sophisticated evidential fusion
|
|
* reliabilityWeighting?: boolean // Weight results by reliability for this union
|
|
* },
|
|
* intersection: {
|
|
* rules: Array<Rule>, // AND operation on child rules
|
|
* aggregator?: string, // OWA aggregation method: min, average, pessimistic, etc.
|
|
* owaWeights?: Array<number>, // Custom OWA weights for sophisticated evidential fusion
|
|
* reliabilityWeighting?: boolean // Weight results by reliability for this intersection
|
|
* },
|
|
* exclusion: {
|
|
* rules: [Rule, Rule], // A AND NOT B operation
|
|
* aggregator?: string, // OWA aggregation method for exclusion evidence
|
|
* owaWeights?: Array<number>, // Custom OWA weights for exclusion fusion
|
|
* reliabilityWeighting?: boolean // Weight results by reliability for this exclusion
|
|
* }
|
|
* }
|
|
*
|
|
* === EVALUATION MODES ===
|
|
* - Binary mode: Classic defeasible logic with tie-breakers going to priority
|
|
* - Normal mode: Possibilistic space where defeaters raise possibility of denial,
|
|
* Always sets minimum possible, When provides positive possibility that defeaters erode
|
|
* - Threshold mode: Bail out to binary-like operation with fastPath early exits
|
|
*
|
|
* === EVIDENTIAL REASONING EXAMPLES ===
|
|
*
|
|
* Consensus-based authorization (majority rule):
|
|
* {
|
|
* type: 'logical',
|
|
* union: {
|
|
* rules: [managerApproval, peerReview, systemCheck],
|
|
* aggregator: 'majority' // Requires consensus, not just one approval
|
|
* }
|
|
* }
|
|
*
|
|
* Priority-weighted evidence:
|
|
* {
|
|
* type: 'logical',
|
|
* union: {
|
|
* rules: [
|
|
* { type: 'direct', priority: 10 },
|
|
* { type: 'inferred', priority: 5 },
|
|
* { type: 'computed', priority: 1 }
|
|
* ],
|
|
* aggregator: 'priority' // Weight by rule priority values
|
|
* }
|
|
* }
|
|
*
|
|
* Custom evidential fusion:
|
|
* {
|
|
* type: 'logical',
|
|
* intersection: {
|
|
* rules: [securityCheck, businessLogic, complianceRule],
|
|
* aggregator: 'custom',
|
|
* owaWeights: [0.6, 0.3, 0.1], // Security most important, compliance least
|
|
* reliabilityWeighting: true // Also consider reliability of each check
|
|
* }
|
|
* }
|
|
*/
|
|
export class LogicalOperators extends BaseRule {
|
|
constructor(arbiter, ruleEvaluator) {
|
|
super(arbiter);
|
|
this.ruleEvaluator = ruleEvaluator;
|
|
}
|
|
|
|
/**
|
|
* Determine if this is a defeasible logic rule or traditional logical operation
|
|
* @private
|
|
*/
|
|
_isDefeasibleLogic(rule) {
|
|
return !!(rule.never || rule.unless || rule.always || rule.when || rule.requires);
|
|
}
|
|
|
|
/**
|
|
* Normalize rule configuration to support both new per-operation format and legacy format
|
|
* @private
|
|
*/
|
|
_normalizeRule(rule) {
|
|
const normalized = { ...rule };
|
|
|
|
// Handle defeasible logic structure
|
|
if (this._isDefeasibleLogic(rule)) {
|
|
return this._normalizeDefeasibleRule(normalized);
|
|
}
|
|
|
|
// Legacy logical operations normalization
|
|
// Convert legacy array format to new object format with backward compatibility
|
|
if (rule.union && Array.isArray(rule.union)) {
|
|
normalized.union = {
|
|
rules: rule.union,
|
|
aggregator: rule.aggregator,
|
|
owaWeights: rule.owaWeights,
|
|
reliabilityWeighting: rule.reliabilityWeighting
|
|
};
|
|
} else if (rule.union && typeof rule.union === 'object' && rule.union.rules) {
|
|
// New format, apply top-level defaults if not specified
|
|
normalized.union = {
|
|
...rule.union,
|
|
aggregator: rule.union.aggregator || rule.aggregator,
|
|
owaWeights: rule.union.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.union.reliabilityWeighting !== undefined ?
|
|
rule.union.reliabilityWeighting : rule.reliabilityWeighting
|
|
};
|
|
} else if (rule.union && typeof rule.union === 'object') {
|
|
// Handle case where union is an object but missing rules property - treat as legacy
|
|
normalized.union = {
|
|
rules: [],
|
|
...rule.union,
|
|
// Apply top-level defaults if not specified
|
|
aggregator: rule.union.aggregator || rule.aggregator,
|
|
owaWeights: rule.union.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.union.reliabilityWeighting !== undefined ?
|
|
rule.union.reliabilityWeighting : rule.reliabilityWeighting
|
|
};
|
|
}
|
|
|
|
if (rule.intersection && Array.isArray(rule.intersection)) {
|
|
normalized.intersection = {
|
|
rules: rule.intersection,
|
|
aggregator: rule.aggregator,
|
|
owaWeights: rule.owaWeights,
|
|
reliabilityWeighting: rule.reliabilityWeighting
|
|
};
|
|
} else if (rule.intersection && typeof rule.intersection === 'object' && rule.intersection.rules) {
|
|
normalized.intersection = {
|
|
...rule.intersection,
|
|
aggregator: rule.intersection.aggregator || rule.aggregator,
|
|
owaWeights: rule.intersection.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.intersection.reliabilityWeighting !== undefined ?
|
|
rule.intersection.reliabilityWeighting : rule.reliabilityWeighting
|
|
};
|
|
} else if (rule.intersection && typeof rule.intersection === 'object') {
|
|
normalized.intersection = {
|
|
rules: [],
|
|
...rule.intersection,
|
|
aggregator: rule.intersection.aggregator || rule.aggregator,
|
|
owaWeights: rule.intersection.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.intersection.reliabilityWeighting !== undefined ?
|
|
rule.intersection.reliabilityWeighting : rule.reliabilityWeighting
|
|
};
|
|
}
|
|
|
|
if (rule.exclusion && Array.isArray(rule.exclusion)) {
|
|
normalized.exclusion = {
|
|
rules: rule.exclusion,
|
|
aggregator: rule.aggregator,
|
|
owaWeights: rule.owaWeights,
|
|
reliabilityWeighting: rule.reliabilityWeighting
|
|
};
|
|
} else if (rule.exclusion && typeof rule.exclusion === 'object' && rule.exclusion.rules) {
|
|
normalized.exclusion = {
|
|
...rule.exclusion,
|
|
aggregator: rule.exclusion.aggregator || rule.aggregator,
|
|
owaWeights: rule.exclusion.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.exclusion.reliabilityWeighting !== undefined ?
|
|
rule.exclusion.reliabilityWeighting : rule.reliabilityWeighting
|
|
};
|
|
} else if (rule.exclusion && typeof rule.exclusion === 'object') {
|
|
normalized.exclusion = {
|
|
rules: [],
|
|
...rule.exclusion,
|
|
aggregator: rule.exclusion.aggregator || rule.aggregator,
|
|
owaWeights: rule.exclusion.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.exclusion.reliabilityWeighting !== undefined ?
|
|
rule.exclusion.reliabilityWeighting : rule.reliabilityWeighting
|
|
};
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* Normalize defeasible logic rule structure
|
|
* @private
|
|
*/
|
|
_normalizeDefeasibleRule(rule) {
|
|
const normalized = { ...rule };
|
|
|
|
if (rule.union && Array.isArray(rule.union)) {
|
|
normalized.union = {
|
|
rules: rule.union,
|
|
aggregator: rule.aggregator,
|
|
owaWeights: rule.owaWeights,
|
|
reliabilityWeighting: rule.reliabilityWeighting
|
|
};
|
|
} else if (rule.union && rule.union.rules) {
|
|
normalized.union = {
|
|
...rule.union,
|
|
aggregator: rule.union.aggregator || rule.aggregator,
|
|
owaWeights: rule.union.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.union.reliabilityWeighting !== undefined ?
|
|
rule.union.reliabilityWeighting : rule.reliabilityWeighting
|
|
};
|
|
}
|
|
|
|
// Normalize never (absolute denials)
|
|
if (rule.never) {
|
|
if (rule.never.union && Array.isArray(rule.never.union)) {
|
|
normalized.never = {
|
|
union: {
|
|
rules: rule.never.union,
|
|
aggregator: rule.never.aggregator || rule.aggregator,
|
|
owaWeights: rule.never.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.never.reliabilityWeighting !== undefined ?
|
|
rule.never.reliabilityWeighting : rule.reliabilityWeighting
|
|
}
|
|
};
|
|
} else if (rule.never.union) {
|
|
normalized.never = {
|
|
union: {
|
|
...rule.never.union,
|
|
aggregator: rule.never.union.aggregator || rule.never.aggregator || rule.aggregator,
|
|
owaWeights: rule.never.union.owaWeights || rule.never.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.never.union.reliabilityWeighting !== undefined ?
|
|
rule.never.union.reliabilityWeighting :
|
|
(rule.never.reliabilityWeighting !== undefined ? rule.never.reliabilityWeighting : rule.reliabilityWeighting)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
// Normalize unless (defeaters)
|
|
if (rule.unless) {
|
|
if (rule.unless.union && Array.isArray(rule.unless.union)) {
|
|
normalized.unless = {
|
|
union: {
|
|
rules: rule.unless.union,
|
|
aggregator: rule.unless.aggregator || rule.aggregator,
|
|
owaWeights: rule.unless.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.unless.reliabilityWeighting !== undefined ?
|
|
rule.unless.reliabilityWeighting : rule.reliabilityWeighting
|
|
}
|
|
};
|
|
} else if (rule.unless.union) {
|
|
normalized.unless = {
|
|
union: {
|
|
...rule.unless.union,
|
|
aggregator: rule.unless.union.aggregator || rule.unless.aggregator || rule.aggregator,
|
|
owaWeights: rule.unless.union.owaWeights || rule.unless.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.unless.union.reliabilityWeighting !== undefined ?
|
|
rule.unless.union.reliabilityWeighting :
|
|
(rule.unless.reliabilityWeighting !== undefined ? rule.unless.reliabilityWeighting : rule.reliabilityWeighting)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
// Normalize always (strict rules)
|
|
if (rule.always) {
|
|
if (rule.always.direct) {
|
|
normalized.always = {
|
|
...rule.always,
|
|
aggregator: rule.always.aggregator || rule.aggregator,
|
|
owaWeights: rule.always.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.always.reliabilityWeighting !== undefined ?
|
|
rule.always.reliabilityWeighting : rule.reliabilityWeighting
|
|
};
|
|
} else {
|
|
// Treat as direct rule
|
|
normalized.always = {
|
|
direct: rule.always,
|
|
aggregator: rule.aggregator,
|
|
owaWeights: rule.owaWeights,
|
|
reliabilityWeighting: rule.reliabilityWeighting
|
|
};
|
|
}
|
|
}
|
|
|
|
// Normalize when (defeasible rules)
|
|
if (rule.when) {
|
|
if (rule.when.intersection && Array.isArray(rule.when.intersection)) {
|
|
normalized.when = {
|
|
intersection: {
|
|
rules: rule.when.intersection,
|
|
aggregator: rule.when.aggregator || rule.aggregator,
|
|
owaWeights: rule.when.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.when.reliabilityWeighting !== undefined ?
|
|
rule.when.reliabilityWeighting : rule.reliabilityWeighting
|
|
}
|
|
};
|
|
} else if (rule.when.intersection) {
|
|
normalized.when = {
|
|
intersection: {
|
|
...rule.when.intersection,
|
|
aggregator: rule.when.intersection.aggregator || rule.when.aggregator || rule.aggregator,
|
|
owaWeights: rule.when.intersection.owaWeights || rule.when.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.when.intersection.reliabilityWeighting !== undefined ?
|
|
rule.when.intersection.reliabilityWeighting :
|
|
(rule.when.reliabilityWeighting !== undefined ? rule.when.reliabilityWeighting : rule.reliabilityWeighting)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
// Normalize requires (inverse defeaters)
|
|
if (rule.requires) {
|
|
if (rule.requires.union && Array.isArray(rule.requires.union)) {
|
|
normalized.requires = {
|
|
union: {
|
|
rules: rule.requires.union,
|
|
aggregator: rule.requires.aggregator || rule.aggregator,
|
|
owaWeights: rule.requires.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.requires.reliabilityWeighting !== undefined ?
|
|
rule.requires.reliabilityWeighting : rule.reliabilityWeighting
|
|
}
|
|
};
|
|
} else if (rule.requires.union) {
|
|
normalized.requires = {
|
|
union: {
|
|
...rule.requires.union,
|
|
aggregator: rule.requires.union.aggregator || rule.requires.aggregator || rule.aggregator,
|
|
owaWeights: rule.requires.union.owaWeights || rule.requires.owaWeights || rule.owaWeights,
|
|
reliabilityWeighting: rule.requires.union.reliabilityWeighting !== undefined ?
|
|
rule.requires.union.reliabilityWeighting :
|
|
(rule.requires.reliabilityWeighting !== undefined ? rule.requires.reliabilityWeighting : rule.reliabilityWeighting)
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
/**
|
|
* Evaluate logical operations on child rules
|
|
* @protected
|
|
*/
|
|
_evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
|
|
const normalizedRule = this._normalizeRule(rule);
|
|
|
|
// Check if this is defeasible logic
|
|
if (this._isDefeasibleLogic(normalizedRule)) {
|
|
return this.evaluateDefeasible(userId, userKey, objectId, objectKey, normalizedRule, visited, currentRelation, options);
|
|
}
|
|
|
|
// Traditional logical operations
|
|
let result;
|
|
if (normalizedRule.union) {
|
|
result = this.evaluateUnion(userId, userKey, objectId, objectKey, normalizedRule, visited, currentRelation, options);
|
|
} else if (normalizedRule.intersection) {
|
|
result = this.evaluateIntersection(userId, userKey, objectId, objectKey, normalizedRule, visited, currentRelation, options);
|
|
} else if (normalizedRule.exclusion) {
|
|
result = this.evaluateExclusion(userId, userKey, objectId, objectKey, normalizedRule, visited, currentRelation, options);
|
|
} else {
|
|
result = {
|
|
possibility: 0,
|
|
meta: { reason: 'no_logical_operation_specified' }
|
|
};
|
|
}
|
|
|
|
// Apply negation if the rule specifies negate: true (used by NOT operator)
|
|
if (normalizedRule.intersection?.negate || normalizedRule.union?.negate || normalizedRule.exclusion?.negate) {
|
|
const originalPossibility = result.possibility || 0;
|
|
result = {
|
|
...result,
|
|
possibility: Math.max(0, 1 - originalPossibility),
|
|
reason: 'negated'
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Evaluate defeasible logic structure
|
|
* @protected
|
|
*/
|
|
evaluateDefeasible(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
|
|
const { binary = false, fastPath = false, minPossibility = 0.0, valueContext = null } = options;
|
|
const mode = rule.mode || (binary ? 'binary' : (fastPath ? 'threshold' : 'normal'));
|
|
|
|
Arbiter.DEBUG && Arbiter.log('evaluateDefeasible called', {
|
|
mode,
|
|
hasNever: !!rule.never,
|
|
hasUnless: !!rule.unless,
|
|
hasAlways: !!rule.always,
|
|
hasWhen: !!rule.when,
|
|
hasRequires: !!rule.requires,
|
|
priority: rule.priority
|
|
});
|
|
|
|
const allCollectedValues = [];
|
|
let neverResult = null, defeatersResult = null, strictResult = null, defeasibleResult = null, requiresResult = null;
|
|
const timingLevels = { never: 0, always: 0, requires: 0, when: 0, unless: 0, ordinary: 0 };
|
|
|
|
// Evaluate each component
|
|
|
|
// 1. Evaluate absolute denials (never) - highest precedence
|
|
if (rule.never) {
|
|
const levelStart = Date.now();
|
|
const neverRule = { union: rule.never.union };
|
|
neverResult = this.evaluateUnion(userId, userKey, objectId, objectKey, neverRule, visited, currentRelation, options);
|
|
timingLevels.never = Math.max(0, Date.now() - levelStart);
|
|
if (neverResult.collectedValues) {
|
|
allCollectedValues.push(...neverResult.collectedValues);
|
|
}
|
|
}
|
|
|
|
// 2. Evaluate strict rules (always)
|
|
if (rule.always) {
|
|
const levelStart = Date.now();
|
|
strictResult = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, rule.always.direct, visited, currentRelation, options);
|
|
timingLevels.always = Math.max(0, Date.now() - levelStart);
|
|
if (strictResult.collectedValues) {
|
|
allCollectedValues.push(...strictResult.collectedValues);
|
|
}
|
|
}
|
|
|
|
// 3. Evaluate security requirements (requires)
|
|
if (rule.requires) {
|
|
const levelStart = Date.now();
|
|
const requiresRule = { union: rule.requires.union };
|
|
requiresResult = this.evaluateUnion(userId, userKey, objectId, objectKey, requiresRule, visited, currentRelation, options);
|
|
timingLevels.requires = Math.max(0, Date.now() - levelStart);
|
|
if (requiresResult.collectedValues) {
|
|
allCollectedValues.push(...requiresResult.collectedValues);
|
|
}
|
|
}
|
|
|
|
// 4. Evaluate defeaters (unless) - invert minPossibility for defeater evaluation
|
|
if (rule.unless) {
|
|
const levelStart = Date.now();
|
|
const unlessRule = { union: rule.unless.union };
|
|
defeatersResult = this.evaluateUnion(userId, userKey, objectId, objectKey, unlessRule, visited, currentRelation, options);
|
|
timingLevels.unless = Math.max(0, Date.now() - levelStart);
|
|
if (defeatersResult.collectedValues) {
|
|
allCollectedValues.push(...defeatersResult.collectedValues);
|
|
}
|
|
}
|
|
|
|
// 5. Evaluate defeasible rules (when or base union)
|
|
if (rule.when) {
|
|
const levelStart = Date.now();
|
|
const whenRule = { intersection: rule.when.intersection };
|
|
defeasibleResult = this.evaluateIntersection(userId, userKey, objectId, objectKey, whenRule, visited, currentRelation, options);
|
|
timingLevels.when = Math.max(0, Date.now() - levelStart);
|
|
if (defeasibleResult.collectedValues) {
|
|
allCollectedValues.push(...defeasibleResult.collectedValues);
|
|
}
|
|
} else if (rule.union) {
|
|
const levelStart = Date.now();
|
|
const unionRule = { union: rule.union };
|
|
defeasibleResult = this.evaluateUnion(userId, userKey, objectId, objectKey, unionRule, visited, currentRelation, options);
|
|
timingLevels.when = Math.max(0, Date.now() - levelStart);
|
|
if (defeasibleResult.collectedValues) {
|
|
allCollectedValues.push(...defeasibleResult.collectedValues);
|
|
}
|
|
}
|
|
|
|
// Now apply defeasible logic based on mode
|
|
const finalResult = this._applyDefeasibleLogic(mode, rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues);
|
|
if (finalResult && finalResult.meta) {
|
|
finalResult.meta.timingLevels = timingLevels;
|
|
}
|
|
return finalResult;
|
|
}
|
|
|
|
/**
|
|
* Apply defeasible logic based on evaluation mode
|
|
* @private
|
|
*/
|
|
_applyDefeasibleLogic(mode, rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues) {
|
|
switch (mode) {
|
|
case 'binary':
|
|
return this._applyBinaryDefeasibleLogic(rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues);
|
|
case 'threshold':
|
|
return this._applyThresholdDefeasibleLogic(rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues);
|
|
case 'normal':
|
|
default:
|
|
return this._applyNormalDefeasibleLogic(rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply binary defeasible logic (classic defeasible logic with priority tie-breaking)
|
|
* @private
|
|
*/
|
|
_applyBinaryDefeasibleLogic(rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues) {
|
|
|
|
const minPossibility = rule.minPossibility < 0.5 ? 0.5 : (rule.minPossibility || 0.5);
|
|
// Convert to binary decisions
|
|
const isNever = (neverResult && neverResult.possibility > minPossibility);
|
|
const isDefeated = (defeatersResult && defeatersResult.possibility > minPossibility);
|
|
const isStrict = (strictResult && strictResult.possibility > minPossibility);
|
|
const isDefeasible = (defeasibleResult && defeasibleResult.possibility > minPossibility);
|
|
const isRequired = !requiresResult || (requiresResult.possibility > minPossibility);
|
|
|
|
// NEVER rules override everything - highest precedence
|
|
if (isNever) {
|
|
return {
|
|
possibility: 0,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...neverResult.meta,
|
|
mode: 'binary',
|
|
never: isNever,
|
|
reason: 'never_rule_triggered',
|
|
defeats: [{ defeater: 'never', target: 'when', evidence: 'never_rule', possibility: neverResult.possibility }]
|
|
}
|
|
};
|
|
}
|
|
|
|
// Handle cases where we only have strict or defeasible rules
|
|
const hasStrictRules = !!strictResult;
|
|
const hasDefeasibleRules = !!defeasibleResult;
|
|
|
|
// If we only have strict rules, ignore defeasible logic
|
|
if (hasStrictRules && !hasDefeasibleRules) {
|
|
return {
|
|
possibility: isStrict ? 1 : 0,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...strictResult.meta,
|
|
mode: 'binary',
|
|
reason: isStrict ? 'strict_rule_passed' : 'strict_rule_failed',
|
|
never: isNever,
|
|
strict: isStrict,
|
|
strictOnly: true
|
|
}
|
|
};
|
|
}
|
|
|
|
// If we only have defeasible rules, ignore strict logic
|
|
if (!hasStrictRules && hasDefeasibleRules) {
|
|
return {
|
|
possibility: isDefeasible ? 1 : 0,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...defeasibleResult.meta,
|
|
mode: 'binary',
|
|
never: isNever,
|
|
defeasible: isDefeasible,
|
|
defeasibleOnly: true
|
|
}
|
|
};
|
|
}
|
|
|
|
|
|
// Strict rules always win (unless NEVER is true)
|
|
if (isStrict) {
|
|
return {
|
|
possibility: 1,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...strictResult.meta,
|
|
mode: 'binary',
|
|
never: isNever,
|
|
defeaters: isDefeated,
|
|
strict: isStrict,
|
|
defeasible: isDefeasible,
|
|
required: isRequired
|
|
}
|
|
};
|
|
}
|
|
// Check if requirements not met (security requirements take precedence over defeaters)
|
|
else if (!isRequired) {
|
|
return {
|
|
possibility: 0,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...requiresResult.meta,
|
|
mode: 'binary',
|
|
never: isNever,
|
|
defeaters: isDefeated,
|
|
strict: isStrict,
|
|
defeasible: isDefeasible,
|
|
required: isRequired,
|
|
reason: 'requirements_not_met',
|
|
defeats: [{ defeater: 'requires', target: 'when', evidence: 'requirements_not_met', possibility: requiresResult.possibility }]
|
|
}
|
|
};
|
|
}
|
|
// Check if defeated
|
|
else if (isDefeated) {
|
|
return {
|
|
possibility: 0,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...defeatersResult.meta,
|
|
mode: 'binary',
|
|
never: isNever,
|
|
defeaters: isDefeated,
|
|
strict: isStrict,
|
|
defeasible: isDefeasible,
|
|
required: isRequired,
|
|
reason: 'defeated_by_unless',
|
|
defeats: [{ defeater: 'unless', target: 'when', evidence: 'defeated_by_unless', possibility: defeatersResult.possibility }]
|
|
}
|
|
};
|
|
}
|
|
// Defeasible rules apply if not defeated
|
|
else if (isDefeasible) {
|
|
return {
|
|
possibility: 1,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...defeasibleResult.meta,
|
|
mode: 'binary',
|
|
never: isNever,
|
|
defeaters: isDefeated,
|
|
strict: isStrict,
|
|
defeasible: isDefeasible,
|
|
required: isRequired
|
|
}
|
|
};
|
|
}
|
|
|
|
// Default case - no rules matched
|
|
return {
|
|
possibility: 0,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
mode: 'binary',
|
|
never: isNever,
|
|
defeaters: isDefeated,
|
|
strict: isStrict,
|
|
defeasible: isDefeasible,
|
|
required: isRequired
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Apply threshold defeasible logic (early exit to binary-like operation)
|
|
* @private
|
|
*/
|
|
_applyThresholdDefeasibleLogic(rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues) {
|
|
|
|
// Use threshold logic similar to binary but with early exits
|
|
// NEVER rules override everything - highest precedence
|
|
if (neverResult && neverResult.possibility >= 0.8) {
|
|
return {
|
|
possibility: 0,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...neverResult.meta,
|
|
mode: 'threshold',
|
|
earlyExit: 'never_rule',
|
|
never: true,
|
|
defeats: [{ defeater: 'never', target: 'when', evidence: 'never_rule', possibility: neverResult.possibility }]
|
|
}
|
|
};
|
|
}
|
|
|
|
// Strict rules override everything except NEVER
|
|
if (strictResult && strictResult.possibility >= 0.8) {
|
|
return {
|
|
possibility: strictResult.possibility,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...strictResult.meta,
|
|
mode: 'threshold',
|
|
earlyExit: 'strict_rule',
|
|
never: neverResult?.possibility >= 0.8
|
|
}
|
|
};
|
|
}
|
|
|
|
// Requirements not met (security requirements take precedence over defeaters)
|
|
if (requiresResult && requiresResult.possibility < 0.2) {
|
|
return {
|
|
possibility: requiresResult.possibility,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...requiresResult.meta,
|
|
mode: 'threshold',
|
|
earlyExit: 'requirements_not_met',
|
|
defeats: [{ defeater: 'requires', target: 'when', evidence: 'requirements_not_met', possibility: requiresResult.possibility }]
|
|
}
|
|
};
|
|
}
|
|
|
|
// Strong defeaters override everything except strict and requirements
|
|
if (defeatersResult && defeatersResult.possibility >= 0.8) {
|
|
return {
|
|
possibility: 1 - defeatersResult.possibility, // Invert defeater strength
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...defeatersResult.meta,
|
|
mode: 'threshold',
|
|
earlyExit: 'defeater',
|
|
defeats: [{ defeater: 'unless', target: 'when', evidence: 'defeater', possibility: defeatersResult.possibility }]
|
|
}
|
|
};
|
|
}
|
|
|
|
// Fall back to normal logic
|
|
return this._applyNormalDefeasibleLogic(rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues);
|
|
}
|
|
|
|
/**
|
|
* Apply normal defeasible logic (possibilistic space)
|
|
* @private
|
|
*/
|
|
_applyNormalDefeasibleLogic(rule, neverResult, defeatersResult, strictResult, defeasibleResult, requiresResult, allCollectedValues) {
|
|
|
|
let possibility = 0;
|
|
let resultMeta = {};
|
|
// Reliability mirrors the possibility combination: the base leg's
|
|
// reliability is eroded by requirements and defeaters (both legs of
|
|
// each combination matter), consistent with TTU path semantics.
|
|
let reliability = 1.0;
|
|
|
|
// NEVER rules override everything when their evidence possibility exceeds threshold (0.5 default)
|
|
if (neverResult && neverResult.possibility >= 0.5) {
|
|
possibility = 0;
|
|
reliability = neverResult.reliability !== undefined ? neverResult.reliability : 1.0;
|
|
resultMeta = { ...neverResult.meta, never: true };
|
|
return {
|
|
possibility: 0,
|
|
reliability,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...resultMeta,
|
|
mode: 'normal',
|
|
components: {
|
|
never: neverResult.possibility,
|
|
defeaters: defeatersResult?.possibility || 0,
|
|
strict: strictResult?.possibility || 0,
|
|
defeasible: defeasibleResult?.possibility || 0,
|
|
requires: requiresResult?.possibility || 1
|
|
},
|
|
defeats: [{ defeater: 'never', target: 'when', evidence: 'never_rule', possibility: neverResult.possibility }]
|
|
}
|
|
};
|
|
}
|
|
|
|
// Start with defeasible rules as base
|
|
if (defeasibleResult) {
|
|
possibility = defeasibleResult.possibility;
|
|
reliability = defeasibleResult.reliability !== undefined ? defeasibleResult.reliability : 1.0;
|
|
resultMeta = { ...defeasibleResult.meta };
|
|
}
|
|
|
|
// Strict rules set minimum allow possibility
|
|
if (strictResult) {
|
|
possibility = Math.max(possibility, strictResult.possibility);
|
|
if (strictResult.possibility > (defeasibleResult?.possibility || 0)) {
|
|
reliability = strictResult.reliability !== undefined ? strictResult.reliability : 1.0;
|
|
resultMeta = { ...strictResult.meta };
|
|
}
|
|
}
|
|
|
|
// Requirements not met reduce possibility (security requirements take precedence)
|
|
if (requiresResult) {
|
|
const reqStrength = requiresResult.possibility;
|
|
possibility = possibility * reqStrength; // Requirements failure reduces possibility
|
|
reliability = reliability * (requiresResult.reliability !== undefined ? requiresResult.reliability : 1.0);
|
|
if (reqStrength < 0.5) {
|
|
resultMeta = { ...requiresResult.meta, requirementsFailed: requiresResult.meta };
|
|
}
|
|
}
|
|
|
|
// Defeaters reduce possibility (after requirements are checked)
|
|
if (defeatersResult) {
|
|
const defeatStrength = defeatersResult.possibility;
|
|
possibility = possibility * (1 - defeatStrength); // Defeaters erode possibility
|
|
reliability = reliability * (defeatersResult.reliability !== undefined ? defeatersResult.reliability : 1.0);
|
|
if (defeatStrength > 0.5) {
|
|
resultMeta = { ...defeatersResult.meta, defeatedBy: defeatersResult.meta };
|
|
}
|
|
}
|
|
|
|
return {
|
|
possibility: Math.max(0, Math.min(1, possibility)),
|
|
reliability,
|
|
collectedValues: allCollectedValues,
|
|
meta: {
|
|
...resultMeta,
|
|
mode: 'normal',
|
|
components: {
|
|
never: neverResult?.possibility || 0,
|
|
defeaters: defeatersResult?.possibility || 0,
|
|
strict: strictResult?.possibility || 0,
|
|
defeasible: defeasibleResult?.possibility || 0,
|
|
requires: requiresResult?.possibility || 1
|
|
},
|
|
defeats: [
|
|
...(requiresResult && requiresResult.possibility < 0.5 ? [{ defeater: 'requires', target: 'when', evidence: 'requirements_not_met', possibility: requiresResult.possibility }] : []),
|
|
...(defeatersResult ? [{ defeater: 'unless', target: 'when', evidence: 'defeaters_applied', possibility: defeatersResult.possibility }] : [])
|
|
]
|
|
}
|
|
};
|
|
}
|
|
|
|
evaluateUnion(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
|
|
// Normalize the rule to handle both legacy and new formats
|
|
const normalizedRule = this._normalizeRule(rule);
|
|
|
|
const unionConfig = normalizedRule.union;
|
|
const childRules = unionConfig.rules || [];
|
|
|
|
let possibilities = [], reliabilities = [], metas = [], reasons = [];
|
|
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
|
|
const includeOwaTrace = trackEvaluation && includeMeta;
|
|
const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null;
|
|
|
|
|
|
for (const child of childRules) {
|
|
|
|
let res = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, child, visited, currentRelation, options);
|
|
if (!res || typeof res.possibility !== 'number') {
|
|
res = { possibility: 0, meta: { reason: 'missing_rule' }, collectedValues: [] };
|
|
}
|
|
|
|
|
|
if (res.reason === 'cycle') reasons.push('cycle');
|
|
mergeRemediationOptions(remediationOptions, extractRemediation(res));
|
|
possibilities.push(res.possibility);
|
|
reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0);
|
|
metas.push(includeMeta ? res.meta : null);
|
|
|
|
// Collect values from child rule results
|
|
if (collectValues && res.collectedValues && Array.isArray(res.collectedValues)) {
|
|
allCollectedValues.push(...res.collectedValues);
|
|
|
|
// Add to shared value context if available
|
|
if (valueContext) {
|
|
valueContext.addCollectedValues(res.collectedValues, child.type, child);
|
|
}
|
|
}
|
|
|
|
// Fast path for union: if ANY rule gives strong possibility, we can stop (OR semantics)
|
|
if (fastPath && minPossibility !== null && res.possibility >= minPossibility) {
|
|
Arbiter.DEBUG && Arbiter.log('union early exit: threshold met', {
|
|
threshold: minPossibility,
|
|
actual: res.possibility,
|
|
rulesEvaluated: possibilities.length,
|
|
totalRules: childRules.length
|
|
});
|
|
|
|
const remediation = buildRemediation(extractRemediation(res));
|
|
return {
|
|
possibility: res.possibility,
|
|
reliability: res.reliability !== undefined ? res.reliability : 1.0,
|
|
...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values so far
|
|
...(includeMeta && { meta: res.meta }),
|
|
...(remediation ? { remediation } : {}),
|
|
reason: reasons.includes('cycle') ? 'cycle' : undefined
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
if (!possibilities.length) {
|
|
return {
|
|
possibility: 0,
|
|
reliability: 1.0,
|
|
...(collectValues && { collectedValues: allCollectedValues }),
|
|
...(includeMeta && { meta: { operation: 'union', childCount: 0 } }),
|
|
reason: reasons.includes('cycle') ? 'cycle' : undefined
|
|
};
|
|
}
|
|
|
|
// Apply OWA fusion with per-operation aggregator support and optional bilattice reasoning
|
|
const unionOWAWeights = getOWAWeightsFromRule(unionConfig, possibilities.length, metas);
|
|
const useBilattice = unionConfig.useBilattice || false;
|
|
const epistemicMode = unionConfig.epistemicMode || 'hybrid';
|
|
const capacityType = unionConfig.capacityType || 'simple_support';
|
|
|
|
let result;
|
|
let epistemicAnalysis = null;
|
|
|
|
// Use bilattice-enhanced evidence combination if enabled
|
|
if (useBilattice && collectValues && allCollectedValues.length > 0) {
|
|
const scale = QualitativeScale.fivePoint(); // Default scale for bilattice analysis
|
|
const capacity = this._createCapacityFromValues(allCollectedValues, scale, capacityType);
|
|
|
|
const bilatticeResult = this._combineEvidenceWithBilattice(allCollectedValues, {
|
|
method: unionConfig.aggregator || 'max',
|
|
useBilattice: true,
|
|
capacity: capacity,
|
|
scale: scale,
|
|
epistemicMode: epistemicMode
|
|
});
|
|
|
|
result = {
|
|
value: bilatticeResult.value,
|
|
meta: bilatticeResult.aggregationMeta?.selectedSource || metas[0]
|
|
};
|
|
epistemicAnalysis = bilatticeResult.epistemicAnalysis;
|
|
|
|
Arbiter.DEBUG && Arbiter.log('union using bilattice evidential fusion', {
|
|
aggregator: unionConfig.aggregator || 'max',
|
|
epistemicMode: epistemicMode,
|
|
capacityType: capacityType,
|
|
inputCount: possibilities.length,
|
|
bilatticeAnalysis: epistemicAnalysis
|
|
});
|
|
} else if (unionConfig.reliabilityWeighting) {
|
|
const weighted = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0));
|
|
result = OWAFusion.fuseWithMeta(weighted, metas, unionOWAWeights, unionConfig.aggregator || 'max', true, owaTraceOptions);
|
|
} else if (unionOWAWeights.some(w => w > 0)) {
|
|
Arbiter.DEBUG && Arbiter.log('union using OWA evidential fusion', {
|
|
aggregator: unionConfig.aggregator || 'max',
|
|
weights: unionOWAWeights,
|
|
inputPossibilities: possibilities,
|
|
evidentialFusion: {
|
|
mode: unionConfig.aggregator || 'max',
|
|
customWeights: !!unionConfig.owaWeights,
|
|
sophisticatedAggregation: unionConfig.aggregator !== 'max'
|
|
}
|
|
});
|
|
|
|
result = OWAFusion.fuseWithMeta(possibilities, metas, unionOWAWeights, unionConfig.aggregator || 'max', true, owaTraceOptions);
|
|
} else {
|
|
result = OWAFusion.fuseWithMeta(possibilities, metas, null, 'max', true, owaTraceOptions);
|
|
}
|
|
|
|
Arbiter.DEBUG && Arbiter.log('union evidential fusion results:', {
|
|
result,
|
|
fusedPossibility: result.value,
|
|
selectedMeta: result.meta,
|
|
evidentialSummary: {
|
|
inputCount: possibilities.length,
|
|
fusionMethod: unionConfig.aggregator || 'max',
|
|
resultPossibility: result.value,
|
|
sourceRule: result.meta?.ruleType || result.meta?.operation || 'unknown'
|
|
}
|
|
});
|
|
|
|
const unionRemediation = result.value === 0
|
|
? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' })
|
|
: null;
|
|
// Reliability of the child driving the fused value: the OWA selected
|
|
// source when traced, else the max/min possibility child (max is the
|
|
// default union aggregator).
|
|
let unionReliability = 1.0;
|
|
if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') {
|
|
unionReliability = reliabilities[result.trace.selectedIndex] ?? 1.0;
|
|
} else if ((unionConfig.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;
|
|
}
|
|
return {
|
|
possibility: result.value,
|
|
reliability: unionReliability,
|
|
...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values from child rules
|
|
...(includeMeta && {
|
|
meta: {
|
|
...result.meta,
|
|
operation: 'union',
|
|
childCount: possibilities.length,
|
|
aggregator: unionConfig.aggregator || 'max',
|
|
useBilattice: useBilattice,
|
|
epistemicMode: useBilattice ? epistemicMode : undefined,
|
|
epistemicAnalysis: epistemicAnalysis,
|
|
...(includeOwaTrace && result.trace ? {
|
|
owa: {
|
|
level: null,
|
|
aggregator: unionConfig.aggregator || 'max',
|
|
weights: result.trace.weights,
|
|
sortedValues: result.trace.sortedValues,
|
|
contributions: result.trace.contributions,
|
|
selectedIndex: result.trace.selectedIndex
|
|
}
|
|
} : {}),
|
|
...(unionRemediation ? { remediation: unionRemediation } : {})
|
|
}
|
|
}),
|
|
...(unionRemediation ? { remediation: unionRemediation } : {}),
|
|
reason: reasons.includes('cycle') ? 'cycle' : undefined
|
|
};
|
|
}
|
|
|
|
evaluateIntersection(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
|
|
// Normalize the rule to handle both legacy and new formats
|
|
const normalizedRule = this._normalizeRule(rule);
|
|
|
|
const intersectionConfig = normalizedRule.intersection;
|
|
const childRules = intersectionConfig.rules || [];
|
|
|
|
let possibilities = [], reliabilities = [], metas = [], reasons = [];
|
|
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
|
|
const includeOwaTrace = trackEvaluation && includeMeta;
|
|
const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null;
|
|
|
|
Arbiter.DEBUG && Arbiter.log('evaluateIntersection called', {
|
|
fastPath,
|
|
minPossibility,
|
|
numRules: childRules.length,
|
|
intersectionAggregator: intersectionConfig.aggregator,
|
|
hasCustomWeights: !!intersectionConfig.owaWeights,
|
|
hasValueContext: !!valueContext
|
|
});
|
|
|
|
for (const child of childRules) {
|
|
const res = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, child, visited, currentRelation, options);
|
|
if (res.reason === 'cycle') reasons.push('cycle');
|
|
possibilities.push(res.possibility);
|
|
reliabilities.push(res.reliability !== undefined ? res.reliability : 1.0);
|
|
metas.push(includeMeta ? res.meta : null);
|
|
mergeRemediationOptions(remediationOptions, extractRemediation(res));
|
|
|
|
// Collect values from child rule results
|
|
if (collectValues && res.collectedValues && Array.isArray(res.collectedValues)) {
|
|
allCollectedValues.push(...res.collectedValues);
|
|
|
|
// Add to shared value context if available
|
|
if (valueContext) {
|
|
valueContext.addCollectedValues(res.collectedValues, child.type, child);
|
|
}
|
|
}
|
|
|
|
// Fast path for intersection: if ANY rule falls below the allow
|
|
// threshold, the conjunction is denied — stop (AND semantics).
|
|
// NOTE: must compare against minPossibility itself, not (1 - minPossibility):
|
|
// for t < 0.5 a child in [t, 1-t) triggered the old exit with a value
|
|
// >= t, producing a false allow while the true min was below t.
|
|
if (fastPath && minPossibility !== null && res.possibility < minPossibility) {
|
|
Arbiter.DEBUG && Arbiter.log('intersection early exit: low possibility threshold met', {
|
|
threshold: minPossibility,
|
|
actual: res.possibility,
|
|
rulesEvaluated: possibilities.length,
|
|
totalRules: childRules.length
|
|
});
|
|
|
|
return {
|
|
possibility: res.possibility,
|
|
...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values so far
|
|
...(includeMeta && { meta: res.meta }),
|
|
reason: reasons.includes('cycle') ? 'cycle' : undefined
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!possibilities.length) {
|
|
return {
|
|
possibility: 0,
|
|
...(collectValues && { collectedValues: allCollectedValues }),
|
|
...(includeMeta && { meta: { operation: 'intersection', childCount: 0 } }),
|
|
reason: reasons.includes('cycle') ? 'cycle' : undefined
|
|
};
|
|
}
|
|
|
|
// Use intersection-specific aggregator configuration
|
|
const intersectionOWAWeights = getOWAWeightsFromRule(intersectionConfig, possibilities.length, metas);
|
|
|
|
// For intersection (AND), default to min semantics if no aggregator specified
|
|
const defaultMode = intersectionConfig.aggregator || 'min';
|
|
const finalWeights = intersectionConfig.aggregator || intersectionConfig.owaWeights ?
|
|
intersectionOWAWeights :
|
|
[...Array(Math.max(0, possibilities.length - 1)).fill(0), 1]; // min weights
|
|
|
|
Arbiter.DEBUG && Arbiter.log('intersection using aggregation', {
|
|
aggregator: defaultMode,
|
|
weights: finalWeights,
|
|
});
|
|
|
|
let result;
|
|
if (intersectionConfig.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 intersectionRemediation = result.value === 0
|
|
? buildRemediation(remediationOptions.length > 0 ? { status: 'required', options: remediationOptions } : null, { status: 'required' })
|
|
: null;
|
|
// Reliability of the child driving the fused value (min is the
|
|
// default intersection aggregator).
|
|
let intersectionReliability = 1.0;
|
|
if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') {
|
|
intersectionReliability = reliabilities[result.trace.selectedIndex] ?? 1.0;
|
|
} else if ((intersectionConfig.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;
|
|
}
|
|
return {
|
|
possibility: result.value,
|
|
reliability: intersectionReliability,
|
|
...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values from child rules
|
|
...(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
|
|
}
|
|
} : {}),
|
|
...(intersectionRemediation ? { remediation: intersectionRemediation } : {})
|
|
}
|
|
}),
|
|
...(intersectionRemediation ? { remediation: intersectionRemediation } : {}),
|
|
reason: reasons.includes('cycle') ? 'cycle' : undefined
|
|
};
|
|
}
|
|
|
|
evaluateExclusion(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) {
|
|
// Normalize the rule to handle both legacy and new formats
|
|
const normalizedRule = this._normalizeRule(rule);
|
|
|
|
const exclusionConfig = normalizedRule.exclusion;
|
|
const childRules = exclusionConfig.rules || [];
|
|
|
|
const { valueContext = null, collectValues = false, includeMeta = true, trackEvaluation = false } = options;
|
|
const includeOwaTrace = trackEvaluation && includeMeta;
|
|
const owaTraceOptions = includeOwaTrace ? { includeTrace: true } : null;
|
|
if (childRules.length !== 2) {
|
|
return {
|
|
possibility: 0,
|
|
...(collectValues && { collectedValues: [] }),
|
|
...(includeMeta && {
|
|
meta: {
|
|
operation: 'exclusion',
|
|
error: 'exclusion_requires_exactly_two_rules'
|
|
}
|
|
}),
|
|
reason: 'exclusion_requires_exactly_two_rules'
|
|
};
|
|
}
|
|
|
|
Arbiter.DEBUG && Arbiter.log('evaluateExclusion called', {
|
|
numRules: childRules.length,
|
|
exclusionAggregator: exclusionConfig.aggregator,
|
|
hasCustomWeights: !!exclusionConfig.owaWeights,
|
|
hasValueContext: !!valueContext
|
|
});
|
|
|
|
const a = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, childRules[0], visited, currentRelation, options);
|
|
const b = this.ruleEvaluator.evaluateRule(userId, userKey, objectId, objectKey, childRules[1], visited, currentRelation, options);
|
|
|
|
// Collect values from both child rules
|
|
const allCollectedValues = collectValues ? [] : null;
|
|
if (collectValues && a.collectedValues && Array.isArray(a.collectedValues)) {
|
|
allCollectedValues.push(...a.collectedValues);
|
|
if (valueContext) {
|
|
valueContext.addCollectedValues(a.collectedValues, childRules[0].type, childRules[0]);
|
|
}
|
|
}
|
|
if (collectValues && b.collectedValues && Array.isArray(b.collectedValues)) {
|
|
allCollectedValues.push(...b.collectedValues);
|
|
if (valueContext) {
|
|
valueContext.addCollectedValues(b.collectedValues, childRules[1].type, childRules[1]);
|
|
}
|
|
}
|
|
|
|
if (a.reason === 'cycle' || b.reason === 'cycle') {
|
|
return {
|
|
possibility: 0,
|
|
...(collectValues && { collectedValues: allCollectedValues }),
|
|
...(includeMeta && {
|
|
meta: {
|
|
operation: 'exclusion',
|
|
childA: a.meta,
|
|
childB: b.meta
|
|
}
|
|
}),
|
|
reason: 'cycle'
|
|
};
|
|
}
|
|
|
|
// Exclusion logic: A AND NOT B
|
|
// For possibility theory: possibility = P(A) * (1 - P(B))
|
|
let possibility;
|
|
let result = null;
|
|
|
|
if (exclusionConfig.aggregator || exclusionConfig.owaWeights) {
|
|
// Use custom aggregation for exclusion if specified
|
|
const possibilities = [a.possibility, 1 - b.possibility];
|
|
const metas = [a.meta, { exclusion_complement: b.meta }];
|
|
|
|
const exclusionOWAWeights = getOWAWeightsFromRule(exclusionConfig, 2, metas);
|
|
|
|
Arbiter.DEBUG && Arbiter.log('exclusion using custom aggregation', {
|
|
aggregator: exclusionConfig.aggregator,
|
|
weights: exclusionOWAWeights,
|
|
});
|
|
|
|
if (exclusionConfig.reliabilityWeighting) {
|
|
const legReliabilities = [
|
|
a.reliability !== undefined ? a.reliability : 1.0,
|
|
b.reliability !== undefined ? b.reliability : 1.0
|
|
];
|
|
const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (legReliabilities[i] ?? 1.0));
|
|
result = OWAFusion.fuseWithMeta(reliabilityWeightedPossibilities, metas, exclusionOWAWeights, exclusionConfig.aggregator || 'min', true, owaTraceOptions);
|
|
} else {
|
|
result = OWAFusion.fuseWithMeta(possibilities, metas, exclusionOWAWeights, exclusionConfig.aggregator || 'min', true, owaTraceOptions);
|
|
}
|
|
|
|
possibility = result.value;
|
|
} else {
|
|
// Standard exclusion logic: A AND NOT B
|
|
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),
|
|
// 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 }),
|
|
...(collectValues && { collectedValues: allCollectedValues }), // Include all collected values from both child rules
|
|
...(includeMeta && {
|
|
meta: {
|
|
operation: 'exclusion',
|
|
childA: a.meta,
|
|
childB: b.meta,
|
|
aggregator: exclusionConfig.aggregator || 'standard',
|
|
...(includeOwaTrace && result?.trace ? {
|
|
owa: {
|
|
level: null,
|
|
aggregator: exclusionConfig.aggregator || 'standard',
|
|
weights: result.trace.weights,
|
|
sortedValues: result.trace.sortedValues,
|
|
contributions: result.trace.contributions,
|
|
selectedIndex: result.trace.selectedIndex
|
|
}
|
|
} : {})
|
|
}
|
|
})
|
|
};
|
|
}
|
|
|
|
}
|