#!/usr/bin/env python3 """ Backfill star_systems.cultural_corridor and bodies.cultural_corridor. The schema has a `cultural_corridor` column on both tables, but `wiki_sync.py` never populated it from the wiki index.md files — the cultural/geographic identity of each system lives in `star_systems.geographic_sector` instead (values: core, north_reach, south_reach, east_reach, west_reach, deep_frontier). These two fields refer to the same concept: which arc of the reach the system belongs to. Leaving `cultural_corridor` NULL on 99%+ of rows defeats every downstream consumer that actually wants to filter by corridor (the atlas UI, narrative tools, and the server generation cascade). This script treats `geographic_sector` as the source of truth and copies it into `cultural_corridor`: star_systems.cultural_corridor := star_systems.geographic_sector WHERE cultural_corridor IS NULL bodies.cultural_corridor := parent star_systems.cultural_corridor WHERE bodies.cultural_corridor IS NULL It is safe to re-run — idempotent, NULL-only updates, explicit transaction wrapper so a crash never leaves a half-populated state. Run it after any `wiki_sync.py` pass that creates fresh systems.db rows. Usage: tooling/db/backfill_cultural_corridor.py tooling/db/backfill_cultural_corridor.py --db path/to/systems.db tooling/db/backfill_cultural_corridor.py --dry-run Decisions: D-191 (atlas pipeline — downstream consumer) """ import argparse import sqlite3 import sys from pathlib import Path SCRIPT_DIR = Path(__file__).resolve().parent REPO_ROOT = (SCRIPT_DIR / ".." / "..").resolve() DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" def main(): parser = argparse.ArgumentParser( description="Backfill cultural_corridor on star_systems and bodies" ) parser.add_argument("--db", default=str(DB_PATH), help="Path to systems.db") parser.add_argument( "--dry-run", action="store_true", help="Report counts without writing", ) args = parser.parse_args() db_path = Path(args.db) if not db_path.exists(): print(f"error: {db_path} not found", file=sys.stderr) sys.exit(1) conn = sqlite3.connect(str(db_path)) conn.execute("PRAGMA foreign_keys=ON") # Counts before. before_systems_null = conn.execute( "SELECT COUNT(*) FROM star_systems WHERE cultural_corridor IS NULL" ).fetchone()[0] before_bodies_null = conn.execute( "SELECT COUNT(*) FROM bodies WHERE cultural_corridor IS NULL" ).fetchone()[0] print("\n cultural_corridor backfill") print(f" DB: {db_path}") if args.dry_run: print(" Mode: DRY RUN") print() print(" Before:") print(f" star_systems.cultural_corridor NULL: {before_systems_null}") print(f" bodies.cultural_corridor NULL: {before_bodies_null}") conn.execute("BEGIN") try: # 1. Star systems — copy geographic_sector into cultural_corridor # where the latter is still NULL. If geographic_sector is also # NULL, leave cultural_corridor NULL — there is nothing to # copy and a bogus placeholder is worse than honest NULL. sys_rows_updated = conn.execute( """ UPDATE star_systems SET cultural_corridor = geographic_sector WHERE cultural_corridor IS NULL AND geographic_sector IS NOT NULL """ ).rowcount # 2. Bodies — inherit from the parent star_systems row. body_rows_updated = conn.execute( """ UPDATE bodies SET cultural_corridor = ( SELECT s.cultural_corridor FROM star_systems s WHERE s.system_id = bodies.system_id ) WHERE cultural_corridor IS NULL AND EXISTS ( SELECT 1 FROM star_systems s WHERE s.system_id = bodies.system_id AND s.cultural_corridor IS NOT NULL ) """ ).rowcount if args.dry_run: conn.rollback() print() print(" Would update:") print(f" star_systems: {sys_rows_updated}") print(f" bodies: {body_rows_updated}") print("\n Dry run — no changes written.") else: conn.commit() print() print(" Updated:") print(f" star_systems: {sys_rows_updated}") print(f" bodies: {body_rows_updated}") # Counts after. after_systems_null = conn.execute( "SELECT COUNT(*) FROM star_systems WHERE cultural_corridor IS NULL" ).fetchone()[0] after_bodies_null = conn.execute( "SELECT COUNT(*) FROM bodies WHERE cultural_corridor IS NULL" ).fetchone()[0] print() print(" After:") print(f" star_systems.cultural_corridor NULL: {after_systems_null}") print(f" bodies.cultural_corridor NULL: {after_bodies_null}") # Show the distribution so the outcome is visible. print() print(" star_systems.cultural_corridor distribution:") for corridor, count in conn.execute( "SELECT cultural_corridor, COUNT(*) FROM star_systems " "GROUP BY cultural_corridor ORDER BY COUNT(*) DESC" ).fetchall(): print(f" {corridor!r}: {count}") except BaseException: conn.rollback() conn.close() raise conn.close() print() if __name__ == "__main__": main()