ADR-dataflow-003
Configurable IterIn/IterOut ports via `config.ports`
Context
IterIn/IterOut iteration gate nodes had hardcoded domain-specific ports (observation, viewpoint_id, navigable) added ad-hoc for NavGPT graphs. This violated ADR-platform-001 (zero domain knowledge in the framework). Different agent graphs need different iteration data channels (VLN-CE uses rgb/depth/state; NavGPT uses observation/viewpoint_id/navigable), but the class-level port list forced all instances to share the same ports. The runtime already handled arbitrary port names via dict pass-through in forward() and dict-based IterOutβIterIn transfer in GraphExecutor β the problem was purely at the schema/UI layer.
Decision
(1) Add _resolve_ports(config) classmethod to BaseCanvasNode (returns class-level ports by default). IterIn/IterOut override: when config.ports exists (array of {name, wire_type}), build instance-level PortDef lists β IterIn mirrors to both input+output, IterOut to input only (sink). All generated ports forced optional=True. (2) Remove domain-specific ports from class-level defaults. Keep generic VLN ports (rgb, depth, state, action, response, raw_obs) as defaults for backward compatibility. (3) Add ConfigField("ports", "port_list") to both nodes' ui_config for UI editing. (4) Migrate all 3 graph presets with explicit config.ports matching edge handles. (5) Frontend resolveInstancePorts(schema, config) utility overrides _schema ports at all injection paths. (6) PortListEditor component for the new port_list ConfigField type β add/remove ports with name + wire_type, orphan edge cleanup.
Alternatives
(a) Framework-wide dynamic ports on BaseCanvasNode β rejected: over-engineered for 2 node types; _resolve_ports() on BaseCanvasNode with no-op default provides the extension point for cheap future generalization without changing every node. (b) Minimal ports + generic pass-through (no visible handles) β rejected: defeats the purpose of a visual editor; users can't see what data flows through iterations.
Rationale
The runtime already works with arbitrary port names β zero changes to graph_executor.py. config.ports as an instance-level override follows the existing freeform NodeDef.config pattern. Placing _resolve_ports() on BaseCanvasNode (not just IterIn/IterOut) makes generalization to a third node type a one-line override rather than a rearchitecture. Critical invariant: all IterIn/IterOut ports must remain optional=True because get_required_inputs() in node_io.py reads class-level ports only β non-optional instance ports would be invisible to _is_ready(), causing premature firing or deadlock.
Affected docs
roadmap.md, glossary.md, canvas-system.md