feat: tuple_to_userset intermediate reachability; ratio-based benchmark gate
CI / publish (push) Successful in 15s
CI / test (push) Successful in 5m7s
CI / benchmark (push) Successful in 53s

- ChainRule._expandRuleFromSrc now expands tuple_to_userset configs: from a
  source node the reachable set is the objects sharing an intermediate with
  the source (src ->computed-> intermediate ->tupleset-> object, direction
  aware, weakest-link combined). Lets a TTU evidence serve as an intermediate
  condition step in a chain.
- scripts/benchmark.js: ratio-based self-calibration. Comparing each action's
  RATIO to a cheap reference action (default check[direct-hit]) cancels
  machine-load swings that scale all actions proportionally, so the gate only
  fails on code regressions that shift a single action's ratio. The reference
  is still checked absolutely with a loose bound. Verified: stable across
  runs, and a simulated union-ttu slowdown is caught (+76.8% ratio).

Tests: chain-condition-step intermediate TTU expansion.
This commit is contained in:
John Dvorak
2026-08-03 15:43:48 -07:00
parent ab0e569552
commit bd9c74fb0e
5 changed files with 167 additions and 69 deletions
+57 -57
View File
@@ -1,140 +1,140 @@
{ {
"version": 1, "version": 1,
"generated": "2026-08-03T21:02:18.659Z", "generated": "2026-08-03T22:37:59.060Z",
"actions": { "actions": {
"check[direct-hit]": { "check[direct-hit]": {
"mostPlausible": 0.13270348756111364, "mostPlausible": 0.10672237227527051,
"alphaCuts": { "alphaCuts": {
"p50": { "p50": {
"lower": 0.13113648835948058, "lower": 0.105663874043709,
"upper": 0.13428864360995807 "upper": 0.10779178510155751
}, },
"p95": { "p95": {
"lower": 0.12820153883971488, "lower": 0.10367558838055704,
"upper": 0.13736359391644548 "upper": 0.10985914641251807
}, },
"p99": { "p99": {
"lower": 0.12681899549493264, "lower": 0.10273583797909441,
"upper": 0.13886133499002185 "upper": 0.11086333384764993
} }
} }
}, },
"check[union-ttu]": { "check[union-ttu]": {
"mostPlausible": 0.13272407787199741, "mostPlausible": 0.11249228961043199,
"alphaCuts": { "alphaCuts": {
"p50": { "p50": {
"lower": 0.13111540538562938, "lower": 0.111604019277894,
"upper": 0.13435208823425798 "upper": 0.11338800600185026
}, },
"p95": { "p95": {
"lower": 0.12810370624158607, "lower": 0.10993048219120201,
"upper": 0.13751164134564425 "upper": 0.11511402952231622
}, },
"p99": { "p99": {
"lower": 0.12668522950869812, "lower": 0.1091378967617418,
"upper": 0.1390508250938302 "upper": 0.11594970606842417
} }
} }
}, },
"check[denied-miss]": { "check[denied-miss]": {
"mostPlausible": 0.10464274362739566, "mostPlausible": 0.08747524001151907,
"alphaCuts": { "alphaCuts": {
"p50": { "p50": {
"lower": 0.1035911653567046, "lower": 0.08668417849217541,
"upper": 0.10570542262187044 "upper": 0.08827353325683636
}, },
"p95": { "p95": {
"lower": 0.10161518016378715, "lower": 0.085196295997943,
"upper": 0.10776062041045117 "upper": 0.08981478022606769
}, },
"p99": { "p99": {
"lower": 0.10068232198106161, "lower": 0.08449268650925873,
"upper": 0.10875916306014438 "upper": 0.09056310270952082
} }
} }
}, },
"check[include-meta]": { "check[include-meta]": {
"mostPlausible": 0.2032086398120905, "mostPlausible": 0.17368093893555905,
"alphaCuts": { "alphaCuts": {
"p50": { "p50": {
"lower": 0.20130903880967277, "lower": 0.17235808249234505,
"upper": 0.20512602330365767 "upper": 0.17501399072522753
}, },
"p95": { "p95": {
"lower": 0.19773841496722822, "lower": 0.1698647154489402,
"upper": 0.20882979063960022 "upper": 0.17758260042495247
}, },
"p99": { "p99": {
"lower": 0.19605028516281864, "lower": 0.16868343694884977,
"upper": 0.21062828369506048 "upper": 0.178826872871734
} }
} }
}, },
"check[overlay-on-top]": { "check[overlay-on-top]": {
"mostPlausible": 0.43801922339274096, "mostPlausible": 0.34658090606556824,
"alphaCuts": { "alphaCuts": {
"p50": { "p50": {
"lower": 0.4331486167515239, "lower": 0.343563635087355,
"upper": 0.4429442096584253 "upper": 0.3496248120197711
}, },
"p95": { "p95": {
"lower": 0.4240166116932131, "lower": 0.33788494447103923,
"upper": 0.45248423243066815 "upper": 0.3555006559358586
}, },
"p99": { "p99": {
"lower": 0.4197093259995535, "lower": 0.33519739588838326,
"upper": 0.45712770847629536 "upper": 0.35835082770353416
} }
} }
}, },
"check[binary-direct]": { "check[binary-direct]": {
"mostPlausible": 0.12194744387843882, "mostPlausible": 0.10136432040214981,
"alphaCuts": { "alphaCuts": {
"p50": { "p50": {
"lower": 0.12053468994850938, "lower": 0.10038730449515523,
"upper": 0.12337633544038318 "upper": 0.10235072528492808
}, },
"p95": { "p95": {
"lower": 0.1178875990998377, "lower": 0.0985518364498951,
"upper": 0.1261469438761139 "upper": 0.10425698760463886
}, },
"p99": { "p99": {
"lower": 0.11663982372031695, "lower": 0.09768455509480933,
"upper": 0.12749639398836587 "upper": 0.10518292733500886
} }
} }
}, },
"snapshot[build-binary]": { "snapshot[build-binary]": {
"mostPlausible": 4.699470380992978, "mostPlausible": 2.848037533444555,
"alphaCuts": { "alphaCuts": {
"p50": { "p50": {
"lower": 4.649083103245884, "lower": 2.827413825511722,
"upper": 4.750403850161681 "upper": 2.868811889350609
}, },
"p95": { "p95": {
"lower": 4.554543327557406, "lower": 2.788520370277795,
"upper": 4.849008463493959 "upper": 2.9088252291455836
}, },
"p99": { "p99": {
"lower": 4.509934735938718, "lower": 2.770077133158435,
"upper": 4.896973552928932 "upper": 2.9281936091667653
} }
} }
}, },
"snapshot[restore-binary]": { "snapshot[restore-binary]": {
"mostPlausible": 5.015702105071855, "mostPlausible": 2.9488926124434385,
"alphaCuts": { "alphaCuts": {
"p50": { "p50": {
"lower": 4.960977858992745, "lower": 2.9247809483999716,
"upper": 5.071029889482844 "upper": 2.9732031186198484
}, },
"p95": { "p95": {
"lower": 4.858330254069017, "lower": 2.8793716893148757,
"upper": 5.178172214368928 "upper": 3.020092288118393
}, },
"p99": { "p99": {
"lower": 4.809905283270391, "lower": 2.8578657882531426,
"upper": 5.2303028693638565 "upper": 3.0428172805315117
} }
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@arbiter/core", "name": "@arbiter/core",
"version": "1.0.4", "version": "1.0.5",
"description": "Arbiter core engine: graph indices, relation/reachability, authorization rule evaluator, DSL/AST, condensed & sharded snapshots, and evidence fusion.", "description": "Arbiter core engine: graph indices, relation/reachability, authorization rule evaluator, DSL/AST, condensed & sharded snapshots, and evidence fusion.",
"license": "ISC", "license": "ISC",
"author": "", "author": "",
+43 -10
View File
@@ -207,17 +207,50 @@ if (SAVE) {
const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8')) const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8'))
const regResult = detectRegressions(current, baseline) const regResult = detectRegressions(current, baseline)
if (!AS_JSON) console.log(formatRegressions(regResult, 'pretty')) if (!AS_JSON) console.log(formatRegressions(regResult, 'pretty'))
// The lib flags a regression whenever current > baseline.p95/p99, which is
// tighter than run-to-run noise at sub-ms scales (baseline alphaCuts capture // ── Ratio-based self-calibration ─────────────────────────────────────
// per-run sample spread, not cross-run variance). Require a minimum change // Absolute timings swing with machine load (a shared runner at load 19
// before treating a high-severity flag as a CI failure, so the gate fails on // shifted every action +15..+50%). Comparing each action's RATIO to a cheap
// real regressions (>10% past the p99) instead of ~3-5% noise. // reference action cancels the load: load scales all actions proportionally,
const MIN_HIGH_REGRESSION_PERCENT = Number(process.env.MIN_HIGH_REGRESSION_PERCENT ?? 10) // while a code regression shifts only the affected action's ratio. The
const critical = regResult.regressions.filter( // reference action itself is still gate-checked absolutely with a loose
r => r.severity === 'high' && r.changePercent >= MIN_HIGH_REGRESSION_PERCENT // bound (a regression of the reference would otherwise mask every ratio).
const RATIO_REFERENCE = process.env.BENCH_RATIO_REFERENCE || 'check[direct-hit]'
const RATIO_PERCENT = Number(process.env.RATIO_REGRESSION_PERCENT ?? 15)
const REFERENCE_PERCENT = Number(process.env.REFERENCE_REGRESSION_PERCENT ?? 30)
const currentRef = current.actions?.[RATIO_REFERENCE]?.mostPlausible
const baselineRef = baseline.actions?.[RATIO_REFERENCE]?.mostPlausible
const ratioViolations = []
let referenceViolation = false
if (currentRef > 0 && baselineRef > 0) {
if (currentRef > baselineRef * (1 + REFERENCE_PERCENT / 100)) {
referenceViolation = true
ratioViolations.push(
`${RATIO_REFERENCE} (reference) baseline=${baselineRef.toFixed(4)} → current=${currentRef.toFixed(4)} (+${(((currentRef / baselineRef) - 1) * 100).toFixed(1)}%)`
) )
if (critical.length > 0) { }
console.error(`${critical.length} critical regression(s): ${critical.map(r => r.name).join(', ')}`) for (const [name, cur] of Object.entries(current.actions)) {
if (name === RATIO_REFERENCE) continue
const base = baseline.actions?.[name]
if (!base || !(base.mostPlausible > 0)) continue
const curRatio = cur.mostPlausible / currentRef
const baseRatio = base.mostPlausible / baselineRef
if (curRatio > baseRatio * (1 + RATIO_PERCENT / 100)) {
ratioViolations.push(
`${name} ratio ${curRatio.toFixed(3)} → baseline ${baseRatio.toFixed(3)} (+${(((curRatio / baseRatio) - 1) * 100).toFixed(1)}%)`
)
}
}
}
if (ratioViolations.length > 0) {
console.error(`${ratioViolations.length} ratio regression(s):`)
for (const v of ratioViolations) console.error(` - ${v}`)
if (referenceViolation) {
console.error(' (the reference action regressed absolutely — ratios may be unreliable)')
}
process.exit(1) process.exit(1)
} }
} else if (!SAVE) { } else if (!SAVE) {
+36
View File
@@ -515,6 +515,9 @@ export class ChainRule extends BaseRule {
if (rule.type === 'chain' && Array.isArray(rule.steps)) { if (rule.type === 'chain' && Array.isArray(rule.steps)) {
return this._expandChainRuleFromSrc(srcId, rule, options); return this._expandChainRuleFromSrc(srcId, rule, options);
} }
if (rule.type === 'tuple_to_userset') {
return this._expandTtuFromSrc(srcId, rule, options);
}
if (rule.type === 'logical') { if (rule.type === 'logical') {
if (rule.when || rule.unless || rule.never || rule.requires || rule.always) { if (rule.when || rule.unless || rule.never || rule.requires || rule.always) {
return this._expandDefeasibleFromSrc(srcId, rule, options); return this._expandDefeasibleFromSrc(srcId, rule, options);
@@ -538,6 +541,39 @@ export class ChainRule extends BaseRule {
return out; return out;
} }
/**
* Expand a tuple_to_userset config from a source node. Semantics (forward,
* the common case): src →computed→ intermediate, and intermediate ↔object
* via tupleset (direction decides which end the intermediate sits on). The
* reachable set is the OBJECTS sharing an intermediate with src. Combined
* possibility is the weakest link across the two hops.
*/
_expandTtuFromSrc(srcId, rule, options = null) {
const { tuplesetRelation, computedRelation, tuplesetDirection = 'out', reverse = false } = rule;
if (!tuplesetRelation || !computedRelation) return new Map();
// reverse swaps the roles: src is the object, intermediates come from the
// tupleset side, and computed edges go from intermediate to the user.
const srcAsUser = !reverse;
const computedRel = srcAsUser ? computedRelation : tuplesetRelation;
const tuplesetRel = srcAsUser ? tuplesetRelation : computedRelation;
const computedEdges = this.arbiter.relationManager.getRelationsFromSrc(srcId, computedRel, options);
const out = new Map();
for (const ce of computedEdges || []) {
const intermediateId = ce.dst;
const tsEdges = tuplesetDirection === 'in'
? this.arbiter.relationManager.getRelationsFromSrc(intermediateId, tuplesetRel, options)
: this.arbiter.relationManager.getRelationsToDst(intermediateId, tuplesetRel, options);
for (const te of tsEdges || []) {
const objId = tuplesetDirection === 'in' ? te.dst : te.src;
const poss = Math.min(ce.possibility ?? 1, te.possibility ?? 1);
const rel = (ce.reliability ?? 1) * (te.reliability ?? 1);
const cur = out.get(objId);
if (!cur || poss > cur.possibility) out.set(objId, { possibility: poss, reliability: rel });
}
}
return out;
}
_expandLogicalNodeFromSrc(srcId, node, op, options = null) { _expandLogicalNodeFromSrc(srcId, node, op, options = null) {
const rules = (node && node.rules) || []; const rules = (node && node.rules) || [];
if (rules.length === 0) return new Map(); if (rules.length === 0) return new Map();
+29
View File
@@ -106,6 +106,35 @@ describe('ChainRule condition step (rule-based final hop)', () => {
assert.equal(denied.possibility, 0); assert.equal(denied.possibility, 0);
}); });
it('expands a tuple_to_userset evidence as an intermediate condition step', () => {
arbiter.setRelationConfig('member_of', { type: 'direct' });
arbiter.setRelationConfig('owner', { type: 'direct' });
arbiter.setRelationConfig('doc_route', { type: 'direct' });
arbiter.addNode('group:g', 'group');
arbiter.addNode('doc:1', 'doc');
arbiter.addNode('doc:2', 'doc');
arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 1.0 });
arbiter.addRelation('group:g', 'owner', 'doc:1', { possibility: 0.8 });
arbiter.addRelation('doc:1', 'doc_route', 'doc:2', { possibility: 0.9 });
const ttuConfig = {
type: 'tuple_to_userset',
tuplesetRelation: 'owner',
computedRelation: 'member_of',
tuplesetDirection: 'in'
};
const rule = {
type: 'chain',
steps: [{ rule: ttuConfig, conditionStep: true }, 'doc_route']
};
// TTU reachable from user = doc:1 (min(1.0, 0.8) = 0.8), then doc_route → 0.9
const res = evalRule('user:u', 'doc:2', rule);
assert.ok(Math.abs(res.possibility - 0.8) < 1e-9, `expected 0.8, got ${res.possibility} (${res.reason})`);
// Direct expansion unit check
const expanded = chainRule._expandRuleFromSrc(arbiter.resolveNodeId('user:u'), ttuConfig, {});
assert.equal(expanded.size, 1);
assert.equal(arbiter.resolveKey([...expanded.keys()][0]), 'doc:1');
});
it('combines across multiple parallel intermediates (max aggregation)', () => { it('combines across multiple parallel intermediates (max aggregation)', () => {
arbiter.addNode('group:g2', 'group'); arbiter.addNode('group:g2', 'group');
arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 0.5 }); arbiter.addRelation('user:u', 'member_of', 'group:g', { possibility: 0.5 });