# @arbiter/core — API Reference

> The complete public interface of the Arbiter engine. For the package overview, quick start, and concepts, see the [README](../README.md). This document is the authoritative reference: every public export, method signature, option, result shape, and configuration format.

## Contents

1. [Exports](#1-exports)
2. [The `Arbiter` class](#2-the-arbiter-class)
3. [`check` options and result shape](#3-check-options-and-result-shape)
4. [Relation configurations](#4-relation-configurations)
5. [The `valueManager`](#5-the-valuemanager)
6. [Partial graphs](#6-partial-graphs)
7. [Snapshots](#7-snapshots)
8. [Reachability](#8-reachability)
9. [The `OWAFusion` utility](#9-the-owafusion-utility)
10. [Audit hook](#10-audit-hook)
11. [Errors](#11-errors)
12. [Reason codes](#12-reason-codes)
13. [Validity taxonomy](#13-validity-taxonomy)

---

## 1. Exports

```js
import {
  Arbiter,              // the engine — everything hangs off this
  GraphIndices,         // persistent-relation index structure
  NodeManager,          // node lifecycle
  RelationManager,      // relation lifecycle + value access
  GraphData, GraphOperations, GraphAnalysis, GraphManager,
  AuthorizationChecker, // the check() implementation (low-level)
  RuleEvaluator,        // rule dispatch
  RuleCollector,        // rule flattening
  OWAFusion             // interval-fusion utilities
} from '@arbiter/core';
```

In normal use you only need `Arbiter`. The other exports exist for advanced composition and are documented inline in source.

---

## 2. The `Arbiter` class

### `new Arbiter(options?)`

**Parameters:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `disableCaching` | `boolean` | `false` | Disable the direct-check, rule-result, and chain caches |
| `disableChainCaching` | `boolean` | `false` | Disable only the chain result cache |
| `directCheckCacheSize` | `number` | `10000` | Direct-check cache capacity |
| `directCheckCacheTTL` | `number` (ms) | `60000` | Direct-check entry freshness |
| `ruleResultCacheTTL` | `number` (ms) | `60000` | Rule-result entry freshness |
| `enableRuleResultCache` | `boolean` | `true` | Master switch for the rule-result cache |
| `clock` | `() => number` | `Date.now` | Time source for **unpinned** cache freshness. Per-check time always comes from `{ now }` / `partialGraph.now` |
| `cacheFactory` | `function` | built-in LRU | DI hook for a custom cache implementation |
| `audit` | `(record) => void` | `null` | Audit hook, invoked once per check (see [§10](#10-audit-hook)) |
| `partialGraphPolicy` | `object` | — | Overlay admission limits (see [§6](#6-partial-graphs)) |
| `keyManager` | `object` | built-in | Advanced: custom id/key mapping |

### Graph and relation management

| Method | Signature | Description |
|--------|-----------|-------------|
| `addNode` | `(key, type, data?) => void` | Create a node. `key` is a string, `type` is a free-form string |
| `getNodeData` | `(key) => object` | Node metadata |
| `updateNodeData` | `(key, data) => void` | Patch node metadata |
| `setRelationConfig` | `(relation, config) => void` | Define how a relation derives decisions (see [§4](#4-relation-configurations)). Re-defining invalidates caches for that relation |
| `addRelation` | `(srcKey, relation, dstKey, options?) => void` | Add an edge. `options`: `{ possibility, reliability, value, validity, changed_last_at, decayConfig, source, layer_name }` |
| `removeRelation` | `(srcKey, relation, dstKey) => void` | Remove an edge. Immediately revokes grants (no stale cache) |
| `resolveNodeId` | `(key) => number \| undefined` | Key → numeric id (`undefined` when unknown) |
| `resolveKey` | `(id) => string \| undefined` | Numeric id → key |
| `invalidateRuleResultCacheByRelation` | `(relation) => void` | Drop cached rule results for a relation |
| `invalidateAllRuleResultCache` | `() => void` | Drop all cached rule results |

**Write semantics.** `addRelation` upserts: re-adding an existing `(src, relation, dst)` edge updates it. `changed_last_at` defaults to the write time when not supplied; values decay and expire against it (see [§5](#5-the-valuemanager)). All mutations invalidate the affected caches immediately — a removed edge's grant is gone on the next `check`.

### Check

| Method | Signature | Description |
|--------|-----------|-------------|
| `check` | `(userKey, relation, objectKey, options?) => result` | The core question: derive the decision |
| `explain` | `(userKey, relation, objectKey, options?) => result` | Alias for `check(..., { explain: true })` — enriches `meta` with the evaluation trace |

See [§3](#3-check-options-and-result-shape) for options and result shape.

### Graph algorithms

| Method | Signature | Description |
|--------|-----------|-------------|
| `estimateGraphDistance` | `(key1, key2) => number` | Estimated distance |
| `shortestPathLength` | `(key1, key2) => number` | Exact shortest path |
| `walk` | `(startKey, steps, stepFn?, neighborWeightsFn?) => void` | Traverse with a visitor |
| `monteCarloWalk` | `(startKey, steps, options?) => void` | Randomized traversal |
| `getSituationTree` | `(nodeId, maxDepth?) => tree` | Neighborhood snapshot |
| `getReachableNodes` | `(sourceKey, maxResults?) => string[]` | Nodes reachable from source |
| `getReachingNodes` | `(targetKey, maxResults?) => string[]` | Nodes that reach target |

### Reachability

| Method | Signature | Description |
|--------|-----------|-------------|
| `initializeReachabilityChecker` | `(options?) => Promise<void>` | Build the PLTC index (async) |
| `isReachable` | `(sourceKey, targetKey) => boolean \| null` | `true`/`false` when the PLTC index decides; `null` = deferred to rule evaluation (documented contract, see [§8](#8-reachability)) |
| `getReachabilityStats` | `() => object` | PLTC hit/miss/query statistics |
| `adaptToFalsePositive` | `(sourceKey, targetKey, falsePositiveKey, conflictingKey) => void` | PLTC adaptation |
| `getAdaptationStats` | `() => object` | Adaptation statistics |

### Snapshots

| Method | Signature | Description |
|--------|-----------|-------------|
| `enableCondensedSnapshot` | `(options?) => void` | Switch to condensed binary mode. **Permanently makes the engine read-only** (writes throw) — call it last |
| `toSnapshotBinary` | `(options?) => ArrayBuffer` | Serialize the graph |

Restore is a static: `Arbiter.fromSnapshotBinary(buffer)` (see [§7](#7-snapshots)).

---

## 3. `check` options and result shape

### Options

| Option | Type | Default | Meaning |
|--------|------|---------|---------|
| `binary` | `boolean` | `false` | Binary fast path with strict thresholds; results carry `binary: true` |
| `includeMeta` | `boolean` | `false` | Attach full provenance in `meta` |
| `explain` | `boolean` | `false` | Enrich `meta` with the evaluation trace |
| `now` | `number` (ms) | engine clock | **Pinned temporal context.** Every TTL gate, decay computation, and proof expiry honors it; a rerun with the same `now` reproduces the decision |
| `partialGraph` | `object` | `null` | Raw overlay spec — re-ingested into a `PartialGraphContext` (see [§6](#6-partial-graphs)) |
| `partialGraphContext` | `PartialGraphContext` | `null` | Pre-built overlay context (preferred when reusing one) |
| `collectValues` | `boolean` | `false` | Collect value evidence into `collectedValues` |
| `returnHints` | `boolean` | `false` | On denial, attach `hints` (alternative access paths) |
| `simpleBinary` | `boolean` | `false` | With `binary`, return the minimal `{ allow, deny, reason }` shape |
| `fastPath` | `boolean` | `false` | Enable early-exit thresholds in rule evaluation |
| `minAllowPossibility` / `maxDenyPossibility` | `number` | `0.8` / `0.8` | Binary-mode decision thresholds |
| `clientStateId` | `string` | `null` | Pass-through client identifier |
| `epsilon` / `delta` | `number` | — | Pass-through numerical tolerances |

### Result shape

Every check returns the same core shape:

```js
{
  possibility: 0.9,        // number in [0,1] — the derived decision strength
  reliability: 1,          // number in [0,1] — 0 for denials
  validity: {
    label: 'heuristic',    // one of the validity taxonomy (see §13)
    operator: 'identity',  // the fusion operator applied
    regime: 'arbitrary'    // dependence assumption
    // includeMeta adds: sources[], conflictMass, validifiedPossibility, nonMaxitive
  },
  reason: 'direct_match',  // outcome class (see §12)
  // optional:
  meta: { ... },           // includeMeta / explain
  collectedValues: [...],  // collectValues
  hints: [...],            // returnHints on denial
  binary: true,            // binary mode
  allow: true,             // binary mode
  deny: false              // binary mode
}
```

**Contract invariants** (enforced by the rigor suite, `tests/rigor/public-contract.test.js`):

- `possibility` and `reliability` are always finite numbers in `[0, 1]`.
- Denials (`possibility === 0`) carry `reliability === 0` — a denied decision never leaks a source's reliability.
- `reason` is always a known outcome class.
- The result is JSON-serializable — no circular references, no undefined values.
- `includeMeta` enriches `meta` but never changes the decision fields.

### Collected values

```js
{
  value: 42,               // the relation's value (or an interval for blurred paths)
  possibility: 0.9,
  path: ['u:1', 'doc:9'],  // entity keys along the derivation
  source: { entityKey: 'u:1', relation: 'can_read', step: 0 },
  metadata: { timestamp, reliability, ... }
}
```

Collected values are **TTL-gated**: an expired value is never collected (see [§5](#5-the-valuemanager)).

---

## 4. Relation configurations

`setRelationConfig(relation, config)` defines how decisions for a relation are derived. The config object is also the DSL's AST — it is compiled (validated) on registration; compile errors land in `config._compileErrors`.

### Direct

```js
{ type: 'direct' }
```

A single edge grants with exactly its possibility. The fastest path.

### Tuple-to-userset (group membership)

```js
{
  type: 'tuple_to_userset',
  tuplesetRelation: 'owner',   // object → intermediate
  computedRelation: 'member',  // user → intermediate
  reverse: false,
  minPossibility: 0,
  owaWeights: [1, 0, ...],     // fusion weights (default = max)
  earlyExitThreshold: 0.95,
  maxIntermediates: 20
}
```

User `u` gains `relation` on object `o` when there exists an intermediate `m` with `u -computedRelation-> m` and `m -tuplesetRelation-> o`.

### Chain (multi-step traversal)

```js
{
  type: 'chain',
  steps: [
    { relation: 'member_of', direction: 'out' },
    { relation: 'owner', direction: 'out' }
  ],
  collectValues: false,
  valueAggregation: 'sum',  // 'sum' | 'max' | 'min' | 'average'
  valueFilters: { relations: [...], minValue, maxValue }
}
```

Possibility along a chain = min of the edge possibilities (weakest link); parallel paths disjunct (max).

### Multi-hop (path search)

```js
{
  type: 'multi_hop',
  relation: 'owner',       // relation to traverse
  maxDepth: 5,
  pathAggregation: 'max',  // 'max' | 'sum' | 'owa'
  reverse: false,
  fallbackToBasicPaths: true,
  owaWeights: [...],
  collectValues: true,
  valueFilters: { relations: [...], minValue, maxValue },
  valueAggregation: 'sum'
}
```

Unbounded-depth path search over a single relation.

### Defeasible logic

```js
{
  type: 'defeasible',
  when: <rule>,      // grants
  unless: <rule>,    // defeats when it fires
  never: <rule>,     // absolute veto
  always: <rule>,    // unconditional grant
  requires: <rule>   // precondition
}
```

Rules compose: `{ when, unless }` = "grants unless defeated"; `{ always, unless }` = "grants unconditionally unless defeated"; `{ never }` = absolute deny.

### Logical operators

```js
{ union: [rule, rule, ...] }        // disjunction — max
{ intersection: [rule, rule, ...] } // conjunction — min / conflict-aware
{ exclusion: [grantRule, denyRule] } // grant minus deny
```

Union configs additionally support OWA fusion:

```js
{
  union: { rules: [...], aggregator: 'owa', owaWeights: [0.7, 0.3], useBilattice: false, epistemicMode: 'hybrid' }
}
```

Aggregators: `max`, `min`, `owa`, `top2`, `median`, `sum`.

### Relational comparator (ABAC)

```js
{
  type: 'relational_comparator',
  comparator: '>=',            // any JS comparison operator
  fallbackBehavior: 'deny',    // 'deny' | 'allow' | 'unknown'
  left: {
    rule: <rule>,              // e.g. { type: 'direct', relation: 'has_balance' }
    extractValue: true,
    valueRelation: 'has_balance',
    evaluateFrom: 'user',      // 'user' | 'object'
    aggregator: 'owa', owaWeights: [...]
  },
  right: {
    rule: <rule>,
    extractValue: true,
    valueRelation: 'min_age',
    evaluateFrom: 'object'
  }
}
```

Compares value evidence from two operands (supports nested rules and OWA fusion of multi-source values). **Value-freshness gated**: expired operands deny.

### Qualitative relational comparator

```js
{
  type: 'relational_comparator',
  qualitative: true,             // routes to the qualitative implementation
  comparator: '>=',
  left: { rule: <rule>, scaleName: 'five-point', decayPeriod: 'HOUR', decaySteps: 1, ... },
  right: { rule: <rule>, ... },
  marginSteps: 0
}
```

The router (`RelationalComparatorRouter`) sends a comparator to the qualitative implementation when `rule.qualitative === true` or when either operand carries a `scaleName`. Qualitative scales decay: possibility decays over time periods, values blur by steps — all against the caller's pinned clock.

### Challenge (MFA-style)

```js
{
  type: 'challenge',
  challenge: 'mfa',            // proof name; defaults to rule name/relation
  withinMs: 300000             // proof freshness window
}
```

Grants when the partial graph carries a proof for `challenge` within `withinMs`. Missing proofs produce structured `remediation` in the result.

### Parent

```js
{ type: 'parent', parentRelation: 'parent', relation: 'owner' }
```

Grants when the object's parent (via `parentRelation`) holds `relation` for the user.

### Computed

```js
{ type: 'computed', relation: 'has_balance' }
```

Defers to the referenced relation's own config.

### Composition

Any rule position accepts a config object, so policies nest: unions of chains, exclusions of comparators, defeasible wrapping unions, etc. The `union` operator is flattened during collection (max semantics preserved); `intersection`/`exclusion` preserve their structure.

---

## 5. The `valueManager`

Accessible as `arbiter.valueManager`. Values are the *evidence* attached to edges; TTL and decay govern their freshness.

| Method | Signature | Description |
|--------|-----------|-------------|
| `setTTL` | `(relationType, ttlMs) => void` | Configure value freshness for a relation |
| `getTTL` | `(relationType) => number` | Current TTL (default `24h`) |
| `setDecayConfig` | `(relationType, config) => void` | Possibility/interval decay over time |
| `getDecayConfig` | `(relationType) => config` | Current decay config |
| `setDefaultDecayConfig` | `(config) => void` | Global decay default |
| `getBlurredValue` | `(relation, now?) => { interval, possibility, reliability }` | TTL-gated value interval. `interval: null` when the value is absent or **expired** |
| `getDecayedRelation` | `(relation, now?) => { pointValue, blurredInterval, currentPossibility, ... }` | Decay-aware value |
| `invalidateCache` | `(keys) => void` | Drop cached value computations |
| `refreshStaleValues` | `(count) => number` | Background recomputation of stale values |
| `aggregateCrispValues` | `(values, aggregator?) => number` | Aggregate point values |
| `aggregateBlurredValues` | `(values, aggregator?) => interval` | Aggregate intervals |
| `compareIntervals` | `(left, right, comparator, epsilon?) => number` | Interval comparison |

**TTL contract** (pinned by `tests/rigor/ttl-contract.test.js`):

- TTL is a **value-freshness** gate, not an access-expiry mechanism. A direct relation's grant is timeless — `setTTL('can_read', ...)` does not expire the grant.
- Expired values: deny comparators, drop from collected values, return `interval: null` from `getBlurredValue`.
- Expiry is evaluated against the caller's pinned `now` when provided; the wall clock only applies to unpinned callers.

---

## 6. Partial graphs

An overlay of caller-supplied evidence evaluated *on top of* the persistent graph. The caller provides facts — relations and proofs — that the check consumes as if they were persistent.

### Raw spec form

```js
const result = arbiter.check(u, 'can_read', o, {
  partialGraph: {
    now: 1_700_000_000_000,           // the overlay's temporal context
    relations: [
      { src: 'u:1', relation: 'owner', dst: 'doc:9', possibility: 0.9, value: 42, layer_name: 'witness' }
    ],
    nodes: [{ key: 'u:1', type: 'user' }],
    challenges: [{ name: 'mfa', subject: 'u:1', issuedAt: 1_700_000_000_000, expiresAt: 1_700_000_300_000 }]
    // 'challengeProofs' is accepted as an alias for 'challenges'
  }
});
```

The raw form is validated against `partialGraphPolicy` before allocation (a DoS guard: `maxRelations` default `2000`, `maxNodes` default `1000`). `partialGraph.now`, when present, becomes the check's temporal context (it feeds `options.now`).

### Pre-built context form

```js
import { PartialGraphContext } from '@arbiter/core';

const ctx = new PartialGraphContext(arbiter, {
  now: 1_700_000_000_000,
  relations: [{ src: 'u:1', relation: 'owner', dst: 'doc:9', possibility: 0.9 }]
});
const result = arbiter.check(u, 'can_read', o, { partialGraphContext: ctx });
```

**Use `partialGraphContext` for a pre-built context.** Passing a context under `partialGraph` re-ingests it as a raw spec (it would be treated as an empty overlay).

**Precedence contract** (pinned by `tests/rigor/overlay-precedence.test.js`): persistent facts win over overlay facts for the same `(src, relation, dst)`; when the persistent fact is removed, the overlay surfaces. Overlay facts ride the *direct relations a policy consumes* — a TTU-derived relation does not consult the overlay directly.

**Proofs** (challenge rules): `{ name, subject, issuedAt, expiresAt }`. `issuedAt`/`expiresAt` are explicit (epoch timestamps are valid — `0` means already expired).

---

## 7. Snapshots

### Serialize

```js
arbiter.enableCondensedSnapshot();   // permanent read-only switch
const buf = arbiter.toSnapshotBinary();  // ArrayBuffer
```

### Restore

```js
const restored = Arbiter.fromSnapshotBinary(buf);
```

**Format** (v2): outer header `ARB1` magic + version 2 + graph length; embedded condensed graph section (`CGB1` magic); trailing JSON payload carrying relation metadata (validity, decay configs) and value TTLs. ~170 bytes/node for typical authorization graphs.

**Contracts**:

- **Lossless**: possibility, reliability, validity, decay configs, and TTLs round-trip. A restored engine answers checks identically (within 16-bit quantization, ≤ 1/65535).
- **Frozen**: the restored engine is read-only; mutations throw.
- **Trust boundary**: `fromSnapshotBinary` accepts untrusted bytes. Malformed buffers fail fast with clean, bounded errors — never hangs, crashes, or silently corrupted data. Every count field is cross-validated before use (pinned by `tests/rigor/snapshot-adversarial-fuzz.test.js`).
- **TTL caveat**: restore resets `changed_last_at` to access time, so post-restore TTL expiry is measured from the restore moment, not the original write.

---

## 8. Reachability

```js
await arbiter.initializeReachabilityChecker();
const r = arbiter.isReachable('u:1', 'doc:9');  // true | false | null
```

- `true` — a path exists (PLTC index is exact for reachability).
- `false` — no path exists; **sound fast-fail**: rule evaluation can skip.
- `null` — the PLTC index is unavailable (not initialized, or bypassed); **defer to rule evaluation** — this is the documented contract, not an error.

Chain rules use the PLTC index internally for fast-fail; `getReachabilityStats()` exposes hit/miss/query telemetry.

---

## 9. The `OWAFusion` utility

Static interval-fusion functions used by OWA aggregators and exported for direct use:

| Method | Signature | Description |
|--------|-----------|-------------|
| `fuseIntervalsWithMeta` | `(intervals, metas, weights, operator) => { interval, possibility, meta? }` | Fusion with validity metadata |
| `maxInterval` | `(intervals, metas) => result` | Disjunctive (max) fusion |
| `isWithinTTL` | `(timestamp, ttlMs, now?) => boolean` | TTL check honoring the caller clock |
| `compareIntervals` | `(left, right, comparator, epsilon?) => number` | Interval comparison |

Operators: `max`, `min`, `owa`, `sum`, `average`, `product`, `top2`, `median`. Conjunctive operators surface conflict mass instead of silently averaging.

---

## 10. Audit hook

```js
const arbiter = new Arbiter({
  audit(record) {
    // record: { timestamp, userKey, relation, objectKey, decision,
    //           possibility, binary, partialGraphUsed, validityLabel, sources }
  }
});
```

The engine stores nothing — the caller owns persistence and retention. The hook fires once per `check` (not for internal recursive evaluations), at zero cost when absent.

---

## 11. Errors

`check` and `addRelation` throw on invalid input; all other failures surface as `reason` codes in the result:

| Error | Trigger |
|-------|---------|
| `Invalid relation config ...` | `setRelationConfig` with a non-object config |
| `Invalid possibility: expected a finite number in [0, 1]` | `addRelation` with out-of-range possibility |
| `Cannot add/remove relation while in snapshot read-only mode` | mutation after `enableCondensedSnapshot` |
| `Partial graph exceeds max relations/nodes ...` | overlay larger than `partialGraphPolicy` limits |
| `Invalid condensed graph binary ...` | malformed snapshot bytes (clean, bounded) |
| `Invalid arbiter snapshot payload` | snapshot JSON payload not an object |

---

## 12. Reason codes

| Code | Meaning |
|------|---------|
| `direct_match` | Direct edge granted |
| `no_relation` | No edge for the relation |
| `threshold_not_met` | Direct edge below the allow threshold |
| `missing_node` | User or object unknown |
| `no_config` | Relation has no configuration |
| `cycle` | Recursion cycle detected |
| `allow` / `deny` | Binary-mode decision |
| `insufficient_confidence` | Binary mode, thresholds not decisive |
| `chain_path_found` / `no_chain_path_found` | Chain traversal |
| `multi_hop_path_found` | Multi-hop path |
| `no_matching_rule` | No rule matched |
| `logical_operator_evaluation` | Union/intersection/exclusion result |
| `defeated_by_unless` / `never_rule_triggered` / `requirements_not_met` | Defeasible outcomes |
| `challenge_missing` / `challenge_missing_context` / `challenge_satisfied` | Challenge rules |
| `exists` / `relation_exists` / `no_direct_match` | Rule-internal outcomes |
| `qualitative_interval_comparison` | Qualitative comparator |
| `not_reachable` | PLTC fast-fail |
| `unknown_rule_type` / `invalid_rule` | Unrecognized config |
| `evaluation_error` / `error` | Evaluation failure (never crashes the process) |

---

## 13. Validity taxonomy

Validity labels (weakest → strongest of *evidence*):

```
finite_sample < anytime < conformal < approximate < heuristic < unknown
```

- `finite_sample` — evidence from an explicit sample
- `anytime` — anytime-algorithm guarantees
- `conformal` — conformal prediction coverage
- `approximate` — approximation with stated bounds
- `heuristic` — unvalidated ranking (default for unlabeled relations)
- `unknown` — unrecognized label

`validity.label` is the **weakest** source label, downgraded by the fusion operator's class: identity/max preserve the weakest label; min surfaces `conflictMass = 1 − fused_possibility` and a `validifiedPossibility`; product-style and interior-OWA operators are always `heuristic` (no linear validification under arbitrary dependence).
