1280 lines
50 KiB
JavaScript
1280 lines
50 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 = {};
|
||
|
|
|
||
|
|
// NEVER rules override everything when their evidence possibility exceeds threshold (0.5 default)
|
||
|
|
if (neverResult && neverResult.possibility >= 0.5) {
|
||
|
|
possibility = 0;
|
||
|
|
resultMeta = { ...neverResult.meta, never: true };
|
||
|
|
return {
|
||
|
|
possibility: 0,
|
||
|
|
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;
|
||
|
|
resultMeta = { ...defeasibleResult.meta };
|
||
|
|
}
|
||
|
|
|
||
|
|
// Strict rules set minimum allow possibility
|
||
|
|
if (strictResult) {
|
||
|
|
possibility = Math.max(possibility, strictResult.possibility);
|
||
|
|
if (strictResult.possibility > (defeasibleResult?.possibility || 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
|
||
|
|
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
|
||
|
|
if (defeatStrength > 0.5) {
|
||
|
|
resultMeta = { ...defeatersResult.meta, defeatedBy: defeatersResult.meta };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
possibility: Math.max(0, Math.min(1, possibility)),
|
||
|
|
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 = [], 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);
|
||
|
|
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,
|
||
|
|
...(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,
|
||
|
|
...(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;
|
||
|
|
return {
|
||
|
|
possibility: result.value,
|
||
|
|
...(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 = [], 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);
|
||
|
|
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;
|
||
|
|
return {
|
||
|
|
possibility: result.value,
|
||
|
|
...(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,
|
||
|
|
// 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
|
||
|
|
}
|
||
|
|
} : {})
|
||
|
|
}
|
||
|
|
})
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|