Files
core/benchmarks/zipfian-cache-eval.js
T

1314 lines
46 KiB
JavaScript
Raw Normal View History

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));
}
}