ADR-executor-002
Three-pivot scheduling — seed rule + `pairedWith` transfer + `validate_graph_connectivity`
Scope split. The original ADR-031 decided both the boundary-node typology (three pivot types + ports_mode) and the executor algorithm change (seed discovery + internal transfer + load-time validation). This file covers the executor-side half. The dataflow / boundary-node half is ADR-dataflow-006. Both landed together.
Context
The executor in graph_executor.py:198-211 carried three startup-firing rules that were doing related-but-distinct work: (a) an explicit config.seed=true opt-in flag (only ever set on iterIn itself), (b) an implicit "no required ports AND no incoming edges" structural test, and (c) an iterIn special-case skip. Once iterIn was reshaped into an output-only pivot (see ADR-dataflow-006), the seed=true flag had zero remaining producers, and the special-case skip was the only rule still preventing iterIn from self-seeding empty. Separately, the old implicit ordering of "seed nodes fire first, then iterIn" was never visible in the graph — misconfigured graphs silently parked required-but-unwired ports in pending_inputs forever, a debugging black hole the team stepped on more than once during MapGPT bring-up.
Decision
(1) Executor-internal pairedWith transfer for Initialize. On Initialize firing, copy its outputs into the paired iterIn's pending_inputs (then queue iterIn) — byte-for-byte mirror of the existing iterOut → iterIn transfer at graph_executor.py:294-337. No canvas wire connects Initialize to iterIn; the transfer is purely runtime. (2) Collapse seed-discovery to one structural rule: type != "iterIn" AND no incoming edges AND no required input ports. Drop the is_seed / config.seed lookup entirely. get_required_inputs() + the per-instance _resolve_ports(config) resolver (ADR-dataflow-003) together decide which ports are required at seed-discovery time. (3) Load-time validator validate_graph_connectivity() in graph_def.py (sibling of the existing validate_edge_wire_type) walks each node's required ports — honouring _resolve_ports(config) exactly the way _get_required_ports_for_node does at runtime — and raises ValueError for any required-but-unwired port. Hooked at the API boundary in app/api/execution/run.py and inside _load_graph_by_name in app/api/execution/eval.py, returning HTTP 400 on failure. (4) Remove the dead seed: true flag from opennav_habitat.json and navgpt_mp3d.json.
Alternatives
(a) Drop the seed-discovery rule entirely and require every startup node to be wired transitively into Initialize — rejected; constant-source nodes (text providers like system_prompt, get_instruction) live outside Initialize's chain and need the structural "no required, no incoming" rule to fire. (b) Walk backwards from Initialize's input wires to discover the chain heads — rejected; it covers env-state seeds cleanly but doesn't cover the constant-source case (same gap). (c) Drop the no-required-ports exclusion and let the validator be the only line of defence — rejected by the explicit principle: required-but-unwired nodes should never silently fire with None; the runtime exclusion is a belt to the validator's suspenders. (d) Collapse the _seed/_loop env-node duplication in the same change — explicitly deferred; that requires either a default-valued port on iterIn (so step 0 emits a NoOp action and a single env node fires N+1 times) or a per-iteration node lifecycle in the executor — both larger refactors with their own design space.
Rationale
The executor change is one new branch (Initialize → iterIn transfer) modelled byte-for-byte on the existing iterOut → iterIn block, plus a strictly-tightening change to seed-discovery; no new firing modes, no new lifecycle, no new state-machine. The Phase-B validator + simplified discovery rule is a textbook fail-fast move: a misconfigured graph is rejected at the API boundary with a precise error message instead of silently parking a node forever in pending_inputs. The _seed/_loop env-node duplication is left in place precisely because the new pivot model gives that future refactor a cleaner foundation to land on (Initialize is now the formal target for the _seed chain, iterIn the formal source for the _loop chain — orthogonal slots), turning a confused dual-role question into a well-posed one.
Affected docs
design-docs/graph-executor.md (Phase 2 seed-discovery rewrite), core/glossary.md (new terms: validate_graph_connectivity), core/architecture.md (Execution Engines section), core/roadmap.md (changelog), core/codebase-map.md (new validator)