Pipeline builder dispatch kinds#
Note
This page is generated by scripts/generate_dispatch_reference.py
from
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
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:
Pre-mutation: the dispatcher runs
_seed_input_imageon every reachable scope (idempotent guard for validation Rule 6).Mutation: the named
kindbranch applies to a deep-copied state dict.Post-mutation: the fan-in callback re-runs
phenotypic.gui.builder._validation.validate(); the resulting list lands inSTORE_ISSUES, drives the toolbar count badge, and gates Run preview / Save pipeline (see Pipeline builder validation rules).
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.
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 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 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 <label> and its N inner block(s)?” where N excludes InputImage).
block_delete_confirm#
Payload schema: {block_id: str, ts: int}
Second stage (or single stage if no confirmation was needed). Atomically removes the block, its nested scope (if any) including every inner block + edge, and every edge in the containing scope whose source or target is the block. Clears selected_block_id / selected_edge_id / pending_delete_block_id when they pointed at the deleted block or one of its edges.