#!/usr/bin/env python3 """ find-stubs — Find unwritten wiki system stubs by sector and hop range. Usage: tooling/wiki/find-stubs west_reach 7 9 tooling/wiki/find-stubs south_reach 3 5 --limit 10 tooling/wiki/find-stubs --all-sectors 6 8 Outputs a markdown table with all data needed for batch planning. A page is "written" if it has >36 lines. Data sources: - star-map.json: topology (adjacency, hop distance, apertures, gate_topology) - wiki stubs: star type, sector, settlement wave (from READ-ONLY System Profile) """ import argparse import json import re from pathlib import Path STAR_MAP = Path(__file__).parent.parent.parent / "docs/design/star-map.json" WIKI_DIR = Path(__file__).parent.parent.parent / "wiki/star-systems" STUB_MAX_LINES = 36 def sid_to_dir(system_id: str) -> str: """Convert system_id like 'GJ 103' to directory name 'GJ-103'.""" return system_id.replace(" ", "-") def read_wiki(system_id: str) -> dict | None: """Read star type, sector, wave, and name from a wiki page.""" f = WIKI_DIR / sid_to_dir(system_id) / "index.md" if not f.exists(): return None lines = f.read_text().splitlines() info = {"lines": len(lines), "written": len(lines) > STUB_MAX_LINES} # Line 1: # Name if lines and lines[0].startswith("# "): info["name"] = lines[0][2:].strip() # Line 2: **GJ ID** | star-type | sector if len(lines) > 1: parts = lines[1].split("|") if len(parts) >= 3: info["star"] = parts[1].strip() info["sector"] = parts[2].strip() # Find settlement wave in System Profile table for line in lines: if "wave_" in line or "unsettled" in line: m = re.search(r"(wave_\d|unsettled)", line) if m: info["wave"] = m.group(1) break return info def main(): parser = argparse.ArgumentParser(description="Find unwritten wiki stubs") parser.add_argument("sector", nargs="?", help="Sector (e.g. west_reach)") parser.add_argument("hop_min", nargs="?", type=int, default=0) parser.add_argument("hop_max", nargs="?", type=int, default=99) parser.add_argument("--all-sectors", action="store_true") parser.add_argument("--limit", type=int, default=0) parser.add_argument("--written", action="store_true", help="Show written pages instead of stubs") args = parser.parse_args() if args.all_sectors: if args.sector and args.sector.isdigit(): args.hop_min = int(args.sector) if args.hop_min and args.hop_max == 99: pass # keep default args.sector = None with open(STAR_MAP) as f: data = json.load(f) # Build node lookup and adjacency from star-map.json nodes = {n["system_id"]: n for n in data["nodes"]} adj: dict[str, list[str]] = {n["system_id"]: [] for n in data["nodes"]} for a, b in data["edges"]: adj[a].append(b) adj[b].append(a) results = [] for sid, node in nodes.items(): hop = node.get("hop_distance_from_gateway", 99) if hop < args.hop_min or hop > args.hop_max: continue wiki = read_wiki(sid) if not wiki: continue # Filter by sector sector = wiki.get("sector", "") if args.sector and sector != args.sector: continue # Filter by written status if args.written and not wiki["written"]: continue if not args.written and wiki["written"]: continue # Build neighbor info neighbors = [] for n_sid in sorted(adj.get(sid, [])): n_wiki = read_wiki(n_sid) if n_wiki and n_wiki["written"]: neighbors.append(f"✓{sid_to_dir(n_sid)} ({n_wiki.get('name', '?')})") else: neighbors.append(f"·{sid_to_dir(n_sid)}") results.append({ "hop": hop, "sid": sid, "star": wiki.get("star", "?"), "apertures": node.get("aperture_count", 0), "topology": node.get("gate_topology", "?"), "wave": wiki.get("wave", "?"), "neighbors": neighbors, }) results.sort(key=lambda r: (r["hop"], r["sid"])) if args.limit: results = results[:args.limit] # Output sector_label = args.sector or "all sectors" mode = "written" if args.written else "stubs" print(f"## {mode.title()}: {sector_label} hops {args.hop_min}-{args.hop_max}") print(f"**Found {len(results)} systems**\n") print("| Hop | System | Star | Apt | Topology | Wave | Adjacent |") print("|-----|--------|------|-----|----------|------|----------|") for r in results: adj_str = ", ".join(r["neighbors"]) print(f"| {r['hop']} | {r['sid']} | {r['star']} | {r['apertures']} " f"| {r['topology']} | {r['wave']} | {adj_str} |") # Summary stats if results: by_hop = {} for r in results: by_hop.setdefault(r["hop"], 0) by_hop[r["hop"]] += 1 print(f"\n**By hop:** " + ", ".join(f"hop {h}: {c}" for h, c in sorted(by_hop.items()))) if __name__ == "__main__": main()