import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; import { Arbiter } from '../../src/index.js'; import { OWAFusion } from '../../src/utils/OWAFusion.js'; const EPS = 1e-6; function approxEqual(actual, expected, epsilon = EPS) { assert.ok(Math.abs(actual - expected) <= epsilon, `expected ${expected} but got ${actual}`); } function addValueRelation(arbiter, src, relation, dst, value, possibility, reliability) { arbiter.addRelation(src, relation, dst, { value, possibility, reliability }); arbiter.setRelationConfig(relation, { type: 'direct' }); } function buildComparatorRule(leftRelations, rightRelations, weights) { return { type: 'relational_comparator', comparator: '>=', fallbackBehavior: 'deny', left: { rule: { union: { rules: leftRelations.map((relation) => ({ type: 'direct', relation })), aggregator: 'owa', owaWeights: weights } }, extractValue: true, aggregator: 'owa', owaWeights: weights }, right: { rule: { union: { rules: rightRelations.map((relation) => ({ type: 'direct', relation })), aggregator: 'owa', owaWeights: weights } }, extractValue: true, aggregator: 'owa', owaWeights: weights, evaluateFrom: 'object' } }; } function fuseTriples(values, possibilities, reliabilities, weights) { const metas = values.map(() => ({})); return OWAFusion.fuseTriplesWithMeta(values, possibilities, reliabilities, metas, weights, 'owa'); } function computeComparatorOutcome(left, right, weights) { const fusedLeft = fuseTriples(left.values, left.possibilities, left.reliabilities, weights); const fusedRight = fuseTriples(right.values, right.possibilities, right.reliabilities, weights); const comparison = fusedLeft.value >= fusedRight.value ? 1 : 0; const averageOperandPossibility = (fusedLeft.possibility + fusedRight.possibility) / 2; const confidenceWeight = Math.min(averageOperandPossibility * 2, 1.0); return { possibility: comparison * confidenceWeight, reliability: Math.min(fusedLeft.reliability, fusedRight.reliability) }; } describe('Relational comparator uncertainty', () => { test('OWA value aggregation drives comparator possibility', () => { const arbiter = new Arbiter(); arbiter.addNode('user:alice', 'user'); arbiter.addNode('doc:1', 'doc'); const weights = [0.6, 0.3, 0.1]; const left = { relations: ['risk_low', 'risk_mid', 'risk_high'], values: [10, 20, 40], possibilities: [0.9, 0.6, 0.4], reliabilities: [0.8, 0.9, 0.7] }; const right = { relations: ['limit_low', 'limit_mid', 'limit_high'], values: [15, 25, 30], possibilities: [0.8, 0.5, 0.9], reliabilities: [0.9, 0.8, 0.95] }; left.relations.forEach((relation, idx) => { addValueRelation(arbiter, 'user:alice', relation, 'doc:1', left.values[idx], left.possibilities[idx], left.reliabilities[idx]); }); right.relations.forEach((relation, idx) => { addValueRelation(arbiter, 'doc:1', relation, 'doc:1', right.values[idx], right.possibilities[idx], right.reliabilities[idx]); }); arbiter.setRelationConfig('risk_ok', buildComparatorRule(left.relations, right.relations, weights)); const expected = computeComparatorOutcome(left, right, weights); const result = arbiter.check('user:alice', 'risk_ok', 'doc:1', { fastPath: false, explain: true }); approxEqual(result.possibility, expected.possibility); approxEqual(result.reliability, expected.reliability); }); test('low operand confidence scales comparator output', () => { const arbiter = new Arbiter(); arbiter.addNode('user:alice', 'user'); arbiter.addNode('doc:1', 'doc'); const weights = [0.5, 0.3, 0.2]; const left = { relations: ['risk_a', 'risk_b', 'risk_c'], values: [50, 30, 10], possibilities: [0.1, 0.1, 0.1], reliabilities: [0.9, 0.9, 0.9] }; const right = { relations: ['limit_a', 'limit_b', 'limit_c'], values: [20, 15, 5], possibilities: [0.1, 0.1, 0.1], reliabilities: [0.9, 0.9, 0.9] }; left.relations.forEach((relation, idx) => { addValueRelation(arbiter, 'user:alice', relation, 'doc:1', left.values[idx], left.possibilities[idx], left.reliabilities[idx]); }); right.relations.forEach((relation, idx) => { addValueRelation(arbiter, 'doc:1', relation, 'doc:1', right.values[idx], right.possibilities[idx], right.reliabilities[idx]); }); arbiter.setRelationConfig('risk_ok', buildComparatorRule(left.relations, right.relations, weights)); const expected = computeComparatorOutcome(left, right, weights); const result = arbiter.check('user:alice', 'risk_ok', 'doc:1', { fastPath: false, explain: true }); approxEqual(result.possibility, expected.possibility); approxEqual(result.reliability, expected.reliability); assert.ok(result.possibility < 1, 'expected confidence scaling to reduce possibility'); }); test('OWA union aggregates comparator results with uncertainty', () => { const arbiter = new Arbiter(); arbiter.addNode('user:alice', 'user'); arbiter.addNode('doc:1', 'doc'); const weights = [0.6, 0.3, 0.1]; const unionWeights = [0.7, 0.3]; const leftA = { relations: ['risk_a1', 'risk_a2', 'risk_a3'], values: [12, 18, 25], possibilities: [0.9, 0.7, 0.5], reliabilities: [0.9, 0.8, 0.7] }; const rightA = { relations: ['limit_a1', 'limit_a2', 'limit_a3'], values: [10, 15, 20], possibilities: [0.8, 0.6, 0.7], reliabilities: [0.9, 0.9, 0.8] }; const leftB = { relations: ['risk_b1', 'risk_b2', 'risk_b3'], values: [8, 9, 11], possibilities: [0.4, 0.5, 0.6], reliabilities: [0.8, 0.8, 0.8] }; const rightB = { relations: ['limit_b1', 'limit_b2', 'limit_b3'], values: [9, 10, 12], possibilities: [0.7, 0.6, 0.5], reliabilities: [0.9, 0.9, 0.9] }; leftA.relations.forEach((relation, idx) => { addValueRelation(arbiter, 'user:alice', relation, 'doc:1', leftA.values[idx], leftA.possibilities[idx], leftA.reliabilities[idx]); }); rightA.relations.forEach((relation, idx) => { addValueRelation(arbiter, 'doc:1', relation, 'doc:1', rightA.values[idx], rightA.possibilities[idx], rightA.reliabilities[idx]); }); leftB.relations.forEach((relation, idx) => { addValueRelation(arbiter, 'user:alice', relation, 'doc:1', leftB.values[idx], leftB.possibilities[idx], leftB.reliabilities[idx]); }); rightB.relations.forEach((relation, idx) => { addValueRelation(arbiter, 'doc:1', relation, 'doc:1', rightB.values[idx], rightB.possibilities[idx], rightB.reliabilities[idx]); }); const comparatorA = buildComparatorRule(leftA.relations, rightA.relations, weights); const comparatorB = buildComparatorRule(leftB.relations, rightB.relations, weights); arbiter.setRelationConfig('risk_union', { union: { rules: [comparatorA, comparatorB], aggregator: 'owa', owaWeights: unionWeights } }); const outcomeA = computeComparatorOutcome(leftA, rightA, weights); const outcomeB = computeComparatorOutcome(leftB, rightB, weights); const expectedUnion = OWAFusion.fuseWithMeta( [outcomeA.possibility, outcomeB.possibility], [{}, {}], unionWeights, 'owa', true ).value; const result = arbiter.check('user:alice', 'risk_union', 'doc:1', { fastPath: false }); approxEqual(result.possibility, expectedUnion); }); });