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.
24 KiB
@arbiter/core — API Reference
The complete public interface of the Arbiter engine. For the package overview, quick start, and concepts, see the README. This document is the authoritative reference: every public export, method signature, option, result shape, and configuration format.
Contents
- Exports
- The
Arbiterclass checkoptions and result shape- Relation configurations
- The
valueManager - Partial graphs
- Snapshots
- Reachability
- The
OWAFusionutility - Audit hook
- Errors
- Reason codes
- Validity taxonomy
1. Exports
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) |
partialGraphPolicy |
object |
— | Overlay admission limits (see §6) |
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). 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). 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 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) |
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).
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) |
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:
{
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):
possibilityandreliabilityare always finite numbers in[0, 1].- Denials (
possibility === 0) carryreliability === 0— a denied decision never leaks a source's reliability. reasonis always a known outcome class.- The result is JSON-serializable — no circular references, no undefined values.
includeMetaenrichesmetabut never changes the decision fields.
Collected values
{
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).
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
{ type: 'direct' }
A single edge grants with exactly its possibility. The fastest path.
Tuple-to-userset (group membership)
{
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)
{
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)
{
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
{
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
{ union: [rule, rule, ...] } // disjunction — max
{ intersection: [rule, rule, ...] } // conjunction — min / conflict-aware
{ exclusion: [grantRule, denyRule] } // grant minus deny
Union configs additionally support OWA fusion:
{
union: { rules: [...], aggregator: 'owa', owaWeights: [0.7, 0.3], useBilattice: false, epistemicMode: 'hybrid' }
}
Aggregators: max, min, owa, top2, median, sum.
Relational comparator (ABAC)
{
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
{
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)
{
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
{ type: 'parent', parentRelation: 'parent', relation: 'owner' }
Grants when the object's parent (via parentRelation) holds relation for the user.
Computed
{ 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: nullfromgetBlurredValue. - Expiry is evaluated against the caller's pinned
nowwhen 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
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
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
arbiter.enableCondensedSnapshot(); // permanent read-only switch
const buf = arbiter.toSnapshotBinary(); // ArrayBuffer
Restore
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:
fromSnapshotBinaryaccepts 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 bytests/rigor/snapshot-adversarial-fuzz.test.js). - TTL caveat: restore resets
changed_last_atto access time, so post-restore TTL expiry is measured from the restore moment, not the original write.
8. Reachability
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
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 sampleanytime— anytime-algorithm guaranteesconformal— conformal prediction coverageapproximate— approximation with stated boundsheuristic— 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).