import { OWAFusion } from '../../utils/OWAFusion.js'; import { buildValidity, normalizeValidity, DEFAULT_VALIDITY } from '../../core/validity.js'; import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js'; import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js'; import { QualitativeScale } from '../../qualitative/QualitativeScale.js'; import { UnifiedEvidenceFusion } from '../../qualitative/UnifiedEvidenceFusion.js'; /** * BaseRule - Standard interface for all authorization rules * * This abstract base class defines the standard API that all rule implementations * must follow to ensure consistency, performance, and maintainability. * * STANDARDIZED RESULT STRUCTURE: * All rules return both authorization results AND collected values: * { * // AUTHORIZATION (core purpose) * possibility_allow: number, // 0-1, aggregated possibility across paths * possibility_deny: number, // 0-1, denial confidence * reliability: number, // 0-1, confidence in evaluation * * // VALUE COLLECTION (optional metadata) * collectedValues: [ // Array of individual values with paths * { * value: number, // The actual value * possibility: number, // Individual confidence in this value * path: string[], // Full traversal path to this value * source: { // Where value came from * entityKey: string, * relation: string, * step: number * }, * metadata: { // Rich metadata * timestamp: number, * reliability: number, * decay: object * } * } * ], * * // EXISTING FIELDS * meta_allow: object, * meta_deny: object, * reason: string * } */ export class BaseRule { constructor(arbiter) { if (new.target === BaseRule) { throw new Error('BaseRule is abstract and cannot be instantiated directly'); } this.arbiter = arbiter; this.ruleType = this.constructor.name.replace('Rule', '').toLowerCase(); // Performance tracking this.evaluationCount = 0; this.totalEvaluationTime = 0; } /** * Standard rule evaluation interface * * @param {number} userId - Internal user node ID * @param {string} userKey - User key (e.g., "user:alice") * @param {number} objectId - Internal object node ID * @param {string} objectKey - Object key (e.g., "doc:secret") * @param {Object} rule - Rule configuration object * @param {Set} visited - Set of visited nodes for cycle detection * @param {string} currentRelation - Current relation being evaluated * @param {Object} options - Evaluation options * @returns {RuleEvaluationResult} Standardized result object */ evaluate(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options = {}) { const startTime = Date.now(); this.evaluationCount++; try { // Validate inputs const validation = this._validateInputs(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options); if (!validation.valid) { return this._createErrorResult(validation.reason, validation.details); } // Normalize options const normalizedOptions = this._normalizeOptions(options, rule); // Check for early termination conditions const earlyExit = this._checkEarlyExit(normalizedOptions, rule); if (earlyExit) { return earlyExit; } // Delegate to concrete implementation const result = this._evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, normalizedOptions); // Post-process result const finalResult = this._postProcessResult(result, rule, normalizedOptions); // Track performance this.totalEvaluationTime += Date.now() - startTime; return finalResult; } catch (error) { this.totalEvaluationTime += Date.now() - startTime; return this._createErrorResult('evaluation_error', { error: error.message, stack: error.stack }); } } /** * Get performance statistics for this rule * @returns {Object} Performance metrics */ getPerformanceStats() { return { ruleType: this.ruleType, evaluationCount: this.evaluationCount, totalEvaluationTime: this.totalEvaluationTime, averageEvaluationTime: this.evaluationCount > 0 ? this.totalEvaluationTime / this.evaluationCount : 0 }; } /** * Reset performance counters */ resetPerformanceStats() { this.evaluationCount = 0; this.totalEvaluationTime = 0; } // ========== HELPER METHODS FOR STANDARDIZED RESULTS ========== /** * Create standardized rule result with collected values * @protected */ _defaultValidity() { return DEFAULT_VALIDITY; } _validity(operator, sources = [], sourceLabels = [], K = 1, possibility = 0) { return buildValidity({ operator, sources, sourceLabels, K, possibility }); } _relationValidity(rel) { return normalizeValidity(rel && rel.validity !== undefined ? rel.validity : null); } _createStandardResult(authResult, collectedValues = []) { const result = { // AUTHORIZATION possibility: authResult.possibility || 0, reliability: authResult.reliability !== undefined ? authResult.reliability : 1.0, validity: authResult.validity || DEFAULT_VALIDITY, // BINARY MODE FIELDS possibility_allow: authResult.possibility_allow !== undefined ? authResult.possibility_allow : (authResult.possibility || 0), possibility_deny: authResult.possibility_deny !== undefined ? authResult.possibility_deny : 0, // VALUE COLLECTION collectedValues: collectedValues, // EXISTING FIELDS meta: authResult.meta || null, meta_allow: authResult.meta_allow, meta_deny: authResult.meta_deny, remediation: authResult.remediation, reason: authResult.reason }; return result; } /** * Create a collected value entry with full metadata * @protected */ _createCollectedValue(value, possibility, path, source, metadata = {}) { // Timestamps honor the caller's pinned clock when present; the wall // clock is only the fallback for unpinned callers. const clockNow = (typeof metadata._now === 'number') ? metadata._now : Date.now(); return { value: value, possibility: possibility ?? 1.0, path: Array.isArray(path) ? [...path] : [path], source: { ...source, entityKey: source.entityKey, relation: source.relation, step: source.step !== undefined ? source.step : 0 }, metadata: { timestamp: metadata.timestamp || clockNow, reliability: metadata.reliability !== undefined ? metadata.reliability : 1.0, decay: metadata.decay || null, ...metadata } }; } /** * Quick reachability failure check for early termination * @protected * @param {string} sourceKey - Source entity key * @param {string} targetKey - Target entity key * @param {Object} options - Options (direction: 'forward' | 'backward') * @returns {boolean|null} - true if reachable, false if not reachable, null if no reachability checker */ _quickReachabilityCheck(sourceKey, targetKey, options = {}) { if (!this.arbiter.reachabilityChecker) { return null; // No reachability checker available } try { // Get node IDs for PLTC query const sourceId = this.arbiter.resolveNodeId(sourceKey, options); const targetId = this.arbiter.resolveNodeId(targetKey, options); if (sourceId === undefined || targetId === undefined) { return false; // Nodes don't exist } // Use reachability checker with direction option for PLTC return this.arbiter.reachabilityChecker.isReachable(sourceId, targetId, options); } catch (error) { console.warn(`Reachability check failed for ${sourceKey} -> ${targetKey}:`, error); return null; } } /** * Quick reachability failure check with early termination * Returns a standardized result for unreachable cases * @protected * @param {string} sourceKey - Source entity key * @param {string} targetKey - Target entity key * @param {string} reason - Reason for the check * @returns {Object|null} - Standardized result if unreachable, null if reachable or no checker */ _quickReachabilityFailure(sourceKey, targetKey, reason = 'Not reachable via reachability index', options = {}) { const isReachable = this._quickReachabilityCheck(sourceKey, targetKey, options); if (isReachable === false) { // Quick failure - not reachable return this._createStandardResult({ possibility: 0, possibility_allow: 0, possibility_deny: 1.0, reliability: 1.0, reason: reason, meta: { method: 'reachability_quick_fail', sourceKey, targetKey, timestamp: Date.now() } }); } return null; // Reachable or no checker available } /** * Batch reachability check for multiple source-target pairs * @protected * @param {Array} pairs - Array of {sourceKey, targetKey} objects * @returns {Object} - Map of reachability results */ _batchReachabilityCheck(pairs) { if (!this.arbiter.reachabilityChecker) { return null; } const results = {}; for (const {sourceKey, targetKey} of pairs) { try { results[`${sourceKey}->${targetKey}`] = this.arbiter.isReachable(sourceKey, targetKey); } catch (error) { console.warn(`Batch reachability check failed for ${sourceKey} -> ${targetKey}:`, error); results[`${sourceKey}->${targetKey}`] = null; } } return results; } /** * Get reachable nodes for a source with quick failure check * @protected * @param {string} sourceKey - Source entity key * @param {number} maxResults - Maximum number of results * @returns {Array|null} - Array of reachable node keys or null if no checker */ _getReachableNodes(sourceKey, maxResults = 1000) { if (!this.arbiter.reachabilityChecker) { return null; } try { return this.arbiter.getReachableNodes(sourceKey, maxResults); } catch (error) { console.warn(`Get reachable nodes failed for ${sourceKey}:`, error); return null; } } /** * Get nodes that can reach a target with quick failure check * @protected * @param {string} targetKey - Target entity key * @param {number} maxResults - Maximum number of results * @returns {Array|null} - Array of reaching node keys or null if no checker */ _getReachingNodes(targetKey, maxResults = 1000) { if (!this.arbiter.reachabilityChecker) { return null; } try { return this.arbiter.getReachingNodes(targetKey, maxResults); } catch (error) { console.warn(`Get reaching nodes failed for ${targetKey}:`, error); return null; } } /** * Aggregate collected values for consumption by other rules using OWAFusion * @protected */ _aggregateCollectedValues(collectedValues, method = 'sum', options = {}) { if (!collectedValues || collectedValues.length === 0) { return { value: null, hasValue: false }; } const values = collectedValues.map(cv => cv.value); const possibilities = collectedValues.map(cv => cv.possibility); const metas = collectedValues.map(cv => ({ path: cv.path, source: cv.source, reliability: cv.metadata?.reliability || 1.0, timestamp: cv.metadata?.timestamp, originalValue: cv.value, originalPossibility: cv.possibility })); // Use OWAFusion for aggregation const valueResult = OWAFusion.fuseWithMeta(values, metas, null, method); const possibilityResult = OWAFusion.fuseWithMeta(possibilities, metas, null, method); return { value: valueResult.value, possibility: possibilityResult.value, hasValue: true, aggregationMeta: { method: method, sourceCount: collectedValues.length, sourcePaths: collectedValues.map(cv => cv.path), selectedSource: valueResult.meta, valueContribution: valueResult.value, possibilityContribution: possibilityResult.value, allValues: values, allPossibilities: possibilities } }; } /** * Bilattice-enhanced evidence combination for rigorous epistemic reasoning * * This method provides sophisticated evidence fusion using bilattice orderings * while maintaining compatibility with existing possibilistic infrastructure. * * @param {Array} collectedValues - Array of collected values with metadata * @param {Object} options - Combination options * @param {string} options.method - Fusion method ('max', 'min', 'majority', etc.) * @param {boolean} options.useBilattice - Whether to use bilattice reasoning (default: false) * @param {QualitativeCapacity} options.capacity - Capacity for bilattice analysis * @param {QualitativeScale} options.scale - Qualitative scale for bilattice operations * @param {string} options.epistemicMode - 'information', 'truth', or 'hybrid' (default: 'hybrid') * @returns {Object} Enhanced fusion result with epistemic analysis * @protected */ _combineEvidenceWithBilattice(collectedValues, options = {}) { const { method = 'max', useBilattice = false, capacity = null, scale = null, epistemicMode = 'hybrid', reconciliationMethod = 'bilattice', capacityType = 'simple_support', mode = 'qualitative' } = options; if (!useBilattice) { return this._aggregateCollectedValues(collectedValues, method, options); } try { // Use the unified evidence fusion system const fusionResult = UnifiedEvidenceFusion.fuse(collectedValues, { mode, aggregationMethod: method, reconciliationMethod: useBilattice ? reconciliationMethod : 'none', epistemicMode, capacityType, scale: scale || QualitativeScale.fivePoint(), useReconciliation: useBilattice, reliabilityWeighting: options.reliabilityWeighting || false }); return { value: fusionResult.value, possibility: fusionResult.possibility, hasValue: fusionResult.hasValue, aggregationMeta: { method: fusionResult.fusionMethod, aggregationMethod: fusionResult.aggregationMethod, reconciliationMethod: fusionResult.reconciliationMethod, selectedSource: { type: 'unified_evidence_fusion', mode, epistemicMode, capacityType } }, epistemicAnalysis: fusionResult.epistemicAnalysis }; } catch (error) { console.warn('Unified evidence fusion failed, falling back to standard aggregation:', error); return this._aggregateCollectedValues(collectedValues, method, options); } } /** * Create a qualitative capacity from collected values for bilattice analysis * * @param {Array} collectedValues - Array of collected values * @param {QualitativeScale} scale - Qualitative scale to use * @param {string} capacityType - Type of capacity ('simple_support', 'possibility', 'necessity') * @returns {QualitativeCapacity} Capacity for bilattice analysis * @protected */ _createCapacityFromValues(collectedValues, scale, capacityType = 'simple_support') { if (!collectedValues || collectedValues.length === 0) { return null; } // Create state space from unique values const uniqueValues = [...new Set(collectedValues.map(cv => cv.value))]; const stateSpace = uniqueValues.map((_, index) => `evidence_${index}`); switch (capacityType) { case 'simple_support': // Create simple support capacity for each piece of evidence const qmt = new Map(); collectedValues.forEach((cv, index) => { const evidenceSet = new Set([`evidence_${index}`]); qmt.set(evidenceSet, cv.possibility); }); return new QualitativeCapacity(stateSpace, scale, qmt); case 'possibility': // Create possibility measure (all focal sets are singletons) const possibilityQmt = new Map(); collectedValues.forEach((cv, index) => { const singletonSet = new Set([`evidence_${index}`]); possibilityQmt.set(singletonSet, cv.possibility); }); return new QualitativeCapacity(stateSpace, scale, possibilityQmt); case 'necessity': // Create necessity measure (nested focal sets) const necessityQmt = new Map(); const sortedValues = collectedValues .map((cv, index) => ({ value: cv.value, possibility: cv.possibility, index })) .sort((a, b) => b.possibility - a.possibility); sortedValues.forEach((item, rank) => { const nestedSet = new Set(sortedValues.slice(0, rank + 1).map(sv => `evidence_${sv.index}`)); necessityQmt.set(nestedSet, item.possibility); }); return new QualitativeCapacity(stateSpace, scale, necessityQmt); default: throw new Error(`Unknown capacity type: ${capacityType}`); } } // ========== PROTECTED METHODS (to be implemented by subclasses) ========== /** * Concrete rule evaluation implementation * @protected */ _evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) { throw new Error(`${this.constructor.name} must implement _evaluateRule()`); } // ========== PRIVATE HELPER METHODS ========== /** * Validate input parameters * @private */ _validateInputs(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) { // Check for required parameters if (userId === undefined || userId === null) { return { valid: false, reason: 'missing_user_id', details: { userKey } }; } if (objectId === undefined || objectId === null) { return { valid: false, reason: 'missing_object_id', details: { objectKey } }; } if (!userKey || typeof userKey !== 'string') { return { valid: false, reason: 'invalid_user_key', details: { userKey } }; } if (!objectKey || typeof objectKey !== 'string') { return { valid: false, reason: 'invalid_object_key', details: { objectKey } }; } if (!rule || typeof rule !== 'object') { return { valid: false, reason: 'invalid_rule', details: { rule } }; } if (!visited || typeof visited.has !== 'function') { return { valid: false, reason: 'invalid_visited_set', details: { visited } }; } return { valid: true }; } /** * Normalize and validate options * @private */ _normalizeOptions(options, rule) { const normalized = { // Performance options fastPath: options.fastPath || false, minPossibility: options.minPossibility || null, maxPossibility: options.maxPossibility || null, // Inference options noInfer: options.noInfer || options.no_infer || rule.no_infer || false, allowInference: options.allowInference !== false && !options.noInfer && !options.no_infer && !rule.no_infer, // Binary mode binary: options.binary || false, // Evaluation tracking trackEvaluation: options.trackEvaluation !== false, // Rule-specific options (pass through) ...options }; // Validate threshold values if (normalized.minPossibility !== null) { normalized.minPossibility = Math.max(0, Math.min(1, normalized.minPossibility)); } if (normalized.maxPossibility !== null) { normalized.maxPossibility = Math.max(0, Math.min(1, normalized.maxPossibility)); } return normalized; } /** * Check for early exit conditions * @private */ _checkEarlyExit(options, rule) { // Cycle detection is handled at a higher level // Rule-specific early exits should be implemented in _evaluateRule return null; } /** * Post-process evaluation result * @private */ _postProcessResult(result, rule, options) { const includeMeta = options.includeMeta !== undefined ? options.includeMeta : true; const includeValues = options.collectValues !== undefined ? options.collectValues : true; const { meta, meta_allow, meta_deny, collectedValues, ...rest } = result; // Ensure result has required fields with standardized structure const processed = { // AUTHORIZATION possibility: result.possibility || 0, reliability: result.reliability !== undefined ? result.reliability : 1.0, // EXISTING FIELDS reason: result.reason, // PRESERVE OTHER FIELDS (for backward compatibility) ...rest }; if (includeValues) { processed.collectedValues = collectedValues || []; } if (includeMeta) { processed.meta = meta || null; if (meta_allow !== undefined) { processed.meta_allow = meta_allow; } if (meta_deny !== undefined) { processed.meta_deny = meta_deny; } } // Add evaluation metadata if tracking is enabled if (options.trackEvaluation && includeMeta && result.evaluation) { processed.evaluation = { ruleType: this.ruleType, evaluationTime: Date.now() - (result.evaluation.evaluationStarted || Date.now()), ...result.evaluation }; } // Apply early exit metadata if applicable if (options.fastPath && includeMeta && this._shouldMarkEarlyExit(processed, options)) { if (processed.meta) { processed.meta.earlyExit = true; processed.meta.earlyExitReason = this._getEarlyExitReason(processed, options); } } return processed; } /** * Create standardized error result * @private */ _createErrorResult(reason, details = {}) { return { possibility_allow: 0, possibility_deny: 0, reliability: 1.0, meta_allow: null, meta_deny: null, reason, error: true, details }; } /** * Check if result should be marked as early exit * @private */ _shouldMarkEarlyExit(result, options) { if (options.minAllowPossibility !== null && result.possibility_allow >= options.minAllowPossibility) { return true; } if (options.maxDenyPossibility !== null && result.possibility_deny >= options.maxDenyPossibility) { return true; } return false; } /** * Get early exit reason * @private */ _getEarlyExitReason(result, options) { if (options.minAllowPossibility !== null && result.possibility_allow >= options.minAllowPossibility) { return 'allow_threshold_met'; } if (options.maxDenyPossibility !== null && result.possibility_deny >= options.maxDenyPossibility) { return 'deny_threshold_met'; } return 'unknown'; } } /** * Standard result structure for rule evaluations * @typedef {Object} RuleEvaluationResult * @property {number} possibility_allow - Possibility of allowing access (0-1) * @property {number} possibility_deny - Possibility of denying access (0-1) * @property {number} reliability - Reliability of the evaluation (0-1) * @property {Object|null} meta_allow - Metadata for allow decision * @property {Object|null} meta_deny - Metadata for deny decision * @property {string} reason - Reason for the result (optional) * @property {boolean} error - Whether this is an error result (optional) * @property {Object} details - Additional details (optional) * @property {Object} evaluation - Evaluation tracking data (optional) */