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,80 @@
|
||||
import { Arbiter } from '../../src/core/Arbiter.js';
|
||||
|
||||
export function createTestArbiter(options = {}) {
|
||||
const defaults = {
|
||||
embeddingDimensions: 256,
|
||||
directCheckCacheSize: 10000,
|
||||
directCheckCacheTTL: 60000,
|
||||
disableCaching: false,
|
||||
disableChainCaching: false,
|
||||
disableDirectCaching: false
|
||||
};
|
||||
|
||||
return new Arbiter({ ...defaults, ...options });
|
||||
}
|
||||
|
||||
export function seedBasicGraph(arbiter) {
|
||||
const nodes = [
|
||||
['user:alice', 'user'],
|
||||
['user:bob', 'user'],
|
||||
['user:charlie', 'user'],
|
||||
['doc:report', 'document'],
|
||||
['doc:invoice', 'document'],
|
||||
['project:web-app', 'project'],
|
||||
['group:engineering', 'group'],
|
||||
['group:management', 'group'],
|
||||
['account:main', 'account'],
|
||||
['session:sess-1', 'session']
|
||||
];
|
||||
|
||||
for (const [key, type] of nodes) {
|
||||
arbiter.addNode(key, type);
|
||||
}
|
||||
|
||||
const relations = [
|
||||
['user:alice', 'member_of', 'group:engineering', 1.0],
|
||||
['user:bob', 'member_of', 'group:management', 1.0],
|
||||
['group:engineering', 'can_read', 'doc:report', 0.8],
|
||||
['group:engineering', 'can_read', 'project:web-app', 0.9],
|
||||
['group:management', 'can_read', 'doc:invoice', 1.0],
|
||||
['user:alice', 'controls', 'account:main', 1.0],
|
||||
['session:sess-1', 'authenticated_as', 'user:alice', 1.0]
|
||||
];
|
||||
|
||||
for (const [src, rel, dst, possibility, metadata] of relations) {
|
||||
arbiter.addRelation(src, rel, dst, possibility, metadata);
|
||||
}
|
||||
|
||||
const relationConfigs = [
|
||||
['member_of', { type: 'direct' }],
|
||||
['can_read', { type: 'direct' }],
|
||||
['controls', { type: 'direct' }],
|
||||
['authenticated_as', { type: 'direct' }]
|
||||
];
|
||||
|
||||
for (const [rel, config] of relationConfigs) {
|
||||
arbiter.setRelationConfig(rel, config);
|
||||
}
|
||||
|
||||
return arbiter;
|
||||
}
|
||||
|
||||
export function seedRelation(arbiter, src, rel, dst, possibility = 1.0, metadata = {}) {
|
||||
arbiter.addRelation(src, rel, dst, possibility, metadata);
|
||||
if (!arbiter.relationConfigs.has(rel)) {
|
||||
arbiter.setRelationConfig(rel, { type: 'direct' });
|
||||
}
|
||||
return arbiter;
|
||||
}
|
||||
|
||||
export function createPartialGraph(nodes = [], relations = []) {
|
||||
return { nodes, relations };
|
||||
}
|
||||
|
||||
export function seedPartialRelation(arbiter, src, rel, dst, possibility = 1.0, value = undefined) {
|
||||
const relObj = { src, relation: rel, dst, possibility };
|
||||
if (value !== undefined) {
|
||||
relObj.value = value;
|
||||
}
|
||||
return { nodes: [], relations: [relObj] };
|
||||
}
|
||||
@@ -0,0 +1,598 @@
|
||||
import { Arbiter } from '../../src/core/Arbiter.js';
|
||||
|
||||
/**
|
||||
* Big Graph Generator for Enterprise Authorization Testing
|
||||
* Generates realistic authorization graphs with complex multi-hop relationships
|
||||
*/
|
||||
export class BigGraphGenerator {
|
||||
constructor(config = {}) {
|
||||
this.config = {
|
||||
scale: 'small',
|
||||
seed: 12345,
|
||||
...config
|
||||
};
|
||||
|
||||
// Scale configurations - reduced for testing
|
||||
this.scaleConfigs = {
|
||||
small: {
|
||||
users: 100,
|
||||
documents: 500,
|
||||
relations: 1000,
|
||||
complexChains: 200, // Multi-hop authorization chains
|
||||
targetQPS: 100,
|
||||
maxMemoryMB: 128,
|
||||
maxLatencyMs: 50
|
||||
},
|
||||
medium: {
|
||||
users: 500,
|
||||
documents: 2000,
|
||||
relations: 5000,
|
||||
complexChains: 1000,
|
||||
targetQPS: 200,
|
||||
maxMemoryMB: 256,
|
||||
maxLatencyMs: 100
|
||||
},
|
||||
large: {
|
||||
users: 1000,
|
||||
documents: 5000,
|
||||
relations: 10000,
|
||||
complexChains: 2000,
|
||||
targetQPS: 500,
|
||||
maxMemoryMB: 512,
|
||||
maxLatencyMs: 200
|
||||
},
|
||||
enterprise: {
|
||||
users: 2000,
|
||||
documents: 10000,
|
||||
relations: 20000,
|
||||
complexChains: 5000,
|
||||
targetQPS: 1000,
|
||||
maxMemoryMB: 1024,
|
||||
maxLatencyMs: 500
|
||||
},
|
||||
million: {
|
||||
users: 100000,
|
||||
documents: 900000,
|
||||
relations: 2000000,
|
||||
complexChains: 10000, // Reduced complexity to avoid stack overflow
|
||||
targetQPS: 100,
|
||||
maxMemoryMB: 8192,
|
||||
maxLatencyMs: 1000
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize random number generator with seed
|
||||
this.rng = this._createSeededRNG(this.config.seed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create seeded random number generator for reproducible results
|
||||
*/
|
||||
_createSeededRNG(seed) {
|
||||
let state = seed;
|
||||
return {
|
||||
next: () => {
|
||||
state = (state * 1664525 + 1013904223) % 4294967296;
|
||||
return state / 4294967296;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate authorization graph for specified model
|
||||
*/
|
||||
generateGraph(model = 'enterprise') {
|
||||
const scale = this.scaleConfigs[this.config.scale];
|
||||
|
||||
const graphData = {
|
||||
users: [],
|
||||
documents: [],
|
||||
relations: [],
|
||||
enterprises: [],
|
||||
saasTenants: [],
|
||||
metadata: {
|
||||
model,
|
||||
scale: this.config.scale,
|
||||
generatedAt: new Date().toISOString(),
|
||||
seed: this.config.seed
|
||||
}
|
||||
};
|
||||
|
||||
// Generate users
|
||||
graphData.users = this._generateUsers(scale);
|
||||
|
||||
// Generate documents
|
||||
graphData.documents = this._generateDocuments(scale);
|
||||
|
||||
// Generate basic user-document relationships
|
||||
const basicRelations = this._generateBasicRelations(scale, graphData.users, graphData.documents);
|
||||
graphData.relations.push(...basicRelations);
|
||||
|
||||
// Generate complex authorization chains (multi-hop relationships)
|
||||
const { complexChains, roleNodes, departmentNodes } = this._generateComplexAuthorizationChains(scale, graphData.users, graphData.documents);
|
||||
graphData.relations.push(...complexChains);
|
||||
|
||||
// Add the generated nodes to the graph data
|
||||
graphData.roles = roleNodes;
|
||||
graphData.departments = departmentNodes;
|
||||
|
||||
// Generate organizational hierarchies
|
||||
const orgRelations = this._generateOrganizationalHierarchies(scale, graphData.users);
|
||||
graphData.relations.push(...orgRelations);
|
||||
|
||||
// Generate long organizational chains for multi-hop testing
|
||||
const longChains = this._generateLongOrganizationalChains(scale, graphData.users, graphData.documents);
|
||||
graphData.relations.push(...longChains);
|
||||
|
||||
// Generate role-based access chains
|
||||
const roleChains = this._generateRoleBasedChains(scale, graphData.users, graphData.documents);
|
||||
graphData.relations.push(...roleChains);
|
||||
|
||||
return graphData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load graph data into Arbiter instance
|
||||
*/
|
||||
loadIntoArbiter(graphData, options = {}) {
|
||||
const arbiter = new Arbiter({
|
||||
fastConstructionMode: true,
|
||||
...options
|
||||
});
|
||||
|
||||
// Add all nodes
|
||||
graphData.users.forEach(user => {
|
||||
arbiter.addNode(user.id, 'user');
|
||||
});
|
||||
|
||||
graphData.documents.forEach(doc => {
|
||||
arbiter.addNode(doc.id, 'document');
|
||||
});
|
||||
|
||||
// Add role nodes if they exist
|
||||
if (graphData.roles) {
|
||||
graphData.roles.forEach(role => {
|
||||
arbiter.addNode(role.id, 'role');
|
||||
});
|
||||
}
|
||||
|
||||
// Add department nodes if they exist
|
||||
if (graphData.departments) {
|
||||
graphData.departments.forEach(dept => {
|
||||
arbiter.addNode(dept.id, 'department');
|
||||
});
|
||||
}
|
||||
|
||||
// Add intermediate nodes for long chains
|
||||
const longChainRelations = graphData.relations.filter(r => r.metadata?.type === 'long_chain');
|
||||
const intermediateNodes = new Set();
|
||||
|
||||
longChainRelations.forEach(relation => {
|
||||
// Add all intermediate nodes (teams, departments, divisions, companies)
|
||||
if (relation.src.startsWith('team:') || relation.src.startsWith('dept:') ||
|
||||
relation.src.startsWith('division:') || relation.src.startsWith('company:')) {
|
||||
intermediateNodes.add(relation.src);
|
||||
}
|
||||
if (relation.dst.startsWith('team:') || relation.dst.startsWith('division:') || relation.dst.startsWith('company:')) {
|
||||
intermediateNodes.add(relation.dst);
|
||||
}
|
||||
});
|
||||
|
||||
intermediateNodes.forEach(nodeId => {
|
||||
const nodeType = nodeId.startsWith('team:') ? 'team' :
|
||||
nodeId.startsWith('dept:') ? 'department' :
|
||||
nodeId.startsWith('division:') ? 'division' : 'company';
|
||||
arbiter.addNode(nodeId, nodeType);
|
||||
});
|
||||
|
||||
// Add all relations
|
||||
graphData.relations.forEach(relation => {
|
||||
arbiter.addRelation(relation.src, relation.relation, relation.dst, relation.possibility);
|
||||
});
|
||||
|
||||
// Disable fast construction mode and build indices now that loading is complete
|
||||
arbiter.setFastConstructionMode(false);
|
||||
|
||||
// Configure relation types with proper ReBAC patterns
|
||||
const relationTypes = ['can_read', 'can_write', 'can_delete', 'can_share', 'can_admin'];
|
||||
relationTypes.forEach(relType => {
|
||||
// Configure direct relations
|
||||
arbiter.setRelationConfig(relType, { type: 'direct' });
|
||||
|
||||
// Configure role-based access using chain rule for proper role-based access
|
||||
const roleBasedRel = `${relType}_via_role`;
|
||||
arbiter.setRelationConfig(roleBasedRel, {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: relType, direction: 'out' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
// Configure membership relations as direct
|
||||
const membershipTypes = ['member_of', 'manager_of', 'reports_to'];
|
||||
membershipTypes.forEach(relType => {
|
||||
arbiter.setRelationConfig(relType, { type: 'direct' });
|
||||
});
|
||||
|
||||
// Configure department-based access using chain rule
|
||||
const departmentBasedRel = 'can_write_via_department';
|
||||
arbiter.setRelationConfig(departmentBasedRel, {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'can_write', direction: 'out' }
|
||||
]
|
||||
});
|
||||
|
||||
// Configure multi-hop access for long chain traversal (computationally intensive)
|
||||
// Use chain rule for complex multi-step authorization
|
||||
arbiter.setRelationConfig('can_access_multi_hop', {
|
||||
type: 'chain',
|
||||
steps: [
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'member_of', direction: 'out' },
|
||||
{ relation: 'can_read', direction: 'out' }
|
||||
],
|
||||
collectValues: true,
|
||||
valueAggregation: 'sum'
|
||||
});
|
||||
|
||||
return arbiter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate users
|
||||
*/
|
||||
_generateUsers(scale) {
|
||||
const users = [];
|
||||
const names = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry', 'Ivy', 'Jack'];
|
||||
const surnames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez'];
|
||||
|
||||
for (let i = 0; i < scale.users; i++) {
|
||||
const firstName = names[Math.floor(this.rng.next() * names.length)];
|
||||
const lastName = surnames[Math.floor(this.rng.next() * surnames.length)];
|
||||
|
||||
users.push({
|
||||
id: `user:${firstName} ${lastName}-${i}`,
|
||||
type: 'employee',
|
||||
name: `${firstName} ${lastName}`,
|
||||
email: `${firstName.toLowerCase()} ${lastName.toLowerCase()}@company.com`,
|
||||
role: ['manager', 'employee', 'contractor', 'intern'][Math.floor(this.rng.next() * 4)],
|
||||
clearance: ['public', 'internal', 'confidential', 'secret', 'top_secret'][Math.floor(this.rng.next() * 5)],
|
||||
department: `dept:${this._generateDepartmentName()}-${i}`,
|
||||
organization: `org:company-${Math.floor(i / 50)}`,
|
||||
manager: null,
|
||||
startDate: new Date(Date.now() - Math.floor(this.rng.next() * 365 * 24 * 60 * 60 * 1000)).toISOString(),
|
||||
isActive: true
|
||||
});
|
||||
}
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate documents
|
||||
*/
|
||||
_generateDocuments(scale) {
|
||||
const documents = [];
|
||||
const docTypes = ['report', 'analysis', 'proposal', 'contract', 'policy', 'spec', 'budget'];
|
||||
|
||||
for (let i = 0; i < scale.documents; i++) {
|
||||
const docType = docTypes[Math.floor(this.rng.next() * docTypes.length)];
|
||||
|
||||
documents.push({
|
||||
id: `doc:${docType}-${i}`,
|
||||
type: 'document',
|
||||
name: `${docType}_${i}`,
|
||||
classification: ['public', 'internal', 'confidential', 'secret', 'top_secret'][Math.floor(this.rng.next() * 5)],
|
||||
department: `dept:${this._generateDepartmentName()}-${i}`,
|
||||
organization: `org:company-${Math.floor(i / 100)}`,
|
||||
requiredClearance: ['public', 'internal', 'confidential', 'secret', 'top_secret'][Math.floor(this.rng.next() * 5)],
|
||||
cost: Math.floor(this.rng.next() * 10000),
|
||||
createdDate: new Date(Date.now() - Math.floor(this.rng.next() * 365 * 24 * 60 * 60 * 1000)).toISOString(),
|
||||
lastModified: new Date(Date.now() - Math.floor(this.rng.next() * 30 * 24 * 60 * 60 * 1000)).toISOString(),
|
||||
size: Math.floor(this.rng.next() * 1000000),
|
||||
tags: this._generateTags()
|
||||
});
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate basic user-document relationships
|
||||
* FIXED: Reduce direct relations to avoid conflicts with chain relations
|
||||
*/
|
||||
_generateBasicRelations(scale, users, documents) {
|
||||
const relations = [];
|
||||
const operations = ['can_read', 'can_write', 'can_delete', 'can_share'];
|
||||
|
||||
// Reduce the number of direct relations to avoid conflicts with chain relations
|
||||
const reducedRelations = Math.floor(scale.relations * 0.3); // Only 30% of original
|
||||
|
||||
for (let i = 0; i < reducedRelations; i++) {
|
||||
const user = users[Math.floor(this.rng.next() * users.length)];
|
||||
const document = documents[Math.floor(this.rng.next() * documents.length)];
|
||||
const operation = operations[Math.floor(this.rng.next() * operations.length)];
|
||||
const possibility = 0.5 + (this.rng.next() * 0.5); // 0.5 to 1.0
|
||||
|
||||
relations.push({
|
||||
src: user.id,
|
||||
relation: operation,
|
||||
dst: document.id,
|
||||
possibility,
|
||||
metadata: {
|
||||
role: user.role,
|
||||
clearance: user.clearance,
|
||||
department: user.department,
|
||||
grantedDate: new Date().toISOString(),
|
||||
expiresDate: null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate complex authorization chains (multi-hop relationships)
|
||||
* These create realistic enterprise authorization scenarios
|
||||
*/
|
||||
_generateComplexAuthorizationChains(scale, users, documents) {
|
||||
const relations = [];
|
||||
const roleNodes = [];
|
||||
const departmentNodes = [];
|
||||
const chainCount = scale.complexChains || Math.floor(scale.relations * 0.2);
|
||||
|
||||
// Create role nodes first
|
||||
const roleTypes = ['admin', 'manager', 'employee', 'contractor', 'intern'];
|
||||
const createdRoles = new Set();
|
||||
|
||||
for (let i = 0; i < chainCount; i++) {
|
||||
const user = users[Math.floor(this.rng.next() * users.length)];
|
||||
const document = documents[Math.floor(this.rng.next() * documents.length)];
|
||||
|
||||
// Chain 1: User -> Role -> Document (Role-based access)
|
||||
const roleType = roleTypes[Math.floor(this.rng.next() * roleTypes.length)];
|
||||
const roleId = `role:${roleType}-${Math.floor(this.rng.next() * 10)}`;
|
||||
|
||||
// Create role node if it doesn't exist
|
||||
if (!createdRoles.has(roleId)) {
|
||||
roleNodes.push({
|
||||
id: roleId,
|
||||
type: 'role',
|
||||
name: `${roleType} role`,
|
||||
roleType: roleType,
|
||||
permissions: ['can_read', 'can_write', 'can_share']
|
||||
});
|
||||
createdRoles.add(roleId);
|
||||
}
|
||||
|
||||
relations.push({
|
||||
src: user.id,
|
||||
relation: 'member_of',
|
||||
dst: roleId,
|
||||
possibility: 0.9,
|
||||
metadata: { type: 'role_membership' }
|
||||
});
|
||||
|
||||
relations.push({
|
||||
src: roleId,
|
||||
relation: 'can_read',
|
||||
dst: document.id,
|
||||
possibility: 0.8,
|
||||
metadata: { type: 'role_permission' }
|
||||
});
|
||||
|
||||
// Chain 2: User -> Department -> Document (Department-based access)
|
||||
const deptName = this._generateDepartmentName();
|
||||
const deptId = `dept:${deptName}-${Math.floor(this.rng.next() * 20)}`;
|
||||
|
||||
// Create department node if it doesn't exist
|
||||
if (!createdRoles.has(deptId)) {
|
||||
departmentNodes.push({
|
||||
id: deptId,
|
||||
type: 'department',
|
||||
name: `${deptName} department`,
|
||||
departmentName: deptName,
|
||||
permissions: ['can_read', 'can_write']
|
||||
});
|
||||
createdRoles.add(deptId);
|
||||
}
|
||||
|
||||
relations.push({
|
||||
src: user.id,
|
||||
relation: 'member_of',
|
||||
dst: deptId,
|
||||
possibility: 0.95,
|
||||
metadata: { type: 'department_membership' }
|
||||
});
|
||||
|
||||
relations.push({
|
||||
src: deptId,
|
||||
relation: 'can_write',
|
||||
dst: document.id,
|
||||
possibility: 0.7,
|
||||
metadata: { type: 'department_permission' }
|
||||
});
|
||||
|
||||
// Chain 3: User -> Manager -> Document (Manager approval chain)
|
||||
const manager = users[Math.floor(this.rng.next() * users.length)];
|
||||
relations.push({
|
||||
src: user.id,
|
||||
relation: 'reports_to',
|
||||
dst: manager.id,
|
||||
possibility: 0.9,
|
||||
metadata: { type: 'reporting_chain' }
|
||||
});
|
||||
|
||||
relations.push({
|
||||
src: manager.id,
|
||||
relation: 'can_share',
|
||||
dst: document.id,
|
||||
possibility: 0.6,
|
||||
metadata: { type: 'manager_permission' }
|
||||
});
|
||||
}
|
||||
|
||||
return { complexChains: relations, roleNodes, departmentNodes };
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate long organizational chains for multi-hop testing
|
||||
* These create deep hierarchies that will be computationally intensive to traverse
|
||||
*/
|
||||
_generateLongOrganizationalChains(scale, users, documents) {
|
||||
const relations = [];
|
||||
const chainCount = Math.floor(scale.users * 0.1); // 10% of users get long chains
|
||||
|
||||
for (let i = 0; i < chainCount; i++) {
|
||||
const user = users[Math.floor(this.rng.next() * users.length)];
|
||||
const document = documents[Math.floor(this.rng.next() * documents.length)];
|
||||
|
||||
// Create a long chain: User -> Team -> Department -> Division -> Company -> Document
|
||||
const teamId = `team:${this._generateTeamName()}-${Math.floor(this.rng.next() * 50)}`;
|
||||
const deptId = `dept:${this._generateDepartmentName()}-${Math.floor(this.rng.next() * 100)}`;
|
||||
const divisionId = `division:${this._generateDivisionName()}-${Math.floor(this.rng.next() * 20)}`;
|
||||
const companyId = `company:${this._generateCompanyName()}-${Math.floor(this.rng.next() * 10)}`;
|
||||
|
||||
// Create the long chain with decreasing possibility (realistic for deep hierarchies)
|
||||
relations.push({
|
||||
src: user.id,
|
||||
relation: 'member_of',
|
||||
dst: teamId,
|
||||
possibility: 0.95,
|
||||
metadata: { type: 'long_chain', step: 1, chainId: `chain-${i}` }
|
||||
});
|
||||
|
||||
relations.push({
|
||||
src: teamId,
|
||||
relation: 'member_of',
|
||||
dst: deptId,
|
||||
possibility: 0.9,
|
||||
metadata: { type: 'long_chain', step: 2, chainId: `chain-${i}` }
|
||||
});
|
||||
|
||||
relations.push({
|
||||
src: deptId,
|
||||
relation: 'member_of',
|
||||
dst: divisionId,
|
||||
possibility: 0.85,
|
||||
metadata: { type: 'long_chain', step: 3, chainId: `chain-${i}` }
|
||||
});
|
||||
|
||||
relations.push({
|
||||
src: divisionId,
|
||||
relation: 'member_of',
|
||||
dst: companyId,
|
||||
possibility: 0.8,
|
||||
metadata: { type: 'long_chain', step: 4, chainId: `chain-${i}` }
|
||||
});
|
||||
|
||||
relations.push({
|
||||
src: companyId,
|
||||
relation: 'can_read',
|
||||
dst: document.id,
|
||||
possibility: 0.75,
|
||||
metadata: { type: 'long_chain', step: 5, chainId: `chain-${i}` }
|
||||
});
|
||||
}
|
||||
|
||||
return relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate organizational hierarchies
|
||||
*/
|
||||
_generateOrganizationalHierarchies(scale, users) {
|
||||
const relations = [];
|
||||
|
||||
// Create department hierarchies
|
||||
for (let i = 0; i < Math.floor(scale.users / 10); i++) {
|
||||
const deptId = `dept:${this._generateDepartmentName()}-${i}`;
|
||||
const parentDeptId = `dept:${this._generateDepartmentName()}-${Math.floor(i / 3)}`;
|
||||
|
||||
if (i > 0) {
|
||||
relations.push({
|
||||
src: deptId,
|
||||
relation: 'reports_to',
|
||||
dst: parentDeptId,
|
||||
possibility: 0.9,
|
||||
metadata: { type: 'department_hierarchy' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate role-based access chains
|
||||
*/
|
||||
_generateRoleBasedChains(scale, users, documents) {
|
||||
const relations = [];
|
||||
|
||||
// Create role hierarchies
|
||||
const roles = ['admin', 'manager', 'senior', 'employee', 'contractor', 'intern'];
|
||||
|
||||
for (let i = 0; i < roles.length - 1; i++) {
|
||||
const currentRole = `role:${roles[i]}`;
|
||||
const parentRole = `role:${roles[i + 1]}`;
|
||||
|
||||
relations.push({
|
||||
src: currentRole,
|
||||
relation: 'inherits_from',
|
||||
dst: parentRole,
|
||||
possibility: 0.8,
|
||||
metadata: { type: 'role_inheritance' }
|
||||
});
|
||||
}
|
||||
|
||||
return relations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate department name
|
||||
*/
|
||||
_generateDepartmentName() {
|
||||
const departments = ['engineering', 'finance', 'hr', 'legal', 'operations', 'marketing', 'sales'];
|
||||
return departments[Math.floor(this.rng.next() * departments.length)];
|
||||
}
|
||||
|
||||
_generateTeamName() {
|
||||
const teams = ['Frontend', 'Backend', 'DevOps', 'QA', 'Design', 'Analytics', 'Security', 'Mobile'];
|
||||
return teams[Math.floor(this.rng.next() * teams.length)];
|
||||
}
|
||||
|
||||
_generateDivisionName() {
|
||||
const divisions = ['Product', 'Engineering', 'Sales', 'Marketing', 'Operations', 'Finance', 'Legal', 'HR'];
|
||||
return divisions[Math.floor(this.rng.next() * divisions.length)];
|
||||
}
|
||||
|
||||
_generateCompanyName() {
|
||||
const companies = ['AcmeCorp', 'TechGiant', 'InnovateLabs', 'DataFlow', 'CloudSystems', 'NextGen', 'FutureTech', 'SmartSolutions'];
|
||||
return companies[Math.floor(this.rng.next() * companies.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate document tags
|
||||
*/
|
||||
_generateTags() {
|
||||
const allTags = ['important', 'urgent', 'confidential', 'draft', 'final', 'reviewed', 'approved'];
|
||||
const tagCount = Math.floor(this.rng.next() * 3) + 1;
|
||||
const tags = [];
|
||||
|
||||
for (let i = 0; i < tagCount; i++) {
|
||||
const tag = allTags[Math.floor(this.rng.next() * allTags.length)];
|
||||
if (!tags.includes(tag)) {
|
||||
tags.push(tag);
|
||||
}
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
function toContain(arrayLike, expected) {
|
||||
if (!Array.isArray(arrayLike) && typeof arrayLike !== 'string') {
|
||||
throw new assert.AssertionError({ message: 'toContain expects array or string' });
|
||||
}
|
||||
if (typeof arrayLike === 'string') {
|
||||
assert.equal(arrayLike.includes(String(expected)), true);
|
||||
return;
|
||||
}
|
||||
assert.equal(arrayLike.includes(expected), true);
|
||||
}
|
||||
|
||||
function toContainEqual(arrayLike, expected) {
|
||||
assert.equal(Array.isArray(arrayLike), true);
|
||||
const found = arrayLike.some((entry) => {
|
||||
try {
|
||||
if (expected && expected.__matcher === 'objectContaining') {
|
||||
for (const [key, value] of Object.entries(expected.value || {})) {
|
||||
assert.deepEqual(entry?.[key], value);
|
||||
}
|
||||
} else {
|
||||
assert.deepEqual(entry, expected);
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
assert.equal(found, true);
|
||||
}
|
||||
|
||||
function toHaveProperty(value, key) {
|
||||
assert.equal(value != null, true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(value, key), true);
|
||||
}
|
||||
|
||||
export function expect(actual) {
|
||||
const api = {
|
||||
toBe(expected) {
|
||||
assert.equal(actual, expected);
|
||||
},
|
||||
toBeDefined() {
|
||||
assert.notEqual(actual, undefined);
|
||||
},
|
||||
toBeNull() {
|
||||
assert.equal(actual, null);
|
||||
},
|
||||
toBeGreaterThan(expected) {
|
||||
assert.equal(Number(actual) > Number(expected), true);
|
||||
},
|
||||
toBeLessThan(expected) {
|
||||
assert.equal(Number(actual) < Number(expected), true);
|
||||
},
|
||||
toBeLessThanOrEqual(expected) {
|
||||
assert.equal(Number(actual) <= Number(expected), true);
|
||||
},
|
||||
toContain(expected) {
|
||||
toContain(actual, expected);
|
||||
},
|
||||
toContainEqual(expected) {
|
||||
toContainEqual(actual, expected);
|
||||
},
|
||||
toHaveProperty(key) {
|
||||
toHaveProperty(actual, key);
|
||||
},
|
||||
not: {
|
||||
toContain(expected) {
|
||||
if (typeof actual === 'string') {
|
||||
assert.equal(actual.includes(String(expected)), false);
|
||||
return;
|
||||
}
|
||||
assert.equal(Array.isArray(actual), true);
|
||||
assert.equal(actual.includes(expected), false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(api, 'rejects', {
|
||||
get() {
|
||||
return {
|
||||
async toThrow(expectedMessage) {
|
||||
let thrown = null;
|
||||
try {
|
||||
await actual;
|
||||
} catch (error) {
|
||||
thrown = error;
|
||||
}
|
||||
assert.notEqual(thrown, null);
|
||||
if (expectedMessage !== undefined) {
|
||||
const text = String(thrown?.message || thrown || '');
|
||||
assert.equal(text.includes(String(expectedMessage)), true);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
expect.objectContaining = function objectContaining(value) {
|
||||
return {
|
||||
__matcher: 'objectContaining',
|
||||
value
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,506 @@
|
||||
/**
|
||||
* Performance Metrics Collection and Analysis
|
||||
*
|
||||
* Comprehensive performance metrics collection for zanzibar-graph
|
||||
* performance testing with statistical analysis and reporting.
|
||||
*/
|
||||
|
||||
export class PerformanceMetrics {
|
||||
constructor() {
|
||||
this.metrics = new Map();
|
||||
this.startTime = Date.now();
|
||||
this.testCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a performance metric
|
||||
*/
|
||||
record(testName, data) {
|
||||
if (!this.metrics.has(testName)) {
|
||||
this.metrics.set(testName, []);
|
||||
}
|
||||
|
||||
const metric = {
|
||||
timestamp: Date.now(),
|
||||
testName,
|
||||
data,
|
||||
testId: ++this.testCount
|
||||
};
|
||||
|
||||
this.metrics.get(testName).push(metric);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metrics for a specific test
|
||||
*/
|
||||
getMetrics(testName) {
|
||||
return this.metrics.get(testName) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all metrics
|
||||
*/
|
||||
getAllMetrics() {
|
||||
const allMetrics = {};
|
||||
for (const [testName, metrics] of this.metrics) {
|
||||
allMetrics[testName] = metrics;
|
||||
}
|
||||
return allMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate statistical summary for a test
|
||||
*/
|
||||
calculateSummary(testName) {
|
||||
const metrics = this.getMetrics(testName);
|
||||
if (metrics.length === 0) return null;
|
||||
|
||||
const values = metrics.map(m => m.data);
|
||||
|
||||
// Extract numeric values for statistical analysis
|
||||
const numericValues = this._extractNumericValues(values);
|
||||
|
||||
if (numericValues.length === 0) return null;
|
||||
|
||||
return {
|
||||
count: numericValues.length,
|
||||
min: Math.min(...numericValues),
|
||||
max: Math.max(...numericValues),
|
||||
mean: this._calculateMean(numericValues),
|
||||
median: this._calculateMedian(numericValues),
|
||||
p95: this._calculatePercentile(numericValues, 95),
|
||||
p99: this._calculatePercentile(numericValues, 99),
|
||||
stdDev: this._calculateStdDev(numericValues),
|
||||
variance: this._calculateVariance(numericValues)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate comprehensive performance report
|
||||
*/
|
||||
generateReport() {
|
||||
const report = {
|
||||
summary: this._generateSummary(),
|
||||
testResults: {},
|
||||
performanceTargets: this._getPerformanceTargets(),
|
||||
recommendations: this._generateRecommendations(),
|
||||
generatedAt: new Date().toISOString(),
|
||||
duration: Date.now() - this.startTime
|
||||
};
|
||||
|
||||
// Generate test-specific results
|
||||
for (const [testName, metrics] of this.metrics) {
|
||||
report.testResults[testName] = {
|
||||
summary: this.calculateSummary(testName),
|
||||
rawData: metrics,
|
||||
analysis: this._analyzeTest(testName, metrics)
|
||||
};
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate overall summary
|
||||
*/
|
||||
_generateSummary() {
|
||||
const allMetrics = this.getAllMetrics();
|
||||
const totalTests = Object.keys(allMetrics).length;
|
||||
|
||||
// Calculate overall performance metrics
|
||||
let totalLatency = 0;
|
||||
let totalMemory = 0;
|
||||
let totalQueries = 0;
|
||||
let testCount = 0;
|
||||
|
||||
for (const [testName, metrics] of Object.entries(allMetrics)) {
|
||||
for (const metric of metrics) {
|
||||
if (metric.data.avgLatency) {
|
||||
totalLatency += metric.data.avgLatency;
|
||||
testCount++;
|
||||
}
|
||||
if (metric.data.memoryUsed) {
|
||||
totalMemory += metric.data.memoryUsed;
|
||||
}
|
||||
if (metric.data.queryCount) {
|
||||
totalQueries += metric.data.queryCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalTests,
|
||||
avgLatency: testCount > 0 ? totalLatency / testCount : 0,
|
||||
totalMemoryUsage: totalMemory,
|
||||
totalQueries,
|
||||
testCount,
|
||||
duration: Date.now() - this.startTime
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance targets
|
||||
*/
|
||||
_getPerformanceTargets() {
|
||||
return {
|
||||
latency: {
|
||||
avg: 100, // ms
|
||||
p95: 200, // ms
|
||||
p99: 500 // ms
|
||||
},
|
||||
memory: {
|
||||
small: 512, // MB
|
||||
medium: 1024, // MB
|
||||
large: 2048, // MB
|
||||
enterprise: 4096 // MB
|
||||
},
|
||||
throughput: {
|
||||
qps: 1000, // queries per second
|
||||
tps: 500 // transactions per second
|
||||
},
|
||||
cache: {
|
||||
hitRate: 0.8, // 80%
|
||||
evictionRate: 0.1 // 10%
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate performance recommendations
|
||||
*/
|
||||
_generateRecommendations() {
|
||||
const recommendations = [];
|
||||
const summary = this._generateSummary();
|
||||
const targets = this._getPerformanceTargets();
|
||||
|
||||
// Latency recommendations
|
||||
if (summary.avgLatency > targets.latency.avg) {
|
||||
recommendations.push({
|
||||
type: 'latency',
|
||||
severity: 'high',
|
||||
message: `Average latency ${summary.avgLatency}ms exceeds target ${targets.latency.avg}ms`,
|
||||
suggestion: 'Consider optimizing authorization logic or increasing cache size'
|
||||
});
|
||||
}
|
||||
|
||||
// Memory recommendations
|
||||
if (summary.totalMemoryUsage > targets.memory.medium) {
|
||||
recommendations.push({
|
||||
type: 'memory',
|
||||
severity: 'medium',
|
||||
message: `Memory usage ${summary.totalMemoryUsage}MB exceeds target ${targets.memory.medium}MB`,
|
||||
suggestion: 'Consider implementing memory optimization or increasing heap size'
|
||||
});
|
||||
}
|
||||
|
||||
// Throughput recommendations
|
||||
if (summary.totalQueries > 0) {
|
||||
const avgQPS = (summary.totalQueries / summary.duration) * 1000;
|
||||
if (avgQPS < targets.throughput.qps * 0.8) {
|
||||
recommendations.push({
|
||||
type: 'throughput',
|
||||
severity: 'medium',
|
||||
message: `Average QPS ${avgQPS} below target ${targets.throughput.qps}`,
|
||||
suggestion: 'Consider optimizing query performance or increasing concurrency'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze specific test results
|
||||
*/
|
||||
_analyzeTest(testName, metrics) {
|
||||
const analysis = {
|
||||
performance: 'good',
|
||||
issues: [],
|
||||
suggestions: []
|
||||
};
|
||||
|
||||
// Analyze based on test type
|
||||
switch (testName) {
|
||||
case 'graph_loading':
|
||||
this._analyzeGraphLoading(metrics, analysis);
|
||||
break;
|
||||
case 'authorization_qps':
|
||||
this._analyzeAuthorizationQPS(metrics, analysis);
|
||||
break;
|
||||
case 'memory_leak_test':
|
||||
this._analyzeMemoryLeak(metrics, analysis);
|
||||
break;
|
||||
case 'cache_performance':
|
||||
this._analyzeCachePerformance(metrics, analysis);
|
||||
break;
|
||||
default:
|
||||
this._analyzeGeneric(metrics, analysis);
|
||||
}
|
||||
|
||||
return analysis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze graph loading performance
|
||||
*/
|
||||
_analyzeGraphLoading(metrics, analysis) {
|
||||
for (const metric of metrics) {
|
||||
const data = metric.data;
|
||||
|
||||
if (data.loadTime > 30000) {
|
||||
analysis.issues.push('Graph loading time exceeds 30s limit');
|
||||
analysis.suggestions.push('Consider optimizing graph construction or using lazy loading');
|
||||
analysis.performance = 'poor';
|
||||
}
|
||||
|
||||
if (data.memoryUsed > 1024) {
|
||||
analysis.issues.push('Memory usage exceeds 1GB limit');
|
||||
analysis.suggestions.push('Consider implementing memory optimization or reducing graph size');
|
||||
analysis.performance = 'poor';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze authorization QPS performance
|
||||
*/
|
||||
_analyzeAuthorizationQPS(metrics, analysis) {
|
||||
for (const metric of metrics) {
|
||||
const data = metric.data;
|
||||
|
||||
if (data.actualQPS < data.targetQPS * 0.8) {
|
||||
analysis.issues.push(`QPS ${data.actualQPS} below 80% of target ${data.targetQPS}`);
|
||||
analysis.suggestions.push('Consider optimizing authorization logic or increasing concurrency');
|
||||
analysis.performance = 'poor';
|
||||
}
|
||||
|
||||
if (data.avgLatency > 100) {
|
||||
analysis.issues.push(`Average latency ${data.avgLatency}ms exceeds 100ms limit`);
|
||||
analysis.suggestions.push('Consider optimizing query performance or increasing cache size');
|
||||
analysis.performance = 'poor';
|
||||
}
|
||||
|
||||
if (data.p95Latency > 200) {
|
||||
analysis.issues.push(`P95 latency ${data.p95Latency}ms exceeds 200ms limit`);
|
||||
analysis.suggestions.push('Consider optimizing worst-case performance or reducing query complexity');
|
||||
analysis.performance = 'poor';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze memory leak test results
|
||||
*/
|
||||
_analyzeMemoryLeak(metrics, analysis) {
|
||||
for (const metric of metrics) {
|
||||
const data = metric.data;
|
||||
|
||||
if (data.totalGrowth > 200) {
|
||||
analysis.issues.push(`Memory growth ${data.totalGrowth}MB exceeds 200MB limit`);
|
||||
analysis.suggestions.push('Investigate potential memory leaks in authorization logic');
|
||||
analysis.performance = 'poor';
|
||||
}
|
||||
|
||||
if (data.totalGrowth > 100) {
|
||||
analysis.issues.push(`Memory growth ${data.totalGrowth}MB exceeds 100MB limit`);
|
||||
analysis.suggestions.push('Monitor memory usage and consider implementing garbage collection');
|
||||
analysis.performance = 'fair';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze cache performance
|
||||
*/
|
||||
_analyzeCachePerformance(metrics, analysis) {
|
||||
for (const metric of metrics) {
|
||||
const data = metric.data;
|
||||
|
||||
if (data.speedup < 1.5) {
|
||||
analysis.issues.push(`Cache speedup ${data.speedup}x below 1.5x threshold`);
|
||||
analysis.suggestions.push('Consider optimizing cache implementation or increasing cache size');
|
||||
analysis.performance = 'poor';
|
||||
}
|
||||
|
||||
if (data.speedup < 2.0) {
|
||||
analysis.issues.push(`Cache speedup ${data.speedup}x below 2.0x threshold`);
|
||||
analysis.suggestions.push('Consider optimizing cache hit rate or cache eviction strategy');
|
||||
analysis.performance = 'fair';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic analysis for unknown test types
|
||||
*/
|
||||
_analyzeGeneric(metrics, analysis) {
|
||||
const summary = this.calculateSummary(metrics[0]?.testName);
|
||||
if (!summary) return;
|
||||
|
||||
if (summary.mean > 1000) {
|
||||
analysis.issues.push(`Average performance ${summary.mean}ms exceeds 1000ms threshold`);
|
||||
analysis.suggestions.push('Consider optimizing performance or reducing complexity');
|
||||
analysis.performance = 'poor';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract numeric values from metric data
|
||||
*/
|
||||
_extractNumericValues(values) {
|
||||
const numericValues = [];
|
||||
|
||||
for (const value of values) {
|
||||
if (typeof value === 'number') {
|
||||
numericValues.push(value);
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// Extract numeric values from objects
|
||||
for (const [key, val] of Object.entries(value)) {
|
||||
if (typeof val === 'number') {
|
||||
numericValues.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return numericValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate mean
|
||||
*/
|
||||
_calculateMean(values) {
|
||||
return values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate median
|
||||
*/
|
||||
_calculateMedian(values) {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const mid = Math.floor(sorted.length / 2);
|
||||
return sorted.length % 2 === 0
|
||||
? (sorted[mid - 1] + sorted[mid]) / 2
|
||||
: sorted[mid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentile
|
||||
*/
|
||||
_calculatePercentile(values, percentile) {
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const index = Math.ceil((percentile / 100) * sorted.length) - 1;
|
||||
return sorted[Math.max(0, index)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate standard deviation
|
||||
*/
|
||||
_calculateStdDev(values) {
|
||||
const mean = this._calculateMean(values);
|
||||
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
||||
return Math.sqrt(variance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate variance
|
||||
*/
|
||||
_calculateVariance(values) {
|
||||
const mean = this._calculateMean(values);
|
||||
return values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export metrics to JSON
|
||||
*/
|
||||
exportToJSON() {
|
||||
return JSON.stringify(this.generateReport(), null, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export metrics to CSV
|
||||
*/
|
||||
exportToCSV() {
|
||||
const csv = [];
|
||||
csv.push('TestName,Timestamp,TestId,Data');
|
||||
|
||||
for (const [testName, metrics] of this.metrics) {
|
||||
for (const metric of metrics) {
|
||||
csv.push(`${testName},${metric.timestamp},${metric.testId},"${JSON.stringify(metric.data)}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return csv.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all metrics
|
||||
*/
|
||||
clear() {
|
||||
this.metrics.clear();
|
||||
this.startTime = Date.now();
|
||||
this.testCount = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metrics for a specific time range
|
||||
*/
|
||||
getMetricsInRange(startTime, endTime) {
|
||||
const filteredMetrics = new Map();
|
||||
|
||||
for (const [testName, metrics] of this.metrics) {
|
||||
const filtered = metrics.filter(m =>
|
||||
m.timestamp >= startTime && m.timestamp <= endTime
|
||||
);
|
||||
if (filtered.length > 0) {
|
||||
filteredMetrics.set(testName, filtered);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredMetrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance trends over time
|
||||
*/
|
||||
getPerformanceTrends(testName, windowSize = 1000) {
|
||||
const metrics = this.getMetrics(testName);
|
||||
if (metrics.length === 0) return null;
|
||||
|
||||
const trends = [];
|
||||
const window = Math.min(windowSize, metrics.length);
|
||||
|
||||
for (let i = window; i <= metrics.length; i++) {
|
||||
const windowMetrics = metrics.slice(i - window, i);
|
||||
const trend = this._calculateTrend(windowMetrics);
|
||||
trends.push({
|
||||
timestamp: metrics[i - 1].timestamp,
|
||||
trend: trend
|
||||
});
|
||||
}
|
||||
|
||||
return trends;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate trend for a window of metrics
|
||||
*/
|
||||
_calculateTrend(metrics) {
|
||||
if (metrics.length < 2) return null;
|
||||
|
||||
const values = this._extractNumericValues(metrics.map(m => m.data));
|
||||
if (values.length < 2) return null;
|
||||
|
||||
const firstHalf = values.slice(0, Math.floor(values.length / 2));
|
||||
const secondHalf = values.slice(Math.floor(values.length / 2));
|
||||
|
||||
const firstMean = this._calculateMean(firstHalf);
|
||||
const secondMean = this._calculateMean(secondHalf);
|
||||
|
||||
return {
|
||||
direction: secondMean > firstMean ? 'increasing' : 'decreasing',
|
||||
change: secondMean - firstMean,
|
||||
changePercent: ((secondMean - firstMean) / firstMean) * 100
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
function randomInt(n) {
|
||||
return Math.floor(Math.random() * n);
|
||||
}
|
||||
|
||||
function generateBusinessGraph(numUsers = 5000, numGroups = 200, numDocs = 2000) {
|
||||
const nodes = [];
|
||||
const relations = [];
|
||||
const adminKeys = [];
|
||||
const newEmployeeKeys = [];
|
||||
const testUserKeys = [];
|
||||
|
||||
console.log('🏗️ Generating realistic business authorization graph...');
|
||||
console.log(`📊 Scale: ${numUsers} users, ${numGroups} groups, ${numDocs} documents`);
|
||||
|
||||
// Add organizational groups with realistic structure
|
||||
const departments = ['engineering', 'product', 'design', 'marketing', 'sales', 'hr', 'finance', 'legal', 'operations', 'security', 'data', 'research'];
|
||||
const levels = ['intern', 'junior', 'mid', 'senior', 'staff', 'principal', 'lead', 'manager', 'director', 'vp'];
|
||||
const clearanceLevels = ['public', 'internal', 'confidential', 'restricted', 'secret', 'top_secret'];
|
||||
const documentTypes = ['manual', 'specification', 'budget', 'contract', 'policy', 'report', 'plan'];
|
||||
const currencies = ['usd', 'eur', 'gbp'];
|
||||
|
||||
console.log('📁 Creating organizational structure...');
|
||||
|
||||
// Create department groups
|
||||
for (let i = 0; i < departments.length; i++) {
|
||||
const dept = departments[i];
|
||||
nodes.push({ key: `group:${dept}`, type: 'group' });
|
||||
|
||||
// Create level-based subgroups within each department
|
||||
for (let j = 0; j < levels.length; j++) {
|
||||
const level = levels[j];
|
||||
const groupKey = `group:${dept}_${level}`;
|
||||
nodes.push({ key: groupKey, type: 'group' });
|
||||
|
||||
// Create hierarchy: department contains level groups
|
||||
relations.push({ src: `group:${dept}`, rel: 'contains', dst: groupKey });
|
||||
|
||||
// Create management hierarchy (for computed_userset rules)
|
||||
if (j < levels.length - 1) {
|
||||
const higherLevel = levels[j + 1];
|
||||
const higherGroupKey = `group:${dept}_${higherLevel}`;
|
||||
relations.push({ src: higherGroupKey, rel: 'manages', dst: groupKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add projects and teams
|
||||
const projectTypes = ['project', 'team', 'committee', 'guild', 'workgroup'];
|
||||
for (let i = 0; i < numGroups - (departments.length * (levels.length + 1)); i++) {
|
||||
const type = projectTypes[i % projectTypes.length];
|
||||
nodes.push({ key: `group:${type}_${i}`, type: 'group' });
|
||||
}
|
||||
|
||||
// Add clearance levels, document types, and currencies
|
||||
for (const clearance of clearanceLevels) {
|
||||
nodes.push({ key: `clearance:${clearance}`, type: 'clearance' });
|
||||
}
|
||||
for (const docType of documentTypes) {
|
||||
nodes.push({ key: `doctype:${docType}`, type: 'document_type' });
|
||||
}
|
||||
for (const currency of currencies) {
|
||||
nodes.push({ key: `currency:${currency}`, type: 'currency' });
|
||||
}
|
||||
|
||||
// Add budget and cost tracking nodes for financial rules
|
||||
for (let i = 0; i < departments.length; i++) {
|
||||
const dept = departments[i];
|
||||
nodes.push({ key: `budget:${dept}_2024`, type: 'budget' });
|
||||
nodes.push({ key: `account:${dept}_operational`, type: 'account' });
|
||||
}
|
||||
|
||||
console.log('👥 Creating users with realistic attributes...');
|
||||
|
||||
// Add users with realistic department/level assignments
|
||||
for (let i = 0; i < numUsers; i++) {
|
||||
const userKey = `user:${i}`;
|
||||
nodes.push({ key: userKey, type: 'user' });
|
||||
|
||||
// Assign to department and level with realistic distribution
|
||||
const dept = departments[randomInt(departments.length)];
|
||||
const level = levels[Math.min(levels.length - 1, Math.floor(Math.abs(gaussianRandom()) * 3) + 2)]; // Bias toward mid-level
|
||||
const deptGroupKey = `group:${dept}`;
|
||||
const levelGroupKey = `group:${dept}_${level}`;
|
||||
|
||||
relations.push({ src: userKey, rel: 'member_of', dst: deptGroupKey });
|
||||
relations.push({ src: userKey, rel: 'member_of', dst: levelGroupKey });
|
||||
|
||||
// Add user accounts with balances (for relational_comparator rules)
|
||||
const accountKey = `account:user_${i}`;
|
||||
nodes.push({ key: accountKey, type: 'account' });
|
||||
relations.push({ src: userKey, rel: 'owns_account', dst: accountKey });
|
||||
|
||||
// Assign random balance (10K - 100K USD)
|
||||
const balance = 10000 + randomInt(90000);
|
||||
relations.push({ src: accountKey, rel: 'has_balance', dst: 'currency:usd', value: balance });
|
||||
|
||||
// Assign clearance level (higher levels get higher clearance)
|
||||
const clearanceIndex = Math.min(
|
||||
clearanceLevels.length - 1,
|
||||
levels.indexOf(level) + randomInt(3)
|
||||
);
|
||||
const clearance = clearanceLevels[clearanceIndex];
|
||||
relations.push({ src: userKey, rel: 'has_clearance', dst: `clearance:${clearance}` });
|
||||
|
||||
// Users join multiple cross-functional groups (realistic for large orgs)
|
||||
const numGroups = Math.floor(Math.abs(gaussianRandom()) * 3) + 1; // 1-4 groups
|
||||
for (let g = 0; g < numGroups; g++) {
|
||||
if (Math.random() < 0.4) {
|
||||
const projectGroup = `group:project_${randomInt(numGroups - (departments.length * (levels.length + 1)))}`;
|
||||
relations.push({ src: userKey, rel: 'member_of', dst: projectGroup });
|
||||
}
|
||||
}
|
||||
|
||||
if (i % 500 === 0) console.log(` 👤 Added ${i} users...`);
|
||||
}
|
||||
|
||||
// Add system admins with full access
|
||||
console.log('🔐 Creating system administrators...');
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const adminKey = `admin:${i}`;
|
||||
nodes.push({ key: adminKey, type: 'admin' });
|
||||
adminKeys.push(adminKey);
|
||||
|
||||
// Admins have top secret clearance and access to all groups
|
||||
relations.push({ src: adminKey, rel: 'has_clearance', dst: 'clearance:top_secret' });
|
||||
for (const dept of departments) {
|
||||
relations.push({ src: adminKey, rel: 'superadmin', dst: `group:${dept}` });
|
||||
}
|
||||
|
||||
// Admins have large budgets
|
||||
const adminAccount = `account:admin_${i}`;
|
||||
nodes.push({ key: adminAccount, type: 'account' });
|
||||
relations.push({ src: adminKey, rel: 'owns_account', dst: adminAccount });
|
||||
relations.push({ src: adminAccount, rel: 'has_balance', dst: 'currency:usd', value: 1000000 });
|
||||
}
|
||||
|
||||
// Add new employees (for inference testing - no direct access)
|
||||
console.log('🆕 Creating new employees for inference testing...');
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const newEmpKey = `newbie:${i}`;
|
||||
nodes.push({ key: newEmpKey, type: 'user' });
|
||||
newEmployeeKeys.push(newEmpKey);
|
||||
|
||||
// Give them basic attributes but limited document access
|
||||
const dept = departments[randomInt(departments.length)];
|
||||
const level = 'intern'; // New employees start as interns
|
||||
relations.push({ src: newEmpKey, rel: 'member_of', dst: `group:${dept}` });
|
||||
relations.push({ src: newEmpKey, rel: 'member_of', dst: `group:${dept}_${level}` });
|
||||
relations.push({ src: newEmpKey, rel: 'has_clearance', dst: 'clearance:internal' });
|
||||
|
||||
// Give them small budgets
|
||||
const newEmpAccount = `account:newbie_${i}`;
|
||||
nodes.push({ key: newEmpAccount, type: 'account' });
|
||||
relations.push({ src: newEmpKey, rel: 'owns_account', dst: newEmpAccount });
|
||||
relations.push({ src: newEmpAccount, rel: 'has_balance', dst: 'currency:usd', value: 5000 + randomInt(10000) });
|
||||
}
|
||||
|
||||
// Add test users with specific patterns for inference validation
|
||||
console.log('🧪 Creating test users for inference validation...');
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const testUserKey = `test_user:${i}`;
|
||||
nodes.push({ key: testUserKey, type: 'user' });
|
||||
testUserKeys.push(testUserKey);
|
||||
|
||||
// Create similar patterns to existing users
|
||||
const dept = departments[i % departments.length];
|
||||
const level = levels[Math.floor(i / departments.length) % levels.length];
|
||||
relations.push({ src: testUserKey, rel: 'member_of', dst: `group:${dept}` });
|
||||
relations.push({ src: testUserKey, rel: 'member_of', dst: `group:${dept}_${level}` });
|
||||
|
||||
const clearanceIndex = Math.min(clearanceLevels.length - 1, levels.indexOf(level) + 1);
|
||||
relations.push({ src: testUserKey, rel: 'has_clearance', dst: `clearance:${clearanceLevels[clearanceIndex]}` });
|
||||
|
||||
// Give them moderate budgets
|
||||
const testAccount = `account:test_${i}`;
|
||||
nodes.push({ key: testAccount, type: 'account' });
|
||||
relations.push({ src: testUserKey, rel: 'owns_account', dst: testAccount });
|
||||
relations.push({ src: testAccount, rel: 'has_balance', dst: 'currency:usd', value: 15000 + randomInt(50000) });
|
||||
}
|
||||
|
||||
console.log('📄 Creating documents with complex access patterns...');
|
||||
|
||||
// Create documents with rich metadata
|
||||
const docKeys = [];
|
||||
for (let i = 0; i < numDocs; i++) {
|
||||
const docKey = `doc:${i}`;
|
||||
nodes.push({ key: docKey, type: 'doc' });
|
||||
docKeys.push(docKey);
|
||||
|
||||
// Classify document with bias toward lower classifications
|
||||
const classificationIndex = Math.min(
|
||||
clearanceLevels.length - 1,
|
||||
Math.floor(Math.abs(gaussianRandom()) * 2) + 1
|
||||
);
|
||||
const classification = clearanceLevels[classificationIndex];
|
||||
relations.push({ src: docKey, rel: 'classified_as', dst: `clearance:${classification}` });
|
||||
|
||||
// Assign document type
|
||||
const docType = documentTypes[randomInt(documentTypes.length)];
|
||||
relations.push({ src: docKey, rel: 'has_type', dst: `doctype:${docType}` });
|
||||
|
||||
// Assign document to departments
|
||||
const owningDept = departments[randomInt(departments.length)];
|
||||
relations.push({ src: docKey, rel: 'owned_by', dst: `group:${owningDept}` });
|
||||
|
||||
// Add cost information for budget documents (for relational_comparator rules)
|
||||
if (docType === 'budget' || docType === 'contract') {
|
||||
const cost = 1000 + randomInt(50000); // $1K - $50K
|
||||
relations.push({ src: docKey, rel: 'has_cost', dst: 'currency:usd', value: cost });
|
||||
}
|
||||
}
|
||||
|
||||
// Generate realistic access patterns
|
||||
console.log('🔥 Generating complex authorization patterns...');
|
||||
|
||||
// Sort documents by "popularity" with more realistic distribution
|
||||
const superPopularDocs = docKeys.slice(0, Math.floor(numDocs * 0.05)); // Top 5% - super popular
|
||||
const popularDocs = docKeys.slice(Math.floor(numDocs * 0.05), Math.floor(numDocs * 0.25)); // Next 20% - popular
|
||||
const commonDocs = docKeys.slice(Math.floor(numDocs * 0.25), Math.floor(numDocs * 0.70)); // Next 45% - common
|
||||
const rareDocs = docKeys.slice(Math.floor(numDocs * 0.70)); // Bottom 30% - rare
|
||||
|
||||
console.log(` 📊 Super popular: ${superPopularDocs.length}, Popular: ${popularDocs.length}, Common: ${commonDocs.length}, Rare: ${rareDocs.length}`);
|
||||
|
||||
// Get all regular users (not admins/newbies/test users)
|
||||
const regularUsers = Array.from({ length: numUsers }, (_, i) => `user:${i}`);
|
||||
|
||||
// Create GROUP-BASED access patterns (for tuple_to_userset rules)
|
||||
console.log('👥 Creating group-based access patterns...');
|
||||
for (const dept of departments) {
|
||||
const deptGroupKey = `group:${dept}`;
|
||||
|
||||
// Department groups get access to documents they own
|
||||
const deptDocs = docKeys.filter((_, i) => i % departments.length === departments.indexOf(dept));
|
||||
for (const docKey of deptDocs.slice(0, 20)) { // Limit for performance
|
||||
relations.push({ src: deptGroupKey, rel: 'can_read', dst: docKey });
|
||||
}
|
||||
}
|
||||
|
||||
// SUPER POPULAR DOCS: Mix of direct and group access
|
||||
for (const docKey of superPopularDocs) {
|
||||
// 50% direct access, 50% group access
|
||||
if (Math.random() < 0.5) {
|
||||
// Direct user access
|
||||
const numUsersWithAccess = Math.floor(regularUsers.length * 0.4);
|
||||
const usersWithAccess = new Set();
|
||||
|
||||
for (let i = 0; i < numUsersWithAccess; i++) {
|
||||
const userIndex = Math.floor(Math.random() * regularUsers.length);
|
||||
const userKey = regularUsers[userIndex];
|
||||
if (!usersWithAccess.has(userKey)) {
|
||||
usersWithAccess.add(userKey);
|
||||
relations.push({ src: userKey, rel: 'can_read', dst: docKey });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Group-based access (will require tuple_to_userset rule)
|
||||
const owningDept = departments[randomInt(departments.length)];
|
||||
relations.push({ src: `group:${owningDept}`, rel: 'can_read', dst: docKey });
|
||||
}
|
||||
}
|
||||
|
||||
// POPULAR DOCS: Mostly group access
|
||||
for (const docKey of popularDocs) {
|
||||
if (Math.random() < 0.8) {
|
||||
// Group access
|
||||
const owningDept = departments[randomInt(departments.length)];
|
||||
relations.push({ src: `group:${owningDept}`, rel: 'can_read', dst: docKey });
|
||||
} else {
|
||||
// Limited direct access
|
||||
const numUsersWithAccess = Math.floor(regularUsers.length * 0.1);
|
||||
const usersWithAccess = new Set();
|
||||
|
||||
for (let i = 0; i < numUsersWithAccess; i++) {
|
||||
const userIndex = Math.floor(Math.random() * regularUsers.length);
|
||||
const userKey = regularUsers[userIndex];
|
||||
if (!usersWithAccess.has(userKey)) {
|
||||
usersWithAccess.add(userKey);
|
||||
relations.push({ src: userKey, rel: 'can_read', dst: docKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// COMMON and RARE DOCS: Mostly group access
|
||||
for (const docKey of [...commonDocs, ...rareDocs]) {
|
||||
if (Math.random() < 0.9) {
|
||||
// Group access
|
||||
const owningDept = departments[randomInt(departments.length)];
|
||||
relations.push({ src: `group:${owningDept}`, rel: 'can_read', dst: docKey });
|
||||
}
|
||||
}
|
||||
|
||||
// Give admins direct access to all documents
|
||||
console.log('🔐 Granting admin access...');
|
||||
for (const adminKey of adminKeys) {
|
||||
for (const docKey of docKeys) {
|
||||
relations.push({ src: adminKey, rel: 'can_read', dst: docKey });
|
||||
}
|
||||
}
|
||||
|
||||
// Create patterns for new employees and test users
|
||||
console.log('🎯 Creating inference-friendly patterns...');
|
||||
|
||||
// Give new employees limited direct access
|
||||
for (const newEmpKey of newEmployeeKeys) {
|
||||
const docsToAccess = Math.floor(superPopularDocs.length * 0.1);
|
||||
const accessedDocs = new Set();
|
||||
for (let i = 0; i < docsToAccess; i++) {
|
||||
const docKey = superPopularDocs[Math.floor(Math.random() * superPopularDocs.length)];
|
||||
if (!accessedDocs.has(docKey)) {
|
||||
accessedDocs.add(docKey);
|
||||
relations.push({ src: newEmpKey, rel: 'can_read', dst: docKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Give test users mixed access patterns
|
||||
for (const testUserKey of testUserKeys) {
|
||||
const superPopularAccess = Math.floor(superPopularDocs.length * 0.2);
|
||||
const accessedDocs = new Set();
|
||||
|
||||
for (let i = 0; i < superPopularAccess; i++) {
|
||||
const docKey = superPopularDocs[Math.floor(Math.random() * superPopularDocs.length)];
|
||||
if (!accessedDocs.has(docKey)) {
|
||||
accessedDocs.add(docKey);
|
||||
relations.push({ src: testUserKey, rel: 'can_read', dst: docKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Complex graph generation complete!');
|
||||
console.log(`📊 Final stats:`);
|
||||
console.log(` • ${nodes.length} nodes`);
|
||||
console.log(` • ${relations.length} relations`);
|
||||
console.log(` • ${adminKeys.length} admins`);
|
||||
console.log(` • ${newEmployeeKeys.length} new employees`);
|
||||
console.log(` • ${testUserKeys.length} test users`);
|
||||
console.log(` • ${departments.length} departments with ${levels.length} levels each`);
|
||||
console.log(` • ${clearanceLevels.length} clearance levels`);
|
||||
console.log(` • ${documentTypes.length} document types`);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
relations,
|
||||
adminKeys,
|
||||
newEmployeeKeys,
|
||||
testUserKeys,
|
||||
departments,
|
||||
levels,
|
||||
clearanceLevels,
|
||||
documentTypes,
|
||||
currencies
|
||||
};
|
||||
}
|
||||
|
||||
// Gaussian random number generator for more realistic distributions
|
||||
function gaussianRandom() {
|
||||
let u = 0, v = 0;
|
||||
while(u === 0) u = Math.random(); // Converting [0,1) to (0,1)
|
||||
while(v === 0) v = Math.random();
|
||||
return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
|
||||
}
|
||||
|
||||
// Generate different sized graphs for testing - MUCH BIGGER
|
||||
const configs = [
|
||||
{ name: 'small', users: 1000, groups: 100, docs: 500 },
|
||||
{ name: 'medium', users: 5000, groups: 250, docs: 2000 },
|
||||
{ name: 'large', users: 10000, groups: 500, docs: 5000 },
|
||||
{ name: 'huge', users: 25000, groups: 1000, docs: 10000 }
|
||||
];
|
||||
|
||||
const configName = process.argv[2] || 'medium';
|
||||
const config = configs.find(c => c.name === configName) || configs[1];
|
||||
|
||||
console.log(`🎯 Generating ${config.name} graph configuration...`);
|
||||
const data = generateBusinessGraph(config.users, config.groups, config.docs);
|
||||
|
||||
const filename = `big-graph-data-${config.name}.json`;
|
||||
fs.writeFileSync(filename, JSON.stringify(data, null, 2));
|
||||
console.log(`💾 Saved to ${filename}`);
|
||||
console.log(`🚀 Run: node big-graph.test.js ${config.name}`);
|
||||
Reference in New Issue
Block a user