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