Merge remote-tracking branch 'origin/sprint-36/copy'
This commit is contained in:
@@ -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}' — "
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user