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>
252 lines
7.8 KiB
Python
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()
|