From 33dd15bba55d68c58e2c98a4394678c44627c177 Mon Sep 17 00:00:00 2001 From: John Dvorak Date: Sun, 2 Aug 2026 20:50:19 -0700 Subject: [PATCH] docs: full API reference + capabilities; export PartialGraphContext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- README.md | 24 ++- docs/API.md | 559 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/index.js | 1 + 3 files changed, 574 insertions(+), 10 deletions(-) create mode 100644 docs/API.md diff --git a/README.md b/README.md index d6cc3cd..c3ef0f4 100644 --- a/README.md +++ b/README.md @@ -72,23 +72,27 @@ Denied decisions never leak a source's reliability. `includeMeta: true` adds `me ## API -The public surface is the `Arbiter` class: +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), `getSituationTree`, `monteCarloWalk` +- **Value context**: `valueManager` (TTL-gated evidence), `PartialGraphContext` (overlays), `getSituationTree`, `monteCarloWalk` -Check `options`: +## Capabilities -| 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 | +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 diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..8c723cc --- /dev/null +++ b/docs/API.md @@ -0,0 +1,559 @@ +# @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` | 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: , // grants + unless: , // defeats when it fires + never: , // absolute veto + always: , // unconditional grant + requires: // 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: , // e.g. { type: 'direct', relation: 'has_balance' } + extractValue: true, + valueRelation: 'has_balance', + evaluateFrom: 'user', // 'user' | 'object' + aggregator: 'owa', owaWeights: [...] + }, + right: { + 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: , scaleName: 'five-point', decayPeriod: 'HOUR', decaySteps: 1, ... }, + right: { 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). diff --git a/src/index.js b/src/index.js index e756b63..333d9c8 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,5 @@ export { Arbiter } from './core/Arbiter.js'; +export { PartialGraphContext } from './core/PartialGraphContext.js'; export { GraphIndices } from './core/GraphIndices.js'; export { NodeManager } from './core/NodeManager.js'; export { RelationManager } from './core/RelationManager.js';