{"version":3,"file":"types.cjs","names":[],"sources":["../../../src/batteries/orchestration/types.ts"],"sourcesContent":["/**\n * The orchestration battery's shared type contracts — the normative source for every type more\n * than one part of the battery touches.\n *\n * @module @nhtio/adk/batteries/orchestration/types\n *\n * @remarks\n * A type belongs here when two or more modules read or write it. Anything with no dependents —\n * a cell's internal AST, the in-memory store's private index, a renderer's line-wrapping helper\n * — is deliberately left to its implementation rather than fixed here.\n *\n * Two conventions in this file are load-bearing and easy to undo by accident. Discrimination is\n * by CLASS, not by a `kind` field: `NodeRef` and `ParamRef` are registered encoder classes whose\n * `is*` guards are `instanceof` checks, because a plain record can wear `{kind: 'nodeRef'}` and a\n * resolver keying on that would silently rewrite a literal. And `EncodableValue` is a deliberate\n * SUBSET of the encoder's own `Encodable` — it omits `Function`, `Error` and consumer-defined\n * custom classes, so a staged argument cannot carry a closure that serialises by source text or\n * a class the battery cannot register on the consumer's behalf.\n */\nimport type { renderPlan } from './render'\nimport type { planOutline } from './outline'\nimport type { Tool } from '@nhtio/adk/common'\nimport type { PlanLockFactory } from './locks'\nimport type { NodeRef, ParamRef } from './encoding'\nimport type { rawPlan, rawOps, rawDiff } from './raw'\nimport type { DateTime, Duration, Interval } from 'luxon'\nimport type { PlanStore, TransitionResult } from './store'\nimport type { ObjectSchema, Schema } from '@nhtio/validation'\n\n// ── identity and lifecycle ───────────────────────────────────────────────────\n/**\n * The three lifecycle states, in the only order they may be traversed.\n *\n * @remarks\n * The permission gate IS the `reviewable → executable` transition, which is what makes \"approved\"\n * and \"executable\" one fact rather than two that can disagree. `editable` admits free mutation;\n * `reviewable` is frozen content awaiting a decision; `executable` is approved and may run at most\n * once. There is no path back — recovery from a spent or rejected plan is `clonePlan`, which mints\n * a fresh `editable` plan.\n */\nexport type PlanState = 'editable' | 'reviewable' | 'executable'\n/** A plan's stable identity, unique per store and never reused across a clone. */\nexport type PlanId = string\n/**\n * A node's identity within one plan. Validated snake_case with no `/` and no leading `.`, so it can\n * never be mistaken for a path or copied as a citation.\n */\nexport type NodeId = string\n\n// ── the value domain ────────────────────────────────────────────────────────\n/**\n * The value space of a staged argument and of a node's output: a DELIBERATE SUBSET of\n * `@nhtio/encoder`'s `Encodable`, declared structurally here rather than re-exported.\n *\n * Why a subset. The encoder's own `Encodable`\n * (`nhtio-encoder/src/private/types.ts:46-69`) also admits `Error` and its subclasses,\n * `PhoneModel`, `Function`, arbitrary call signatures, and `CustomEncodable`. Re-exporting it\n * would (a) let a live function into a staged tool argument — the encoder serialises functions by\n * SOURCE TEXT, so closures silently lose their bindings, and a plan is exactly the wrong place\n * for that; (b) admit consumer-defined `CustomEncodable` classes whose decode requires\n * `registerClass` calls this battery cannot make on the consumer's behalf, so a plan could\n * encode successfully and then fail hydration; and (c) undermine `predicate: EncodableValue`,\n * which is meant to exclude live evaluator objects by domain.\n *\n * The subset below is closed, digest-safe, and hydratable with only\n * `registerOrchestrationEncodables()`:\n */\nexport type EncodableValue =\n  | string\n  | number\n  | boolean\n  | null\n  | undefined\n  | bigint\n  | Date\n  | RegExp\n  | DateTime\n  | Duration\n  | Interval\n  | ArrayBuffer\n  | DataView\n  | Int8Array\n  | Uint8Array\n  | Uint8ClampedArray\n  | Int16Array\n  | Uint16Array\n  | Int32Array\n  | Uint32Array\n  | Float32Array\n  | Float64Array\n  | BigInt64Array\n  | BigUint64Array\n  | EncodableValue[]\n  | { [key: string]: EncodableValue }\n  | Map<EncodableValue, EncodableValue>\n  | Set<EncodableValue>\n// DISCRIMINATION IS BY CLASS, NOT BY A `kind` FIELD. A plain record can wear\n// `{kind: 'nodeRef', …}` — it is an ordinary encodable record — so a marker property cannot\n// separate a reference from a literal that happens to look like one, and a resolver keying on it\n// would silently rewrite the literal. So `NodeRef` and `ParamRef` are registered ENCODER CLASSES\n// (instances with `[ENCODE_METHOD]`/`[DECODE_METHOD]`, registered by\n// `registerOrchestrationEncodables()`): the `is*` guards are `instanceof` checks no record can\n// satisfy, and the encoder round-trips them as `custom:NodeRef`/`custom:ParamRef` rather than as\n// records. Same mechanism core uses for `Media`/`Tokenizable`, and why\n// `registerOrchestrationEncodables()` must run before any `decode()`.\n//\n// THE WIRE PROBLEM THIS CREATES, AND ITS ONE ANSWER. A tool call cannot transmit a class\n// instance — a model's `add_node`/`set_node_config`/`set_node_field` arguments arrive as decoded\n// JSON records, which by the rule above are literals. So the IR's in-memory form and the tool\n// surface's wire form are deliberately DIFFERENT, and the forge is the single named conversion\n// point between them:\n//\n//   · WIRE (tool inputs only): a reference is `{$ref: {node, select, path?, branchId?}}` and a\n//     template hole is `{$param: {path}}`. Single-key wrapper objects, whose keys are reserved:\n//     freeze refuses a staged record whose sole key is `$ref` or `$param` reaching the IR\n//     unconverted, so an author cannot smuggle a literal that mimics the wire form.\n//   · IR (everything persisted, digested, resolved): real `NodeRef`/`ParamRef` instances.\n//   · CONVERSION: `forge.ts` normalises wire → IR on the way in (`hydrateRefs`) and IR → wire on\n//     the way out, so a model reading a plan back sees the same `$ref` shape it writes. Nothing\n//     else in the battery sees the wire form; nothing outside the forge constructs these classes\n//     from model input.\n//\n// The wrapper form is unambiguous on the wire because `$ref`/`$param` are reserved there, and the\n// IR stays unambiguous because it holds classes. Neither representation alone could do both jobs.\n// A freeze-time guard rejects any value outside this subset — a `Function`, an `Error`, or an\n// unregistered custom class reaching a plan is refused with a named error rather than discovered\n// at hydration.\n//\n// TYPE MEMBERSHIP IS NOT SUFFICIENT: a record, array, `Map` or `Set` is inside the subset and can\n// still be CYCLIC, and the encoder tracks seen values and throws `E_CIRCULAR_REFERENCE`\n// (`nhtio-encoder/src/private/structured_data.ts`). A cyclic staged argument would therefore pass\n// a subset-only guard and then blow up while computing the digest — the one operation the whole\n// lifecycle depends on. So the freeze guard is an *encodability* check, not a type check: it walks\n// each staged value with a seen-set and refuses a cycle as a named submit-time issue. Equivalently\n// and more cheaply, freeze may simply attempt the encode it is about to need and surface a thrown\n// `E_CIRCULAR_REFERENCE` as that issue — the encoder is already the authority on what encodes.\n\n/** A staged argument: any encodable value, or a reference to another node's output. */\nexport type ArgValue = EncodableValue | NodeRef | ArgValue[] | { [k: string]: ArgValue }\n\n/**\n * A serializable reference to another node's output. Never a live value, never a string DSL.\n *\n * `branchId` identifies WHICH EXECUTION of `node` to read — the same path identity `NodeOutput`\n * and `FrameRef` carry. It is NOT an outgoing branch of `node`: a node fanning out to two\n * successors still runs once and produces one output, so there is nothing per-outgoing-edge to\n * address. What creates several outputs for one node is that node being *reached* by several\n * paths, which is exactly what a path identity distinguishes.\n */\n// `NodeRef` is a real runtime CLASS and its single definition lives in `./encoding`, which is the\n// module that implements it and registers it with the encoder. It is re-exported here as a TYPE\n// only, so this module stays the one place to import the battery's type surface from without\n// declaring a second, implementation-free copy of a value that must exist at runtime.\nexport type { NodeRef } from './encoding'\n\n// ── node outputs ────────────────────────────────────────────────────────────\n/**\n * One unit of a node's output. Items, after n8n — a node may emit several. The field is named\n * `json` for continuity with that lineage, but its values are `EncodableValue`, so a tool may\n * legitimately return a `Date`, a `RegExp`, a `Map` — the same domain a staged argument may hold,\n * which is what lets an output feed an argument without a lossy hop. It is NOT restricted to\n * JSON-representable values, and an earlier draft's \"JSON-shaped only\" gloss was wrong.\n */\nexport interface OutputItem {\n  /**\n   * The item's fields, addressable by a `NodeRef.path`. Named `json` for continuity with the n8n\n   * lineage, but its values are `EncodableValue` — a `Date`, `RegExp` or `Map` is legitimate here.\n   */\n  json: Record<string, EncodableValue>\n}\n/**\n * A PATH identity — the route from `entry` to a frame, kept as the ROUTE ITSELF, not a hash of it,\n * so ancestry questions are answered by reading the value. (An earlier draft hashed it and then\n * tried to correlate joins by \"longest common prefix of the ids\", which computes nothing: a hash\n * has no prefix relation to what it hashes.)\n *\n * A route is a list of SEGMENTS. A segment is either one traversed edge, or a join — which is what\n * makes the structure total: a join has no single parent route, but a node *after* a join still\n * needs a route relative to something. An earlier draft gave a join `{edges: [], joinOf}`, which\n * reset the route to empty and made every post-join frame's `edges` relative to nothing, so two\n * different post-join paths collided.\n *\n * - **Entry frame:** `{ segments: [] }`.\n * - **Produced by an edge:** parent's `segments` + `{ edge: edgeId }`.\n * - **Produced by a JOIN:** the **correlation prefix** (the arriving route truncated at the fork —\n *   the same prefix `JoinState.correlationKey` is built from) + `{ join: nodeId, of: sorted(ALL\n *   incoming edge ids) }`. Subsequent edges then extend it normally.\n *\n *   Retaining that prefix is load-bearing: `of` is a graph constant, so a join segment ALONE is\n *   the same value for every execution of the join. If the route were reset to just that segment,\n *   two executions of the fork reached by different outer routes would produce identical merged\n *   identities and collide in `OutputTable` — even though their barriers correctly stayed\n *   separate. The prefix is exactly the coordinate that distinguishes them, and it is already\n *   computed for correlation, so nothing new is needed.\n *\n * `branchKey` is the canonical string form used for map keys and in events. It MUST be injective,\n * because it keys `OutputTable`, identifies `NodeRef.branchId`, orders join contributors, and makes\n * duplicate arrivals idempotent — a collision would overwrite one node's output with another's or\n * merge unrelated barriers. Naive delimiter-joining is NOT injective: an edge id containing the\n * delimiter (`a>b`) collides with two segments (`a`, `b`), and an id shaped like `join:x(y)`\n * collides with a join segment.\n *\n * Two mutually-reinforcing measures, both required:\n * 1. **Freeze validates edge ids** against `/^[A-Za-z0-9_-]{1,64}$/` — no delimiter, no colon, no\n *    parenthesis, so the grammar below cannot be forged. Node ids already carry an equivalent\n *    charset rule (see the re-cite-loop guard), and edge ids are minted by the same authoring\n *    tools, so this costs authors nothing.\n * 2. **The rendering is length-prefixed**, not delimiter-joined: each segment renders as\n *    `` `e${id.length}:${id}` `` or `` `j${nodeId.length}:${nodeId}(${of.map(len-prefix).join('')})` ``,\n *    concatenated with no separator. Length-prefixing is injective regardless of content, so the\n *    charset rule is defence in depth rather than the sole guarantee.\n *\n * Bounded by the acyclicity invariant plus `PlanBounds.maxNodes` — a route cannot revisit a node,\n * so it cannot exceed the node count.\n *\n * Why no run coordinate: a plan has at most one run ever (decision 10) and the graph is acyclic,\n * so a node executes exactly once per route reaching it. And why not an edge ordinal: given\n * `entry→a`, `entry→b`, `a→c`, `b→c`, both incoming edges of `c` are ordinal 0 among their own\n * source's outgoing edges, so two distinct frames would collide.\n */\nexport type RouteSegment = { edge: string } | { join: NodeId; of: string[] }\n/** A path identity: the route from `entry` to a frame, kept as the route itself. */\nexport interface BranchId {\n  /**\n   * The route, in traversal order. Empty for the entry frame. A join segment carries the\n   * correlation prefix before it — see the type's own remarks for why that prefix is load-bearing.\n   */\n  segments: RouteSegment[]\n}\n/**\n * The canonical string form of a `BranchId`, used for map keys and in events.\n *\n * @remarks\n * MUST be injective: it keys `OutputTable` and `ArtifactTable`, identifies `NodeRef.branchId`,\n * orders join contributors and makes duplicate arrivals idempotent, so a collision would overwrite\n * one node's output with another's. The rendering is length-prefixed rather than delimiter-joined,\n * which is injective regardless of content; the freeze-time edge-id charset rule is defence in\n * depth rather than the sole guarantee. Always build a key with this function, never by\n * interpolating the object.\n */\nexport type { branchKey } from './ops'\n\n/** What a node produced on one path. Always an ARRAY, even for a single result. */\nexport interface NodeOutput {\n  /** Always an array, even for a single result. */\n  items: OutputItem[]\n  /** Which execution of the node produced this — the path that reached it. */\n  branchId: BranchId\n}\n/** Append-only, keyed `${nodeId}:${branchKey(branchId)}` — path-unique, which is what makes a\n *  NodeRef resolve. Always build the key with `branchKey`, never by interpolating the object. */\nexport type OutputTable = ReadonlyMap<string, NodeOutput>\n\n/**\n * Live artifact instances produced by `call` nodes, keyed **identically** to `OutputTable`\n * (`${nodeId}:${branchKey(branchId)}`) so a `transform`'s `source: NodeRef` addresses one with no\n * second addressing scheme. This is the channel a `transform` receives its instance through — see\n * `TransformNodeDefinition`, which explains why the value dataflow cannot carry it.\n *\n * An entry exists only where `CallInvokerFn` returned a `SpooledArtifactLike`; a `string`-returning\n * call contributes nothing. Persisted as encoder HANDLES (`{tag, locator}` via each artifact's own\n * `[ENCODE_METHOD]`), never bytes, and rebound on resume through `resolveSpoolReader` — so\n * registering a durable store's reader resolver is load-bearing for resume, not optional hygiene.\n */\nexport type ArtifactTable = ReadonlyMap<string, SpooledArtifactLike>\n\n// ── node definitions, closed per kind ───────────────────────────────────────\n/** The closed set of node kinds. Each has its own definition type, and freeze validates per kind. */\nexport type PlanNodeKind = 'entry' | 'call' | 'reason' | 'transform' | 'branch' | 'select' | 'join'\n/**\n * An edge's firing condition, drawn from the outcome of its source node.\n *\n * @remarks\n * Applicability is by source kind and is enforced at freeze; any other pairing is refused:\n * `entry` → `always`; `call`/`reason`/`transform` → `always` | `error`; `branch` → `match` |\n * `no_match` | `default` | `error`; `select` → `` case_${string} `` | `default` | `error` (a\n * `default` edge is REQUIRED); `join` → `always` | `error`.\n *\n * On a node settling, EVERY edge whose handle applies fires, each exactly once — so `always` fires\n * alongside `match`/`case_*` on success, and duplicates of one handle are legal fan-out. On a\n * FAILURE outcome only `error` edges fire; `always` does NOT — an `always` edge is a success-path\n * edge, not a finally. `default` fires only when no `match`/`case_*` matched.\n */\nexport type EdgeHandle = 'always' | 'match' | 'no_match' | 'default' | 'error' | `case_${string}`\n\n/** A directed edge. Its `handle` decides when it fires; the graph alone orders execution. */\nexport interface PlanEdge {\n  /**\n   * The edge's identity. Matches `/^[A-Za-z0-9_-]{1,64}$/` so `branchKey` cannot be forged.\n   * Uniqueness is a FREEZE-time invariant, not an append-time one: refusing \"whichever arrived\n   * second\" would make the op fold order-dependent, so a same-id collision resolves by LWW on\n   * `(lamport, actorId, opId)` and the losing edge is surfaced as a submit-time issue naming both.\n   */\n  id: string\n  /** Source node. */\n  from: NodeId\n  /** Target node. */\n  to: NodeId\n  /** When this edge fires, given the source's outcome. */\n  handle: EdgeHandle\n}\n// HANDLE APPLICABILITY, by source node kind — enforced at freeze; any other pairing is refused:\n//   entry            → 'always'\n//   call | reason | transform → 'always' | 'error'\n//   branch           → 'match' | 'no_match' | 'default' | 'error'\n//   select           → `case_${string}` | 'default' | 'error'   (a 'default' edge is REQUIRED)\n//   join             → 'always' | 'error'\n// FIRING: on a node settling, EVERY edge whose handle applies to that outcome fires, and each\n// fires exactly once. So `always` fires alongside `match`/`case_*` on success, and duplicates of\n// one applicable handle are legal fan-out (two `always` edges = two successors, both enqueued).\n// On a FAILURE outcome only `error` edges fire; `always` does NOT — an `always` edge is a\n// success-path edge, not a finally. `default` fires only when no `match`/`case_*` matched.\n\n/**\n * One field a node promises to produce, or that an entry/template accepts. Declaring a field is\n * what makes it addressable by a `NodeRef.path` and checkable at freeze rather than at run time.\n */\nexport type DeclaredField =\n  | { path: string; type: 'string'; maxBytes?: number }\n  | { path: string; type: 'number' }\n  | { path: string; type: 'boolean' }\n  | { path: string; type: 'enum'; values: string[] }\n\n/**\n * The one place external input enters the graph.\n *\n * @remarks\n * TOPOLOGY INVARIANTS, all enforced at freeze: EXACTLY ONE `entry` node — zero means nothing can\n * start, more than one means `executePlan` cannot tell which to materialise, and it takes no entry\n * argument by design. The entry node has NO incoming edges. Every other node is reachable from it.\n * The graph is acyclic over every handle, `error` and `default` included. Every `join` is a\n * DIAMOND (see `JoinNodeDefinition`). Every edge id matches `/^[A-Za-z0-9_-]{1,64}$/`.\n */\nexport interface EntryNodeDefinition {\n  /** The fields a run's `input` must supply. Validated before any node runs. */\n  input: DeclaredField[]\n}\n// TOPOLOGY INVARIANTS, all enforced at freeze:\n//  · EXACTLY ONE `entry` node — zero means nothing can start, more than one means\n//    `executePlan(planId, options)` cannot tell which to materialise, and it takes no entry\n//    argument by design.\n//  · The entry node has NO incoming edges.\n//  · Every other node is reachable from it (the reachability check).\n//  · The graph is acyclic over every handle, `error` and `default` included.\n//  · Every `join` is a DIAMOND: all routes from `entry` to it pass through one common fork (its\n//    immediate dominator), and the fork→join region contains no nested join. `required` is the\n//    number of fork→join routes. See JoinNodeDefinition — this is what makes joins implementable,\n//    and it is stated over the fork, NOT over immediate predecessors (which would refuse the\n//    canonical diamond `a→b→j`, `a→c→j`).\n//  · Every edge id matches /^[A-Za-z0-9_-]{1,64}$/ so `branchKey` cannot be forged. Uniqueness is\n//    not cosmetic — `remove_edge {edgeId}`, `edge_taken.edgeId`, route identity, `PlanDiff` and\n//    join-arrival idempotence (keyed `(branchKey, edgeId)`) all treat it as an identifier — but it\n//    is NOT enforced by refusal, because refusing \"whichever arrived second\" would make the fold\n//    order-dependent and two offline writers can each legally append before their logs meet. See\n//    the op-log section: a same-id collision resolves by LWW on `(lamport, actorId, opId)`, so the\n//    fold stays convergent, and the LOSING edge is surfaced as a submit-time `issue` naming both\n//    so the author renames one. Uniqueness is therefore a freeze-time invariant, not an\n//    append-time one.\n\n/** A staged tool invocation — the node kind the whole staging environment exists to gate. */\nexport interface CallNodeDefinition {\n  /** The tool to invoke. Refused at freeze unless `InvocableTools.has(tool)` — the Tier-C boundary. */\n  tool: string\n  /** The staged arguments. A value may be a literal or a `NodeRef` to another node's output. */\n  args: Record<string, ArgValue>\n  /** The fields this call promises to produce, so downstream `NodeRef`s are checkable at freeze. */\n  output: DeclaredField[]\n  /** What to do when a `NodeRef` resolves to nothing. Required, no default. */\n  onMissingValue: 'fail' | 'omit'\n  /** What this call is authorised to do. Approval binds the canonicalised union of these. */\n  authority: AuthorityClaim[]\n  /**\n   * Whether re-invoking this tool with the same arguments is safe. Required, and a fact about the\n   * TOOL — not a decision about this call, which is what `onIndeterminate` records.\n   */\n  replaySafe: boolean\n  /**\n   * What to do when this call was entered but never settled — a resume cannot tell whether the\n   * side effect happened. Required, and a decision about THIS call, which is why it is separate\n   * from `replaySafe`.\n   */\n  onIndeterminate: 'retry' | 'halt' | 'skip'\n  /**\n   * Output field paths this tool is trusted to have SANITISED — the only way taint is cleared.\n   * Omitted (the default) means this node declassifies nothing. See the taint rules: type\n   * validation is not sanitisation, so declassification must be asserted, never inferred.\n   */\n  declassifies?: string[]\n}\n\n/**\n * A prompt is a SEQUENCE of literal text and references — never a string with an embedded DSL.\n *\n * @remarks\n * Same reasoning as `ArgValue`: this repo represents references structurally so they are checkable\n * at freeze and need no parser. The battery joins the parts, substituting each ref's resolved\n * value, immediately before calling `ReasonerFn`.\n */\nexport type PromptPart = { text: string } | NodeRef\n/**\n * A reason node ENDS IN A TOOL CALL, and that tool call IS its output — it never returns prose\n * to be parsed. `outputSchema` becomes the forced tool's `inputSchema`, so the model physically\n * cannot answer unstructured: the validator rejects malformed args and the battery retries\n * within `maxAttempts`. The captured, validated args are the node's `OutputItem.json`.\n * Note this node carries a Schema, NOT DeclaredField[] — a validator expresses nested objects\n * and unions that DeclaredField cannot, and it is what the forced tool needs anyway.\n */\nexport interface ReasonNodeDefinition {\n  /** The prompt as a sequence of literal text and references, joined immediately before dispatch. */\n  prompt: PromptPart[]\n  /**\n   * The output schema as an ENCODED STRING, not a live `Schema`. A live validation schema is NOT\n   * `Encodable` — encoding one throws\n   * `E_UNENCODABLE_VALUE: Value of type symbol (Symbol(override)) is not encodable` — so\n   * embedding it would make the plan unpersistable.\n   *\n   * The repo already solved this exact problem in `Tool` (src/lib/classes/tool.ts:449-490):\n   * `[ENCODE_METHOD]` stores `encodeSchema(this.#inputSchema)` and `[DECODE_METHOD]` rebuilds it.\n   * NOTE the real export names — `@nhtio/validation` exports `encode` and `decode`, and\n   * tool.ts:7 aliases them at the import:\n   * `import { validator, encode as encodeSchema, decode as decodeSchema } from '@nhtio/validation'`.\n   * There is no export literally named `encodeSchema`. Use the same aliasing so the call sites\n   * read unambiguously next to the encoder's own `encode`/`decode`.\n   */\n  outputSchema: string\n  /** Bounds the retry loop when the model's forced-tool args fail validation. Required, no default. */\n  maxAttempts: number\n}\n\n/**\n * `predicate` is `EncodableValue`, NOT `unknown` — the whole plan must persist, and an `unknown`\n * could hold a symbol, a circular object, or a live evaluator object that\n * `@nhtio/encoder` refuses. Each cell interprets the value its own way (the structured cell reads\n * a `{path, op, value}` tree; the jexl and Lua cells read a source string), and a cell's\n * `validate()` is what rejects a shape it cannot use — but the outer type guarantees the plan is\n * serialisable regardless of which cell is wired.\n */\n/**\n * A `transform` node converts one node's output into the shape a downstream node needs — the\n * bridge between what ADK tools actually return (`string | Uint8Array | SpooledArtifact | Media[]`)\n * and the pathable fields a `NodeRef` reads.\n *\n * **It invents no mapping language.** Each step names a descriptor from the artifact classes' OWN\n * `toolMethods` registry (`src/lib/classes/spooled_artifact.ts:56-70` — `{name, method,\n * description, argsSchema?, serialise?}`), which is exactly the declarative surface `forgeTools`\n * already drives for the model.\n *\n * **ONE VOCABULARY, stated once: a step names the descriptor's `name`** — the absolute,\n * LLM-facing identifier (`artifact_json_get`, `artifact_head`), which is why the field is called\n * `name` and not `method`: it is byte-equal to the `ArtifactMethodDescriptor` field it must match.\n * The battery then invokes that descriptor's own `method` (`json_get`, `head`) on the instance.\n * So the rule is: **the plan names what the model sees; the battery invokes what the descriptor\n * says.** An earlier draft called the field `method` while its examples used `name` values, so\n * two WPs and the test fixture read one contract two incompatible ways — and the two sets are\n * disjoint strings (`artifact_json_get` ≠ `json_get`), so half the examples were wrong either\n * way. `name` is the right half to keep: it is documented as \"Absolute tool name as exposed to\n * the LLM\", it is the identifier a model has already seen in a tool catalogue, and it is unique\n * across a class chain in a way `method` is not guaranteed to be. Note also that three base\n * `method` values are camelCase (`byteLength`, `lineCount`, `estimateTokens`) while every `name`\n * is snake_case — one more reason authoring against `name` is the stabler surface.\n *\n * The vocabulary is therefore `artifact_json_get`/`_pluck`/`_filter`/`_slice`,\n * `artifact_md_sections`/`_frontmatter`/`_headings`, `artifact_head`/`_tail`/`_grep`/`_cat` and\n * the rest of the base seven, plus whatever a consumer's own `SpooledArtifact` subclass adds — a\n * battery-specific expression language would have been a second surface to specify, validate and\n * lint, and this needs none.\n *\n * **Class-aware, and EXTENSIBLE for free.** The source class comes from\n * `InvocableTools.returns(tool)` — the consumer's declaration of what each tool returns, which is\n * the only party that knows. Freeze validates `steps[].name` against the class's **effective**\n * descriptor set and `args` against the descriptor's own `argsSchema`, so an `artifact_json_get`\n * on a Markdown artifact or a bad path is refused before approval. Where `returns()` yields\n * `undefined`, freeze refuses the transform naming the undeclared tool — the battery does not\n * guess a class.\n *\n * **`toolMethods` SHADOWS, so the effective set must be computed — the battery does it, not the\n * consumer.** This is the trap that makes `returns()` carry the CLASS rather than a descriptor\n * array. `spooled_artifact.ts:232-240` states it outright: *\"Each `toolMethods` array lists\n * **only** its own class's descriptors — subclasses do not concatenate inherited descriptors.\"*\n * `SpooledJsonArtifact.toolMethods` is its seven JSON descriptors and nothing else; the base\n * seven are composed at a different layer entirely (`SpooledJsonArtifact.forgeTools` calls\n * `SpooledArtifact.forgeTools(ctx)` and merges registries). So `instance.constructor.toolMethods`\n * returns the LEAF set only, and an earlier draft that took a descriptor array from the consumer\n * advertised a vocabulary including `artifact_head`/`_tail`/`_grep`/`_cat` that freeze would then\n * have refused on every JSON or Markdown artifact — a stated invariant the seam could not uphold.\n * There is no helper in core that unions the chain.\n *\n * So the battery ships one, and it is the only place the union is computed —\n * `effectiveToolMethods(ctor)`, declared below alongside `ArtifactClassLike`.\n *\n * It collects each class's OWN `toolMethods` (`Object.getOwnPropertyDescriptor`, so an inherited\n * static is not counted twice) from the leaf up through `Object.getPrototypeOf`, and dedupes by\n * `name` with **nearest class wins** — matching the `Tool.onCollision = 'replace'` semantics core\n * already documents for the same overlap. WP 01 owns it; WP 04 (freeze) and WP 07 (the transform\n * runtime) both call it, and it is exported so a consumer and the tests can assert the same set.\n *\n * The consequence for the consumer is that they declare a class, not a list — which is the\n * declaration they can actually get right, since `Tool.artifactConstructor` is exactly a\n * `() => SpooledArtifact subclass` closure they already wrote.\n *\n * Two consequences worth stating: freeze validates against a **declaration**, so a consumer whose\n * declaration disagrees with the tool's actual return gets a node failure at run time (the\n * transform's own output validation catches it) rather than a silent wrong answer; and\n * `{kind: 'text'}` needs no transform at all when the node declares a single field.\n *\n * Because the vocabulary IS the registry rather than a list this battery maintains, a new\n * `SpooledArtifact` subclass becomes usable in a `transform` the moment it exists, with no\n * orchestration change at all: a future `SpooledYamlArtifact` declaring\n * `artifact_yaml_get`/`_keys` is immediately a legal `transform` source, its args validated by its\n * own `argsSchema`, its docs generated from its own descriptors. The same holds for a consumer's\n * private subclass — the battery never needs to know the format. This is the concrete reason to\n * prefer the registry over a mapping DSL: a DSL would have to grow a YAML accessor; this does not.\n *\n * **This is what makes a handle useful in a plan.** A `call` returning a `SpooledArtifact` keeps\n * its bytes out of the plan; a `transform` reads exactly the slice the next node needs and emits\n * structured `OutputItem`s. No **plan content** holds a reference to bytes — the instance lives in\n * run state and the artifact is consumed on the branch that produced it — so the no-media-handles\n * rule survives intact in the sense that actually matters: nothing inside the approved digest\n * points at a store. See the content-vs-execution-state boundary stated with that rule.\n *\n * ### How the transform actually RECEIVES the artifact — the `artifacts` channel\n *\n * The methods a step names are real async instance methods reading through a `SpoolReader`\n * (`SpooledJsonArtifact.json_get` calls `this.#resolveRecords()` against its reader), so they\n * cannot be invoked on a plain value. `OutputItem.json` is `EncodableValue`-only by design and\n * `NodeRef` resolution yields `item.json`, so **the dataflow path that carries values cannot\n * carry the instance.** An earlier draft specified the node's purpose without ever naming the\n * channel that feeds it, which left WP 07 an interface with no implementable input and made the\n * resume note (\"its source artifact must still be resolvable\") a fallback assigned to a condition\n * no mechanism could satisfy.\n *\n * **The mechanism already exists in core, and it is a HANDLE, not a side table.** A\n * `SpooledArtifact` is itself encoder-round-trippable: `[ENCODE_METHOD]` emits\n * `{reader: ReaderDescriptor}` — a `{tag, locator}` pointer, *never* the bytes\n * (`spooled_artifact.ts:287-315`) — and `[DECODE_METHOD]` re-binds it through\n * `resolveSpoolReader(descriptor)`, the registry whose resolver closure re-injects the live\n * binding (a flydrive `Disk`, an OPFS root, `fetch`) a locator cannot carry\n * (`src/lib/contracts/reader_resolvers.ts`). Subclasses override the snapshot to add their own\n * discriminator, so a decoded `SpooledJsonArtifact` comes back as a `SpooledJsonArtifact`.\n * **This is verified in-repo, not inferred:** `tests/unit/encoding/round_trip.cross.spec.ts:190-198`\n * — *\"SpooledArtifact round-trips as an in-memory handle and re-reads its bytes\"* — plus a\n * durable-reader case with a consumer-registered resolver at :261-278. The whole file is green\n * (15/15).\n *\n * So the channel is a second, **non-encodable-by-value but handle-encodable** table alongside\n * `OutputTable`, keyed identically:\n *\n * `ArtifactTable` (declared in Shared contracts alongside `OutputTable`) —\n * `ReadonlyMap<string, SpooledArtifactLike>`, keyed `${nodeId}:${branchKey(branchId)}`, the SAME\n * key as `OutputTable`, so a `transform`'s `source: NodeRef` addresses one without a second\n * addressing scheme.\n *\n * - **Populated at `call` settlement.** When `CallInvokerFn` returns a `SpooledArtifactLike`, the\n *   executor records the *declared-output* `NodeOutput` in `OutputTable` as usual **and** the\n *   instance in `ArtifactTable` under the same key. A `call` whose result is a `string` puts\n *   nothing here.\n * - **Carried on the frame.** `PendingFrame` gains `artifacts: ArtifactTable`, branch-local and\n *   cloned on fan-out exactly like `outputs`, and `edge_taken` carries it for the same reason it\n *   carries `outputs`: a successor must receive its own branch's accumulation.\n * - **Persisted as HANDLES.** `PendingFrame.artifacts` and `edge_taken.artifacts` encode via each\n *   artifact's own `[ENCODE_METHOD]`, so the run log holds `{tag, locator}` pointers and never\n *   bytes — which is precisely the property the no-bytes-in-plan-state rule wanted, now achieved\n *   by the same mechanism core already uses rather than by refusing to persist anything.\n * - **Resume rebinds through the resolver registry**, which is what makes the previously-unsatisfiable\n *   resume note true: a resumed `transform` decodes its source handle and gets a working instance\n *   **iff** a resolver for that `tag` is registered. Two honest consequences, both stated rather\n *   than papered over:\n *     · The **`registerSpoolReaderResolver` requirement is load-bearing for resume**, not\n *       optional hygiene. In-memory and fetch resolvers auto-register with the encoding battery;\n *       a durable store's resolver the consumer must register themselves, because only they hold\n *       the live binding (already a verified fact in this plan's table). A missing resolver throws\n *       `E_NO_READER_RESOLVER`, which the transform surfaces as an ordinary node failure naming\n *       the tag — the specified mechanism the earlier \"producing turn is gone\" note lacked.\n *     · An **in-memory reader's locator carries its own bytes base64-encoded** (per\n *       `ReaderDescriptor`'s own contract), so an in-memory artifact survives resume but counts\n *       against `PlanBounds.maxEncodedBytes` — the reason a durable spool store is the right\n *       choice for a plan whose calls produce large artifacts. Stated, not enforced: this is a\n *       deployment decision, not something the battery can pick.\n *\n * ### What a step's result feeds the next step\n *\n * Chaining passes the **raw method return value**, never a serialised form. `serialise` is\n * `(result: unknown) => string`, so chaining through it would flatten exactly the structure a\n * following `emit: {as:'rows'}` needs as an array. Note this is not hypothetical laxity: **no\n * descriptor in any of the three core classes actually carries `serialise`** (verified: 7/7/8\n * descriptors, none), so an unstated rule would have been guessed differently by every\n * implementor.\n *\n * `serialise` is therefore consulted at exactly one point — converting a **final** result into a\n * string for an `emit: {as:'value'}` field whose `DeclaredField` type is `string` — and where the\n * descriptor supplies none, the battery calls core's **exported** `defaultSerialise`\n * (`spooled_artifact.ts:129`, documented *\"Exported for reuse by subclass `forgeTools`\n * overrides\"*) rather than reimplementing its rules (string as-is; `string[]` newline-joined with\n * `'(empty list)'` when empty; `number` via `String`; `undefined` → `'(undefined)'`; otherwise\n * `JSON.stringify(v, null, 2)`).\n */\nexport interface TransformNodeDefinition {\n  /** Which node's output to read. Its declared artifact class determines the legal methods. */\n  source: NodeRef\n  /**\n   * Applied in order; each step's result feeds the next. `name` is the DESCRIPTOR'S `name` —\n   * the absolute LLM-facing tool name (`artifact_json_get`), never the instance method name\n   * (`json_get`). Validated at freeze against `effectiveToolMethods(sourceClass)`.\n   */\n  steps: { name: string; args?: Record<string, EncodableValue> }[]\n  /** How the final result becomes items. `rows` expects an array and emits one item per element. */\n  emit: { as: 'value'; field: string } | { as: 'rows' }\n  /** What downstream nodes may reference — validated against the emitted items, as for `call`. */\n  output: DeclaredField[]\n}\n\n/** A two-way branch: the predicate's verdict picks the `match` or `no_match` handle. */\nexport interface BranchNodeDefinition {\n  /** Which `PredicateEvaluator` cell interprets `predicate`. Refused at freeze if not wired. */\n  evaluator: string\n  /** The cell's own predicate form. `EncodableValue` so the whole plan stays persistable. */\n  predicate: EncodableValue\n}\n/** An n-way switch: the verdict names a case, or falls to the required `default` handle. */\nexport interface SelectNodeDefinition {\n  /** Which `PredicateEvaluator` cell interprets `predicate`. Refused at freeze if not wired. */\n  evaluator: string\n  /** The cell's own predicate form. `EncodableValue` so the whole plan stays persistable. */\n  predicate: EncodableValue\n  /** The legal case labels. Each names a `` case_${label} `` handle. */\n  cases: string[]\n}\n/**\n * A join is a **DIAMOND join only**: it closes a fan-out that a single ancestor opened. The\n * restriction is what makes joins implementable, and it is stated over the DIVERGENCE POINT, not\n * over the immediate predecessors.\n *\n * Freeze-enforced topology, checked by walking the graph:\n * - The join's **fork** is its immediate dominator. `entry → a; a → b; a → c; b → j; c → j` is the\n *   canonical diamond and its fork is `a`. (An earlier draft required all incoming edges to share\n *   one *immediate source*, which refused exactly this graph while admitting only a degenerate\n *   double-edge. The fork is the right notion.)\n * - **The fork must actually diverge toward the join: there must be MORE THAN ONE distinct\n *   fork→join route.** Without this the rule accepts two degenerate shapes that are not diamonds\n *   at all: `entry → a → join` (a one-route \"barrier\" that is just a pass-through), and\n *   `fork → left|right → shared → join`, where the paths have already reconverged at `shared`, so\n *   the immediate dominator slides down to `shared` and the join again sees one route. Both are\n *   refused: a `join` whose fork→join route count is 1 is a `join` that should not exist, and the\n *   error says so.\n * - **No reconvergence inside the diamond**: the fork→join region must contain no node with\n *   in-degree > 1 other than the join itself. This is what makes \"the immediate dominator is the\n *   divergence point\" true rather than accidental, and it removes the second degenerate case above\n *   at its root.\n * - Every route from the fork to the join is join-free: no nested join inside the diamond, so the\n *   contributor set cannot itself depend on another barrier.\n * - `required` is not authored. It IS the number of distinct routes from fork to join, computable\n *   at freeze because the region is acyclic, join-free and reconvergence-free.\n *\n * Four consequences, each an unresolvable problem under general DAG joins:\n *\n * 1. **Correlation is decidable from the first arrival**, because the fork is known statically:\n *    the barrier key is the arriving route truncated at the fork (see `JoinState.correlationKey`).\n * 2. **Late arrivals cannot occur**: `required` equals the route count, so the barrier fires when\n *    every route has arrived and never before. No fired-barrier state, no dropped work, no second\n *    firing.\n * 3. **The join's identity is a GRAPH CONSTANT**: `of` is the sorted list of ALL its incoming edge\n *    ids — known at freeze, independent of which predicates fired — so a downstream `NodeRef` to\n *    a post-join node is authorable.\n * 4. **Its output is deterministic and its SHAPE is specified**: one `OutputItem` per **arrival**\n *    — which equals the incoming-edge count exactly because the reconvergence-free rule makes\n *    every fork→join route traverse a distinct incoming edge, so \"per arrival\" and \"per edge\"\n *    cannot disagree. Sorted by `via` then `branch` for a total order. Each carries\n *    `{ via: <edgeId>, from: <source nodeId>, branch: <branchKey of that arrival> }`. A join\n *    contributes no data of its own — it is a barrier — so its items are *provenance*, which is\n *    the only thing it actually knows. A downstream `NodeRef` to the join therefore reads which\n *    routes converged (useful in a predicate: \"did the retry path contribute?\"), while the\n *    contributing nodes' real outputs are read by referencing those nodes directly, which the\n *    successor's unioned `OutputTable` makes possible. Leaving the shape unstated would let one\n *    implementation emit `{}` and another wrap arrival tables — incompatible observable APIs.\n *\n * Interaction with branching, stated because it is the sharp edge: a `branch`/`select` INSIDE a\n * diamond can leave a route unfired, so the barrier never completes. That is not a hang — the\n * executor settles the run `halted` with `{kind:'join_unsatisfiable', nodeId}` once no live frame\n * can still reach it. An author who wants \"proceed with whichever finished\" wants a `select` on a\n * prior result, not a partial join; the plan says so rather than offering a quorum knob whose\n * semantics it cannot pin down.\n *\n * The successor frame's `OutputTable` is the UNION of the arrivals' tables; keys are\n * `${nodeId}:${branchKey}`, path-unique, so the union cannot collide and needs no merge policy.\n */\nexport interface JoinNodeDefinition {\n  /** Nothing to configure. `required` and the fork are both derived from the graph. */\n  readonly kind?: 'diamond'\n}\n\n/**\n * `phase` is a LABEL: it groups nodes for reading, rendering and progress, and has NO execution\n * meaning — edges alone order execution. A NodeId is validated snake_case (no `/`, no leading\n * `.`) so it can never be mistaken for a path or copied as a citation.\n */\ninterface PlanNodeBase {\n  id: NodeId\n  phase?: string\n}\n/** A node: its identity and phase, plus exactly one kind-specific definition. */\nexport type PlanNode = PlanNodeBase &\n  (\n    | { kind: 'entry'; definition: EntryNodeDefinition }\n    | { kind: 'call'; definition: CallNodeDefinition }\n    | { kind: 'reason'; definition: ReasonNodeDefinition }\n    | { kind: 'transform'; definition: TransformNodeDefinition }\n    | { kind: 'branch'; definition: BranchNodeDefinition }\n    | { kind: 'select'; definition: SelectNodeDefinition }\n    | { kind: 'join'; definition: JoinNodeDefinition }\n  )\n\n// ── the scoped reading surface ───────────────────────────────────────────────\n/** ONE flat level. Entries carry exact surface forms, not paraphrase. */\nexport interface PlanOutline {\n  /** The plan this outlines. */\n  planId: PlanId\n  /** Its lifecycle state NOW — read from the store, not folded from the log. */\n  state: PlanState\n  /** The content digest at the revision outlined. */\n  digest: string\n  /** Total nodes, so a reader knows what the phase entries account for. */\n  nodeCount: number\n  /** One entry per phase label, in authoring order. */\n  phases: PhaseEntry[]\n  /** Nodes with no `phase`, addressed exactly the same way. `undefined` when every node has one. */\n  unphased: PhaseEntry | undefined\n}\n/** One phase's entry in an outline. Carries exact surface forms, never paraphrase. */\nexport interface PhaseEntry {\n  /** The phase label. */\n  phase: string\n  /** One line. */\n  summary: string\n  /** VERBATIM node ids — this is what a `NodeRef` must cite, so it cannot be abbreviated. */\n  nodeIds: NodeId[]\n  /** Verbatim tool names of this phase's `call` nodes. */\n  tools: string[]\n  /** How many issues fall in this phase, so a reader knows where to look without reading all. */\n  issueCount: number\n}\n/** A slice. Self-locating: it carries enough neighbourhood to keep linking without re-reading. */\nexport interface PlanSlice {\n  /** The nodes in this slice, in full. */\n  nodes: PlanNode[]\n  /** The phase this slice was taken from, when it was taken by phase. */\n  phase?: string\n  /** Immediate predecessors/successors of the slice, by id, so linking needs no second read. */\n  boundary: { incoming: NodeId[]; outgoing: NodeId[] }\n  /** Issues falling within this slice. */\n  issues: PlanIssue[]\n}\n\n// ── run events: the persisted wire contract ─────────────────────────────────\n/** How one frame settled. The three cases are exhaustive — a frame that has not settled has none. */\nexport type NodeOutcome =\n  | { status: 'ok'; output: NodeOutput }\n  | { status: 'failed'; handled: boolean; error: { name: string; message: string } }\n  | { status: 'skipped'; reason: 'indeterminate_skip' }\n\n/**\n * Identifies one execution of one node: the node, and the path that reached it. `kind` rides\n * along so the fold can classify without the graph (only a `call` frame can be indeterminate).\n * `viaEdgeId` is the edge that produced this frame — `undefined` for the entry frame, which no\n * edge produced. `branchId` is the path identity and is what makes a frame unique.\n */\nexport interface FrameRef {\n  /** Which node. */\n  nodeId: NodeId\n  /** Rides along so the fold can classify without the graph — only a `call` can be indeterminate. */\n  kind: PlanNodeKind\n  /** The path that reached it. This is what makes a frame unique. */\n  branchId: BranchId\n  /** The edge that produced this frame. `undefined` for the entry frame, which no edge produced. */\n  viaEdgeId: string | undefined\n}\n\n/**\n * The persisted wire contract. `foldRun` derives an entire `RunProjection` from a list of these\n * with no graph, no store and no side channel, which is what makes resume a pure function of the\n * log rather than of surviving process state.\n */\nexport type RunEvent =\n  /** Carries `runId` so `foldRun` can produce it from events alone, with no side channel. */\n  | { kind: 'run_started'; runId: string; digest: string; at: string }\n  | { kind: 'node_entered'; frame: FrameRef; at: string }\n  /** Carries the OUTPUT, not merely a status — this is what the resume fold rebuilds from. */\n  | { kind: 'node_settled'; frame: FrameRef; outcome: NodeOutcome; at: string }\n  /**\n   * Carries the frame it PRODUCED **and that frame's branch-local `outputs`**, so the fold can\n   * rebuild a complete `PendingFrame` without the graph. Carrying only `from`/`to` was not enough:\n   * a `PendingFrame` needs the accumulated branch-local table, and the global outputs fold cannot\n   * supply it — a successor must receive precisely its own branch's accumulation, not every output\n   * recorded run-wide, which after a fan-out or a join are different things.\n   *\n   * Size note, since this duplicates state: the table holds `NodeOutput` values already present\n   * in earlier `node_settled` events, so an implementation may persist it as the list of\n   * `${nodeId}:${branchKey}` keys and rehydrate the values from those settlements. The contract is\n   * the *content*; the encoding is the store's business.\n   */\n  | {\n      kind: 'edge_taken'\n      edgeId: string\n      handle: EdgeHandle\n      from: FrameRef\n      to: FrameRef\n      outputs: OutputTable\n      artifacts: ArtifactTable\n      evidence?: EncodableValue\n      at: string\n    }\n  | { kind: 'frontier_snapshot'; frames: PendingFrame[]; joins: JoinState[]; at: string }\n  | { kind: 'run_interrupted'; cause: InterruptionCause; frame?: FrameRef; at: string }\n  | { kind: 'run_settled'; outcome: 'completed' | 'halted' | 'aborted'; at: string }\n\n/**\n * A live frame: its identity, its branch-local value table, and its branch-local artifact table.\n * `artifacts` is cloned on fan-out exactly like `outputs` and is what a `transform` reads its\n * source instance from; it persists as handles, so a snapshot carries pointers, never bytes.\n */\nexport interface PendingFrame {\n  /** Which execution of which node this frame is. */\n  frame: FrameRef\n  /** The branch-local value table this frame sees. Cloned on fan-out. */\n  outputs: OutputTable\n  /**\n   * The branch-local artifact table — where a `transform` reads its source instance. Cloned on\n   * fan-out exactly like `outputs`, and persisted as handles, so a snapshot carries pointers,\n   * never bytes.\n   */\n  artifacts: ArtifactTable\n}\n/**\n * A join's partial barrier.\n *\n * **Correlation is decidable from ONE arrival**, and the diamond restriction is what buys it: the\n * join's **fork** is known at freeze, so the barrier key is the arriving route **truncated at the\n * fork** — the prefix of segments up to and including the one that entered the fork:\n * `` correlationKey = `${nodeId}@${branchKey(truncateAtFork(arriving, fork))}` ``.\n * Every sibling route passes through the same fork by the topology rule, so every sibling\n * truncates to the same prefix; and two different *executions* of the fork (reached by different\n * outer routes) truncate to different prefixes, so their barriers stay separate.\n *\n * Two rules that do NOT work, recorded because both were tried: the longest common prefix of the\n * *contributors* is unknown when the first arrival lands, and \"drop the last segment\" gives the\n * immediate predecessor's route — which differs per sibling on a real diamond\n * (`a→b→j` truncates to `e1`, `a→c→j` to `e2`). Truncating at a statically-known fork is what\n * makes all three properties hold at once.\n *\n * This is also why `BranchId` keeps its route rather than a hash: the rule needs the structure.\n * And why edge ids alone are insufficient: two arrivals over the same incoming edge from different\n * fork executions differ only in earlier segments.\n *\n * `arrivals` records what landed; the barrier fires when `arrivals.length` equals `required`, and\n * a repeat of the same `(branchKey, edgeId)` pair is idempotent. Because the threshold IS the\n * fork→join route count, there is no late-arrival case. The merged frame's identity is the\n * correlation prefix + `{ join: nodeId, of: sorted(ALL incoming edge ids) }` — see `BranchId` for\n * why the prefix must be retained, and note `of` being a graph constant is what makes a downstream\n * `NodeRef` to a post-join node authorable at freeze.\n */\nexport interface JoinState {\n  /** The join this barrier belongs to. */\n  nodeId: NodeId\n  /**\n   * The barrier's identity: `` `${nodeId}@${branchKey(truncateAtFork(arriving, fork))}` ``. Every\n   * sibling route truncates to the same prefix; two executions of the fork truncate to different\n   * ones, so their barriers stay separate. See this type's remarks for the two rules that do not\n   * work and why.\n   */\n  correlationKey: string\n  /**\n   * Each arrival carries the branch-local `OutputTable` **and `ArtifactTable`** it arrived with.\n   * Without these a resumed half-satisfied join could not build its successor's dataflow context:\n   * the contributing branches' outputs and artifact handles were consumed into the barrier and are\n   * nowhere else in the frontier. `artifacts` is here for exactly the reason `outputs` is — a\n   * `transform` downstream of a join must still reach an instance a contributing branch produced.\n   */\n  arrivals: {\n    branch: BranchId\n    edgeId: string\n    outputs: OutputTable\n    artifacts: ArtifactTable\n  }[]\n  /** The join's in-degree. Not authored — derived from the graph, so it cannot disagree with it. */\n  required: number\n}\n\n/**\n * The fold. Every field derives from the events alone — no graph, no store, no side channel:\n * `runId`/`digest` from `run_started`; `outputs` from each `node_settled` with status 'ok';\n * `frameStatus` from the entered/settled pairing per frame; `indeterminate` from entered-unsettled\n * frames whose `FrameRef.kind === 'call'`; the frontier from the last `frontier_snapshot`, then\n * advanced by the events after it — each later `node_settled` removes its frame and each later\n * `edge_taken` adds its `to` frame **with the `outputs` and `artifacts` that event carries**,\n * which is why `edge_taken` carries the produced frame and both of its branch-local tables: a\n * `PendingFrame` is `{frame, outputs, artifacts}` and no part is derivable from the others.\n *\n * `artifacts` is the one field whose values are not plain data: they decode from persisted\n * `{tag, locator}` handles through `resolveSpoolReader`, so folding a log whose artifact tags have\n * no registered resolver throws `E_NO_READER_RESOLVER` from the decode rather than yielding a\n * half-built projection. That is the same \"register before you decode\" precondition the encoding\n * battery already imposes, surfacing here rather than silently later.\n *\n * Deterministic and total. A list whose first event is not `run_started` is malformed and throws\n * rather than defaulting.\n */\nexport type { foldRun } from './runs'\n\n// ── injected seams ──────────────────────────────────────────────────────────\n/** The `call` node's invoker. Consumer owns tool resolution and execution; the battery owns\n *  validating the result against the node's declared `output` and recording it. */\n/**\n * Returns the tool's result **in the shape the tool actually produced** — the same union an ADK\n * `ToolHandler` returns (`src/lib/classes/tool.ts:57`). An earlier draft had this return\n * `OutputItem[]`, which quietly obliged every consumer to invent a conversion the plan never\n * specified: a tool returning a JSON-in-a-string, or a `SpooledArtifact` handle, has no obvious\n * mapping to pathable fields, and three implementors would have chosen three.\n *\n * So the invoker just invokes, and the conversion is explicit in the graph:\n *   · a `string` result the node declares as one field → available directly;\n *   · anything needing extraction (a JSON string, an artifact, a markdown document) → a\n *     `transform` node names the artifact method that extracts it.\n * `Media`/`Uint8Array` results are refused at freeze for a node whose `output` declares fields —\n * bytes are not pathable and the IR holds no media handles.\n */\nexport type CallInvokerFn = (req: {\n  tool: string\n  args: Record<string, EncodableValue> // NodeRefs already resolved\n  signal?: AbortSignal\n}) => Promise<ToolResult>\n\n/** Exactly what an ADK tool handler may return. The battery narrows it, never guesses at it. */\nexport type ToolResult = string | Uint8Array | SpooledArtifactLike | MediaLike | MediaLike[]\n/**\n * Structural, per CONTRIBUTING §13 — the battery does not import the core classes. Note\n * `argsSchema`: an earlier draft omitted it while claiming freeze validates a step's args against\n * it, so the type could not support the check it was cited for. `serialise` likewise, since the\n * transform runtime needs the descriptor's own formatter rather than a guess.\n */\nexport interface ArtifactMethodDescriptor {\n  /** Absolute, LLM-facing tool name (`'artifact_json_get'`). What a `steps[].name` cites. */\n  name: string\n  /** Instance method this descriptor invokes (`'json_get'`, `'head'`). Not what a step names. */\n  method: string\n  /** The model-facing description, as core wrote it. */\n  description: string\n  /** The method's argument schema. Freeze validates a step's `args` against it. */\n  argsSchema?: ObjectSchema\n  /**\n   * The descriptor's own formatter, consulted at exactly ONE point: converting a FINAL result into\n   * a string for an `emit: {as:'value'}` field whose declared type is `string`. Never used when\n   * chaining steps — that would flatten the structure a following `emit: {as:'rows'}` needs. Where\n   * absent, the battery calls core's exported `defaultSerialise` rather than reimplementing it.\n   */\n  serialise?: (result: unknown) => string\n}\n\n/**\n * An artifact CLASS, structurally. Carries only its OWN descriptors — the base seven are on an\n * ancestor, per core's shadowing rule — so the battery must walk the chain rather than read this\n * one array. `effectiveToolMethods` is the only place that walk happens.\n */\nexport interface ArtifactClassLike {\n  /**\n   * This class's OWN descriptors only. Core's `toolMethods` SHADOWS rather than concatenates, so\n   * reading this array directly yields the leaf set — use `effectiveToolMethods` to get the union.\n   */\n  readonly toolMethods?: readonly ArtifactMethodDescriptor[]\n}\n/**\n * Every descriptor reachable on a class, leaf-first up the static prototype chain, deduped by\n * `name` with nearest-class-wins. Counts only OWN `toolMethods` per class\n * (`Object.getOwnPropertyDescriptor`) so an inherited static is not collected twice. Exported\n * because freeze (WP 04), the transform runtime (WP 07) and the tests must all agree on the set,\n * and a consumer needs to be able to print it.\n */\nexport type { effectiveToolMethods } from './artifact_methods'\n\n/**\n * A spooled artifact instance, structurally — per CONTRIBUTING §13, the battery does not import\n * the core classes. This is the value a `call` may return and a `transform` reads its methods from.\n */\nexport interface SpooledArtifactLike {\n  /** The class, for `effectiveToolMethods`. NOT a pre-unioned descriptor list — see above. */\n  readonly constructor: ArtifactClassLike\n  /** The descriptor-named instance methods, invoked by a `transform` step. */\n  [method: string]: unknown\n}\n/** A media value, structurally. Refused at freeze for a node whose `output` declares fields. */\nexport interface MediaLike {\n  /** The media's MIME type — the only member the battery reads. */\n  readonly mimeType: string\n}\n\n/**\n * How a run is started, and the only place external input enters the graph. The `entry` node's\n * output is materialised from `input` BEFORE any other node runs — validated against its\n * `EntryNodeDefinition.input` (`DeclaredField[]`), then committed as the entry frame's\n * `node_settled`, so `NodeRef`s address it exactly like any other node's output and the resume\n * fold rebuilds it from events like any other.\n *\n * `input` is the taint origin: every value in it is tainted, and provenance propagates from here\n * (see the taint rules). Input that fails validation aborts the run before any side effect —\n * never a partially-started run.\n */\n/**\n * The execution dependencies. Supplied at construction, per run, or both.\n *\n * PRECEDENCE, stated once so it cannot drift: **per-run wins, field by field, over construction**;\n * anything absent from both is a construction-time error if the plan needs it. `evaluators` merges\n * by cell `id` (a per-run cell replaces the configured one with the same id, others survive)\n * because a run legitimately swaps one cell while keeping the rest. Everything else replaces\n * wholesale.\n */\nexport interface RunDeps {\n  /** How a `call` node actually invokes its tool. The consumer owns resolution and execution. */\n  invokeCall: CallInvokerFn\n  /** How a `reason` node dispatches to a model. */\n  reason: ReasonerFn\n  /** The predicate cells. A plan needing a cell that is absent is refused at freeze, not at run. */\n  evaluators: PredicateEvaluator[]\n  /**\n   * Optional execution lease. Best-effort COORDINATION, not mutual exclusion: a TTL lease without\n   * a fencing token cannot exclude a partitioned holder.\n   */\n  locks?: PlanLockFactory\n}\n\n/**\n * Per-run inputs. Every `RunDeps` member is OPTIONAL here: `createOrchestration` already holds\n * whatever was configured, so a caller repeats only what it wants to override. `Orchestration`'s\n * `executePlan` is therefore this shape, not the fully-required `RunDeps` — an earlier draft made\n * every dependency mandatory per run while also calling construction the assembly point, which\n * meant a caller had to repeat what it had just configured and left the override rule undefined.\n */\nexport interface RunOptions extends Partial<RunDeps> {\n  /**\n   * The run's input, materialised as the entry node's output before any other node runs. This is\n   * the taint origin: every value here is tainted, and provenance propagates from it. Input that\n   * fails validation aborts the run before any side effect — never a partially-started run.\n   */\n  input: Record<string, EncodableValue>\n  /** Resume an interrupted run rather than start one. */\n  resumeRunId?: string\n  /** Cancels the run. Surfaces as a `turn_abort` interruption. */\n  signal?: AbortSignal\n  /**\n   * Deliberately uninhabited. Indeterminate policy is per-node (`CallNodeDefinition.onIndeterminate`)\n   * and never a run-level default — the decision belongs with the call whose side effect is at stake.\n   */\n  indeterminate?: never\n}\n/**\n * Starts or resumes a run. Takes no entry argument — the entry node is unique by freeze invariant\n * and its output is materialised from `RunOptions.input`.\n */\nexport type ExecutePlanFn = (planId: PlanId, options: RunOptions) => Promise<RunProjection>\n\n/** The `reason` node's dispatcher. See ReasonNodeDefinition: it terminates in a tool call. */\nexport type ReasonerFn = (req: {\n  prompt: string // parts already joined and refs resolved\n  outputSchema: Schema // already decoded by the battery\n  maxAttempts: number\n  signal?: AbortSignal\n}) => Promise<Record<string, EncodableValue>> // the captured, validated tool args\n\n/**\n * A predicate cell: the seam that lets a `branch`/`select` interpret its own predicate form without\n * this battery specifying an expression language.\n */\nexport interface PredicateEvaluator {\n  /** How a node names this cell in `evaluator`. Cells merge by this id across construction and run. */\n  readonly id: string\n  /**\n   * Acquire whatever the cell needs — typically an optional peer. Awaited at construction, so a\n   * missing peer fails at boot with a named error rather than part-way through a freeze.\n   */\n  load(): Promise<void>\n  /** Reject a predicate shape this cell cannot use. Called at freeze, so refusal precedes approval. */\n  validate(node: PlanNode): Promise<void>\n  /** Decide the node's outcome. Called once per frame reaching the node. */\n  evaluate(node: PlanNode, ctx: PredicateContext): Promise<PredicateVerdict>\n}\n/** What a cell may read when evaluating. Deliberately narrow: outputs and the frame, nothing live. */\nexport interface PredicateContext {\n  /** The frame's branch-local value table. */\n  outputs: OutputTable\n  /** Which execution of which node is being decided. */\n  frame: FrameRef\n}\n/** A cell's decision, in the shape the node kind that asked for it expects. */\nexport type PredicateVerdict =\n  | { kind: 'branch'; matched: boolean }\n  | { kind: 'select'; caseLabel: string | null } // null → the 'default' handle\n\n/**\n * One unit of authority a `call` node claims. Approval binds the canonicalised union of every claim\n * in the plan, so what an operator approved and what may run are the same set.\n */\nexport interface AuthorityClaim {\n  /** What kind of thing may be acted on. */\n  capability: string\n  /** Which instances of it. */\n  scope: string\n  /** What may be done to them. */\n  verb: AuthorityVerb\n}\n/** The closed verb set. Closed so an authority set is comparable, not merely readable. */\nexport type AuthorityVerb = 'list' | 'read' | 'create' | 'update' | 'delete'\n\n// ── ops, projections and views ──────────────────────────────────────────────\n/** Every op carries actor/lamport identity; the fold is deterministic over any arrival order. */\ninterface OpBase {\n  opId: string\n  actorId: string\n  lamport: number\n  at: string\n}\n/**\n * One authoring edit. Every op carries actor and lamport identity, and the fold is deterministic\n * over any arrival order — so two offline authors' logs converge when they meet.\n */\nexport type PlanOp = OpBase &\n  (\n    | { op: 'add_node'; node: PlanNode }\n    /** Records the removed node AND its incident edge ids so the fold is order-independent. */\n    | { op: 'remove_node'; nodeId: NodeId; incidentEdgeIds: string[] }\n    /**\n     * `ArgValue`, not `EncodableValue` — a staged argument or a `PromptPart` may contain a\n     * `NodeRef`, and `NodeRef` sits deliberately OUTSIDE `EncodableValue`\n     * (`ArgValue = EncodableValue | NodeRef | …`). Typing the op's value as `EncodableValue` made\n     * the only node-update op unable to express the commonest authoring edit — \"point this\n     * argument at that node's output\" — so an implementor would have had to cast around the\n     * contract or invent a second op.\n     */\n    | { op: 'set_node_field'; nodeId: NodeId; path: string; value: ArgValue }\n    /** Replace a whole definition — what tier B's `set_node_config` compiles to. */\n    | { op: 'set_node_definition'; nodeId: NodeId; definition: PlanNode['definition'] }\n    | { op: 'set_node_phase'; nodeId: NodeId; phase: string | null }\n    | { op: 'add_edge'; edge: PlanEdge }\n    | { op: 'remove_edge'; edgeId: string }\n    | { op: 'set_bounds'; bounds: PlanBounds }\n  )\n\n/** The resource envelope. Plan CONTENT, so it is digested and an operator approves it. */\nexport interface PlanBounds {\n  /** Cap on nodes. Also bounds route length, since a route cannot revisit a node. */\n  maxNodes: number\n  /** Cap on edges. */\n  maxEdges: number\n  /** Cap on total node executions in a run — the bound on fan-out breadth times depth. */\n  maxSteps: number\n  /** Cap on simultaneously live frames. */\n  maxConcurrentFrames: number\n  /** Cap on the encoded plan size in bytes — the bound on staged byte payloads. */\n  maxEncodedBytes: number\n}\n\n/**\n * The canonical initial bounds — the **fold's seed**, not an op. `foldOps` starts from these, so a\n * plan at revision 0 (an empty log) has a complete `RawPlanView` and a well-defined digest, and\n * `set_bounds` ops override it thereafter. Without a fixed seed each implementor would invent a\n * default or leave bounds absent, and since bounds are plan CONTENT that would give the same\n * logical plan different digests across stores — breaking approval binding and store conformance.\n * Every member stays required, so an override is total and cannot half-specify.\n */\nexport const DEFAULT_PLAN_BOUNDS: PlanBounds = {\n  maxNodes: 256,\n  maxEdges: 512,\n  maxSteps: 4096,\n  maxConcurrentFrames: 32,\n  maxEncodedBytes: 1_048_576,\n}\n\n/**\n * What freeze validation needs that is NOT derivable from the plan itself. Passed to\n * `freezePlan()` by whoever holds it — WP 09's forge holds the Tier-C allowlist, WP 04's\n * `validation.ts` consumes this interface, so WP 04 depends on the TYPE (declared here, in WP 01)\n * and never on WP 09's implementation. That is what keeps the dependency acyclic: `validation.ts`\n * imports `InvocableTools` from shared contracts; the forge supplies an instance at call time.\n */\n/**\n * THE battery's single entry point. Everything public is reached through the object it returns, so\n * it is the one place a precondition can be enforced for every operation — which is why the\n * encoder check lives here (see Serialization).\n *\n * It is `async` because it eagerly `await import('@nhtio/encoder')`, throwing\n * `E_ORCH_ENCODER_REQUIRED` (naming the package and install command) before any plan can exist.\n * It also `await`s `load()` on every supplied evaluator cell, so a missing optional peer surfaces\n * at construction rather than at freeze. A cell supplied per-run instead is loaded at that point,\n * with the same named error.\n */\nexport type CreateOrchestration = (config: {\n  store: PlanStore\n  invocable: InvocableTools\n  /** Defaults for every run. A run may override any field — see `RunOptions` for precedence. */\n  deps?: Partial<RunDeps>\n  /** Consumer-defined plan shapes a model can instantiate. Validated at construction. */\n  templates?: PlanTemplate[]\n}) => Promise<Orchestration>\n\n// ── templates ───────────────────────────────────────────────────────────────\n/**\n * A consumer-defined plan shape, written in TypeScript and registered at construction — so it\n * versions with the consuming application, needs no store seeding, and can be validated once at\n * boot rather than per instantiation.\n *\n * Note what a template holds: **op INPUTS without identity**. A `PlanOp` requires\n * `opId`/`actorId`/`lamport`/`at`, none of which a static literal can carry (the same reason\n * bounds are a fold seed rather than an implied op). Instantiation mints that identity.\n */\nexport interface PlanTemplate {\n  /** Stable identity — what a model names to instantiate. */\n  id: string\n  /** One line, shown by `list_templates`. */\n  summary: string\n  /** Declared holes, same type as the entry node's input — so a model fills a form, not a graph. */\n  params: DeclaredField[]\n  /**\n   * The shape. NOT `PlanNode[]` — a template's staged values may hold a `ParamRef`, which\n   * `ArgValue` deliberately excludes, so a consumer writing a template in TypeScript could not\n   * place a hole without a cast. `TemplateNode` widens exactly that one axis.\n   */\n  nodes: TemplateNode[]\n  /**\n   * The shape's edges. Identical to a plan's — an edge holds no staged values, so no hole can sit\n   * in one.\n   */\n  edges: PlanEdge[]\n  /** Omitted ⇒ `DEFAULT_PLAN_BOUNDS`. */\n  bounds?: PlanBounds\n}\n\n/** A plan node whose staged values may additionally contain template holes. */\nexport type TemplateArgValue =\n  | ArgValue\n  | ParamRef\n  | TemplateArgValue[]\n  | { [k: string]: TemplateArgValue }\n/** A `PlanNode` whose definition may additionally contain template holes. */\nexport type TemplateNode = Omit<PlanNode, 'definition'> & {\n  definition: TemplateDefinitionOf<PlanNode['definition']>\n}\n/** Structurally identical to the node definitions, with `ArgValue` widened to `TemplateArgValue`. */\nexport type TemplateDefinitionOf<D> = {\n  [K in keyof D]: D[K] extends ArgValue\n    ? TemplateArgValue\n    : D[K] extends Record<string, ArgValue>\n      ? Record<string, TemplateArgValue>\n      : D[K] extends PromptPart[]\n        ? ({ text: string } | NodeRef | ParamRef)[]\n        : D[K]\n}\n\n/**\n * A hole in a template's staged values, substituted at instantiation. A CLASS for the same reason\n * `NodeRef` is: a look-alike record must not be mistaken for a hole. Registered by\n * `registerOrchestrationEncodables()` so a template value round-trips.\n */\n// A real runtime CLASS; its single definition lives in `./encoding`. Type-only re-export, for the\n// same reason as `NodeRef` above.\nexport type { ParamRef } from './encoding'\n\n/** The outcome of instantiating a template. Failure is a value, not a throw — a model reads it. */\nexport type InstantiateResult =\n  | { ok: true; planId: PlanId; issues: PlanIssue[] } // issues are non-fatal; plan is `editable`\n  | { ok: false; reason: 'unknown_template' | 'invalid_args'; detail: string }\n\n/** The public surface. Each member is specified in its own section; this is the assembly. */\nexport interface Orchestration {\n  /**\n   * `inputs` is optional because `createOrchestration` already holds `invocable` and any\n   * configured `evaluators`; a caller passes it only to override, with the same field-by-field\n   * precedence as `RunOptions` (evaluators merging by cell id).\n   *\n   * **A supplied `invocable` REPLACES the configured allowlist wholesale — it is not intersected\n   * with it.** That is deliberate: `evaluators` merge because two cells with different ids are\n   * additive, while two allowlists are a single answer to \"what may a staged call invoke\", and\n   * silently intersecting them would make the effective allowlist something neither the assembly\n   * nor the caller wrote.\n   *\n   * The consequence is worth stating plainly: passing a WIDER `invocable` here freezes a plan\n   * against that wider set, so a plan naming a tool outside the assembly's allowlist can reach\n   * `executable`. This is a HOST-ONLY capability — the forged tools always pass the assembly's\n   * own `invocable`, so no model can reach this parameter — but if you expose `freezePlan` to\n   * anything less trusted than your own assembly code, pass no `inputs` at all.\n   *\n   * Freeze needs them because a\n   * `branch`/`select` with no wired cell, or a `call` naming a tool outside tier C, is refused\n   * there — so a plan whose cell is supplied per-run must supply it here too, and that is the\n   * point of the override. An earlier draft exported `freezePlan(planId)` while specifying\n   * `freezePlan(planId, inputs: FreezeInputs)` elsewhere, which left WP 04 and WP 12 without one\n   * contract to implement.\n   */\n  freezePlan(\n    planId: PlanId,\n    inputs?: Partial<FreezeInputs>\n  ): Promise<{ ok: boolean; issues: PlanIssue[] }>\n  /**\n   * The permission gate: it IS the `reviewable → executable` transition, so \"approved\" and\n   * \"executable\" are one fact. Refused unless the record's digest and authority set match the\n   * frozen plan exactly — approving content that was never shown is the failure this prevents.\n   */\n  approvePlan(planId: PlanId, record: ApprovalRecord): Promise<TransitionResult>\n  /** Start or resume the plan's one run. A plan id admits at most one run, ever. */\n  executePlan: ExecutePlanFn\n  /** Mint a new `editable` plan from a registered template with `args` substituted for its holes. */\n  instantiate(templateId: string, args: Record<string, EncodableValue>): Promise<InstantiateResult>\n  /** The registered templates, as a model sees them: what to name, what it does, what to fill. */\n  templates(): readonly { id: string; summary: string; params: DeclaredField[] }[]\n  /** Prose rendering — what an operator actually reads before approving. */\n  render: typeof renderPlan\n  /** The structured views, for diff-rendering UIs rather than for reading. */\n  raw: {\n    plan: typeof rawPlan\n    ops: typeof rawOps\n    diff: typeof rawDiff\n    outline: typeof planOutline\n  }\n  /**\n   * The forge. Tier A (`front`) withholds graph mechanics; tier B (`authoring`) exposes them.\n   * There is no tier C here — that is `invocable`, the consumer's own tools, which this battery\n   * gates rather than forges.\n   */\n  tools(tier: 'front' | 'authoring'): Record<string, Tool>\n  /**\n   * The backing store, exposed so a consumer can list, read history and clone without a second\n   * handle.\n   */\n  readonly store: PlanStore\n}\n\n/**\n * The Tier-C boundary: which of the consumer's tools a staged `call` may invoke, and what each\n * returns. Supplied by the consumer, because they are the only party that knows.\n */\nexport interface InvocableTools {\n  /** `true` if a staged `call` may invoke this tool unattended. The Tier-C boundary. */\n  has(tool: string): boolean\n  /** For a model-addressed refusal that names what IS available. */\n  names(): readonly string[]\n  /**\n   * What this tool returns, so freeze can validate a downstream `transform` against it. The\n   * consumer knows: an ADK `Tool` carries `artifactConstructor` — a `() => SpooledArtifact\n   * subclass` closure they already wrote (`src/lib/classes/tool.ts:103`) — so\n   * `{kind:'artifact', artifactClass: tool.artifactConstructor()}` is a declaration they can\n   * make correctly with no new bookkeeping. `undefined` for a tool the consumer has not\n   * declared: a `transform` over such a node is then refused at freeze (naming the tool), rather\n   * than the battery guessing a class it cannot know.\n   *\n   * **It carries the CLASS, not a descriptor array, and that is the fix for a real trap.** Core's\n   * `toolMethods` static SHADOWS rather than concatenates — `SpooledJsonArtifact.toolMethods` is\n   * its seven JSON descriptors only, with the base seven composed at the `forgeTools` layer\n   * instead (`spooled_artifact.ts:232-240`). A seam taking a descriptor array would therefore\n   * have been handed the leaf set by every consumer reading `.toolMethods`, and freeze would\n   * refuse `artifact_head` on a JSON artifact while the plan advertised it. Handing over the\n   * class moves the union into the battery, where `effectiveToolMethods` computes it once.\n   *\n   * This is also the channel the transform freeze-check needs, and an earlier draft claimed the\n   * check without providing any channel at all — no IR field and no seam exposed a tool's\n   * artifact class, so the \"refuse a step absent from the source class, naming the legal set\"\n   * rule was unbuildable.\n   */\n  returns(\n    tool: string\n  ):\n    | { kind: 'text' }\n    | { kind: 'bytes' }\n    | { kind: 'media' }\n    | { kind: 'artifact'; artifactClass: ArtifactClassLike }\n    | undefined\n}\n/**\n * What freeze validation needs that is NOT derivable from the plan itself.\n *\n * @remarks\n * Resolved the same way as `RunDeps`: construction supplies the defaults, a call may override\n * field by field, evaluators merge by cell id. `Orchestration.freezePlan` therefore takes\n * `Partial<FreezeInputs>` and `validation.ts`'s internal entry point takes this fully-resolved\n * shape — the resolution happens once, at the assembly point.\n */\nexport interface FreezeInputs {\n  /** The Tier-C allowlist, and the source of each tool's declared return class. */\n  invocable: InvocableTools\n  /** The wired cells. A `branch`/`select` naming an absent cell is refused here. */\n  evaluators: PredicateEvaluator[]\n}\n// Resolved the same way as RunDeps: construction supplies the defaults, a call may override\n// field by field, evaluators merge by cell id. `Orchestration.freezePlan` therefore takes\n// `Partial<FreezeInputs>`, and `validation.ts`'s internal entry point takes the fully-resolved\n// `FreezeInputs` — the resolution happens once, at the assembly point (WP 12).\n\n/**\n * A clone's lineage. `completedAtClone` is the part that is not derivable later: the renderer must\n * warn that \"the parent already completed X, Y, Z, and approving this repeats them\", and the\n * parent's id/digest/revision identify CONTENT, not execution — a plan at that revision may never\n * have run, may be halted, or may have completed a subset. So `clonePlan` snapshots the parent's\n * completed node ids at clone time, which also makes the warning stable if the parent's run is\n * later re-read or the parent is archived.\n */\nexport type PlanProvenance =\n  | ({ kind: 'clone' } & ClonedFrom)\n  | ({ kind: 'template' } & InstantiatedFrom)\n\n/** A clone's lineage. */\nexport interface ClonedFrom {\n  /** The plan cloned from. */\n  parent: PlanId\n  /** The parent's content digest at clone time. */\n  parentDigest: string\n  /** The parent's revision at clone time. */\n  parentRevision: number\n  /** Node ids the parent had settled `ok` when the clone was taken; `[]` if it never ran. */\n  completedAtClone: NodeId[]\n}\n\n/**\n * Instantiation lineage, for the renderer and for audit. **Not a taint mechanism** — see below.\n */\nexport interface InstantiatedFrom {\n  /** The template's id. */\n  template: string\n  /** The arguments substituted for its holes. */\n  args: Record<string, EncodableValue>\n}\n\n// WHERE TEMPLATE TAINT IS ACTUALLY ENFORCED — and why not here.\n//\n// An earlier draft carried `taintedPaths: {nodeId, path}[]` recording where each hole landed, and\n// had freeze treat those paths as entry-derived. That cannot work, and its own specified test\n// proved it: the test required an IDENTICAL literal authored directly to be accepted, which means\n// after instantiation an ordinary `set_node_field` can copy the substituted value into a `call`\n// arg and it is indistinguishable from the accepted one. Paths also go stale under the free\n// mutation `editable` guarantees, and `clonePlan` replaces provenance wholesale so a clone lost\n// the origin entirely. A per-value taint marker is no better: a literal is a literal.\n//\n// The check belongs where the thing being checked is IMMUTABLE — the template itself, which is\n// code-defined and validated at construction:\n//\n//   · At CONSTRUCTION, for each registered template: a `ParamRef` reaching a `call` node's `args`\n//     is refused unless a node on every route to it declares the corresponding field in\n//     `declassifies`. Static, total, and decidable over a fixed graph — the template cannot\n//     change afterwards, so the answer cannot go stale.\n//   · At INSTANTIATION nothing further is needed: a template that could not launder its params\n//     cannot produce a plan that does.\n//   · AFTER instantiation the result is an ordinary `editable` plan, and a subsequent edit that\n//     routes a literal into a `call` arg is exactly as scrutinised as any authored plan — which\n//     is to say: it is in the operator's rendered prose, and the operator approves it. That is the\n//     honest boundary, and it is the same one every hand-authored plan already has.\n//\n// So the invariant the plan now claims is narrower and true: A TEMPLATE CANNOT LAUNDER ITS OWN\n// PARAMETERS. It does not claim that a value's template origin is tracked through arbitrary later\n// edits, because nothing in a freely-mutable graph can track that.\n\n/**\n * The folded plan CONTENT at a revision. `rawPlan()` returns this; the renderer and validator\n * read it. Note there is deliberately no `state` field: lifecycle state is not a `PlanOp`, so it\n * cannot be folded from the log, and a historical revision therefore has no recoverable\n * lifecycle state to report. `state` is a property of the plan NOW — read it from\n * `PlanStore.readState()`, which is where it lives. A `RawPlanView` at revision 7 answers \"what\n * did the content look like then\", not \"what state was it in then\".\n */\nexport interface RawPlanView {\n  /** The plan. */\n  planId: PlanId\n  /**\n   * The content digest at this revision. Lossless, not a canonicalisation — see the digest rules.\n   */\n  digest: string\n  /** Which revision this view folded to. */\n  revision: number\n  /** The nodes at that revision. */\n  nodes: PlanNode[]\n  /** The edges at that revision. */\n  edges: PlanEdge[]\n  /**\n   * The bounds at that revision, seeded from `DEFAULT_PLAN_BOUNDS` and overridden by `set_bounds`.\n   */\n  bounds: PlanBounds\n  /** Where the plan came from, when it was not authored from scratch. */\n  provenance?: PlanProvenance\n}\n\n/** A plan's headline, for listings. Carries `state`, which `RawPlanView` deliberately does not. */\nexport interface PlanSummary {\n  /** The plan. */\n  planId: PlanId\n  /** Its lifecycle state NOW. */\n  state: PlanState\n  /** The current content digest. */\n  digest: string\n  /** The current revision. */\n  revision: number\n  /** How many nodes it holds. */\n  nodeCount: number\n  /** An author-supplied label, when one was set. */\n  label?: string\n  /** Where the plan came from, when it was not authored from scratch. */\n  provenance?: PlanProvenance\n  /** When the plan last changed, ISO-8601. */\n  updatedAt: string\n}\n\n/** One finding from validation. Model-addressed, because a model is what usually acts on it. */\nexport interface PlanIssue {\n  /** Stable code, e.g. `'missing_authority'`, `'dangling_edge'`. Safe to branch on. */\n  code: string\n  /** Model-addressed prose that NAMES THE FIX, not merely the fault. */\n  message: string\n  /** The node at fault, where one is implicated. */\n  nodeId?: NodeId\n  /** The edge at fault, where one is implicated. */\n  edgeId?: string\n  /** `blocking` refuses the freeze; `advisory` is surfaced and allowed through. */\n  severity: 'blocking' | 'advisory'\n}\n\n/** Structural delta between two folded states — what a diff UI renders. */\nexport interface PlanDiff {\n  /** The revision compared from. */\n  from: { revision: number; digest: string }\n  /** The revision compared to. */\n  to: { revision: number; digest: string }\n  /** Nodes present in `to` and absent from `from`. */\n  nodesAdded: PlanNode[]\n  /** Nodes present in `from` and absent from `to`. */\n  nodesRemoved: PlanNode[]\n  /** `ArgValue`, not `EncodableValue` — a changed field may hold a `NodeRef`, and typing it\n   *  narrower would make a legitimate change unrepresentable in the diff (the `set_node_field`\n   *  correction has to propagate here too). */\n  nodesChanged: { nodeId: NodeId; fields: { path: string; before: ArgValue; after: ArgValue }[] }[]\n  /** Edges present in `to` and absent from `from`. */\n  edgesAdded: PlanEdge[]\n  /** Edges present in `from` and absent from `to`. */\n  edgesRemoved: PlanEdge[]\n}\n\n/**\n * An operator's decision, bound to exactly the content they were shown. The digest is lossless, so\n * two plans that differ in any staged value cannot share one — which is what stops an approval\n * authorising a plan that was never rendered.\n */\nexport interface ApprovalRecord {\n  /** The plan approved. */\n  planId: PlanId\n  /** The content digest approved. A later revision does not inherit this approval. */\n  digest: string\n  /** Canonicalised: deduped and lexicographically sorted, so set equality is a byte comparison. */\n  authoritySet: AuthorityClaim[]\n  /** Who decided. */\n  decidedBy: string\n  /** When, ISO-8601. */\n  decidedAt: string\n  /** A denial is not recorded as an approval; it is ABSENT. So this has exactly one inhabitant. */\n  disposition: 'approved'\n}\n\n/** Why a run stopped short of completing. Closed, so a caller can handle every case it must. */\nexport type InterruptionCause =\n  | { kind: 'turn_abort' }\n  | { kind: 'operator_stop' }\n  | { kind: 'gate_timeout' }\n  | { kind: 'process_death' }\n  /**\n   * The run hit `maxSteps` — its own declared settlement budget — and stopped.\n   *\n   * @remarks\n   * Distinct from `process_death` on purpose. Process death is NEVER inferred (see `foldRun`); it\n   * reaches the log only when a resuming caller records it. Budget exhaustion is the opposite: the\n   * executor knows exactly why it stopped, and reporting it as a death both lies about the cause\n   * and tells an operator to look for a crash that never happened. It is also NOT a cycle — freeze\n   * proves the graph acyclic, so a plan can exhaust a budget through legitimate fan-out.\n   *\n   * `settled` is the count reached, so a caller can decide whether to raise `maxSteps` and clone,\n   * or accept that the plan is too large for its bound.\n   */\n  | { kind: 'budget_exhausted'; settled: number }\n  | { kind: 'deviation_abort'; detail: string }\n  | { kind: 'node_failed'; nodeId: NodeId; handled: false }\n  | { kind: 'predicate_unevaluatable'; nodeId: NodeId }\n  /** A join can no longer be satisfied: no live frame can still reach it. */\n  | { kind: 'join_unsatisfiable'; nodeId: NodeId }\n  | { kind: 'output_schema_violation'; nodeId: NodeId }\n  | { kind: 'authority_revoked'; claim: AuthorityClaim }\n\n/** What `foldRun` returns: the whole answer to \"where did it stop and what happened\". */\nexport interface RunProjection {\n  /** The run. Folded from `run_started`, so it needs no side channel. */\n  runId: string\n  /** The plan digest the run started against. */\n  digest: string\n  /**\n   * Keyed by FRAME, not by NodeId: a node may run on several branches and several runs, so one\n   * node can simultaneously have a settled frame and an entered-but-unsettled one. The key is\n   * `${nodeId}:${branchKey(branchId)}`. `nodeStatusById` is the convenience rollup — a node is\n   * `running` if any frame is, `failed` if any frame failed, `done` only if every frame settled.\n   */\n  frameStatus: ReadonlyMap<string, 'running' | 'done' | 'failed' | 'skipped'>\n  /**\n   * The per-node rollup: `running` if any frame is, `failed` if any failed, `done` only if every\n   * frame settled.\n   */\n  nodeStatusById: ReadonlyMap<NodeId, 'pending' | 'running' | 'done' | 'failed' | 'skipped'>\n  /** Every `ok` settlement's output, run-wide. Plain data, unlike the artifact tables. */\n  outputs: OutputTable\n  /**\n   * DELIBERATELY no run-wide `artifacts` counterpart. `outputs` is run-wide because a\n   * `RunProjection` is the audit answer to \"what did each node produce\", and its values are plain\n   * data. Artifact instances are *branch-local execution state*, not results: they are read by the\n   * `transform` running on the branch that produced them, and they live on `PendingFrame.artifacts`\n   * and `JoinState.arrivals[].artifacts`, which the `frontier` already carries. A run-wide table\n   * would additionally force every artifact of every completed branch to be rebound on every fold\n   * — resolver calls, and for in-memory readers base64 bytes — to answer a question nothing asks.\n   */\n  frontier: { frames: PendingFrame[]; joins: JoinState[] }\n  /** Entered without settling AND of kind `call` — the indeterminate set, exactly. Other kinds\n   *  are re-entered unconditionally (see the commit protocol), so they never appear here. */\n  indeterminate: FrameRef[]\n  /** Every edge that fired, with whatever evidence the predicate recorded. */\n  edgesTaken: { edgeId: string; handle: EdgeHandle; evidence?: EncodableValue }[]\n  /**\n   * Where the run stands. `running` is what a fold reports for a log with no `run_settled` — it\n   * does NOT assert the process is alive, because a dead process's log is byte-identical to a live\n   * in-flight one. The difference is liveness, not history, and `foldRun` reads only history.\n   */\n  outcome: 'running' | 'completed' | 'halted' | 'aborted'\n  /** Why it stopped, when it stopped short. */\n  interruption?: InterruptionCause\n}\n"],"mappings":";;;;;;;;;;;AA6oCA,IAAa,sBAAkC;CAC7C,UAAU;CACV,UAAU;CACV,UAAU;CACV,qBAAqB;CACrB,iBAAiB;AACnB"}