# @arbiter/core

> Possibilistic authorization engine: graph indices, relation/reachability queries, rule evaluation over declarative configurations, and lossless condensed snapshots.

> The Evidence DSL (a natural-language layer that compiles to these configurations) lives in the separate [`@arbiter/evidence-dsl`](https://hub.kl1.tenere.ai/Arbiter/evidence-dsl) package.

## 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.

**TTL is a value-freshness gate, not an access-expiry mechanism.** `valueManager.setTTL(relation, ms)` controls how long a relation's *value* stays fresh for value-consuming paths (relational comparators, chain/multi-hop value collection): once `age > TTL` the value is treated as absent, which denies the comparator and drops the value from collected values. Possibility-based grants — a direct relation's allow/deny, union disjunction, chain traversal — are **timeless**: an edge grants regardless of its age. If you need access to expire, express it in the policy (e.g. a comparator over a time-carrying value), not via `setTTL`.

**Clocks and caches.** The decision caches (direct-check, rule-result, chain) are keyed on the meta-less decision form only: `includeMeta` callers and pinned-clock callers always get a fresh evaluation, and value-carrying results are never cached (values are TTL-gated evidence). Unpinned callers share wall-clock cache entries — the correct default for timeless decisions. Pin `{ now }` whenever the answer depends on when you ask; every cache bypasses itself for pinned-clock callers, so a rerun with the same `{ now }` reproduces the decision exactly.

### 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 full reference — every export, method signature, option, result shape, configuration format, error, and reason code — is in [docs/API.md](./docs/API.md). The public surface at a glance:

- **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), `PartialGraphContext` (overlays), `getSituationTree`, `monteCarloWalk`

## Capabilities

The engine derives authorization decisions from a relation graph. What it does, in one pass:

- **Ten policy kinds** compose arbitrarily: direct, tuple-to-userset (groups), chain, multi-hop, defeasible (when/unless/never/always/requires), union/intersection/exclusion (with OWA fusion), relational comparator (ABAC over values), qualitative comparator (decaying scales), challenge (proofs/MFA), parent.
- **Possibilistic semantics**: decisions are maxitive rankings in `[0,1]`, not booleans — with reliability, validity provenance, and reason codes on every result.
- **Caller-owned time**: every TTL gate, decay, and proof expiry honors the caller's pinned `{ now }`; a rerun reproduces the decision.
- **Overlays**: evaluate "what if this evidence existed" without mutating the graph; caller-supplied facts ride the policy's direct relations.
- **Lossless snapshots**: condensed binary serialization (~170 bytes/node) with frozen restore; malformed input fails fast and bounded.
- **Reachability**: exact PLTC index with sound fast-fail and a documented `null`-defer contract.
- **Determinism**: 251 seeded rigor campaigns — parity across normal/binary/snapshot-restored evaluation, mutation freshness, TTL contracts, complexity classes, and adversarial snapshot fuzzing.

What it does **not** do: no storage, no transport, no policy source of truth, no user/group management — it is a library that answers one question: *may user U perform relation R on object O?*

## 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.
