rigor: six complex-graph crucibles + seed the TTU campaigns

Extends the complex-graph suite to the remaining uncovered surfaces:

- complex-graph-ttl-crucible: value-TTL expiry over community/scale-free
  graphs (pinned changed_last_at writes, {now} reads, mirror freshness
  rule), mutation-with-time binary parity, snapshot round-trip preserves
  the TTL gate. Notable find: snapshot restore resets changed_last_at to
  access time (RelationSnapshotAccess.js:91), so TTL assertions compare
  each engine against its own effective write clock.
- complex-graph-overlay-crucible: partial-graph overlay over complex
  graphs. Key discovery: pre-built PartialGraphContext must be passed as
  partialGraphContext (partialGraph is a raw spec re-ingested at
  ArbiterChecks.js:15); overlay rides the direct relations a policy
  consumes (TTU-derived can_read ignores it, verified by probe).
- complex-graph-values-crucible: relational-comparator over value-carrying
  relations with pinned clocks — comparator parity, value-mutation flips
  the decision immediately, TTL expiry on the comparator denies.
- complex-graph-reachability-crucible: PLTC reachability vs ground-truth
  BFS on scale-free/community graphs — verdict parity, fast-fail
  soundness (no false positives), null-defer contract honored.
- complex-graph-batch-crucible: addRelationsBatch vs sequential build
  parity, mutation parity across both, cache-freshness after mutation.
- complex-graph-quantization-crucible: 16-bit quantization band over real
  possibility spreads, allow/deny agreement outside the band, snapshot-of-
  snapshot semantic identity.

Also seeds the two unseeded campaigns in tuple-to-userset-rule.test.js
(flagged flake ~1/15 — nondeterministic runs on a deterministic engine).

Rigor 245/245, full suite 847/785/0.
This commit is contained in:
John Dvorak
2026-08-02 15:06:50 -07:00
parent 1a2a6fc22e
commit 8141930764
7 changed files with 969 additions and 2 deletions
@@ -0,0 +1,161 @@
/**
* rigor/complex-graph-overlay-crucible.test.js — partial-graph overlay over
* the complex graphs (community block model + org hierarchy).
*
* An overlay is a caller-supplied set of facts consulted alongside the
* persistent graph. The check option key for a pre-built
* PartialGraphContext is `partialGraphContext` (AuthorizationChecker
* reads that key; passing the context under `partialGraph` would be
* re-ingested as a raw spec and silently empty). The persistent relation
* is ORed into the direct lookup, so it wins when both are present.
*
* OVERLAY-SURFACES — with no persistent edge, an overlay fact
* grants exactly its possibility on a direct
* relation.
* PERSISTENT-WINS — persistent + overlay -> persistent value;
* removing the persistent edge surfaces the
* overlay.
* OVERLAY-BINARY-PARITY — binary and normal agree on the same overlay
* (binary.allow === (normal >= 0.8), and the
* direct path returns the overlay possibility
* in both modes).
* OVERLAY-ON-COMPLEX — an overlay fact on a DIRECT relation that a
* complex policy consumes (union: direct_access;
* chain: member/parent/owns) surfaces through
* that policy. Overlay facts on a relation name
* that is itself configured tuple_to_userset
* are ignored by the TTU evaluator, so the
* overlay must ride the direct edge.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { rigor } from '@rigor/core';
import { PartialGraphContext } from '../../src/core/PartialGraphContext.js';
import { makeCommunityGraph, makeHierarchyGraph } from './complex-graphs.js';
const EPS = 1e-9;
const P_OVERLAY = [0.2, 0.4, 0.6, 0.8, 0.9];
const P_PERSISTENT = 0.85;
const GENERATORS = [
{ name: 'community', make: makeCommunityGraph, directRel: 'direct_access', complexRel: 'can_read_with_direct' },
{ name: 'hierarchy', make: makeHierarchyGraph, directRel: 'owns', complexRel: 'can_access_org' }
];
function fail(message) {
throw new Error(message);
}
describe('Complex-graph overlay crucibles (rigor)', () => {
it('OVERLAY-SURFACES / PERSISTENT-WINS / BINARY-PARITY / ON-COMPLEX hold across complex graphs', async () => {
async function check(args) {
const { genKind, seed, layer } = args;
const spec = GENERATORS.find(g => g.name === genKind);
const g = spec.make(seed);
const arbiter = g.arbiter;
const u = g.users[0];
const pOverlay = P_OVERLAY[layer % P_OVERLAY.length];
let o;
if (genKind === 'community') {
// Pick a resource with NO persistent can_read_with_direct path, so
// the overlay is the only source for the complex-policy check.
o = g.resources.find(r => arbiter.check(u, spec.complexRel, r).possibility === 0);
if (!o) fail(`[surfaces] no zero-persistent resource on community seed=${seed}`);
// Drop any persistent direct_access edge on the triple.
for (const r of (g.relations || [])) {
if (r.src === u && r.rel === 'direct_access' && r.dst === o) arbiter.removeRelation(u, 'direct_access', o);
}
} else {
// Hierarchy: users never hold persistent 'owns' edges, so any
// resource is overlay-clean on the direct relation.
o = g.resources[0];
}
const ctx = new PartialGraphContext(arbiter, {
relations: [{ src: u, relation: spec.directRel, dst: o, possibility: pOverlay }]
});
const checkWithOverlay = (rel, object, ctxFor) =>
arbiter.check(u, rel, object, { partialGraphContext: ctxFor });
// OVERLAY-SURFACES: no persistent edge -> overlay grants exactly pOverlay.
const surfaced = checkWithOverlay(spec.directRel, o, ctx).possibility;
if (Math.abs(surfaced - pOverlay) > EPS) {
fail(`[surfaces] overlay ${pOverlay} did not surface on ${genKind} ${u}->${o}: ${surfaced}`);
}
// PERSISTENT-WINS: persistent edge wins; removal surfaces the overlay.
arbiter.addRelation(u, spec.directRel, o, { possibility: P_PERSISTENT });
const withBoth = checkWithOverlay(spec.directRel, o, ctx).possibility;
if (Math.abs(withBoth - P_PERSISTENT) > EPS) {
fail(`[wins] persistent ${P_PERSISTENT} did not win over overlay ${pOverlay}: ${withBoth}`);
}
arbiter.removeRelation(u, spec.directRel, o);
const resurfaced = checkWithOverlay(spec.directRel, o, ctx).possibility;
if (Math.abs(resurfaced - pOverlay) > EPS) {
fail(`[wins] overlay did not resurface after persistent removal: ${resurfaced}`);
}
// OVERLAY-BINARY-PARITY on the direct relation.
const normal = checkWithOverlay(spec.directRel, o, ctx);
const binary = arbiter.check(u, spec.directRel, o, { partialGraphContext: ctx, binary: true });
if (Math.abs(binary.possibility - normal.possibility) > EPS) {
fail(`[binary] direct overlay binary=${binary.possibility} normal=${normal.possibility} disagree`);
}
if (binary.allow !== (normal.possibility >= 0.8)) {
fail(`[binary] binary.allow=${binary.allow} != normal>=0.8 (${normal.possibility})`);
}
// OVERLAY-ON-COMPLEX: the overlay rides a direct relation the policy
// consumes and surfaces through the complex relation.
if (genKind === 'community') {
const viaUnion = checkWithOverlay(spec.complexRel, o, ctx).possibility;
if (Math.abs(viaUnion - pOverlay) > EPS) {
fail(`[complex] overlay ${pOverlay} did not surface through union ${spec.complexRel}: ${viaUnion}`);
}
} else {
// can_access_org = member -> parent -> owns. The overlay provides the
// full chain; the last hop carries the overlay strength.
const team = g.teams[0];
const dept = team.split(':team:')[0];
const org = 'org:0';
const chainCtx = new PartialGraphContext(arbiter, {
relations: [
{ src: u, relation: 'member', dst: team, possibility: 1 },
{ src: team, relation: 'parent', dst: dept, possibility: 1 },
{ src: dept, relation: 'owns', dst: org, possibility: pOverlay }
]
});
const viaChain = checkWithOverlay(spec.complexRel, org, chainCtx).possibility;
if (Math.abs(viaChain - pOverlay) > EPS) {
fail(`[complex] overlay ${pOverlay} did not surface through chain ${spec.complexRel}: ${viaChain}`);
}
}
return { ok: true };
}
const report = await rigor.campaign(
[
rigor.fn('check', check, rigor.args(
rigor.gen.object({
genKind: rigor.gen.oneOf(GENERATORS.map(g => g.name)),
seed: rigor.gen.int(1, 6),
layer: rigor.gen.int(0, 4)
})
))
],
rigor.crucible([
rigor.invariant('overlay-surfaces', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[surfaces]')),
rigor.invariant('persistent-wins', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[wins]')),
rigor.invariant('overlay-binary-parity', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[binary]')),
rigor.invariant('overlay-on-complex', ({ error, errorMessage }) => !error || !String(errorMessage).startsWith('[complex]'))
])
).run({ effort: 200, seed: 'complex-graph-overlay-crucible', artifacts: { dir: '', persist: 'never' } });
for (const name of ['overlay-surfaces', 'persistent-wins', 'overlay-binary-parity', 'overlay-on-complex']) {
const inv = report.crucibleVerdict?.invariants?.find(i => i.name === name);
assert.ok(inv, `invariant ${name} missing`);
assert.equal(inv.passed, true, `overlay ${name} violated in ${inv.failureCount} cases`);
}
});
});