Files
settled-reach/tooling/archive/wiki-bootstrap/generate-stubs.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

243 lines
6.4 KiB
Python

#!/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
<!-- Required for manufacturing/transit_logistics base and narrative_notable systems -->
## Faction Notes
<!-- Significant local factions not covered by standard columns -->
## Silence Topic
<!-- Required for narrative_notable and Institutional Core / Diplomatic Periphery zones -->
## Narrative Hook
<!-- Required if narrative_notable = true. One sentence. -->
## Calibration Note
<!-- Unusual column combinations requiring justification -->
## Topology
<!-- READ-ONLY — regenerated from star-map.json edges -->
- **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("\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()