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:
John Dvorak
2026-07-31 13:44:06 -07:00
commit 717ae1031e
373 changed files with 654131 additions and 0 deletions
@@ -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);
});
});