/** * Graph Model v2 — Structural Validator * * Phase 1, Subtask 2 (parsing & validation). * * Validates the STRUCTURE of a graph document only. By design there are no * node-type checks: the v2 model is role-agnostic (a node is {id, agent, * prompt}), so validation here never inspects agent/prompt semantics. It also * never checks whether an `agent` is a known dispatchable identifier — that is * an environment/binding concern, out of scope for structural validation. * * Checks (each produces an independent, human-readable message): * 1. `version` present and equal to 2 * 2. node-id uniqueness * 3. edge endpoint validity (from/to must reference declared nodes) * 4. cycle containment (Tarjan SCC-based — see below) * 5. loop-group node references must be declared nodes (+ loop-group id * uniqueness, symmetric to node-id uniqueness) * 6. `needs_approval` nodes may only have non-`always` outgoing edges * 7. `data_passthrough` shape (non-negative `max_chars`) * 8. loop-group root check — ERROR when, after excluding revise * back-edges, no node has in-degree zero AND at least one loop * group exists (pure-cycle deadlock). WARNING when no roots exist * but no loop groups are declared. Additionally flags loop groups * whose member nodes have no incoming edges from outside the group. * 9. `join.quorum` presence + bounds for quorum-strategy nodes — a * quorum-strategy declaration MUST carry its required-answer count * (absence is an error, never a silent default), the count must be a * positive integer, and it must not exceed the node's in-degree (the * join would be unsatisfiable). The upper-bound check is deferred * while a node has no incoming edges yet (incremental construction). * 10. per-node `budget.timeout_ms` / `budget.max_retries` bounds — * mirroring the `data_passthrough.max_chars` pattern (see rule 7): * negative `timeout_ms` and non-nonnegative-integer `max_retries` * are rejected. `timeout_ms: 0` is VALID (documented opt-out). * 11. `on_condition` edges must name a condition from the registered * condition vocabulary (the same `KNOWN_CONDITIONS` source * `asset_validate` uses) — an unknown name or a missing/empty * `condition` is rejected, so a never-satisfiable edge cannot * silently deadlock the graph at run. * * ## Cycle-containment semantics (check 4) * * The v2 model holds "the graph is a DAG at rest; cycles exist only inside * explicitly declared loop groups" (graph-model.md §4). We validate both * directions of that contract: * * (a) ERROR — every declared loop group must actually induce a directed * cycle over its declared nodes (via Tarjan on the induced subgraph). * Declaring a "loop" over an acyclic node set is a structural error. * * (b) WARNING/ERROR (mode-split) — every directed cycle in the full graph * (a Tarjan SCC with >1 node or a self-loop) must be covered by at * least one loop group. Each uncovered cyclic SCC is reported * independently (see the mode split below). * * ### Direction (b) severity split by mode * * `validateGraphDeclaration` takes an optional `mode`: * - `"construct"` (default) — incremental building. An uncovered cycle is * a WARNING: the builder may add the cycle-closing edge first and declare * the loop group afterward, so neither edge-first nor loop-first ordering * may fail. Fully backward-compatible for existing callers. * - `"execution"` — the graph is about to run. An uncovered cycle that * contains a revise back-edge (an `on_signal` edge with * `signal_filter: [revise_needed]`) stays a WARNING: that is exactly the * canonical dag-yaml-schema.md Appendix B pattern, where a revise * back-edge pulls a node (e.g. `final-gate`) into a loop group's SCC * without declaring it in any loop group. Failing that document as an * ERROR would contradict the documented canonical graph; surfacing it as * a warning preserves the diagnostic while keeping the canonical graph * valid. An uncovered cycle with NO revise back-edge (pure `always`-edge * cycles, self-loops, non-revise signal/condition cycles) is promoted to * an ERROR: no edge within the SCC can ever be excluded from root * discovery (`checkLoopGroupRoots`), so the cycle can never activate and * the graph deadlocks at runtime. * * Reuses the Tarjan SCC approach from ./loop-detector.ts (which operates on v1 * FlowEdge) by way of a self-contained v2 EdgeDeclaration adaptation. * * Design reference: .rolebox/design/dag-yaml-schema.md §5, graph-model.md §4. */ import type { EdgeDeclaration } from "../types.graph-v2.ts"; import type { GraphDocument } from "./parser-v2.ts"; /** Result of structural validation — errors are fatal, warnings are not. */ export interface GraphValidationResult { /** `false` when at least one fatal structural error was found. */ valid: boolean; /** Fatal structural errors (each describes exactly one failing rule). */ errors: string[]; /** Non-fatal diagnostics (e.g. an uncovered cycle). */ warnings: string[]; } /** * Validate the structure of a graph document. Never throws. * * @param graph - a parsed graph document (see parser-v2.ts `parseGraph`). * @param opts - optional validation context. * @param opts.mode - severity context: `"construct"` (default) treats * uncovered cycles as warnings so incremental building (add the * cycle-closing edge, then declare the loop group) keeps working; * `"execution"` promotes uncovered cycles with no revise back-edge to * errors, since such a cycle can never activate and deadlocks at runtime. * @returns a `GraphValidationResult` with independent messages per rule. */ export declare function validateGraphDeclaration(graph: GraphDocument, opts?: { mode?: "construct" | "execution"; }): GraphValidationResult; /** True when the given edge set contains at least one directed cycle. */ export declare function hasCycle(edges: EdgeDeclaration[]): boolean; //# sourceMappingURL=validator-v2.d.ts.map