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,542 @@
|
||||
import { BaseRule } from './BaseRule.js';
|
||||
import { Arbiter } from '../../core/Arbiter.js';
|
||||
import { OWAFusion } from '../../utils/OWAFusion.js';
|
||||
|
||||
/**
|
||||
* TupleToUsersetRule - Evaluates access through intermediate entities (group membership pattern)
|
||||
*
|
||||
* This rule implements the tuple-to-userset pattern where access is granted if:
|
||||
* 1. Object has a tupleset relation to an intermediate entity (e.g., doc -> group)
|
||||
* 2. User has a computed relation to that same intermediate entity (e.g., user -> group)
|
||||
*
|
||||
* Supports sophisticated batch processing and OWA fusion for combining multiple paths.
|
||||
*
|
||||
* Configuration:
|
||||
* {
|
||||
* type: 'tuple_to_userset',
|
||||
* tuplesetRelation: string, // Relation from object to intermediate (e.g., 'owner')
|
||||
* computedRelation: string, // Relation from user to intermediate (e.g., 'member_of')
|
||||
* reverse: boolean, // Check in reverse direction (default: false)
|
||||
* minPossibility: number, // Minimum possibility for a path to be considered (0-1, default: 0)
|
||||
* owaWeights: Array<number>, // OWA weights for fusion (default: 'max' like behavior [1,0,...])
|
||||
* earlyExitThreshold: number, // Stop when path exceeds this threshold (default: 0.95)
|
||||
* maxIntermediates: number // Maximum intermediates to check (default: 20)
|
||||
* }
|
||||
*/
|
||||
export class TupleToUsersetRule extends BaseRule {
|
||||
constructor(arbiter) {
|
||||
super(arbiter);
|
||||
// Performance tracking
|
||||
this.performanceStats = {
|
||||
totalChecks: 0,
|
||||
earlyExits: 0,
|
||||
maxIntermediatesHit: 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate tuple-to-userset relationship with aggressive early exit optimization
|
||||
* @protected
|
||||
*/
|
||||
_evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
|
||||
this.performanceStats.totalChecks++;
|
||||
|
||||
const { fastPath, includeMeta = true } = options;
|
||||
const collectValues = options.collectValues !== undefined ? options.collectValues : true;
|
||||
const minPossibility = rule.minPossibility !== undefined ? rule.minPossibility : (options.minPossibility !== undefined ? options.minPossibility : 0);
|
||||
const earlyExitThreshold = rule.earlyExitThreshold !== undefined ? rule.earlyExitThreshold : 0.95;
|
||||
const maxIntermediates = rule.maxIntermediates !== undefined ? rule.maxIntermediates : 20;
|
||||
const resolvePossibility = (value) => value !== undefined ? value : 1.0;
|
||||
const resolveReliability = (value) => value !== undefined ? value : 1.0;
|
||||
|
||||
const reverse = rule.reverse || false;
|
||||
if (!rule._computedRelationId) {
|
||||
rule._computedRelationId = this.arbiter.keyManager._getRelationId(rule.computedRelation);
|
||||
}
|
||||
if (!rule.computedRelationConfig) {
|
||||
rule.computedRelationConfig = this.arbiter.relationConfigs.get(rule.computedRelation);
|
||||
}
|
||||
if (!rule.tuplesetRelationConfig) {
|
||||
rule.tuplesetRelationConfig = this.arbiter.relationConfigs.get(rule.tuplesetRelation);
|
||||
}
|
||||
|
||||
const ruleMetaBase = includeMeta ? {
|
||||
ruleType: 'TupleToUsersetRule',
|
||||
userKey,
|
||||
objectKey,
|
||||
tuplesetRelation: rule.tuplesetRelation,
|
||||
computedRelation: rule.computedRelation,
|
||||
reverse,
|
||||
minPossibilityUsed: minPossibility,
|
||||
earlyExitThreshold,
|
||||
maxIntermediates,
|
||||
evaluationStarted: Date.now()
|
||||
} : null;
|
||||
|
||||
let evaluationMeta = options.trackEvaluation && includeMeta ? { ...ruleMetaBase } : null;
|
||||
|
||||
// Get tuples (intermediate entities) with early circuit breaker
|
||||
// tuplesetDirection: 'out' = object has relation TO intermediates (document → owner → group)
|
||||
// tuplesetDirection: 'in' = intermediates have relation TO object (group → belongs_to → org)
|
||||
const tuplesetDirection = rule.tuplesetDirection || 'out'; // Default to 'out' for backward compatibility
|
||||
|
||||
let tuples;
|
||||
const useRelationGraph = !options?.partialGraphContext;
|
||||
if (reverse) {
|
||||
const useGraphNeighbors = useRelationGraph &&
|
||||
this.arbiter.relationManager.shouldUseRelationGraphTraversal(userId, rule.tuplesetRelation, false);
|
||||
const neighbors = useGraphNeighbors
|
||||
? this.arbiter.relationManager.getRelationGraphNeighbors(userId, rule.tuplesetRelation, false)
|
||||
: null;
|
||||
if (neighbors) {
|
||||
tuples = [];
|
||||
for (const neighborId of neighbors) {
|
||||
const edge = this.arbiter.relationManager.getDirectRelation(userId, rule.tuplesetRelation, neighborId, options);
|
||||
if (edge) tuples.push(edge);
|
||||
}
|
||||
} else {
|
||||
tuples = this.arbiter.relationManager.getRelationsFromSrc(userId, rule.tuplesetRelation, options);
|
||||
}
|
||||
} else {
|
||||
const reverseLookup = tuplesetDirection === 'in';
|
||||
const useGraphNeighbors = useRelationGraph &&
|
||||
this.arbiter.relationManager.shouldUseRelationGraphTraversal(objectId, rule.tuplesetRelation, reverseLookup);
|
||||
const neighbors = useGraphNeighbors
|
||||
? this.arbiter.relationManager.getRelationGraphNeighbors(objectId, rule.tuplesetRelation, reverseLookup)
|
||||
: null;
|
||||
if (neighbors) {
|
||||
tuples = [];
|
||||
for (const neighborId of neighbors) {
|
||||
const srcId = reverseLookup ? neighborId : objectId;
|
||||
const dstId = reverseLookup ? objectId : neighborId;
|
||||
const edge = this.arbiter.relationManager.getDirectRelation(srcId, rule.tuplesetRelation, dstId, options);
|
||||
if (edge) tuples.push(edge);
|
||||
}
|
||||
} else if (reverseLookup) {
|
||||
tuples = this.arbiter.relationManager.getRelationsToDst(objectId, rule.tuplesetRelation, options);
|
||||
} else {
|
||||
tuples = this.arbiter.relationManager.getRelationsFromSrc(objectId, rule.tuplesetRelation, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Circuit breaker: if too many intermediates, limit and warn
|
||||
if (tuples.length > maxIntermediates * 3) {
|
||||
// Sort by possibility and take top N
|
||||
tuples.sort((a, b) => resolvePossibility(b.possibility) - resolvePossibility(a.possibility));
|
||||
tuples = tuples.slice(0, maxIntermediates);
|
||||
if (evaluationMeta) evaluationMeta.circuitBreakerTriggered = true;
|
||||
}
|
||||
|
||||
const checkingId = reverse ? this.arbiter.resolveNodeId(objectKey, options) : userId;
|
||||
|
||||
const useDirectJoin = rule.computedRelation && rule.computedRelationConfig?.type === 'direct';
|
||||
const computedEdges = useDirectJoin
|
||||
? this.arbiter.relationManager.getRelationsFromSrc(checkingId, rule.computedRelation, options)
|
||||
: null;
|
||||
const joinMode = useDirectJoin && computedEdges && computedEdges.length < tuples.length
|
||||
? 'computed'
|
||||
: 'tuples';
|
||||
|
||||
const tuplesWithKeys = [];
|
||||
if (includeMeta || joinMode === 'tuples') {
|
||||
for (const t of tuples) {
|
||||
const srcKey = this.arbiter.resolveKey(t.src, options);
|
||||
const dstKey = this.arbiter.resolveKey(t.dst, options);
|
||||
const intermediateId = tuplesetDirection === 'in' ? t.src : t.dst;
|
||||
const intermediateKey = tuplesetDirection === 'in' ? srcKey : dstKey;
|
||||
tuplesWithKeys.push({ tuple: t, srcKey, dstKey, intermediateId, intermediateKey });
|
||||
}
|
||||
}
|
||||
|
||||
if (evaluationMeta) {
|
||||
evaluationMeta.directTuplesFound = tuplesWithKeys.length;
|
||||
evaluationMeta.directTuples = tuplesWithKeys.map(({ tuple, srcKey, dstKey }) => ({
|
||||
from: srcKey,
|
||||
to: dstKey,
|
||||
relation: tuple.rel,
|
||||
possibility: tuple.possibility,
|
||||
reliability: tuple.reliability,
|
||||
source: tuple.source || 'persistent'
|
||||
}));
|
||||
}
|
||||
|
||||
const useLightweightPaths = !includeMeta && !collectValues;
|
||||
let bestPath = null;
|
||||
let allValidPaths = [];
|
||||
let reasons = [];
|
||||
let intermediateEvaluationDetails = includeMeta ? [] : null;
|
||||
let processedCount = 0;
|
||||
|
||||
// Process direct tuples with early exit optimization
|
||||
const computedRelationCache = new Map();
|
||||
const computedByIntermediate = joinMode === 'tuples' && computedEdges
|
||||
? new Map(computedEdges.map(edge => [edge.dst, edge]))
|
||||
: null;
|
||||
|
||||
if (joinMode === 'computed' && computedEdges) {
|
||||
for (const edge of computedEdges) {
|
||||
if (processedCount >= maxIntermediates) {
|
||||
if (evaluationMeta) evaluationMeta.maxIntermediatesReached = true;
|
||||
this.performanceStats.maxIntermediatesHit++;
|
||||
break;
|
||||
}
|
||||
|
||||
const intermediateId = edge.dst;
|
||||
const intermediateKey = this.arbiter.resolveKey(intermediateId, options);
|
||||
if (!intermediateKey) continue;
|
||||
|
||||
processedCount++;
|
||||
|
||||
const tupleEdge = reverse
|
||||
? this.arbiter.relationManager.getDirectRelation(userId, rule.tuplesetRelation, intermediateId, options)
|
||||
: (tuplesetDirection === 'in'
|
||||
? this.arbiter.relationManager.getDirectRelation(intermediateId, rule.tuplesetRelation, objectId, options)
|
||||
: this.arbiter.relationManager.getDirectRelation(objectId, rule.tuplesetRelation, intermediateId, options));
|
||||
|
||||
if (!tupleEdge) continue;
|
||||
|
||||
let res;
|
||||
if (visited && visited.size) {
|
||||
const visitKey = `${checkingId}|${rule._computedRelationId}|${intermediateId}`;
|
||||
if (visited.has(visitKey)) {
|
||||
res = { possibility: 0, reliability: 1.0, reason: 'cycle' };
|
||||
}
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
res = {
|
||||
possibility: edge.possibility,
|
||||
reliability: edge.reliability !== undefined ? edge.reliability : 1.0,
|
||||
reason: 'direct_match'
|
||||
};
|
||||
}
|
||||
|
||||
if (includeMeta && intermediateEvaluationDetails) {
|
||||
const pathDetail = {
|
||||
intermediateKey,
|
||||
type: 'direct_tuple',
|
||||
tuplesetRelationPossibility: resolvePossibility(tupleEdge.possibility),
|
||||
tuplesetRelationReliability: resolveReliability(tupleEdge.reliability),
|
||||
computedRelationResult: {
|
||||
possibility: res.possibility,
|
||||
reliability: res.reliability,
|
||||
reason: res.reason
|
||||
},
|
||||
metaFromCheck: res.meta
|
||||
};
|
||||
if (evaluationMeta) intermediateEvaluationDetails.push(pathDetail);
|
||||
}
|
||||
|
||||
if (res.reason === 'cycle') reasons.push('cycle');
|
||||
|
||||
const combinedPossibility = Math.min(resolvePossibility(tupleEdge.possibility), res.possibility);
|
||||
const combinedReliability = resolveReliability(tupleEdge.reliability) * (res.reliability !== undefined ? res.reliability : 1.0);
|
||||
|
||||
if (combinedPossibility >= minPossibility) {
|
||||
const path = {
|
||||
intermediateKey,
|
||||
tuplesetPossibility: resolvePossibility(tupleEdge.possibility),
|
||||
computedPossibility: res.possibility,
|
||||
combinedPossibility,
|
||||
combinedReliability
|
||||
};
|
||||
allValidPaths.push(path);
|
||||
if (!bestPath || combinedPossibility > bestPath.combinedPossibility) {
|
||||
bestPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (joinMode === 'tuples') for (const entry of tuplesWithKeys) {
|
||||
const t = entry.tuple;
|
||||
if (processedCount >= maxIntermediates) {
|
||||
if (evaluationMeta) evaluationMeta.maxIntermediatesReached = true;
|
||||
this.performanceStats.maxIntermediatesHit++;
|
||||
break;
|
||||
}
|
||||
|
||||
// For 'out' direction: intermediate is destination (document → owner → group)
|
||||
// For 'in' direction: intermediate is source (group → belongs_to → org)
|
||||
const intermediateKey = entry.intermediateKey;
|
||||
if (!intermediateKey) continue;
|
||||
|
||||
processedCount++;
|
||||
|
||||
// In reverse mode: check if the "object" (3rd param) has computed relation to intermediate
|
||||
// In normal mode: check if the "user" (1st param) has computed relation to intermediate
|
||||
const checkingEntityKey = reverse ? objectKey : userKey;
|
||||
|
||||
|
||||
let res = computedRelationCache.get(entry.intermediateId);
|
||||
if (!res) {
|
||||
if (useDirectJoin) {
|
||||
if (visited && visited.size) {
|
||||
const visitKey = `${checkingId}|${rule._computedRelationId}|${entry.intermediateId}`;
|
||||
if (visited.has(visitKey)) {
|
||||
res = {
|
||||
possibility: 0,
|
||||
reliability: 1.0,
|
||||
reason: 'cycle'
|
||||
};
|
||||
}
|
||||
}
|
||||
if (!res) {
|
||||
const directRel = computedByIntermediate
|
||||
? computedByIntermediate.get(entry.intermediateId)
|
||||
: this.arbiter.relationManager.getDirectRelation(checkingId, rule.computedRelation, entry.intermediateId, options);
|
||||
if (directRel && (minPossibility === null || directRel.possibility >= minPossibility)) {
|
||||
res = {
|
||||
possibility: directRel.possibility,
|
||||
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
||||
reason: 'direct_match'
|
||||
};
|
||||
} else {
|
||||
res = {
|
||||
possibility: 0,
|
||||
reliability: 1.0,
|
||||
reason: 'no_direct_match'
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
res = this.arbiter.authChecker.check(checkingEntityKey, rule.computedRelation, intermediateKey, {
|
||||
...options,
|
||||
minPossibility,
|
||||
fastPath: true, // Enable fast path for intermediate checks
|
||||
_visited: visited,
|
||||
_currentRelation: rule.computedRelation
|
||||
});
|
||||
}
|
||||
computedRelationCache.set(entry.intermediateId, res);
|
||||
}
|
||||
|
||||
|
||||
if (includeMeta && intermediateEvaluationDetails) {
|
||||
const pathDetail = {
|
||||
intermediateKey,
|
||||
type: 'direct_tuple',
|
||||
tuplesetRelationPossibility: resolvePossibility(t.possibility),
|
||||
tuplesetRelationReliability: resolveReliability(t.reliability),
|
||||
computedRelationResult: {
|
||||
possibility: res.possibility,
|
||||
reliability: res.reliability,
|
||||
reason: res.reason
|
||||
},
|
||||
metaFromCheck: res.meta
|
||||
};
|
||||
if (evaluationMeta) intermediateEvaluationDetails.push(pathDetail);
|
||||
}
|
||||
|
||||
if (res.reason === 'cycle') reasons.push('cycle');
|
||||
|
||||
const combinedPossibility = Math.min(resolvePossibility(t.possibility), res.possibility);
|
||||
const combinedReliability = resolveReliability(t.reliability) * (res.reliability !== undefined ? res.reliability : 1.0);
|
||||
|
||||
if (combinedPossibility >= minPossibility) {
|
||||
const pathData = {
|
||||
possibility: combinedPossibility,
|
||||
reliability: combinedReliability,
|
||||
...(!useLightweightPaths && includeMeta && {
|
||||
meta: {
|
||||
intermediateKey,
|
||||
pathType: 'direct',
|
||||
tuplesetRelation: { relation: rule.tuplesetRelation, possibility: resolvePossibility(t.possibility), reliability: resolveReliability(t.reliability) },
|
||||
computedRelation: { relation: rule.computedRelation, possibility: res.possibility, reliability: res.reliability, meta: res.meta }
|
||||
}
|
||||
}),
|
||||
...(!useLightweightPaths && collectValues && { collectedValue: intermediateKey })
|
||||
};
|
||||
|
||||
allValidPaths.push(pathData);
|
||||
|
||||
// Update best path
|
||||
if (!bestPath || combinedPossibility > bestPath.possibility) {
|
||||
bestPath = pathData;
|
||||
}
|
||||
|
||||
// AGGRESSIVE EARLY EXIT: Stop if we found a very good path
|
||||
if (combinedPossibility >= earlyExitThreshold) {
|
||||
if (evaluationMeta) {
|
||||
evaluationMeta.earlyExitTriggered = true;
|
||||
evaluationMeta.earlyExitReason = 'direct_path_threshold_exceeded';
|
||||
evaluationMeta.earlyExitPossibility = combinedPossibility;
|
||||
evaluationMeta.intermediatesProcessed = processedCount;
|
||||
}
|
||||
this.performanceStats.earlyExits++;
|
||||
|
||||
// Return immediately with the excellent path
|
||||
return this._buildFinalResult(
|
||||
[pathData],
|
||||
reasons,
|
||||
ruleMetaBase,
|
||||
evaluationMeta,
|
||||
'early_exit_direct_path',
|
||||
includeMeta,
|
||||
collectValues,
|
||||
options.trackEvaluation
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (evaluationMeta) {
|
||||
evaluationMeta.evaluationCompleted = Date.now();
|
||||
evaluationMeta.evaluationDuration = evaluationMeta.evaluationCompleted - (evaluationMeta.evaluationStarted || evaluationMeta.evaluationCompleted);
|
||||
evaluationMeta.intermediateEvaluationDetails = intermediateEvaluationDetails;
|
||||
evaluationMeta.totalValidPathsFound = allValidPaths.length;
|
||||
evaluationMeta.intermediatesProcessed = processedCount;
|
||||
}
|
||||
|
||||
return this._buildFinalResult(allValidPaths, reasons, ruleMetaBase, evaluationMeta, 'complete_evaluation', includeMeta, collectValues, options.trackEvaluation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the final result from valid paths
|
||||
* @private
|
||||
*/
|
||||
_buildFinalResult(validPathData, reasons, ruleMetaBase, evaluationMeta, evaluationType, includeMeta = true, collectValues = true, trackEvaluation = false) {
|
||||
if (!validPathData.length) {
|
||||
const reason = reasons.includes('cycle') ? 'cycle' : 'no_valid_intermediate_paths';
|
||||
if (evaluationMeta) {
|
||||
evaluationMeta.outcome = reason;
|
||||
evaluationMeta.finalPossibility = 0;
|
||||
evaluationMeta.finalReliability = 1.0;
|
||||
evaluationMeta.evaluationType = evaluationType;
|
||||
}
|
||||
|
||||
return {
|
||||
possibility: 0,
|
||||
reliability: 1.0,
|
||||
...(includeMeta && { meta: { ...ruleMetaBase, outcomeReason: reason, evaluation: evaluationMeta } }),
|
||||
...(collectValues && { collectedValues: [] }),
|
||||
reason
|
||||
};
|
||||
}
|
||||
|
||||
// For single path (common with early exit), skip OWA fusion overhead
|
||||
if (validPathData.length === 1) {
|
||||
const singlePath = validPathData[0];
|
||||
if (evaluationMeta) {
|
||||
evaluationMeta.outcome = 'single_path_found';
|
||||
evaluationMeta.finalPossibility = singlePath.possibility;
|
||||
evaluationMeta.finalReliability = singlePath.reliability;
|
||||
evaluationMeta.evaluationType = evaluationType;
|
||||
evaluationMeta.fusionSkipped = 'single_path_optimization';
|
||||
}
|
||||
|
||||
return {
|
||||
possibility: singlePath.possibility,
|
||||
reliability: singlePath.reliability,
|
||||
...(includeMeta && {
|
||||
meta: {
|
||||
...ruleMetaBase,
|
||||
outcomeReason: reasons.includes('cycle') ? 'cycle' : 'tuple_to_userset_evaluated',
|
||||
pathMeta: singlePath.meta,
|
||||
evaluation: evaluationMeta
|
||||
}
|
||||
}),
|
||||
...(collectValues && { collectedValues: [singlePath.collectedValue] }),
|
||||
reason: reasons.includes('cycle') ? 'cycle' : 'tuple_to_userset_found'
|
||||
};
|
||||
}
|
||||
|
||||
if (!includeMeta && !collectValues) {
|
||||
let maxPossibility = 0;
|
||||
let maxReliability = 1.0;
|
||||
for (const path of validPathData) {
|
||||
if (path.possibility > maxPossibility) {
|
||||
maxPossibility = path.possibility;
|
||||
maxReliability = path.reliability;
|
||||
}
|
||||
}
|
||||
return {
|
||||
possibility: maxPossibility,
|
||||
reliability: maxReliability,
|
||||
reason: reasons.includes('cycle') ? 'cycle' : (maxPossibility > 0 ? 'tuple_to_userset_found' : 'no_sufficient_tuple_to_userset_path')
|
||||
};
|
||||
}
|
||||
|
||||
// Multiple paths - use OWA fusion
|
||||
const possibilities = validPathData.map(p => p.possibility);
|
||||
const reliabilities = validPathData.map(p => p.reliability);
|
||||
const metaObjects = includeMeta ? validPathData.map(p => p.meta) : [];
|
||||
const collectedValues = collectValues ? validPathData.map(p => p.collectedValue) : [];
|
||||
|
||||
const owaWeights = [1, ...Array(possibilities.length - 1).fill(0)]; // Default to MAX for performance
|
||||
const fusionResult = OWAFusion.fuseWithMeta(possibilities, metaObjects, owaWeights, 'max', true, trackEvaluation ? { includeTrace: true } : null);
|
||||
|
||||
let fusedReliability = 1.0;
|
||||
if (fusionResult.meta && fusionResult.meta.intermediateKey) {
|
||||
const winningPath = validPathData.find(p => p.meta.intermediateKey === fusionResult.meta.intermediateKey && p.meta.pathType === fusionResult.meta.pathType);
|
||||
if (winningPath) {
|
||||
fusedReliability = winningPath.reliability;
|
||||
} else if (reliabilities.length > 0) {
|
||||
fusedReliability = Math.max(...reliabilities); // Use max reliability for performance
|
||||
}
|
||||
} else if (reliabilities.length > 0) {
|
||||
fusedReliability = Math.max(...reliabilities);
|
||||
}
|
||||
|
||||
if (evaluationMeta) {
|
||||
evaluationMeta.outcome = 'paths_evaluated_and_fused';
|
||||
evaluationMeta.fusion = {
|
||||
method: 'owa',
|
||||
weightsUsed: owaWeights,
|
||||
fusedPossibility: fusionResult.value,
|
||||
fusedReliability: fusedReliability,
|
||||
winningPathMeta: fusionResult.meta,
|
||||
...(trackEvaluation && fusionResult.trace ? {
|
||||
owa: {
|
||||
level: null,
|
||||
aggregator: 'max',
|
||||
weights: fusionResult.trace.weights,
|
||||
sortedValues: fusionResult.trace.sortedValues,
|
||||
contributions: fusionResult.trace.contributions,
|
||||
selectedIndex: fusionResult.trace.selectedIndex
|
||||
}
|
||||
} : {})
|
||||
};
|
||||
evaluationMeta.finalPossibility = fusionResult.value;
|
||||
evaluationMeta.finalReliability = fusedReliability;
|
||||
evaluationMeta.evaluationType = evaluationType;
|
||||
}
|
||||
|
||||
const finalMeta = includeMeta ? {
|
||||
...ruleMetaBase,
|
||||
outcomeReason: reasons.includes('cycle') ? 'cycle' : 'tuple_to_userset_evaluated',
|
||||
fusionMethod: 'owa',
|
||||
owaWeightsUsed: owaWeights,
|
||||
contributingPathMeta: fusionResult.meta,
|
||||
evaluation: evaluationMeta
|
||||
} : null;
|
||||
|
||||
return {
|
||||
possibility: fusionResult.value,
|
||||
reliability: fusedReliability,
|
||||
...(includeMeta && { meta: finalMeta }),
|
||||
...(collectValues && { collectedValues: fusionResult.value > 0 ? collectedValues : [] }),
|
||||
reason: reasons.includes('cycle') ? 'cycle' : (fusionResult.value > 0 ? 'tuple_to_userset_found' : 'no_sufficient_tuple_to_userset_path')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance statistics
|
||||
* @returns {Object} Performance statistics
|
||||
*/
|
||||
getPerformanceStats() {
|
||||
return { ...this.performanceStats };
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset performance statistics
|
||||
*/
|
||||
resetPerformanceStats() {
|
||||
this.performanceStats = {
|
||||
totalChecks: 0,
|
||||
earlyExits: 0,
|
||||
maxIntermediatesHit: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user