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,681 @@
|
||||
import { BaseRule } from './BaseRule.js';
|
||||
import { Arbiter } from '../../core/Arbiter.js';
|
||||
import { OWAQualitativeFusion, getOWAQualitativeWeights } from '../../qualitative/OWAQualitativeFusion.js';
|
||||
import { QualitativeScale, DEFAULT_QUALITATIVE_SCALE } from '../../qualitative/QualitativeScale.js';
|
||||
import { BilatticeOrderings } from '../../qualitative/BilatticeOrderings.js';
|
||||
import { QualitativeCapacity } from '../../qualitative/QualitativeCapacity.js';
|
||||
|
||||
/**
|
||||
* QualitativeRelationalComparatorRule - Evaluates access by comparing qualitative values from relations,
|
||||
* treating values as qualitative intervals that "blur" over time based on decaying possibility.
|
||||
*
|
||||
* This implementation uses qualitative scales and possibility theory instead of numeric intervals.
|
||||
*
|
||||
* Configuration:
|
||||
* {
|
||||
* type: 'relational_comparator',
|
||||
* qualitative: true, // Flag to indicate qualitative mode
|
||||
* scaleName: string, // Name of the qualitative scale to use (e.g., 'five-point', 'ternary')
|
||||
* leftOperand: {
|
||||
* rule: {...}, // Any rule configuration
|
||||
* extractValue: true, // Extract value from relation (if false, rule's possibility is used as value)
|
||||
* valueRelation: string, // Optional: specific relation for value
|
||||
* aggregator: string, // 'max', 'min', 'majority', 'priority', 'optimistic', etc.
|
||||
* owaWeights: number[], // Optional: custom OWA weights for aggregation
|
||||
* decaySteps: number, // Number of steps to decay per period on the qualitative scale
|
||||
* decayPeriod: string, // 'MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR' (default 'HOUR')
|
||||
* possibilityDecayDirection: string, // 'down' (towards 0), 'neutral' (towards 0.5), 'up' (towards 1), 'stable' (no decay)
|
||||
* valueBlurDirection: string, // 'neutral' (symmetric), 'down' (expands lower bound), 'up' (expands upper bound), 'stable' (minimal blur)
|
||||
* baseBlurSteps: number, // Number of steps to blur per possibility decay step
|
||||
* minOperandPossibility: number, // If operand's decayed possibility < this, considered no value
|
||||
* evaluateFrom: string // 'user', 'object', or 'auto' (for rule evaluation perspective)
|
||||
* },
|
||||
* rightOperand: {...}, // Same structure as leftOperand
|
||||
* comparator: string, // '>', '>=', '<', '<=', '==', '!='
|
||||
* marginSteps: number, // Number of steps to shift right operand on the scale before blurring
|
||||
* minRulePossibility: number, // Optional: if final rule possibility < this, considered 0
|
||||
* fallbackBehavior: string // 'allow' or 'deny' if values/operands are insufficient
|
||||
* }
|
||||
*/
|
||||
export class QualitativeRelationalComparatorRule extends BaseRule {
|
||||
constructor(arbiter, ruleEvaluator) {
|
||||
super(arbiter);
|
||||
this.ruleEvaluator = ruleEvaluator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate qualitative relational comparison
|
||||
* @protected
|
||||
*/
|
||||
_evaluateRule(userId, userKey, objectId, objectKey, rule, visited, currentRelation, options) {
|
||||
const {
|
||||
left: leftOperand,
|
||||
right: rightOperand,
|
||||
comparator,
|
||||
marginSteps = 0,
|
||||
fallbackBehavior = 'deny',
|
||||
minRulePossibility = 0,
|
||||
scaleName = 'five-point'
|
||||
} = rule;
|
||||
|
||||
try {
|
||||
// Get the qualitative scale
|
||||
const scale = this._getQualitativeScale(scaleName);
|
||||
|
||||
const ruleMetaBase = {
|
||||
ruleType: 'QualitativeRelationalComparatorRule',
|
||||
userKey,
|
||||
objectKey,
|
||||
comparator,
|
||||
scaleName,
|
||||
marginStepsApplied: rightOperand ? marginSteps : 0,
|
||||
fallbackBehavior,
|
||||
minRulePossibilityUsed: minRulePossibility,
|
||||
evaluationStarted: Date.now()
|
||||
};
|
||||
|
||||
let evaluationMeta = options.trackEvaluation ? {
|
||||
...ruleMetaBase,
|
||||
leftOperandDetails: {},
|
||||
rightOperandDetails: {}
|
||||
} : null;
|
||||
|
||||
// Evaluate left operand
|
||||
const leftOpResult = this._evaluateOperand(
|
||||
userId, userKey, objectId, objectKey,
|
||||
leftOperand, visited, currentRelation, options, 'left', 0, evaluationMeta, scale // No margin for left
|
||||
);
|
||||
if (evaluationMeta) evaluationMeta.leftOperandDetails = leftOpResult.meta || {};
|
||||
|
||||
// Evaluate right operand
|
||||
const rightOpResult = this._evaluateOperand(
|
||||
userId, userKey, objectId, objectKey,
|
||||
rightOperand, visited, currentRelation, options, 'right', marginSteps, evaluationMeta, scale // Apply margin for right
|
||||
);
|
||||
if (evaluationMeta) evaluationMeta.rightOperandDetails = rightOpResult.meta || {};
|
||||
|
||||
// Perform comparison of qualitative intervals
|
||||
let comparisonOutput = this._compareBlurredValues(
|
||||
leftOpResult, rightOpResult, comparator, fallbackBehavior, rule, options, ruleMetaBase, scale
|
||||
);
|
||||
|
||||
// Apply minimum rule possibility threshold
|
||||
if (scale.compare(comparisonOutput.possibility, minRulePossibility) < 0) {
|
||||
if (evaluationMeta && evaluationMeta.comparisonStep) {
|
||||
evaluationMeta.comparisonStep.finalPossibilityBeforeMinRule = comparisonOutput.possibility;
|
||||
}
|
||||
comparisonOutput.possibility = scale.bottom;
|
||||
comparisonOutput.reason = comparisonOutput.reason + '_belowMinRulePossibility';
|
||||
if (evaluationMeta && evaluationMeta.comparisonStep) {
|
||||
evaluationMeta.comparisonStep.adjustedToZeroByMinRule = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (evaluationMeta) {
|
||||
evaluationMeta.finalResult = {
|
||||
possibility: comparisonOutput.possibility,
|
||||
reliability: comparisonOutput.reliability,
|
||||
reason: comparisonOutput.reason
|
||||
};
|
||||
evaluationMeta.evaluationCompleted = Date.now();
|
||||
evaluationMeta.totalEvaluationTime = evaluationMeta.evaluationCompleted - evaluationMeta.evaluationStarted;
|
||||
}
|
||||
|
||||
return {
|
||||
possibility: comparisonOutput.possibility,
|
||||
reliability: comparisonOutput.reliability,
|
||||
reason: comparisonOutput.reason,
|
||||
meta: evaluationMeta
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('QualitativeRelationalComparatorRule evaluation error:', error);
|
||||
return {
|
||||
possibility: fallbackBehavior === 'allow' ? 1 : 0,
|
||||
reliability: 0,
|
||||
reason: 'error',
|
||||
meta: { error: error.message, fallbackBehavior }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the qualitative scale by name
|
||||
* @private
|
||||
*/
|
||||
_getQualitativeScale(scaleName) {
|
||||
switch (scaleName) {
|
||||
case 'binary':
|
||||
return QualitativeScale.binary();
|
||||
case 'ternary':
|
||||
return QualitativeScale.ternary();
|
||||
case 'five-point':
|
||||
return QualitativeScale.fivePoint();
|
||||
case 'ten-point':
|
||||
return QualitativeScale.tenPoint();
|
||||
default:
|
||||
console.warn(`Unknown scale name: ${scaleName}, using default five-point scale`);
|
||||
return DEFAULT_QUALITATIVE_SCALE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single operand and return qualitative interval and possibility
|
||||
* @private
|
||||
*/
|
||||
_evaluateOperand(userId, userKey, objectId, objectKey, operandConfig, visited, currentRelation, options, side, marginSteps, evaluationMeta, scale) {
|
||||
if (!operandConfig) {
|
||||
return {
|
||||
values: [],
|
||||
possibility: scale.bottom,
|
||||
meta: { error: `No ${side} operand configuration` }
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const operandMeta = options.trackEvaluation ? {
|
||||
side,
|
||||
operandConfig: { ...operandConfig },
|
||||
marginStepsApplied: marginSteps,
|
||||
evaluationStarted: Date.now()
|
||||
} : null;
|
||||
|
||||
const evaluateFrom = operandConfig.evaluateFrom || 'auto';
|
||||
let evalUserId = userId;
|
||||
let evalUserKey = userKey;
|
||||
let evalObjectId = objectId;
|
||||
let evalObjectKey = objectKey;
|
||||
const nestedRuleConfig = operandConfig.rule;
|
||||
if (evaluateFrom === 'user') {
|
||||
if (nestedRuleConfig.type === 'direct' || nestedRuleConfig.type === 'computed') {
|
||||
evalUserId = userId;
|
||||
evalUserKey = userKey;
|
||||
evalObjectId = userId;
|
||||
evalObjectKey = userKey;
|
||||
} else if (!nestedRuleConfig.extractValues) {
|
||||
evalUserId = objectId;
|
||||
evalUserKey = objectKey;
|
||||
evalObjectId = userId;
|
||||
evalObjectKey = userKey;
|
||||
}
|
||||
} else if (evaluateFrom === 'object') {
|
||||
if (nestedRuleConfig.type === 'direct' || nestedRuleConfig.type === 'computed') {
|
||||
evalUserId = objectId;
|
||||
evalUserKey = objectKey;
|
||||
evalObjectId = objectId;
|
||||
evalObjectKey = objectKey;
|
||||
} else {
|
||||
evalUserId = objectId;
|
||||
evalUserKey = objectKey;
|
||||
evalObjectId = userId;
|
||||
evalObjectKey = userKey;
|
||||
}
|
||||
}
|
||||
if (operandMeta) {
|
||||
operandMeta.evaluateFrom = evaluateFrom;
|
||||
operandMeta.evalUserKey = evalUserKey;
|
||||
operandMeta.evalObjectKey = evalObjectKey;
|
||||
}
|
||||
|
||||
// Evaluate the underlying rule
|
||||
const ruleResult = this.ruleEvaluator.evaluateRule(
|
||||
evalUserId, evalUserKey, evalObjectId, evalObjectKey, operandConfig.rule, visited, currentRelation, options
|
||||
);
|
||||
|
||||
if (operandMeta) {
|
||||
operandMeta.ruleResult = {
|
||||
possibility: ruleResult.possibility,
|
||||
reliability: ruleResult.reliability,
|
||||
reason: ruleResult.reason
|
||||
};
|
||||
}
|
||||
|
||||
// Extract values from the rule result
|
||||
const extractedValues = this._extractValuesFromRuleResult(ruleResult, operandConfig, scale);
|
||||
|
||||
if (operandMeta) {
|
||||
operandMeta.extractedValues = extractedValues;
|
||||
}
|
||||
|
||||
// Apply margin of safety (shift on the scale)
|
||||
const adjustedValues = this._applyMarginSteps(extractedValues, marginSteps, scale);
|
||||
|
||||
if (operandMeta) {
|
||||
operandMeta.adjustedValues = adjustedValues;
|
||||
}
|
||||
|
||||
// Extract blurred values with qualitative decay and blur
|
||||
const blurredValues = this._extractBlurredValues(adjustedValues, operandConfig, scale);
|
||||
|
||||
if (operandMeta) {
|
||||
operandMeta.blurredValues = blurredValues;
|
||||
operandMeta.evaluationCompleted = Date.now();
|
||||
operandMeta.totalEvaluationTime = operandMeta.evaluationCompleted - operandMeta.evaluationStarted;
|
||||
}
|
||||
|
||||
// Aggregate the blurred values using qualitative OWA
|
||||
const aggregatedResult = this._aggregateCrispValues(blurredValues, operandConfig, scale);
|
||||
|
||||
return {
|
||||
values: blurredValues,
|
||||
possibility: aggregatedResult.possibility,
|
||||
meta: operandMeta
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error evaluating ${side} operand:`, error);
|
||||
return {
|
||||
values: [],
|
||||
possibility: scale.bottom,
|
||||
meta: { error: error.message, side }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract values from rule result and convert to qualitative scale
|
||||
* @private
|
||||
*/
|
||||
_extractValuesFromRuleResult(ruleResult, operandConfig, scale) {
|
||||
const values = [];
|
||||
|
||||
if (operandConfig.extractValue !== false && (ruleResult.values || ruleResult.collectedValues)) {
|
||||
const collected = ruleResult.values || ruleResult.collectedValues;
|
||||
// Extract actual values from relations
|
||||
for (const valueObj of collected) {
|
||||
if (valueObj.value !== undefined) {
|
||||
// Convert numeric value to closest qualitative scale value
|
||||
const qualitativeValue = this._convertToQualitativeValue(valueObj.value, scale);
|
||||
values.push({
|
||||
value: qualitativeValue,
|
||||
possibility: this._convertToQualitativeValue(valueObj.possibility || 1, scale),
|
||||
timestamp: valueObj.timestamp,
|
||||
relation: valueObj.relation,
|
||||
meta: valueObj.meta
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use rule's possibility as the value
|
||||
const qualitativeValue = this._convertToQualitativeValue(ruleResult.possibility, scale);
|
||||
values.push({
|
||||
value: qualitativeValue,
|
||||
possibility: qualitativeValue,
|
||||
timestamp: Date.now(),
|
||||
relation: 'rule_result',
|
||||
meta: { source: 'rule_possibility' }
|
||||
});
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a numeric value to the closest qualitative scale value
|
||||
* @private
|
||||
*/
|
||||
_convertToQualitativeValue(numericValue, scale) {
|
||||
// Find the closest value in the scale
|
||||
let closestValue = scale.bottom;
|
||||
let minDistance = Math.abs(numericValue - scale.bottom);
|
||||
|
||||
for (const scaleValue of scale.values) {
|
||||
const distance = Math.abs(numericValue - scaleValue);
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
closestValue = scaleValue;
|
||||
}
|
||||
}
|
||||
|
||||
return closestValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply margin steps to shift values on the qualitative scale
|
||||
* @private
|
||||
*/
|
||||
_applyMarginSteps(values, marginSteps, scale) {
|
||||
if (marginSteps === 0) return values;
|
||||
|
||||
return values.map(valueObj => {
|
||||
const currentIndex = scale.indexOf(valueObj.value);
|
||||
const newIndex = Math.max(0, Math.min(scale.size - 1, currentIndex + marginSteps));
|
||||
const newValue = scale.at(newIndex);
|
||||
|
||||
return {
|
||||
...valueObj,
|
||||
value: newValue
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract blurred values with qualitative decay and blur
|
||||
* @private
|
||||
*/
|
||||
_extractBlurredValues(values, operandConfig, scale) {
|
||||
const {
|
||||
decaySteps = 1,
|
||||
decayPeriod = 'HOUR',
|
||||
possibilityDecayDirection = 'down',
|
||||
valueBlurDirection = 'neutral',
|
||||
baseBlurSteps = 1,
|
||||
minOperandPossibility = 0
|
||||
} = operandConfig;
|
||||
|
||||
const blurredValues = [];
|
||||
|
||||
for (const valueObj of values) {
|
||||
const pointValue = valueObj.value;
|
||||
const initialPossibility = valueObj.possibility;
|
||||
const timestamp = valueObj.timestamp || Date.now();
|
||||
|
||||
// Calculate periods elapsed
|
||||
const periodsElapsed = this._calculatePeriodsElapsed(timestamp, decayPeriod);
|
||||
|
||||
// Calculate decayed possibility
|
||||
const decayedPossibility = this._calculateDecayedPossibility(
|
||||
initialPossibility, periodsElapsed, decaySteps, possibilityDecayDirection, scale
|
||||
);
|
||||
|
||||
// Skip if possibility is too low
|
||||
if (scale.compare(decayedPossibility, minOperandPossibility) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate blur amount based on possibility loss
|
||||
const possibilityLossSteps = this._calculatePossibilityLossSteps(
|
||||
initialPossibility, decayedPossibility, scale
|
||||
);
|
||||
const blurSteps = Math.floor(possibilityLossSteps * baseBlurSteps);
|
||||
|
||||
// Create qualitative interval
|
||||
const blurredInterval = this._createQualitativeInterval(
|
||||
pointValue, blurSteps, valueBlurDirection, scale
|
||||
);
|
||||
|
||||
blurredValues.push({
|
||||
interval: blurredInterval,
|
||||
possibility: decayedPossibility,
|
||||
originalValue: pointValue,
|
||||
originalPossibility: initialPossibility,
|
||||
timestamp,
|
||||
relation: valueObj.relation,
|
||||
meta: {
|
||||
...valueObj.meta,
|
||||
periodsElapsed,
|
||||
possibilityLossSteps,
|
||||
blurSteps
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return blurredValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate periods elapsed since timestamp
|
||||
* @private
|
||||
*/
|
||||
_calculatePeriodsElapsed(timestamp, decayPeriod) {
|
||||
const now = Date.now();
|
||||
const elapsed = now - timestamp;
|
||||
|
||||
const periodMs = {
|
||||
'MINUTE': 60 * 1000,
|
||||
'HOUR': 60 * 60 * 1000,
|
||||
'DAY': 24 * 60 * 60 * 1000,
|
||||
'WEEK': 7 * 24 * 60 * 60 * 1000,
|
||||
'MONTH': 30 * 24 * 60 * 60 * 1000,
|
||||
'YEAR': 365 * 24 * 60 * 60 * 1000
|
||||
};
|
||||
|
||||
return Math.floor(elapsed / (periodMs[decayPeriod] || periodMs['HOUR']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate decayed possibility using qualitative scale steps
|
||||
* @private
|
||||
*/
|
||||
_calculateDecayedPossibility(initialPossibility, periodsElapsed, decaySteps, direction, scale) {
|
||||
const initialIndex = scale.indexOf(initialPossibility);
|
||||
const totalDecaySteps = periodsElapsed * decaySteps;
|
||||
|
||||
let newIndex;
|
||||
switch (direction) {
|
||||
case 'down':
|
||||
newIndex = Math.max(0, initialIndex - totalDecaySteps);
|
||||
break;
|
||||
case 'up':
|
||||
newIndex = Math.min(scale.size - 1, initialIndex + totalDecaySteps);
|
||||
break;
|
||||
case 'neutral':
|
||||
const targetIndex = Math.floor(scale.size / 2); // Middle of scale
|
||||
if (initialIndex > targetIndex) {
|
||||
newIndex = Math.max(targetIndex, initialIndex - totalDecaySteps);
|
||||
} else {
|
||||
newIndex = Math.min(targetIndex, initialIndex + totalDecaySteps);
|
||||
}
|
||||
break;
|
||||
case 'stable':
|
||||
default:
|
||||
newIndex = initialIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
return scale.at(newIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the number of steps of possibility loss
|
||||
* @private
|
||||
*/
|
||||
_calculatePossibilityLossSteps(initialPossibility, decayedPossibility, scale) {
|
||||
const initialIndex = scale.indexOf(initialPossibility);
|
||||
const decayedIndex = scale.indexOf(decayedPossibility);
|
||||
return Math.abs(initialIndex - decayedIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a qualitative interval by blurring around a point value
|
||||
* @private
|
||||
*/
|
||||
_createQualitativeInterval(pointValue, blurSteps, direction, scale) {
|
||||
const pointIndex = scale.indexOf(pointValue);
|
||||
|
||||
let lowerIndex, upperIndex;
|
||||
switch (direction) {
|
||||
case 'down':
|
||||
lowerIndex = Math.max(0, pointIndex - blurSteps);
|
||||
upperIndex = pointIndex;
|
||||
break;
|
||||
case 'up':
|
||||
lowerIndex = pointIndex;
|
||||
upperIndex = Math.min(scale.size - 1, pointIndex + blurSteps);
|
||||
break;
|
||||
case 'neutral':
|
||||
default:
|
||||
lowerIndex = Math.max(0, pointIndex - blurSteps);
|
||||
upperIndex = Math.min(scale.size - 1, pointIndex + blurSteps);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
lower: scale.at(lowerIndex),
|
||||
upper: scale.at(upperIndex)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate blurred values using qualitative OWA fusion with optional bilattice reasoning
|
||||
* @private
|
||||
*/
|
||||
_aggregateCrispValues(blurredValues, operandConfig, scale) {
|
||||
if (blurredValues.length === 0) {
|
||||
return { possibility: scale.bottom };
|
||||
}
|
||||
|
||||
if (blurredValues.length === 1) {
|
||||
return { possibility: blurredValues[0].possibility };
|
||||
}
|
||||
|
||||
const aggregator = operandConfig.aggregator || 'max';
|
||||
const useBilattice = operandConfig.useBilattice || false;
|
||||
const epistemicMode = operandConfig.epistemicMode || 'hybrid';
|
||||
const capacityType = operandConfig.capacityType || 'simple_support';
|
||||
|
||||
// Convert blurred values to collected values format for bilattice analysis
|
||||
const collectedValues = blurredValues.map((bv, index) => ({
|
||||
value: bv.possibility, // Use possibility as the value for bilattice analysis
|
||||
possibility: bv.possibility,
|
||||
path: [`blurred_value_${index}`],
|
||||
source: {
|
||||
entityKey: 'qualitative_operand',
|
||||
relation: 'blurred_value',
|
||||
step: index
|
||||
},
|
||||
metadata: {
|
||||
timestamp: bv.timestamp || Date.now(),
|
||||
reliability: 1.0,
|
||||
originalValue: bv.originalValue,
|
||||
originalPossibility: bv.originalPossibility,
|
||||
interval: bv.interval,
|
||||
blurSteps: bv.meta?.blurSteps,
|
||||
periodsElapsed: bv.meta?.periodsElapsed
|
||||
}
|
||||
}));
|
||||
|
||||
// Use bilattice-enhanced evidence combination if enabled
|
||||
if (useBilattice) {
|
||||
const capacity = this._createCapacityFromValues(collectedValues, scale, capacityType);
|
||||
const bilatticeResult = this._combineEvidenceWithBilattice(collectedValues, {
|
||||
method: aggregator,
|
||||
useBilattice: true,
|
||||
capacity: capacity,
|
||||
scale: scale,
|
||||
epistemicMode: epistemicMode
|
||||
});
|
||||
|
||||
return {
|
||||
possibility: bilatticeResult.value,
|
||||
epistemicAnalysis: bilatticeResult.epistemicAnalysis,
|
||||
aggregationMethod: `bilattice_${epistemicMode}`
|
||||
};
|
||||
}
|
||||
|
||||
// Standard qualitative OWA fusion
|
||||
const values = blurredValues.map(v => v.possibility);
|
||||
const metas = blurredValues.map(v => v.meta);
|
||||
|
||||
// Generate OWA weights
|
||||
const weights = getOWAQualitativeWeights(aggregator, values.length, null, scale);
|
||||
|
||||
// Perform qualitative OWA fusion
|
||||
const result = OWAQualitativeFusion.fuseWithMeta(values, weights, weights, aggregator, scale);
|
||||
|
||||
return {
|
||||
possibility: result.value,
|
||||
aggregationMethod: `owa_${aggregator}`
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use _aggregateCrispValues instead. Removal after Stage 2.
|
||||
*/
|
||||
_aggregateBlurredValues(blurredValues, operandConfig, scale) {
|
||||
if (!this._warnedAggregateBlurredValues) {
|
||||
this._warnedAggregateBlurredValues = true;
|
||||
console.warn('[QualitativeRelationalComparatorRule] _aggregateBlurredValues is deprecated; use _aggregateCrispValues instead.');
|
||||
}
|
||||
return this._aggregateCrispValues(blurredValues, operandConfig, scale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare blurred qualitative intervals
|
||||
* @private
|
||||
*/
|
||||
_compareBlurredValues(leftResult, rightResult, comparator, fallbackBehavior, rule, options, ruleMetaBase, scale) {
|
||||
const leftValues = leftResult.values || [];
|
||||
const rightValues = rightResult.values || [];
|
||||
|
||||
if (leftValues.length === 0 || rightValues.length === 0) {
|
||||
return {
|
||||
possibility: fallbackBehavior === 'allow' ? scale.top : scale.bottom,
|
||||
reliability: 0,
|
||||
reason: 'insufficient_values'
|
||||
};
|
||||
}
|
||||
|
||||
// Compare all combinations of left and right intervals
|
||||
const comparisonResults = [];
|
||||
|
||||
for (const leftValue of leftValues) {
|
||||
for (const rightValue of rightValues) {
|
||||
const comparisonPossibility = this._calculateQualitativeIntervalComparison(
|
||||
leftValue.interval, rightValue.interval, comparator, scale
|
||||
);
|
||||
|
||||
// Combine with confidence weights (min operation in qualitative logic)
|
||||
const combinedPossibility = scale.min(comparisonPossibility, scale.min(leftValue.possibility, rightValue.possibility));
|
||||
|
||||
comparisonResults.push({
|
||||
possibility: combinedPossibility,
|
||||
leftInterval: leftValue.interval,
|
||||
rightInterval: rightValue.interval,
|
||||
leftPossibility: leftValue.possibility,
|
||||
rightPossibility: rightValue.possibility
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Take the maximum possibility across all comparisons
|
||||
const maxPossibility = scale.maxAll(comparisonResults.map(r => r.possibility));
|
||||
|
||||
return {
|
||||
possibility: maxPossibility,
|
||||
reliability: 1, // Qualitative comparisons are considered fully reliable
|
||||
reason: 'qualitative_interval_comparison'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate qualitative interval comparison using possibility theory
|
||||
* @private
|
||||
*/
|
||||
_calculateQualitativeIntervalComparison(leftInterval, rightInterval, comparator, scale) {
|
||||
const { lower: lLower, upper: lUpper } = leftInterval;
|
||||
const { lower: rLower, upper: rUpper } = rightInterval;
|
||||
|
||||
switch (comparator) {
|
||||
case '>':
|
||||
// Possibility(L > R): Is it possible that a value from L is greater than a value from R?
|
||||
// This is true if the top of L is greater than the bottom of R
|
||||
return scale.compare(lUpper, rLower) > 0 ? scale.top : scale.bottom;
|
||||
|
||||
case '>=':
|
||||
// Possibility(L >= R): Is it possible that a value from L is >= a value from R?
|
||||
return scale.compare(lUpper, rLower) >= 0 ? scale.top : scale.bottom;
|
||||
|
||||
case '<':
|
||||
// Possibility(L < R): Is it possible that a value from L is less than a value from R?
|
||||
// This is true if the bottom of L is less than the top of R
|
||||
return scale.compare(lLower, rUpper) < 0 ? scale.top : scale.bottom;
|
||||
|
||||
case '<=':
|
||||
// Possibility(L <= R): Is it possible that a value from L is <= a value from R?
|
||||
return scale.compare(lLower, rUpper) <= 0 ? scale.top : scale.bottom;
|
||||
|
||||
case '==':
|
||||
// Possibility(L == R): Is it possible that intervals overlap?
|
||||
// This is true if there's any overlap between the intervals
|
||||
return (scale.compare(lUpper, rLower) >= 0 && scale.compare(lLower, rUpper) <= 0) ? scale.top : scale.bottom;
|
||||
|
||||
case '!=':
|
||||
// Possibility(L != R): Is it possible that intervals don't overlap?
|
||||
// This is true if there's no overlap between the intervals
|
||||
return (scale.compare(lUpper, rLower) < 0 || scale.compare(lLower, rUpper) > 0) ? scale.top : scale.bottom;
|
||||
|
||||
default:
|
||||
console.warn(`Unknown comparator: ${comparator}`);
|
||||
return scale.bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user