Files
core/docs/API.md
T
John Dvorak 33dd15bba5
CI / test (push) Successful in 5m32s
CI / benchmark (push) Successful in 40s
CI / publish (push) Has been skipped
docs: full API reference + capabilities; export PartialGraphContext
docs/API.md is the authoritative interface reference: exports, Arbiter
methods, check options/result shape with contract invariants, all ten
relation configuration formats, valueManager, partial graphs (raw spec
+ pre-built context, precedence contract), snapshots (format, frozen
restore, trust boundary), reachability (null-defer contract), OWAFusion,
audit hook, errors, reason codes, and the validity taxonomy.

README gains a capabilities section (what the engine does and does not
do) and links the reference, per the map-vs-manual split.

The reference pass surfaced one interface gap: PartialGraphContext —
a first-class public type used in the pre-built overlay form — was not
exported. It is now part of the public surface.

Verified claims against source (overlay spec keys: challenges/
challengeProofs not proofs; qualitative routing via qualitative: true /
scaleName; simpleBinary shape; reason enum; config formats). Rigor
251/251, full suite 853/791/0.
2026-08-02 20:50:19 -07:00

560 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# @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).