import assert from 'node:assert/strict'; import { describe, test } from 'node:test'; import { Arbiter } from '../../src/core/Arbiter.js'; import { OWAFusion } from '../../src/utils/OWAFusion.js'; function createRng(seed) { let state = seed >>> 0; return () => { state = (1664525 * state + 1013904223) >>> 0; return state / 0x100000000; }; } function randInt(rng, max) { return Math.floor(rng() * max); } function randFloat(rng, min = 0, max = 1) { return min + (max - min) * rng(); } function buildComparatorArbiter() { const arbiter = new Arbiter(); arbiter.addNode('user:1', 'user'); arbiter.addNode('resource:1', 'resource'); arbiter.setRelationConfig('risk_score', { type: 'direct' }); arbiter.setRelationConfig('risk_bonus', { type: 'direct' }); arbiter.setRelationConfig('risk_noise', { type: 'direct' }); arbiter.setRelationConfig('risk_limit', { type: 'direct' }); arbiter.setRelationConfig('risk_ok_owa', { type: 'relational_comparator', comparator: '<=', fallbackBehavior: 'deny', left: { rule: { union: { rules: [ { type: 'direct', relation: 'risk_score' }, { type: 'direct', relation: 'risk_bonus' }, { type: 'direct', relation: 'risk_noise' } ], aggregator: 'owa', owaWeights: [0.5, 0.3, 0.2] } }, extractValue: true, valueRelation: 'risk_score', aggregator: 'owa', owaWeights: [0.5, 0.3, 0.2] }, right: { rule: { type: 'direct', relation: 'risk_limit' }, extractValue: true, valueRelation: 'risk_limit', evaluateFrom: 'object' } }); arbiter.registerDependencyIndex(new Map([ ['risk_score', { all: new Set(['risk_ok_owa']), byLevel: { never: new Set(), always: new Set(), requires: new Set(), when: new Set(), unless: new Set(), ordinary: new Set(['risk_ok_owa']) } }], ['risk_bonus', { all: new Set(['risk_ok_owa']), byLevel: { never: new Set(), always: new Set(), requires: new Set(), when: new Set(), unless: new Set(), ordinary: new Set(['risk_ok_owa']) } }], ['risk_noise', { all: new Set(['risk_ok_owa']), byLevel: { never: new Set(), always: new Set(), requires: new Set(), when: new Set(), unless: new Set(), ordinary: new Set(['risk_ok_owa']) } }], ['risk_limit', { all: new Set(['risk_ok_owa']), byLevel: { never: new Set(), always: new Set(), requires: new Set(), when: new Set(), unless: new Set(), ordinary: new Set(['risk_ok_owa']) } }] ])); return arbiter; } describe('Comparator aggregation properties', () => { test('OWA aggregated value drives comparator decision', () => { const rng = createRng(24); const arbiter = buildComparatorArbiter(); const metas = [{}, {}, {}]; for (let i = 0; i < 120; i++) { const values = [ randFloat(rng, 0, 100), randFloat(rng, 0, 100), randFloat(rng, 0, 100) ]; const limit = randFloat(rng, 0, 100); arbiter.removeRelation('user:1', 'risk_score', 'resource:1'); arbiter.removeRelation('user:1', 'risk_bonus', 'resource:1'); arbiter.removeRelation('user:1', 'risk_noise', 'resource:1'); arbiter.removeRelation('resource:1', 'risk_limit', 'resource:1'); arbiter.addRelation('user:1', 'risk_score', 'resource:1', 1.0, { value: values[0] }); arbiter.addRelation('user:1', 'risk_bonus', 'resource:1', 1.0, { value: values[1] }); arbiter.addRelation('user:1', 'risk_noise', 'resource:1', 1.0, { value: values[2] }); arbiter.addRelation('resource:1', 'risk_limit', 'resource:1', 1.0, { value: limit }); const fused = OWAFusion.fuseWithMeta(values, metas, [0.5, 0.3, 0.2], 'owa', true).value; const expectedAllow = fused <= limit; const result = arbiter.check('user:1', 'risk_ok_owa', 'resource:1', { fastPath: false }); const allow = result.possibility > 0; assert.strictEqual(allow, expectedAllow, 'comparator respects aggregation'); } }); test('increasing a component value does not decrease fused value', () => { const rng = createRng(88); const arbiter = buildComparatorArbiter(); const metas = [{}, {}, {}]; for (let i = 0; i < 120; i++) { const values = [ randFloat(rng, 0, 100), randFloat(rng, 0, 100), randFloat(rng, 0, 100) ]; const index = randInt(rng, values.length); const delta = randFloat(rng, 0, 20); const bumped = values.slice(); bumped[index] += delta; const base = OWAFusion.fuseWithMeta(values, metas, [0.5, 0.3, 0.2], 'owa', true).value; const higher = OWAFusion.fuseWithMeta(bumped, metas, [0.5, 0.3, 0.2], 'owa', true).value; assert.ok(higher >= base - 1e-6, 'fused value is monotonic'); } }); });