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:
@@ -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`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user