#!/usr/bin/env python3 """ generate-stubs.py Generates wiki stub markdown files for all star systems from star-map.json. Each system gets: wiki/star-systems/{gj_dir}/index.md where {gj_dir} is the astronomical_id with spaces replaced by hyphens. Wiki pages are the SINGLE SOURCE OF TRUTH for all per-system data. star-map.json provides initial values for topology/identity fields; once written to wiki, the wiki page is authoritative. Safe to re-run: skips files that already exist (use --force to overwrite). """ import argparse import json from pathlib import Path STAR_MAP_PATH = Path(__file__).parent.parent.parent / "docs/design/star-map.json" WIKI_DIR = Path(__file__).parent.parent.parent / "wiki/star-systems" def gj_to_dirname(astronomical_id: str) -> str: """Convert 'GJ 71' to 'GJ-71', '2MASS J...' to '2MASS-J...'.""" return astronomical_id.replace(" ", "-") def render_stub(node: dict, adjacent_ids: list[str]) -> str: """Render a wiki stub with ALL fields in frontmatter.""" sid = node["system_id"] astro_id = node["astronomical_id"] proper_name = node.get("proper_name") system_name = proper_name if proper_name else node.get("system_name", f"System {sid}") star_type = node.get("star_type", "~") sector = node.get("geographic_sector", "~") band = node.get("geographic_band", "~") zone = node.get("political_zone", "~") wave = node.get("settlement_wave", "~") horizon = node.get("aperture_count", 0) > 0 aperture = node.get("aperture_count", 0) connections = node.get("gate_connections", 0) topology = node.get("gate_topology", "~") earth_prox = node.get("earth_proximity", "~") hop = node.get("hop_distance_from_gateway", "~") dist_ly = node.get("dist_ly", 0) spectral = node.get("spectral_class", "~") # Format proper_name for YAML proper_yaml = f'"{proper_name}"' if proper_name else "~" # Adjacent systems list adjacent_str = ", ".join(adjacent_ids) if adjacent_ids else "none" return f"""--- # {astro_id} — {system_name} # Star Systems Wiki — The Settled Reach # Source of truth for all per-system data. # Topology graph (edges) lives in star-map.json. # I. Identity and Location system_id: {sid} astronomical_id: "{astro_id}" proper_name: {proper_yaml} system_name: "{system_name}" star_type: {star_type} spectral_class: "{spectral}" dist_ly: {dist_ly} geographic_sector: {sector} geographic_band: {band} political_zone: {zone} # II. Physical Character habitable_planet_count: ~ inhabited_planet_count: ~ asteroid_belt: ~ gas_giant: ~ habitability_profile: ~ # III. Gate Infrastructure horizon_station: {str(horizon).lower()} aperture_count: {aperture} gate_connections: {connections} gate_topology: {topology} span_gate_network: ~ # IV. Settlement History settlement_wave: {wave} founding_motivation: ~ founding_culture_primary: ~ founding_culture_secondary: ~ cultural_persistence: ~ historical_events: [] religious_status: ~ religious_generation_count: ~ # V. Economic Life economic_tier: ~ population: ~ economic_base_primary: ~ economic_base_secondary: ~ distribution_index: ~ imprint_access: ~ # VI. Governance governance_type: ~ dominant_faction: ~ primary_fault_line: ~ secondary_fault_line: ~ # VII. Faction Presence assembly_presence: ~ commission_presence: ~ syndic_presence: ~ syndic_type: ~ separatist_presence: ~ separatist_type: ~ institute_presence: ~ guardians_presence: ~ # VIII. Cultural Voice cultural_register: ~ cultural_register_secondary: ~ ambient_anxiety: ~ local_pride: ~ atmospheric_tone: ~ atmospheric_tone_secondary: ~ active_situation: ~ silence_threshold: ~ # IX. Political Character earth_alignment: ~ earth_proximity: {earth_prox} earth_tension: ~ # X. Narrative Profile primary_archetype: ~ secondary_archetype: ~ narrative_notable: ~ generation_priority: ~ # XI. Content Notes stability_index: ~ system_volatility: ~ cultural_corridor: ~ --- # {system_name} **{astro_id}** | {sid} | {star_type}-type | {sector} {band} ## Supply Dependency ## Faction Notes ## Silence Topic ## Narrative Hook ## Calibration Note ## Topology - **Hop Distance from Gateway:** {hop} - **Adjacent Systems:** {adjacent_str} """ def main(): parser = argparse.ArgumentParser( description="Generate wiki stub files from star-map.json" ) parser.add_argument( "--force", action="store_true", help="Overwrite existing index.md files (never touches subdirectories)", ) args = parser.parse_args() with open(STAR_MAP_PATH, "r") as f: data = json.load(f) nodes = data["nodes"] edges = data["edges"] # Build adjacency map: system_id -> list of connected system_ids adj: dict[str, list[str]] = {n["system_id"]: [] for n in nodes} for a, b in edges: adj[a].append(b) adj[b].append(a) # Sort adjacency lists for deterministic output for sid in adj: adj[sid].sort() created = 0 overwritten = 0 skipped = 0 for node in sorted(nodes, key=lambda n: n["system_id"]): sid = node["system_id"] astro_id = node["astronomical_id"] dirname = gj_to_dirname(astro_id) outdir = WIKI_DIR / dirname outfile = outdir / "index.md" if outfile.exists() and not args.force: skipped += 1 continue existed = outfile.exists() outdir.mkdir(parents=True, exist_ok=True) adjacent = adj.get(sid, []) content = render_stub(node, adjacent) outfile.write_text(content) if existed: overwritten += 1 else: created += 1 print(f"Wiki stubs: {created} created, {overwritten} overwritten, {skipped} skipped") print(f"Output directory: {WIKI_DIR}") # Print a few examples print(f"\nExample directories:") dirs = sorted(WIKI_DIR.iterdir())[:5] for d in dirs: if d.is_dir(): idx = d / "index.md" size = idx.stat().st_size if idx.exists() else 0 print(f" {d.name}/index.md ({size} bytes)") if __name__ == "__main__": main()