r"""Rebuild plan/ from the archived poc-modules snapshot. The active working surface is plan/. This script is retained so the smaller markdown slices can be regenerated from the archived legacy monolith when needed. Run with: .venv\Scripts\python.exe scripts\split_plan.py """ from __future__ import annotations import re import shutil from dataclasses import dataclass, field from pathlib import Path from typing import Final ROOT = Path(__file__).resolve().parents[1] SOURCE = ROOT / "archive" / "poc-modules.md" OUTPUT = ROOT / "plan" SENTINEL = OUTPUT / ".generated-by-split-plan" HEADING_RE = re.compile(r"^(#{1,6})\s+(.*)$") MODULE_ID_RE = re.compile(r"\b(M\d{2}[a-z]?)\b", re.IGNORECASE) QUESTION_ID_RE = re.compile(r"\b(Q\d+)\b", re.IGNORECASE) BACKEND_SECTIONS: Final[list[str]] = [ "Overview", "Spirit guardrails", "Architecture: three layers that must not collapse", "Trust model (critical — never flatten into one field)", "Dependency Graph (corrected)", "Critical Path", "Parallel Workstreams", ] BACKEND_BLOCKERS: Final[list[str]] = ["Q1", "Q2", "Q4", "Q5", "Q6", "Q7"] BACKEND_MODULES: Final[list[str]] = [ "M01", "M03", "M08", "M11", "M13", "M14", "M15", "M17", "M20", "M21", "M22", "M23", ] FRONTEND_SECTIONS: Final[list[str]] = [ "Overview", "Spirit guardrails", "Architecture: three layers that must not collapse", "Parallel Workstreams", "First actions before writing any code (Wave 0 justification)", ] FRONTEND_BLOCKERS: Final[list[str]] = ["Q1", "Q3", "Q4"] FRONTEND_MODULES: Final[list[str]] = ["M17", "M18", "M19", "M22"] LLM_SECTIONS: Final[list[str]] = [ "Overview", "Spirit guardrails", "Trust model (critical — never flatten into one field)", "Critical Path", "Risk Register", ] LLM_BLOCKERS: Final[list[str]] = ["Q1", "Q2", "Q5", "Q6"] LLM_MODULES: Final[list[str]] = [ "M02", "M04b", "M08", "M09", "M10", "M13", "M14", "M22", ] @dataclass(slots=True) class Section: level: int heading: str body_lines: list[str] = field(default_factory=list) children: list[Section] = field(default_factory=list) def title(self) -> str: return self.heading.strip() def content(self, *, heading_level: int = 1) -> str: lines = self.render(heading_level=heading_level) return "\n".join(lines).rstrip() + "\n" def render(self, *, heading_level: int) -> list[str]: lines = [f"{'#' * heading_level} {self.title()}", ""] lines.extend(self.body_lines) if self.body_lines and self.children: lines.append("") for index, child in enumerate(self.children): lines.extend(child.render(heading_level=heading_level + 1)) if index < len(self.children) - 1: lines.append("") return lines def slugify(value: str) -> str: value = value.strip().lower() value = value.replace("→", " to ") value = value.replace("/", " ") value = re.sub(r"[`*_(){}\[\]:.,!?'\"]", "", value) value = re.sub(r"[^a-z0-9]+", "-", value) return value.strip("-") or "section" def parse_sections(text: str) -> tuple[list[str], list[Section]]: preamble: list[str] = [] roots: list[Section] = [] stack: list[Section] = [] in_code_block = False for raw_line in text.splitlines(): line = raw_line.rstrip() stripped = line.strip() if stripped.startswith("```"): in_code_block = not in_code_block match = None if in_code_block else HEADING_RE.match(line) if match: level = len(match.group(1)) heading = match.group(2).strip() section = Section(level=level, heading=heading) while stack and stack[-1].level >= level: stack.pop() if stack: stack[-1].children.append(section) else: roots.append(section) stack.append(section) continue if stack: stack[-1].body_lines.append(line) else: preamble.append(line) return preamble, roots def write_file(path: Path, content: str) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(content, encoding="utf-8") def clean_output_dir(output_dir: Path) -> None: if not output_dir.exists(): return if not SENTINEL.exists(): raise RuntimeError( f"Refusing to replace {output_dir} because it was not created by this script." ) shutil.rmtree(output_dir) def heading_link(path: Path) -> str: return path.relative_to(OUTPUT).as_posix() def module_filename(section: Section) -> str: match = MODULE_ID_RE.search(section.title()) module_id = match.group(1).lower() if match else "module" tail = slugify(MODULE_ID_RE.sub("", section.title(), count=1)) return f"{module_id}-{tail}.md" if tail else f"{module_id}.md" def require_path(path_map: dict[str, str], key: str) -> str: try: return path_map[key] except KeyError as exc: raise RuntimeError(f"Missing generated path for {key!r}") from exc def normalize_module_id(module_id: str) -> str: return module_id.upper() def question_filename(section: Section) -> str: match = QUESTION_ID_RE.search(section.title()) question_id = match.group(1).lower() if match else "question" tail = slugify(QUESTION_ID_RE.sub("", section.title(), count=1)) return f"{question_id}-{tail}.md" if tail else f"{question_id}.md" def write_blocker_breakout( section: Section, *, section_paths: dict[str, str], blocker_paths: dict[str, str] ) -> None: blockers_dir = OUTPUT / "blockers" blockers_dir.mkdir(parents=True, exist_ok=True) readme_lines = [ "# Blockers By Question", "", "Split from the pre-coding blockers section so agents can load one decision at a time.", "", ] for child in section.children: match = QUESTION_ID_RE.search(child.title()) if match is None: continue question_id = match.group(1).upper() filename = question_filename(child) question_path = blockers_dir / filename write_file(question_path, child.content()) rel_path = heading_link(question_path) blocker_paths[question_id] = rel_path readme_lines.append(f"- [{child.title()}]({filename})") write_file(blockers_dir / "README.md", "\n".join(readme_lines).rstrip() + "\n") section_paths["Blockers By Question"] = heading_link(blockers_dir / "README.md") def write_agent_guide( *, filename: str, title: str, summary: str, section_keys: list[str], blocker_ids: list[str], module_ids: list[str], section_paths: dict[str, str], blocker_paths: dict[str, str], module_paths: dict[str, str], ) -> None: lines = [ f"# {title}", "", summary, "", "Read the source file only when you need full surrounding prose; otherwise start from these slices.", "", "## Foundation", "", ] for key in section_keys: lines.append(f"- [{key}]({require_path(section_paths, key)})") lines.extend(["", "## Blockers", ""]) for blocker_id in blocker_ids: lines.append(f"- [{blocker_id}]({require_path(blocker_paths, blocker_id)})") lines.extend(["", "## Modules", ""]) for module_id in module_ids: lines.append( f"- [{module_id}]({require_path(module_paths, normalize_module_id(module_id))})" ) write_file(OUTPUT / filename, "\n".join(lines).rstrip() + "\n") def write_agent_guides( *, section_paths: dict[str, str], blocker_paths: dict[str, str], module_paths: dict[str, str], ) -> None: write_agent_guide( filename="for-backend.md", title="For Backend Agents", summary="Backend-heavy reading order for contracts, orchestration, validation, session management, and export work.", section_keys=BACKEND_SECTIONS, blocker_ids=BACKEND_BLOCKERS, module_ids=BACKEND_MODULES, section_paths=section_paths, blocker_paths=blocker_paths, module_paths=module_paths, ) write_agent_guide( filename="for-frontend.md", title="For Frontend Agents", summary="Frontend-heavy reading order for the Gradio host, renderer boundary, and Cytoscape component work.", section_keys=FRONTEND_SECTIONS, blocker_ids=FRONTEND_BLOCKERS, module_ids=FRONTEND_MODULES, section_paths=section_paths, blocker_paths=blocker_paths, module_paths=module_paths, ) write_agent_guide( filename="for-llm.md", title="For LLM Agents", summary="LLM-heavy reading order for structured output, prompting, validation boundaries, and model-facing risks.", section_keys=LLM_SECTIONS, blocker_ids=LLM_BLOCKERS, module_ids=LLM_MODULES, section_paths=section_paths, blocker_paths=blocker_paths, module_paths=module_paths, ) def build_plan(preamble: list[str], roots: list[Section]) -> None: clean_output_dir(OUTPUT) OUTPUT.mkdir(parents=True, exist_ok=True) SENTINEL.write_text("generated by scripts/split_plan.py\n", encoding="utf-8") title_section = next((section for section in roots if section.level == 1), None) if title_section is None: raise RuntimeError("Expected an H1 title in archive/poc-modules.md") h2_sections = [section for section in title_section.children if section.level == 2] if not h2_sections: raise RuntimeError("Expected H2 sections under the document title") index_lines = [ "# Plan Index", "", "Originally generated from the archived `archive/poc-modules.md` snapshot by `scripts/split_plan.py`.", "`plan/` is now the active working surface; `archive/` keeps the legacy monolith and research notes.", "", "## Archive Snapshot", "", f"- `../archive/poc-modules.md` ({SOURCE.as_posix()})", "", "## Agent Reading Lists", "", "- [For backend agents](for-backend.md)", "- [For frontend agents](for-frontend.md)", "- [For LLM agents](for-llm.md)", "", "## Sections", "", ] section_paths: dict[str, str] = {} module_paths: dict[str, str] = {} blocker_paths: dict[str, str] = {} overview_lines = [f"# {title_section.title()}", ""] if preamble: overview_lines.extend(preamble) overview_lines.append("") overview_lines.extend(title_section.body_lines) write_file(OUTPUT / "00-overview.md", "\n".join(overview_lines).rstrip() + "\n") index_lines.append("- [Overview](00-overview.md)") section_paths["Overview"] = "00-overview.md" non_module_counter = 1 for h2 in h2_sections: if h2.title() == "Module List": module_dir = OUTPUT / "modules" module_dir.mkdir(parents=True, exist_ok=True) module_index = [ "# Modules", "", "Split from the `Module List` section.", "", ] for layer_index, layer in enumerate(h2.children, start=1): layer_slug = f"{layer_index:02d}-{slugify(layer.title())}" layer_dir = module_dir / layer_slug layer_dir.mkdir(parents=True, exist_ok=True) layer_readme_lines = [f"# {layer.title()}", ""] if layer.body_lines: layer_readme_lines.extend(layer.body_lines) layer_readme_lines.append("") layer_readme_lines.append("## Modules") layer_readme_lines.append("") for module in layer.children: filename = module_filename(module) module_path = layer_dir / filename write_file(module_path, module.content()) layer_readme_lines.append(f"- [{module.title()}]({filename})") match = MODULE_ID_RE.search(module.title()) if match is not None: module_paths[match.group(1).upper()] = heading_link(module_path) write_file( layer_dir / "README.md", "\n".join(layer_readme_lines).rstrip() + "\n", ) rel_layer = heading_link(layer_dir / "README.md") module_index.append(f"- [{layer.title()}]({rel_layer})") write_file( module_dir / "README.md", "\n".join(module_index).rstrip() + "\n" ) index_lines.append("- [Modules](modules/README.md)") section_paths["Module List"] = "modules/README.md" continue filename = f"{non_module_counter:02d}-{slugify(h2.title())}.md" non_module_counter += 1 write_file(OUTPUT / filename, h2.content()) index_lines.append(f"- [{h2.title()}]({filename})") section_paths[h2.title()] = filename if ( h2.title() == "Pre-coding blockers (resolve in order before assigning any module)" ): write_blocker_breakout( h2, section_paths=section_paths, blocker_paths=blocker_paths ) index_lines.append("- [Blockers by question](blockers/README.md)") write_agent_guides( section_paths=section_paths, blocker_paths=blocker_paths, module_paths=module_paths, ) write_file(OUTPUT / "README.md", "\n".join(index_lines).rstrip() + "\n") def main() -> None: text = SOURCE.read_text(encoding="utf-8") preamble, roots = parse_sections(text) build_plan(preamble, roots) print(f"Wrote split plan to {OUTPUT}") if __name__ == "__main__": main()