packaging: README (NASA style) + possibilistic perf baseline + CI benchmark job
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:
@@ -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.
|
||||
Reference in New Issue
Block a user