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,946 @@
|
||||
import { performance } from 'perf_hooks';
|
||||
import { Arbiter } from '../src/index.js';
|
||||
|
||||
console.log('⛓️ ChainRule Performance Benchmark Analysis\n');
|
||||
|
||||
// Benchmark utilities
|
||||
class ChainBenchmark {
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.results = [];
|
||||
this.labels = [];
|
||||
}
|
||||
|
||||
async run(fn, iterations = 1000, label = '') {
|
||||
// Warmup
|
||||
for (let i = 0; i < Math.min(10, iterations / 10); i++) {
|
||||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Actual benchmark
|
||||
const times = [];
|
||||
const totalStart = performance.now();
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
try {
|
||||
const start = performance.now();
|
||||
await fn();
|
||||
const end = performance.now();
|
||||
times.push(end - start);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
const totalTime = performance.now() - totalStart;
|
||||
const avg = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
const min = Math.min(...times);
|
||||
const max = Math.max(...times);
|
||||
const p50 = times.sort((a, b) => a - b)[Math.floor(times.length * 0.5)];
|
||||
const p95 = times[Math.floor(times.length * 0.95)];
|
||||
const p99 = times[Math.floor(times.length * 0.99)];
|
||||
const qps = Math.round(iterations / (totalTime / 1000));
|
||||
this.results.push({
|
||||
avg: avg.toFixed(3),
|
||||
min: min.toFixed(3),
|
||||
max: max.toFixed(3),
|
||||
p50: p50.toFixed(3),
|
||||
p95: p95.toFixed(3),
|
||||
p99: p99.toFixed(3),
|
||||
qps: qps
|
||||
});
|
||||
this.labels.push(label);
|
||||
return { avg, min, max, p50, p95, p99, qps };
|
||||
}
|
||||
|
||||
report() {
|
||||
console.log('Configuration | Avg (ms) | P50 (ms) | P95 (ms) | P99 (ms) | QPS | Speedup');
|
||||
console.log('---------------------------------|----------|----------|----------|----------|---------|--------');
|
||||
const baselineQPS = this.results[0].qps;
|
||||
this.results.forEach((result, i) => {
|
||||
const label = this.labels[i] || `Test ${i + 1}`;
|
||||
const speedup = i === 0 ? '1.00x' : `${(result.qps / baselineQPS).toFixed(2)}x`;
|
||||
console.log(`${label.padEnd(32)} | ${result.avg.padStart(8)} | ${result.p50.padStart(8)} | ${result.p95.padStart(8)} | ${result.p99.padStart(8)} | ${result.qps.toString().padStart(7)} | ${speedup.padStart(6)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Enterprise organizational data generator
|
||||
function setupEnterpriseChainGraph(scale = 'medium') {
|
||||
const scales = {
|
||||
small: { users: 200, groups: 50, projects: 20, departments: 8, budgets: 100 },
|
||||
medium: { users: 2000, groups: 200, projects: 100, departments: 15, budgets: 500 },
|
||||
large: { users: 10000, groups: 1000, projects: 500, departments: 25, budgets: 2000 },
|
||||
xlarge: { users: 20000, groups: 5000, projects: 2000, departments: 40, budgets: 8000 },
|
||||
xxlarge: { users: 100000, groups: 10000, projects: 5000, departments: 50, budgets: 20000 }
|
||||
};
|
||||
|
||||
const config = scales[scale];
|
||||
console.log(`🏢 Setting up ${scale} enterprise graph: ${config.users} users, ${config.groups} groups, ${config.projects} projects`);
|
||||
|
||||
const arbiter = new Arbiter({
|
||||
fastConstructionMode: true,
|
||||
enableInference: false // Disable by default for clean baseline measurements
|
||||
});
|
||||
|
||||
// Create organizational hierarchy: users → groups → departments → divisions → company
|
||||
const entities = {
|
||||
users: [],
|
||||
groups: [],
|
||||
departments: [],
|
||||
divisions: ['engineering', 'sales', 'marketing', 'finance', 'operations'],
|
||||
projects: [],
|
||||
budgets: [],
|
||||
resources: [],
|
||||
facilities: []
|
||||
};
|
||||
|
||||
// Generate users
|
||||
for (let i = 0; i < config.users; i++) {
|
||||
const userKey = `user:emp${i}`;
|
||||
entities.users.push(userKey);
|
||||
arbiter.addNode(userKey, 'user');
|
||||
}
|
||||
|
||||
// Generate groups (teams within departments)
|
||||
for (let i = 0; i < config.groups; i++) {
|
||||
const groupKey = `group:team${i}`;
|
||||
entities.groups.push(groupKey);
|
||||
arbiter.addNode(groupKey, 'group');
|
||||
}
|
||||
|
||||
// Generate departments
|
||||
for (let i = 0; i < config.departments; i++) {
|
||||
const deptKey = `dept:dept${i}`;
|
||||
entities.departments.push(deptKey);
|
||||
arbiter.addNode(deptKey, 'department');
|
||||
}
|
||||
|
||||
// Generate divisions
|
||||
entities.divisions.forEach(div => {
|
||||
const divKey = `division:${div}`;
|
||||
arbiter.addNode(divKey, 'division');
|
||||
});
|
||||
|
||||
// Add company root
|
||||
arbiter.addNode('company:acme', 'company');
|
||||
|
||||
// Generate projects
|
||||
for (let i = 0; i < config.projects; i++) {
|
||||
const projectKey = `project:proj${i}`;
|
||||
entities.projects.push(projectKey);
|
||||
arbiter.addNode(projectKey, 'project');
|
||||
}
|
||||
|
||||
// Generate budgets with values
|
||||
for (let i = 0; i < config.budgets; i++) {
|
||||
const budgetKey = `budget:budget${i}`;
|
||||
const value = Math.floor(Math.random() * 5000000) + 100000; // $100K to $5M
|
||||
entities.budgets.push({ key: budgetKey, value });
|
||||
arbiter.addNode(budgetKey, 'budget');
|
||||
}
|
||||
|
||||
// Generate resources (servers, databases, etc.)
|
||||
for (let i = 0; i < config.projects / 2; i++) {
|
||||
const resourceKey = `resource:res${i}`;
|
||||
const cost = Math.floor(Math.random() * 100000) + 5000; // $5K to $100K
|
||||
entities.resources.push({ key: resourceKey, cost });
|
||||
arbiter.addNode(resourceKey, 'resource');
|
||||
}
|
||||
|
||||
// Generate facilities
|
||||
const facilityNames = ['hq', 'east-office', 'west-office', 'remote', 'datacenter'];
|
||||
facilityNames.forEach(name => {
|
||||
const facilityKey = `facility:${name}`;
|
||||
entities.facilities.push(facilityKey);
|
||||
arbiter.addNode(facilityKey, 'facility');
|
||||
});
|
||||
|
||||
console.log(' 🔗 Building organizational chains...');
|
||||
|
||||
// Build 5-level organizational hierarchy: user → group → department → division → company
|
||||
|
||||
// User → Group membership
|
||||
entities.users.forEach(user => {
|
||||
// Each user belongs to 1-3 groups
|
||||
const groupCount = Math.floor(Math.random() * 3) + 1;
|
||||
for (let i = 0; i < groupCount; i++) {
|
||||
const group = entities.groups[Math.floor(Math.random() * entities.groups.length)];
|
||||
arbiter.addRelation(user, 'member_of', group);
|
||||
}
|
||||
});
|
||||
|
||||
// Group → Department membership
|
||||
entities.groups.forEach(group => {
|
||||
const dept = entities.departments[Math.floor(Math.random() * entities.departments.length)];
|
||||
arbiter.addRelation(group, 'belongs_to', dept);
|
||||
});
|
||||
|
||||
// Department → Division membership
|
||||
entities.departments.forEach(dept => {
|
||||
const division = entities.divisions[Math.floor(Math.random() * entities.divisions.length)];
|
||||
arbiter.addRelation(dept, 'part_of', `division:${division}`);
|
||||
});
|
||||
|
||||
// Division → Company membership
|
||||
entities.divisions.forEach(div => {
|
||||
arbiter.addRelation(`division:${div}`, 'part_of', 'company:acme');
|
||||
});
|
||||
|
||||
// Project chains: user → group → project → budget
|
||||
entities.projects.forEach(project => {
|
||||
// Projects owned by groups
|
||||
const ownerGroup = entities.groups[Math.floor(Math.random() * entities.groups.length)];
|
||||
arbiter.addRelation(ownerGroup, 'manages', project);
|
||||
|
||||
// Projects have budgets
|
||||
const budget = entities.budgets[Math.floor(Math.random() * entities.budgets.length)];
|
||||
arbiter.addRelation(project, 'has_budget', budget.key, { value: budget.value });
|
||||
|
||||
// Projects have resource costs
|
||||
if (Math.random() < 0.7) {
|
||||
const resource = entities.resources[Math.floor(Math.random() * entities.resources.length)];
|
||||
arbiter.addRelation(project, 'uses_resource', resource.key, { cost: resource.cost });
|
||||
}
|
||||
});
|
||||
|
||||
// Facility chains: user → facility, department → facility
|
||||
entities.users.forEach(user => {
|
||||
const facility = entities.facilities[Math.floor(Math.random() * entities.facilities.length)];
|
||||
arbiter.addRelation(user, 'located_in', facility);
|
||||
});
|
||||
|
||||
entities.departments.forEach(dept => {
|
||||
const facility = entities.facilities[Math.floor(Math.random() * entities.facilities.length)];
|
||||
arbiter.addRelation(dept, 'operates_in', facility);
|
||||
});
|
||||
|
||||
// Configure basic relation types
|
||||
const relationTypes = [
|
||||
'member_of', 'belongs_to', 'part_of', 'manages', 'has_budget',
|
||||
'uses_resource', 'located_in', 'operates_in', 'can_access', 'has_permission'
|
||||
];
|
||||
|
||||
relationTypes.forEach(rel => {
|
||||
arbiter.setRelationConfig(rel, { type: 'direct' });
|
||||
});
|
||||
|
||||
console.log(' ✅ Enterprise chain graph ready');
|
||||
|
||||
// Initialize PLTC for reachability checks
|
||||
console.log(' ⚡ Initializing PLTC indices...');
|
||||
const initStart = Date.now();
|
||||
arbiter.graphManager.initializeReachabilityChecker();
|
||||
const initTime = Date.now() - initStart;
|
||||
console.log(` ✅ PLTC initialized in ${initTime}ms`);
|
||||
|
||||
return { arbiter, ...entities };
|
||||
}
|
||||
|
||||
// Benchmark 1: Chain Length Performance
|
||||
async function benchmarkChainLength() {
|
||||
console.log('📏 Benchmark 1: Chain Length Performance Impact\n');
|
||||
|
||||
const graph = setupEnterpriseChainGraph('medium');
|
||||
const benchmark = new ChainBenchmark('Chain Length Performance');
|
||||
|
||||
// Baseline: Direct access (no chain)
|
||||
graph.arbiter.setRelationConfig('direct_access', {
|
||||
type: 'direct',
|
||||
relation: 'can_access'
|
||||
});
|
||||
|
||||
// Create a substantial number of direct access relations for realistic testing
|
||||
// Instead of 100 random relations, create relations for every user to some projects
|
||||
const directAccessPairs = [];
|
||||
graph.users.forEach((user, i) => {
|
||||
// Each user has direct access to 1-3 projects
|
||||
const numProjects = Math.floor(Math.random() * 3) + 1;
|
||||
for (let j = 0; j < numProjects; j++) {
|
||||
const project = graph.projects[(i * 7 + j) % graph.projects.length]; // Deterministic but spread out
|
||||
graph.arbiter.addRelation(user, 'can_access', project);
|
||||
directAccessPairs.push({ user, project });
|
||||
}
|
||||
});
|
||||
|
||||
console.log(` ✅ Created ${directAccessPairs.length} direct access relations (${(directAccessPairs.length / (graph.users.length * graph.projects.length) * 100).toFixed(1)}% coverage)`);
|
||||
|
||||
await benchmark.run(() => {
|
||||
// Test EXISTING relations for accurate performance measurement
|
||||
const pair = directAccessPairs[Math.floor(Math.random() * directAccessPairs.length)];
|
||||
return graph.arbiter.check(pair.user, 'direct_access', pair.project, { noInfer: true });
|
||||
}, 1000, 'Direct Access (baseline)');
|
||||
|
||||
// 2-step chain: user → group → project
|
||||
graph.arbiter.setRelationConfig('chain_2_step', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
// Build collection of valid 2-step chain paths for testing
|
||||
const validChain2Paths = [];
|
||||
graph.users.forEach(user => {
|
||||
// Find groups this user belongs to
|
||||
const userGroups = graph.arbiter.relationManager.getRelationsFromSrc(user, 'member_of');
|
||||
userGroups.forEach(groupRel => {
|
||||
// Find projects this group manages
|
||||
const groupProjects = graph.arbiter.relationManager.getRelationsFromSrc(groupRel.object, 'manages');
|
||||
groupProjects.forEach(projectRel => {
|
||||
validChain2Paths.push({ user, project: projectRel.object });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log(` ✅ Found ${validChain2Paths.length} valid 2-step chain paths (user→group→project)`);
|
||||
|
||||
await benchmark.run(() => {
|
||||
// Test EXISTING chain paths for accurate performance measurement
|
||||
if (validChain2Paths.length === 0) {
|
||||
// Fallback to random if no valid paths found
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'chain_2_step', project, { noInfer: true });
|
||||
} else {
|
||||
const path = validChain2Paths[Math.floor(Math.random() * validChain2Paths.length)];
|
||||
return graph.arbiter.check(path.user, 'chain_2_step', path.project, { noInfer: true });
|
||||
}
|
||||
}, 1000, '2-Step Chain (user→group→project)');
|
||||
|
||||
// 3-step chain: user → group → department → division
|
||||
graph.arbiter.setRelationConfig('chain_3_step', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'belongs_to', direction: 'out' },
|
||||
{ relation: 'part_of', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
// Build collection of valid 3-step chain paths for testing
|
||||
const validChain3Paths = [];
|
||||
graph.users.forEach(user => {
|
||||
// Find groups this user belongs to
|
||||
const userGroups = graph.arbiter.relationManager.getRelationsFromSrc(user, 'member_of');
|
||||
userGroups.forEach(groupRel => {
|
||||
// Find departments this group belongs to
|
||||
const groupDepts = graph.arbiter.relationManager.getRelationsFromSrc(groupRel.object, 'belongs_to');
|
||||
groupDepts.forEach(deptRel => {
|
||||
// Find divisions this department is part of
|
||||
const deptDivisions = graph.arbiter.relationManager.getRelationsFromSrc(deptRel.object, 'part_of');
|
||||
deptDivisions.forEach(divisionRel => {
|
||||
validChain3Paths.push({ user, division: divisionRel.object });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
console.log(` ✅ Found ${validChain3Paths.length} valid 3-step chain paths (user→group→dept→division)`);
|
||||
|
||||
await benchmark.run(() => {
|
||||
// Test EXISTING chain paths for accurate performance measurement
|
||||
if (validChain3Paths.length === 0) {
|
||||
// Fallback to random if no valid paths found
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const division = graph.divisions[Math.floor(Math.random() * graph.divisions.length)];
|
||||
return graph.arbiter.check(user, 'chain_3_step', `division:${division}`, { noInfer: true });
|
||||
} else {
|
||||
const path = validChain3Paths[Math.floor(Math.random() * validChain3Paths.length)];
|
||||
return graph.arbiter.check(path.user, 'chain_3_step', path.division, { noInfer: true });
|
||||
}
|
||||
}, 1000, '3-Step Chain (user→group→dept→division)');
|
||||
|
||||
// 4-step chain: user → group → project → budget → facility (create a valid 4-step path)
|
||||
// First add budget→facility relationships to create valid 4-step chains
|
||||
graph.budgets.forEach(budget => {
|
||||
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||||
graph.arbiter.addRelation(budget.key, 'allocated_to', facility);
|
||||
});
|
||||
|
||||
graph.arbiter.setRelationConfig('allocated_to', { type: 'direct' });
|
||||
graph.arbiter.setRelationConfig('chain_4_step', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' }, // user → group
|
||||
{ relation: 'manages', direction: 'out' }, // group → project
|
||||
{ relation: 'has_budget', direction: 'out' }, // project → budget
|
||||
{ relation: 'allocated_to', direction: 'out' } // budget → facility
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||||
return graph.arbiter.check(user, 'chain_4_step', facility, { noInfer: true });
|
||||
}, 1000, '4-Step Chain (user→group→project→budget→facility)');
|
||||
|
||||
// 5-step chain: user → group → department → division → company → facility
|
||||
// Add company→facility relationship for valid 5-step chain
|
||||
graph.facilities.forEach(facility => {
|
||||
graph.arbiter.addRelation('company:acme', 'operates', facility);
|
||||
});
|
||||
|
||||
graph.arbiter.setRelationConfig('operates', { type: 'direct' });
|
||||
graph.arbiter.setRelationConfig('chain_5_step', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' }, // user → group
|
||||
{ relation: 'belongs_to', direction: 'out' }, // group → department
|
||||
{ relation: 'part_of', direction: 'out' }, // department → division
|
||||
{ relation: 'part_of', direction: 'out' }, // division → company
|
||||
{ relation: 'operates', direction: 'out' } // company → facility
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||||
return graph.arbiter.check(user, 'chain_5_step', facility, { noInfer: true });
|
||||
}, 1000, '5-Step Chain (user→group→dept→div→company→facility)');
|
||||
|
||||
benchmark.report();
|
||||
}
|
||||
|
||||
// Benchmark 2: ChainRule vs Traditional Approaches
|
||||
async function benchmarkVsTraditional() {
|
||||
console.log('⚔️ Benchmark 2: ChainRule vs Traditional Authorization\n');
|
||||
|
||||
const graph = setupEnterpriseChainGraph('medium');
|
||||
const benchmark = new ChainBenchmark('ChainRule vs Traditional');
|
||||
|
||||
// Traditional ParentRule approach
|
||||
graph.arbiter.setRelationConfig('traditional_parent', {
|
||||
type: 'parent',
|
||||
parentRelation: 'manages',
|
||||
relation: 'member_of'
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'traditional_parent', project, { noInfer: true });
|
||||
}, 1000, 'ParentRule (baseline)');
|
||||
|
||||
// Equivalent ChainRule approach
|
||||
graph.arbiter.setRelationConfig('chain_equivalent', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'chain_equivalent', project, { noInfer: true });
|
||||
}, 1000, 'ChainRule (equivalent logic)');
|
||||
|
||||
// MultiHopRule approach
|
||||
graph.arbiter.setRelationConfig('multihop_approach', {
|
||||
type: 'multi_hop',
|
||||
relation: 'member_of',
|
||||
maxDepth: 3,
|
||||
pathAggregation: 'max'
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'multihop_approach', project, { noInfer: true });
|
||||
}, 1000, 'MultiHopRule (flexible paths)');
|
||||
|
||||
// ChainRule with reverse direction
|
||||
graph.arbiter.setRelationConfig('chain_reverse', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'manages', direction: 'in' },
|
||||
{ relation: 'member_of', direction: 'in' }
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
return graph.arbiter.check(project, 'chain_reverse', user, { noInfer: true });
|
||||
}, 1000, 'ChainRule (reverse direction)');
|
||||
|
||||
benchmark.report();
|
||||
}
|
||||
|
||||
// Benchmark 3: Value Extraction Performance
|
||||
async function benchmarkValueExtraction() {
|
||||
console.log('💰 Benchmark 3: Value Extraction Performance\n');
|
||||
|
||||
const graph = setupEnterpriseChainGraph('medium');
|
||||
const benchmark = new ChainBenchmark('Value Extraction Performance');
|
||||
|
||||
// Chain without value extraction (baseline)
|
||||
graph.arbiter.setRelationConfig('chain_no_values', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' },
|
||||
{ relation: 'has_budget', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||||
return graph.arbiter.check(user, 'chain_no_values', budget.key);
|
||||
}, 1000, 'Chain (no value extraction)');
|
||||
|
||||
// Chain with value extraction - SUM aggregation
|
||||
graph.arbiter.setRelationConfig('chain_sum_values', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' },
|
||||
{ relation: 'has_budget', direction: 'out' }
|
||||
],
|
||||
extractValues: true,
|
||||
extractFrom: 2,
|
||||
extractRelation: 'has_budget',
|
||||
valueAggregation: 'sum'
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||||
return graph.arbiter.check(user, 'chain_sum_values', budget.key);
|
||||
}, 1000, 'Chain with SUM aggregation');
|
||||
|
||||
// Chain with value extraction - MAX aggregation
|
||||
graph.arbiter.setRelationConfig('chain_max_values', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' },
|
||||
{ relation: 'has_budget', direction: 'out' }
|
||||
],
|
||||
extractValues: true,
|
||||
extractFrom: 2,
|
||||
extractRelation: 'has_budget',
|
||||
valueAggregation: 'max'
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||||
return graph.arbiter.check(user, 'chain_max_values', budget.key);
|
||||
}, 1000, 'Chain with MAX aggregation');
|
||||
|
||||
// Chain with value extraction - MIN aggregation
|
||||
graph.arbiter.setRelationConfig('chain_min_values', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' },
|
||||
{ relation: 'has_budget', direction: 'out' }
|
||||
],
|
||||
extractValues: true,
|
||||
extractFrom: 2,
|
||||
extractRelation: 'has_budget',
|
||||
valueAggregation: 'min'
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||||
return graph.arbiter.check(user, 'chain_min_values', budget.key);
|
||||
}, 1000, 'Chain with MIN aggregation');
|
||||
|
||||
// Chain with OWA fusion aggregation
|
||||
graph.arbiter.setRelationConfig('chain_owa_values', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' },
|
||||
{ relation: 'has_budget', direction: 'out' }
|
||||
],
|
||||
extractValues: true,
|
||||
extractFrom: 2,
|
||||
extractRelation: 'has_budget',
|
||||
valueAggregation: 'optimistic',
|
||||
owaWeights: [0.7, 0.5, 0.3, 0.1]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const budget = graph.budgets[Math.floor(Math.random() * graph.budgets.length)];
|
||||
return graph.arbiter.check(user, 'chain_owa_values', budget.key);
|
||||
}, 1000, 'Chain with OWA aggregation');
|
||||
|
||||
benchmark.report();
|
||||
}
|
||||
|
||||
// Benchmark 4: Direction and Pattern Performance
|
||||
async function benchmarkDirectionPatterns() {
|
||||
console.log('🧭 Benchmark 4: Chain Direction and Pattern Performance\n');
|
||||
|
||||
const graph = setupEnterpriseChainGraph('medium');
|
||||
const benchmark = new ChainBenchmark('Direction and Pattern Performance');
|
||||
|
||||
// Forward chain (baseline)
|
||||
graph.arbiter.setRelationConfig('forward_chain', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'belongs_to', direction: 'out' },
|
||||
{ relation: 'part_of', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const division = graph.divisions[Math.floor(Math.random() * graph.divisions.length)];
|
||||
return graph.arbiter.check(user, 'forward_chain', `division:${division}`);
|
||||
}, 1000, 'Forward Chain (baseline)');
|
||||
|
||||
// Backward chain
|
||||
graph.arbiter.setRelationConfig('backward_chain', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'part_of', direction: 'in' },
|
||||
{ relation: 'belongs_to', direction: 'in' },
|
||||
{ relation: 'member_of', direction: 'in' }
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const division = graph.divisions[Math.floor(Math.random() * graph.divisions.length)];
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
return graph.arbiter.check(`division:${division}`, 'backward_chain', user);
|
||||
}, 1000, 'Backward Chain');
|
||||
|
||||
// Mixed direction chain
|
||||
graph.arbiter.setRelationConfig('mixed_chain', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' }, // user → group
|
||||
{ relation: 'belongs_to', direction: 'in' }, // group ← department
|
||||
{ relation: 'operates_in', direction: 'out' } // department → facility
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||||
return graph.arbiter.check(user, 'mixed_chain', facility);
|
||||
}, 1000, 'Mixed Direction Chain');
|
||||
|
||||
// Parallel chains pattern (testing multiple chain endpoints)
|
||||
graph.arbiter.setRelationConfig('parallel_chains', {
|
||||
union: [
|
||||
{
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'located_in', direction: 'out' },
|
||||
{ relation: 'operates_in', direction: 'in' },
|
||||
{ relation: 'belongs_to', direction: 'in' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'parallel_chains', project);
|
||||
}, 1000, 'Parallel Chains (Union)');
|
||||
|
||||
benchmark.report();
|
||||
}
|
||||
|
||||
// Benchmark 5: ChainRule with Inference
|
||||
async function benchmarkChainInference() {
|
||||
console.log('🧠 Benchmark 5: ChainRule with Inference Integration\n');
|
||||
|
||||
const graph = setupEnterpriseChainGraph('small'); // Smaller graph for inference
|
||||
const benchmark = new ChainBenchmark('ChainRule Inference Performance');
|
||||
|
||||
// Remove some relationships to create inference opportunities
|
||||
const relationships = [];
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const user = graph.users[i];
|
||||
const group = graph.groups[Math.floor(Math.random() * Math.min(20, graph.groups.length))];
|
||||
relationships.push({ user, group });
|
||||
graph.arbiter.relationManager.removeRelation(user, 'member_of', group);
|
||||
}
|
||||
|
||||
// Chain without inference (baseline)
|
||||
graph.arbiter.setRelationConfig('chain_no_inference', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
],
|
||||
allowInference: false
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'chain_no_inference', project, { noInfer: true });
|
||||
}, 500, 'Chain (no inference)');
|
||||
|
||||
// Chain with basic inference
|
||||
graph.arbiter.setRelationConfig('chain_basic_inference', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
],
|
||||
allowInference: true
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'chain_basic_inference', project);
|
||||
}, 500, 'Chain (basic inference)');
|
||||
|
||||
// Chain with inference and reliability threshold
|
||||
graph.arbiter.setRelationConfig('chain_reliable_inference', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
],
|
||||
allowInference: true,
|
||||
minReliability: 0.7
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'chain_reliable_inference', project);
|
||||
}, 500, 'Chain (high reliability inference)');
|
||||
|
||||
benchmark.report();
|
||||
}
|
||||
|
||||
|
||||
// Benchmark 7: Real-World Enterprise Scenarios
|
||||
async function benchmarkEnterpriseScenarios() {
|
||||
console.log('🏢 Benchmark 7: Real-World Enterprise Authorization Scenarios\n');
|
||||
|
||||
const graph = setupEnterpriseChainGraph('medium');
|
||||
const benchmark = new ChainBenchmark('Enterprise Scenarios');
|
||||
|
||||
// Scenario 1: Budget Authorization (user → group → project → budget >= threshold)
|
||||
graph.arbiter.setRelationConfig('budget_authorization', {
|
||||
type: 'relational_comparator',
|
||||
leftOperand: {
|
||||
rule: {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' },
|
||||
{ relation: 'has_budget', direction: 'out' }
|
||||
],
|
||||
extractValues: true,
|
||||
extractFrom: 2,
|
||||
extractRelation: 'has_budget',
|
||||
valueAggregation: 'sum'
|
||||
},
|
||||
extractValue: true
|
||||
},
|
||||
rightOperand: {
|
||||
rule: { type: 'direct', relation: 'has_value' },
|
||||
extractValue: true
|
||||
},
|
||||
comparator: '>=',
|
||||
fallbackBehavior: 'deny'
|
||||
});
|
||||
|
||||
// Add budget thresholds
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const threshold = Math.floor(Math.random() * 1000000) + 50000;
|
||||
graph.arbiter.addRelation(`threshold:${i}`, 'has_value', `value:${threshold}`, { value: threshold });
|
||||
}
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const threshold = `threshold:${Math.floor(Math.random() * 50)}`;
|
||||
return graph.arbiter.check(user, 'budget_authorization', threshold);
|
||||
}, 500, 'Budget Authorization Chain');
|
||||
|
||||
// Scenario 2: Facility Access (user → department → facility)
|
||||
graph.arbiter.setRelationConfig('facility_access', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'belongs_to', direction: 'out' },
|
||||
{ relation: 'operates_in', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const facility = graph.facilities[Math.floor(Math.random() * graph.facilities.length)];
|
||||
return graph.arbiter.check(user, 'facility_access', facility);
|
||||
}, 1000, 'Facility Access Chain');
|
||||
|
||||
// Scenario 3: Resource Approval (user → group → project → resource)
|
||||
graph.arbiter.setRelationConfig('resource_approval', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' },
|
||||
{ relation: 'uses_resource', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const resource = graph.resources[Math.floor(Math.random() * graph.resources.length)];
|
||||
return graph.arbiter.check(user, 'resource_approval', resource.key);
|
||||
}, 1000, 'Resource Approval Chain');
|
||||
|
||||
// Scenario 4: Cross-Division Access (complex multi-step authorization)
|
||||
graph.arbiter.setRelationConfig('cross_division_access', {
|
||||
union: [
|
||||
{
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'belongs_to', direction: 'out' },
|
||||
{ relation: 'part_of', direction: 'out' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'located_in', direction: 'out' },
|
||||
{ relation: 'operates_in', direction: 'in' },
|
||||
{ relation: 'part_of', direction: 'out' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const division = graph.divisions[Math.floor(Math.random() * graph.divisions.length)];
|
||||
return graph.arbiter.check(user, 'cross_division_access', `division:${division}`);
|
||||
}, 1000, 'Cross-Division Access (Union)');
|
||||
|
||||
benchmark.report();
|
||||
}
|
||||
|
||||
// Main benchmark runner
|
||||
async function runChainRuleBenchmarks() {
|
||||
console.log('🚀 Starting ChainRule Performance Benchmarks...\n');
|
||||
|
||||
try {
|
||||
await benchmarkChainLength();
|
||||
await benchmarkVsTraditional();
|
||||
await benchmarkValueExtraction();
|
||||
await benchmarkDirectionPatterns();
|
||||
await benchmarkChainInference();
|
||||
await benchmarkEnterpriseScenarios();
|
||||
|
||||
console.log('\n✅ All ChainRule benchmarks completed successfully!');
|
||||
|
||||
console.log('\n🚀 X-LARGE SCALE BENCHMARKS (xlarge)');
|
||||
await benchmarkChainLengthScale('xlarge', 100);
|
||||
await benchmarkVsTraditionalScale('xlarge', 100);
|
||||
console.log('\n🚀 XX-LARGE SCALE BENCHMARKS (xxlarge)');
|
||||
await benchmarkChainLengthScale('xxlarge', 50);
|
||||
await benchmarkVsTraditionalScale('xxlarge', 50);
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Benchmark failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if this file is executed directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
runChainRuleBenchmarks();
|
||||
}
|
||||
|
||||
export { runChainRuleBenchmarks };
|
||||
|
||||
// Add helper functions for large scale runs:
|
||||
async function benchmarkChainLengthScale(scale, iterations) {
|
||||
console.log(`\n📏 [${scale.toUpperCase()}] Chain Length Performance Impact`);
|
||||
const graph = setupEnterpriseChainGraph(scale);
|
||||
const benchmark = new ChainBenchmark(`Chain Length Performance (${scale})`);
|
||||
// Baseline: Direct access (no chain)
|
||||
graph.arbiter.setRelationConfig('direct_access', {
|
||||
type: 'direct',
|
||||
relation: 'can_access'
|
||||
});
|
||||
const directAccessPairs = [];
|
||||
graph.users.forEach((user, i) => {
|
||||
const numProjects = Math.floor(Math.random() * 3) + 1;
|
||||
for (let j = 0; j < numProjects; j++) {
|
||||
const project = graph.projects[(i * 7 + j) % graph.projects.length];
|
||||
graph.arbiter.addRelation(user, 'can_access', project);
|
||||
directAccessPairs.push({ user, project });
|
||||
}
|
||||
});
|
||||
await benchmark.run(() => {
|
||||
const pair = directAccessPairs[Math.floor(Math.random() * directAccessPairs.length)];
|
||||
return graph.arbiter.check(pair.user, 'direct_access', pair.project, { noInfer: true });
|
||||
}, iterations, 'Direct Access (baseline)');
|
||||
// 2-step chain
|
||||
graph.arbiter.setRelationConfig('chain_2_step', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
]
|
||||
});
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'chain_2_step', project, { noInfer: true });
|
||||
}, iterations, '2-Step Chain (user→group→project)');
|
||||
benchmark.report();
|
||||
}
|
||||
async function benchmarkVsTraditionalScale(scale, iterations) {
|
||||
console.log(`\n⚔️ [${scale.toUpperCase()}] ChainRule vs Traditional Authorization`);
|
||||
const graph = setupEnterpriseChainGraph(scale);
|
||||
const benchmark = new ChainBenchmark(`ChainRule vs Traditional (${scale})`);
|
||||
// Traditional ParentRule approach
|
||||
graph.arbiter.setRelationConfig('traditional_parent', {
|
||||
type: 'parent',
|
||||
parentRelation: 'manages',
|
||||
relation: 'member_of'
|
||||
});
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'traditional_parent', project, { noInfer: true });
|
||||
}, iterations, 'ParentRule (baseline)');
|
||||
// Equivalent ChainRule approach
|
||||
graph.arbiter.setRelationConfig('chain_equivalent', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'manages', direction: 'out' }
|
||||
]
|
||||
});
|
||||
await benchmark.run(() => {
|
||||
const user = graph.users[Math.floor(Math.random() * graph.users.length)];
|
||||
const project = graph.projects[Math.floor(Math.random() * graph.projects.length)];
|
||||
return graph.arbiter.check(user, 'chain_equivalent', project, { noInfer: true });
|
||||
}, iterations, 'ChainRule (equivalent logic)');
|
||||
benchmark.report();
|
||||
}
|
||||
Reference in New Issue
Block a user