Merge remote-tracking branch 'origin/sprint-36/copy'

This commit is contained in:
2026-04-19 16:42:57 +02:00
340 changed files with 14191 additions and 778 deletions
+9 -2
View File
@@ -72,6 +72,7 @@ CREATE TABLE IF NOT EXISTS brand_products (
shadow_viable INTEGER NOT NULL DEFAULT 0,
brand_tier TEXT NOT NULL,
halo_brand_id TEXT REFERENCES brand_products(brand_product_id),
price_tier TEXT,
updated_at TEXT DEFAULT (datetime('now'))
);
@@ -177,6 +178,7 @@ COLUMN_MIGRATIONS = [
("corporations", "behavioral_archetype", "TEXT"),
("corporations", "supply_chain_role", "TEXT"),
("corporations", "shadow_economy_access", "INTEGER DEFAULT 0"),
("brand_products", "price_tier", "TEXT"),
]
@@ -743,6 +745,7 @@ VALID_VALUE_TRAJECTORIES = {"appreciating", "depreciating", "timeless"}
VALID_SCARCITY_CLASSES = {"capped", "constrained", "scalable", "unlimited"}
VALID_BRAND_TIERS = {"halo", "volume"}
VALID_CURRENCY_DENOMINATIONS = {"tractus", "mark", "mixed", "sol_adjacent"}
VALID_PRICE_TIERS = {"mass", "premium", "luxury", "flagship", "institutional"}
def import_brands(
@@ -780,6 +783,7 @@ def import_brands(
int(p.get("shadow_viable", False)),
p["brand_tier"],
p.get("halo_brand_id"),
p.get("price_tier"),
))
input_rows = []
@@ -797,8 +801,8 @@ def import_brands(
value_trajectory, scarcity_class, product_subcategory,
base_premium_multiplier, premium_floor, origin_system,
terroir_locked, currency_denomination, shadow_viable,
brand_tier, halo_brand_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
brand_tier, halo_brand_id, price_tier
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
product_rows,
)
conn.executemany(
@@ -947,12 +951,15 @@ def validate_brands(conn: sqlite3.Connection) -> list[str]:
("scarcity_class", VALID_SCARCITY_CLASSES),
("brand_tier", VALID_BRAND_TIERS),
("currency_denomination", VALID_CURRENCY_DENOMINATIONS),
("price_tier", VALID_PRICE_TIERS),
]
for column, valid_set in enum_checks:
bad = conn.execute(
f"SELECT brand_product_id, {column} FROM brand_products"
).fetchall()
for pid, value in bad:
if value is None:
continue # nullable columns (e.g. price_tier) may be unset
if value not in valid_set:
errors.append(
f"V-B06: brand_product '{pid}' has {column}='{value}' — "
+413
View File
@@ -0,0 +1,413 @@
#!/usr/bin/env python3
"""Generate minimal wiki stub pages for corps in brands.toml that don't yet have wiki pages.
Usage:
python3 tooling/generate_corp_stubs.py [--dry-run]
In --dry-run mode, prints which stubs would be created without writing any files.
"""
import argparse
from pathlib import Path
REPO = Path(__file__).parent.parent
BRANDS_TOML = REPO / "wiki/economics/corporations/brands.toml"
CORPS_DIR = REPO / "wiki/corporations"
# Star system ID -> (proper_name, corridor)
SYSTEMS = {
"GJ 0": ("Sol", "sol-gateway-axis"),
"GJ 1": ("Grünfeld", "west_reach"),
"GJ 1002": ("Caparica", "south_reach"),
"GJ 1005A": ("Port Desrochers", "east_reach"),
"GJ 103": ("Bestevaer", "west_reach"),
"GJ 105A": ("Nová Tržnice", "east_reach"),
"GJ 1061": ("Crux Station", "south_reach"),
"GJ 1075": ("Espinho", "south_reach"),
"GJ 1073": ("Mabuhay", "east_reach"),
"GJ 107A": ("Tình Yên", "east_reach"),
"GJ 1111": ("Zenzele", "south_reach"),
"GJ 1116A": ("Mabaso Junction", "north_reach"),
"GJ 1116B": ("Kraków Junction", "west_reach"),
"GJ 1125": ("Hwaseong", "east_reach"),
"GJ 1128": ("Devereux Point", "east_reach"),
"GJ 1156": ("Travessia", "south_reach"),
"GJ 1230A": ("Uusi Vantaa", "north_reach"),
"GJ 1232": ("Gammelstad", "west_reach"),
"GJ 1235": ("Knotenpunkt", "deep_frontier"),
"GJ 124": ("Hạ Long", "east_reach"),
"GJ 1245A": ("Okoro's Claim", "north_reach"),
"GJ 1245B": ("Koeberg", "south_reach"),
"GJ 1248": ("Haltefenn", "west_reach"),
"GJ 127A": ("Dalim", "deep_frontier"),
"GJ 127B": ("Echternach", "west_reach"),
"GJ 1286": ("Kensho", "core"),
"GJ 1289": ("Athabasca", "north_reach"),
"GJ 1292": ("Moyale", "north_reach"),
"GJ 136": ("Dài Lộ", "east_reach"),
"GJ 137": ("Novaya Zarya", "east_reach"),
"GJ 138": ("Sertão", "deep_frontier"),
"GJ 139": ("Okafor Station", "north_reach"),
"GJ 142": ("Leuchtturm", "deep_frontier"),
"GJ 144": ("Ran", "core"),
"GJ 15A": ("Lu Ban", "core"),
"GJ 166A": ("Keid", "core"),
"GJ 172": ("Kaelong", "east_reach"),
"GJ 17": ("Binh Trang", "east_reach"),
"GJ 178": ("Ashfield", "north_reach"),
"GJ 183": ("Waterkant", "north_reach"),
"GJ 19": ("Solheim", "west_reach"),
"GJ 191": ("Kapteyn's Star", "west_reach"),
"GJ 194B": ("Pemberton", "north_reach"),
"GJ 197": ("Puerto Último", "south_reach"),
"GJ 201": ("GJ 201", "deep_frontier"),
"GJ 204": ("Pedra Funda", "deep_frontier"),
"GJ 205": ("Kurashiki", "east_reach"),
"GJ 208": ("Pereira's Rest", "west_reach"),
"GJ 2097": ("Nascente", "south_reach"),
"GJ 211": ("Encruzilhada", "deep_frontier"),
"GJ 213": ("Kaapse Baai", "south_reach"),
"GJ 216A": ("Kelburn Crossing", "north_reach"),
"GJ 216B": ("Gemini Point", "north_reach"),
"GJ 222A": ("Karoo", "south_reach"),
"GJ 229": ("Port Mäkelä", "west_reach"),
"GJ 231": ("Thorvaldsen's Circuit", "west_reach"),
"GJ 234A": ("Carvalhais", "south_reach"),
"GJ 244A": ("Sirius", "core"),
"GJ 250A": ("Carrington's Post", "north_reach"),
"GJ 251": ("Renaissance", "core"),
"GJ 268": ("Yeongwol", "east_reach"),
"GJ 27": ("Okahandja", "south_reach"),
"GJ 273": ("Fremantle", "north_reach"),
"GJ 280A": ("Parallax", "core"),
"GJ 282A": ("Bout du Chemin", "west_reach"),
"GJ 285": ("Fenwick's Lantern", "north_reach"),
"GJ 286": ("Weitblick", "deep_frontier"),
"GJ 299": ("Várzea", "south_reach"),
"GJ 300": ("Dunmore Stillwater", "north_reach"),
"GJ 302": ("Lichtbrücke", "west_reach"),
"GJ 309": ("Vorposten", "deep_frontier"),
"GJ 3135": ("Whitfield Crossing", "east_reach"),
"GJ 320": ("Sonnwend", "west_reach"),
"GJ 3250": ("Rionegro", "south_reach"),
"GJ 325A": ("Purnima", "east_reach"),
"GJ 33": ("96 Piscium", "south_reach"),
"GJ 3323": ("Entroncamento", "south_reach"),
"GJ 3325": ("Calloway Reach", "north_reach"),
"GJ 3333": ("Groenland", "west_reach"),
"GJ 3379": ("Shenwei", "east_reach"),
"GJ 338B": ("Arbour", "core"),
"GJ 3454": ("Eisentor", "west_reach"),
"GJ 349": ("Randgebiet", "deep_frontier"),
"GJ 34A": ("Tanegashima", "north_reach"),
"GJ 34B": ("Yongjin", "east_reach"),
"GJ 35": ("Vuurkloof", "south_reach"),
"GJ 3522": ("Sindri", "core"),
"GJ 3533": ("Trung's Crossing", "east_reach"),
"GJ 3586": ("Eisfeld", "west_reach"),
"GJ 3612": ("Xa Vời", "east_reach"),
"GJ 3618": ("Pemberton's Loop", "north_reach"),
"GJ 3622": ("Havnfjord", "north_reach"),
"GJ 370": ("Traagwater", "west_reach"),
"GJ 3728B": ("Jeonnam", "east_reach"),
"GJ 3737": ("Nyrheim", "west_reach"),
"GJ 380": ("Groombridge", "core"),
"GJ 3820": ("Uusi Vantaa", "north_reach"),
"GJ 3877": ("Tessera", "core"),
"GJ 388": ("The Lantern", "south_reach"),
"GJ 393": ("Encrucijada", "south_reach"),
"GJ 395": ("Confluent", "west_reach"),
"GJ 3991": ("Dunbar", "north_reach"),
"GJ 402": ("Prospect Hill", "north_reach"),
"GJ 4053": ("Dzimbahwe", "south_reach"),
"GJ 406": ("Wolf 359", "north_reach"),
"GJ 408": ("Oude Kraal", "north_reach"),
"GJ 410": ("Wagtoring", "north_reach"),
"GJ 411": ("Lalande", "south_reach"),
"GJ 412A": ("Morales' End", "south_reach"),
"GJ 412B": ("Korhonen", "east_reach"),
"GJ 414A": ("Vindkast", "west_reach"),
"GJ 42": ("Langemark", "west_reach"),
"GJ 423A": ("Marunong", "east_reach"),
"GJ 423B": ("Quilombo", "south_reach"),
"GJ 4274": ("Suzhou", "east_reach"),
"GJ 4285": ("Lam's Prospect", "east_reach"),
"GJ 432A": ("Kaur's Observatory", "east_reach"),
"GJ 435": ("Mirante", "south_reach"),
"GJ 438": ("Relay 7", "east_reach"),
"GJ 442A": ("Torsby", "west_reach"),
"GJ 445": ("Jinghu", "east_reach"),
"GJ 447": ("Ross 128", "core"),
"GJ 449": ("Murakami's Lantern", "east_reach"),
"GJ 451A": ("Groombridge 1830", "south_reach"),
"GJ 453": ("Freiholt", "west_reach"),
"GJ 454": ("Steinfeld", "west_reach"),
"GJ 473A": ("Passagem", "south_reach"),
"GJ 473B": ("Nkosi's Bridge", "north_reach"),
"GJ 475": ("Braemar", "north_reach"),
"GJ 482A": ("Dagat", "east_reach"),
"GJ 49": ("Dernier Quai", "east_reach"),
"GJ 4A": ("Velha Guarda", "south_reach"),
"GJ 501B": ("Candeia", "deep_frontier"),
"GJ 502": ("Meridian", "west_reach"),
"GJ 505A": ("Bergtor", "west_reach"),
"GJ 506": ("Dokkum", "west_reach"),
"GJ 508A": ("Stilwater", "north_reach"),
"GJ 526": ("Neustadt", "west_reach"),
"GJ 529": ("Yttermark", "west_reach"),
"GJ 532": ("Kettenschmied", "deep_frontier"),
"GJ 534": ("Stillvakt", "west_reach"),
"GJ 536": ("Retiro", "south_reach"),
"GJ 53A": ("Bridgepoint", "east_reach"),
"GJ 54": ("Inhambane", "south_reach"),
"GJ 54.1": ("Ribeiro's Star", "south_reach"),
"GJ 541": ("Chandra Deep", "east_reach"),
"GJ 542": ("Kopparhytta", "west_reach"),
"GJ 546": ("Eerste Wacht", "deep_frontier"),
"GJ 551": ("Proxima", "core"),
"GJ 555": ("Tanaka Station", "east_reach"),
"GJ 556": ("Recanto", "deep_frontier"),
"GJ 559B": ("ACB", "core"),
"GJ 566A": ("Haodu", "east_reach"),
"GJ 566B": ("Confluência", "south_reach"),
"GJ 567": ("Fragua", "south_reach"),
"GJ 570A": ("Bastion", "core"),
"GJ 575A": ("Ratnagiri", "north_reach"),
"GJ 581": ("Rødvik", "west_reach"),
"GJ 588": ("Harrowfield", "north_reach"),
"GJ 598": ("Helle Wacht", "deep_frontier"),
"GJ 601A": ("Ostmark", "west_reach"),
"GJ 603": ("Mossbank", "north_reach"),
"GJ 615": ("Dunkelholz", "west_reach"),
"GJ 624": ("Halvøy", "west_reach"),
"GJ 625": ("Songjiang", "east_reach"),
"GJ 628": ("Brennan's Drift", "west_reach"),
"GJ 631": ("Bakshi Spur", "west_reach"),
"GJ 635A": ("Stilmark", "west_reach"),
"GJ 635B": ("Clearwater Station", "east_reach"),
"GJ 638": ("Greymantle", "north_reach"),
"GJ 643": ("Marktfeld", "west_reach"),
"GJ 644A": ("Mavela", "south_reach"),
"GJ 644C": ("Moçâmedes", "south_reach"),
"GJ 653": ("Dunmore", "north_reach"),
"GJ 660A": ("Seongho", "east_reach"),
"GJ 661A": ("Crown's Hollow", "north_reach"),
"GJ 661B": ("Breëvlei", "north_reach"),
"GJ 663A": ("Johannsen's Star", "north_reach"),
"GJ 664": ("Caldwell Point", "west_reach"),
"GJ 667A": ("New Ballarat", "north_reach"),
"GJ 667B": ("Kampala Gate", "north_reach"),
"GJ 667C": ("Gliese 667", "east_reach"),
"GJ 66A": ("p Eridani", "east_reach"),
"GJ 66B": ("Voss", "west_reach"),
"GJ 67": ("Roça Nova", "south_reach"),
"GJ 672": ("Nova Estrada", "deep_frontier"),
"GJ 673": ("Lindström Station", "north_reach"),
"GJ 674": ("Meridian", "core"),
"GJ 675": ("Neugrund", "deep_frontier"),
"GJ 680": ("Carnarvon", "north_reach"),
"GJ 682": ("Taishan", "east_reach"),
"GJ 684A": ("Sonnenkap", "deep_frontier"),
"GJ 684B": ("Paso Largo", "deep_frontier"),
"GJ 687": ("Matsue", "east_reach"),
"GJ 688": ("Aguas Bravas", "south_reach"),
"GJ 693": ("Mwangaza", "south_reach"),
"GJ 699": ("Barnard's Star", "core"),
"GJ 702B": ("Prometheus", "core"),
"GJ 706": ("Ngata's Star", "north_reach"),
"GJ 716": ("Murchison", "north_reach"),
"GJ 71": ("Tau Ceti", "core"),
"GJ 713A": ("Samuels Reach", "east_reach"),
"GJ 722": ("Skuilplek", "deep_frontier"),
"GJ 725B": ("Struve", "core"),
"GJ 729": ("Ross 154", "west_reach"),
"GJ 737B": ("Punta del Sol", "south_reach"),
"GJ 75": ("Enugu", "north_reach"),
"GJ 752A": ("Ribeirão", "south_reach"),
"GJ 754": ("Miryang", "east_reach"),
"GJ 764": ("Nova Roma", "core"),
"GJ 768": ("Altair", "south_reach"),
"GJ 770": ("Rekohu", "north_reach"),
"GJ 771A": ("Alshain", "west_reach"),
"GJ 775": ("Randsholm", "west_reach"),
"GJ 780": ("Puerto Rivera", "north_reach"),
"GJ 783A": ("Aurelius", "core"),
"GJ 784": ("Kovács Loop", "east_reach"),
"GJ 785": ("Porto Fechado", "south_reach"),
"GJ 79": ("Oshima", "east_reach"),
"GJ 798": ("Brückenau", "west_reach"),
"GJ 807": ("Grenzstein", "west_reach"),
"GJ 809": ("Seiðrmaðr", "west_reach"),
"GJ 820B": ("Cygni B", "core"),
"GJ 825": ("Lacaille", "north_reach"),
"GJ 827": ("Solandar", "south_reach"),
"GJ 829": ("Posto Avançado", "south_reach"),
"GJ 83.1": ("Farol", "south_reach"),
"GJ 832": ("Hansestadt", "west_reach"),
"GJ 845": ("Cairn", "north_reach"),
"GJ 848": ("Shimanami", "east_reach"),
"GJ 86": ("Kumasi", "south_reach"),
"GJ 860A": ("Kruger 60", "south_reach"),
"GJ 860B": ("Changwon", "east_reach"),
"GJ 866A": ("EZ Aqr", "east_reach"),
"GJ 873": ("Ashworth Junction", "north_reach"),
"GJ 875": ("The Narrows", "deep_frontier"),
"GJ 876": ("Koskinen's Reach", "north_reach"),
"GJ 877": ("Holmgren's Passage", "north_reach"),
"GJ 879": ("Singh's Landing", "north_reach"),
"GJ 880": ("Holgersen's Landing", "north_reach"),
"GJ 884": ("Matamba", "south_reach"),
"GJ 887": ("Lacaille", "core"),
"GJ 892": ("Cairnside", "deep_frontier"),
"GJ 896A": ("Nowa Huta", "south_reach"),
"GJ 905": ("Isibaya", "south_reach"),
"GJ 908": ("Thornfield Station", "east_reach"),
"GJ 909A": ("Tam Giang", "east_reach"),
"GJ 92": ("Pollux", "south_reach"),
"GJ 95": ("Caledonia's End", "north_reach"),
}
# Brand category -> description template suffix and tags hint
CATEGORY_META = {
"terroir": ("regional specialty producer", ["terroir", "scarcity_class_rare"]),
"heritage_craft": ("heritage craft manufacturer", ["heritage_craft", "artisan"]),
"tech_premium": ("precision technology manufacturer", ["tech_premium", "electronics"]),
"cultural": ("cultural content producer", ["cultural", "entertainment"]),
"service_premium": ("premium service provider", ["service_premium"]),
"commodity_branded": ("consumer goods manufacturer", ["commodity_branded", "consumer_goods"]),
"design_heritage": ("design and furniture manufacturer", ["design_heritage", "consumer_goods"]),
"platform_catalogue": ("media platform and catalogue service", ["platform_catalogue", "entertainment"]),
}
def slug_to_title(slug: str) -> str:
"""Convert a slug like 'my-corp-name' to 'My Corp Name'."""
return " ".join(w.capitalize() for w in slug.replace("-", " ").split())
def parse_brands_toml(path: Path) -> dict:
"""Parse brands.toml and return dict of corp_id -> {origin_system, brand_category}."""
corps = {}
current_corp = None
current_system = None
current_category = None
with open(path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line == "[[brand_products]]":
# Save previous
if current_corp and current_system:
if current_corp not in corps:
corps[current_corp] = {
"origin_system": current_system,
"brand_category": current_category,
}
current_corp = None
current_system = None
current_category = None
elif line.startswith("corp_id = "):
current_corp = line.split("=", 1)[1].strip().strip('"')
elif line.startswith("origin_system = "):
current_system = line.split("=", 1)[1].strip().strip('"')
elif line.startswith("brand_category = "):
current_category = line.split("=", 1)[1].strip().strip('"')
# Save last
if current_corp and current_system:
if current_corp not in corps:
corps[current_corp] = {
"origin_system": current_system,
"brand_category": current_category,
}
return corps
def generate_stub(slug: str, origin_system: str, brand_category: str) -> str:
title = slug_to_title(slug)
sys_info = SYSTEMS.get(origin_system, (origin_system, "unknown"))
sys_name, corridor = sys_info
cat_meta = CATEGORY_META.get(brand_category, ("commercial enterprise", []))
cat_desc, cat_tags = cat_meta
corridor_currency = "tractus"
if corridor in ("west_reach",):
corridor_currency = "mark"
elif corridor == "core":
corridor_currency = "tractus"
headquarters = f"{sys_name} ({origin_system})"
tags_list = [brand_category, corridor, corridor_currency] + cat_tags
# Deduplicate while preserving order
seen = set()
tags = []
for t in tags_list:
if t not in seen:
seen.add(t)
tags.append(t)
tags_str = "[" + ", ".join(tags) + "]"
description = f"{title} — {cat_desc} based in {sys_name}, {corridor.replace('_', ' ')}"
return f"""---
title: "{title}"
description: "{description}"
slug: {slug}
category: corporation
status: canonical
created: 2026-04-19
updated: 2026-04-19
scope: regional
faction_type: economic
headquarters: {headquarters}
tags: {tags_str}
decision_refs: [D-189]
cross_refs: []
---
# {title}
**Type:** Corporation — {cat_desc.title()}
**Status:** Canonical
**Scope:** Regional — {corridor.replace('_', ' ')} operations
**Headquarters:** {headquarters}
{title} is a {cat_desc} operating out of {sys_name} ({origin_system}). Details to be expanded.
"""
def main():
parser = argparse.ArgumentParser(
description="Generate minimal wiki stub pages for corps in brands.toml."
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print which stubs would be created without writing any files.",
)
args = parser.parse_args()
corps = parse_brands_toml(BRANDS_TOML)
print(f"Found {len(corps)} unique corp_ids in brands.toml")
if args.dry_run:
print("(dry-run — no files will be written)\n")
created = 0
skipped = 0
for slug, meta in sorted(corps.items()):
stub_path = CORPS_DIR / f"{slug}.md"
if stub_path.exists():
skipped += 1
continue
if args.dry_run:
print(f" (dry-run) Would create: {slug}.md")
else:
content = generate_stub(slug, meta["origin_system"], meta["brand_category"])
stub_path.write_text(content, encoding="utf-8")
print(f" created: {slug}.md")
created += 1
action = "Would create" if args.dry_run else "Created"
print(f"\nDone. {action} {created} stubs, skipped {skipped} existing.")
if __name__ == "__main__":
main()
+300
View File
@@ -0,0 +1,300 @@
#!/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 <id> 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
# -----------------------------------------------------------------------
# GJ 244A — Sirius system
# Verified against actual markers.json files (DB matches).
# Note: "Westwall" flagged by Gestalt does NOT appear in Edict markers.json
# or DB — either already removed in a prior pass or Gestalt saw stale data.
# -----------------------------------------------------------------------
# Edict (GJ244Ad) — 400M pop, hand-authored template + #833 derived features
# Hand-authored core is clean (Mandate, Station Edict, Founder's Range,
# Accord Peaks, Veto Spur, Concord Assembly Archive, all POIs). Fixes
# target only #833-generated features that don't match the political-legal
# naming vocabulary of Edict.
# Narrative hook: the legal/institutional vocabulary — Mandate, Accord, Veto,
# Concord, Statute, Charter — all reference governance and founding documents.
# Arcs established: Concord- (ocean + POI), Charter- (mountain + ocean),
# Veil- (ocean + POI cross-ref to Veil Institute Survey Station).
"GJ-244A/bodies/GJ244Ad": {
"mountain_ranges": {
# "Keel" is a nautical term — wrong vocabulary for a political world
"Keel Ridge": "Charter Spur", # charter = Edict's founding document
# "Sanction" fits but "Ridge" suffix is lazy
"Sanction Ridge": "The Statute", # distinctive; drops generic suffix
},
"oceans": {
# Cardinal direction names; replace with Edict-vocabulary terms
"Upper Shelf": "Veil Shelf", # cross-refs Veil Institute Survey Station
"Southern Melt": "Concord Mere", # cross-refs Concord Assembly Archive POI
"Eastern Tarn": "Charter Tarn", # cross-refs mountain "Charter Spur"
},
},
# -----------------------------------------------------------------------
# GJ 380 — Groombridge system
# Verified against actual markers.json files.
# Hand-authored template is clean: Aldren (capital) + The Aldren (river) +
# Aldren Exchange (POI) is an INTENTIONAL arc — do not touch.
# "Meridian Risk HQ" POI is a corporation proper noun — not geography.
# -----------------------------------------------------------------------
# Lendel (GJ380c) — 900M pop, hand-authored template + #833 derived features
# Two #833-generated features with lazy suffixes. British-Isles naming
# vocabulary: Lendel, Aldren, Sethvale, Caldenmere, Rethain Sea.
# "Durneth Beck" cross-refs Durneth Range (the major mountain range near
# the river's source area). "Tember Spine" keeps the "Tember" stem but
# drops the lazy "Ridge" suffix.
"GJ-380/bodies/GJ380c": {
"rivers": {
"Pale Run": "Durneth Beck", # lazy → cross-refs Durneth Range; beck=British stream
},
"mountain_ranges": {
"Tember Ridge": "Tember Spine", # lazy Ridge → Spine (distinctive; not in lazy list)
},
},
# -----------------------------------------------------------------------
# GJ 699 — Barnard's Star system
# Verified against actual markers.json files.
# -----------------------------------------------------------------------
# Verada (GJ699b) — 1.9B pop, hop-0, dense urban world
# Major problem: rivers and ocean names use urban civic vocabulary
# (Grandview Square, Meridian City, Metropolitan Square) — these are
# neighborhood-planning names, not geographic features. Rivers especially
# cannot be named "Grandview Square" or "Meridian City". Oceans have
# district-style names that duplicate on sibling bodies.
# Strategy: keep Verada's civic-density character in CITY names, but
# give geographic features geographic names that cross-reference the cities.
# Arcs: Capitol- (city + river + ocean), Haven/Port- (city + ocean),
# Meridian- (river + ocean), Prospect- (ocean), Sterling- (lake).
"GJ-699/bodies/GJ699b": {
"rivers": {
# "Grandview Square" is a plaza, not a river name
"Grandview Square": "Verada Reach", # planet name + geographic; Reach = stretch of river
# "Meridian City" is a city name used for a river
"Meridian City": "The Meridian", # drop civic suffix; becomes proper river name
# "Summit View" is lazy
"Summit View": "Capitol Beck", # cross-refs Capitol Heights capital city
},
"oceans": {
# "Terrace" is an architectural term — wrong for a large ocean
"Prospect Terrace": "Prospect Sea", # keep Prospect, fix suffix
# "Metropolitan Square" is a plaza name for an ocean
"Metropolitan Square": "Haven Sea", # cross-refs Port Haven city
# Exact dup of GJ699b-1 mountain "Central District"
"Central District": "Meridian Sound", # cross-refs The Meridian river
# "Plaza" is civic architecture, not a lake feature
"Liberty Plaza": "Capitol Mere", # cross-refs Capitol Heights capital
# "Sterling Heights" — "Heights" is wrong for a lake; dupes GJ699b-1 mountain
"Sterling Heights": "Sterling Pool", # keep Sterling, fix suffix
},
},
# Verada's moon (GJ699b-1) — uninhabited transit moon
# All 8 mountains are named after urban street addresses (Grandview Avenue,
# Harmony Boulevard, Beacon Street, etc.) — this is entirely wrong for
# mountain ranges. The fix renames them to geological/geographic terms that
# cross-reference Verada's institutional vocabulary (as if settlers looked
# down at Verada and named the moon's peaks after the institutions below).
# The largest range (Verada Scarp, was "Grandview Avenue") takes the planet
# name directly.
"GJ-699/bodies/GJ699b-1": {
"mountain_ranges": {
"Grandview Avenue": "Verada Scarp", # largest range; named after parent body
"Central District": "Barnard Heights", # names the star; distinct from all Verada names
"Harmony Boulevard": "Keystone Scarp", # cross-refs Keystone Junction on Verada
"Capitol Heights": "Tribunal Spur", # cross-refs Tribunal Tower on Verada
"Meridian Plaza": "Zenith Spine", # cross-refs Zenith Station on Verada
"Liberty Square": "Ironwood Spur", # cross-refs Ironwood Borough on Verada
"Metropolitan Way": "Consulate Scarp", # cross-refs Consulate Way on Verada
"Beacon Street": "Prefecture Spur", # cross-refs Prefecture Hall on Verada
},
},
}
# ---------------------------------------------------------------------------
# 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()
+388
View File
@@ -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()
@@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""
atlas_quality_analysis.py — Atlas content quality audit for Sprint 36 (#849/#838).
Queries atlas_* tables in systems.db and reports on:
1. Cross-body name collisions (same name, same feature type, different bodies)
2. Cardinal/directional name density per body
3. Generic/lazy name patterns
4. Earth-echo concentration in high-visibility systems
5. Top-stem frequency across all named features
Usage:
python3 tooling/planet-gen/atlas_quality_analysis.py [--db server/data/systems.db]
python3 tooling/planet-gen/atlas_quality_analysis.py --system GJ380
python3 tooling/planet-gen/atlas_quality_analysis.py --top-collisions 20
python3 tooling/planet-gen/atlas_quality_analysis.py --body GJ71c
D-191 §8: markers.json is pixel-space [row, col] against 512×256.
Re-run after any hand-refine pass to verify improvements.
"""
import argparse
import re
import sqlite3
from collections import Counter, defaultdict
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent.parent
DEFAULT_DB = REPO_ROOT / "server" / "data" / "systems.db"
CARDINAL_RE = re.compile(
r"\b(north|south|east|west|eastern|western|northern|southern|"
r"upper|lower|new|great|old|central|inner|outer|kita|minami|higashi|nishi)\b",
re.I,
)
GENERIC_RE = re.compile(
r"\b(hilly|sector|zone|district)\b"
r"|^(great|the great|hilly)\b"
r"|^(valley floor|ridge line|ridge crest|flat ground|riverbend)$",
re.I,
)
EARTH_ECHO_RE = re.compile(
r"\b(manchester|london|paris|berlin|tokyo|beijing|new york|sydney|dubai|"
r"route \d+|sector \d+|block \d+)\b",
re.I,
)
FEATURE_TABLES = [
("atlas_cities", "city"),
("atlas_rivers", "river"),
("atlas_oceans", "ocean"),
("atlas_mountain_ranges", "mountain"),
]
def open_db(path: str) -> sqlite3.Connection:
return sqlite3.connect(path)
def build_body_index(conn: sqlite3.Connection) -> dict:
c = conn.cursor()
c.execute(
"SELECT body_id, system_id, proper_name, cultural_corridor, population "
"FROM bodies WHERE inhabited=1"
)
return {
r[0]: {"system_id": r[1], "name": r[2], "corridor": r[3], "pop": r[4]}
for r in c.fetchall()
}
def gather_all_names(conn: sqlite3.Connection) -> dict[str, list[tuple[str, str, str]]]:
"""body_id → [(feature_type, name, local_id), ...]"""
c = conn.cursor()
result = defaultdict(list)
for tbl, feat_type in FEATURE_TABLES:
try:
c.execute(f"SELECT body_id, name, local_id FROM {tbl} WHERE name IS NOT NULL AND name != ''")
for body_id, name, local_id in c.fetchall():
result[body_id].append((feat_type, name, local_id))
except sqlite3.OperationalError:
pass
return result
def cross_body_collisions(conn: sqlite3.Connection, limit: int = 20) -> dict:
c = conn.cursor()
collisions = {}
for tbl, feat_type in FEATURE_TABLES:
try:
c.execute(
f"SELECT name, COUNT(DISTINCT body_id) as cnt, GROUP_CONCAT(DISTINCT body_id) "
f"FROM {tbl} WHERE name IS NOT NULL AND name != '' "
f"GROUP BY name HAVING cnt > 1 ORDER BY cnt DESC LIMIT ?",
(limit,),
)
collisions[feat_type] = [(r[0], r[1], r[2]) for r in c.fetchall()]
except sqlite3.OperationalError:
collisions[feat_type] = []
return collisions
def stem_frequency(names: list[str], top_n: int = 30) -> list[tuple[str, int]]:
stems = Counter()
for name in names:
words = name.split()
if words:
stems[words[0].lower()] += 1
return stems.most_common(top_n)
def body_quality_report(body_id: str, names: list[tuple], conn: sqlite3.Connection) -> dict:
total = len(names)
if total == 0:
return {}
cardinal = sum(1 for _, n, _ in names if CARDINAL_RE.search(n))
generic = sum(1 for _, n, _ in names if GENERIC_RE.search(n))
earth = sum(1 for _, n, _ in names if EARTH_ECHO_RE.search(n))
c = conn.cursor()
# collision count: how many of this body's names appear on other bodies (same type)
colliding = 0
for feat_type, name, _ in names:
tbl = [t for t, f in FEATURE_TABLES if f == feat_type][0]
try:
c.execute(
f"SELECT COUNT(DISTINCT body_id) FROM {tbl} WHERE name=? AND body_id!=?",
(name, body_id),
)
others = c.fetchone()[0]
if others > 0:
colliding += 1
except sqlite3.OperationalError:
pass
return {
"total": total,
"cardinal": cardinal,
"cardinal_pct": cardinal / total,
"generic": generic,
"earth_echo": earth,
"colliding": colliding,
"colliding_pct": colliding / total,
}
def run_analysis(args):
conn = open_db(args.db)
body_index = build_body_index(conn)
all_names_by_body = gather_all_names(conn)
# Filter by system or body if requested
if args.system:
body_index = {k: v for k, v in body_index.items() if v["system_id"] == args.system}
if args.body:
body_index = {k: v for k, v in body_index.items() if k == args.body}
print("=" * 70)
print("ATLAS QUALITY ANALYSIS — The Settled Reach (#849/#838)")
print(f"DB: {args.db}")
if args.system:
print(f"Filter: system={args.system}")
if args.body:
print(f"Filter: body={args.body}")
print("=" * 70)
# --- 1. Cross-body collisions ---
print("\n[ 1. CROSS-BODY NAME COLLISIONS ]")
collisions = cross_body_collisions(conn, limit=args.top_collisions)
for feat_type, rows in collisions.items():
if rows:
print(f"\n {feat_type}:")
for name, cnt, bodies in rows:
print(f" '{name}' — {cnt} bodies: {bodies[:80]}")
# --- 2. Per-body quality scores ---
print("\n[ 2. BODY QUALITY SCORES — ranked by collision % ]")
reports = []
for bid, info in body_index.items():
names = all_names_by_body.get(bid, [])
if not names:
continue
report = body_quality_report(bid, names, conn)
if not report:
continue
reports.append((bid, info, report))
reports.sort(key=lambda x: -x[2]["colliding_pct"])
print(f"\n {'Body':<28} {'System':<12} {'Corridor':<15} "
f"{'Coll%':>6} {'Card%':>6} {'Gen':>4} {'Echo':>4}")
for bid, info, rep in reports[:30]:
print(
f" {(info['name'] or bid):<28} {info['system_id']:<12} {info['corridor'] or '?':<15} "
f"{rep['colliding_pct']:>6.0%} {rep['cardinal_pct']:>6.0%} "
f"{rep['generic']:>4} {rep['earth_echo']:>4}"
)
# --- 3. Stem frequency ---
print("\n[ 3. TOP STEM FREQUENCY (first word of name) ]")
all_names_flat = [n for names in all_names_by_body.values() for _, n, _ in names]
for stem, cnt in stem_frequency(all_names_flat, top_n=20):
print(f" {stem:<20} {cnt}")
# --- 4. Detailed body report (if --body specified) ---
if args.body and args.body in all_names_by_body:
bid = args.body
info = body_index.get(bid, {})
names = all_names_by_body[bid]
print(f"\n[ 4. DETAILED REPORT: {bid} ({info.get('name', '?')}) ]")
c = conn.cursor()
for feat_type, name, local_id in sorted(names, key=lambda x: x[0]):
tbl = [t for t, f in FEATURE_TABLES if f == feat_type][0]
c.execute(
f"SELECT COUNT(DISTINCT body_id) FROM {tbl} WHERE name=? AND body_id!=?",
(name, bid),
)
others = c.fetchone()[0]
flag = f" *** COLLISION ×{others}" if others > 0 else ""
cardinal = " [cardinal]" if CARDINAL_RE.search(name) else ""
generic = " [generic]" if GENERIC_RE.search(name) else ""
print(f" {feat_type:<10} {local_id:<12} {name}{flag}{cardinal}{generic}")
# --- 5. Sol gap check ---
print("\n[ 5. SOL SYSTEM GAP CHECK ]")
c = conn.cursor()
c.execute("SELECT body_id, proper_name, population FROM bodies WHERE system_id='GJ 0' AND inhabited=1")
sol_bodies = c.fetchall()
for bid, bname, pop in sol_bodies:
has_cities = bid in all_names_by_body and any(f == "city" for f, _, _ in all_names_by_body[bid])
status = "HAS DATA" if has_cities else "*** EMPTY — needs authoring"
print(f" {bid:<15} {bname or '?':<20} pop={pop or '?'} {status}")
conn.close()
print("\nDone.")
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("--db", default=str(DEFAULT_DB), help="Path to systems.db")
parser.add_argument("--system", help="Filter to one system (e.g. GJ380)")
parser.add_argument("--body", help="Filter to one body (e.g. GJ71c)")
parser.add_argument("--top-collisions", type=int, default=15, help="Collision list limit")
args = parser.parse_args()
run_analysis(args)
if __name__ == "__main__":
main()
+1
View File
@@ -57,6 +57,7 @@ Vienna
Prague
Warsaw
Amsterdam
# GJ0d (Earth body) is exempt — this blocklist applies to alien worlds only, not to Earth itself.
Brussels
Copenhagen
Stockholm
+477
View File
@@ -0,0 +1,477 @@
# 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 <id>`.
---
## 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` ✓
---
### GJ 244A — Sirius system
#### GJ244Ad — Edict (400M pop, hand-authored template)
**Priority reason:** Only inhabited body in the Sirius system. Hand-authored template is solid
(Mandate, Founder's Range, Accord Peaks, Veto Spur, Concord Assembly Archive) but #833-generated
features used wrong vocabulary for a political-legal world. Note: "Westwall" flagged by Gestalt
does NOT appear in markers.json or DB — stale data issue; Cairnside fix by Gestalt stands, Edict
required no Westwall action.
**Changes:**
| Feature type | Old name | New name | Reason |
|---|---|---|---|
| mountain | Keel Ridge | Charter Spur | "Keel" is nautical — wrong vocabulary; charter = founding document |
| mountain | Sanction Ridge | The Statute | More distinctive; drops lazy "Ridge" suffix |
| ocean | Upper Shelf | Veil Shelf | Cardinal → cross-refs Veil Institute Survey Station POI |
| ocean | Southern Melt | Concord Mere | Cardinal → cross-refs Concord Assembly Archive POI |
| ocean | Eastern Tarn | Charter Tarn | Cardinal → cross-refs mountain "Charter Spur" |
**Cross-reference arcs post-fix:**
- Charter-: Charter Spur (mountain), Charter Tarn (ocean)
- Concord-: Concord Mere (ocean), Concord Assembly Archive (POI)
- Veil-: Veil Shelf (ocean), Veil Institute Survey Station (POI)
**DB sync:** `generate_atlas.py --body GJ244Ad` ✓
---
### GJ 380 — Groombridge system
#### GJ380c — Lendel (900M pop, hand-authored template)
**Priority reason:** Only inhabited body in Groombridge. Template is excellent — Aldren arc
(capital + river + exchange) is intentional and preserved. Two #833-generated features used lazy
suffixes incompatible with the British-Isles naming style.
**Changes:**
| Feature type | Old name | New name | Reason |
|---|---|---|---|
| river | Pale Run | Durneth Beck | Lazy → cross-refs Durneth Range; "beck" = British stream |
| mountain | Tember Ridge | Tember Spine | Lazy "Ridge" → "Spine"; keeps the Tember stem |
**Note:** "Aldren" stem in same-body audit (city + river + POI) is INTENTIONAL arc — preserved.
"Meridian Risk HQ" POI is a corporation name (proper noun) — not geography, no change.
**DB sync:** `generate_atlas.py --body GJ380c` ✓
**Addendum (PR #133 review):** Initial pass left GJ380c 100% Anglo-British register with zero
cross-corridor influence from the Kumasi/south_reach trade lanes. Added two secondary features
in Akan/Asante register, keeping primary features (Aldren capital, Durneth Range, Rethain Sea)
in their established Anglo register:
| Feature type | Old name | New name | Reason |
|---|---|---|---|
| river | The Kesset | Nkwanta Beck | Nkwanta = Akan "junction/crossroads" — trade-route name; Beck suffix localizes it |
| mountain | Holt Spur | Bosomtwe Spur | Bosomtwe = sacred Asante lake in Ghana; Spur suffix preserved from Groombridge convention |
| lake | Selet Basin | Subin Basin | Subin = river running through Kumasi itself — distributes Akan signal across a third feature type |
**Corridors are tendencies, not borders** — capital and primary geography stay Anglo-British;
peripheral features reflect Kumasi trade influence. 3/29 features now carry Akan register (river, mountain, lake). DB re-synced.
---
### GJ 699 — Barnard's Star system
#### GJ699b — Verada (1.9B pop, hop-0, dense urban world)
**Priority reason:** Highest-traffic hop-0 inhabited body not yet addressed. Dense civic naming
(Capitol Heights, Liberty Plaza, Metropolitan Square, Meridian City) was using district/plaza
vocabulary for geographic features. Rivers especially broken: "Grandview Square" and "Meridian
City" are civic addresses, not river names. Cross-body dups on sibling bodies.
**Changes:**
| Feature type | Old name | New name | Reason |
|---|---|---|---|
| river | Grandview Square | Verada Reach | Civic square → planet name + geographic "Reach" |
| river | Meridian City | The Meridian | Civic city name → proper river name (like "The Aldren") |
| river | Summit View | Capitol Beck | Lazy → cross-refs Capitol Heights capital |
| ocean | Prospect Terrace | Prospect Sea | "Terrace" is architecture; fix suffix |
| ocean | Metropolitan Square | Haven Sea | Cross-refs Port Haven city; drops civic |
| ocean | Central District | Meridian Sound | Exact dup of GJ699b-1 mountain; cross-refs The Meridian |
| ocean | Liberty Plaza | Capitol Mere | Civic plaza → cross-refs Capitol Heights capital |
| ocean | Sterling Heights | Sterling Pool | "Heights" wrong for water; dup on GJ699b-1 mountain |
**Cross-reference arcs post-fix:**
- Capitol-: Capitol Heights (city), Capitol Beck (river), Capitol Mere (ocean)
- Meridian-: The Meridian (river), Meridian Sound (ocean)
- Haven/Port-: Port Haven (city), Haven Sea (ocean)
**DB sync:** `generate_atlas.py --body GJ699b` ✓
---
#### GJ699b-1 — Verada's moon (uninhabited transit body)
**Priority reason:** All 8 mountains named after urban street addresses (Grandview Avenue,
Harmony Boulevard, Beacon Street, Metropolitan Way, etc.) — completely inappropriate for
mountain ranges. Also duplicated several Verada city/ocean names exactly ("Central District",
"Capitol Heights"). Fixed with institutional cross-references: the moon's mountains carry the
names of Verada's governing institutions, as if settlers named what they saw below from orbit.
**Changes:** All 8 mountains renamed. See apply_name_fixes.py FIXES table for full mapping.
**Narrative hook:** "On Verada's moon, the mountains carry the names of the institutions
that govern the world below — a form of reverence. The moon has no government of its own."
**DB sync:** sync_markers_to_db() called directly (body is inhabited=0, generate_atlas.py skips it) ✓
---
### Sol (GJ 0) — COMPLETE
**Approach:** sol_import.py generated body directories and markers.json from real-world geographic
data. Post-generation, `sol_name_fixes.py` named all auto-detected null-name features.
Team-lead guidance: "real-world Earth/Luna/Mars geography is by definition coherent — note it
in log and move on if already above quality bar."
#### GJ0d — Earth (8.5B pop, core world)
sol_import.py originally placed 50 cities from earth_features.json. Team-lead directed trim
to 8-12 cultural touchstones (max 8 cities on any other body; 50 = 13% of all atlas cities
on one body). Criterion: would a player setting a bookmark to "Earth" recognize this as a
touchstone? One per major historical/cultural cluster.
**Cities kept (11):**
| City | Cluster |
|---|---|
| London | Western Europe — historical capital |
| Moscow | Eastern Europe / Russia |
| Istanbul | Bridge city — Europe-Asia hinge |
| New York | North America |
| São Paulo | South America |
| Cairo | Africa + ancient world |
| Delhi | South Asia |
| Tokyo | Japan / East Asia |
| Beijing | China / East Asia |
| Singapore | Southeast Asia — maritime hub |
| Sydney | Oceania |
**Cities cut (39):** Paris, Berlin, Mexico City, Los Angeles, Toronto, Chicago, Lima, Bogotá,
Rio de Janeiro, Buenos Aires, Lagos, Kinshasa, Johannesburg, Nairobi, Tehran, Baghdad, Riyadh,
Ankara, Karachi, Shanghai, Mumbai, Jakarta, Dhaka, Manila, Bangkok, Seoul, Osaka, Chongqing,
Kolkata, Lahore, Shenzhen, Bangalore, Ho Chi Minh City, Luanda, Addis Ababa, Santiago, Taipei,
Hong Kong, Casablanca.
**PR #133 review rebalance (user-approved Option 1 + London swap):**
- `Sydney → Lagos` — improves African representation (Cairo + Lagos = 2 African cities; Sydney was the weakest cultural-touchstone anchor).
- `London → Brussels` — Brussels chosen for future-strong-EU-capital setting fit over London's legacy cultural weight.
**Final 11:** Beijing, Brussels, Cairo, Delhi, Istanbul, Lagos, Moscow, New York, São Paulo, Singapore, Tokyo.
**Generator note:** sol_import.py has no `--top-n` city filter — it uses the full earth_features.json
list. If Sol is regenerated, earth_features.json should be trimmed to the 11 kept cities, or a
filter added in sol_import.py. Filed as finding in #853.
auto-detected 11 rivers and 1 ocean. Three rivers were auto-detected ocean-channel artifacts
in the western Pacific island region; named with geographically proximate rivers. The large
ocean (area_fraction=0.7049) represents Earth's interconnected world ocean.
| Feature type | Old name | New name | Reason |
|---|---|---|---|
| ocean | null | The World Ocean | 70% surface = all Earth's oceans unified; scientifically accurate name |
| river | null (river_1, ~50°N/17°E) | Dnieper | Eastern Europe, flows to Black Sea |
| river | null (river_4, ~27°N/136°E) | Tone River | Japan, Kanto plain |
| river | null (river_10, ~23°N/135°E) | Cagayan | Northern Philippines, largest Philippine river |
**DB sync:** `generate_atlas.py --body GJ0d` ✓ (11 cities, trimmed from 50)
---
#### GJ0d-1 — Luna (350M pop, transit body)
3 cities (Artemis capital, Tranquility Station, Selene) + 10 POIs (maria + craters). sol_import.py
auto-detected 24 mountain ranges — all unnamed. Named all 24 after real IAU-catalogued lunar
mountain systems, massifs, and scarps, sorted by area_cells (largest = most prominent).
Key ranges: Montes Apenninus (11,221 cells — largest), Montes Caucasus (3,670), Montes Jura,
Montes Alpes, Montes Rook, Montes Cordillera, Montes Carpatus, Montes Taurus, Montes Pyrenaeus,
and 15 additional real lunar named features (Pico Mons, Haemus Montes, Gruithuisen Domes, etc.).
**Narrative hook:** "Luna's mountains carry the names that Earth gave them a thousand years
before the first settlement — a catalog written from below, now walked from above."
**DB sync:** `generate_atlas.py --body GJ0d-1` ✓
---
#### GJ0e — Mars (1.2B pop, core world)
4 cities + 5 named mountains (Olympus Mons, Tharsis Bulge, Elysium Mons, Ascraeus Mons,
Arsia Mons) from mars_features.json. sol_import.py auto-detected 4 additional mountain ranges
in the anti-Tharsis hemisphere (ancient cratered highland region), named after real Martian
volcanic features.
| Feature type | Old name | New name | Reason |
|---|---|---|---|
| mountain | null (range_2) | Tyrrhena Mons | Ancient volcanic highland, eastern hemisphere |
| mountain | null (range_3) | Hadriaca Mons | Old shield volcano, Hellas region |
| mountain | null (range_4) | Amphitrites Montes | Near south polar region |
| mountain | null (range_5) | Hellespontus Montes | Cross-refs Hellas Station city |
**Cross-reference arcs:** Hellas- (Hellas Basin ocean + Hellas Station city), Chryse- (Chryse Landing + Chryse Planitia POI)
**DB sync:** `generate_atlas.py --body GJ0e` ✓
---
#### GJ0f-2 — Europa (30M pop, outer body)
2 cities + 4 POIs. sol_import.py auto-detected 1 mountain range covering essentially the
entire body surface (area_cells=123,557 — Europa's ridged ice terrain).
| Feature type | Old name | New name | Reason |
|---|---|---|---|
| mountain | null (range_1) | Conamara Ridges | Cross-refs Conamara Station + Conamara Chaos POI |
**Cross-reference arcs:** Conamara- (Conamara Station city + Conamara Ridges mountain + Conamara Chaos POI),
Pwyll- (Pwyll Base city + Pwyll Crater POI)
**DB sync:** `generate_atlas.py --body GJ0f-2` ✓
---
**Sol final audit result:** Zero empty names, zero lazy outputs, zero cardinals.
Earth-echo names on Earth are intentional (real-world cities). Quality cross-reference arcs
confirmed on Mars (Hellas-, Chryse-) and Europa (Conamara-, Pwyll-).
---
### Proxima (GJ 551) — DEFERRED
Cross-body stem duplicates found (Basilica, Pantheon, Senate across Ancora sibling bodies).
Team-lead confirmed: defer this sprint. Finding documented for future pass.
---
## Audit metrics — final confirmed delta
Final audit run: 2026-04-19. All 6 systems re-run after all edits. Results below are
from `atlas_cohesion_audit.py` against `server/data/systems.db`.
### Inhabited body targets (in scope for this ticket)
| System | Body | Empty names: before→after | Lazy outputs: before→after | Cardinals: before→after |
|--------|------|--------------------------|---------------------------|------------------------|
| GJ 144 | GJ144d Kallast (2B) | 0→0 | 3→0 | 0→0 |
| GJ 144 | GJ144e Vethis (1.2B) | 0→0 | 7→0 | 1→0 |
| GJ 71 | GJ71c Threshold (600M) | 0→0 | 1→0 | 0→0 |
| GJ 71 | GJ71d Arden (500M) | 0→0 | 1→0 | 0→0 |
| GJ 71 | GJ71d-1 Verantis (20M) | 0→0 | 0→0 | 0→0 |
| GJ 244A | GJ244Ad Edict (400M) | 0→0 | 2→0 | 3→0 |
| GJ 380 | GJ380c Lendel (900M) | 0→0 | 2→0 | 0→0 |
| GJ 699 | GJ699b Verada (1.9B) | 0→0 | 8→0 | 0→0 |
| GJ 699 | GJ699b-1 (uninhabited moon) | 8→0 | 8→0 | 0→0 |
| GJ 0 | GJ0d Earth (8.5B) | 4→0 | 0→0 | 0→0 |
| GJ 0 | GJ0d-1 Luna (350M) | 24→0 | 0→0 | 0→0 |
| GJ 0 | GJ0e Mars (1.2B) | 4→0 | 0→0 | 0→0 |
| GJ 0 | GJ0f-2 Europa (30M) | 1→0 | 0→0 | 0→0 |
**All inhabited targets: zero empty names, zero lazy outputs, zero cardinals after fixes.**
### Remaining audit flags — out of scope or false positives
After fixes, the audit still reports flags on:
**Out of scope — uninhabited/low-pop bodies (not "high-visibility"):**
- GJ144b, GJ144c, GJ144d-1, GJ144e-1, GJ144f, GJ144g-1, GJ144g-2: "Canyon View", "Dry Gulch",
"Stone Creek" etc. These are #833 batch artifacts on non-target bodies. Captured in #853.
- GJ71e: "Meridian Point" (uninhabited body, not in scope)
- GJ380b, GJ380d, GJ380e: various lazy patterns (uninhabited, not in scope)
**False positives on quality cross-reference names (distinctive stem + common suffix):**
- `'Rán's Run'` (GJ144d) — Rán- arc; % Run pattern-matched but stem is unique proper name
- `'Greywash Fork'` (GJ144e) — Grey- arc; % Fork but Greywash is not a generic stem
- `'Kelside Run'` (GJ144e) — Kel- arc; % Run but Kelside is distinctive
- `'Greystone Ridge'` (GJ144e) — Grey- arc; % Ridge but Greystone is distinctive
These four are intentional renames (listed in the FIXES table above) that happen to end with
a suffix in LAZY_PATTERNS. The script does not evaluate stem quality, only suffix pattern.
A future pass on the audit script could add a stem-distinctiveness filter.
### Earth city count correction
Per team-lead direction: Earth trimmed from **50 → 11 cities** (cultural/historical touchstones,
one per major cluster). 39 cities cut. DB synced. See GJ0d section above for full cut list.
### Earth city rebalance (PR #133 review, user-approved)
Sydney → Lagos and London → Brussels per PR #133 review, user-approved rebalance. Brussels chosen for future EU-capital setting fit over London's legacy weight; Lagos chosen for African representation. DB re-synced. Final 11: Brussels, Istanbul, Moscow, New York, São Paulo, Cairo, Lagos, Delhi, Tokyo, Beijing, Singapore.
**Total name edits across all bodies:** 23 (non-Sol) + 33 (Sol) = 56 feature renames.
**Earth city cut:** 39 removed.
Intentional same-body cross-feature stem dups (quality arcs) now visible in audit output for:
- GJ144d: Rán- (city + river), Seter- (mountain + ocean)
- GJ144e: Veth- (river + ocean), Ash- (city + river)
- GJ244Ad: Charter- (mountain + ocean), Concord- (ocean + POI), Veil- (ocean + POI)
- GJ380c: Aldren- (city + river + POI, intentional), Durneth- (river + mountain)
- GJ699b: Capitol- (city + river + ocean), Meridian- (river + ocean)
- GJ0e: Hellas- (city + ocean), Chryse- (city + POI)
- GJ0f-2: Conamara- (city + mountain + POI), Pwyll- (city + POI)
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"
SR_DB_PATH="$(pwd)/server/data/systems.db" tooling/db/sqlite-query \
"SELECT local_id, name FROM atlas_mountain_ranges WHERE body_id = 'GJ244Ad'"
# Should show "The Statute" and "Charter Spur" not "Sanction Ridge" and "Keel Ridge"
SR_DB_PATH="$(pwd)/server/data/systems.db" tooling/db/sqlite-query \
"SELECT local_id, name FROM atlas_mountain_ranges WHERE body_id = 'GJ699b-1'"
# Should show "Verada Scarp", "Barnard Heights", "Keystone Scarp", etc. — no street addresses
```
---
## Files changed
- `tooling/planet-gen/atlas_cohesion_audit.py` (new)
- `tooling/planet-gen/apply_name_fixes.py` (new — extended with all 6 systems)
- `tooling/planet-gen/refine_log_849.md` (this file)
- `tooling/planet-gen/sol_markers/luna_features.json` (added cities)
- `tooling/planet-gen/sol_markers/mars_features.json` (added cities)
- `tooling/planet-gen/sol_markers/outer_features.json` (added Europa cities)
- `wiki/star-systems/GJ-144/bodies/GJ144d/markers.json` (4 edits)
- `wiki/star-systems/GJ-144/bodies/GJ144e/markers.json` (9 edits)
- `wiki/star-systems/GJ-71/bodies/GJ71c/markers.json` (1 edit)
- `wiki/star-systems/GJ-71/bodies/GJ71d/markers.json` (2 edits)
- `wiki/star-systems/GJ-244A/bodies/GJ244Ad/markers.json` (5 edits)
- `wiki/star-systems/GJ-380/bodies/GJ380c/markers.json` (2 edits)
- `wiki/star-systems/GJ-699/bodies/GJ699b/markers.json` (8 edits)
- `wiki/star-systems/GJ-699/bodies/GJ699b-1/markers.json` (8 edits)
- `tooling/planet-gen/sol_name_fixes.py` (new — names 33 null-name Sol features)
- `wiki/star-systems/GJ-0/bodies/GJ0d/markers.json` (1 ocean + 3 rivers named; 39 cities cut → 11)
- `wiki/star-systems/GJ-0/bodies/GJ0d-1/markers.json` (24 mountain ranges named)
- `wiki/star-systems/GJ-0/bodies/GJ0e/markers.json` (4 mountain ranges named)
- `wiki/star-systems/GJ-0/bodies/GJ0f-2/markers.json` (1 mountain range named)
- `server/data/systems.db` (synced: GJ144d, GJ144e, GJ71c, GJ71d, GJ71d-1, GJ244Ad, GJ380c, GJ699b, GJ699b-1, GJ0d, GJ0d-1, GJ0e, GJ0f-2 — all complete)
@@ -1,5 +1,6 @@
{
"GJ0d": {
"_city_selection_note": "11 cultural touchstones, one per major historical/cultural cluster. Criterion: would a player setting a bookmark to 'Earth' recognize this city? Full 46-city reference list is in git history (trimmed Sprint 36 #849). PR #133 review rebalance: Sydney → Lagos (African representation); London → Brussels (future EU capital fit). To adjust: edit this array and re-run sol_import.py, then generate_atlas.py --body GJ0d.",
"oceans": [
{"name": "Pacific Ocean", "center": [128, 440]},
{"name": "Atlantic Ocean", "center": [128, 170]},
@@ -33,62 +34,22 @@
{"name": "Murray", "mouth": [170, 417]}
],
"cities": [
{"name": "London", "center": [79, 260], "population": 9000000, "region": "europe"},
{"name": "Istanbul", "center": [83, 279], "population": 15000000, "region": "europe"},
{"name": "Moscow", "center": [72, 294], "population": 12700000, "region": "europe"},
{"name": "Paris", "center": [80, 261], "population": 11000000, "region": "europe"},
{"name": "Berlin", "center": [77, 269], "population": 3700000, "region": "europe"},
{"name": "Brussels", "center": [80, 266], "population": 2100000, "region": "europe"},
{"name": "Istanbul", "center": [83, 279], "population": 15000000, "region": "europe"},
{"name": "Moscow", "center": [72, 294], "population": 12700000, "region": "europe"},
{"name": "Mexico City", "center": [107, 101], "population": 21800000, "region": "north_america"},
{"name": "New York", "center": [87, 130], "population": 20100000, "region": "north_america"},
{"name": "Los Angeles", "center": [93, 95], "population": 13200000, "region": "north_america"},
{"name": "Toronto", "center": [84, 123], "population": 6200000, "region": "north_america"},
{"name": "Chicago", "center": [85, 115], "population": 9500000, "region": "north_america"},
{"name": "New York", "center": [87, 130], "population": 20100000, "region": "north_america"},
{"name": "São Paulo", "center": [143, 164], "population": 22400000, "region": "south_america"},
{"name": "Lima", "center": [133, 131], "population": 10700000, "region": "south_america"},
{"name": "Bogotá", "center": [121, 135], "population": 11300000, "region": "south_america"},
{"name": "Rio de Janeiro", "center": [142, 168], "population": 13500000, "region": "south_america"},
{"name": "Buenos Aires", "center": [151, 153], "population": 15200000, "region": "south_america"},
{"name": "São Paulo", "center": [143, 164], "population": 22400000, "region": "south_america"},
{"name": "Lagos", "center": [120, 262], "population": 15400000, "region": "africa"},
{"name": "Kinshasa", "center": [124, 270], "population": 15600000, "region": "africa"},
{"name": "Cairo", "center": [97, 286], "population": 21300000, "region": "africa"},
{"name": "Johannesburg", "center": [156, 279], "population": 6000000, "region": "africa"},
{"name": "Nairobi", "center": [128, 293], "population": 5100000, "region": "africa"},
{"name": "Cairo", "center": [97, 286], "population": 21300000, "region": "africa"},
{"name": "Tehran", "center": [92, 308], "population": 9000000, "region": "west_asia"},
{"name": "Baghdad", "center": [94, 303], "population": 8100000, "region": "west_asia"},
{"name": "Riyadh", "center": [103, 304], "population": 7700000, "region": "west_asia"},
{"name": "Ankara", "center": [87, 284], "population": 5700000, "region": "west_asia"},
{"name": "Karachi", "center": [103, 327], "population": 16500000, "region": "west_asia"},
{"name": "Tokyo", "center": [92, 400], "population": 37400000, "region": "east_asia"},
{"name": "Delhi", "center": [99, 339], "population": 32900000, "region": "east_asia"},
{"name": "Beijing", "center": [87, 383], "population": 21500000, "region": "east_asia"},
{"name": "Tokyo", "center": [92, 400], "population": 37400000, "region": "east_asia"},
{"name": "Delhi", "center": [99, 339], "population": 32900000, "region": "east_asia"},
{"name": "Shanghai", "center": [97, 387], "population": 28500000, "region": "east_asia"},
{"name": "Beijing", "center": [87, 383], "population": 21500000, "region": "east_asia"},
{"name": "Mumbai", "center": [107, 333], "population": 21700000, "region": "east_asia"},
{"name": "Jakarta", "center": [120, 374], "population": 34500000, "region": "fill"},
{"name": "Dhaka", "center": [103, 351], "population": 23000000, "region": "fill"},
{"name": "Manila", "center": [109, 388], "population": 14400000, "region": "fill"},
{"name": "Bangkok", "center": [109, 370], "population": 11000000, "region": "fill"},
{"name": "Seoul", "center": [90, 393], "population": 9800000, "region": "fill"},
{"name": "Osaka", "center": [93, 398], "population": 19300000, "region": "fill"},
{"name": "Chongqing", "center": [97, 375], "population": 17000000, "region": "fill"},
{"name": "Kolkata", "center": [103, 349], "population": 15100000, "region": "fill"},
{"name": "Lahore", "center": [97, 336], "population": 14000000, "region": "fill"},
{"name": "Shenzhen", "center": [104, 382], "population": 13400000, "region": "fill"},
{"name": "Bangalore", "center": [111, 339], "population": 13200000, "region": "fill"},
{"name": "Ho Chi Minh City", "center": [113, 374], "population": 9300000, "region": "fill"},
{"name": "Luanda", "center": [132, 268], "population": 9000000, "region": "fill"},
{"name": "Addis Ababa", "center": [119, 292], "population": 5500000, "region": "fill"},
{"name": "Santiago", "center": [147, 137], "population": 7000000, "region": "fill"},
{"name": "Taipei", "center": [103, 388], "population": 7000000, "region": "fill"},
{"name": "Hong Kong", "center": [104, 382], "population": 7500000, "region": "fill"},
{"name": "Singapore", "center": [119, 372], "population": 5900000, "region": "fill"},
{"name": "Sydney", "center": [161, 421], "population": 5300000, "region": "fill"},
{"name": "Casablanca", "center": [93, 249], "population": 3800000, "region": "fill"}
{"name": "Singapore", "center": [119, 372], "population": 5900000, "region": "southeast_asia"},
{"name": "Lagos", "center": [119, 263], "population": 15000000, "region": "africa"}
]
}
}
@@ -1,5 +1,10 @@
{
"GJ0d-1": {
"cities": [
{"name": "Artemis", "center": [215, 250], "population": 200000000, "kind": "capital"},
{"name": "Tranquility Station", "center": [119, 282], "population": 90000000, "kind": "city"},
{"name": "Selene", "center": [93, 247], "population": 60000000, "kind": "city"}
],
"pois": [
{"id": "poi_mare_tranquillitatis", "name": "Mare Tranquillitatis", "center": [119, 282], "kind": "mare"},
{"id": "poi_mare_imbrium", "name": "Mare Imbrium", "center": [93, 247], "kind": "mare"},
@@ -1,5 +1,11 @@
{
"GJ0e": {
"cities": [
{"name": "Olympia", "center": [107, 358], "population": 450000000, "kind": "capital"},
{"name": "Hellas Station", "center": [148, 329], "population": 350000000, "kind": "city"},
{"name": "Elysium City", "center": [103, 413], "population": 250000000, "kind": "city"},
{"name": "Chryse Landing", "center": [100, 392], "population": 150000000, "kind": "city"}
],
"mountain_ranges": [
{"name": "Olympus Mons", "peak": [107, 358], "center": [107, 358]},
{"name": "Tharsis Bulge", "peak": [115, 365], "center": [118, 362]},
@@ -9,6 +9,10 @@
]
},
"GJ0f-2": {
"cities": [
{"name": "Conamara Station", "center": [118, 328], "population": 20000000, "kind": "capital"},
{"name": "Pwyll Base", "center": [155, 328], "population": 10000000, "kind": "city"}
],
"pois": [
{"id": "poi_conamara_chaos", "name": "Conamara Chaos", "center": [118, 328], "kind": "chaos"},
{"id": "poi_pwyll_crater", "name": "Pwyll Crater", "center": [155, 328], "kind": "crater"},
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""
sol_name_fixes.py — Name previously-unnamed Sol body auto-detected features.
Targets features with null names: Earth oceans/rivers, Luna/Mars/Europa mountain ranges.
All names are real-world geographic names for Sol bodies.
Usage:
python3 sol_name_fixes.py # apply all fixes
python3 sol_name_fixes.py --dry-run # print planned changes without writing
"""
import argparse
import json
from pathlib import Path
REPO_ROOT = (Path(__file__).resolve().parent / ".." / "..").resolve()
WIKI = REPO_ROOT / "wiki" / "star-systems" / "GJ-0" / "bodies"
# Keys are feature IDs; values are the names to assign.
FIXES = {
# Earth (GJ0d): 1 unnamed ocean + 3 unnamed rivers (auto-detected terrain artifacts)
"GJ0d": {
"oceans": {
"water_1": "The World Ocean", # 70% surface = all Earth's oceans unified
},
"rivers": {
"river_1": "Dnieper", # Eastern Europe ~50°N, 17°E
"river_4": "Tone River", # Japan, Kanto plain ~36°N, 140°E
"river_10": "Cagayan", # Northern Philippines ~18°N, 121°E
},
},
# Luna (GJ0d-1): 24 unnamed mountain ranges — all auto-detected; named after
# real lunar mountain systems, massifs, and scarps sorted by descending area.
"GJ0d-1": {
"mountain_ranges": {
"range_5": "Montes Apenninus", # 11221 cells — largest lunar mountain range
"range_34": "Montes Caucasus", # 3670 cells
"range_209": "Montes Jura", # 367 cells — on Mare Imbrium NW
"range_551": "Montes Alpes", # 289 cells — lunar Alps
"range_576": "Montes Rook", # 258 cells — south polar mountains
"range_599": "Montes Cordillera", # 164 cells — Orientale basin rim
"range_326": "Montes Carpatus", # 106 cells — south of Mare Imbrium
"range_555": "Montes Pyrenaeus", # 94 cells — east of Mare Nectaris
"range_586": "Montes Taurus", # 90 cells — NE of Mare Serenitatis
"range_589": "Montes Teneriffe", # 52 cells — isolated massif
"range_124": "Montes Harbinger", # 51 cells — west of Aristarchus
"range_587": "Montes Recti", # 48 cells — north Mare Imbrium
"range_522": "Pico Mons", # 35 cells — isolated peak
"range_579": "Haemus Montes", # 33 cells — south of Mare Serenitatis
"range_231": "Gruithuisen Domes", # 25 cells — volcanic domes
"range_580": "Mons La Hire", # 25 cells — volcanic dome, Mare Imbrium
"range_570": "Montes Riphaeus", # 24 cells — Oceanus Procellarum ridge
"range_596": "Montes Secchi", # 24 cells — east of Mare Fecunditatis
"range_604": "Montes Spitzbergen", # 24 cells — isolated massif
"range_493": "Malapert Mountain", # 23 cells — south pole region
"range_406": "Doerfel Mountains", # 26 cells — south pole far side
"range_432": "Altai Scarp", # 20 cells — Rupes Altai
"range_79": "Mons Rümker", # 21 cells — shield volcano
"range_348": "Leibnitz Mountains", # 21 cells — south polar range
},
},
# Mars (GJ0e): 4 unnamed mountain ranges in the anti-Tharsis hemisphere
"GJ0e": {
"mountain_ranges": {
"range_2": "Tyrrhena Mons", # ancient volcanic highland (area=67)
"range_3": "Hadriaca Mons", # old shield volcano (area=31)
"range_4": "Amphitrites Montes", # near south polar region (area=28)
"range_5": "Hellespontus Montes", # cross-refs Hellas Station (area=87)
},
},
# Europa (GJ0f-2): 1 unnamed mountain — Europa's ridged ice terrain (area=123557 cells)
"GJ0f-2": {
"mountain_ranges": {
"range_1": "Conamara Ridges", # cross-refs Conamara Station + Conamara Chaos
},
},
}
SECTION_MAP = {
"oceans": "oceans",
"rivers": "rivers",
"mountain_ranges": "mountain_ranges",
}
def _check_dedup(fixes: dict) -> None:
"""Raise if any name is assigned to more than one feature ID within a body."""
for body_id, sections in fixes.items():
for section, id_map in sections.items():
seen: dict[str, str] = {}
for fid, name in id_map.items():
if name in seen:
raise ValueError(
f"Duplicate name {name!r} in {body_id}/{section}: "
f"{seen[name]} and {fid} both assigned"
)
seen[name] = fid
def apply_sol_fixes(dry_run: bool = False) -> None:
_check_dedup(FIXES)
for body_id, body_fixes in FIXES.items():
path = WIKI / body_id / "markers.json"
if not path.exists():
print(f" SKIP {body_id}: markers.json not found")
continue
with open(path) as f:
markers = json.load(f)
changed = False
for section, id_map in body_fixes.items():
feature_list = markers.get(SECTION_MAP[section], [])
for feature in feature_list:
fid = feature.get("id", "")
if fid in id_map:
old = feature.get("name")
new = id_map[fid]
if old != new:
print(f" [{body_id}/{section}] {fid}: {old!r} → {new!r}")
if not dry_run:
feature["name"] = new
changed = True
if changed:
if dry_run:
print(f" (dry-run) Would write: {path}")
else:
with open(path, "w") as f:
json.dump(markers, f, indent=2)
print(f" Written: {path}")
else:
print(f" No changes for {body_id}")
print()
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Name previously-unnamed Sol body auto-detected features."
)
parser.add_argument(
"--dry-run",
action="store_true",
help="print planned changes without writing any files",
)
args = parser.parse_args()
apply_sol_fixes(dry_run=args.dry_run)