Files
settled-reach/tooling/archive/wiki-bootstrap/find-stubs
T
jpmschweitzerandClaude Opus 5.5 4537b71b92 refactor(tooling): T-1290 — the wiki domain, and the renderer that must not run
`reach wiki stats` and `reach wiki gttr-hook` replace tooling/db/wiki_sync.py
and populate_gttr_hook.py. Both are output-identical to the originals:
`stats` byte-for-byte, and all 301 extracted GTTR hooks line-for-line.

wiki_sync.py moved whole, but generate_wiki() and import_from_wiki() are NOT
verbs. Before porting, the old `--generate` was run against a clean tree to get
a parity baseline. It changed all 301 system pages, +940 / -10,761, and was
reverted at once. It deletes the Celestial Bodies / Stations blocks (owned by
the Rust atlas sync, which it does not know about), deletes the
Industries / Exports / Imports rows (nothing writes those any more), and
rewrites star types where systems.db and the pages disagree. D-262, CLAUDE.md
and the wiki skill all described it as the routine, prose-preserving render.
CLAUDE.md and the skill now say not to run it; D-262 needs amending — T-1292.

Provenance moves to tooling/archive/, with a README naming what each script
did and why it is not run:

- pql-migrate/ (the T-1271 ruling)
- wiki-bootstrap/: assign-astro-ids + its catalog, migrate-s-to-gj,
  patch-core-sector (hardcodes a dead path), fill-missing-globes,
  generate-stubs and find-stubs (finds 0 stubs — Phase 1 is done),
  backfill_cultural_corridor (a raw systems.db patch script, outside D-262),
  and process-wiki-system-changes, whose last step is the destructive render

Also:

- stats() printed "run import first" and exited 0 when a table was missing;
  it now fails with a remedy. generate_wiki() counted created pages after
  writing them, so `created` was always 0.
- tooling/godot-cold-parse and godot-parse-sweep were never retired after
  T-1283, and the pr-process skill still told agents to run them. Removed;
  the skill and parse_sweep.gd now name the reach verbs.
- systems.db re-stamped: schema comments changed, and the stamp records the
  schema file's SHA for tamper detection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:25:51 +02:00

159 lines
5.1 KiB
Python
Executable File

#!/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()