Files
ComfyUI-Dataset-Gates/docs/plans/2026-06-21-multi-reroute-implementation.md
Ethanfel ce371ffe13 Add Multi-Reroute (Rail) design + implementation plan
Multi-lane any-type pass-through node ("rail"): in_i -> out_i, empty lane
-> ExecutionBlocker; +/- to add/remove lanes (bottom in P1, top with
wiring-preserving remap in P2). Pure build_outputs + shared AnyType; lazy
comfy import keeps it unit-testable. No IS_CHANGED (transparent passthrough).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 23:27:05 +02:00

238 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Multi-Reroute (Rail) Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** A `MultiReroute` node — N parallel any-type pass-through lanes ("rail") with +/ to add/remove lanes — so wire bundles stay tidy without many separate Reroute nodes.
**Architecture:** A real pass-through node with `AnyType("*")` lanes: `in_i → out_i`, empty lane → `ExecutionBlocker`. Pure `build_outputs` is unit-tested; `ExecutionBlocker` is imported lazily. The frontend manages dynamic lane slots and persists the lane count. Bottom add/remove is Phase 1 (wiring-safe); top add/remove (link-preserving) is Phase 2.
**Tech Stack:** Python 3.12 (stdlib), pytest 9; vanilla JS frontend.
---
## Conventions (read once)
- **Test python:** `/media/p5/miniforge3/bin/python` (`PY=...`).
- **Run tests:** `cd /media/p5/ComfyUI-Datasete-Gates && $PY -m pytest tests/test_anytype.py tests/test_reroute.py -v`
- `gates/anytype.py` and `gates/reroute_node.py` import-safe without comfy (lazy
`ExecutionBlocker` inside `run`).
- `__init__.py` edit is **additive** — re-Read first, extend the mappings.
- `MAX_LANES = 32`, default visible lanes = 4.
- Commit style: Conventional Commits + repo Co-Authored-By; stage only this node's paths.
---
### Task 1: `anytype.py` — shared wildcard
**Files:** Create `gates/anytype.py`; Test `tests/test_anytype.py`
**Step 1: Failing test**
```python
# tests/test_anytype.py
from gates import anytype
def test_any_equals_everything():
assert (anytype.ANY != "IMAGE") is False
assert (anytype.ANY != "LATENT") is False
assert isinstance(anytype.ANY, str)
```
**Step 2: Run → FAIL.**
**Step 3: Implement**
```python
# gates/anytype.py
"""Shared ComfyUI wildcard type."""
class AnyType(str):
def __ne__(self, other):
return False
ANY = AnyType("*")
```
**Step 4: Run → PASS.** **Step 5: Commit** `feat: shared AnyType wildcard`
---
### Task 2: `reroute_node.py` — `build_outputs`
**Files:** Create `gates/reroute_node.py`; Test `tests/test_reroute.py`
**Step 1: Failing test**
```python
# tests/test_reroute.py
from gates import reroute_node as rr
def test_build_outputs_forwards_and_blocks():
B = object() # blocker sentinel
vals = {"in_1": "A", "in_3": "C"}
out = rr.build_outputs(vals, max_lanes=4, blocker=B)
assert out == ("A", B, "C", B)
def test_build_outputs_length():
B = object()
assert len(rr.build_outputs({}, max_lanes=rr.MAX_LANES, blocker=B)) == rr.MAX_LANES
```
**Step 2: Run → FAIL.**
**Step 3: Implement**
```python
# gates/reroute_node.py
from .anytype import ANY
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
MAX_LANES = 32
def build_outputs(values, max_lanes, blocker):
out = []
for i in range(max_lanes):
v = values.get(f"in_{i + 1}")
out.append(v if v is not None else blocker)
return tuple(out)
```
**Step 4: Run → PASS.** **Step 5: Commit** `feat: multi-reroute build_outputs`
---
### Task 3: `reroute_node.py` — `MultiReroute` node
**Files:** Modify `gates/reroute_node.py`, `tests/test_reroute.py`
**Step 1: Failing test**
```python
def test_io_shape():
assert len(rr.MultiReroute.RETURN_TYPES) == rr.MAX_LANES
assert all(t == "*" for t in rr.MultiReroute.RETURN_TYPES)
assert rr.MultiReroute.RETURN_NAMES[0] == "out_1"
it = rr.MultiReroute.INPUT_TYPES()
assert "in_1" in it["optional"] and f"in_{rr.MAX_LANES}" in it["optional"]
```
**Step 2: Run → FAIL.**
**Step 3: Implement (append)**
```python
class MultiReroute:
CATEGORY = "Datasete Gates"
FUNCTION = "run"
RETURN_TYPES = (ANY,) * MAX_LANES
RETURN_NAMES = tuple(f"out_{i + 1}" for i in range(MAX_LANES))
@classmethod
def INPUT_TYPES(cls):
return {"optional": {f"in_{i + 1}": (ANY,) for i in range(MAX_LANES)}}
def run(self, **kwargs):
from comfy_execution.graph_utils import ExecutionBlocker
return build_outputs(kwargs, MAX_LANES, ExecutionBlocker(None))
NODE_CLASS_MAPPINGS = {"MultiReroute": MultiReroute}
NODE_DISPLAY_NAME_MAPPINGS = {"MultiReroute": "Multi Reroute (Rail)"}
```
> `RETURN_TYPES` entries are the `AnyType` instance (`== "*"`), so the test's `t == "*"`
> holds. No `IS_CHANGED` (transparent/cacheable passthrough).
**Step 4: Run → PASS.** **Step 5: Commit** `feat: MultiReroute node (any-type pass-through lanes)`
---
### Task 4: Register in `__init__.py` (MERGE)
**Files:** Modify `__init__.py`
**Step 1:** Re-Read `__init__.py`; add inside `if __package__:`:
```python
from .gates.reroute_node import NODE_CLASS_MAPPINGS as _RR_NODES, \
NODE_DISPLAY_NAME_MAPPINGS as _RR_NAMES
```
and merge into the final dicts:
```python
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **_RR_NODES}
NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **_RR_NAMES}
```
**Step 2:** `$PY -c "import gates.reroute_node; print(gates.reroute_node.NODE_CLASS_MAPPINGS)"`.
**Step 3:** Full suite green: `$PY -m pytest tests/ -v`.
**Step 4: Commit** `feat: register MultiReroute`
---
### Task 5: `web/multi_reroute.js` — dynamic lanes + bottom +/ (Phase 1)
**Files:** Create `web/multi_reroute.js`
`app.registerExtension` for `MultiReroute`:
- On `nodeCreated`: ensure the node shows `lanes` lane pairs (default 4). Keep a hidden
`lanes` widget (count) so the rail count **persists** across save/reload — restore by
re-adding that many slot pairs (mirror the repo's existing dynamic-slot restore that reads
raw `widgets_values` before link rewiring).
- Maintain exactly `lanes` visible input slots `in_1..in_lanes` and output slots
`out_1..out_lanes` (add/remove the trailing pair as the count changes).
- **Bottom +**: `lanes++` → add `in_{n}`/`out_{n}`. **Bottom **: `lanes--` → remove the last
pair (only the end moves, so existing wiring is untouched). Clamp `1..MAX_LANES`.
- Buttons via on-node widgets (or the node context menu) labeled `+ lane` / ` lane`.
- Slots are any-type (`*`) so any wire connects.
**Manual verify:** add/remove lanes from the bottom; wire IMAGE through lane 1 and LATENT
through lane 2; values pass; save+reload restores the lane count and wiring.
**Commit** `feat: multi-reroute frontend — dynamic lanes + bottom add/remove`
---
### Task 6: `web/multi_reroute.js` — top +/ with wiring preservation (Phase 2)
**Files:** Modify `web/multi_reroute.js`
- **Top +**: insert a new lane at the top. Because slots are positional and links bind to
slot index, capture all current input/output links, add a pair, and **re-map links** so the
existing lanes keep their connections and the new empty lane is visually first.
- **Top **: remove the top lane, remap the rest up.
- Implement against a small lane-model (list of logical lanes ↔ slot indices); rebuild slots
and reconnect from the model so a failure can't silently drop wires.
**Manual verify:** with lanes 13 wired, Top + adds an empty lane at top and 13 stay wired;
Top removes it cleanly. Save/reload still consistent.
**Commit** `feat: multi-reroute top add/remove (wiring-preserving)`
---
### Task 7: Live smoke test in ComfyUI
Restart ComfyUI. Verify:
- [ ] "Multi Reroute (Rail)" appears under "Datasete Gates" with 4 lanes.
- [ ] Bottom +/ add/remove lanes; Top +/ (Phase 2) keep existing wiring.
- [ ] Route mixed types (IMAGE, MASK, LATENT, STRING) through separate lanes → all pass intact.
- [ ] An unconnected lane doesn't trigger its downstream (ExecutionBlocker).
- [ ] Save + reload restores lane count and all connections.
**Commit** (if fixes) `fix: multi-reroute live-test adjustments`
---
## Definition of done
- `$PY -m pytest tests/test_anytype.py tests/test_reroute.py -v` green; full `tests/` green.
- Manual checklist passes: lanes add/remove (bottom; top in P2), mixed-type pass-through,
empty-lane blocking, persistence.