initial commit: @arbiter/core authorization engine with js-rigor hardening
Zanzibar-style authorization graph engine (direct/chain/TTU/defeasible/ binary modes, condensed snapshots, value relations) with 39 rigor test campaigns. Includes fixes for snapshot binary writer/reader format mismatch (snapshot-of-snapshot corruption), possibility write-boundary validation, empty-graph snapshot serialization, relation lookup cache direction collision, config-redefinition cache invalidation, binary threshold semantics, defeasible compiled routing, and comparator reason whitelisting.
This commit is contained in:
@@ -0,0 +1,686 @@
|
||||
import { OWAFusion } from '../../utils/OWAFusion.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
|
||||
*/
|
||||
_createStandardResult(authResult, collectedValues = []) {
|
||||
const result = {
|
||||
// AUTHORIZATION
|
||||
possibility: authResult.possibility || 0,
|
||||
reliability: authResult.reliability !== undefined ? authResult.reliability : 1.0,
|
||||
|
||||
// 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 = {}) {
|
||||
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 || Date.now(),
|
||||
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)
|
||||
*/
|
||||
Reference in New Issue
Block a user