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,210 @@
|
||||
import { test, describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { Arbiter } from '../../src/core/Arbiter.js';
|
||||
import { ChainRule } from '../../src/authorization/rules/ChainRule.js';
|
||||
|
||||
describe('HyperbolicLRUCache Invalidation', () => {
|
||||
let arbiter;
|
||||
let chainRule;
|
||||
|
||||
beforeEach(() => {
|
||||
arbiter = new Arbiter({
|
||||
directCheckCacheSize: 1000,
|
||||
directCheckCacheTTL: 60000
|
||||
});
|
||||
chainRule = new ChainRule(arbiter);
|
||||
});
|
||||
|
||||
it('should support key deletion', () => {
|
||||
const cache = chainRule.chainResultCache;
|
||||
|
||||
// Add some entries
|
||||
cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() });
|
||||
cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
cache.set('key3', { result: { possibility: 0.3 }, timestamp: Date.now() });
|
||||
|
||||
assert.ok(cache.has('key1'));
|
||||
assert.ok(cache.has('key2'));
|
||||
assert.ok(cache.has('key3'));
|
||||
|
||||
// Delete specific key
|
||||
const deleted = cache.delete('key2');
|
||||
assert.ok(deleted);
|
||||
assert.ok(cache.has('key1'));
|
||||
assert.ok(!cache.has('key2'));
|
||||
assert.ok(cache.has('key3'));
|
||||
});
|
||||
|
||||
it('should support key invalidation', () => {
|
||||
const cache = chainRule.chainResultCache;
|
||||
|
||||
// Add some entries
|
||||
cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() });
|
||||
cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
|
||||
// Invalidate specific key
|
||||
const invalidated = cache.invalidate('key1');
|
||||
assert.ok(invalidated);
|
||||
assert.ok(!cache.has('key1'));
|
||||
assert.ok(cache.has('key2'));
|
||||
});
|
||||
|
||||
it('should support invalidating multiple keys', () => {
|
||||
const cache = chainRule.chainResultCache;
|
||||
|
||||
// Add some entries
|
||||
cache.set('key1', { result: { possibility: 0.5 }, timestamp: Date.now() });
|
||||
cache.set('key2', { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
cache.set('key3', { result: { possibility: 0.3 }, timestamp: Date.now() });
|
||||
|
||||
// Invalidate multiple keys
|
||||
const invalidatedCount = cache.invalidateMany(['key1', 'key3']);
|
||||
assert.strictEqual(invalidatedCount, 2);
|
||||
assert.ok(!cache.has('key1'));
|
||||
assert.ok(cache.has('key2'));
|
||||
assert.ok(!cache.has('key3'));
|
||||
});
|
||||
|
||||
it('should support pattern-based invalidation', () => {
|
||||
const cache = chainRule.chainResultCache;
|
||||
|
||||
// Add entries with different patterns
|
||||
cache.set('user_123_role_membership', { result: { possibility: 0.5 }, timestamp: Date.now() });
|
||||
cache.set('user_123_permission', { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
cache.set('user_456_role_membership', { result: { possibility: 0.3 }, timestamp: Date.now() });
|
||||
cache.set('user_456_permission', { result: { possibility: 0.7 }, timestamp: Date.now() });
|
||||
|
||||
// Invalidate all entries for user_123
|
||||
const invalidatedCount = cache.invalidateByPattern('user_123');
|
||||
assert.strictEqual(invalidatedCount, 2);
|
||||
assert.ok(!cache.has('user_123_role_membership'));
|
||||
assert.ok(!cache.has('user_123_permission'));
|
||||
assert.ok(cache.has('user_456_role_membership'));
|
||||
assert.ok(cache.has('user_456_permission'));
|
||||
});
|
||||
|
||||
it('should support regex pattern invalidation', () => {
|
||||
const cache = chainRule.chainResultCache;
|
||||
|
||||
// Add entries with different patterns
|
||||
cache.set('user_123_role_membership', { result: { possibility: 0.5 }, timestamp: Date.now() });
|
||||
cache.set('user_123_permission', { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
cache.set('user_456_role_membership', { result: { possibility: 0.3 }, timestamp: Date.now() });
|
||||
cache.set('user_456_permission', { result: { possibility: 0.7 }, timestamp: Date.now() });
|
||||
|
||||
// Invalidate all role_membership entries using regex
|
||||
const roleMembershipPattern = /.*role_membership.*/;
|
||||
const invalidatedCount = cache.invalidateByPattern(roleMembershipPattern);
|
||||
assert.strictEqual(invalidatedCount, 2);
|
||||
assert.ok(!cache.has('user_123_role_membership'));
|
||||
assert.ok(cache.has('user_123_permission'));
|
||||
assert.ok(!cache.has('user_456_role_membership'));
|
||||
assert.ok(cache.has('user_456_permission'));
|
||||
});
|
||||
|
||||
it('should handle cache invalidation in Arbiter direct check cache', () => {
|
||||
// Add some direct check cache entries
|
||||
arbiter.directCheckCache.set('user:alice|can_read|doc:secret', { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
arbiter.directCheckCache.set('user:bob|can_write|doc:public', { result: { possibility: 0.9 }, timestamp: Date.now() });
|
||||
|
||||
assert.ok(arbiter.directCheckCache.has('user:alice|can_read|doc:secret'));
|
||||
assert.ok(arbiter.directCheckCache.has('user:bob|can_write|doc:public'));
|
||||
|
||||
// Invalidate entries for user:alice
|
||||
const invalidatedCount = arbiter.directCheckCache.invalidateByPattern('user:alice');
|
||||
assert.strictEqual(invalidatedCount, 1);
|
||||
assert.ok(!arbiter.directCheckCache.has('user:alice|can_read|doc:secret'));
|
||||
assert.ok(arbiter.directCheckCache.has('user:bob|can_write|doc:public'));
|
||||
});
|
||||
|
||||
it('should handle cache invalidation in RelationManager', () => {
|
||||
// Cache state lives in RelationCaches (RF-08); reach it via `_caches`.
|
||||
const caches = arbiter.relationManager._caches;
|
||||
const srcRelKey = caches.makeSrcRelCacheKey(123, 'role_membership');
|
||||
const dstRelKey = caches.makeDstRelCacheKey(456, 'role_membership');
|
||||
const directKey = caches.makeDirectCacheKey(123, 'role_membership', 456);
|
||||
const otherKey = caches.makeDirectCacheKey(123, 'permission', 789);
|
||||
|
||||
caches.relationLookupCache.set(srcRelKey, [{ src: 123, rel: 'role_membership', dst: 456 }]);
|
||||
caches.relationLookupCache.set(dstRelKey, [{ src: 456, rel: 'role_membership', dst: 789 }]);
|
||||
caches.relationLookupCache.set(directKey, [{ src: 123, rel: 'role_membership', dst: 456 }]);
|
||||
caches.relationLookupCache.set(otherKey, [{ src: 123, rel: 'permission', dst: 789 }]);
|
||||
|
||||
// Test the invalidation method
|
||||
arbiter.relationManager._invalidateRelationCaches(123, 'role_membership', 456);
|
||||
|
||||
// Entries for the (src, rel), (dst, rel) and (src, rel, dst) triple vanish
|
||||
assert.ok(!caches.relationLookupCache.has(srcRelKey));
|
||||
assert.ok(!caches.relationLookupCache.has(dstRelKey));
|
||||
assert.ok(!caches.relationLookupCache.has(directKey));
|
||||
assert.ok(caches.relationLookupCache.has(otherKey)); // Different relation
|
||||
});
|
||||
|
||||
it('should handle cache invalidation in NodeManager', () => {
|
||||
// Add nodes
|
||||
const aliceId = arbiter.nodeManager.addNode('user:alice', 'user', { name: 'Alice' });
|
||||
const bobId = arbiter.nodeManager.addNode('user:bob', 'user', { name: 'Bob' });
|
||||
const carolId = arbiter.nodeManager.addNode('user:carol', 'user', { name: 'Carol' });
|
||||
|
||||
// Direct-check cache keys are composite keys over numeric node ids
|
||||
const aliceKey = arbiter.keyManager.createCompositeKey(aliceId, 'can_read', bobId);
|
||||
const bobTouchingAlice = arbiter.keyManager.createCompositeKey(bobId, 'can_read', aliceId);
|
||||
const carolKey = arbiter.keyManager.createCompositeKey(carolId, 'can_read', bobId);
|
||||
|
||||
arbiter.directCheckCache.set(aliceKey, { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
arbiter.directCheckCache.set(bobTouchingAlice, { result: { possibility: 0.9 }, timestamp: Date.now() });
|
||||
arbiter.directCheckCache.set(carolKey, { result: { possibility: 0.7 }, timestamp: Date.now() });
|
||||
|
||||
assert.ok(arbiter.directCheckCache.has(aliceKey));
|
||||
assert.ok(arbiter.directCheckCache.has(bobTouchingAlice));
|
||||
assert.ok(arbiter.directCheckCache.has(carolKey));
|
||||
|
||||
// Update node data (should trigger cache invalidation via the DecisionCache port)
|
||||
arbiter.nodeManager.updateNodeData('user:alice', { name: 'Alice Updated' });
|
||||
|
||||
// Any entry whose composite key contains alice's id is invalidated
|
||||
assert.ok(!arbiter.directCheckCache.has(aliceKey));
|
||||
assert.ok(!arbiter.directCheckCache.has(bobTouchingAlice));
|
||||
assert.ok(arbiter.directCheckCache.has(carolKey)); // Does not touch alice
|
||||
});
|
||||
|
||||
it('should handle cache invalidation in ChainRule', () => {
|
||||
// Add some chain cache entries
|
||||
chainRule.chainResultCache.set('0_154_role_membership_out|role_permission_out', { result: { possibility: 0.8 }, timestamp: Date.now() });
|
||||
chainRule.chainResultCache.set('0_154_permission_out|access_out', { result: { possibility: 0.9 }, timestamp: Date.now() });
|
||||
|
||||
// Invalidate all chain result cache entries
|
||||
chainRule._invalidateAllChainCaches();
|
||||
|
||||
assert.ok(!chainRule.chainResultCache.has('0_154_role_membership_out|role_permission_out'));
|
||||
assert.ok(!chainRule.chainResultCache.has('0_154_permission_out|access_out'));
|
||||
});
|
||||
|
||||
it('should track eviction statistics', () => {
|
||||
const cache = chainRule.chainResultCache;
|
||||
|
||||
// Fill cache beyond capacity to trigger evictions
|
||||
for (let i = 0; i < chainRule.maxCacheSize + 50; i++) {
|
||||
cache.set(`key_${i}`, { result: { possibility: 0.5 }, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
// Check if eviction stats are tracked
|
||||
if (chainRule.stats) {
|
||||
console.log(`Chain result cache evictions: ${chainRule.stats.evictedResults || 0}`);
|
||||
console.log(`Chain path cache evictions: ${chainRule.stats.evictedPaths || 0}`);
|
||||
}
|
||||
|
||||
// Cache should not exceed capacity
|
||||
assert.ok(cache.size() <= chainRule.maxCacheSize);
|
||||
});
|
||||
});
|
||||
|
||||
console.log('✅ HyperbolicLRUCache Invalidation Test Suite');
|
||||
console.log('🔧 New Features:');
|
||||
console.log(' - delete(key) - Remove specific key');
|
||||
console.log(' - invalidate(key) - Alias for delete');
|
||||
console.log(' - invalidateMany(keys) - Remove multiple keys');
|
||||
console.log(' - invalidateByPattern(pattern) - Remove keys matching pattern');
|
||||
console.log(' - Support for both string and regex patterns');
|
||||
console.log(' - Automatic cache invalidation on node/relation changes');
|
||||
console.log(' - Better staleness handling');
|
||||
@@ -0,0 +1,128 @@
|
||||
import { test, describe, it, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { Arbiter } from '../../src/core/Arbiter.js';
|
||||
import { ChainRule } from '../../src/authorization/rules/ChainRule.js';
|
||||
|
||||
describe('Cache Memory Improvements (SimpleLRUCache default, injectable factory)', () => {
|
||||
let arbiter;
|
||||
let chainRule;
|
||||
|
||||
beforeEach(() => {
|
||||
arbiter = new Arbiter({
|
||||
directCheckCacheSize: 1000,
|
||||
directCheckCacheTTL: 60000
|
||||
});
|
||||
chainRule = new ChainRule(arbiter);
|
||||
});
|
||||
|
||||
it('should use SimpleLRUCache (default cacheFactory) for chain caches', () => {
|
||||
// Verify ChainRule uses the default SimpleLRUCache
|
||||
assert.ok(chainRule.chainResultCache.constructor.name === 'SimpleLRUCache');
|
||||
|
||||
// Verify cache capacity
|
||||
assert.strictEqual(chainRule.maxCacheSize, 2000);
|
||||
});
|
||||
|
||||
it('should use SimpleLRUCache for direct check cache', () => {
|
||||
// Verify Arbiter uses the default SimpleLRUCache
|
||||
assert.ok(arbiter.directCheckCache.constructor.name === 'SimpleLRUCache');
|
||||
|
||||
// Verify cache capacity
|
||||
assert.strictEqual(arbiter.directCheckCacheSize, 1000);
|
||||
});
|
||||
|
||||
it('should use SimpleLRUCache for relation manager caches', () => {
|
||||
// Cache state lives in RelationCaches (RF-08): reach it via `_caches`.
|
||||
assert.ok(arbiter.relationManager._caches.relationLookupCache.constructor.name === 'SimpleLRUCache');
|
||||
assert.ok(arbiter.relationManager._caches.valueLookupCache.constructor.name === 'SimpleLRUCache');
|
||||
|
||||
// Verify cache capacity
|
||||
assert.strictEqual(arbiter.relationManager._caches.maxCacheSize, 10000);
|
||||
});
|
||||
|
||||
it('should use SimpleLRUCache for value manager caches', () => {
|
||||
// Verify ValueManager uses the default SimpleLRUCache
|
||||
assert.ok(arbiter.valueManager.blurredValueCache.constructor.name === 'SimpleLRUCache');
|
||||
assert.ok(arbiter.valueManager.distributionCache.constructor.name === 'SimpleLRUCache');
|
||||
});
|
||||
|
||||
it('should use SimpleLRUCache for similarity manager cache', () => {
|
||||
// SimilarityManager is only constructed when similarityParams are provided
|
||||
const simArbiter = new Arbiter({ similarityParams: {} });
|
||||
assert.ok(simArbiter.similarityManager.nodeKeyToVector.constructor.name === 'SimpleLRUCache');
|
||||
});
|
||||
|
||||
it('should handle cache eviction automatically', () => {
|
||||
// Test that the default cache handles eviction automatically
|
||||
const cache = chainRule.chainResultCache;
|
||||
const initialSize = cache.size();
|
||||
|
||||
// Fill cache beyond capacity
|
||||
for (let i = 0; i < chainRule.maxCacheSize + 100; i++) {
|
||||
cache.set(`key_${i}`, { result: { possibility: 0.5 }, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
// Cache should not exceed capacity
|
||||
assert.ok(cache.size() <= chainRule.maxCacheSize);
|
||||
console.log(`Cache size after overflow: ${cache.size()} (max: ${chainRule.maxCacheSize})`);
|
||||
});
|
||||
|
||||
it('should track eviction statistics', () => {
|
||||
// Test that eviction statistics are tracked
|
||||
const cache = chainRule.chainResultCache;
|
||||
|
||||
// Fill cache beyond capacity to trigger evictions
|
||||
for (let i = 0; i < chainRule.maxCacheSize + 50; i++) {
|
||||
cache.set(`key_${i}`, { result: { possibility: 0.5 }, timestamp: Date.now() });
|
||||
}
|
||||
|
||||
// Check if eviction stats are tracked
|
||||
if (chainRule.stats) {
|
||||
console.log(`Chain result cache evictions: ${chainRule.stats.evictedResults || 0}`);
|
||||
console.log(`Chain path cache evictions: ${chainRule.stats.evictedPaths || 0}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should provide better memory management than Map', () => {
|
||||
// Compare memory usage patterns
|
||||
const mapCache = new Map();
|
||||
const hyperbolicCache = chainRule.chainResultCache;
|
||||
|
||||
// Fill both caches
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const value = { result: { possibility: 0.5 }, timestamp: Date.now() };
|
||||
mapCache.set(`key_${i}`, value);
|
||||
hyperbolicCache.set(`key_${i}`, value);
|
||||
}
|
||||
|
||||
// Map grows indefinitely, the default cache is bounded
|
||||
assert.ok(mapCache.size === 1000);
|
||||
assert.ok(hyperbolicCache.size() <= chainRule.maxCacheSize);
|
||||
|
||||
console.log(`Map size: ${mapCache.size}`);
|
||||
console.log(`SimpleLRUCache size: ${hyperbolicCache.size()}`);
|
||||
});
|
||||
|
||||
it('should handle TTL without manual cleanup', () => {
|
||||
// Test that TTL is handled without manual cleanup
|
||||
const cache = arbiter.directCheckCache;
|
||||
|
||||
// Add entries with different timestamps
|
||||
const now = Date.now();
|
||||
cache.set('key1', { result: { possibility: 0.5 }, timestamp: now });
|
||||
cache.set('key2', { result: { possibility: 0.5 }, timestamp: now - 70000 }); // Expired
|
||||
|
||||
// SimpleLRUCache keeps both entries; TTL is enforced at read time
|
||||
// No manual cleanup needed
|
||||
assert.ok(cache.has('key1'));
|
||||
assert.ok(cache.has('key2')); // Still in cache, but will be evicted based on frequency
|
||||
});
|
||||
});
|
||||
|
||||
console.log('✅ Cache Memory Improvements Test Suite');
|
||||
console.log('📊 Benefits:');
|
||||
console.log(' - Automatic eviction based on frequency and recency');
|
||||
console.log(' - No manual TTL cleanup needed');
|
||||
console.log(' - Bounded memory usage');
|
||||
console.log(' - Better cache hit rates through intelligent eviction');
|
||||
console.log(' - Protection against memory leaks');
|
||||
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* Test Bilattice Integration with Existing Possibilistic Infrastructure
|
||||
*
|
||||
* This test demonstrates how bilattice orderings are integrated into our existing
|
||||
* rule system while maintaining compatibility with possibilistic reasoning.
|
||||
*/
|
||||
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import {
|
||||
QualitativeScale,
|
||||
QualitativeCapacity,
|
||||
BilatticeOrderings,
|
||||
getSetKey
|
||||
} from '../../src/qualitative/index.js';
|
||||
|
||||
describe('Bilattice Integration with Possibilistic Infrastructure', () => {
|
||||
|
||||
test('should demonstrate BaseRule bilattice-enhanced evidence combination', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['evidence1', 'evidence2', 'evidence3'];
|
||||
|
||||
// Create a capacity for bilattice analysis
|
||||
const qmt = new Map();
|
||||
qmt.set(getSetKey(new Set(['evidence1'])), 0.75); // High belief evidence
|
||||
qmt.set(getSetKey(new Set(['evidence2'])), 0.5); // Medium belief evidence
|
||||
qmt.set(getSetKey(new Set(['evidence3'])), 0.25); // Low belief evidence
|
||||
qmt.set(getSetKey(new Set(['evidence1', 'evidence2'])), 1); // Combined evidence
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
// Simulate collected values from rule evaluation - using valid fivePoint scale values
|
||||
const collectedValues = [
|
||||
{
|
||||
value: 0.75, // Valid fivePoint scale value
|
||||
possibility: 0.75, // Valid fivePoint scale value
|
||||
path: ['user', 'relation1'],
|
||||
source: { entityKey: 'user1', relation: 'score', step: 0 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.9 }
|
||||
},
|
||||
{
|
||||
value: 0.5, // Valid fivePoint scale value
|
||||
possibility: 0.5, // Valid fivePoint scale value
|
||||
path: ['user', 'relation2'],
|
||||
source: { entityKey: 'user2', relation: 'rating', step: 1 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.8 }
|
||||
},
|
||||
{
|
||||
value: 0.25, // Valid fivePoint scale value
|
||||
possibility: 0.25, // Valid fivePoint scale value
|
||||
path: ['user', 'relation3'],
|
||||
source: { entityKey: 'user3', relation: 'feedback', step: 2 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.7 }
|
||||
}
|
||||
];
|
||||
|
||||
// Test information-based evidence selection
|
||||
const epistemicPairs = collectedValues.map(cv => ({
|
||||
belief: cv.possibility,
|
||||
disbelief: 1 - cv.possibility
|
||||
}));
|
||||
|
||||
// Create propositions for bilattice analysis
|
||||
const propositions = collectedValues.map((cv, index) => [`evidence${index + 1}`]);
|
||||
|
||||
const mostInformative = BilatticeOrderings.findMostInformative(propositions, capacity);
|
||||
|
||||
// The most informative evidence should be the one with highest belief AND disbelief
|
||||
// Evidence 1: belief=0.75, disbelief=0.25
|
||||
// Evidence 2: belief=0.5, disbelief=0.5
|
||||
// Evidence 3: belief=0.25, disbelief=0.75
|
||||
// Evidence 1 and 2 are incomparable in information ordering (neither dominates)
|
||||
// The algorithm picks the first one (evidence 1) as default
|
||||
assert.strictEqual(mostInformative.epistemic.belief, 0.75);
|
||||
// Disbelief = capacity of the complement: max focal subset of
|
||||
// {evidence2, evidence3} is {evidence2}=0.5.
|
||||
assert.strictEqual(mostInformative.epistemic.disbelief, 0.5);
|
||||
assert.strictEqual(mostInformative.rank, 1);
|
||||
});
|
||||
|
||||
test('should demonstrate qualitative relational comparator with bilattice reasoning', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
|
||||
// Simulate blurred values from qualitative decay
|
||||
const blurredValues = [
|
||||
{
|
||||
interval: { lower: 0.5, upper: 0.75 },
|
||||
possibility: 0.75,
|
||||
originalValue: 0.75,
|
||||
originalPossibility: 1.0,
|
||||
timestamp: Date.now(),
|
||||
relation: 'score',
|
||||
meta: { periodsElapsed: 1, blurSteps: 1 }
|
||||
},
|
||||
{
|
||||
interval: { lower: 0.25, upper: 0.5 },
|
||||
possibility: 0.5,
|
||||
originalValue: 0.5,
|
||||
originalPossibility: 0.8,
|
||||
timestamp: Date.now(),
|
||||
relation: 'rating',
|
||||
meta: { periodsElapsed: 2, blurSteps: 2 }
|
||||
}
|
||||
];
|
||||
|
||||
// Test epistemic comparison of blurred values
|
||||
const epistemicPairs = blurredValues.map(bv => ({
|
||||
belief: bv.possibility,
|
||||
disbelief: 1 - bv.possibility
|
||||
}));
|
||||
|
||||
// Create a simple capacity for comparison
|
||||
const stateSpace = ['blurred1', 'blurred2'];
|
||||
const qmt = new Map();
|
||||
qmt.set(getSetKey(new Set(['blurred1'])), 0.75);
|
||||
qmt.set(getSetKey(new Set(['blurred2'])), 0.5);
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
const comparison = BilatticeOrderings.compareEpistemicStatus(
|
||||
['blurred1'], ['blurred2'], capacity
|
||||
);
|
||||
|
||||
// blurred1 should be more true than blurred2 (higher capacity value)
|
||||
assert.ok(comparison.truthOrdering);
|
||||
assert.strictEqual(comparison.relationship, 'A more true than B');
|
||||
});
|
||||
|
||||
test('should demonstrate defeasible logic with bilattice reasoning', () => {
|
||||
const scale = QualitativeScale.tenPoint();
|
||||
|
||||
// Simulate evidence from different rule types in defeasible logic
|
||||
const strictEvidence = {
|
||||
value: 1.0,
|
||||
possibility: 1.0,
|
||||
path: ['strict_rule'],
|
||||
source: { entityKey: 'system', relation: 'strict_check', step: 0 },
|
||||
metadata: { timestamp: Date.now(), reliability: 1.0, ruleType: 'strict' }
|
||||
};
|
||||
|
||||
const defeasibleEvidence = {
|
||||
value: 0.8,
|
||||
possibility: 0.8,
|
||||
path: ['defeasible_rule'],
|
||||
source: { entityKey: 'user', relation: 'user_check', step: 1 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.9, ruleType: 'defeasible' }
|
||||
};
|
||||
|
||||
const defeaterEvidence = {
|
||||
value: 0.6,
|
||||
possibility: 0.6,
|
||||
path: ['defeater_rule'],
|
||||
source: { entityKey: 'security', relation: 'security_check', step: 2 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.8, ruleType: 'defeater' }
|
||||
};
|
||||
|
||||
const allEvidence = [strictEvidence, defeasibleEvidence, defeaterEvidence];
|
||||
|
||||
// Create capacity representing the defeasible logic structure
|
||||
const stateSpace = ['strict', 'defeasible', 'defeater'];
|
||||
const qmt = new Map();
|
||||
qmt.set(getSetKey(new Set(['strict'])), 1.0); // Strict rules have highest priority
|
||||
qmt.set(getSetKey(new Set(['defeasible'])), 0.8); // Defeasible rules have medium priority
|
||||
qmt.set(getSetKey(new Set(['defeater'])), 0.6); // Defeaters have lower priority
|
||||
qmt.set(getSetKey(new Set(['strict', 'defeasible'])), 1.0); // Strict + defeasible = strict wins
|
||||
qmt.set(getSetKey(new Set(['strict', 'defeater'])), 1.0); // Strict + defeater = strict wins
|
||||
qmt.set(getSetKey(new Set(['defeasible', 'defeater'])), 0.8); // Defeasible + defeater = defeasible wins
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
// Test information ordering for defeasible logic
|
||||
const epistemicPairs = allEvidence.map(ev => ({
|
||||
belief: ev.possibility,
|
||||
disbelief: 1 - ev.possibility
|
||||
}));
|
||||
|
||||
// Create propositions for bilattice analysis (must match the capacity state space)
|
||||
const propositions = [['strict'], ['defeasible'], ['defeater']];
|
||||
|
||||
const informationRanking = BilatticeOrderings.rankByInformation(propositions, capacity);
|
||||
|
||||
// Strict evidence should rank highest in information ordering
|
||||
const strictRank = informationRanking.find(r => r.epistemic.belief === 1.0)?.rank;
|
||||
const defeasibleRank = informationRanking.find(r => r.epistemic.belief === 0.8)?.rank;
|
||||
const defeaterRank = informationRanking.find(r => r.epistemic.belief === 0.6)?.rank;
|
||||
|
||||
assert.ok(strictRank <= defeasibleRank);
|
||||
assert.ok(defeasibleRank <= defeaterRank);
|
||||
});
|
||||
|
||||
test('should demonstrate chain rule with epistemic path analysis', () => {
|
||||
const scale = QualitativeScale.tenPoint();
|
||||
|
||||
// Simulate collected values from a chain traversal
|
||||
const chainValues = [
|
||||
{
|
||||
value: 0.9,
|
||||
possibility: 0.9,
|
||||
path: ['user', 'account', 'balance'],
|
||||
source: { entityKey: 'account1', relation: 'balance', step: 2 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.95, pathPossibility: 0.9 }
|
||||
},
|
||||
{
|
||||
value: 0.7,
|
||||
possibility: 0.7,
|
||||
path: ['user', 'account', 'credit'],
|
||||
source: { entityKey: 'account2', relation: 'credit', step: 2 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.85, pathPossibility: 0.7 }
|
||||
},
|
||||
{
|
||||
value: 0.5,
|
||||
possibility: 0.5,
|
||||
path: ['user', 'account', 'debt'],
|
||||
source: { entityKey: 'account3', relation: 'debt', step: 2 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.75, pathPossibility: 0.5 }
|
||||
}
|
||||
];
|
||||
|
||||
// Create capacity for path-based epistemic analysis
|
||||
const stateSpace = ['path1', 'path2', 'path3'];
|
||||
const qmt = new Map();
|
||||
qmt.set(getSetKey(new Set(['path1'])), 0.9); // High confidence path
|
||||
qmt.set(getSetKey(new Set(['path2'])), 0.7); // Medium confidence path
|
||||
qmt.set(getSetKey(new Set(['path3'])), 0.5); // Low confidence path
|
||||
qmt.set(getSetKey(new Set(['path1', 'path2'])), 1.0); // Combined high-confidence paths
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
// Test truth ordering for path selection
|
||||
const pathPropositions = [
|
||||
['path1'], ['path2'], ['path3']
|
||||
];
|
||||
|
||||
const truthRanking = BilatticeOrderings.rankByTruth(pathPropositions, capacity);
|
||||
|
||||
// Path1 should rank highest in truth ordering (highest capacity value)
|
||||
const path1Rank = truthRanking.find(r => r.capacity === 0.9)?.rank;
|
||||
const path2Rank = truthRanking.find(r => r.capacity === 0.7)?.rank;
|
||||
const path3Rank = truthRanking.find(r => r.capacity === 0.5)?.rank;
|
||||
|
||||
assert.strictEqual(path1Rank, 1);
|
||||
assert.ok(path2Rank > path1Rank);
|
||||
assert.ok(path3Rank > path2Rank);
|
||||
});
|
||||
|
||||
test('should demonstrate hybrid epistemic reasoning', () => {
|
||||
const scale = QualitativeScale.tenPoint();
|
||||
|
||||
// Simulate mixed evidence from different sources
|
||||
const mixedEvidence = [
|
||||
{
|
||||
value: 0.8,
|
||||
possibility: 0.8,
|
||||
path: ['direct_evidence'],
|
||||
source: { entityKey: 'direct', relation: 'observation', step: 0 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.9, sourceType: 'direct' }
|
||||
},
|
||||
{
|
||||
value: 0.6,
|
||||
possibility: 0.6,
|
||||
path: ['inferred_evidence'],
|
||||
source: { entityKey: 'inference', relation: 'deduction', step: 1 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.7, sourceType: 'inferred' }
|
||||
},
|
||||
{
|
||||
value: 0.4,
|
||||
possibility: 0.4,
|
||||
path: ['similarity_evidence'],
|
||||
source: { entityKey: 'similarity', relation: 'analogy', step: 2 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.6, sourceType: 'similarity' }
|
||||
}
|
||||
];
|
||||
|
||||
// Create capacity for hybrid analysis
|
||||
const stateSpace = ['direct', 'inferred', 'similarity'];
|
||||
const qmt = new Map();
|
||||
qmt.set(getSetKey(new Set(['direct'])), 0.8); // Direct evidence has highest reliability
|
||||
qmt.set(getSetKey(new Set(['inferred'])), 0.6); // Inferred evidence has medium reliability
|
||||
qmt.set(getSetKey(new Set(['similarity'])), 0.4); // Similarity evidence has lowest reliability
|
||||
qmt.set(getSetKey(new Set(['direct', 'inferred'])), 0.9); // Direct + inferred = high confidence
|
||||
qmt.set(getSetKey(new Set(['direct', 'similarity'])), 0.8); // Direct + similarity = direct dominates
|
||||
qmt.set(getSetKey(new Set(['inferred', 'similarity'])), 0.6); // Inferred + similarity = inferred dominates
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
// Test comprehensive epistemic comparison
|
||||
const epistemicPairs = mixedEvidence.map(ev => ({
|
||||
belief: ev.possibility,
|
||||
disbelief: 1 - ev.possibility
|
||||
}));
|
||||
|
||||
// Create propositions for bilattice analysis (must match the capacity state space)
|
||||
const propositions = [['direct'], ['inferred'], ['similarity']];
|
||||
|
||||
const informationRanking = BilatticeOrderings.rankByInformation(propositions, capacity);
|
||||
const truthRanking = BilatticeOrderings.rankByTruth(propositions, capacity);
|
||||
|
||||
// Direct evidence should rank highest in both orderings
|
||||
const directInfoRank = informationRanking.find(r => r.epistemic.belief === 0.8)?.rank;
|
||||
const directTruthRank = truthRanking.find(r => r.capacity === 0.8)?.rank;
|
||||
|
||||
assert.strictEqual(directInfoRank, 1);
|
||||
assert.strictEqual(directTruthRank, 1);
|
||||
|
||||
// Test comprehensive comparison
|
||||
const comparison = BilatticeOrderings.compareEpistemicStatus(
|
||||
['direct'], ['inferred'], capacity
|
||||
);
|
||||
|
||||
assert.ok(comparison.truthOrdering);
|
||||
assert.strictEqual(comparison.relationship, 'A more true than B');
|
||||
});
|
||||
|
||||
test('should maintain backward compatibility with existing possibilistic infrastructure', () => {
|
||||
const scale = QualitativeScale.tenPoint();
|
||||
|
||||
// Test that bilattice reasoning can be disabled and standard OWA fusion still works
|
||||
const standardEvidence = [
|
||||
{ value: 0.8, possibility: 0.8, path: ['evidence1'], source: {}, metadata: {} },
|
||||
{ value: 0.6, possibility: 0.6, path: ['evidence2'], source: {}, metadata: {} },
|
||||
{ value: 0.4, possibility: 0.4, path: ['evidence3'], source: {}, metadata: {} }
|
||||
];
|
||||
|
||||
// Test standard OWA fusion (bilattice disabled)
|
||||
const epistemicPairs = standardEvidence.map(ev => ({
|
||||
belief: ev.possibility,
|
||||
disbelief: 1 - ev.possibility
|
||||
}));
|
||||
|
||||
// Standard max operation should select the highest value
|
||||
const maxValue = Math.max(...standardEvidence.map(ev => ev.possibility));
|
||||
assert.strictEqual(maxValue, 0.8);
|
||||
|
||||
// Test that bilattice reasoning can be enabled when needed
|
||||
const stateSpace = ['ev1', 'ev2', 'ev3'];
|
||||
const qmt = new Map();
|
||||
qmt.set(getSetKey(new Set(['ev1'])), 0.8);
|
||||
qmt.set(getSetKey(new Set(['ev2'])), 0.6);
|
||||
qmt.set(getSetKey(new Set(['ev3'])), 0.4);
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
// Create propositions for bilattice analysis
|
||||
const propositions = standardEvidence.map((ev, index) => [`ev${index + 1}`]);
|
||||
|
||||
const mostInformative = BilatticeOrderings.findMostInformative(propositions, capacity);
|
||||
|
||||
// Epistemic pairs from the capacity:
|
||||
// ev1 ({ev1}): belief=0.8, disbelief=gamma({ev2,ev3})=0.6
|
||||
// ev2 ({ev2}): belief=0.6, disbelief=gamma({ev1,ev3})=0.8
|
||||
// ev3 ({ev3}): belief=0.4, disbelief=gamma({ev1,ev2})=0.8
|
||||
// ev1 dominates in belief; the algorithm keeps the first best (ev1).
|
||||
assert.strictEqual(mostInformative.epistemic.belief, 0.8);
|
||||
assert.strictEqual(mostInformative.epistemic.disbelief, 0.6);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Test Bilattice Orderings for Evidential Reasoning
|
||||
*
|
||||
* This test demonstrates the bilattice orderings (≥ᵢ, ≥ₜ) for comparing
|
||||
* epistemic status of propositions in qualitative capacity systems.
|
||||
*/
|
||||
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import {
|
||||
QualitativeScale,
|
||||
QualitativeCapacity,
|
||||
BilatticeOrderings
|
||||
} from '../../src/qualitative/index.js';
|
||||
|
||||
describe('Bilattice Orderings', () => {
|
||||
test('should implement information ordering correctly', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
|
||||
// Test information ordering: (c₁, c₁') ≥ᵢ (c₂, c₂') ⟺ c₁ ≥ c₂ and c₁' ≥ c₂'
|
||||
const epistemic1 = { belief: 0.75, disbelief: 0.5 };
|
||||
const epistemic2 = { belief: 0.5, disbelief: 0.25 };
|
||||
const epistemic3 = { belief: 0.75, disbelief: 0.25 };
|
||||
|
||||
// epistemic1 should be more informative than epistemic2
|
||||
assert.ok(BilatticeOrderings.informationOrdering(epistemic1, epistemic2, scale));
|
||||
|
||||
// epistemic1 should be more informative than epistemic3 (higher disbelief)
|
||||
assert.ok(BilatticeOrderings.informationOrdering(epistemic1, epistemic3, scale));
|
||||
|
||||
// epistemic3 should NOT be more informative than epistemic1 (lower disbelief)
|
||||
assert.ok(!BilatticeOrderings.informationOrdering(epistemic3, epistemic1, scale));
|
||||
});
|
||||
|
||||
test('should implement truth ordering correctly', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['s1', 's2', 's3'];
|
||||
|
||||
// Create a capacity where s1 has high belief, s2 has medium belief
|
||||
const qmt = new Map();
|
||||
qmt.set(new Set(['s1']), 0.75); // High belief in s1
|
||||
qmt.set(new Set(['s2']), 0.5); // Medium belief in s2
|
||||
qmt.set(new Set(['s1', 's2']), 1); // Full belief in s1 OR s2
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
const propositionA = ['s1']; // High belief
|
||||
const propositionB = ['s2']; // Medium belief
|
||||
|
||||
// A should be more true than B: γ(A) ≥ γ(B) and γ(Bᶜ) ≥ γ(Aᶜ)
|
||||
assert.ok(BilatticeOrderings.truthOrdering(propositionA, propositionB, capacity));
|
||||
|
||||
// B should NOT be more true than A
|
||||
assert.ok(!BilatticeOrderings.truthOrdering(propositionB, propositionA, capacity));
|
||||
});
|
||||
|
||||
test('should compare epistemic status comprehensively', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['s1', 's2', 's3'];
|
||||
|
||||
// Create a capacity with different belief levels
|
||||
const qmt = new Map();
|
||||
qmt.set(new Set(['s1']), 0.75);
|
||||
qmt.set(new Set(['s2']), 0.5);
|
||||
qmt.set(new Set(['s3']), 0.25);
|
||||
qmt.set(new Set(['s1', 's2']), 1);
|
||||
qmt.set(new Set(['s1', 's2', 's3']), 1);
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
const propositionA = ['s1'];
|
||||
const propositionB = ['s2'];
|
||||
|
||||
const comparison = BilatticeOrderings.compareEpistemicStatus(propositionA, propositionB, capacity);
|
||||
|
||||
// Check that we get a comprehensive comparison
|
||||
assert.ok(comparison.propositionA);
|
||||
assert.ok(comparison.propositionB);
|
||||
assert.ok(typeof comparison.informationOrdering === 'boolean');
|
||||
assert.ok(typeof comparison.truthOrdering === 'boolean');
|
||||
assert.ok(typeof comparison.relationship === 'string');
|
||||
assert.ok(typeof comparison.analysis === 'string');
|
||||
|
||||
// A should be more true than B, but not more informative
|
||||
assert.ok(!comparison.informationOrdering);
|
||||
assert.ok(comparison.truthOrdering);
|
||||
assert.strictEqual(comparison.relationship, 'A more true than B');
|
||||
});
|
||||
|
||||
test('should find most informative proposition', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['s1', 's2', 's3'];
|
||||
|
||||
// Create a capacity with varying belief levels
|
||||
const qmt = new Map();
|
||||
qmt.set(new Set(['s1']), 0.75);
|
||||
qmt.set(new Set(['s2']), 0.5);
|
||||
qmt.set(new Set(['s3']), 0.25);
|
||||
qmt.set(new Set(['s1', 's2']), 1);
|
||||
qmt.set(new Set(['s1', 's2', 's3']), 1);
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
const propositions = [['s1'], ['s2'], ['s3']];
|
||||
|
||||
const mostInformative = BilatticeOrderings.findMostInformative(propositions, capacity);
|
||||
|
||||
// s1 should be most informative (highest belief and disbelief)
|
||||
assert.deepStrictEqual(mostInformative.proposition, ['s1']);
|
||||
assert.strictEqual(mostInformative.rank, 1);
|
||||
assert.strictEqual(mostInformative.total, 3);
|
||||
assert.ok(mostInformative.analysis.includes('Most informative'));
|
||||
});
|
||||
|
||||
test('should find most true proposition', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['s1', 's2', 's3'];
|
||||
|
||||
// Create a capacity with varying belief levels
|
||||
const qmt = new Map();
|
||||
qmt.set(new Set(['s1']), 0.75);
|
||||
qmt.set(new Set(['s2']), 0.5);
|
||||
qmt.set(new Set(['s3']), 0.25);
|
||||
qmt.set(new Set(['s1', 's2']), 1);
|
||||
qmt.set(new Set(['s1', 's2', 's3']), 1);
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
const propositions = [['s1'], ['s2'], ['s3']];
|
||||
|
||||
const mostTrue = BilatticeOrderings.findMostTrue(propositions, capacity);
|
||||
|
||||
// s1 should be most true (highest capacity value)
|
||||
assert.deepStrictEqual(mostTrue.proposition, ['s1']);
|
||||
assert.strictEqual(mostTrue.capacity, 0.75);
|
||||
assert.strictEqual(mostTrue.rank, 1);
|
||||
assert.strictEqual(mostTrue.total, 3);
|
||||
assert.ok(mostTrue.analysis.includes('Most true'));
|
||||
});
|
||||
|
||||
test('should rank propositions by information content', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['s1', 's2', 's3'];
|
||||
|
||||
// Create a capacity with varying belief levels
|
||||
const qmt = new Map();
|
||||
qmt.set(new Set(['s1']), 0.75);
|
||||
qmt.set(new Set(['s2']), 0.5);
|
||||
qmt.set(new Set(['s3']), 0.25);
|
||||
qmt.set(new Set(['s1', 's2']), 1);
|
||||
qmt.set(new Set(['s1', 's2', 's3']), 1);
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
const propositions = [['s1'], ['s2'], ['s3']];
|
||||
|
||||
const ranking = BilatticeOrderings.rankByInformation(propositions, capacity);
|
||||
|
||||
// Should have 3 ranked propositions
|
||||
assert.strictEqual(ranking.length, 3);
|
||||
|
||||
// Check ranking structure
|
||||
for (let i = 0; i < ranking.length; i++) {
|
||||
assert.strictEqual(ranking[i].rank, i + 1);
|
||||
assert.ok(ranking[i].proposition);
|
||||
assert.ok(ranking[i].epistemic);
|
||||
assert.ok(ranking[i].analysis);
|
||||
}
|
||||
|
||||
// s1 should be ranked first (most informative)
|
||||
assert.deepStrictEqual(ranking[0].proposition, ['s1']);
|
||||
});
|
||||
|
||||
test('should rank propositions by truth content', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['s1', 's2', 's3'];
|
||||
|
||||
// Create a capacity with varying belief levels
|
||||
const qmt = new Map();
|
||||
qmt.set(new Set(['s1']), 0.75);
|
||||
qmt.set(new Set(['s2']), 0.5);
|
||||
qmt.set(new Set(['s3']), 0.25);
|
||||
qmt.set(new Set(['s1', 's2']), 1);
|
||||
qmt.set(new Set(['s1', 's2', 's3']), 1);
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
const propositions = [['s1'], ['s2'], ['s3']];
|
||||
|
||||
const ranking = BilatticeOrderings.rankByTruth(propositions, capacity);
|
||||
|
||||
// Should have 3 ranked propositions
|
||||
assert.strictEqual(ranking.length, 3);
|
||||
|
||||
// Check ranking structure
|
||||
for (let i = 0; i < ranking.length; i++) {
|
||||
assert.strictEqual(ranking[i].rank, i + 1);
|
||||
assert.ok(ranking[i].proposition);
|
||||
assert.ok(typeof ranking[i].capacity === 'number');
|
||||
assert.ok(ranking[i].analysis);
|
||||
}
|
||||
|
||||
// s1 should be ranked first (most true)
|
||||
assert.deepStrictEqual(ranking[0].proposition, ['s1']);
|
||||
assert.strictEqual(ranking[0].capacity, 0.75);
|
||||
});
|
||||
|
||||
test('should handle edge cases correctly', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['s1', 's2'];
|
||||
|
||||
// Create a simple capacity
|
||||
const qmt = new Map();
|
||||
qmt.set(new Set(['s1']), 0.5);
|
||||
qmt.set(new Set(['s1', 's2']), 1);
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
// Test with single proposition
|
||||
const singleProposition = [['s1']];
|
||||
|
||||
const mostInformative = BilatticeOrderings.findMostInformative(singleProposition, capacity);
|
||||
const mostTrue = BilatticeOrderings.findMostTrue(singleProposition, capacity);
|
||||
|
||||
assert.deepStrictEqual(mostInformative.proposition, ['s1']);
|
||||
assert.strictEqual(mostInformative.rank, 1);
|
||||
assert.strictEqual(mostInformative.total, 1);
|
||||
|
||||
assert.deepStrictEqual(mostTrue.proposition, ['s1']);
|
||||
assert.strictEqual(mostTrue.rank, 1);
|
||||
assert.strictEqual(mostTrue.total, 1);
|
||||
});
|
||||
|
||||
test('should handle incomparable propositions', () => {
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
const stateSpace = ['s1', 's2', 's3'];
|
||||
|
||||
// Create a capacity where propositions are incomparable
|
||||
const qmt = new Map();
|
||||
qmt.set(new Set(['s1']), 0.75); // High belief, low disbelief
|
||||
qmt.set(new Set(['s2']), 0.5); // Medium belief, medium disbelief
|
||||
qmt.set(new Set(['s1', 's2']), 1);
|
||||
qmt.set(new Set(['s1', 's2', 's3']), 1);
|
||||
|
||||
const capacity = new QualitativeCapacity(stateSpace, scale, qmt);
|
||||
|
||||
const propositionA = ['s1'];
|
||||
const propositionB = ['s2'];
|
||||
|
||||
const comparison = BilatticeOrderings.compareEpistemicStatus(propositionA, propositionB, capacity);
|
||||
|
||||
// Should identify the relationship correctly
|
||||
assert.ok(typeof comparison.relationship === 'string');
|
||||
assert.ok(comparison.relationship !== '');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* Test Unified Evidence Fusion System
|
||||
*
|
||||
* This test demonstrates the new separation of concerns between aggregation
|
||||
* (OWA) and reconciliation (bilattice/Dempster-Shafer) logic, supporting
|
||||
* both qualitative and quantitative modes with the same lexicon.
|
||||
*/
|
||||
|
||||
import { test, describe } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import {
|
||||
QualitativeScale,
|
||||
UnifiedEvidenceFusion,
|
||||
EvidenceAggregation,
|
||||
EvidenceReconciliation,
|
||||
NumericBilatticeOrderings
|
||||
} from '../../src/qualitative/index.js';
|
||||
|
||||
describe('Unified Evidence Fusion System', () => {
|
||||
|
||||
test('should demonstrate aggregation vs reconciliation separation', () => {
|
||||
// Test data with conflicting evidence
|
||||
const collectedValues = [
|
||||
{
|
||||
value: 0.8,
|
||||
possibility: 0.9,
|
||||
path: ['evidence1'],
|
||||
source: { type: 'direct', confidence: 0.95 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.9 }
|
||||
},
|
||||
{
|
||||
value: 0.3,
|
||||
possibility: 0.4,
|
||||
path: ['evidence2'],
|
||||
source: { type: 'inferred', confidence: 0.6 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.7 }
|
||||
},
|
||||
{
|
||||
value: 0.7,
|
||||
possibility: 0.6,
|
||||
path: ['evidence3'],
|
||||
source: { type: 'derived', confidence: 0.8 },
|
||||
metadata: { timestamp: Date.now(), reliability: 0.8 }
|
||||
}
|
||||
];
|
||||
|
||||
// Test pure aggregation (no reconciliation)
|
||||
const aggregationResult = UnifiedEvidenceFusion.fuse(collectedValues, {
|
||||
mode: 'quantitative',
|
||||
aggregationMethod: 'majority',
|
||||
useReconciliation: false
|
||||
});
|
||||
|
||||
assert.ok(aggregationResult.hasValue);
|
||||
assert.strictEqual(aggregationResult.fusionMethod, 'aggregation');
|
||||
assert.strictEqual(aggregationResult.reconciliationMethod, 'none');
|
||||
assert.ok(aggregationResult.epistemicAnalysis === null);
|
||||
|
||||
// Test reconciliation-based fusion
|
||||
const reconciliationResult = UnifiedEvidenceFusion.fuse(collectedValues, {
|
||||
mode: 'quantitative',
|
||||
aggregationMethod: 'max',
|
||||
reconciliationMethod: 'dempster_shafer',
|
||||
epistemicMode: 'hybrid',
|
||||
useReconciliation: true
|
||||
});
|
||||
|
||||
assert.ok(reconciliationResult.hasValue);
|
||||
assert.strictEqual(reconciliationResult.fusionMethod, 'reconciliation');
|
||||
assert.strictEqual(reconciliationResult.reconciliationMethod, 'dempster_shafer');
|
||||
assert.ok(reconciliationResult.epistemicAnalysis !== null);
|
||||
});
|
||||
|
||||
test('should support both qualitative and quantitative modes with same lexicon', () => {
|
||||
const collectedValues = [
|
||||
{
|
||||
value: 0.75, // Valid fivePoint scale value
|
||||
possibility: 0.75,
|
||||
path: ['evidence1'],
|
||||
source: { type: 'direct' },
|
||||
metadata: { reliability: 0.9 }
|
||||
},
|
||||
{
|
||||
value: 0.5, // Valid fivePoint scale value
|
||||
possibility: 0.5,
|
||||
path: ['evidence2'],
|
||||
source: { type: 'inferred' },
|
||||
metadata: { reliability: 0.7 }
|
||||
}
|
||||
];
|
||||
|
||||
const scale = QualitativeScale.fivePoint();
|
||||
|
||||
// Test quantitative mode
|
||||
const quantitativeResult = UnifiedEvidenceFusion.fuse(collectedValues, {
|
||||
mode: 'quantitative',
|
||||
aggregationMethod: 'average',
|
||||
useReconciliation: false
|
||||
});
|
||||
|
||||
// Test qualitative mode
|
||||
const qualitativeResult = UnifiedEvidenceFusion.fuse(collectedValues, {
|
||||
mode: 'qualitative',
|
||||
aggregationMethod: 'average',
|
||||
scale: scale,
|
||||
useReconciliation: false
|
||||
});
|
||||
|
||||
assert.ok(quantitativeResult.hasValue);
|
||||
assert.ok(qualitativeResult.hasValue);
|
||||
assert.strictEqual(quantitativeResult.aggregationMethod, 'average');
|
||||
assert.strictEqual(qualitativeResult.aggregationMethod, 'average');
|
||||
|
||||
// Both should use the same aggregation lexicon
|
||||
assert.ok(quantitativeResult.value > 0);
|
||||
assert.ok(qualitativeResult.value > 0);
|
||||
});
|
||||
|
||||
test('should demonstrate numeric bilattice orderings', () => {
|
||||
// Create a simple numeric capacity
|
||||
const capacity = {
|
||||
stateSpace: ['evidence1', 'evidence2', 'evidence3'],
|
||||
getCapacity: (set) => {
|
||||
// For single elements
|
||||
if (set.size === 1) {
|
||||
if (set.has('evidence1')) return 0.8;
|
||||
if (set.has('evidence2')) return 0.6;
|
||||
if (set.has('evidence3')) return 0.4;
|
||||
}
|
||||
// For complements (multiple elements)
|
||||
if (set.size === 2) {
|
||||
return 0.2; // Some belief in complements
|
||||
}
|
||||
// For empty set
|
||||
if (set.size === 0) {
|
||||
return 0;
|
||||
}
|
||||
// For full set
|
||||
if (set.size === 3) {
|
||||
return 1.0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const propositions = [['evidence1'], ['evidence2'], ['evidence3']];
|
||||
|
||||
// Test information ordering
|
||||
const mostInformative = NumericBilatticeOrderings.findMostInformative(propositions, capacity);
|
||||
assert.ok(mostInformative);
|
||||
assert.strictEqual(mostInformative.rank, 1);
|
||||
|
||||
// Test truth ordering
|
||||
const mostTrue = NumericBilatticeOrderings.findMostTrue(propositions, capacity);
|
||||
assert.ok(mostTrue);
|
||||
assert.strictEqual(mostTrue.rank, 1);
|
||||
|
||||
// Test Dempster-Shafer measures
|
||||
const belief = NumericBilatticeOrderings.dempsterShaferBelief(['evidence1'], capacity);
|
||||
const plausibility = NumericBilatticeOrderings.dempsterShaferPlausibility(['evidence1'], capacity);
|
||||
const uncertainty = NumericBilatticeOrderings.dempsterShaferUncertainty(['evidence1'], capacity);
|
||||
|
||||
assert.strictEqual(belief, 0.8);
|
||||
// Plausibility should be 1 - capacity of complement
|
||||
// Complement of ['evidence1'] is ['evidence2', 'evidence3']
|
||||
// Capacity of ['evidence2', 'evidence3'] is 0.2
|
||||
// So plausibility = 1 - 0.2 = 0.8
|
||||
assert.strictEqual(plausibility, 0.8);
|
||||
assert.strictEqual(uncertainty, 0); // 0.8 - 0.8 = 0
|
||||
});
|
||||
|
||||
test('should demonstrate reconciliation methods comparison', () => {
|
||||
const collectedValues = [
|
||||
{
|
||||
value: 0.8,
|
||||
possibility: 0.9,
|
||||
path: ['evidence1'],
|
||||
source: { type: 'direct' },
|
||||
metadata: { reliability: 0.9 }
|
||||
},
|
||||
{
|
||||
value: 0.3,
|
||||
possibility: 0.4,
|
||||
path: ['evidence2'],
|
||||
source: { type: 'inferred' },
|
||||
metadata: { reliability: 0.7 }
|
||||
}
|
||||
];
|
||||
|
||||
// Test different reconciliation methods
|
||||
const bilatticeResult = UnifiedEvidenceFusion.fuse(collectedValues, {
|
||||
mode: 'quantitative',
|
||||
reconciliationMethod: 'bilattice',
|
||||
epistemicMode: 'hybrid',
|
||||
useReconciliation: true
|
||||
});
|
||||
|
||||
const dempsterShaferResult = UnifiedEvidenceFusion.fuse(collectedValues, {
|
||||
mode: 'quantitative',
|
||||
reconciliationMethod: 'dempster_shafer',
|
||||
epistemicMode: 'hybrid',
|
||||
useReconciliation: true
|
||||
});
|
||||
|
||||
const subjectiveLogicResult = UnifiedEvidenceFusion.fuse(collectedValues, {
|
||||
mode: 'quantitative',
|
||||
reconciliationMethod: 'subjective_logic',
|
||||
epistemicMode: 'hybrid',
|
||||
useReconciliation: true
|
||||
});
|
||||
|
||||
// All should produce valid results
|
||||
assert.ok(bilatticeResult.hasValue);
|
||||
assert.ok(dempsterShaferResult.hasValue);
|
||||
assert.ok(subjectiveLogicResult.hasValue);
|
||||
|
||||
// All should have epistemic analysis
|
||||
assert.ok(bilatticeResult.epistemicAnalysis);
|
||||
assert.ok(dempsterShaferResult.epistemicAnalysis);
|
||||
assert.ok(subjectiveLogicResult.epistemicAnalysis);
|
||||
|
||||
// Different methods may produce different results
|
||||
console.log('Bilattice result:', bilatticeResult.value);
|
||||
console.log('Dempster-Shafer result:', dempsterShaferResult.value);
|
||||
console.log('Subjective Logic result:', subjectiveLogicResult.value);
|
||||
});
|
||||
|
||||
test('should demonstrate aggregation methods comparison', () => {
|
||||
const collectedValues = [
|
||||
{
|
||||
value: 0.8,
|
||||
possibility: 0.8,
|
||||
path: ['evidence1'],
|
||||
source: { type: 'direct' },
|
||||
metadata: { reliability: 0.9 }
|
||||
},
|
||||
{
|
||||
value: 0.6,
|
||||
possibility: 0.6,
|
||||
path: ['evidence2'],
|
||||
source: { type: 'inferred' },
|
||||
metadata: { reliability: 0.7 }
|
||||
},
|
||||
{
|
||||
value: 0.4,
|
||||
possibility: 0.4,
|
||||
path: ['evidence3'],
|
||||
source: { type: 'derived' },
|
||||
metadata: { reliability: 0.8 }
|
||||
}
|
||||
];
|
||||
|
||||
const aggregationMethods = ['max', 'min', 'average', 'majority', 'median'];
|
||||
|
||||
const results = {};
|
||||
for (const method of aggregationMethods) {
|
||||
results[method] = UnifiedEvidenceFusion.fuse(collectedValues, {
|
||||
mode: 'quantitative',
|
||||
aggregationMethod: method,
|
||||
useReconciliation: false
|
||||
});
|
||||
}
|
||||
|
||||
// All methods should produce valid results
|
||||
for (const [method, result] of Object.entries(results)) {
|
||||
assert.ok(result.hasValue, `Method ${method} should produce valid result`);
|
||||
assert.strictEqual(result.aggregationMethod, method);
|
||||
assert.ok(result.value >= 0 && result.value <= 1);
|
||||
}
|
||||
|
||||
// Different methods should produce different results
|
||||
assert.ok(results.max.value >= results.average.value);
|
||||
assert.ok(results.average.value >= results.min.value);
|
||||
});
|
||||
|
||||
test('should demonstrate method comparison functionality', () => {
|
||||
const collectedValues = [
|
||||
{
|
||||
value: 0.7,
|
||||
possibility: 0.7,
|
||||
path: ['evidence1'],
|
||||
source: { type: 'direct' },
|
||||
metadata: { reliability: 0.9 }
|
||||
},
|
||||
{
|
||||
value: 0.5,
|
||||
possibility: 0.5,
|
||||
path: ['evidence2'],
|
||||
source: { type: 'inferred' },
|
||||
metadata: { reliability: 0.7 }
|
||||
}
|
||||
];
|
||||
|
||||
const comparison = UnifiedEvidenceFusion.compareMethods(collectedValues, {
|
||||
mode: 'quantitative',
|
||||
aggregationMethods: ['max', 'average', 'majority'],
|
||||
reconciliationMethods: ['none', 'dempster_shafer'],
|
||||
epistemicModes: ['hybrid']
|
||||
});
|
||||
|
||||
assert.ok(comparison.results);
|
||||
assert.ok(comparison.summary);
|
||||
assert.ok(comparison.summary.bestMethods.length > 0);
|
||||
assert.ok(comparison.summary.worstMethods.length > 0);
|
||||
assert.ok(comparison.summary.valueRange.min <= comparison.summary.valueRange.max);
|
||||
});
|
||||
|
||||
test('should validate fusion options', () => {
|
||||
const validation = UnifiedEvidenceFusion.validateOptions({
|
||||
mode: 'quantitative',
|
||||
aggregationMethod: 'max',
|
||||
reconciliationMethod: 'bilattice',
|
||||
epistemicMode: 'hybrid',
|
||||
capacityType: 'simple_support'
|
||||
});
|
||||
|
||||
assert.ok(validation.valid);
|
||||
assert.strictEqual(validation.errors.length, 0);
|
||||
|
||||
// Test invalid options
|
||||
const invalidValidation = UnifiedEvidenceFusion.validateOptions({
|
||||
mode: 'invalid',
|
||||
aggregationMethod: 'invalid',
|
||||
reconciliationMethod: 'invalid'
|
||||
});
|
||||
|
||||
assert.ok(!invalidValidation.valid);
|
||||
assert.ok(invalidValidation.errors.length > 0);
|
||||
});
|
||||
|
||||
test('should get available methods and descriptions', () => {
|
||||
const availableMethods = UnifiedEvidenceFusion.getAvailableMethods();
|
||||
assert.ok(availableMethods.aggregation);
|
||||
assert.ok(availableMethods.reconciliation);
|
||||
assert.ok(availableMethods.epistemicModes);
|
||||
assert.ok(availableMethods.capacityTypes);
|
||||
|
||||
const descriptions = UnifiedEvidenceFusion.getMethodDescriptions();
|
||||
assert.ok(descriptions.aggregation);
|
||||
assert.ok(descriptions.reconciliation);
|
||||
assert.ok(descriptions.epistemicModes);
|
||||
assert.ok(descriptions.capacityTypes);
|
||||
|
||||
// Test specific descriptions
|
||||
assert.ok(descriptions.aggregation.max);
|
||||
assert.ok(descriptions.reconciliation.bilattice);
|
||||
assert.ok(descriptions.epistemicModes.hybrid);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user