Files
settled-reach/tooling/migrate-s-to-gj.py
T
jpmschweitzerandClaude Opus 4.6 e5911838c0 chore(tooling): update tooling for GJ ID migration and wiki sync
Update wiki_sync.py and process-wiki-system-changes to remove
astronomical_id references. Add migrate-s-to-gj.py migration script.
Update generate-stubs.py and validate-ron for new schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 03:55:01 +01:00

252 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""Migrate all S-number references to GJ astronomical IDs.
Replaces S-XXX system identifiers with GJ XXX astronomical IDs across:
- star-map.json (nodes and edges)
- systems.db (all tables)
- wiki pages (headers and topology sections)
- catalog-index.md
Usage: python3 tooling/migrate-s-to-gj.py [--dry-run]
"""
import json
import os
import re
import sqlite3
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
STAR_MAP = os.path.join(ROOT, "docs", "design", "star-map.json")
SYSTEMS_DB = os.path.join(ROOT, "server", "data", "systems.db")
SCHEMA_SQL = os.path.join(ROOT, "server", "data", "systems-schema.sql")
WIKI_DIR = os.path.join(ROOT, "wiki", "star-systems")
CATALOG = os.path.join(ROOT, "wiki", "catalog-index.md")
dry_run = "--dry-run" in sys.argv
def build_mapping():
"""Build S-number -> GJ ID mapping from systems.db or star-map.json."""
# Try DB first (has both system_id and astronomical_id)
if os.path.exists(SYSTEMS_DB):
conn = sqlite3.connect(SYSTEMS_DB)
cur = conn.cursor()
cur.execute("SELECT system_id, astronomical_id FROM star_systems WHERE system_id LIKE 'S-%'")
rows = cur.fetchall()
conn.close()
if rows:
mapping = {r[0]: r[1] for r in rows}
with open(STAR_MAP) as f:
data = json.load(f)
return mapping, data
# Fall back to star-map.json
with open(STAR_MAP) as f:
data = json.load(f)
mapping = {}
for node in data["nodes"]:
sid = node["system_id"]
aid = node.get("astronomical_id")
if aid and sid.startswith("S-"):
mapping[sid] = aid
return mapping, data
def migrate_star_map(mapping, data):
"""Replace S-numbers with GJ IDs in star-map.json."""
print(f"Migrating star-map.json ({len(data['nodes'])} nodes, {len(data['edges'])} edges)...")
# Update nodes
for node in data["nodes"]:
old_id = node["system_id"]
new_id = mapping.get(old_id, old_id)
node["system_id"] = new_id
# Remove astronomical_id field (now redundant with system_id)
if "astronomical_id" in node:
del node["astronomical_id"]
# Update edges
new_edges = []
for edge in data["edges"]:
new_edge = [mapping.get(e, e) for e in edge]
new_edges.append(new_edge)
data["edges"] = new_edges
# Update meta references
if "_meta" in data:
meta = data["_meta"]
# Replace S-number references in meta strings
for key, val in meta.items():
if isinstance(val, str):
for sid, gj in mapping.items():
val = val.replace(sid, gj)
meta[key] = val
if not dry_run:
with open(STAR_MAP, "w") as f:
json.dump(data, f, indent=2)
print(" Written.")
else:
print(" (dry run)")
def migrate_database(mapping):
"""Replace S-numbers with GJ IDs in systems.db."""
if not os.path.exists(SYSTEMS_DB):
print("Database not found, skipping.")
return
print(f"Migrating systems.db...")
conn = sqlite3.connect(SYSTEMS_DB)
cur = conn.cursor()
# Get all tables with system_id column
cur.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = [r[0] for r in cur.fetchall()]
tables_with_sid = []
for table in tables:
cur.execute(f"PRAGMA table_info({table})")
cols = [r[1] for r in cur.fetchall()]
if "system_id" in cols:
tables_with_sid.append(table)
if dry_run:
for table in tables_with_sid:
cur.execute(f"SELECT COUNT(*) FROM {table}")
count = cur.fetchone()[0]
print(f" Would migrate {table} ({count} rows)")
conn.close()
return
# Must update in dependency order: children first, then parent
# Temporarily disable foreign key checks
cur.execute("PRAGMA foreign_keys = OFF")
# star_systems is the parent table
parent = "star_systems"
children = [t for t in tables_with_sid if t != parent]
# Update children first
for table in children:
cur.execute(f"SELECT DISTINCT system_id FROM {table}")
ids = [r[0] for r in cur.fetchall()]
for old_id in ids:
new_id = mapping.get(old_id, old_id)
if new_id != old_id:
cur.execute(f"UPDATE {table} SET system_id = ? WHERE system_id = ?",
(new_id, old_id))
# Update parent
cur.execute(f"SELECT system_id FROM {parent}")
ids = [r[0] for r in cur.fetchall()]
for old_id in ids:
new_id = mapping.get(old_id, old_id)
if new_id != old_id:
cur.execute(f"UPDATE {parent} SET system_id = ? WHERE system_id = ?",
(new_id, old_id))
cur.execute("PRAGMA foreign_keys = ON")
conn.commit()
conn.close()
print(f" Migrated {len(tables_with_sid)} tables.")
def migrate_wiki_pages(mapping):
"""Replace S-numbers in wiki page headers and topology sections."""
print(f"Migrating wiki pages...")
count = 0
# Build reverse: need to match S-numbers in text
# Sort by length descending to avoid partial matches (S-10 before S-1)
sorted_sids = sorted(mapping.keys(), key=lambda x: -len(x))
for gj_dir in os.listdir(WIKI_DIR):
index_path = os.path.join(WIKI_DIR, gj_dir, "index.md")
if not os.path.isfile(index_path):
continue
with open(index_path) as f:
content = f.read()
original = content
# Replace S-numbers with GJ IDs
for sid in sorted_sids:
gj = mapping[sid]
# Replace in header line (e.g., "**GJ 570A** | S-235 | M-type | core")
# Remove the "| S-XXX " segment entirely since GJ is already in the header
content = re.sub(
r'\| ' + re.escape(sid) + r' \|',
'|',
content
)
# Replace standalone S-number references (in topology, prose, etc.)
# But be careful not to match partial (S-10 in S-100)
content = re.sub(
r'\b' + re.escape(sid) + r'\b',
gj,
content
)
if content != original:
count += 1
if not dry_run:
with open(index_path, "w") as f:
f.write(content)
print(f" {'Would modify' if dry_run else 'Modified'} {count} wiki pages.")
def migrate_catalog(mapping):
"""Replace S-numbers in catalog-index.md."""
if not os.path.exists(CATALOG):
print("Catalog index not found, skipping.")
return
print("Migrating catalog-index.md...")
with open(CATALOG) as f:
content = f.read()
original = content
sorted_sids = sorted(mapping.keys(), key=lambda x: -len(x))
for sid in sorted_sids:
gj = mapping[sid]
content = re.sub(r'\b' + re.escape(sid) + r'\b', gj, content)
if content != original and not dry_run:
with open(CATALOG, "w") as f:
f.write(content)
print(" Written.")
elif content != original:
print(" (dry run)")
else:
print(" No changes needed.")
def main():
if dry_run:
print("=== DRY RUN ===\n")
mapping, star_map_data = build_mapping()
print(f"Built mapping: {len(mapping)} systems (S-000..S-{len(mapping)-1} -> GJ IDs)\n")
# Show sample
samples = ["S-000", "S-001", "S-010", "S-067", "S-091", "S-120", "S-181", "S-213", "S-235"]
for s in samples:
if s in mapping:
print(f" {s} -> {mapping[s]}")
print()
migrate_star_map(mapping, star_map_data)
migrate_database(mapping)
migrate_wiki_pages(mapping)
migrate_catalog(mapping)
print("\nDone." + (" (dry run — no files changed)" if dry_run else ""))
if __name__ == "__main__":
main()