chore(wiki): add find-stubs script for batch planning
Replaces manual Explore agent searches with a fast Python script that finds unwritten wiki stubs by sector and hop range. Reads star-map.json for topology and wiki stubs for star type/sector/wave. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Executable
+158
@@ -0,0 +1,158 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user