import { Arbiter } from '../src/core/Arbiter.js'; function parseArgs(argv) { const args = new Map(); for (let i = 2; i < argv.length; i++) { const value = argv[i]; if (!value.startsWith('--')) continue; const [key, inline] = value.slice(2).split('='); if (inline !== undefined) { args.set(key, inline); continue; } const next = argv[i + 1]; if (next && !next.startsWith('--')) { args.set(key, next); i++; } else { args.set(key, true); } } return args; } function createRng(seed) { if (seed === null || seed === undefined) { return Math.random; } let state = seed >>> 0; return () => { state = (1664525 * state + 1013904223) >>> 0; return state / 0x100000000; }; } function buildZipfSampler(size, skew, rng) { const cumulative = new Array(size); let sum = 0; for (let i = 1; i <= size; i++) { sum += 1 / Math.pow(i, skew); cumulative[i - 1] = sum; } return () => { const target = rng() * sum; let low = 0; let high = cumulative.length - 1; while (low < high) { const mid = Math.floor((low + high) / 2); if (target <= cumulative[mid]) { high = mid; } else { low = mid + 1; } } return low; }; } function pickRandom(items, rng) { const index = Math.floor(rng() * items.length); return items[index]; } function addToMapSet(map, key, value) { if (!map.has(key)) { map.set(key, new Set()); } map.get(key).add(value); } function basicSimilarity(key1, key2) { if (key1 === key2) return 1.0; const type1 = key1.split(':')[0]; const type2 = key2.split(':')[0]; if (type1 === type2) { const id1 = key1.split(':')[1] || ''; const id2 = key2.split(':')[1] || ''; const num1 = parseInt(id1, 10); const num2 = parseInt(id2, 10); if (!isNaN(num1) && !isNaN(num2)) { const diff = Math.abs(num1 - num2); if (diff === 0) return 1.0; if (diff <= 5) return 0.8; if (diff <= 20) return 0.6; if (diff <= 100) return 0.4; return 0.2; } const maxLen = Math.max(id1.length, id2.length); const minLen = Math.min(id1.length, id2.length); let matches = 0; for (let i = 0; i < minLen; i++) { if (id1[i] === id2[i]) matches++; } return 0.5 + (maxLen > 0 ? (matches / maxLen) * 0.3 : 0); } return 0.1; } function convertToQualitative(value, scale) { let closestValue = scale[0]; let minDistance = Math.abs(value - scale[0]); for (const scaleValue of scale) { const distance = Math.abs(value - scaleValue); if (distance < minDistance) { minDistance = distance; closestValue = scaleValue; } } return closestValue; } function estimateHitRate(frequencies, cacheSize) { if (!frequencies.size || cacheSize <= 0) return 0; const counts = Array.from(frequencies.values()).sort((a, b) => b - a); let total = 0; for (const count of counts) total += count; let top = 0; for (let i = 0; i < Math.min(cacheSize, counts.length); i++) { top += counts[i]; } return top / total; } function percentile(sortedValues, percentileValue) { if (sortedValues.length === 0) return 0; const idx = Math.min( sortedValues.length - 1, Math.max(0, Math.ceil((percentileValue / 100) * sortedValues.length) - 1) ); return sortedValues[idx]; } function buildGraph({ users, groups, resources, memberPerUser, accessPerGroup, viewerPerUser, ownerPerUser, blockedPerUser, riskPerUser, linkPerUser, linkPerResource, linkReversePerResource, riskLimitBase, riskLimitVariance, trustPerUser, trustRequirementBase, trustRequirementVariance, rng }) { const arbiter = new Arbiter(); const userIds = []; const groupIds = []; const resourceIds = []; for (let i = 0; i < users; i++) { userIds.push(arbiter.addNode(`user:${i}`, 'user')); } for (let i = 0; i < groups; i++) { groupIds.push(arbiter.addNode(`group:${i}`, 'group')); } for (let i = 0; i < resources; i++) { resourceIds.push(arbiter.addNode(`resource:${i}`, 'resource')); } const viewerSet = new Set(); const ownerSet = new Set(); const blockedSet = new Set(); const memberKeySet = new Set(); const groupAccessKeySet = new Set(); const riskScoreMap = new Map(); const linkKeySet = new Set(); const resourceAccessKeySet = new Set(); const memberMap = new Map(); const groupAccessMap = new Map(); const viewerByResource = new Map(); const viewerByUser = new Map(); const linkAdjacency = new Map(); const linkReverseAdjacency = new Map(); const resourceAccessMap = new Map(); const getKey = (nodeId) => arbiter.keyByNodeId.get(nodeId); const addDirectEdge = (relation, srcId, dstId, keySet, value) => { const key = arbiter.keyManager.createCompositeKey(srcId, relation, dstId); if (keySet.has(key)) return false; keySet.add(key); const srcKey = getKey(srcId); const dstKey = getKey(dstId); if (value !== undefined) { arbiter.addRelation(srcKey, relation, dstKey, 1.0, { value }); riskScoreMap.set(key, value); } else if (relation === 'member') { arbiter.addRelation(srcKey, relation, dstKey, 1.0); } else if (relation === 'group_access') { arbiter.addRelation(srcKey, relation, dstKey, 1.0); arbiter.addRelation(dstKey, 'resource_access', srcKey, 1.0); addToMapSet(resourceAccessMap, dstId, srcId); resourceAccessKeySet.add( arbiter.keyManager.createCompositeKey(dstId, 'resource_access', srcId) ); } else { arbiter.addRelation(srcKey, relation, dstKey, 1.0); } return true; }; const addLinkEdge = (srcId, dstId) => { const srcKey = getKey(srcId); const dstKey = getKey(dstId); const key = arbiter.keyManager.createCompositeKey(srcId, 'link', dstId); arbiter.addRelation(srcKey, 'link', dstKey, 1.0); linkKeySet.add(key); addToMapSet(linkAdjacency, srcId, dstId); addToMapSet(linkReverseAdjacency, dstId, srcId); return key; }; for (const userId of userIds) { const membershipCount = Math.max(1, Math.round(memberPerUser)); for (let i = 0; i < membershipCount; i++) { const groupId = pickRandom(groupIds, rng); const key = arbiter.keyManager.createCompositeKey(userId, 'member', groupId); if (memberKeySet.has(key)) continue; memberKeySet.add(key); arbiter.addRelation(getKey(userId), 'member', getKey(groupId), 1.0); addToMapSet(memberMap, userId, groupId); } } for (const groupId of groupIds) { const accessCount = Math.max(1, Math.round(accessPerGroup)); for (let i = 0; i < accessCount; i++) { const resourceId = pickRandom(resourceIds, rng); const key = arbiter.keyManager.createCompositeKey(groupId, 'group_access', resourceId); if (groupAccessKeySet.has(key)) continue; groupAccessKeySet.add(key); arbiter.addRelation(getKey(groupId), 'group_access', getKey(resourceId), 1.0); arbiter.addRelation(getKey(resourceId), 'resource_access', getKey(groupId), 1.0); resourceAccessKeySet.add( arbiter.keyManager.createCompositeKey(resourceId, 'resource_access', groupId) ); addToMapSet(resourceAccessMap, resourceId, groupId); addToMapSet(groupAccessMap, groupId, resourceId); } } const addRandomUserResourceEdges = (relation, count, keySet) => { let added = 0; const maxAttempts = count * 3; for (let attempt = 0; attempt < maxAttempts && added < count; attempt++) { const userId = pickRandom(userIds, rng); const resourceId = pickRandom(resourceIds, rng); if (addDirectEdge(relation, userId, resourceId, keySet)) { added++; } } }; addRandomUserResourceEdges('viewer', Math.round(users * viewerPerUser), viewerSet); addRandomUserResourceEdges('owner', Math.round(users * ownerPerUser), ownerSet); addRandomUserResourceEdges('blocked', Math.round(users * blockedPerUser), blockedSet); for (const key of viewerSet) { const [userId, , resourceId] = JSON.parse(key); addToMapSet(viewerByResource, resourceId, userId); addToMapSet(viewerByUser, userId, resourceId); } const riskCount = Math.round(users * riskPerUser); let riskAdded = 0; const riskAttempts = riskCount * 3; for (let attempt = 0; attempt < riskAttempts && riskAdded < riskCount; attempt++) { const userId = pickRandom(userIds, rng); const resourceId = pickRandom(resourceIds, rng); const key = arbiter.keyManager.createCompositeKey(userId, 'risk_score', resourceId); if (riskScoreMap.has(key)) continue; const value = rng() * 100; arbiter.addRelation(getKey(userId), 'risk_score', getKey(resourceId), 1.0, { value }); riskScoreMap.set(key, value); riskAdded++; } const riskLimitMap = new Map(); for (const resourceId of resourceIds) { const limit = riskLimitBase + rng() * riskLimitVariance; const resourceKey = getKey(resourceId); const key = arbiter.keyManager.createCompositeKey(resourceId, 'risk_limit', resourceId); arbiter.addRelation(resourceKey, 'risk_limit', resourceKey, 1.0, { value: limit }); riskLimitMap.set(key, limit); } const trustLevelMap = new Map(); const trustRequirementMap = new Map(); const trustCount = Math.max(1, Math.round(users * trustPerUser)); let trustAdded = 0; const trustAttempts = trustCount * 3; for (let attempt = 0; attempt < trustAttempts && trustAdded < trustCount; attempt++) { const userId = pickRandom(userIds, rng); const resourceId = pickRandom(resourceIds, rng); const value = rng(); const key = arbiter.keyManager.createCompositeKey(userId, 'trust_level', resourceId); if (trustLevelMap.has(key)) continue; arbiter.addRelation(getKey(userId), 'trust_level', getKey(resourceId), 1.0, { value }); trustLevelMap.set(key, value); trustAdded++; } for (const resourceId of resourceIds) { const value = trustRequirementBase + rng() * trustRequirementVariance; const resourceKey = getKey(resourceId); const key = arbiter.keyManager.createCompositeKey(resourceId, 'trust_requirement', resourceId); arbiter.addRelation(resourceKey, 'trust_requirement', resourceKey, 1.0, { value }); trustRequirementMap.set(key, value); } const linkEdgesCount = Math.max(1, Math.round(users * linkPerUser)); for (let i = 0; i < linkEdgesCount; i++) { const userId = pickRandom(userIds, rng); const resourceId = pickRandom(resourceIds, rng); addLinkEdge(userId, resourceId); } const resourceLinksCount = Math.max(1, Math.round(resources * linkPerResource)); for (let i = 0; i < resourceLinksCount; i++) { const srcResource = pickRandom(resourceIds, rng); const dstResource = pickRandom(resourceIds, rng); if (srcResource !== dstResource) { addLinkEdge(srcResource, dstResource); } } const reverseLinksCount = Math.max(1, Math.round(resources * linkReversePerResource)); for (let i = 0; i < reverseLinksCount; i++) { const srcResource = pickRandom(resourceIds, rng); const dstUser = pickRandom(userIds, rng); addLinkEdge(srcResource, dstUser); } for (const userId of userIds) { const node = arbiter.nodes.get(userId); if (node) { node.embedding = arbiter.jsonhash.generateSketch({ type: 'user', id: userId }); node.stale = false; } } for (const resourceId of resourceIds) { const node = arbiter.nodes.get(resourceId); if (node) { node.embedding = arbiter.jsonhash.generateSketch({ type: 'resource', id: resourceId }); node.stale = false; } } if (arbiter.embeddingManager) { arbiter.embeddingManager.clearPendingStaleNodes(); } arbiter.embeddingManager = null; const relationPools = { direct: [ { name: 'viewer', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keySet: viewerSet }, { name: 'owner', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keySet: ownerSet }, { name: 'blocked', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keySet: blockedSet }, { name: 'member', srcType: 'user', dstType: 'group', srcPool: userIds, dstPool: groupIds, keySet: memberKeySet }, { name: 'group_access', srcType: 'group', dstType: 'resource', srcPool: groupIds, dstPool: resourceIds, keySet: groupAccessKeySet }, { name: 'link', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keySet: linkKeySet }, { name: 'link', srcType: 'resource', dstType: 'resource', srcPool: resourceIds, dstPool: resourceIds, keySet: linkKeySet }, { name: 'link', srcType: 'resource', dstType: 'user', srcPool: resourceIds, dstPool: userIds, keySet: linkKeySet }, { name: 'resource_access', srcType: 'resource', dstType: 'group', srcPool: resourceIds, dstPool: groupIds, keySet: resourceAccessKeySet } ], value: [ { name: 'risk_score', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keyMap: riskScoreMap }, { name: 'risk_limit', srcType: 'resource', dstType: 'resource', srcPool: resourceIds, dstPool: resourceIds, keyMap: riskLimitMap }, { name: 'trust_level', srcType: 'user', dstType: 'resource', srcPool: userIds, dstPool: resourceIds, keyMap: trustLevelMap }, { name: 'trust_requirement', srcType: 'resource', dstType: 'resource', srcPool: resourceIds, dstPool: resourceIds, keyMap: trustRequirementMap } ] }; return { arbiter, userIds, groupIds, resourceIds, relationPools, groundTruth: { viewerSet, ownerSet, blockedSet, memberMap, groupAccessMap, viewerByResource, viewerByUser, riskScoreMap, riskLimitMap, trustLevelMap, trustRequirementMap, linkAdjacency, linkReverseAdjacency, resourceAccessMap } }; } function runQueries({ arbiter, total, valueRatio, samplers, rng, recordFrequency, frequencies, relationPools, validation }) { const relationManager = arbiter.relationManager; const latencies = []; let mismatches = 0; let falsePositives = 0; let falseNegatives = 0; for (let i = 0; i < total; i++) { const start = process.hrtime.bigint(); const useValue = relationPools.value.length > 0 && rng() < valueRatio; const relationDef = useValue ? pickRandom(relationPools.value, rng) : pickRandom(relationPools.direct, rng); const srcSampler = samplers[relationDef.srcType]; const dstSampler = samplers[relationDef.dstType]; const srcId = relationDef.srcPool[srcSampler()]; const dstId = relationDef.dstPool[dstSampler()]; const relation = relationDef.name; const cacheKey = useValue ? relationManager._makeValueCacheKey(srcId, relation, dstId) : relationManager._makeDirectCacheKey(srcId, relation, dstId); if (recordFrequency) { const current = frequencies.get(cacheKey) || 0; frequencies.set(cacheKey, current + 1); } if (useValue) { const result = relationManager.getValueRelation(srcId, relation, dstId); if (validation) { const expected = relationDef.keyMap.has(cacheKey); if (expected && !result) { mismatches++; falseNegatives++; } else if (!expected && result) { mismatches++; falsePositives++; } } } else { const result = relationManager.getDirectRelation(srcId, relation, dstId); if (validation) { const expected = relationDef.keySet.has(cacheKey); if (expected && !result) { mismatches++; falseNegatives++; } else if (!expected && result) { mismatches++; falsePositives++; } } } const durationMs = Number(process.hrtime.bigint() - start) / 1e6; latencies.push(durationMs); } return { mismatches, falsePositives, falseNegatives, latencies }; } function runAuthEval({ arbiter, total, samplers, rng, groundTruth, userIds, resourceIds, debugLimit }) { let tp = 0; let tn = 0; let fp = 0; let fn = 0; let predictedTrue = 0; let predictedFalse = 0; const latencies = []; const debugFailures = []; const debugFalsePositives = []; const perRuleStats = new Map(); const authRelations = [ { name: 'can_view', weight: 0.12 }, { name: 'can_edit', weight: 0.06 }, { name: 'can_view_ttu', weight: 0.1 }, { name: 'can_view_ttu_reverse', weight: 0.08 }, { name: 'can_reach', weight: 0.08 }, { name: 'can_reach_reverse', weight: 0.08 }, { name: 'similar_view', weight: 0.08 }, { name: 'similar_view_reverse', weight: 0.08 }, { name: 'risk_ok', weight: 0.06 }, { name: 'trust_ok_qual', weight: 0.06 }, { name: 'can_view_defeasible', weight: 0.08 }, { name: 'can_view_mixed', weight: 0.06 }, { name: 'viewer', weight: 0.03 }, { name: 'owner', weight: 0.03 } ]; const weightSum = authRelations.reduce((sum, item) => sum + item.weight, 0); const pickAuthRelation = () => { let roll = rng() * weightSum; for (const relation of authRelations) { if (roll <= relation.weight) return relation.name; roll -= relation.weight; } return authRelations[authRelations.length - 1].name; }; const isBlocked = (userId, resourceId) => groundTruth.blockedSet.has(arbiter.keyManager.createCompositeKey(userId, 'blocked', resourceId)); const hasDirect = (relation, userId, resourceId) => groundTruth[`${relation}Set`].has(arbiter.keyManager.createCompositeKey(userId, relation, resourceId)); const ttuExpected = (userId, resourceId) => { const groups = groundTruth.memberMap.get(userId); if (!groups) return false; for (const groupId of groups) { const resources = groundTruth.groupAccessMap.get(groupId); if (resources && resources.has(resourceId)) { return true; } } return false; }; const canViewExpected = (userId, resourceId) => { if (isBlocked(userId, resourceId)) return false; if (hasDirect('viewer', userId, resourceId) || hasDirect('owner', userId, resourceId)) { return true; } return ttuExpected(userId, resourceId); }; const canReachCache = new Map(); const canReachReverseCache = new Map(); const canReachExpected = (userId, resourceId) => { if (!groundTruth.linkAdjacency) return false; if (!canReachCache.has(userId)) { const reachable = new Set(); const queue = [{ id: userId, depth: 0 }]; const visited = new Set([userId]); while (queue.length > 0) { const { id, depth } = queue.shift(); if (depth >= 3) continue; const next = groundTruth.linkAdjacency.get(id); if (!next) continue; for (const neighbor of next) { if (!visited.has(neighbor)) { visited.add(neighbor); reachable.add(neighbor); queue.push({ id: neighbor, depth: depth + 1 }); } } } canReachCache.set(userId, reachable); } return canReachCache.get(userId).has(resourceId); }; const canReachReverseExpected = (userId, resourceId) => { if (!groundTruth.linkReverseAdjacency) return false; if (!canReachReverseCache.has(userId)) { const reachable = new Set(); const queue = [{ id: userId, depth: 0 }]; const visited = new Set([userId]); while (queue.length > 0) { const { id, depth } = queue.shift(); if (depth >= 3) continue; const next = groundTruth.linkReverseAdjacency.get(id); if (!next) continue; for (const neighbor of next) { if (!visited.has(neighbor)) { visited.add(neighbor); reachable.add(neighbor); queue.push({ id: neighbor, depth: depth + 1 }); } } } canReachReverseCache.set(userId, reachable); } return canReachReverseCache.get(userId).has(resourceId); }; const similarityThreshold = 0.6; const similarViewExpected = (userId, resourceId) => { const viewers = groundTruth.viewerByResource.get(resourceId); if (!viewers) return false; const userKey = arbiter.keyByNodeId.get(userId); const userNode = arbiter.nodes.get(userId); for (const viewerId of viewers) { const viewerKey = arbiter.keyByNodeId.get(viewerId); const viewerNode = arbiter.nodes.get(viewerId); if (userNode?.embedding && viewerNode?.embedding) { if (arbiter.jsonhash.compareSketches(userNode.embedding, viewerNode.embedding) >= similarityThreshold) { return true; } } else if (basicSimilarity(userKey, viewerKey) >= similarityThreshold) { return true; } } return false; }; const similarViewReverseExpected = (userId, resourceId) => { const resources = groundTruth.viewerByUser.get(userId); if (!resources) return false; const resourceKey = arbiter.keyByNodeId.get(resourceId); const resourceNode = arbiter.nodes.get(resourceId); for (const viewedResourceId of resources) { const viewedKey = arbiter.keyByNodeId.get(viewedResourceId); const viewedNode = arbiter.nodes.get(viewedResourceId); if (resourceNode?.embedding && viewedNode?.embedding) { if (arbiter.jsonhash.compareSketches(resourceNode.embedding, viewedNode.embedding) >= similarityThreshold) { return true; } } else if (basicSimilarity(resourceKey, viewedKey) >= similarityThreshold) { return true; } } return false; }; const riskOkExpected = (userId, resourceId) => { const riskRel = arbiter.relationManager.getDirectRelation(userId, 'risk_score', resourceId); const limitRel = arbiter.relationManager.getDirectRelation(resourceId, 'risk_limit', resourceId); if (!riskRel || !limitRel) { return false; } const riskValue = arbiter.valueManager.getBlurredValue(riskRel); const limitValue = arbiter.valueManager.getBlurredValue(limitRel); if (!riskValue || !limitValue || !riskValue.interval || !limitValue.interval) { return false; } const possibility = arbiter.valueManager.compareIntervals( riskValue.interval, limitValue.interval, '<=' ); return possibility > 0; }; const trustScale = [0, 0.25, 0.5, 0.75, 1]; const trustOkQualExpected = (userId, resourceId) => { const trustLevel = groundTruth.trustLevelMap.get( arbiter.keyManager.createCompositeKey(userId, 'trust_level', resourceId) ); const trustRequirement = groundTruth.trustRequirementMap.get( arbiter.keyManager.createCompositeKey(resourceId, 'trust_requirement', resourceId) ); if (trustLevel === undefined || trustRequirement === undefined) return false; const left = convertToQualitative(trustLevel, trustScale); const right = convertToQualitative(trustRequirement, trustScale); return left >= right; }; const canViewDefeasibleExpected = (userId, resourceId) => { if (!riskOkExpected(userId, resourceId)) return false; if (isBlocked(userId, resourceId)) return false; return hasDirect('viewer', userId, resourceId) || hasDirect('owner', userId, resourceId) || ttuExpected(userId, resourceId); }; const ttuReverseExpected = (userId, resourceId) => { const groups = groundTruth.memberMap.get(userId); if (!groups) return false; const resourceGroups = groundTruth.resourceAccessMap.get(resourceId); if (!resourceGroups) return false; for (const groupId of groups) { if (resourceGroups.has(groupId)) { return true; } } return false; }; const canViewMixedExpected = (userId, resourceId) => canViewExpected(userId, resourceId) || canReachReverseExpected(userId, resourceId) || similarViewReverseExpected(userId, resourceId) || trustOkQualExpected(userId, resourceId); const pickFromArray = (arr) => arr[Math.floor(rng() * arr.length)]; const viewerPairs = Array.from(groundTruth.viewerSet, key => { const [userId, , resourceId] = JSON.parse(key); return { userId, resourceId }; }); const ownerPairs = Array.from(groundTruth.ownerSet, key => { const [userId, , resourceId] = JSON.parse(key); return { userId, resourceId }; }); const ttuPairs = []; const groupMembers = new Map(); for (const [userId, groups] of groundTruth.memberMap.entries()) { for (const groupId of groups) { addToMapSet(groupMembers, groupId, userId); const resources = groundTruth.groupAccessMap.get(groupId); if (!resources) continue; for (const resourceId of resources) { ttuPairs.push({ userId, resourceId }); } } } const ttuReversePairs = []; for (const [resourceId, groups] of groundTruth.resourceAccessMap.entries()) { for (const groupId of groups) { const members = groupMembers.get(groupId); if (!members) continue; for (const userId of members) { ttuReversePairs.push({ userId, resourceId }); } } } const buildReachPairs = (adjacency, sourceIds) => { const pairs = []; for (const sourceId of sourceIds) { const reachable = new Set(); const queue = [{ id: sourceId, depth: 0 }]; const visited = new Set([sourceId]); while (queue.length > 0) { const { id, depth } = queue.shift(); if (depth >= 3) continue; const next = adjacency.get(id); if (!next) continue; for (const neighbor of next) { if (!visited.has(neighbor)) { visited.add(neighbor); reachable.add(neighbor); queue.push({ id: neighbor, depth: depth + 1 }); } } } let added = 0; for (const targetId of reachable) { pairs.push({ userId: sourceId, resourceId: targetId }); added++; if (added >= 50) break; } } return pairs; }; const canReachPairs = buildReachPairs(groundTruth.linkAdjacency, userIds); const userIdSet = new Set(userIds); const resourceIdSet = new Set(resourceIds); const canReachReversePairs = buildReachPairs(groundTruth.linkReverseAdjacency, userIds) .filter(pair => resourceIdSet.has(pair.resourceId)); const similarViewReversePairs = []; for (const [userId, resources] of groundTruth.viewerByUser.entries()) { for (const resourceId of resources) { similarViewReversePairs.push({ userId, resourceId }); } } const riskOkPairs = []; for (const key of groundTruth.riskScoreMap.keys()) { const [userId, , resourceId] = JSON.parse(key); if (riskOkExpected(userId, resourceId)) { riskOkPairs.push({ userId, resourceId }); } } const trustOkPairs = []; for (const key of groundTruth.trustLevelMap.keys()) { const [userId, , resourceId] = JSON.parse(key); if (trustOkQualExpected(userId, resourceId)) { trustOkPairs.push({ userId, resourceId }); } } const canViewDefeasiblePairs = ttuPairs.filter(pair => canViewDefeasibleExpected(pair.userId, pair.resourceId)); const canViewPairs = [...viewerPairs, ...ownerPairs, ...ttuPairs]; const canViewMixedPairs = [ ...canViewPairs, ...canReachReversePairs, ...similarViewReversePairs, ...trustOkPairs ].filter(pair => canViewMixedExpected(pair.userId, pair.resourceId)); const expectedForRelation = (relation, userId, resourceId) => { if (relation === 'viewer' || relation === 'owner') { return hasDirect(relation, userId, resourceId); } if (relation === 'can_view') { return canViewExpected(userId, resourceId); } if (relation === 'can_edit') { return hasDirect('owner', userId, resourceId) && canViewExpected(userId, resourceId); } if (relation === 'can_view_ttu') { return ttuExpected(userId, resourceId); } if (relation === 'can_view_ttu_reverse') { return ttuReverseExpected(userId, resourceId); } if (relation === 'can_reach') { return canReachExpected(userId, resourceId); } if (relation === 'can_reach_reverse') { return canReachReverseExpected(userId, resourceId); } if (relation === 'similar_view') { return similarViewExpected(userId, resourceId); } if (relation === 'similar_view_reverse') { return similarViewReverseExpected(userId, resourceId); } if (relation === 'risk_ok') { return riskOkExpected(userId, resourceId); } if (relation === 'trust_ok_qual') { return trustOkQualExpected(userId, resourceId); } if (relation === 'can_view_defeasible') { return canViewDefeasibleExpected(userId, resourceId); } if (relation === 'can_view_mixed') { return canViewMixedExpected(userId, resourceId); } return false; }; const positiveSampleRate = 0.45; const maxSampleAttempts = 40; const pickPairForRelation = (relation) => { const positivePools = { viewer: viewerPairs, owner: ownerPairs, can_view: canViewPairs, can_edit: canViewPairs, can_view_ttu: ttuPairs, can_view_ttu_reverse: ttuReversePairs, can_reach: canReachPairs, can_reach_reverse: canReachReversePairs, similar_view: viewerPairs, similar_view_reverse: similarViewReversePairs, risk_ok: riskOkPairs, trust_ok_qual: trustOkPairs, can_view_defeasible: canViewDefeasiblePairs, can_view_mixed: canViewMixedPairs }; const pool = positivePools[relation] || []; const forcePositive = new Set([ 'can_view_ttu_reverse', 'can_reach_reverse', 'similar_view_reverse', 'trust_ok_qual' ]); if (pool.length > 0 && (forcePositive.has(relation) || rng() < positiveSampleRate)) { return pickFromArray(pool); } if (rng() < positiveSampleRate) { const attempts = relation.endsWith('_reverse') ? maxSampleAttempts * 2 : maxSampleAttempts; for (let attempt = 0; attempt < attempts; attempt++) { const candidateUser = userIds[samplers.user()]; const candidateResource = resourceIds[samplers.resource()]; if (expectedForRelation(relation, candidateUser, candidateResource)) { return { userId: candidateUser, resourceId: candidateResource }; } } } return { userId: userIds[samplers.user()], resourceId: resourceIds[samplers.resource()] }; }; for (let i = 0; i < total; i++) { const relation = pickAuthRelation(); const sampled = pickPairForRelation(relation); const userId = sampled.userId; const resourceId = sampled.resourceId; const expected = expectedForRelation(relation, userId, resourceId); const start = process.hrtime.bigint(); const result = arbiter.check( arbiter.keyByNodeId.get(userId), relation, arbiter.keyByNodeId.get(resourceId) ); const durationMs = Number(process.hrtime.bigint() - start) / 1e6; latencies.push(durationMs); const predicted = result && result.possibility > 0; if (predicted) { predictedTrue++; } else { predictedFalse++; } if (expected && predicted) { tp++; } else if (!expected && !predicted) { tn++; } else if (!expected && predicted) { fp++; if (debugFalsePositives.length < debugLimit) { let riskDetails = null; if (relation === 'risk_ok') { const riskRel = arbiter.relationManager.getDirectRelation(userId, 'risk_score', resourceId); const limitRel = arbiter.relationManager.getDirectRelation(resourceId, 'risk_limit', resourceId); const riskValue = riskRel ? arbiter.valueManager.getBlurredValue(riskRel) : null; const limitValue = limitRel ? arbiter.valueManager.getBlurredValue(limitRel) : null; const comparison = riskValue && limitValue && riskValue.interval && limitValue.interval ? arbiter.valueManager.compareIntervals(riskValue.interval, limitValue.interval, '<=') : null; riskDetails = { riskRelationExists: !!riskRel, riskValue: riskRel ? riskRel.value : null, riskInterval: riskValue ? riskValue.interval : null, limitRelationExists: !!limitRel, limitValue: limitRel ? limitRel.value : null, limitInterval: limitValue ? limitValue.interval : null, comparison }; } debugFalsePositives.push({ relation, userId, resourceId, predictedPossibility: result ? result.possibility : 0, predictedReason: result ? result.reason : null, predictedMeta: result ? result.meta : null, riskDetails }); } } else if (expected && !predicted) { fn++; if (debugFailures.length < debugLimit) { const viewerKey = arbiter.keyManager.createCompositeKey(userId, 'viewer', resourceId); const ownerKey = arbiter.keyManager.createCompositeKey(userId, 'owner', resourceId); const blockedKey = arbiter.keyManager.createCompositeKey(userId, 'blocked', resourceId); const viewerDirect = groundTruth.viewerSet.has(viewerKey); const ownerDirect = groundTruth.ownerSet.has(ownerKey); const blocked = groundTruth.blockedSet.has(blockedKey); const memberGroups = groundTruth.memberMap.get(userId) || new Set(); const accessGroups = []; for (const groupId of memberGroups) { const resources = groundTruth.groupAccessMap.get(groupId); if (resources && resources.has(resourceId)) { accessGroups.push(groupId); } } const userKey = arbiter.keyByNodeId.get(userId); const resourceKey = arbiter.keyByNodeId.get(resourceId); const bypassResult = arbiter.authChecker.check(userKey, relation, resourceKey, { bypassPLTC: true }); const memberRelations = arbiter.relationManager.getRelationsFromSrc(userId, 'member'); const groupAccessHasResource = accessGroups.length > 0 ? arbiter.relationManager.getRelationsFromSrc(accessGroups[0], 'group_access').some(rel => rel.dst === resourceId) : false; const chainRule = { type: 'chain', steps: [ { relation: 'member', direction: 'out' }, { relation: 'group_access', direction: 'out' } ] }; const chainResult = arbiter.authChecker.ruleEvaluator.evaluateRule( userId, userKey, resourceId, resourceKey, chainRule, new Set(), relation, { bypassPLTC: true } ); debugFailures.push({ relation, userId, resourceId, viewerDirect, ownerDirect, blocked, memberGroups: Array.from(memberGroups), accessGroups, memberRelationsCount: memberRelations.length, groupAccessHasResource, chainPossibility: chainResult ? chainResult.possibility : 0, predictedPossibility: result ? result.possibility : 0, bypassPossibility: bypassResult ? bypassResult.possibility : 0, bypassReason: bypassResult ? bypassResult.reason : null, bypassMeta: bypassResult ? bypassResult.meta : null }); } } if (!perRuleStats.has(relation)) { perRuleStats.set(relation, { tp: 0, tn: 0, fp: 0, fn: 0, predictedTrue: 0, predictedFalse: 0, latencies: [] }); } const ruleStats = perRuleStats.get(relation); ruleStats.latencies.push(durationMs); if (predicted) { ruleStats.predictedTrue++; } else { ruleStats.predictedFalse++; } if (expected && predicted) { ruleStats.tp++; } else if (!expected && !predicted) { ruleStats.tn++; } else if (!expected && predicted) { ruleStats.fp++; } else if (expected && !predicted) { ruleStats.fn++; } } const precision = tp + fp > 0 ? tp / (tp + fp) : 0; const recall = tp + fn > 0 ? tp / (tp + fn) : 0; const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0; return { tp, tn, fp, fn, predictedTrue, predictedFalse, precision, recall, f1, debugFailures, debugFalsePositives, latencies, perRuleStats }; } const args = parseArgs(process.argv); const config = { users: Number(args.get('users') || 200), groups: Number(args.get('groups') || 100), resources: Number(args.get('resources') || 300), memberPerUser: Number(args.get('member-per-user') || 3), accessPerGroup: Number(args.get('access-per-group') || 6), viewerPerUser: Number(args.get('viewer-per-user') || 2), ownerPerUser: Number(args.get('owner-per-user') || 1), blockedPerUser: Number(args.get('blocked-per-user') || 0.2), riskPerUser: Number(args.get('risk-per-user') || 1.5), linkPerUser: Number(args.get('link-per-user') || 2), linkPerResource: Number(args.get('link-per-resource') || 2), linkReversePerResource: Number(args.get('link-reverse-per-resource') || 1), riskLimitBase: Number(args.get('risk-limit-base') || 50), riskLimitVariance: Number(args.get('risk-limit-variance') || 30), trustPerUser: Number(args.get('trust-per-user') || 2), trustRequirementBase: Number(args.get('trust-requirement-base') || 0.3), trustRequirementVariance: Number(args.get('trust-requirement-variance') || 0.5), queries: Number(args.get('queries') || 20000), warmup: Number(args.get('warmup') || 2000), zipf: Number(args.get('zipf') || 1.1), valueRatio: Number(args.get('value-ratio') || 0.2), seed: args.has('seed') ? Number(args.get('seed')) : null, debugFailures: Number(args.get('debug-failures') || 5) }; const rng = createRng(config.seed); const samplers = { user: buildZipfSampler(config.users, config.zipf, rng), group: buildZipfSampler(config.groups, config.zipf, rng), resource: buildZipfSampler(config.resources, config.zipf, rng) }; const { arbiter, relationPools, groundTruth, userIds, resourceIds } = buildGraph({ users: config.users, groups: config.groups, resources: config.resources, memberPerUser: config.memberPerUser, accessPerGroup: config.accessPerGroup, viewerPerUser: config.viewerPerUser, ownerPerUser: config.ownerPerUser, blockedPerUser: config.blockedPerUser, riskPerUser: config.riskPerUser, linkPerUser: config.linkPerUser, linkPerResource: config.linkPerResource, linkReversePerResource: config.linkReversePerResource, riskLimitBase: config.riskLimitBase, riskLimitVariance: config.riskLimitVariance, trustPerUser: config.trustPerUser, trustRequirementBase: config.trustRequirementBase, trustRequirementVariance: config.trustRequirementVariance, rng }); arbiter.setRelationConfig('viewer', { type: 'direct' }); arbiter.setRelationConfig('owner', { type: 'direct' }); arbiter.setRelationConfig('member', { type: 'direct' }); arbiter.setRelationConfig('group_access', { type: 'direct' }); arbiter.setRelationConfig('blocked', { type: 'direct' }); arbiter.setRelationConfig('risk_score', { type: 'direct' }); arbiter.setRelationConfig('risk_limit', { type: 'direct' }); arbiter.setRelationConfig('link', { type: 'direct' }); arbiter.setRelationConfig('resource_access', { type: 'direct' }); arbiter.setRelationConfig('trust_level', { type: 'direct' }); arbiter.setRelationConfig('trust_requirement', { type: 'direct' }); arbiter.setRelationConfig('can_view', { union: [ { type: 'direct', relation: 'viewer' }, { type: 'direct', relation: 'owner' }, { type: 'chain', steps: [ { relation: 'member', direction: 'out' }, { relation: 'group_access', direction: 'out' } ] } ], unless: { union: [ { type: 'direct', relation: 'blocked' } ] } }); arbiter.setRelationConfig('can_edit', { intersection: [ { type: 'direct', relation: 'owner' }, { type: 'computed', relation: 'can_view' } ] }); arbiter.setRelationConfig('can_view_ttu', { type: 'tuple_to_userset', tuplesetRelation: 'group_access', tuplesetDirection: 'in', computedRelation: 'member' }); arbiter.setRelationConfig('can_view_ttu_reverse', { type: 'tuple_to_userset', tuplesetRelation: 'member', reverse: true, computedRelation: 'resource_access' }); arbiter.setRelationConfig('can_reach', { type: 'multi_hop', relation: 'link', maxDepth: 4 }); arbiter.setRelationConfig('can_reach_reverse', { type: 'multi_hop', relation: 'link', maxDepth: 4, reverse: true }); arbiter.setRelationConfig('similar_view', { type: 'similar_to', relation: 'viewer', k: 5, similarityThreshold: 0.6, fallbackToBasicSimilarity: true }); arbiter.setRelationConfig('similar_view_reverse', { type: 'similar_to', relation: 'viewer', reverse: true, k: 5, similarityThreshold: 0.6, fallbackToBasicSimilarity: true }); arbiter.setRelationConfig('risk_ok', { type: 'relational_comparator', comparator: '<=', fallbackBehavior: 'deny', left: { rule: { type: 'direct', relation: 'risk_score' }, extractValue: true, valueRelation: 'risk_score' }, right: { rule: { type: 'direct', relation: 'risk_limit' }, extractValue: true, valueRelation: 'risk_limit', evaluateFrom: 'object' } }); arbiter.setRelationConfig('trust_ok_qual', { type: 'relational_comparator', qualitative: true, scaleName: 'five-point', comparator: '>=', fallbackBehavior: 'deny', left: { rule: { type: 'direct', relation: 'trust_level' }, extractValue: true, valueRelation: 'trust_level' }, right: { rule: { type: 'direct', relation: 'trust_requirement' }, extractValue: true, valueRelation: 'trust_requirement', evaluateFrom: 'object' } }); arbiter.setRelationConfig('can_view_defeasible', { union: [ { type: 'direct', relation: 'viewer' }, { type: 'direct', relation: 'owner' }, { type: 'computed', relation: 'can_view_ttu' } ], unless: { union: [ { type: 'direct', relation: 'blocked' } ] }, requires: { union: [ { type: 'computed', relation: 'risk_ok' } ] } }); arbiter.setRelationConfig('can_view_mixed', { union: [ { type: 'computed', relation: 'can_view' }, { type: 'computed', relation: 'can_reach_reverse' }, { type: 'computed', relation: 'similar_view_reverse' }, { type: 'computed', relation: 'trust_ok_qual' } ] }); arbiter.graphManager.initializeReachabilityChecker({ enableBackwardIndex: true }); if (config.warmup > 0) { runQueries({ arbiter, total: config.warmup, valueRatio: config.valueRatio, samplers, rng, recordFrequency: false, frequencies: new Map(), relationPools, validation: false }); } arbiter.relationManager.resetCacheStats(); const frequencies = new Map(); const startTime = Date.now(); const validation = runQueries({ arbiter, total: config.queries, valueRatio: config.valueRatio, samplers, rng, recordFrequency: true, frequencies, relationPools, validation: true }); const elapsedMs = Date.now() - startTime; const qps = elapsedMs > 0 ? Math.round((config.queries / elapsedMs) * 1000) : 0; const lookupLatencies = [...validation.latencies].sort((a, b) => a - b); const lookupP95 = percentile(lookupLatencies, 95).toFixed(3); const lookupP99 = percentile(lookupLatencies, 99).toFixed(3); const stats = arbiter.relationManager.getCacheStats(); const estimatedHitRate = estimateHitRate(frequencies, arbiter.relationManager._maxCacheSize); const authStart = Date.now(); const authEval = runAuthEval({ arbiter, total: config.queries, samplers, rng, groundTruth, userIds, resourceIds, debugLimit: config.debugFailures }); const authElapsed = Date.now() - authStart; const authQps = authElapsed > 0 ? Math.round((config.queries / authElapsed) * 1000) : 0; const authLatencies = [...authEval.latencies].sort((a, b) => a - b); const authP95 = percentile(authLatencies, 95).toFixed(3); const authP99 = percentile(authLatencies, 99).toFixed(3); const perRuleOutput = []; for (const [relation, stats] of authEval.perRuleStats.entries()) { const ruleLatencies = [...stats.latencies].sort((a, b) => a - b); const ruleP95 = percentile(ruleLatencies, 95); const ruleP99 = percentile(ruleLatencies, 99); const precision = stats.tp + stats.fp > 0 ? stats.tp / (stats.tp + stats.fp) : 0; const recall = stats.tp + stats.fn > 0 ? stats.tp / (stats.tp + stats.fn) : 0; const f1 = precision + recall > 0 ? (2 * precision * recall) / (precision + recall) : 0; perRuleOutput.push({ relation, tp: stats.tp, tn: stats.tn, fp: stats.fp, fn: stats.fn, predictedTrue: stats.predictedTrue, predictedFalse: stats.predictedFalse, precision: Number(precision.toFixed(3)), recall: Number(recall.toFixed(3)), f1: Number(f1.toFixed(3)), p95: Number(ruleP95.toFixed(3)), p99: Number(ruleP99.toFixed(3)) }); } perRuleOutput.sort((a, b) => a.relation.localeCompare(b.relation)); console.log('Zipfian cache evaluation'); console.log(`users=${config.users} groups=${config.groups} resources=${config.resources} queries=${config.queries}`); console.log(`zipf=${config.zipf} value_ratio=${config.valueRatio} warmup=${config.warmup}`); console.log(`cache_hits=${stats.cacheHits} cache_misses=${stats.cacheMisses} hit_rate=${stats.hitRate}`); console.log(`relation_cache_size=${stats.relationCacheSize} value_cache_size=${stats.valueCacheSize}`); console.log(`estimated_topk_hit_rate=${estimatedHitRate.toFixed(3)}`); console.log(`elapsed_ms=${elapsedMs} qps=${qps}`); console.log(`lookup_p95_ms=${lookupP95} lookup_p99_ms=${lookupP99}`); console.log(`validation_mismatches=${validation.mismatches} false_pos=${validation.falsePositives} false_neg=${validation.falseNegatives}`); console.log(`auth_elapsed_ms=${authElapsed} auth_qps=${authQps}`); console.log(`auth_p95_ms=${authP95} auth_p99_ms=${authP99}`); console.log('auth_per_rule'); for (const entry of perRuleOutput) { console.log(JSON.stringify(entry)); } console.log(`auth_predicted_true=${authEval.predictedTrue} auth_predicted_false=${authEval.predictedFalse}`); console.log(`auth_tp=${authEval.tp} auth_tn=${authEval.tn} auth_fp=${authEval.fp} auth_fn=${authEval.fn}`); console.log(`auth_precision=${authEval.precision.toFixed(3)} auth_recall=${authEval.recall.toFixed(3)} auth_f1=${authEval.f1.toFixed(3)}`); if (authEval.debugFailures.length) { console.log('auth_failure_samples'); for (const sample of authEval.debugFailures) { console.log(JSON.stringify(sample)); } } if (authEval.debugFalsePositives.length) { console.log('auth_false_positive_samples'); for (const sample of authEval.debugFalsePositives) { console.log(JSON.stringify(sample)); } }