Pipeline builder validation rules ================================= .. note:: This page is generated by ``scripts/generate_validation_reference.py`` from :data:`phenotypic.gui.builder._validation.IssueKind` plus a hand-curated rule table that mirrors spec §4.6 verbatim. Run the script after touching either to regenerate; the ``--check`` flag is wired into CI to catch drift. The DAG builder's validation surface is a pure function :func:`phenotypic.gui.builder._validation.validate` that walks every scope reachable from ``state.root`` and emits a flat list of :class:`~phenotypic.gui.builder._validation.Issue` records. Six **blocking** rules (severity ``"error"``) disable ``Run preview`` and ``Save pipeline``; the advisory hints (severity ``"advisory"``) decorate the canvas with yellow borders but never block. Rules emit in deterministic order: 1. ``missing_input`` / ``duplicate_input`` (Rule 6) 2. ``fork`` (Rule 1) 3. ``stub`` (Rule 2) 4. ``required_aux`` / ``unknown_class`` (Rule 3) 5. ``cycle`` (Rule 4) 6. ``container_mode`` (Rule 5) 7. ``stage_order_hint`` (Rule 7, advisory) Nested-scope issues are appended after the parent scope's issues so snapshot-style tests stay stable. Summary table ------------- .. list-table:: :header-rows: 1 :widths: 5 12 10 35 * - Rule - Kind - Severity - Title * - 1 - ``fork`` - error - Image-flow ports have at most one wire * - 2 - ``stub`` - error - All blocks reachable from Input Image * - 3 - ``required_aux`` - error - Required aux ports must be wired * - 4 - ``cycle`` - error - No cycles in the edge graph * - 5 - ``container_mode`` - error - Container left/right wiring consistency * - 6 - ``missing_input``, ``duplicate_input`` - error - Exactly one Input Image per scope * - 7 - ``stage_order_hint`` - advisory - Stage ordering respects ops → meas → post * - — - ``unknown_class`` - advisory - Class not in the operation registry Per-rule reference ------------------ Rule 1 — Image-flow ports have at most one wire ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Issue kind(s):** ``fork`` **Severity:** error **Mechanic:** Per-port wire-count over ``edge.kind == "image"``, plus a total-fan-out check across image + aux (one outgoing wire from any source, total). **Offender:** The block whose output or input violates the rule. Three sub-cases all surface as ``kind="fork"``: a source with >1 outgoing image edge, an ``(target_block_id, "in")`` port with >1 incoming image edge, and a source with >1 outgoing wires *total* across image and aux. Rule 2 — All blocks reachable from Input Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Issue kind(s):** ``stub`` **Severity:** error **Mechanic:** BFS from the ``InputImage`` block across image-flow edges forward and aux edges in both directions. Any block not visited by the walk is flagged as a stub. **Offender:** Each unreachable block (rendered with a dashed red border). Extra ``InputImage`` blocks are excluded from the stub set so they're flagged once as ``duplicate_input`` instead of double-flagged. Rule 3 — Required aux ports must be wired ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Issue kind(s):** ``required_aux`` **Severity:** error **Mechanic:** For each block, walk the registry's ``OperationInfo.parameters``; for every op-typed parameter (``param.is_operation or param.is_pipeline``) without a default (``not param.has_default``), require at least one aux edge targeting ``(block_id, param_name)``. **Offender:** The consumer block. The empty required port renders with a red ring. Rule 4 — No cycles in the edge graph ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Issue kind(s):** ``cycle`` **Severity:** error **Mechanic:** Iterative Tarjan's strongly-connected-components over the combined edge graph (image + aux). Any block participating in a non-trivial SCC (size > 1 OR size 1 with a self-loop) is reported. **Offender:** Every block in the strongly-connected cycle (sorted lexicographically for deterministic test output). Rule 5 — Container left/right wiring consistency ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Issue kind(s):** ``container_mode`` **Severity:** error **Mechanic:** For each Pipeline container, evaluate whether the outer left image-input is wired and what kind of port the right output wires to. The two valid modes are *consumer-fed* (left wired, right wires to image) and *aux-fed* (left unwired, right wires to aux). Mixed modes are rejected. **Offender:** The container block whose wiring is inconsistent. Rule 6 — Exactly one Input Image per scope ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Issue kind(s):** ``missing_input``, ``duplicate_input`` **Severity:** error **Mechanic:** Count ``InputImage`` blocks in each scope. Zero → emit ``missing_input`` as a scope-level issue. Two or more → emit one ``duplicate_input`` issue per extra block. **Offender:** For ``missing_input``: reported as a scope-level issue (``block_id=None``); the dispatcher's auto-seed normally heals this on the next state-load pass. For ``duplicate_input``: the extra block(s). Rule 7 — Stage ordering respects ops → meas → post ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Issue kind(s):** ``stage_order_hint`` **Severity:** advisory **Mechanic:** Walk each image-flow edge; if the source block's stage (via ``_safe_stage(class_name)``) is later in the canonical order than the target's, emit a yellow-border advisory. The runtime partitions by ``isinstance`` so a misordered chain still works — this is a non-blocking nudge. **Offender:** The *source* block of the out-of-order edge (yellow border + "?" badge). Advisory — Class not in the operation registry ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Issue kind(s):** ``unknown_class`` **Severity:** advisory **Mechanic:** Registry lookup for each non-sentinel block's ``class_name``. A miss emits ``unknown_class`` as an advisory so the rule-3 walk can skip the block cleanly without raising. **Offender:** The block whose class is unknown to the registry (yellow border + "?" badge). Typically caused by registry drift (loading a ``pipeline.json`` saved by a newer build).