Files
settled-reach/tooling/archive/wiki-bootstrap/fill-missing-globes.py
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

176 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""
One-shot script: fill missing globe.png files in the wiki by copying a donor
globe from the same planet_class + body_type pool.
Donor selection:
- Hash body_id to pick deterministically from the pool
- Track used donors per system to avoid visual repetition
- Skip oort_cloud and asteroid_belt (handled by generic images)
Run once, then the files are baked into the wiki like authored content.
Usage:
python3 tooling/fill-missing-globes.py [--dry-run]
"""
import hashlib
import shutil
import sqlite3
import sys
from collections import defaultdict
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
WIKI_ROOT = REPO_ROOT / "wiki" / "star-systems"
DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
GENERIC_TYPES = {"oort_cloud", "asteroid_belt"}
GENERIC_DIR = WIKI_ROOT / "_generics"
def globe_path(system_id: str, body_id: str) -> Path:
sys_dir = system_id.replace(" ", "-")
return WIKI_ROOT / sys_dir / "bodies" / body_id / "globe.png"
def body_hash(body_id: str) -> int:
return int(hashlib.sha256(body_id.encode()).hexdigest()[:8], 16)
def main():
dry_run = "--dry-run" in sys.argv
conn = sqlite3.connect(str(DB_PATH))
conn.row_factory = sqlite3.Row
bodies = conn.execute(
"SELECT body_id, system_id, body_type, planet_class FROM bodies"
).fetchall()
# Build pools of existing globes by (body_type, planet_class)
pools: dict[tuple[str, str], list[Path]] = defaultdict(list)
missing: list[dict] = []
for b in bodies:
bid = b["body_id"]
bt = b["body_type"] or "unknown"
pc = (b["planet_class"] or "unknown").lower()
path = globe_path(b["system_id"], bid)
if bt in GENERIC_TYPES:
# Handled by generic images, not donors
if not path.exists():
generic = GENERIC_DIR / bt / "globe.png"
if generic.exists():
missing.append({
"body_id": bid,
"system_id": b["system_id"],
"body_type": bt,
"planet_class": pc,
"donor_path": generic,
"source": "generic",
})
continue
if path.exists():
pools[(bt, pc)].append(path)
else:
missing.append({
"body_id": bid,
"system_id": b["system_id"],
"body_type": bt,
"planet_class": pc,
"donor_path": None,
"source": "donor",
})
# For each missing body, pick a donor
used_per_system: dict[str, set[str]] = defaultdict(set)
assigned = 0
skipped = 0
log_lines = []
for m in missing:
if m["source"] == "generic":
# Generic image copy
target = globe_path(m["system_id"], m["body_id"])
if dry_run:
log_lines.append(
f"[GENERIC] {m['body_id']} <- {m['donor_path'].name} ({m['body_type']})"
)
else:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(m["donor_path"], target)
log_lines.append(
f"[GENERIC] {m['body_id']} <- {m['donor_path']}"
)
assigned += 1
continue
bt = m["body_type"]
pc = m["planet_class"]
key = (bt, pc)
pool = pools.get(key, [])
if not pool:
# Try broader match: same body_type, any class
for (pbt, ppc), p in pools.items():
if pbt == bt and p:
pool = p
break
if not pool:
log_lines.append(
f"[SKIP] {m['body_id']} — no donors for {bt}/{pc}"
)
skipped += 1
continue
# Pick donor by hash, avoid repeats in same system
sys_id = m["system_id"]
h = body_hash(m["body_id"])
pool_size = len(pool)
used = used_per_system[sys_id]
donor = None
for offset in range(pool_size):
candidate = pool[(h + offset) % pool_size]
candidate_key = str(candidate)
if candidate_key not in used:
donor = candidate
used.add(candidate_key)
break
if donor is None:
# All donors used in this system — allow repeat from least-used
donor = pool[h % pool_size]
target = globe_path(sys_id, m["body_id"])
donor_bid = donor.parent.name
if dry_run:
log_lines.append(
f"[DONOR] {m['body_id']} <- {donor_bid} ({bt}/{pc})"
)
else:
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(donor, target)
log_lines.append(
f"[DONOR] {m['body_id']} <- {donor_bid} ({bt}/{pc})"
)
assigned += 1
conn.close()
# Report
prefix = "[DRY RUN] " if dry_run else ""
print(f"{prefix}Assigned: {assigned}, Skipped: {skipped}")
print()
for line in log_lines:
print(line)
if __name__ == "__main__":
main()