Section 03: scripts/plan_routing.py + /create-plan Step 0.6
Status: Complete
Goal: Extract the mechanical routing logic into a tested, reusable Python module, and wire both consumers (/add-bug and /create-plan) to the same SSOT. This eliminates the current algorithmic duplication (/add-bug has the one-liner inline; /create-plan would have to duplicate it if not factored first) and creates the verification surface §04’s reviewer wiring relies on.
Success Criteria:
-
scripts/plan_routing.pyexists, executable, with--helpoutput documenting all CLI modes (connects to mission criterion: “scripts/plan_routing.pyexists and factorsowns_cratesinversion”) - Primary-by-count algorithm is the core decision function; single-crate case is a degenerate instance (same code path)
- Unit tests cover: single-crate hit, dominant multi-crate, tie → Fallback-R, no owner → Fallback-R, stale crate input → error raised
-
/add-bug/workflow.mdStep 0 consumesplan_routing.py(no inline Python one-liner duplicating the inversion) -
/create-plan/SKILL.mdPhase 0 / Phase 0.5 / Step 0.6 section structure is clean: fork decision → routing triage → classification - Smoke test: running plan_routing.py on repr-opt’s scope returns route=subsection target=§21A-llvm
-
./test-all.shgreen; plan_routing tests green
Context: /add-bug/workflow.md:21–70 contains an inline Python one-liner that reads every plans/roadmap/section-*.md frontmatter, inverts owns_crates: into a crate → section-path map, and emits the target section path. The one-liner is correct but unshared; adding /create-plan Step 0.6 as a second consumer would be an impl-hygiene.md LEAK:algorithmic-duplication violation. Extracting to a helper restores SSOT.
Reference implementations:
scripts/plan_corpus/bug_markers.py— canonical single-file SSOT:roadmap_scan.pyandbug_queue_scan.pyboth importparse_bug_entries()+classify_bug_exclusion()rather than duplicate regex.plan_routing.pyfollows the same pattern./add-bug/workflow.md:43–60— existing routing decision logic to preserve behavior parity (single-crate case).
Depends on: Section 01 — plan_routing.py operates on the normalized owns_crates map; empty/stale entries would produce incorrect routing.
Intelligence Reconnaissance
Queries run 2026-04-24:
scripts/intel-query.sh --human symbols "plan_routing" --repo ori --limit 10— no existing symbol; module is new.scripts/intel-query.sh --human file-symbols "scripts/plan_corpus" --repo ori— enumerates existing plan_corpus modules as pattern precedent:schemas.py,bug_markers.py,bug_validators.py,dag.py,parser.py.scripts/intel-query.sh --human similar "classify_bug_exclusion" --repo rust,swift,go --limit 5— cross-repo routing/classification patterns (deprioritized; plan_routing is simpler than bug triage).grep -n "owns_crates" .claude/skills/add-bug/workflow.md— confirms routing logic at lines 21–70.
Results summary (≤500 chars) [ori]: scripts/plan_corpus/bug_markers.py is the canonical single-file SSOT precedent for helper factoring. /add-bug/workflow.md:21-70 contains the inline routing one-liner to extract. scripts/plan_corpus/schemas.py provides RoadmapSectionSchema with owns_crates: list[str] (required per line 89) — the helper parses from this. .claude/skills/create-plan/SKILL.md Phase 0 + Phase 0.5 are the insertion points for Step 0.6.
See .claude/skills/query-intel/compose-intel-summary.md for the full query protocol.
03.1 Author scripts/plan_routing.py with primary-by-count algorithm
File(s): scripts/plan_routing.py (new file)
Target CLI surface:
python3 scripts/plan_routing.py --scope <crate1>[,<crate2>,...] [--plans-dir plans/roadmap]
→ emits JSON: {"route": "subsection"|"fallback-r"|"error", "target_section": "...", "section_hits": {...}, "reason": "..."}
python3 scripts/plan_routing.py --validate-map
→ validates every live crate is claimed by exactly one section; exits 0 clean or 1 with diagnostics
python3 scripts/plan_routing.py --help
→ usage
-
Create
scripts/plan_routing.pywith top-level#!/usr/bin/env python3shebang -
Define
@dataclass(frozen=True) class RoutingDecisionwith fields:route: str,target_section: str | None,section_hits: dict[str, int],routing_justification: str | None,reason: str -
Implement
load_owns_crates_map(plans_dir: Path) -> dict[str, str]— iteratesplans_dir/section-*.md, parses frontmatter viayaml.safe_load, buildscrate → section_pathdict; raisesRoutingErroron duplicate (crate in two sections’ lists) or on missingowns_crates:field per RoadmapSectionSchema requirement -
Implement
primary_by_count(scope: set[str], owns_map: dict[str, str]) -> RoutingDecision:section_hits: Counter[str] = Counter() unowned: set[str] = set() for crate in scope: if crate in owns_map: section_hits[owns_map[crate]] += 1 else: unowned.add(crate) if not section_hits: return RoutingDecision( route="fallback-r", target_section=None, section_hits={}, routing_justification=f"no scoped crate has an owning roadmap section (unowned: {sorted(unowned)})", reason="zero-owner", ) most_common = section_hits.most_common() top_count = most_common[0][1] winners = [s for s, n in most_common if n == top_count] if len(winners) == 1 and not unowned: return RoutingDecision( route="subsection", target_section=winners[0], section_hits=dict(section_hits), routing_justification=None, reason=f"primary-by-count: {winners[0]} owns {top_count} of {len(scope)} crates", ) if len(winners) > 1: return RoutingDecision( route="fallback-r", target_section=None, section_hits=dict(section_hits), routing_justification=f"tie across sections {sorted(winners)} with {top_count} hits each", reason="tie", ) # Single winner but some unowned crates return RoutingDecision( route="fallback-r", target_section=None, section_hits=dict(section_hits), routing_justification=f"scope includes unowned crates {sorted(unowned)} alongside owned crates in {winners[0]}", reason="partial-ownership", ) -
Implement
validate_map(plans_dir: Path, live_crates_dir: Path) -> list[str]returning list of diagnostic strings (empty means clean); detects: stale entries (in owns_crates but not in live_crates_dir), uncovered crates (in live_crates_dir but in no section’s list), duplicates -
Implement CLI entry point in
main()withargparse; subcommands/flags:--scope,--validate-map,--plans-dir,--live-crates-dir,--help -
Emit JSON output for machine consumers (default) and
--humanflag for pretty-print -
Subsection close-out (03.1) — MANDATORY before starting 03.2:
-
python3 scripts/plan_routing.py --helpexits 0 with usage text -
python3 scripts/plan_routing.py --scope ori_canon,ori_arc,ori_repr,ori_llvmreturnsroute=subsection, target_section=plans/roadmap/section-21A-llvm.md - Update this subsection’s
statustocomplete - Repo hygiene check —
compiler_repo/diagnostics/repo-hygiene.sh --check
-
03.2 Unit tests at compiler_repo/tests/plan-routing/
File(s): compiler_repo/tests/plan-routing/test_plan_routing.py (new), compiler_repo/tests/plan-routing/fixtures/ (new synthetic roadmap dirs)
Coverage matrix for the algorithm:
| Case | Scope | Expected route | Expected target |
|---|---|---|---|
| Single-crate hit | {ori_parse} | subsection | section-00-parser |
| Multi-crate all-hit-one-section | {ori_canon, ori_arc, ori_repr, ori_llvm} | subsection | section-21A-llvm |
| Multi-crate dominant with unowned | {ori_parse, ori_parse, missing_crate} | fallback-r | None (partial-ownership) |
| Tie two sections | {ori_parse, ori_llvm} (1 each) | fallback-r | None (tie) |
| Zero owners | {nonexistent_crate} | fallback-r | None (zero-owner) |
| Stale crate in map | live_crates lacks ori_lsp | validate_map returns “STALE: ori_lsp” | — |
| Uncovered crate | live_crates has new_crate, no section owns it | validate_map returns “UNCOVERED: new_crate” | — |
| Duplicate claim | two fixture sections both list ori_parse | load raises RoutingError | — |
-
Create fixture dir
compiler_repo/tests/plan-routing/fixtures/roadmap-basic/with 3–4 syntheticsection-NN-*.mdfiles covering the cases -
Create fixture dir
compiler_repo/tests/plan-routing/fixtures/roadmap-stale/for stale-entry test -
Create fixture dir
compiler_repo/tests/plan-routing/fixtures/roadmap-duplicate/for duplicate-claim test -
Write
test_plan_routing.pywith one test function per row of the matrix; use pytest -
Run tests:
pytest compiler_repo/tests/plan-routing/→ all green -
Add a smoke test that runs the algorithm against the REAL
plans/roadmap/tree and verifies repr-opt’s scope routes to §21A (end-to-end verification against production data) -
Subsection close-out (03.2) — MANDATORY before starting 03.3:
-
pytest compiler_repo/tests/plan-routing/exits 0 with all tests passing - Update this subsection’s
statustocomplete - Repo hygiene check —
compiler_repo/diagnostics/repo-hygiene.sh --check
-
03.3 Migrate /add-bug Step 0 to consume plan_routing.py
File(s): .claude/skills/add-bug/workflow.md (edit Step 0)
The inline one-liner at lines 21–70 produces the same inversion plan_routing.py now owns. Replace it with a plan_routing.py invocation to restore algorithmic SSOT.
-
Read current
.claude/skills/add-bug/workflow.mdStep 0 to capture the single-crate routing logic (for behavior parity) -
Replace the inline Python one-liner with:
python3 scripts/plan_routing.py --scope <owning-crate> --human # Parses JSON output; routes bug to target_section for subsection OR to bug-tracker/plans/BUG-XX-NNN/ for fallback-r -
Preserve the existing tracker-index carve-out (bugs orthogonal to compiler code) — the helper emits
route=fallback-r, reason=zero-owner; the workflow then decides subsection underbug-tracker/vs tracker-index based on subsystem -
Verify /add-bug behavior hasn’t regressed: run a synthetic bug filing (dry-run if available) for a known-single-crate case; confirm output matches pre-migration routing
-
grep -c "owns_crates" .claude/skills/add-bug/workflow.mddrops significantly (the one-liner was the main consumer) -
Subsection close-out (03.3) — MANDATORY before starting 03.4:
-
/add-bugnow consumesplan_routing.py; inline one-liner removed - Behavior parity verified (single-crate routing matches pre-migration)
- Update this subsection’s
statustocomplete - Repo hygiene check —
compiler_repo/diagnostics/repo-hygiene.sh --check
-
03.4 Wire /create-plan Step 0.6
File(s): .claude/skills/create-plan/SKILL.md
Insert a new Step 0.6 block between Phase 0 (fork decision) and Phase 0.5 (classification).
Design decision — Step 0.6 vs Phase 0.6 naming: the skill uses “Phase N” for major workflow stages (Phase 0–5) and “Step N” for minor sub-steps inside phases. “Step 0.6” fits the existing Step nomenclature in Phase 0; “Phase 0.6” would imply a new major stage. Adopt Step 0.6.
-
Locate the insertion point: between the existing Phase 0 “Fork Decision” close (after Phase 0.5 classification per plan-schema drives template selection) and actual Phase 1 prerequisites
-
Draft Step 0.6 body:
### Step 0.6: Mechanical routing triage (MANDATORY after fork decision, before Phase 0.5 classification) Before classifying plan type, determine the plan's target artifact kind via mechanical `owns_crates:` inversion. Plan author declares scope (crates touched); helper decides where the plan lands. 1. Gather scope: list of compiler crates the plan touches (can be inferred from user's expanded mission + Step 1B scope declaration) 2. If scope is empty (pure skill-infra-docs) → route = tracker-index; continue to Phase 0.5 classification 3. Otherwise invoke: `python3 scripts/plan_routing.py --scope <crate1>,<crate2>,... --human` 4. Parse the JSON output and record in the plan's future `00-overview.md` frontmatter: - `route=subsection, target_section=...` → plan will be created inside `plans/roadmap/section-NN-*.md` as a new subsection - `route=fallback-r, routing_justification=...` → plan will be created as `plans/<name>/` peer directory with `reroute: true` + `routing_justification: "<text>"` in `index.md` 5. If `--validate-map` detects stale/uncovered/duplicate crates, STOP and direct the user to fix the roadmap `owns_crates` frontmatter first — do NOT proceed with plan creation on a broken map The decision becomes authoritative for Phase 4 section-writing (SKILL.md §Step 11c template branches on `plan_type` AND on `route`): - `route=subsection` → sections are written inside the target roadmap section file as `## NN.X` subsections (no new directory) - `route=fallback-r` → standard `plans/<name>/` directory creation + full section-file authoring per plan-schema.md - `route=tracker-index` → no plan directory; entry is added to the relevant `bug-tracker/section-NN-*.md` -
Edit SKILL.md to insert Step 0.6 at the correct position
-
Verify:
grep -n "Step 0.6" .claude/skills/create-plan/SKILL.mdreturns at least one match -
Run prose-lint:
python3 scripts/prose-lint.py .claude/skills/create-plan/SKILL.mdexits 0 -
Subsection close-out (03.4) — MANDATORY before starting 03.N:
- Step 0.6 published in SKILL.md
- prose-lint + grep verification green
- Update this subsection’s
statustocomplete - Repo hygiene check —
compiler_repo/diagnostics/repo-hygiene.sh --check
03.N Completion Checklist
- All implementation subsections (03.1–03.4) are
[x]and statuscomplete - All section success criteria have corresponding
[x]checkboxes -
python3 scripts/plan_routing.py --helpexits 0 -
pytest compiler_repo/tests/plan-routing/exits 0 -
python3 scripts/plan_routing.py --scope ori_canon,ori_arc,ori_repr,ori_llvmreturnsroute=subsection target_section=plans/roadmap/section-21A-llvm.md -
/add-bugbehavior parity verified; inline one-liner removed -
/create-plan/SKILL.mdhas Step 0.6 grep-findable -
./test-all.shgreen — regression canary -
python -m scripts.plan_corpus checkexits 0 - Plan sync:
- This section’s frontmatter
status→complete, subsection statuses →complete -
00-overview.mdQuick Reference: Section 03 status →Complete -
00-overview.mdmission success criteria: check off “scripts/plan_routing.pyexists” + “/create-planSKILL.md has Step 0.6” -
index.mdSection 03 status →Complete
- This section’s frontmatter
- Repo hygiene check —
compiler_repo/diagnostics/repo-hygiene.sh --check
Exit Criteria: plan_routing.py runnable + tested; /add-bug consumes it (no duplication); /create-plan Step 0.6 invokes it; repr-opt smoke test returns §21A-llvm; all tests green.