js-rigor: OWA fusion hardened; reliabilityWeighting, shorthand children, cache key
Probe sweep of the OWA surfaces found three real defects:
- reliabilityWeighting was a silent no-op everywhere: every implementation
scaled possibilities by metas[i].reliability, but no child meta ever
carried a reliability field (the compiled direct omitted it and the
DirectRule handler omitted it too), so the weighting was always x1.0.
All weighting branches now use the tracked child reliabilities, and the
DirectRule handler + its meta now carry the relation's reliability.
- The compiled union and the direct_list fast path had no
reliabilityWeighting branch at all; both now apply it.
- Shorthand children ({ relation: 'editor' }) dispatch to the direct
handler but carry no type, so _getRuleResultCacheKey derived the generic
'rule' suffix for every shorthand child of a logical rule — the first
child's cached result was served for all of them (the fallback path
returned the owner's 0.8 for the editor). The key derivation now matches
the shorthand dispatch. The RuleEvaluator also treats shorthand operands
as direct rules instead of unknown_rule_type on the non-compiled path.
New pins: an OWA differential property (custom weights, max/min/average
aggregators, reliabilityWeighting, compiled path) and a multi_hop
pathAggregation=owa fixed pin with reliability propagation.
This commit is contained in:
@@ -455,7 +455,10 @@ export class CompiledEvaluator {
|
||||
? compiled._precomputedWeights
|
||||
: getOWAWeightsFromRule(compiled, possibilities.length, metas);
|
||||
let result;
|
||||
if (unionOWAWeights.some(w => w > 0)) {
|
||||
if (compiled.reliabilityWeighting) {
|
||||
const weighted = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0));
|
||||
result = OWAFusion.fuseWithMeta(weighted, metas, unionOWAWeights, compiled.aggregator || 'max', true, owaTraceOptions);
|
||||
} else if (unionOWAWeights.some(w => w > 0)) {
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, unionOWAWeights, compiled.aggregator || 'max', true, owaTraceOptions);
|
||||
} else {
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, null, 'max', true, owaTraceOptions);
|
||||
@@ -565,7 +568,7 @@ export class CompiledEvaluator {
|
||||
|
||||
let result;
|
||||
if (compiled.reliabilityWeighting) {
|
||||
const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (metas[i]?.reliability || 1.0));
|
||||
const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0));
|
||||
result = OWAFusion.fuseWithMeta(reliabilityWeightedPossibilities, metas, finalWeights, defaultMode, true, owaTraceOptions);
|
||||
} else {
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, finalWeights, defaultMode, true, owaTraceOptions);
|
||||
@@ -644,10 +647,17 @@ export class CompiledEvaluator {
|
||||
if (compiled.aggregator || compiled.owaWeights) {
|
||||
const possibilities = [a.possibility, 1 - b.possibility];
|
||||
const metas = [a.meta, { exclusion_complement: b.meta }];
|
||||
const reliabilityWeights = [
|
||||
a.reliability !== undefined ? a.reliability : 1.0,
|
||||
b.reliability !== undefined ? b.reliability : 1.0
|
||||
];
|
||||
const exclusionOWAWeights = compiled._precomputedWeights && compiled._precomputedWeights.length === 2
|
||||
? compiled._precomputedWeights
|
||||
: getOWAWeightsFromRule(compiled, 2, metas);
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, exclusionOWAWeights, compiled.aggregator || 'min', true, owaTraceOptions);
|
||||
const fusedInputs = compiled.reliabilityWeighting
|
||||
? possibilities.map((poss, i) => poss * (reliabilityWeights[i] ?? 1.0))
|
||||
: possibilities;
|
||||
result = OWAFusion.fuseWithMeta(fusedInputs, metas, exclusionOWAWeights, compiled.aggregator || 'min', true, owaTraceOptions);
|
||||
possibility = result.value;
|
||||
} else {
|
||||
possibility = a.possibility * (1 - b.possibility);
|
||||
@@ -782,7 +792,10 @@ export class CompiledEvaluator {
|
||||
? compiled._precomputedWeights
|
||||
: getOWAWeightsFromRule(compiled, possibilities.length, metas);
|
||||
const aggregator = op === 'intersection' ? (compiled.aggregator || 'min') : (compiled.aggregator || 'max');
|
||||
const result = OWAFusion.fuseWithMeta(possibilities, metas, weights, aggregator, true, owaTraceOptions);
|
||||
const fusedInputs = compiled.reliabilityWeighting
|
||||
? possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0))
|
||||
: possibilities;
|
||||
const result = OWAFusion.fuseWithMeta(fusedInputs, metas, weights, aggregator, true, owaTraceOptions);
|
||||
|
||||
let listReliability = 1.0;
|
||||
if (includeOwaTrace && result.trace && typeof result.trace.selectedIndex === 'number') {
|
||||
|
||||
@@ -95,7 +95,10 @@ export class RuleEvaluator {
|
||||
}
|
||||
|
||||
// For other rule types, evaluate normally first
|
||||
const handler = this.ruleHandlers[rule.type];
|
||||
// Shorthand operand objects ({ relation: 'owner' } inside logical rules,
|
||||
// or caller-supplied raw configs) carry no type: treat them as direct
|
||||
// rules instead of failing with unknown_rule_type.
|
||||
const handler = this.ruleHandlers[rule.type || (rule.relation || rule.rel || rule.label || rule.name ? 'direct' : null)];
|
||||
if (!handler) {
|
||||
return {
|
||||
possibility_allow: 0,
|
||||
@@ -123,8 +126,12 @@ export class RuleEvaluator {
|
||||
if (rule) {
|
||||
if (rule.union || rule.intersection || rule.exclusion) {
|
||||
suffix = 'logical';
|
||||
} else if (rule.type === 'direct') {
|
||||
suffix = `direct:${rule.relation || 'unknown'}`;
|
||||
} else if (rule.type === 'direct' || (!rule.type && (rule.relation || rule.rel || rule.label || rule.name) && !rule.union && !rule.intersection && !rule.exclusion)) {
|
||||
// Shorthand operands ({ relation: 'editor' }) dispatch to the direct
|
||||
// handler but carry no type; without this, every shorthand child of
|
||||
// a logical rule shares one cache key and the first child's result
|
||||
// is served for all of them.
|
||||
suffix = `direct:${rule.relation || rule.rel || rule.label || rule.name || 'unknown'}`;
|
||||
} else if (rule.type === 'tuple_to_userset') {
|
||||
suffix = `tupleset:${rule.tuplesetRelation || 'unknown'}:${rule.computedRelation || 'unknown'}`;
|
||||
} else if (rule.type === 'chain' && Array.isArray(rule.steps)) {
|
||||
|
||||
@@ -70,6 +70,7 @@ export class DirectRule extends BaseRule {
|
||||
|
||||
const authResult = {
|
||||
possibility: relationStrength,
|
||||
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
||||
possibility_allow: relationStrength, // For binary mode
|
||||
possibility_deny: 0, // DirectRule doesn't deny
|
||||
...(includeMeta && {
|
||||
@@ -80,6 +81,7 @@ export class DirectRule extends BaseRule {
|
||||
relation: relName,
|
||||
reverse: reverse || false,
|
||||
strength: relationStrength,
|
||||
reliability: directRel.reliability !== undefined ? directRel.reliability : 1.0,
|
||||
source: _source,
|
||||
allow: _allowMeta
|
||||
},
|
||||
|
||||
@@ -984,6 +984,9 @@ export class LogicalOperators extends BaseRule {
|
||||
inputCount: possibilities.length,
|
||||
bilatticeAnalysis: epistemicAnalysis
|
||||
});
|
||||
} else if (unionConfig.reliabilityWeighting) {
|
||||
const weighted = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0));
|
||||
result = OWAFusion.fuseWithMeta(weighted, metas, unionOWAWeights, unionConfig.aggregator || 'max', true, owaTraceOptions);
|
||||
} else if (unionOWAWeights.some(w => w > 0)) {
|
||||
Arbiter.DEBUG && Arbiter.log('union using OWA evidential fusion', {
|
||||
aggregator: unionConfig.aggregator || 'max',
|
||||
@@ -1148,7 +1151,7 @@ export class LogicalOperators extends BaseRule {
|
||||
|
||||
let result;
|
||||
if (intersectionConfig.reliabilityWeighting) {
|
||||
const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (metas[i]?.reliability || 1.0));
|
||||
const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (reliabilities[i] ?? 1.0));
|
||||
result = OWAFusion.fuseWithMeta(reliabilityWeightedPossibilities, metas, finalWeights, defaultMode, true, owaTraceOptions);
|
||||
} else {
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, finalWeights, defaultMode, true, owaTraceOptions);
|
||||
@@ -1279,7 +1282,11 @@ export class LogicalOperators extends BaseRule {
|
||||
});
|
||||
|
||||
if (exclusionConfig.reliabilityWeighting) {
|
||||
const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (metas[i]?.reliability || 1.0));
|
||||
const legReliabilities = [
|
||||
a.reliability !== undefined ? a.reliability : 1.0,
|
||||
b.reliability !== undefined ? b.reliability : 1.0
|
||||
];
|
||||
const reliabilityWeightedPossibilities = possibilities.map((poss, i) => poss * (legReliabilities[i] ?? 1.0));
|
||||
result = OWAFusion.fuseWithMeta(reliabilityWeightedPossibilities, metas, exclusionOWAWeights, exclusionConfig.aggregator || 'min', true, owaTraceOptions);
|
||||
} else {
|
||||
result = OWAFusion.fuseWithMeta(possibilities, metas, exclusionOWAWeights, exclusionConfig.aggregator || 'min', true, owaTraceOptions);
|
||||
|
||||
Reference in New Issue
Block a user