#!/usr/bin/env python3 """ Scaffold per-body index.md files from a system index.md. Reads the system's Celestial Bodies table, runs body_definition_parser to produce full body definitions, and writes one index.md per body under wiki/star-systems/{system}/bodies/{body_id}/index.md. The frontmatter IS the body definition — the generator reads it directly. Below the frontmatter is space for authored body content (narrative, notes). Usage: python3 scaffold_bodies.py wiki/star-systems/GJ-144/index.md python3 scaffold_bodies.py wiki/star-systems/GJ-144/index.md --overrides sol_overrides.json python3 scaffold_bodies.py wiki/star-systems/GJ-144/index.md --dry-run Only creates files that don't exist yet — never overwrites authored content. Re-running is safe: existing body index.md files are skipped. """ import argparse import json import os import sys from pathlib import Path # Venv bootstrap TOOLING_DIR = Path(__file__).resolve().parent WORKTREE_ROOT = (TOOLING_DIR / ".." / "..").resolve() _venv_python = WORKTREE_ROOT / ".venv" / "bin" / "python" if _venv_python.exists() and Path(sys.executable).resolve() != _venv_python.resolve(): os.execv(str(_venv_python), [str(_venv_python)] + sys.argv) try: import yaml except ImportError: # PyYAML is in pyproject.toml deps print("error: PyYAML not installed — run `make setup-venv`", file=sys.stderr) sys.exit(1) from body_definition_parser import parse_system def _body_to_frontmatter(bd: dict) -> str: """Convert a body definition dict to clean YAML frontmatter.""" # Order fields for readability ordered = {} for key in ("id", "name", "body_type", "planet_class", "body_scale", "seed"): if key in bd: ordered[key] = bd[key] for section in ("star", "orbit", "physical", "terrain", "environment", "clouds", "render", "gas_giant", "rings"): if section in bd and bd[section] is not None: ordered[section] = bd[section] return yaml.dump(ordered, default_flow_style=False, sort_keys=False, allow_unicode=True).rstrip() def _body_prose(bd: dict, system_dir: Path) -> str: """Generate markdown content below the frontmatter.""" name = bd.get("name") or bd.get("id") bid = bd["id"] pclass = bd.get("planet_class", "unknown").replace("_ringed", "") btype = bd.get("body_type", "planet") wiki = bd.get("wiki", {}) phys = bd.get("physical", {}) orbit = bd.get("orbit", {}) terrain = bd.get("terrain", {}) env = bd.get("environment", {}) # System link (relative path from body dir to system index) system_link = "../../index.md" lines = [f"# {name}", ""] # Type line if btype == "moon": lines.append(f"{pclass.title()} moon.") elif pclass in ("gas_giant", "gas_giant_ringed"): lines.append(f"Gas giant.") else: lines.append(f"{pclass.title()} {btype}.") lines.append("") # System link lines.append(f"**System:** [{system_dir.name}]({system_link})") lines.append("") # Visual overview lines.append("## Visual") lines.append("") lines.append(f"![Globe](globe.png)") lines.append("") if pclass not in ("gas_giant",): lines.append(f"![Heightmap](heightmap.png)") lines.append("") # Profile table lines.append("## Profile") lines.append("") is_gas = pclass in ("gas_giant", "gas_giant_ringed") lines.append("| | |") lines.append("|---|---|") lines.append(f"| **Type** | {btype} |") lines.append(f"| **Class** | {pclass} |") if phys.get("gravity_g") and not is_gas: lines.append(f"| **Gravity** | {phys['gravity_g']}g |") if phys.get("atmosphere") and phys["atmosphere"] != "none": lines.append(f"| **Atmosphere** | {phys['atmosphere']} |") hydro = env.get("hydrosphere") if hydro and hydro not in ("none", "—", ""): lines.append(f"| **Hydrosphere** | {hydro} |") if not is_gas and terrain.get("land_fraction") is not None: lines.append(f"| **Land** | {terrain['land_fraction']*100:.0f}% |") if orbit.get("period_days"): label = "Orbit" if btype == "moon" else "Year" lines.append(f"| **{label}** | {orbit['period_days']:.0f} days |") if wiki.get("inhabited"): lines.append(f"| **Inhabited** | yes |") if wiki.get("population"): lines.append(f"| **Population** | {wiki['population']} |") if wiki.get("economy"): lines.append(f"| **Economy** | {wiki['economy']} |") if wiki.get("settlement"): lines.append(f"| **Settlement** | {wiki['settlement']} |") if wiki.get("industrial"): lines.append(f"| **Industry** | {wiki['industrial']} |") lines.append("") # Content section lines.append("## Description") lines.append("") lines.append("") lines.append("") return "\n".join(lines) def main(): parser = argparse.ArgumentParser( description="Scaffold per-body index.md files from a system index.md") parser.add_argument("system_index", help="Path to system index.md") parser.add_argument("--overrides", help="Per-body overrides JSON") parser.add_argument("--dry-run", action="store_true", help="Print what would be created without writing") args = parser.parse_args() system_path = Path(args.system_index) system_dir = system_path.parent bodies_dir = system_dir / "bodies" overrides = {} if args.overrides: with open(args.overrides) as f: overrides = json.load(f) body_defs = parse_system(str(system_path), overrides=overrides) print(f"System: {system_path} — {len(body_defs)} renderable bodies") created = 0 skipped = 0 for bd in body_defs: body_id = bd["id"] body_dir = bodies_dir / body_id index_path = body_dir / "index.md" if index_path.exists(): print(f" skip {body_id} — index.md exists") skipped += 1 continue frontmatter = _body_to_frontmatter(bd) prose = _body_prose(bd, system_dir) content = f"---\n{frontmatter}\n---\n\n{prose}" if args.dry_run: print(f" would create {index_path}") print(f" {bd.get('planet_class', '?')} / {bd.get('body_type', '?')}") else: body_dir.mkdir(parents=True, exist_ok=True) with open(index_path, "w") as f: f.write(content) print(f" created {index_path}") created += 1 action = "would create" if args.dry_run else "created" print(f"\n {action} {created}, skipped {skipped}") if __name__ == "__main__": main()