packaging: README (NASA style) + possibilistic perf baseline + CI benchmark job
CI / test (push) Successful in 4m11s
CI / benchmark (push) Successful in 28s
CI / publish (push) Has been skipped

README: purpose-first (possibility not boolean, caller owns evidence/time),
install, verified quick start, concepts (result shape, overlays, temporal
context), API table, development commands, design notes.

benchmark: scripts/benchmark.js on @tenere/benchmark-lib — eight contours
(direct/union/denied/meta/overlay/binary checks, snapshot build/restore),
committed .rigor-baseline.json, exit 1 on high-severity regressions.

CI: benchmark job compares on push (continue-on-error), re-saves baseline
and uploads it as an artifact on tags; publish now depends on benchmark
passing as well as test.
This commit is contained in:
John Dvorak
2026-08-02 12:52:21 -07:00
parent 2d27321b38
commit 223cfb97c3
6 changed files with 508 additions and 3 deletions
+66 -1
View File
@@ -35,10 +35,75 @@ jobs:
- name: Rigor campaigns - name: Rigor campaigns
run: npm run test:rigor run: npm run test:rigor
benchmark:
runs-on: ubuntu-latest
needs: test
if: startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/master'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
fetch-tags: true
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Auth for Gitea npm registry
run: |
echo "@rigor:registry=https://hub.kl1.tenere.ai/api/packages/Rigor/npm/" > .npmrc
echo "//hub.kl1.tenere.ai/api/packages/Rigor/npm/:_authToken=${{ secrets.PACKAGE_TOKEN }}" >> .npmrc
echo "@tenere:registry=https://hub.kl1.tenere.ai/api/packages/Tenere/npm/" >> .npmrc
echo "//hub.kl1.tenere.ai/api/packages/Tenere/npm/:_authToken=${{ secrets.PACKAGE_TOKEN }}" >> .npmrc
- run: npm ci
- name: Download previous baseline
uses: dawidd6/action-download-artifact@v6
with:
name: baseline
path: .
continue-on-error: true
- name: Run benchmarks (compare against baseline)
run: node --expose-gc scripts/benchmark.js --json > bench-results.json
continue-on-error: true
- name: Save baseline (on tag)
if: startsWith(github.ref, 'refs/tags/v')
run: node --expose-gc scripts/benchmark.js --save
- name: Upload baseline artifact
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v3
with:
name: baseline
path: .rigor-baseline.json
retention-days: 90
- name: Upload benchmark results
if: always()
uses: actions/upload-artifact@v3
with:
name: bench-results
path: bench-results.json
retention-days: 30
- name: Benchmark summary
if: always()
run: |
echo '### Benchmark Results' >> $GITHUB_STEP_SUMMARY
if [ -f bench-results.json ]; then
echo '```json' >> $GITHUB_STEP_SUMMARY
cat bench-results.json >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
fi
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
needs: test needs: [test, benchmark]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
+142
View File
@@ -0,0 +1,142 @@
{
"version": 1,
"generated": "2026-08-02T19:51:19.208Z",
"actions": {
"check[direct-hit]": {
"mostPlausible": 0.25810395981996936,
"alphaCuts": {
"p50": {
"lower": 0.16434574601481775,
"upper": 0.40535024830520205
},
"p95": {
"lower": 0.10394999999999016,
"upper": 1.2158777106838974
},
"p99": {
"lower": 0.10394999999999016,
"upper": 2.040950000000002
}
}
},
"check[union-ttu]": {
"mostPlausible": 0.3581109933665416,
"alphaCuts": {
"p50": {
"lower": 0.07645000000001517,
"upper": 0.35811139703768785
},
"p95": {
"lower": 0.07645000000001517,
"upper": 0.35811139703768785
},
"p99": {
"lower": 0.07645000000001517,
"upper": 0.35811139703768785
}
}
},
"check[denied-miss]": {
"mostPlausible": 0.21718442918988012,
"alphaCuts": {
"p50": {
"lower": 0.18384889559816292,
"upper": 0.2565642603295502
},
"p95": {
"lower": 0.13274999999997988,
"upper": 0.3601500000000823
},
"p99": {
"lower": 0.13274999999997988,
"upper": 0.3601500000000823
}
}
},
"check[include-meta]": {
"mostPlausible": 0.9937660048895368,
"alphaCuts": {
"p50": {
"lower": 0.9937660048895368,
"upper": 0.9937663566453343
},
"p95": {
"lower": 0.9937660048895368,
"upper": 0.9937663566453343
},
"p99": {
"lower": 0.9937660048895368,
"upper": 0.9937663566453343
}
}
},
"check[overlay-on-top]": {
"mostPlausible": 0.3485105914857449,
"alphaCuts": {
"p50": {
"lower": 0.26044999999998425,
"upper": 0.3881187132654935
},
"p95": {
"lower": 0.26044999999998425,
"upper": 0.5286391152207875
},
"p99": {
"lower": 0.26044999999998425,
"upper": 0.5374790233000701
}
}
},
"check[binary-direct]": {
"mostPlausible": 0.2660225524868295,
"alphaCuts": {
"p50": {
"lower": 0.21220996174732032,
"upper": 0.3334807973509898
},
"p95": {
"lower": 0.16554999999993186,
"upper": 0.5780022226626057
},
"p99": {
"lower": 0.16554999999993186,
"upper": 0.8219500000000078
}
}
},
"snapshot[build-binary]": {
"mostPlausible": 4.119453383803492,
"alphaCuts": {
"p50": {
"lower": 3.638457178151043,
"upper": 4.664037328289517
},
"p95": {
"lower": 3.1707499999999906,
"upper": 6.309350037482752
},
"p99": {
"lower": 3.1707499999999906,
"upper": 7.547749999999944
}
}
},
"snapshot[restore-binary]": {
"mostPlausible": 6.906283652168588,
"alphaCuts": {
"p50": {
"lower": 3.2581500000001067,
"upper": 6.906283919208314
},
"p95": {
"lower": 3.2581500000001067,
"upper": 6.906283919208314
},
"p99": {
"lower": 3.2581500000001067,
"upper": 6.906283919208314
}
}
}
}
}
+105
View File
@@ -0,0 +1,105 @@
# @arbiter/core
> Possibilistic authorization engine: graph indices, relation/reachability queries, rule evaluation over a DSL, and lossless condensed snapshots.
## Why
Authorization policies live on a graph: users hold relations to objects, groups, and roles, and rules derive decisions from those relations. `@arbiter/core` answers one question — *may user U perform relation R on object O?* — with a **possibility** ranking, not a boolean. Callers supply evidence with strengths; the engine fuses it through rule operators (`union`, `intersection`, `exclusion`, `defeasible`, chain, multi-hop, relational comparator) and returns the strongest derivable possibility, the reliability of the decision, and the validity provenance of the ranking.
The engine does not police caller-supplied evidence: you supply validated relation strengths and proofs; the engine derives and fuses. It is a library, not a service — no storage, no transport, no policy source of truth.
## Install
```bash
npm install @arbiter/core
```
The package is ESM-only and requires Node 22 or newer.
## Quick Start
```js
import { Arbiter } from '@arbiter/core';
const arbiter = new Arbiter();
// Nodes: an id and a type.
arbiter.addNode('user:1', 'user');
arbiter.addNode('doc:9', 'doc');
// Relations: a config says how decisions for that relation are derived.
arbiter.setRelationConfig('owner', { type: 'direct' });
arbiter.addRelation('user:1', 'owner', 'doc:9', { possibility: 0.9 });
// The core question.
const result = arbiter.check('user:1', 'owner', 'doc:9');
// { possibility: 0.9, reliability: 1,
// validity: { label: 'heuristic', operator: 'identity', regime: 'arbitrary' },
// reason: 'direct_match' }
```
## Concepts
### Possibility, not probability
Every check returns a `possibility` in `[0, 1]` — a maxitive ranking supplied by the caller through relation strengths. `1` means derivable, `0` means not derivable. Fusions take the maximum under `union`, and enforce thresholds and conflicts under `intersection`, `exclusion`, and `defeasible` operators.
### Result shape
Every check result carries the same four fields:
| Field | Type | Meaning |
|-------|------|---------|
| `possibility` | `number` in `[0,1]` | Derived possibility of the decision |
| `reliability` | `number` in `[0,1]` | Reliability of the decision; always `0` for denials |
| `validity` | `object` | Validity provenance: `label`, `operator`, `regime` (minimal form) |
| `reason` | `string` | Outcome class: `direct_match`, `no_relation`, `threshold_not_met`, `missing_node`, `no_config`, `cycle`, ... |
Denied decisions never leak a source's reliability. `includeMeta: true` adds `meta` with the full provenance (allow/deny blocks, rule traces, thresholds) and the full validity block (`sources`, `conflictMass`, `validifiedPossibility`, `nonMaxitive`).
### The caller owns evidence and time
- **Evidence**: relation strengths and validity labels come from the caller. The engine derives and fuses but never judges.
- **Time**: TTL-gated evidence uses the caller's clock. Pass `{ now }` (or `partialGraph.now`) to pin the temporal context; a rerun with the same context reproduces the decision.
### Overlays and partial graphs
`check` accepts a `PartialGraphContext` overlay. Overlay relations take precedence over the base graph, letting you answer "what changes if this evidence appears?" without mutating the graph.
## API
The public surface is the `Arbiter` class:
- **Graph**: `addNode`, `addRelation`, `removeRelation`, `setRelationConfig`, `getNodeData`, `resolveNodeId`, `resolveKey`
- **Check**: `check(userKey, relation, objectKey, options)`, `explain` (enriches `meta`), `binary` mode (fast path, marks results `binary: true`)
- **Reachability**: `isReachable`, `getReachableNodes`, `getReachingNodes`, `shortestPathLength`, `estimateGraphDistance` (`isReachable` returns `null` when no PLTC index is available — a signal to defer to rule evaluation)
- **Snapshots**: `enableCondensedSnapshot`, `toSnapshotBinary`, `Arbiter.fromSnapshotBinary` (lossless: carries relation metadata, TTLs, and validity; malformed buffers fail fast with clean errors)
- **Value context**: `valueManager` (TTL-gated evidence), `getSituationTree`, `monteCarloWalk`
Check `options`:
| Option | Type | Default | Meaning |
|--------|------|---------|---------|
| `includeMeta` | `boolean` | `false` | Attach full provenance in `meta` |
| `explain` | `boolean` | `false` | Enrich `meta` with the evaluation trace |
| `binary` | `boolean` | `false` | Binary fast path; results marked `binary: true` |
| `now` | `number` | engine clock | Pinned temporal context for TTL gates |
| `partialGraphContext` | `PartialGraphContext` | none | Overlay taking precedence over the base graph |
## Development
```bash
npm install # install dependencies
npm test # full suite
npm run test:rigor # js-rigor campaigns (property-based + fuzzing)
npm run benchmark # compare against the committed perf baseline
npm run benchmark:save # record a new perf baseline
```
CI runs the full suite, the rigor campaigns, and the benchmark on every push; `v*` tags additionally publish the package to the `@arbiter` registry.
## Design Notes
- **Possibility is a maxitive ranking.** Fusions preserve the weakest validity label under arbitrary dependence; conjunctive operators surface conflict mass instead of silently averaging it.
- **One evaluation path.** The rule engine has a single, uncompiled evaluator — parity between normal, binary, partial-graph, and snapshot-restored checks is structural, and the rigor campaigns enforce it.
- **Snapshots are a trust boundary.** Restoring untrusted bytes must produce a clean, bounded error — never a hang, a crash, or silently corrupted data. The deserializer cross-validates every count field before use.
+12
View File
@@ -15,6 +15,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rigor/core": "*", "@rigor/core": "*",
"@tenere/benchmark-lib": "^2.0.1",
"fast-check": "^4.5.3", "fast-check": "^4.5.3",
"peggy": "^5.0.6" "peggy": "^5.0.6"
} }
@@ -305,6 +306,17 @@
"node": ">=18.0.0" "node": ">=18.0.0"
} }
}, },
"node_modules/@tenere/benchmark-lib": {
"version": "2.0.1",
"resolved": "https://hub.kl1.tenere.ai/api/packages/Tenere/npm/%40tenere%2Fbenchmark-lib/-/2.0.1/benchmark-lib-2.0.1.tgz",
"integrity": "sha512-ZM+bAVmrg9r/4vrCkNd5sIxQtSzfKOZcg8XFACPhPNajNQDO10VFPzukQrjJ4bmD7Poc4iplAHujKPoH6GXirQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rigor/core": "^3.0.4",
"@rigor/search": "^0.1.2"
}
},
"node_modules/@tenere/graph-core": { "node_modules/@tenere/graph-core": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://hub.kl1.tenere.ai/api/packages/Tenere/npm/%40tenere%2Fgraph-core/-/1.0.1/graph-core-1.0.1.tgz", "resolved": "https://hub.kl1.tenere.ai/api/packages/Tenere/npm/%40tenere%2Fgraph-core/-/1.0.1/graph-core-1.0.1.tgz",
+4 -2
View File
@@ -34,7 +34,7 @@
"test:perf": "RUN_PERF_TESTS=1 node --test --test-force-exit tests/engine/core-performance-targets.test.js", "test:perf": "RUN_PERF_TESTS=1 node --test --test-force-exit tests/engine/core-performance-targets.test.js",
"generate:parser": "node scripts/generate-parser.js", "generate:parser": "node scripts/generate-parser.js",
"build:ast": "npm run generate:parser", "build:ast": "npm run generate:parser",
"benchmark": "node benchmarks/core-performance-benchmark.js", "benchmark": "node --expose-gc scripts/benchmark.js",
"benchmark:core": "node benchmarks/core-performance-benchmark.js", "benchmark:core": "node benchmarks/core-performance-benchmark.js",
"benchmark:batch": "node benchmarks/batch-size-analysis.js", "benchmark:batch": "node benchmarks/batch-size-analysis.js",
"benchmark:chain": "node benchmarks/chain-rule-benchmark.js", "benchmark:chain": "node benchmarks/chain-rule-benchmark.js",
@@ -47,7 +47,8 @@
"benchmark:snapshot": "node benchmarks/arbiter-snapshot-boot-bench.js", "benchmark:snapshot": "node benchmarks/arbiter-snapshot-boot-bench.js",
"benchmark:sharded": "node benchmarks/sharded-snapshot-build.js", "benchmark:sharded": "node benchmarks/sharded-snapshot-build.js",
"benchmark:memory": "node benchmarks/memory-breakdown.js", "benchmark:memory": "node benchmarks/memory-breakdown.js",
"benchmark:multi-hop": "node benchmarks/multi-hop-rule-bench.js" "benchmark:multi-hop": "node benchmarks/multi-hop-rule-bench.js",
"benchmark:save": "node --expose-gc scripts/benchmark.js --save"
}, },
"keywords": [ "keywords": [
"zanzibar", "zanzibar",
@@ -64,6 +65,7 @@
}, },
"devDependencies": { "devDependencies": {
"@rigor/core": "*", "@rigor/core": "*",
"@tenere/benchmark-lib": "^2.0.1",
"fast-check": "^4.5.3", "fast-check": "^4.5.3",
"peggy": "^5.0.6" "peggy": "^5.0.6"
} }
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env node
/**
* CI benchmark script for @arbiter/core — powered by @tenere/benchmark-lib
* (possibilistic contours with valid alpha cuts).
*
* node --expose-gc scripts/benchmark.js compare against baseline
* node --expose-gc scripts/benchmark.js --save save new baseline
* node --expose-gc scripts/benchmark.js --json JSON output
* node --expose-gc scripts/benchmark.js --baseline <path> custom baseline path
*
* Exit codes:
* 0 — all benchmarks pass, no high-severity regressions
* 1 — high-severity regression found
* 2 — crash (benchmark threw unexpectedly)
*/
import fs from 'fs'
import { benchmark, createBaseline, detectRegressions, formatRegressions } from '@tenere/benchmark-lib'
import { Arbiter } from '../src/core/Arbiter.js'
import { PartialGraphContext } from '../src/core/PartialGraphContext.js'
const SAVE = process.argv.includes('--save')
const AS_JSON = process.argv.includes('--json')
const BASELINE_PATH = (() => {
const idx = process.argv.indexOf('--baseline')
return idx >= 0 ? process.argv[idx + 1] : '.rigor-baseline.json'
})()
benchmark.config({
measurements: ['timing'],
uncertaintyThreshold: 0.99,
minSamples: 0,
maxSamples: 200,
overheadCompensation: true,
gcBetweenSamples: true,
})
let CRASHED = 0
// ── Fixtures ──────────────────────────────────────────────────────────
function buildEngine() {
const a = new Arbiter()
const groupCount = 40
for (let g = 0; g < groupCount; g++) a.addNode(`group:${g}`, 'group')
for (let u = 0; u < 200; u++) {
a.addNode(`user:${u}`, 'user')
a.addNode(`doc:${u}`, 'doc')
}
a.setRelationConfig('owner', { type: 'direct' })
a.setRelationConfig('member_of', { type: 'direct' })
a.setRelationConfig('can_read', {
type: 'union',
rules: [{ kind: 'direct', relation: 'owner' }, { kind: 'tuple_to_userset', parentRelation: 'member_of', childRelation: 'owner' }]
})
for (let u = 0; u < 200; u++) {
a.addRelation(`user:${u}`, 'owner', `doc:${u}`, { possibility: 0.9 + (u % 10) / 100 })
a.addRelation(`user:${u}`, 'member_of', `group:${u % groupCount}`, { possibility: 0.7 })
}
for (let g = 0; g < groupCount; g++) a.addRelation(`group:${g}`, 'owner', `doc:${g}`, { possibility: 1.0 })
return a
}
const engine = buildEngine()
const directBench = benchmark('check[direct-hit]', () => {
engine.check('user:1', 'owner', 'doc:1')
})
const unionBench = benchmark('check[union-ttu]', () => {
engine.check('user:1', 'can_read', 'doc:1')
})
const deniedBench = benchmark('check[denied-miss]', () => {
engine.check('user:5', 'owner', 'doc:1')
})
const metaBench = benchmark('check[include-meta]', () => {
engine.check('user:1', 'can_read', 'doc:1', { includeMeta: true })
})
const overlayBench = benchmark('check[overlay-on-top]', () => {
const ctx = new PartialGraphContext(engine, {
relations: [{ src: 'user:1', relation: 'owner', dst: 'doc:1', possibility: 0.5 }]
})
engine.check('user:1', 'owner', 'doc:1', { partialGraphContext: ctx })
})
const binaryBench = (() => {
const snap = buildEngine()
snap.enableCondensedSnapshot()
return benchmark('check[binary-direct]', () => {
snap.check('user:1', 'owner', 'doc:1', { binary: true })
})
})()
const snapshotBuildBench = (() => {
const snap = buildEngine()
snap.enableCondensedSnapshot()
return benchmark('snapshot[build-binary]', () => {
snap.toSnapshotBinary()
})
})()
const snapshotRestoreBench = (() => {
const snap = buildEngine()
snap.enableCondensedSnapshot()
const buf = snap.toSnapshotBinary()
return benchmark('snapshot[restore-binary]', () => {
Arbiter.fromSnapshotBinary(buf)
})
})()
// ── Run ───────────────────────────────────────────────────────────────
function measure(handle) {
try {
return handle.contour('timing')
} catch (e) {
CRASHED++
console.error(`${e.message}`)
return null
}
}
const results = {}
const order = [directBench, unionBench, deniedBench, metaBench, overlayBench, binaryBench, snapshotBuildBench, snapshotRestoreBench]
for (const handle of order) {
const contour = measure(handle)
if (!contour) continue
const mp = contour.mostPlausible()
results[handle.name] = {
mostPlausible: mp.value,
alphaCuts: contour.alphaCuts(),
}
}
const current = { actions: results, campaign: null, failures: 0 }
if (AS_JSON) {
const out = { results: {}, regressions: [], improvements: [], crashed: CRASHED }
for (const [name, r] of Object.entries(results)) {
out.results[name] = { timing: { mp: r.mostPlausible, alphaCuts: r.alphaCuts } }
}
if (fs.existsSync(BASELINE_PATH) && !SAVE) {
const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8'))
const regResult = detectRegressions(current, baseline)
out.regressions = regResult.regressions
out.improvements = regResult.improvements
}
console.log(JSON.stringify(out, null, 2))
} else {
for (const [name, r] of Object.entries(results)) {
const cuts = r.alphaCuts
console.log(`${name}: mp=${r.mostPlausible.toFixed(4)}ms p95=[${cuts.p95.lower.toFixed(4)},${cuts.p95.upper.toFixed(4)}]`)
}
}
if (SAVE) {
const baseline = createBaseline([current])
fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2), 'utf8')
console.log(`baseline saved: ${BASELINE_PATH}`)
} else if (fs.existsSync(BASELINE_PATH)) {
const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, 'utf8'))
const regResult = detectRegressions(current, baseline)
if (!AS_JSON) console.log(formatRegressions(regResult, 'pretty'))
const critical = regResult.regressions.filter(r => r.severity === 'high')
if (critical.length > 0) {
console.error(`${critical.length} critical regression(s): ${critical.map(r => r.name).join(', ')}`)
process.exit(1)
}
} else if (!SAVE) {
console.log('no baseline — first run, use --save')
}
if (CRASHED > 0) {
console.error(`${CRASHED} benchmark(s) crashed`)
process.exit(SAVE ? 0 : 2)
}