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,467 @@
import { test, describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { Arbiter } from '../../../src/index.js';
import { setupChainRuleTestGraph } from './helpers.js';
describe('ChainRule Comprehensive Tests', () => {
describe('Debugging Aggregation Issues (RelationalComparatorRule Integration)', () => {
it('debugs the $9000 vs $4500 aggregation problem', () => {
const arbiter = new Arbiter({ embeddingDimensions: 256 });
// Replicate the exact scenario from RelationalComparatorRule test
arbiter.setRelationConfig('can_debit', { type: 'direct' });
arbiter.setRelationConfig('has_balance', { type: 'direct' });
// Create nodes
arbiter.addNode('user:test', 'user');
arbiter.addNode('account:business', 'account');
arbiter.addNode('account:personal', 'account');
arbiter.addNode('account:restricted', 'account');
arbiter.addNode('unit:usd', 'currency');
// Set up account balances
arbiter.addRelation('account:business', 'has_balance', 'unit:usd', { value: 3000 });
arbiter.addRelation('account:personal', 'has_balance', 'unit:usd', { value: 1500 });
arbiter.addRelation('account:restricted', 'has_balance', 'unit:usd', { value: 10000 });
// Set up debit rights (only to business and personal)
arbiter.addRelation('user:test', 'can_debit', 'account:business');
arbiter.addRelation('user:test', 'can_debit', 'account:personal');
// Configure the chain rule that's causing problems
arbiter.setRelationConfig('balance_chain', {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' }, // user → accounts
{ relation: 'has_balance', direction: 'out' } // accounts → currency
],
extractValues: true,
extractFrom: 1, // Extract from step 1 (accounts)
extractRelation: 'has_balance',
valueAggregation: 'sum', // Sum all authorized balances
evaluateFrom: 'user'
});
// Test the chain rule directly
const result = arbiter.check('user:test', 'balance_chain', 'unit:usd');
if (process.env.TEST_DEBUG === '1') console.log('\n=== CHAIN RULE AGGREGATION DEBUG ===');
if (process.env.TEST_DEBUG === '1') console.log('Expected path: user:test → [account:business, account:personal] → unit:usd');
if (process.env.TEST_DEBUG === '1') console.log('Expected balance sum: $3000 + $1500 = $4500');
if (process.env.TEST_DEBUG === '1') console.log('Actual result:', JSON.stringify(result, null, 2));
// Check if the chain rule reaches the target
assert.equal(result.possibility, 1, 'Chain should successfully reach unit:usd');
assert.equal(result.reason, 'allow_rule_matched');
// The key question: what values are being collected?
if (result.collectedValues) {
if (process.env.TEST_DEBUG === '1') console.log('\n=== COLLECTED VALUES ANALYSIS ===');
if (process.env.TEST_DEBUG === '1') console.log('Number of collected values:', result.collectedValues.length);
result.collectedValues.forEach((cv, i) => {
if (process.env.TEST_DEBUG === '1') console.log(`Value ${i + 1}:`, {
value: cv.value,
source: cv.source,
metadata: cv.metadata || cv.meta
});
});
}
// Check what the RelationManager thinks about this path
if (process.env.TEST_DEBUG === '1') console.log('\n=== RELATION MANAGER DEBUG ===');
const userNodeId = arbiter.nodeManager.getNodeId('user:test');
const debitRels = arbiter.relationManager.getRelationsFromSrc(userNodeId, 'can_debit');
if (process.env.TEST_DEBUG === '1') console.log('Debit relations:', debitRels.map(r => ({
target: arbiter.keyByNodeId.get(r.dst),
possibility: r.possibility
})));
debitRels.forEach(debitRel => {
const balanceRels = arbiter.relationManager.getRelationsFromSrc(debitRel.dst, 'has_balance');
if (process.env.TEST_DEBUG === '1') console.log(`Balance relations from ${arbiter.keyByNodeId.get(debitRel.dst)}:`,
balanceRels.map(r => ({
target: arbiter.keyByNodeId.get(r.dst),
value: r.value,
possibility: r.possibility
}))
);
});
// This test documents the issue for investigation
if (process.env.TEST_DEBUG === '1') console.log('\n=== ISSUE SUMMARY ===');
if (process.env.TEST_DEBUG === '1') console.log('If chain rule aggregation is working correctly, the total should be $4500');
if (process.env.TEST_DEBUG === '1') console.log('If we get $9000, there is likely double counting in the aggregation logic');
});
it('tests simpler two-account aggregation to isolate the issue', () => {
const arbiter = new Arbiter({ embeddingDimensions: 256 });
// Simpler test case
arbiter.setRelationConfig('owns', { type: 'direct' });
arbiter.setRelationConfig('worth', { type: 'direct' });
arbiter.addNode('user:simple', 'user');
arbiter.addNode('asset:a', 'asset');
arbiter.addNode('asset:b', 'asset');
arbiter.addNode('currency:usd', 'currency');
// User owns two assets
arbiter.addRelation('user:simple', 'owns', 'asset:a');
arbiter.addRelation('user:simple', 'owns', 'asset:b');
// Assets have known values
arbiter.addRelation('asset:a', 'worth', 'currency:usd', { value: 100 });
arbiter.addRelation('asset:b', 'worth', 'currency:usd', { value: 200 });
arbiter.setRelationConfig('total_worth_chain', {
type: 'chain',
steps: [
{ relation: 'owns', direction: 'out' },
{ relation: 'worth', direction: 'out' }
],
extractValues: true,
extractFrom: 1, // Extract from step 1 (assets)
extractRelation: 'worth',
valueAggregation: 'sum'
});
const result = arbiter.check('user:simple', 'total_worth_chain', 'currency:usd');
if (process.env.TEST_DEBUG === '1') console.log('\n=== SIMPLE AGGREGATION TEST ===');
if (process.env.TEST_DEBUG === '1') console.log('Expected: $100 + $200 = $300');
if (process.env.TEST_DEBUG === '1') console.log('Result:', JSON.stringify(result, null, 2));
assert.equal(result.possibility, 1);
// This should help us see if the issue is specific to the can_debit scenario
// or a general problem with chain rule aggregation
if (result.collectedValues) {
const totalValue = result.collectedValues.reduce((sum, cv) => {
const val = typeof cv.value === 'number' ? cv.value :
(cv.value && typeof cv.value.value === 'number' ? cv.value.value : 0);
return sum + val;
}, 0);
if (process.env.TEST_DEBUG === '1') console.log('Manually calculated total from collected values:', totalValue);
if (process.env.TEST_DEBUG === '1') console.log('Expected total: 300');
if (totalValue !== 300) {
if (process.env.TEST_DEBUG === '1') console.log('🚨 AGGREGATION ISSUE CONFIRMED: Chain rule is not summing correctly');
} else {
if (process.env.TEST_DEBUG === '1') console.log('✅ Simple aggregation works correctly');
}
}
});
it('tests chain rule value extraction vs RelationalComparatorRule value extraction', () => {
const arbiter = new Arbiter({ embeddingDimensions: 256 });
// Set up the same scenario but test chain rule in isolation
arbiter.setRelationConfig('can_debit', { type: 'direct' });
arbiter.setRelationConfig('has_balance', { type: 'direct' });
arbiter.addNode('user:compare', 'user');
arbiter.addNode('account:x', 'account');
arbiter.addNode('account:y', 'account');
arbiter.addNode('currency:usd', 'currency');
arbiter.addRelation('user:compare', 'can_debit', 'account:x');
arbiter.addRelation('user:compare', 'can_debit', 'account:y');
arbiter.addRelation('account:x', 'has_balance', 'currency:usd', { value: 1000 });
arbiter.addRelation('account:y', 'has_balance', 'currency:usd', { value: 2000 });
// Test 1: Direct ChainRule evaluation
arbiter.setRelationConfig('direct_chain', {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
],
extractValues: true,
extractFrom: 1,
extractRelation: 'has_balance',
valueAggregation: 'sum'
});
const chainResult = arbiter.check('user:compare', 'direct_chain', 'currency:usd');
// Test 2: RelationalComparatorRule using the same chain
arbiter.setRelationConfig('comparator_chain', {
type: 'relational_comparator',
leftOperand: {
rule: {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
],
extractValues: true,
extractFrom: 1,
extractRelation: 'has_balance',
valueAggregation: 'sum',
evaluateFrom: 'user'
},
extractValue: true,
evaluateFrom: 'user'
},
rightOperand: {
rule: {
type: 'direct',
relation: 'has_balance' // dummy
},
extractValue: false // Use possibility (1.0) as value
},
comparator: '>='
});
const comparatorResult = arbiter.check('user:compare', 'comparator_chain', 'currency:usd');
if (process.env.TEST_DEBUG === '1') console.log('\n=== CHAIN VS RELATIONAL_COMPARATOR COMPARISON ===');
if (process.env.TEST_DEBUG === '1') console.log('Expected total: $1000 + $2000 = $3000');
if (process.env.TEST_DEBUG === '1') console.log('\nDirect chain result:', {
possibility: chainResult.possibility,
collectedValues: chainResult.collectedValues?.length || 'none',
reason: chainResult.reason
});
if (process.env.TEST_DEBUG === '1') console.log('\nRelationalComparatorRule result:', {
possibility: comparatorResult.possibility,
leftValue: comparatorResult.meta?.allow?.leftValue || comparatorResult.meta?.deny?.leftValue,
rightValue: comparatorResult.meta?.allow?.rightValue || comparatorResult.meta?.deny?.rightValue,
reason: comparatorResult.reason
});
// Both should reach the target
assert.equal(chainResult.possibility, 1);
if (comparatorResult.meta?.allow?.leftValue) {
if (process.env.TEST_DEBUG === '1') console.log('\n🔍 Investigating value extraction:');
if (process.env.TEST_DEBUG === '1') console.log('RelationalComparatorRule extracted leftValue:', comparatorResult.meta.allow.leftValue);
if (process.env.TEST_DEBUG === '1') console.log('Expected: 3000');
if (comparatorResult.meta.allow.leftValue === 6000) {
if (process.env.TEST_DEBUG === '1') console.log('🚨 DOUBLE COUNTING CONFIRMED: 2x the expected value');
} else if (comparatorResult.meta.allow.leftValue === 3000) {
if (process.env.TEST_DEBUG === '1') console.log('✅ Correct aggregation');
} else {
if (process.env.TEST_DEBUG === '1') console.log('🤔 Unexpected value - needs investigation');
}
}
});
it('isolates collectedValues disappearing issue', () => {
const arbiter = new Arbiter({ embeddingDimensions: 256 });
// Minimal test case
arbiter.setRelationConfig('owns', { type: 'direct' });
arbiter.setRelationConfig('worth', { type: 'direct' });
arbiter.addNode('user:minimal', 'user');
arbiter.addNode('item:test', 'item');
arbiter.addNode('currency:usd', 'currency');
arbiter.addRelation('user:minimal', 'owns', 'item:test');
arbiter.addRelation('item:test', 'worth', 'currency:usd', { value: 100 });
arbiter.setRelationConfig('minimal_chain', {
type: 'chain',
steps: [
{ relation: 'owns', direction: 'out' },
{ relation: 'worth', direction: 'out' }
],
extractValues: true,
extractFrom: 1,
extractRelation: 'worth',
valueAggregation: 'sum'
});
const result = arbiter.check('user:minimal', 'minimal_chain', 'currency:usd');
if (process.env.TEST_DEBUG === '1') console.log('\n=== MINIMAL TEST: COLLECTED VALUES INVESTIGATION ===');
if (process.env.TEST_DEBUG === '1') console.log('Full result structure:', JSON.stringify(result, null, 2));
if (process.env.TEST_DEBUG === '1') console.log('Has collectedValues field?', 'collectedValues' in result);
if (process.env.TEST_DEBUG === '1') console.log('collectedValues type:', typeof result.collectedValues);
if (process.env.TEST_DEBUG === '1') console.log('collectedValues value:', result.collectedValues);
if (result.collectedValues && Array.isArray(result.collectedValues)) {
if (process.env.TEST_DEBUG === '1') console.log('✅ collectedValues preserved:', result.collectedValues.length, 'values');
result.collectedValues.forEach((cv, i) => {
if (process.env.TEST_DEBUG === '1') console.log(` Value ${i + 1}:`, {
value: cv.value,
possibility: cv.possibility,
source: cv.source?.relation
});
});
} else {
if (process.env.TEST_DEBUG === '1') console.log('🚨 collectedValues NOT preserved in final result');
if (process.env.TEST_DEBUG === '1') console.log('This explains why RelationalComparatorRule cannot extract values from ChainRule');
}
assert.equal(result.possibility, 1, 'Chain should reach target');
});
it('isolates why RelationalComparatorRule gets $9000 instead of $4500', () => {
const arbiter = new Arbiter({ embeddingDimensions: 256 });
// Set up the same scenario but test chain rule in isolation
arbiter.setRelationConfig('can_debit', { type: 'direct' });
arbiter.setRelationConfig('has_balance', { type: 'direct' });
arbiter.addNode('user:compare', 'user');
arbiter.addNode('account:x', 'account');
arbiter.addNode('account:y', 'account');
arbiter.addNode('currency:usd', 'currency');
arbiter.addRelation('user:compare', 'can_debit', 'account:x');
arbiter.addRelation('user:compare', 'can_debit', 'account:y');
arbiter.addRelation('account:x', 'has_balance', 'currency:usd', { value: 1000 });
arbiter.addRelation('account:y', 'has_balance', 'currency:usd', { value: 2000 });
// Test 1: Direct ChainRule evaluation
arbiter.setRelationConfig('direct_chain', {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
],
extractValues: true,
extractFrom: 1,
extractRelation: 'has_balance',
valueAggregation: 'sum'
});
const chainResult = arbiter.check('user:compare', 'direct_chain', 'currency:usd');
// Test 2: RelationalComparatorRule using the same chain
arbiter.setRelationConfig('comparator_chain', {
type: 'relational_comparator',
leftOperand: {
rule: {
type: 'chain',
steps: [
{ relation: 'can_debit', direction: 'out' },
{ relation: 'has_balance', direction: 'out' }
],
extractValues: true,
extractFrom: 1,
extractRelation: 'has_balance',
valueAggregation: 'sum',
evaluateFrom: 'user'
},
extractValue: true,
evaluateFrom: 'user'
},
rightOperand: {
rule: {
type: 'direct',
relation: 'has_balance' // dummy
},
extractValue: false // Use possibility (1.0) as value
},
comparator: '>='
});
const comparatorResult = arbiter.check('user:compare', 'comparator_chain', 'currency:usd');
if (process.env.TEST_DEBUG === '1') console.log('\n=== CHAIN VS RELATIONAL_COMPARATOR COMPARISON ===');
if (process.env.TEST_DEBUG === '1') console.log('Expected total: $1000 + $2000 = $3000');
if (process.env.TEST_DEBUG === '1') console.log('\nDirect chain result:', {
possibility: chainResult.possibility,
collectedValues: chainResult.collectedValues?.length || 'none',
reason: chainResult.reason
});
if (process.env.TEST_DEBUG === '1') console.log('\nRelationalComparatorRule result:', {
possibility: comparatorResult.possibility,
leftValue: comparatorResult.meta?.allow?.leftValue || comparatorResult.meta?.deny?.leftValue,
rightValue: comparatorResult.meta?.allow?.rightValue || comparatorResult.meta?.deny?.rightValue,
reason: comparatorResult.reason
});
// Both should reach the target
assert.equal(chainResult.possibility, 1);
if (comparatorResult.meta?.allow?.leftValue) {
if (process.env.TEST_DEBUG === '1') console.log('\n🔍 Investigating value extraction:');
if (process.env.TEST_DEBUG === '1') console.log('RelationalComparatorRule extracted leftValue:', comparatorResult.meta.allow.leftValue);
if (process.env.TEST_DEBUG === '1') console.log('Expected: 3000');
if (comparatorResult.meta.allow.leftValue === 6000) {
if (process.env.TEST_DEBUG === '1') console.log('🚨 DOUBLE COUNTING CONFIRMED: 2x the expected value');
} else if (comparatorResult.meta.allow.leftValue === 3000) {
if (process.env.TEST_DEBUG === '1') console.log('✅ Correct aggregation');
} else {
if (process.env.TEST_DEBUG === '1') console.log('🤔 Unexpected value - needs investigation');
}
}
});
it('regression test: ensures collectedValues are preserved after AuthorizationChecker fix', () => {
const arbiter = new Arbiter({ embeddingDimensions: 256 });
// Set up a simple value collection scenario
arbiter.setRelationConfig('owns', { type: 'direct' });
arbiter.setRelationConfig('worth', { type: 'direct' });
arbiter.addNode('user:collector', 'user');
arbiter.addNode('asset:a', 'asset');
arbiter.addNode('asset:b', 'asset');
arbiter.addNode('currency:usd', 'currency');
// Set up asset ownership and values
arbiter.addRelation('user:collector', 'owns', 'asset:a');
arbiter.addRelation('user:collector', 'owns', 'asset:b');
arbiter.addRelation('asset:a', 'worth', 'currency:usd', { value: 500 });
arbiter.addRelation('asset:b', 'worth', 'currency:usd', { value: 750 });
// Configure chain rule with value collection
arbiter.setRelationConfig('asset_values', {
type: 'chain',
steps: [
{ relation: 'owns', direction: 'out' },
{ relation: 'worth', direction: 'out' }
],
extractValues: true,
extractFrom: 1,
extractRelation: 'worth',
valueAggregation: 'sum'
});
const result = arbiter.check('user:collector', 'asset_values', 'currency:usd', { collectValues: true });
if (process.env.TEST_DEBUG === '1') console.log('\n=== REGRESSION TEST: COLLECTED VALUES PRESERVATION ===');
if (process.env.TEST_DEBUG === '1') console.log('Testing that collectedValues are preserved in final result...');
if (process.env.TEST_DEBUG === '1') console.log('Result structure:', {
possibility: result.possibility,
hasCollectedValues: 'collectedValues' in result,
collectedValuesType: typeof result.collectedValues,
collectedValuesLength: result.collectedValues?.length || 'N/A',
reason: result.reason
});
// CRITICAL REGRESSION TEST: collectedValues must be preserved
assert.equal(result.possibility, 1, 'Chain should reach target successfully');
assert.ok('collectedValues' in result, '🚨 REGRESSION: collectedValues field missing from result');
assert.ok(Array.isArray(result.collectedValues), '🚨 REGRESSION: collectedValues is not an array');
assert.ok(result.collectedValues.length > 0, '🚨 REGRESSION: collectedValues array is empty');
// Verify the collected values have the expected structure
const firstValue = result.collectedValues[0];
assert.ok(firstValue.value !== undefined, 'Collected value should have a value field');
assert.ok(firstValue.possibility !== undefined, 'Collected value should have a possibility field');
if (process.env.TEST_DEBUG === '1') console.log('✅ REGRESSION TEST PASSED: collectedValues properly preserved');
if (process.env.TEST_DEBUG === '1') console.log(' collectedValues count:', result.collectedValues.length);
if (process.env.TEST_DEBUG === '1') console.log(' First collected value:', {
value: firstValue.value,
possibility: firstValue.possibility,
source: firstValue.source
});
// Also verify it's in meta for backwards compatibility
if (result.meta?.collectedValues) {
if (process.env.TEST_DEBUG === '1') console.log('✅ BONUS: collectedValues also available in meta');
}
});
});
});