0%

03 — roadmap.py Decomposition: Prep + Clean Leaves

Goal

Begin decomposing scripts/plan_orchestrator/roadmap.py (11,875 lines, 30 cohesive clusters identified by full-file research on 2026-07-06) into a scripts/plan_orchestrator/roadmap/ SUBPACKAGE — the largest and most heavily test-coupled of the three files (119 of 395 test files import it by qualified name, many reaching into underscore-private functions directly). Do this LAST among the three files (Sections 03-05, after Section 02 stabilizes route_walk.py’s import surface) since roadmap.py is the heaviest consumer of route_walk’s ~30 named symbols — splitting the consumer after the dependency is stable minimizes moving parts.

Given the scale (30 clusters, two >600-line “god functions”), this decomposition spans THREE sections (03/04/05) rather than one, per routing.md §4 promotion discipline (a single section accumulating this many distinct extraction batches would blow every section-body budget).

The _PHASE_SCRATCH_ROOT invariant — MUST preserve

tests/conftest.py carries an autouse fixture that monkeypatch.setattr(roadmap, "_PHASE_SCRATCH_ROOT", tmp_path / ...) for EVERY test in the 395-file suite. Two existing consumer modules (route_progress_guard.py, review_chain.py) already read this back via FUNCTION-SCOPED from scripts.plan_orchestrator.roadmap import _PHASE_SCRATCH_ROOT (not a top-of-file import) specifically so the monkeypatch is visible at call time, not import time.

  • The constant MUST be defined directly in roadmap/__init__.py (the package’s own top-level module object — monkeypatch.setattr(roadmap, "_PHASE_SCRATCH_ROOT", ...) operates on that module object regardless of whether roadmap is a flat file or a package __init__.py; packages ARE modules).
  • Any NEW submodule that reads _PHASE_SCRATCH_ROOT at call time (the function _phase_scratch_dir, moving to roadmap_dispatch_utils.py per the mapping below) MUST do from scripts.plan_orchestrator import roadmap as _roadmap_pkg then reference _roadmap_pkg._PHASE_SCRATCH_ROOT (module-qualified, late-bound) — NEVER from scripts.plan_orchestrator.roadmap import _PHASE_SCRATCH_ROOT at its own top-of-file, which binds a stale copy before any test monkeypatch runs and silently breaks the fixture. This is the SAME pattern route_progress_guard.py/review_chain.py already use — extend it, do not invent a new one.
  • Add a regression test asserting _phase_scratch_dir() reflects a monkeypatched roadmap._PHASE_SCRATCH_ROOT after the extraction (pin against silent regression).

Cluster -> submodule mapping — this section’s scope (leaves only)

Subpackage skeleton + the 5 clean-leaf clusters (each called by roadmap’s other clusters but calling nothing back — safest to extract first):

SubmoduleResponsibility~LinesKey symbols
roadmap/__init__.pyStable public surface: orchestrate() (unchanged signature — cli_shared.py does from .roadmap import orchestrate at module scope) + _PHASE_SCRATCH_ROOT + re-exports of every underscore-private name any package module or test imports directly (full list finalized in Section 05)~450 (orchestrate body, unchanged)orchestrate, _PHASE_SCRATCH_ROOT
roadmap_dispatch_utils.pyDispatch-arg composition, path/slug normalization, phase-scratch-dir resolution~230_compose_autopilot_dispatch_arg, _review_plan_pre_work_target, _section_path_slugs, _phase_scratch_dir, _resolve_plan_dir, _normalize_section_file_path
roadmap_ask_user_options.pyGeneric AskUserQuestion option default/normalize helpers~115_default_options, _normalize_option
roadmap_step4_pacing.pyStep 4 pacing~67_step_4_pacing
roadmap_special_plan_shapes.pySection-less / research-phase plan-shape detection + terminus emission~146_is_section_less_plan, _is_research_phase_plan, _emit_section_less_route, _emit_research_phase_terminus
roadmap_terminal_emitters.pyGeneric terminal next_action builders (roadmap’s own inline-dict style — NOTE: next_action_builders.py already owns this responsibility for review_plan.py/fix_bug.py; roadmap.py’s parallel implementation is a pre-existing DRY gap, OUT OF SCOPE to unify here — flag as a follow-up bug if not resolved by Section 05, do not silently unify mid-split)~270_emit_dispatch, _emit_halt, _emit_ask_user_plan_candidates, _emit_section_terminus_advance

Work items

  • Create scripts/plan_orchestrator/roadmap/ subpackage: roadmap/__init__.py initially re-exports everything from a temporarily-retained roadmap/_legacy.py (verbatim copy of today’s roadmap.py) so scripts.plan_orchestrator.roadmap.orchestrate and every existing import path resolve identically before any real extraction; delete the old top-level scripts/plan_orchestrator/roadmap.py file in the same commit (replaced by the package). Full scripts/plan_orchestrator/tests suite green; commit.
  • Extract the 5 clean-leaf clusters (roadmap_dispatch_utils.py, roadmap_ask_user_options.py, roadmap_step4_pacing.py, roadmap_special_plan_shapes.py, roadmap_terminal_emitters.py) out of roadmap/_legacy.py; apply the _PHASE_SCRATCH_ROOT invariant above to _phase_scratch_dir; add the regression test for the monkeypatch; roadmap/__init__.py re-exports every moved name; full suite green; commit.