/** * Submit-time validation and the lifecycle machine. * * @module @nhtio/adk/batteries/orchestration/validation * * @remarks * This module owns the freeze gate: it folds a plan's op log into a `RawPlanView`, runs every * submit check over that folded graph, and — only on a clean pass — commits the * `editable → reviewable` transition. It is the "the battery validates, the store commits" split * made concrete: every check here is battery policy a bring-your-own store has no business * reimplementing, and the store's `transition` is the only boundary that can atomically move the * lifecycle state. * * The two exported functions are the whole surface: * * - {@link collectIssues} — the pure-ish validator. Given a folded view and the injected * `FreezeInputs` (the tier-C allowlist and the wired predicate cells), it returns every * `PlanIssue` the graph raises. It never throws on a well-typed-but-invalid plan; a malformed * definition surfaces as an issue, not a crash. * - {@link freezePlan} — the lifecycle entry point. It folds the log, runs {@link collectIssues}, * and only when no issue is `blocking` calls `store.transition(editable → reviewable, * {expectedDigest})`. The digest is what makes the commit safe rather than racy: content is * validated at digest D and the store commits only if the plan is still at D, so a concurrent * edit invalidates the transition instead of slipping past an already-passed check. * * Every refusal is a `PlanIssue` with a stable `code`, a model-addressed `message` naming the fix, * the `nodeId`/`edgeId` where applicable, and a `severity`. Blocking issues refuse the freeze; * advisory issues are surfaced for the author but do not stop the transition. * * The checks are grouped into three families, each documented at its call site: * topology (entry, reachability, acyclicity, the diamond-join rule, id and handle rules), * references and dataflow (dangling refs, undeclared fields, join-crossing selections, ambiguous * references, taint), and per-node shape (call, transform, branch/select, encodability, scaffold * placeholders, unreachable calls). */ import type { PlanStore } from "./store"; import type { FreezeInputs, PlanIssue, RawPlanView } from "./types"; /** * Run every submit check over a folded plan view. * * The checks are grouped into three families: * * **Topology** — exactly one `entry` with no incoming edges; every other node reachable from it; * acyclicity over every handle (a diamond fan-in is not a cycle); every `join` a diamond (its fork * is its immediate dominator, more than one fork→join route, no reconvergence, no nested join); * edge ids valid and unique; node ids valid; handle applicability per source kind; a `select` * must carry a `default` edge. * * **References and dataflow** — a `NodeRef` naming a missing node or an undeclared field; a * `first`/`last` selection resolved across a `join`; an omitted `branchId` where more than one path * reaches the referenced node; taint (a tainted reference may reach a `reason` prompt but not a * `call` node's args, with declassification only via a `call` node's `declassifies`); staged * values outside the encodable subset and cyclic values inside it. * * **Per-node shape** — a `call` naming a tool outside the allowlist, with `replaySafe`/ * `onIndeterminate` unset, or the retry-unsafe-repeat contradiction; a `branch`/`select` with no * wired evaluator cell (and `load()`/`validate()` on every wired cell); a `transform` naming a * step absent from the source class's effective method set, with args failing the descriptor's * schema, or whose source tool's return class is undeclared; a `Media`/`Uint8Array`-returning tool * feeding a field-declaring node; an unedited scaffold placeholder; an unreachable `call` node. * * This function never throws on a well-typed-but-invalid plan. A malformed definition surfaces as * an issue rather than a crash, and every evaluator interaction is wrapped so a failing cell * reports an issue instead of propagating. * * @param view - The folded plan content to validate. * @param inputs - The injected tier-C allowlist and wired predicate cells. * @returns Every issue the graph raises, blocking and advisory. */ export declare function collectIssues(view: RawPlanView, inputs: FreezeInputs): Promise; /** * Freeze a plan: fold the log, validate, and commit the `editable → reviewable` transition. * * The op log is folded into a `RawPlanView`, the fold's own issues are carried over (minus the * fold's advisory `duplicate_edge_id`, which {@link collectIssues} reports as a blocking check), * and {@link collectIssues} runs over the folded graph. Only when no issue is `blocking` is the * store's `transition` called, passing the folded digest as `expectedDigest`. * * The digest is what makes the commit safe rather than racy: content is validated at digest D and * the store commits only if the plan is still at D, so a concurrent edit invalidates the * transition instead of slipping past an already-passed check. If the transition is rejected * (the digest moved or the state is not `editable`), a `transition_rejected` blocking issue is * appended and the freeze reports failure. * * @param store - The plan store holding the plan. * @param planId - Identity of the plan to freeze. * @param inputs - The fully-resolved tier-C allowlist and wired predicate cells. * @returns Whether the freeze succeeded, and every issue the plan raised. */ export declare function freezePlan(store: PlanStore, planId: string, inputs: FreezeInputs): Promise<{ ok: boolean; issues: PlanIssue[]; }>;