diff --git a/tooling/planet-gen/apply_name_fixes.py b/tooling/planet-gen/apply_name_fixes.py new file mode 100644 index 000000000..94f6652ad --- /dev/null +++ b/tooling/planet-gen/apply_name_fixes.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +""" +apply_name_fixes.py — Apply curated name replacements to markers.json files. + +Usage: + python3 tooling/planet-gen/apply_name_fixes.py [--dry-run] + +Edits name fields only; all geometry (path, center, area_fraction) is preserved. +After running, call generate_atlas.py --body for each body to sync DB. + +Decisions: D-191 (markers.json format, corridor palettes) +""" +import json +import sys +from pathlib import Path + +REPO_ROOT = (Path(__file__).resolve().parent / ".." / "..").resolve() +WIKI = REPO_ROOT / "wiki" / "star-systems" + +DRY_RUN = "--dry-run" in sys.argv + +# --------------------------------------------------------------------------- +# Name replacement tables per body +# Format: { body_dir_key: { "feature_type": { "old_name": "new_name" } } } +# feature_type: cities | rivers | oceans | mountain_ranges | pois +# --------------------------------------------------------------------------- + +FIXES: dict[str, dict[str, dict[str, str]]] = { + + # ----------------------------------------------------------------------- + # GJ 144 — Ran system + # Verified against actual markers.json files (DB is stale). + # ----------------------------------------------------------------------- + + # Kallast (GJ144d) — 2B pop, agricultural, urban_concentrated, ocean world + # Current state (from markers.json): good Nordic naming pass done; residual + # lazy/generic oceans remain. Fix: rename remaining generics, add cross-ref + # with mountain "Seterfjellet" (ocean → "Seterfjord"), fix POI, and rename + # "Aldren Pass" to avoid cross-system collision with Lendel (GJ380c capital + # "Aldren" and river "The Aldren"). + # Narrative hook: "Rán's Landing" city (names the star), "Rán's Run" and + # "Ranfall Beck" rivers (shared Rán stem = cross-referencing done). Adding + # Seterfjord → Seterfjellet link completes the second cross-reference arc. + "GJ-144/bodies/GJ144d": { + "rivers": { + # Avoid cross-system stem collision with Lendel (GJ380c) "Aldren" + "Aldren Pass": "Randalfoss", + }, + "oceans": { + # Still generic — large ocean near mountains → name ties to Seterfjellet + "Boulder Bluff": "Keldmere", + # "High Mesa" for an ocean is nonsensical + "High Mesa": "Seterfjord", + }, + "pois": { + "Summit Point": "Kallast Gate Terminal", + }, + }, + + # Vethis (GJ144e) — 1.2B pop, agricultural, dispersed_rural + # Current state: partial naming pass done. Remaining: cardinals (West Bend), + # earth-echoes (Blue Ridge), generics (Stone Point, Ironside Run, Golden + # Valley, Broad Fork), "Ash" stem overuse (Ashbluff Sea + Ashstone Ridge + + # Ashvale = 3 features), "Iron" stem overuse (Irongate + Ironside Run = 2). + # Fixes cross-reference existing named features: + # Grey- stem: Greywash + Greywash Fork + Greystone Ridge (intentional arc) + # Thorn- stem: Thorncrests + Thornrun (mountain visible from river) + # Kel- stem: Kelbridge + Kelside Run (river serving the city) + # Veth- stem: Vethis + Veth Delta + Veth Mere (planet name echoed in geography) + "GJ-144/bodies/GJ144e": { + "rivers": { + "West Bend": "Greywash Fork", # cardinal → cross-refs Greywash river + "Blue Ridge": "Ashvale Beck", # earth-echo → cross-refs Ashvale city + "Stone Point": "Thornrun", # generic → cross-refs Thorncrests mtn + "Ironside Run": "Kelside Run", # Iron overuse → cross-refs Kelbridge + }, + "oceans": { + # Ashbluff Sea: "Ash" prefix already on Ashvale city + Ashstone Ridge mtn + "Ashbluff Sea": "Veth Mere", # Ash overuse → cross-refs Veth Delta + }, + "mountain_ranges": { + "Golden Valley": "Greymoor Range", # generic → cross-refs Greywash river + "Broad Fork": "Keld Spur", # generic → Nordic "keld" (spring) + # Ashstone Ridge: "Ash" overuse (Ashvale + Veth Mere rename frees slot) + "Ashstone Ridge": "Greystone Ridge", # Ash overuse → cross-refs Greywash + }, + "pois": { + "Ridge Crossing": "Vethis Gate Terminal", + }, + }, + + # ----------------------------------------------------------------------- + # GJ 71 — Tau Ceti system + # Verified against actual markers.json files (DB is stale). + # ----------------------------------------------------------------------- + + # Threshold (GJ71c) — 600M pop, agricultural, urban_concentrated + # Fix: replace "Aethelred" (Anglo-Saxon; breaks the all-Latin-personal-name + # pattern of the six rivers: Octavius, Septimus, Quintus, Valeria, Marcus). + # Narrative: All six rivers named after the original Commission survey team. + "GJ-71/bodies/GJ71c": { + "rivers": { + "Aethelred": "Gaius", + }, + }, + + # Arden (GJ71d) — 500M pop, agricultural, dispersed_rural + # Fix cross-body stem duplicates: + # "Concordia Hall" city → exact stem match with GJ71c ocean "Concordia" + # "Basilica Nova" river → exact name match with GJ71c POI "Basilica Nova" + # GJ71d-1 has been independently updated — Forum Major no longer conflicts. + # Capital "Palaestra" and other names are clean — keep. + "GJ-71/bodies/GJ71d": { + "cities": { + "Concordia Hall": "The Praxis", # Concordia = GJ71c sea + }, + "rivers": { + "Basilica Nova": "Via Principia", # exact match = GJ71c POI name + }, + }, + + # Verantis (GJ71d-1) — 20M pop, transit moon of Arden + # Already updated in a prior pass (capital=Praetorium, mountains all + # renamed to unique Latin institutional names). No changes needed. + # "GJ-71/bodies/GJ71d-1": {}, # skip +} + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def apply_fixes(body_key: str, fixes: dict[str, dict[str, str]]) -> bool: + """Load markers.json for body_key, apply name fixes, write back. Returns True on change.""" + parts = body_key.split("/") + path = WIKI / Path(*parts) / "markers.json" + + if not path.exists(): + print(f" SKIP {body_key}: markers.json not found at {path}") + return False + + with open(path) as f: + markers = json.load(f) + + changed = False + section_map = { + "cities": "cities", + "rivers": "rivers", + "oceans": "oceans", + "mountain_ranges": "mountain_ranges", + "pois": "pois", + } + + for section, name_map in fixes.items(): + feature_list = markers.get(section_map[section], []) + for feature in feature_list: + old_name = feature.get("name", "") + if old_name in name_map: + new_name = name_map[old_name] + if old_name != new_name: + print(f" [{section}] '{old_name}' → '{new_name}'") + feature["name"] = new_name + changed = True + + if changed and not DRY_RUN: + with open(path, "w") as f: + json.dump(markers, f, indent=2) + print(f" Written: {path}") + elif changed and DRY_RUN: + print(f" (dry-run) Would write: {path}") + else: + print(f" No changes for {body_key}") + + return changed + + +def main(): + print(f"\nApplying name fixes{'(dry-run)' if DRY_RUN else ''}\n") + for body_key, fixes in FIXES.items(): + print(f"=== {body_key} ===") + apply_fixes(body_key, fixes) + print() + print("Done.\n") + + +if __name__ == "__main__": + main() diff --git a/tooling/planet-gen/atlas_cohesion_audit.py b/tooling/planet-gen/atlas_cohesion_audit.py new file mode 100644 index 000000000..bfbc64668 --- /dev/null +++ b/tooling/planet-gen/atlas_cohesion_audit.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +""" +atlas_cohesion_audit.py — Quality analysis script for atlas atlas_* tables. + +Runs a set of SQL queries against server/data/systems.db atlas_* tables to +identify names that need hand-refining: + + 1. Stem duplicates within a system (same root token, different bodies) + 2. Cross-feature same-body near-duplicates (e.g. Aureus River + Aureus Range) + 3. Cardinal direction density per body (Western X / Northern X patterns) + 4. Generic / lazy outputs (Great %, Hilly %, Sector %, The %) + 5. Empty or single-character names + 6. Earth-echo concentration in a given system + +Usage: + python3 tooling/planet-gen/atlas_cohesion_audit.py + python3 tooling/planet-gen/atlas_cohesion_audit.py --system GJ 144 + python3 tooling/planet-gen/atlas_cohesion_audit.py --body GJ144d + +Decisions: D-191 (atlas pipeline, corridor palettes, markers.json format) +""" + +import argparse +import sqlite3 +import sys +from pathlib import Path + +REPO_ROOT = (Path(__file__).resolve().parent / ".." / "..").resolve() +DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" + +CARDINALS = ("North", "South", "East", "West", "Northern", "Southern", + "Eastern", "Western", "Upper", "Lower", "Far", "Kita", "Minami", + "Higashi", "Nishi") + +LAZY_PATTERNS = ( + "Great %", "Hilly %", "Sector %", "Flat %", "High %", + "Big %", "Little %", "Low %", "Deep %", "Old %", + "% Bluff", "% View", "% Gulch", + "Flatland %", "Prairie %", "Desert %", "Canyon %", + "% Crossing", "% Bend", "% Fork", "% Run", "% Creek", + "% Point", "% Pass", "% Peak", "% Ridge", "% Stream", + "% Ground", "% Slope", "% Mesa", +) + +EARTH_ECHOES = ( + "Manchester", "London", "Paris", "Berlin", "Madrid", + "Sierra Nevada", "Blue Ridge", "Appalachian", + "Route %", "Highway %", "Interstate %", + "New York", "Los Angeles", "Chicago", + "Timberline", "Riverbend", "Greenfield", + "Stony Creek", "Iron Creek", "Pine Gulch", "Valley View", +) + + +def get_conn(db_path: Path) -> sqlite3.Connection: + conn = sqlite3.connect(str(db_path)) + conn.row_factory = sqlite3.Row + return conn + + +def section(title: str) -> None: + print(f"\n{'='*70}") + print(f" {title}") + print('='*70) + + +def run_all_features_for_body(conn, body_id: str) -> list[tuple[str, str]]: + """Return (table_label, name) for all named features on a body.""" + features = [] + for table, label in [ + ("atlas_cities", "city"), + ("atlas_rivers", "river"), + ("atlas_oceans", "ocean"), + ("atlas_mountain_ranges", "mountain"), + ("atlas_pois", "poi"), + ]: + rows = conn.execute( + f"SELECT name FROM {table} WHERE body_id=? AND name != ''", (body_id,) + ).fetchall() + for r in rows: + features.append((label, r["name"])) + return features + + +def stem_of(name: str) -> str: + """Extract first significant word as a rough stem.""" + parts = name.replace("The ", "").replace("the ", "").strip().split() + return parts[0].lower() if parts else "" + + +def report_empty_names(conn, system_id: str | None, body_id: str | None) -> None: + section("EMPTY OR SINGLE-CHARACTER NAMES") + where_clauses = [] + params = [] + if body_id: + where_clauses.append("body_id = ?") + params.append(body_id) + elif system_id: + where_clauses.append("body_id IN (SELECT body_id FROM bodies WHERE system_id = ?)") + params.append(system_id) + w = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else "" + found = 0 + for table in ("atlas_cities", "atlas_rivers", "atlas_oceans", + "atlas_mountain_ranges", "atlas_pois"): + rows = conn.execute( + f"SELECT body_id, local_id, name FROM {table} {w} " + f"AND (name = '' OR LENGTH(name) < 2)", params + ).fetchall() + for r in rows: + print(f" [{table}] {r['body_id']}/{r['local_id']}: '{r['name']}'") + found += 1 + if not found: + print(" None found.") + + +def report_generic_lazy(conn, system_id: str | None, body_id: str | None) -> None: + section("GENERIC / LAZY OUTPUTS") + where_body = "" + params_base = [] + if body_id: + where_body = "AND body_id = ?" + params_base = [body_id] + elif system_id: + where_body = "AND body_id IN (SELECT body_id FROM bodies WHERE system_id = ?)" + params_base = [system_id] + + found = 0 + for table, label in [ + ("atlas_cities", "city"), ("atlas_rivers", "river"), + ("atlas_oceans", "ocean"), ("atlas_mountain_ranges", "mountain"), + ("atlas_pois", "poi"), + ]: + hits = {} + for pattern in LAZY_PATTERNS: + rows = conn.execute( + f"SELECT body_id, local_id, name FROM {table} " + f"WHERE name LIKE ? {where_body}", + [pattern] + params_base, + ).fetchall() + for r in rows: + key = f"{r['body_id']}/{r['local_id']}" + if key not in hits: + hits[key] = (r['body_id'], r['local_id'], r['name'], label) + + for key, (bid, lid, name, lbl) in sorted(hits.items()): + print(f" [{lbl}] {bid}/{lid}: '{name}'") + found += 1 + + if not found: + print(" None found.") + + +def report_cardinals(conn, system_id: str | None, body_id: str | None) -> None: + section("CARDINAL DIRECTION NAMES (check if map-orientation correlates)") + where_body = "" + params_base = [] + if body_id: + where_body = "AND body_id = ?" + params_base = [body_id] + elif system_id: + where_body = "AND body_id IN (SELECT body_id FROM bodies WHERE system_id = ?)" + params_base = [system_id] + + by_body: dict[str, list] = {} + for table, label in [ + ("atlas_cities", "city"), ("atlas_rivers", "river"), + ("atlas_oceans", "ocean"), ("atlas_mountain_ranges", "mountain"), + ]: + for cardinal in CARDINALS: + rows = conn.execute( + f"SELECT body_id, local_id, name FROM {table} " + f"WHERE (name LIKE ? OR name LIKE ?) {where_body}", + [f"{cardinal} %", f"% {cardinal} %"] + params_base, + ).fetchall() + for r in rows: + by_body.setdefault(r["body_id"], []).append( + (label, r["local_id"], r["name"]) + ) + + if not by_body: + print(" None found.") + return + for bid, entries in sorted(by_body.items()): + print(f" {bid}: {len(entries)} cardinal name(s)") + for lbl, lid, name in entries: + print(f" [{lbl}] {lid}: '{name}'") + + +def report_earth_echoes(conn, system_id: str | None, body_id: str | None) -> None: + section("EARTH-ECHO CONCENTRATION (allowed but check intentionality)") + where_body = "" + params_base = [] + if body_id: + where_body = "AND body_id = ?" + params_base = [body_id] + elif system_id: + where_body = "AND body_id IN (SELECT body_id FROM bodies WHERE system_id = ?)" + params_base = [system_id] + + by_body: dict[str, list] = {} + for table, label in [ + ("atlas_cities", "city"), ("atlas_rivers", "river"), + ("atlas_oceans", "ocean"), ("atlas_mountain_ranges", "mountain"), + ]: + for pattern in EARTH_ECHOES: + rows = conn.execute( + f"SELECT body_id, local_id, name FROM {table} " + f"WHERE name LIKE ? {where_body}", + [f"%{pattern.replace('%', '')}%"] + params_base, + ).fetchall() + for r in rows: + key = (r["body_id"], label, r["local_id"]) + by_body.setdefault(r["body_id"], []).append( + (label, r["local_id"], r["name"]) + ) + + if not by_body: + print(" None found.") + return + for bid, entries in sorted(by_body.items()): + print(f" {bid}: {len(entries)} earth-echo(s)") + for lbl, lid, name in entries: + print(f" [{lbl}] {lid}: '{name}'") + + +def report_same_body_stem_dupes(conn, system_id: str | None, body_id: str | None) -> None: + section("SAME-BODY CROSS-FEATURE STEM DUPLICATES") + where = "" + params = [] + if body_id: + where = "WHERE body_id = ?" + params = [body_id] + elif system_id: + where = "WHERE system_id = ?" + params = [system_id] + + bodies_q = conn.execute( + f"SELECT body_id FROM bodies {where}", params + ).fetchall() + + found = 0 + for row in bodies_q: + bid = row["body_id"] + features = run_all_features_for_body(conn, bid) + stem_to_features: dict[str, list] = {} + for label, name in features: + s = stem_of(name) + if s and len(s) > 3: + stem_to_features.setdefault(s, []).append((label, name)) + for stem, items in sorted(stem_to_features.items()): + if len(items) > 1: + types = set(i[0] for i in items) + if len(types) > 1: # only flag cross-feature (different types) + print(f" {bid} stem='{stem}':") + for lbl, nm in items: + print(f" [{lbl}] '{nm}'") + found += 1 + if not found: + print(" None found.") + + +def report_cross_body_stem_dupes(conn, system_id: str | None) -> None: + section("CROSS-BODY STEM DUPLICATES WITHIN SYSTEM (same corridor)") + if not system_id: + print(" (requires --system; skipped)") + return + + bodies_q = conn.execute( + "SELECT body_id FROM bodies WHERE system_id = ?", (system_id,) + ).fetchall() + + # Collect all names per table across all bodies + all_names: dict[str, list[tuple[str, str]]] = {} # name -> [(body_id, table)] + for row in bodies_q: + bid = row["body_id"] + for table, label in [ + ("atlas_cities", "city"), ("atlas_rivers", "river"), + ("atlas_oceans", "ocean"), ("atlas_mountain_ranges", "mountain"), + ("atlas_pois", "poi"), + ]: + rows = conn.execute( + f"SELECT name FROM {table} WHERE body_id=? AND name != ''", (bid,) + ).fetchall() + for r in rows: + name = r["name"] + s = stem_of(name) + if s and len(s) > 3: + all_names.setdefault(s, []).append((bid, label, name)) + + found = 0 + for stem, hits in sorted(all_names.items()): + bodies_hit = set(h[0] for h in hits) + if len(bodies_hit) > 1: + print(f" stem='{stem}' appears in {len(bodies_hit)} bodies:") + for bid, label, name in hits: + print(f" {bid} [{label}] '{name}'") + found += 1 + + if not found: + print(" None found.") + + +def report_summary_score(conn, system_id: str | None, body_id: str | None) -> None: + section("BODY SUMMARY SCORES") + where = "" + params = [] + if body_id: + where = "WHERE body_id = ?" + params = [body_id] + elif system_id: + where = "WHERE system_id = ?" + params = [system_id] + else: + where = "WHERE inhabited = 1" + + bodies_q = conn.execute( + f"SELECT body_id, proper_name, population FROM bodies {where}", params + ).fetchall() + + print(f" {'body_id':<20} {'name':<20} {'pop':<12} cities rivers oceans mounts pois") + for row in bodies_q: + bid = row["body_id"] + name = row["proper_name"] or "" + pop = row["population"] or 0 + + counts = {} + for table, key in [ + ("atlas_cities", "c"), ("atlas_rivers", "r"), + ("atlas_oceans", "o"), ("atlas_mountain_ranges", "m"), ("atlas_pois", "p"), + ]: + n = conn.execute( + f"SELECT COUNT(*) FROM {table} WHERE body_id=?", (bid,) + ).fetchone()[0] + counts[key] = n + + print( + f" {bid:<20} {name:<20} {pop:<12,} " + f"{counts['c']:>5} {counts['r']:>6} {counts['o']:>6} " + f"{counts['m']:>6} {counts['p']:>4}" + ) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--system", help="GJ catalog ID (e.g. 'GJ 144')") + parser.add_argument("--body", help="Body ID (e.g. 'GJ144d')") + parser.add_argument("--db", default=str(DB_PATH), help="Path to systems.db") + args = parser.parse_args() + + db = Path(args.db) + if not db.exists(): + print(f"error: database not found at {db}", file=sys.stderr) + sys.exit(1) + + conn = get_conn(db) + + system_id = args.system + body_id = args.body + + if body_id and not system_id: + row = conn.execute( + "SELECT system_id FROM bodies WHERE body_id=?", (body_id,) + ).fetchone() + if row: + system_id = row["system_id"] + + print(f"\nAtlas Cohesion Audit") + print(f" DB: {db}") + if system_id: + print(f" System: {system_id}") + if body_id: + print(f" Body: {body_id}") + + report_summary_score(conn, system_id, body_id) + report_empty_names(conn, system_id, body_id) + report_generic_lazy(conn, system_id, body_id) + report_cardinals(conn, system_id, body_id) + report_earth_echoes(conn, system_id, body_id) + report_same_body_stem_dupes(conn, system_id, body_id) + if system_id and not body_id: + report_cross_body_stem_dupes(conn, system_id) + + conn.close() + print("\nDone.\n") + + +if __name__ == "__main__": + main() diff --git a/tooling/planet-gen/refine_log_849.md b/tooling/planet-gen/refine_log_849.md new file mode 100644 index 000000000..6b7eccd1d --- /dev/null +++ b/tooling/planet-gen/refine_log_849.md @@ -0,0 +1,186 @@ +# Hand-Refine Log — Ticket #849 +# Core-world atlas cohesion pass (Sprint 36) + +Analyst: paula +Date: 2026-04-19 + +--- + +## Analysis infrastructure + +Created `tooling/planet-gen/atlas_cohesion_audit.py` — reusable SQL audit script for the +atlas_* tables. Checks: empty/single-char names, generic/lazy outputs (% Fork, % Run, etc.), +cardinal direction density, earth-echo concentration, same-body cross-feature stem duplicates, +cross-body stem duplicates within a system. Accepts `--system`, `--body`, `--db` flags. + +Created `tooling/planet-gen/apply_name_fixes.py` — applies curated name replacement tables to +markers.json files (name fields only; geometry preserved). `--dry-run` supported. After running, +caller must sync DB via `generate_atlas.py --body `. + +--- + +## Bodies touched + +### GJ 144 — Ran system + +#### GJ144d — Kallast (2B pop, urban_concentrated, ocean world) + +**Priority reason:** Highest-population body in the Ran system; repeated player destination. +Prior naming pass had done good Nordic groundwork (Rán's Landing, Seterfjellet, Rán's Run/Ranfall +Beck cross-reference arc) but left residual generic oceans and one cross-system stem collision. + +**Changes:** + +| Feature type | Old name | New name | Reason | +|---|---|---|---| +| river | Aldren Pass | Randalfoss | "Aldren" stem collides with Lendel (GJ380c) capital + river "The Aldren" — cross-system noise | +| ocean | Boulder Bluff | Keldmere | Generic; "Boulder Bluff" is a geographic descriptor for a landform, not a sea | +| ocean | High Mesa | Seterfjord | "High Mesa" is nonsensical for an ocean; renamed to cross-ref mountain "Seterfjellet" | +| poi | Summit Point | Kallast Gate Terminal | Generic; terminal should reference the capital city name | + +**Cross-reference arcs post-fix:** +- Rán-: Rán's Landing (city), Rán's Run (river), Randalfoss (river), Ranfall Beck (river), Rán Sea (ocean) +- Seter-: Seterfjellet (mountain), Seterfjord (ocean) +- Keld-: Keldmere (ocean), Timber Keld (river) + +**DB sync:** `generate_atlas.py --body GJ144d` ✓ + +--- + +#### GJ144e — Vethis (1.2B pop, agricultural, dispersed_rural) + +**Priority reason:** Second-most-populated Ran body; the rural agricultural contrast to Kallast. +Prior naming pass was partial — extensive cardinals (West Bend), earth-echoes (Blue Ridge), +overused stems (Ash-: 3 features; Iron-: 2 features), and many pure generics (Golden Valley, +Broad Fork, Stone Point). + +**Changes:** + +| Feature type | Old name | New name | Reason | +|---|---|---|---| +| river | West Bend | Greywash Fork | Cardinal → cross-refs Greywash river (shared Grey- stem arc) | +| river | Blue Ridge | Ashvale Beck | Earth-echo → cross-refs Ashvale city (shared Ash- stem arc) | +| river | Stone Point | Thornrun | Generic → cross-refs Thorncrests mountain (shared Thorn- arc) | +| river | Ironside Run | Kelside Run | Iron- overuse → cross-refs Kelbridge city (shared Kel- arc) | +| ocean | Ashbluff Sea | Veth Mere | Ash- overuse (3 Ash- names) → cross-refs Veth Delta river (shared Veth- arc) | +| mountain | Golden Valley | Greymoor Range | Generic → extends Grey- arc | +| mountain | Broad Fork | Keld Spur | Generic → Nordic compound (keld = spring/source) | +| mountain | Ashstone Ridge | Greystone Ridge | Ash- overuse → extends Grey- arc | +| poi | Ridge Crossing | Vethis Gate Terminal | Generic → terminal named for the planet | + +**Cross-reference arcs post-fix:** +- Grey-: Greywash (river), Greywash Fork (river), Greymoor Range (mountain), Greystone Ridge (mountain) +- Thorn-: Thorncrests (mountain), Thornrun (river) +- Kel-: Kelbridge (city), Kelside Run (river), Keld Spur (mountain) +- Veth-: Vethis (planet), Veth Delta (river), Veth Mere (ocean) +- Ash-: Ashvale (city), Ashvale Beck (river) ← 2 names, was 3 before fix + +**DB sync:** `generate_atlas.py --body GJ144e` ✓ + +--- + +### GJ 71 — Tau Ceti system + +#### GJ71c — Threshold (600M pop, agricultural, urban_concentrated) + +**Priority reason:** Tau Ceti's primary inhabited body; high player footfall. +All six rivers were named after the original Commission survey team — a strong narrative hook +("Latin personal names = founding scientists"). "Aethelred" broke the pattern (Anglo-Saxon, +wrong naming tradition). + +**Changes:** + +| Feature type | Old name | New name | Reason | +|---|---|---|---| +| river | Aethelred | Gaius | Anglo-Saxon name breaks all-Latin river pattern (Octavius, Septimus, Quintus, Valeria, Marcus, Gaius) | + +**DB sync:** `generate_atlas.py --body GJ71c` ✓ + +--- + +#### GJ71d — Arden (500M pop, agricultural, dispersed_rural) + +**Priority reason:** Tau Ceti's sister body; shares corridor with GJ71c, so cross-body stem +duplicates matter most here. Two collisions found: exact POI name match + ocean/city stem match. + +**Changes:** + +| Feature type | Old name | New name | Reason | +|---|---|---|---| +| city | Concordia Hall | The Praxis | "Concordia" = GJ71c ocean name; cross-body stem collision within same system/corridor | +| river | Basilica Nova | Via Principia | Exact name match with GJ71c POI "Basilica Nova"; direct duplicate across bodies | + +**DB sync:** `generate_atlas.py --body GJ71d` ✓ + +--- + +#### GJ71d-1 — Verantis (20M pop, transit moon) + +**No changes.** Discovered that Verantis was already updated in a prior sprint pass — +mountains renamed to unique Latin institutional names (The Lateranum, The Curia Magna, The Exedra, +The Aedes Sacra, The Macellum, The Oracle, The Comitium, Moeribee), capital updated to "Praetorium". +DB was stale relative to markers.json; synced only. + +**DB sync:** `generate_atlas.py --body GJ71d-1` ✓ + +--- + +## Systems NOT yet touched + +### Sol (GJ 0) — BLOCKED + +Body directories do not exist. `wiki/star-systems/GJ-0/bodies/` has no subdirectories for Earth +(GJ0d), Luna (GJ0d-1), Mars (GJ0e), or other inhabited bodies. `sol_import.py` must be run first +to create body directories and initial markers.json from the real-world geographic data in +`tooling/planet-gen/sol_markers/`. Flagged to team-lead. + +### Barnard's Star (GJ 699) — FLAGGED, not yet addressed + +Audit revealed significant cross-body naming issues on Verada (1.9B pop), a hop-0 system: + +- "Central District" appears as BOTH a GJ699b ocean and GJ699b-1 mountain (exact duplicate) +- "Capitol Heights" appears as GJ699b city and GJ699b-1 mountain +- "Liberty" stem hits 3 separate bodies +- "Meridian" stem hits 3 separate bodies +- "Zenith" stem hits 3 separate bodies +- Predominantly civic/Earth-style naming (Liberty Plaza, Meridian City, Zenith Station) without + system-internal cross-referencing + +Not addressed yet — flagged to team-lead to decide if within #849 scope or a separate ticket. + +### Proxima (GJ 551) — FLAGGED, not yet addressed + +Cross-body stem duplicates found: Basilica, Pantheon, Senate appear on multiple Ancora sibling +bodies. Not addressed — flagged to team-lead. + +--- + +## Audit metrics (before/after comparison) + +Run `python3 tooling/planet-gen/atlas_cohesion_audit.py --system "GJ 144"` to verify Ran +improvement. Run `--system "GJ 71"` for Tau Ceti. Baseline run captured issues above; post-fix +re-run showed all named changes reflected in DB correctly. + +Spot-check verification queries: +```bash +SR_DB_PATH="$(pwd)/server/data/systems.db" tooling/db/sqlite-query \ + "SELECT local_id, name FROM atlas_rivers WHERE body_id = 'GJ144d'" +# Should show "Randalfoss" not "Aldren Pass" + +SR_DB_PATH="$(pwd)/server/data/systems.db" tooling/db/sqlite-query \ + "SELECT local_id, name FROM atlas_cities WHERE body_id = 'GJ71d'" +# Should show "The Praxis" not "Concordia Hall" +``` + +--- + +## Files changed + +- `tooling/planet-gen/atlas_cohesion_audit.py` (new) +- `tooling/planet-gen/apply_name_fixes.py` (new) +- `tooling/planet-gen/refine_log_849.md` (this file) +- `wiki/star-systems/GJ-144/bodies/GJ144d/markers.json` (4 name edits) +- `wiki/star-systems/GJ-144/bodies/GJ144e/markers.json` (9 name edits) +- `wiki/star-systems/GJ-71/bodies/GJ71c/markers.json` (1 name edit) +- `wiki/star-systems/GJ-71/bodies/GJ71d/markers.json` (2 name edits) +- `server/data/systems.db` (synced: GJ144d, GJ144e, GJ71c, GJ71d, GJ71d-1)