543 lines
22 KiB
JavaScript
543 lines
22 KiB
JavaScript
|
|
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
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|