/** * rigor/complexity-bench-crucible.test.js — advanced perf & complexity * crucibles over the complex graphs. * * These crucibles use js-rigor's complexity and benchmark verdicts, which * are normally dormant in this suite: * * COMPLEXITY — declares an asymptotic class per action; rigor's * e-process verifies the observed cost signal does not grow faster * than the formula across input sizes (calibrated on the smallest * observations, tested on the rest; Ville's inequality gives * P(false alarm) <= 0.05 when eProcess > 20). A `cost` metric (rule * evaluations) is declared so the verdict is deterministic, not * wall-clock noise. * * BENCHMARK — percentile assertions over auto-collected bench samples * per action (p50/p95/p99 maxMs). Bounds are set ~20-100x above the * measured medians so the crucible catches order-of-magnitude * regressions, not dev-machine noise. * * Declared classes (verified against the engine): * check[direct] O(1) in graph size — hash lookup * check[chain-2hop] O(1) in graph size — fixed-depth traversal * check[union-normal] O(k) in rule count — evaluates every rule * check[union-binary] O(1) in rule count — early exit at threshold * check[ttu] O(1) in graph size — one-hop group lookup */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { rigor } from '@rigor/core'; import { Arbiter } from '../../src/index.js'; import { makeCommunityGraph, makeHierarchyGraph } from './complex-graphs.js'; // ── Fixtures: prebuilt graphs at growing sizes, shared across runs ──── function buildSizedCommunityGraphs() { const sizes = [2, 4, 8, 16]; return sizes.map((communities, i) => { const g = makeCommunityGraph(10 + i, { communities, membersPerCommunity: 6, resourcesPerCommunity: 3, subCommunities: 2 }); return { size: g.arbiter.relations.length, arbiter: g.arbiter, users: g.users, resources: g.resources, groups: g.groups, subGroups: g.subGroups, relations: g.relations }; }); } function buildSizedHierarchyGraphs() { const sizes = [2, 4, 8]; return sizes.map((departments, i) => { const g = makeHierarchyGraph(20 + i, { departments, teamsPerDept: 2, membersPerTeam: 3, resourcesPerTeam: 2 }); return { size: g.arbiter.relations.length, arbiter: g.arbiter, users: g.users, resources: g.resources }; }); } const COMMUNITY = buildSizedCommunityGraphs(); const HIERARCHY = buildSizedHierarchyGraphs(); // Pick a query that exercises a non-trivial path on each graph. function communityQuery(graph, seed) { const ownsBySrc = new Map(); for (const r of graph.relations) if (r.rel === 'owns') ownsBySrc.set(r.src, r.dst); const memberEdge = graph.relations.find(r => r.rel === 'member' && ownsBySrc.has(r.dst)); if (!memberEdge) return { user: graph.users[0], object: graph.resources[0] }; return { user: memberEdge.src, object: ownsBySrc.get(memberEdge.dst) }; } // ── Actions ────────────────────────────────────────────────────────── // Each action returns { result, cost } where cost = rule evaluations, // making the complexity verdict deterministic. // Deterministic cost signal for graph-size actions: count actual engine // lookups (getDirectRelation / getRelationsFromSrc / getRelationsToDst) // performed per check. A correct engine does a CONSTANT number of lookups // per query regardless of graph size -> O(1) verified deterministically. // A regression to linear scans would grow the count with n and trip the // e-process. No wall-clock jitter. function instrumentLookupCount(arbiter) { const lookup = arbiter.relationManager._lookup; let count = 0; for (const m of ['getDirectRelation', 'getRelationsFromSrc', 'getRelationsToDst']) { const original = lookup[m].bind(lookup); lookup[m] = function (...args) { count++; return original(...args); }; } return () => count; } const LOOKUP_INSTRUMENTED = new Map(); for (let i = 0; i < COMMUNITY.length; i++) LOOKUP_INSTRUMENTED.set('c' + i, instrumentLookupCount(COMMUNITY[i].arbiter)); for (let i = 0; i < HIERARCHY.length; i++) LOOKUP_INSTRUMENTED.set('h' + i, instrumentLookupCount(HIERARCHY[i].arbiter)); function withLookupCost(prefix, fn) { return (args) => { const before = LOOKUP_INSTRUMENTED.get(prefix + args.graphIdx)(); const result = fn(args); const cost = LOOKUP_INSTRUMENTED.get(prefix + args.graphIdx)() - before; return { result, cost }; }; } // Each action runs a fixed BATCH of checks per invocation so the timing // signal lands in a measurable range (sub-10us single checks are pure // jitter at the e-process spread check). The batch size is CONSTANT across // graph sizes, so O(1)-in-graph-size still means what it says. const BATCH = 2000; const actions = { direct: withLookupCost('c', ({ graphIdx }) => { const g = COMMUNITY[graphIdx]; let last; for (let i = 0; i < BATCH; i++) last = g.arbiter.check(g.users[0], 'direct_access', g.resources[0]); return last; }), chain: withLookupCost('h', ({ graphIdx }) => { const g = HIERARCHY[graphIdx]; let last; for (let i = 0; i < BATCH; i++) last = g.arbiter.check(g.users[0], 'can_access_org', g.resources[0]); return last; }), ttu: withLookupCost('c', ({ graphIdx }) => { const g = COMMUNITY[graphIdx]; const q = communityQuery(g, 1); let last; for (let i = 0; i < BATCH; i++) last = g.arbiter.check(q.user, 'can_read', q.object); return last; }), unionNormal: ({ ruleCount }) => { // Union with ruleCount direct rules; only rule 0 grants, so every rule // is evaluated (no early exit in normal mode) -> cost scales O(k). const arbiter = new Arbiter(); arbiter.addNode('u:1', 'user'); arbiter.addNode('doc:9', 'doc'); const rules = []; for (let r = 0; r < ruleCount; r++) { arbiter.setRelationConfig(`rel${r}`, { type: 'direct' }); rules.push({ type: 'direct', relation: `rel${r}` }); } arbiter.setRelationConfig('can_access', { union: rules }); arbiter.addRelation('u:1', 'rel0', 'doc:9', { possibility: 0.9 }); const result = arbiter.check('u:1', 'can_access', 'doc:9'); return { result, cost: ruleCount }; }, unionBinary: ({ ruleCount }) => { // Same union, binary mode with threshold 0.8: rule 0 grants 0.9 >= 0.8, // so the early exit fires after one rule -> cost stays O(1) in k. const arbiter = new Arbiter(); arbiter.addNode('u:1', 'user'); arbiter.addNode('doc:9', 'doc'); const rules = []; for (let r = 0; r < ruleCount; r++) { arbiter.setRelationConfig(`rel${r}`, { type: 'direct' }); rules.push({ type: 'direct', relation: `rel${r}` }); } arbiter.setRelationConfig('can_access', { union: rules }); arbiter.addRelation('u:1', 'rel0', 'doc:9', { possibility: 0.9 }); const result = arbiter.check('u:1', 'can_access', 'doc:9', { binary: true }); return { result, cost: 1 }; } }; const metricReaders = (name) => ({ n: ({ args }) => { // Metric readers receive the raw generated-args ARRAY (fns get the // spread values); the graph index is the single generated argument. const graphIdx = args[0].graphIdx; if (name === 'direct' || name === 'ttu') return COMMUNITY[graphIdx].size; if (name === 'chain') return HIERARCHY[graphIdx].size; return 0; }, // Deterministic cost = engine lookups per check. Wall-clock timing at // sub-ms scale is pure jitter for the spread check; the lookup count is // noise-free and still grows if the engine ever degrades to scans. cost: ({ result }) => result.cost }); describe('Complexity & benchmark crucibles (rigor)', () => { it('COMPLEXITY: direct/chain/ttu lookups are O(1) in graph size', async () => { const spec = []; for (const name of ['direct', 'chain', 'ttu']) { const isGraph = name !== 'chain'; spec.push(rigor.fn(name, actions[name], rigor.args( rigor.gen.object({ graphIdx: rigor.gen.int(0, (isGraph ? COMMUNITY : HIERARCHY).length - 1) }) ), rigor.metrics(metricReaders(name)))); } const report = await rigor.campaign( spec, rigor.crucible([ rigor.complexity('direct', 'O(1)'), rigor.complexity('chain', 'O(1)'), rigor.complexity('ttu', 'O(1)') ]) ).run({ effort: 600, seed: 'complexity-graph-size', artifacts: { dir: '', persist: 'never' } }); for (const v of report.crucibleVerdict.complexity) { assert.equal(v.passed, true, `${v.name} (${v.formula}): eProcess=${v.eProcess} violated=${v.trendViolated || v.spreadExceeded}`); } }); it('COMPLEXITY: union is O(k) in normal mode, O(1) in binary (early exit)', async () => { const spec = []; for (const name of ['unionNormal', 'unionBinary']) { spec.push(rigor.fn(name, actions[name], rigor.args( rigor.gen.object({ ruleCount: rigor.gen.oneOf([2, 4, 8, 16, 32, 64]) }) ), rigor.metrics({ k: ({ args }) => args[0].ruleCount, cost: ({ result }) => result.cost }))); } const report = await rigor.campaign( spec, rigor.crucible([ rigor.complexity('unionNormal', 'O(k)'), rigor.complexity('unionBinary', 'O(1)') ]) ).run({ effort: 600, seed: 'complexity-rule-count', artifacts: { dir: '', persist: 'never' } }); for (const v of report.crucibleVerdict.complexity) { assert.equal(v.passed, true, `${v.name} (${v.formula}): eProcess=${v.eProcess} violated=${v.trendViolated || v.spreadExceeded}`); } }); it('BENCHMARK: complex queries stay under p95 latency budgets', async () => { // Benchmark actions run a SINGLE check per invocation (not the BATCH // used by the complexity actions) so the sampled latency is the real // per-query latency, and the percentile assertions are meaningful. const benchActions = { direct: ({ graphIdx }) => COMMUNITY[graphIdx].arbiter.check(COMMUNITY[graphIdx].users[0], 'direct_access', COMMUNITY[graphIdx].resources[0]), chain: ({ graphIdx }) => HIERARCHY[graphIdx].arbiter.check(HIERARCHY[graphIdx].users[0], 'can_access_org', HIERARCHY[graphIdx].resources[0]), ttu: ({ graphIdx }) => { const g = COMMUNITY[graphIdx]; const q = communityQuery(g, 1); return g.arbiter.check(q.user, 'can_read', q.object); } }; const spec = []; for (const name of ['direct', 'chain', 'ttu']) { const isGraph = name !== 'chain'; spec.push(rigor.fn(name, benchActions[name], rigor.args( rigor.gen.object({ graphIdx: rigor.gen.int(0, (isGraph ? COMMUNITY : HIERARCHY).length - 1) }) ), rigor.metrics(metricReaders(name)))); } const report = await rigor.campaign( spec, rigor.crucible([ rigor.benchmark('direct', { p50: { maxMs: 0.2 }, p95: { maxMs: 1.0 } }), rigor.benchmark('chain', { p50: { maxMs: 0.2 }, p95: { maxMs: 1.0 } }), rigor.benchmark('ttu', { p50: { maxMs: 0.2 }, p95: { maxMs: 1.0 } }) ]) ).run({ effort: 600, seed: 'bench-complex-queries', artifacts: { dir: '', persist: 'never' } }); for (const v of report.crucibleVerdict.benchmarks) { assert.equal(v.passed, true, `${v.name}: p50/p95 over budget`); } }); });