/** * adjudicate() — the pure deterministic heart of the framework. * * Takes a proposed IntentEnvelope, the current state snapshot, and a * PolicyBundle. Returns a single Decision. No LLM calls. No side effects. No * randomness. Same inputs always produce the same output — the replay harness * depends on this. * * Evaluation order (strict — do not reorder): * 1. Kill switch — operator-engaged global override (engages before schema) * 2. Schema version — unknown versions are SECURITY refusals * 2b. intentHash — re-derived from canonical content; a mismatch is a * SECURITY refusal (content-addressing must be verified, * not trusted). Adopters using buildEnvelope pay no cost. * 3. stateGuards — legality of the transition the intent proposes * 4. taint gate — provenance check via canPropose() (T8: moved ahead of auth) * 5. authGuards — caller identity and scope * 6. business — domain-specific rules * 7. policy.default * * **T8 reorder:** the taint gate runs BEFORE auth guards. Auth guards * with side effects (logging principals, querying permission services) * previously executed on UNTRUSTED inputs; now UNTRUSTED short-circuits * before any auth side effect. The refusal-code distribution in audit * history shifts as a result — taint refusals on UNTRUSTED inputs that * would also have failed auth now surface the taint refusal instead. * * Each guard returning null contributes a "pass" basis to the final decision. * * # Trace variant * * `adjudicateWithTrace()` returns the same Decision plus an evaluation * trace — which guards ran, which one matched. Both functions delegate * to `_adjudicateImpl`, so trace fidelity is structurally guaranteed: * the trace describes the exact path `adjudicate()` would have taken. * The hot path (`adjudicate()` itself) passes `undefined` for `traceOut` * and pays zero allocation cost. */ import { type Decision } from "../decision.js"; import { type IntentEnvelope } from "../envelope.js"; import { type PolicyBundle } from "./policy.js"; export type AdjudicationTracePhase = "kill" | "schema" | "state" | "taint" | "auth" | "business" | "default"; /** * A single step in the kernel's evaluation of an envelope. * * Semantics: * - One entry per evaluated step. Steps that didn't run (because an * earlier match short-circuited) are absent from the trace. * - Single-step phases (`kill`, `schema`, `taint`, `default`) emit one * entry. Array phases (`state`, `auth`, `business`) emit one entry * per guard actually invoked. * - `outcome === "match"` exactly identifies the step that produced * the final Decision. The trace always ends with the match. * - `guardName` is best-effort from `Function.name`. Factory-built * guards (e.g., returned from `createThresholdGuard`) are anonymous; * for those, `guardName` is omitted and consumers fall back to * `phase[index]`. */ export interface AdjudicationTraceEntry { readonly phase: AdjudicationTracePhase; /** 0-based position within array phases. Omitted for single-step phases. */ readonly index?: number; /** Non-empty `Function.name` of the guard. Omitted for anonymous closures and non-guard phases. */ readonly guardName?: string; /** "pass" — step yielded no decision; evaluation continued. "match" — step produced the final decision. */ readonly outcome: "pass" | "match"; } export interface AdjudicationTraceResult { readonly decision: Decision; readonly trace: ReadonlyArray; } /** * The pure, deterministic decision core — NOT the production entry point. * * `adjudicate()` emits NO AuditRecord, consults NO ledger, and produces NO * side effects. It exists for replay, simulation, trace tooling, and property * tests, where re-auditing would be wrong. Determinism is a hard invariant: * the same `(envelope, state, policy)` always yields the same Decision, with * no Date/env/IO read in the path. * * Production mutation paths MUST call `adjudicateAndAudit()` instead — that is * the only entry point that enforces the audit-complete invariant (every * authoritative decision yields a durable AuditRecord) plus the ledger * replay-suppression that stops side effects from double-firing. Wiring a raw * `adjudicate()` call into a mutation path silently bypasses governance. * * **READ-bearing envelopes (012).** This function makes NO distinction between * a "read" and an "intent" envelope: the typed `ToolClassification` that the * adapter loop uses to decide which executor surface an EXECUTE authorizes is a * STRUCTURAL discriminant on the adapter-facing types — it is NOT a kernel * input and introduces NO runtime IO or heuristic here. A READ proposed by the * model is routed (by the adapter) into an ordinary envelope and adjudicated * under the SAME guard order `state → taint → auth → business → default`, so * the taint gate and fail-closed default apply to reads exactly as to * mutations. The kernel stays pure and synchronous (§D); read-vs-write routing * lives entirely in the impure adapter shell. */ export declare function adjudicate(envelope: IntentEnvelope, state: S, policy: PolicyBundle): Decision; /** * Attach a stable display name to a guard so it appears in trace output * (`AdjudicationTraceEntry.guardName`) and learning-event identity * (`LearningEvent.guardId`). * * Guards declared as named consts (e.g., `const validateAmount: Guard<...> = ...`) * already carry a useful `Function.name` automatically — `nameGuard` is for * the case factory-built guards lose: `createThresholdGuard({...})` returns * an anonymous closure with `name === ""`. Wrap it: * * const escalateLargeRefunds = nameGuard( * "escalateLargeRefunds", * createThresholdGuard({ ... }), * ); * * Implementation: `nameGuard(name, g)` is a thin facade over * `withMetadata(g, { name })` — see `policy.ts` for the metadata surface. * No `description` is attached: `nameGuard` is the canonical lightweight- * wrapper case the optional-description rule was designed to support, and * forcing `{ kind: "opaque" }` would import the fake-precision pattern * ADR-105 explicitly rejects. Analyzers seeing a guard with `name` but no * `description` treat it identically to `{ kind: "opaque" }`. * * Identity-preserving: returns the same function object. Stack traces, * referential equality, and registry semantics are preserved. * * Idempotent across identical names — `withMetadata` is per-field write-once * with idempotent reattachment of the same value. Calling `nameGuard("a", g)` * then `nameGuard("b", g)` throws on the second call (different values * for the `name` field). */ export declare function nameGuard unknown>(name: string, guard: F): F; /** * Tracing variant: same Decision as `adjudicate()`, plus the per-step * evaluation trace. Useful for simulation tooling (CLI `simulate`), * Operator Console replay rendering, and future static-verification * over closed-enum guard spaces. * * Trace fidelity: this function and `adjudicate()` share their body — * the only difference is that `adjudicate()` passes `undefined` for * `traceOut`. There is no second implementation that could drift. */ export declare function adjudicateWithTrace(envelope: IntentEnvelope, state: S, policy: PolicyBundle): AdjudicationTraceResult; //# sourceMappingURL=adjudicate.d.ts.map