Pipeline builder dispatch kinds =============================== .. note:: This page is generated by ``scripts/generate_dispatch_reference.py`` from :data:`phenotypic.gui.builder._callbacks.DispatchKind` plus a hand-curated mapping that mirrors spec §5.6 verbatim. Run the script after touching either to regenerate; the ``--check`` flag is wired into CI to catch drift. Every state-mutating event in the pipeline builder funnels through a single fan-in callback that dispatches via :func:`phenotypic.gui.builder._callbacks._dispatch_state_update`. Each ``kind`` value is one of the literals enumerated below; the payload schemas come straight from spec §5.6 of the *Builder canvas — DAG redesign* design document. Dispatch flow per mutation: 1. **Pre-mutation:** the dispatcher runs ``_seed_input_image`` on every reachable scope (idempotent guard for validation Rule 6). 2. **Mutation:** the named ``kind`` branch applies to a deep-copied state dict. 3. **Post-mutation:** the fan-in callback re-runs :func:`phenotypic.gui.builder._validation.validate`; the resulting list lands in ``STORE_ISSUES``, drives the toolbar count badge, and gates Run preview / Save pipeline (see :doc:`builder_validation`). The kinds are documented in declaration order so the literal alias and this page never diverge. Legacy linear builder --------------------- ``add_node`` ~~~~~~~~~~~~ **Payload schema:** ``{class_name: str}`` Append a fresh ``StepNode`` for ``class_name`` to the current legacy scope. Selects the new node. ``add_pipeline`` ~~~~~~~~~~~~~~~~ **Payload schema:** ``{}`` Append a ``StepNode`` carrying an empty nested scope. ``select_node`` ~~~~~~~~~~~~~~~ **Payload schema:** ``{node_id: str}`` Set ``selected_node_id``. ``delete_node`` ~~~~~~~~~~~~~~~ **Payload schema:** ``{}`` Remove ``selected_node_id`` from the current scope. ``drill_in`` ~~~~~~~~~~~~ **Payload schema:** ``{}`` Push ``selected_node_id`` onto the breadcrumb (only if that node has a nested scope). ``drill_out`` ~~~~~~~~~~~~~ **Payload schema:** ``{}`` Pop the breadcrumb tail. ``breadcrumb_to`` ~~~~~~~~~~~~~~~~~ **Payload schema:** ``{depth: int}`` Truncate breadcrumb to ``depth`` entries. ``reorder`` ~~~~~~~~~~~ **Payload schema:** ``{order: list[str]}`` Reorder the current scope's nodes by node-id sequence. ``edit_param`` ~~~~~~~~~~~~~~ **Payload schema:** ``{node_id: str, name: str, value: Any, omit: bool}`` Set or delete ``params[name]`` on a specific node. ``edit_label`` ~~~~~~~~~~~~~~ **Payload schema:** ``{node_id: str, label: str}`` Update the node label. ``port_slot_add`` ~~~~~~~~~~~~~~~~~ **Payload schema:** ``{node_id: str, param: str}`` Append a ``None`` slot to the consumer's ``aux_ports`` list for a list-typed param. No-op for scalar ports. ``port_slot_remove`` ~~~~~~~~~~~~~~~~~~~~ **Payload schema:** ``{node_id: str, param: str, slot: int}`` Remove the slot at ``slot`` from the consumer's ``aux_ports`` list and reindex remaining slots. DAG canvas: palette drag-and-drop --------------------------------- ``block_create`` ~~~~~~~~~~~~~~~~ **Payload schema:** ``{class_name: str, x: float, y: float, container_block_id: str | None, ts: int}`` Append a fresh :class:`BlockNode` to the root scope (when ``container_block_id`` is ``None``) or to a container's nested scope (DFS lookup, innermost-wins hit-test per spec §4.4). Rejects ``class_name == "InputImage"`` with a toast — Input Image is auto-seeded per scope and cannot be created from the palette (spec §4.8 + §4.10). Drop coordinates ``(x, y)`` are **not** persisted; the leaf-first dagre pass re-lays the canvas on the next render (spec §4.7). DAG canvas: wire drawing ------------------------ ``edge_create`` ~~~~~~~~~~~~~~~ **Payload schema:** ``{source_block_id: str, target_block_id: str, target_port: str, edge_kind: "image" | "aux", ts: int}`` Mint an :class:`Edge` between two ports inside the same scope. For scalar aux + image-flow: replaces any existing wire from ``source_block_id`` (single-wire rule, spec §4.2 / §4.3). For list aux: **server-side append** — the dispatcher resolves ``target_slot = block.list_slot_counts.get(target_port, 0)`` and increments ``list_slot_counts[target_port]`` by 1 (eliminates the concurrent-drag race; the client emits no slot index). Cross-scope wires are rejected with a toast (spec §4.4). ``edge_delete`` ~~~~~~~~~~~~~~~ **Payload schema:** ``{edge_id: str, ts: int}`` Remove a single edge by ``edge_id`` (DFS across every scope). For list-aux edges, remaining edges' ``target_slot`` values are NOT renumbered — the freed slot becomes an empty placeholder. Use ``list_aux_reorder`` to compact. DAG canvas: list-aux fan-in --------------------------- ``list_aux_reorder`` ~~~~~~~~~~~~~~~~~~~~ **Payload schema:** ``{block_id: str, param: str, new_order: list[str | None], ts: int}`` Update the canonical execution order of a list-typed aux port. ``new_order`` is a permutation of the wired edge_ids interspersed with ``None`` placeholders for empty slots; the dispatcher rebuilds each edge's ``target_slot`` from its position in the new order and updates ``block.list_slot_counts[param] = len(new_order)``. Non-permutation inputs are rejected with a toast (no-op). ``list_aux_add_empty_slot`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ **Payload schema:** ``{block_id: str, param: str, ts: int}`` Increment ``block.list_slot_counts[param]`` by 1. No edge is materialised — empty slots live solely on the consumer block. At ``to_pipeline_dag`` time, slot indices in ``[0, count)`` not covered by an edge emit ``None`` entries. DAG canvas: selection --------------------- ``wire_select`` ~~~~~~~~~~~~~~~ **Payload schema:** ``{edge_id: str | None, ts: int}`` Set ``selected_edge_id``. ``None`` deselects. Setting a new id clears ``selected_block_id`` (mutual exclusion, spec §4.5). ``block_select`` ~~~~~~~~~~~~~~~~ **Payload schema:** ``{block_id: str | None, ts: int}`` Set ``selected_block_id``. ``None`` deselects. Setting a new id clears ``selected_edge_id`` (mutual exclusion, spec §4.5). DAG canvas: Pipeline containers ------------------------------- ``block_reparent`` ~~~~~~~~~~~~~~~~~~ **Payload schema:** ``{block_id: str, new_parent_block_id: str | None, x: float, y: float, ts: int}`` Move a block between scopes. ``new_parent_block_id == None`` promotes the block to the current scope; a non-None value adopts it into that container's nested scope (sibling-container moves are a single atomic dispatch). Rejects ``InputImage`` block_ids (the source must remain in its scope). Drag-out direction (new parent is an ancestor) with orphan edges → snap-back + toast; drag-in / sibling direction with orphan edges → drop the incompatible edges + toast the count, then commit the move (spec §4.4). ``block_collapsed_toggle`` ~~~~~~~~~~~~~~~~~~~~~~~~~~ **Payload schema:** ``{block_id: str, ts: int}`` Toggle ``block.collapsed`` for a Pipeline container. No-op on non-container blocks (anything whose ``class_name != "ImagePipeline"``). The visual expand/collapse state is per-block (per spec §4.4). ``drill_into_container`` ~~~~~~~~~~~~~~~~~~~~~~~~ **Payload schema:** ``{block_id: str, ts: int}`` Push ``block_id`` onto ``state.breadcrumb``. Validated: ``block_id`` must resolve to a Pipeline container at the current breadcrumb depth (siblings at other depths are rejected silently). ``drill_to_scope`` ~~~~~~~~~~~~~~~~~~ **Payload schema:** ``{target_breadcrumb: list[str], ts: int}`` Atomic breadcrumb replacement — replaces ``state.breadcrumb`` with ``target_breadcrumb`` in one dispatch. Each block_id in ``target_breadcrumb`` must resolve to a real Pipeline container at the right depth in ``state.root``; stale ids → reject + toast. Used by ``scroll_to`` for cross-scope navigation. ``block_delete_request`` ~~~~~~~~~~~~~~~~~~~~~~~~ **Payload schema:** ``{block_id: str, ts: int}`` First stage of the two-stage container delete. Rejects ``InputImage`` block_ids (defense in depth). Non-container OR *empty* container (only the auto-seeded ``InputImage`` sentinel inside) → delegates to ``block_delete_confirm`` immediately. Non-empty container → sets ``state.pending_delete_block_id = block_id`` which opens the confirm-delete modal (body: "Delete container