Files
core/src/authorization/rules/LogicalOperators.js
T

1332 lines
54 KiB
JavaScript
Raw Normal View History

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
}
} : {})
}
})
};
}
}