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, * aggregator?: string, // OWA aggregation for absolute denials * owaWeights?: Array, * reliabilityWeighting?: boolean * } * }, * unless: { // DEFEATERS - If true, defeats the rule * union: { * rules: Array, * aggregator?: string, // OWA aggregation for defeaters * owaWeights?: Array, * 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, * reliabilityWeighting?: boolean * }, * when: { // DEFEASIBLE RULES - Potentially defeasible * intersection: { * rules: Array, * aggregator?: string, // OWA aggregation for defeasible conditions * owaWeights?: Array, * reliabilityWeighting?: boolean * } * }, * requires: { // INVERSE DEFEATERS - If not true, defeats * union: { * rules: Array, * aggregator?: string, // OWA aggregation for requirements * owaWeights?: Array, * 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, // OR operation on child rules * aggregator?: string, // OWA aggregation method: max, average, majority, optimistic, etc. * owaWeights?: Array, // Custom OWA weights for sophisticated evidential fusion * reliabilityWeighting?: boolean // Weight results by reliability for this union * }, * intersection: { * rules: Array, // AND operation on child rules * aggregator?: string, // OWA aggregation method: min, average, pessimistic, etc. * owaWeights?: Array, // 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, // 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 (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 * (metas[i]?.reliability || 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 reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (metas[i]?.reliability || 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 } } : {}) } }) }; } }