diff --git a/docs/design/atlas-generator-refinement-notes.md b/docs/design/atlas-generator-refinement-notes.md new file mode 100644 index 000000000..597bb2f76 --- /dev/null +++ b/docs/design/atlas-generator-refinement-notes.md @@ -0,0 +1,255 @@ +# Atlas Generator Refinement Notes — Sprint 36 + +**Scope:** Systems-level sanity pass across 273 inhabited bodies (ticket #838). +**Date:** 2026-04-19 +**Author:** Gestalt (systems) + +This document records systematic generator artifacts found during the Sprint 36 atlas refinement pass. Each section describes the pattern, its severity, and the recommended generator patch. + +--- + +## 1. Cross-Body City Name Collisions (SEVERE) + +The Gemma naming pipeline exhausted its per-corridor vocabulary and defaulted to repeating high-probability names across bodies. 49 city names appear on more than one body; the worst offenders: + +| Name | Bodies | Corridor | +|---|---|---| +| "Jade Harbor" | 20 | east_reach | +| "Fort Iron" | 10 | deep_frontier | +| "Forum Veritas" | 9 | core | +| "Ridge Marker" | 8 | deep_frontier | +| "Eisenstadt" | 7 | west_reach | +| "Fjordheim" | 6 | west_reach | +| "Fjordholm" | 6 | west_reach / north_reach | +| "Ridge Line" | 6 | deep_frontier | +| "Dusty Gully" | 5 | north_reach | +| "Eisenberg" | 5 | west_reach | +| "Eisenfels" | 5 | west_reach | +| "Hanseong" | 5 | east_reach | + +**Root cause:** The dedup set in `gemma_naming.py` tracks taken names per `(system_id, feature_type)` — only within a single system. Cross-system dedup does not exist. Bodies in different systems can receive identical names from the same high-probability tokens. + +**Fix required:** Implement a global (or corridor-scoped) name registry that persists across system boundaries during batch runs. The `discover_bodies()` / `name_features_batch()` pipeline should seed the taken list from atlas_cities before processing each body, not just from within the current system. + +**Resolution (Sprint 36):** Mellanie completed a full sweep eliminating all city collisions across 273 inhabited bodies (committed 48b73404). Clusters eliminated include Forum Veritas ×10, Jade Harbor ×19, Fort Iron ×10, Eisenstadt ×7, Fjordheim/Fjordholm ×6 each, Eisenberg/Eisenfels/Hanseong ×5 each, and 20+ smaller pairs. City collision count is now zero. + +--- + +## 2. Cross-Body Mountain Name Collisions (SEVERE) + +The same problem afflicts mountain ranges at a larger scale. From atlas_mountain_ranges (15,190 total features across all bodies): + +| Name | Bodies | +|---|---| +| "" (empty/unnamed) | 186 bodies | +| "Riverbend" | 39 bodies | +| "Valley Floor" | 36 bodies | +| "Steinbruch" | 26 bodies | +| "Ridge Line" | 26 bodies | +| "Ridge Crest" | 25 bodies | +| "Gyeongju" | 24 bodies | +| "Bamboo Grove" | 24 bodies | +| "Zen Garden" | 23 bodies | +| "River Bend" | 23 bodies | +| "Ballynahown" | 23 bodies | +| "Oakhaven" | 22 bodies | +| "Feldberg" | 22 bodies | +| "Rio Grande" | 21 bodies | +| "Hanseong" | 21 bodies | + +**186 empty-name mountain ranges** — the generator simply failed to produce a name for these features. They exist in the markers.json with `"name": ""`. + +**Root cause:** Same as city collisions — no cross-system dedup. Additionally, mountain ranges are more numerous per body (avg ~8-12 per inhabited body) so the in-system pool depletes faster. + +**Fix required:** +1. Cross-system mountain name dedup (same approach as city fix above). +2. Empty-name fallback logic: if Gemma returns an empty string or fails to generate a name, retry with a reduced temperature / different prompt pool entry, then fall back to a deterministic constructed name (`{body_name} Range {N}` is ugly but better than empty). + +--- + +## 3. Mountain Suffix Monotony (MEDIUM) + +On per-body passes, certain corridors show suffix clustering that makes mountain ranges feel templated rather than settled. The Vuurkloof (GJ35c) case was flagged in PR #130: 50% of mountains ended in `-rant` (Afrikaans for "edge/cliff"). This was a sampling artifact — the naming pipeline learned the pattern and reinforced it. + +**Pattern:** When a corridor has a high-frequency suffix in its few-shot examples, Gemma completes with that suffix disproportionately. West_reach bodies show heavy `-berg` clustering; east_reach bodies show `-san` and `-yama` clustering. + +**Fix required:** Post-generation suffix dedup — if >40% of a body's mountain names share the same trailing word/morpheme, re-query for the excess features with an explicit instruction to avoid that suffix. + +--- + +## 4. Directional Compass Labels as Feature Names (MEDIUM) + +Several template bodies (bodies with hand-authored names that the pipeline preserves) used pure directional compass labels for mountain ranges: + +- Estrade (GJ280Ad): "Eastern Shelf", "Western Range", "Southern Heights" (all three mountains were compass labels) +- Cairnside (GJ892d): "Westwall Range" (directional) + +These convey no cultural or geographic character — they're the naming equivalent of "Mountain A, B, C." + +**Fix applied (Sprint 36):** Estrade mountains renamed to Parallax Scarp, Vantage Ridge, Ledger Peaks. Cairnside "Westwall Range" renamed to Kappa Escarpment. + +**Generator fix:** The Gemma few-shot pool entries in `_MOUNTAIN_POOLS` should explicitly include a negative example showing "Eastern Range / Northern Heights" as patterns to avoid, with a note: "Settlers name places after events, people, or features they see — not compass directions." + +--- + +## 5. Zero Cross-Cultural Mixing on Corridor-Mismatched Bodies (MEDIUM) + +Vuurkloof (GJ35c, south_reach corridor) was pure Afrikaans monoculture. The body's GTTR explicitly describes three centuries of Kumasi corridor influence and a transit-connected hospitality workforce, yet no Akan, Iberian, or Portuguese names existed in any feature category. + +**Root cause:** The Gemma pipeline uses `cultural_corridor` to select naming palette (south_reach → Iberian/Portuguese) but the founding-culture context in the GTTR is not available to the naming model. When the founding culture and corridor palette diverge, the generator defaults to one or the other, not a blend. + +**Fix applied (Sprint 36):** Vuurkloof mountains: Skerprant → Kwahu Scarp (Akan), Asrant → Crista das Cinzas (Portuguese), Waterrant → Bosomtwe Shelf (Akan). River: Rooistroom → Obotan (Akan). Ocean: Suidelike Poel → Lagoa do Sul (Portuguese). + +**Generator fix:** The `gemma_naming.py` pipeline already reads `gttr_hook` per body. It should parse founding-culture cues from that hook and blend them with the corridor palette. A simple keyword detector for cultural markers (Afrikaans, Cape, Akan, Kumasi, etc.) could drive a `founding_culture_weight` that biases 30% of names toward founding-culture roots. + +--- + +## 6. River Abstract/Navigational Naming (LOW-MEDIUM) + +73 river names were flagged as abstract or navigational (using terms like "Flow", "Current", "Meridian", "Northern Flow"). Examples: + +- "Delta Flow", "Northern Flow", "Celestial Flow" — generic +- "The Meridian" — navigational abstraction +- "Fogo Current", "Lagos Current", "M'Banza Current" — ocean-current framing applied to rivers + +**Note:** "X Current" is appropriate for ocean surface currents; it reads oddly as a river name. Rivers should be named for features, people, or events, not for their direction of flow. + +**Root cause:** The `_RIVER_POOLS` in `gemma_naming.py` include "current" and "flow" as acceptable completions, and some few-shot examples teach this pattern for certain corridors. + +**Fix required:** Move "current" and "flow" suffix examples out of river pools and into ocean/sea pools only. Add a post-generation filter that flags river names ending in "Flow" or "Current" for re-query. + +--- + +## 7. Coverage Gaps — Inhabited Bodies Missing Cities (AUDIT) + +6 of 273 inhabited bodies have no cities in atlas_cities: + +| Body | Name | Corridor | Class | +|---|---|---|---| +| GJ0d | Earth | sol-gateway-axis | temperate | +| GJ0d-1 | Luna | sol-gateway-axis | barren | +| GJ0e | Mars | sol-gateway-axis | arid | +| GJ0f-2 | Europa | sol-gateway-axis | frozen | +| GJ3522-belt | Pilbara Belt | core | — | +| GJ820B-belt | — | core | — | + +**Earth, Luna, Mars, Europa** — deferred to Paula's #849 core-world cohesion pass (Sol system, hop 0). + +**Pilbara Belt, GJ820B-belt** — asteroid belts. These may not need traditional city placements. Recommend clarifying whether belt bodies should have mining stations marked as `kind: "outpost"` rather than cities, or be excluded from city generation entirely. + +--- + +## 8. Road/Railroad Naming Gap (SEVERE — now fixed) + +80% of all roads (37/46) and railroads (37/44) across inhabited bodies had empty names. The infrastructure geometry was generated correctly but the naming pipeline was never applied to road/railroad features — only to geographic features (cities, rivers, oceans, mountains). + +**Fix applied (Sprint 36):** Named all 37 unnamed roads and 37 unnamed railroads using the city-pair convention: `{Capital}–{Destination} {corridor_suffix}` (corridor suffix: "Corridor" for core, "Road" for north_reach, "Estrada" for south_reach, "Strasse" for west_reach, "Track" for deep_frontier, "Express/Line" for railroads). + +**Generator fix:** Extend the Gemma naming pipeline to include `roads` and `railroads` sections. Alternatively, a deterministic naming step from city pairs is sufficient — road names don't need cultural variation, just clarity. + +--- + +## Metrics Before vs After Sprint 36 Refinement + +| Metric | Before | After | +|---|---|---| +| Cross-body city collision names | 49 | 0 (Mellanie Sprint 36 full sweep) | +| Worst collision ("Jade Harbor") | 20 bodies | 0 (eliminated) | +| Unnamed roads | 37 / 46 (80%) | 0 / 46 (0%) | +| Unnamed railroads | 37 / 44 (84%) | 0 / 44 (0%) | +| Mountain cardinal-direction names (inhabited bodies) | ~45+ | 14 / 1638 (0%) | + +--- + +## Fixes Applied This Sprint + +### Pass 1 — Template bodies (PR #130 review) + +| Body | Body Name | System | What Changed | +|---|---|---|---| +| GJ892d | Cairnside | GJ 892 (Cairnside) | "Westwall Range" → "Kappa Escarpment" | +| GJ280Ad | Estrade | GJ 280A (Parallax) | "Eastern Shelf" → "Parallax Scarp"; "Western Range" → "Vantage Ridge"; "Southern Heights" → "Ledger Peaks" | +| GJ35c | Vuurkloof | GJ 35 (Vuurkloof) | Mountains: Skerprant → Kwahu Scarp, Asrant → Crista das Cinzas, Waterrant → Bosomtwe Shelf; River: Rooistroom → Obotan; Ocean: Suidelike Poel → Lagoa do Sul | + +### Pass 2 — Mid-tier bodies (severity-ranked pass) + +| Body | Body Name | System | What Changed | +|---|---|---|---| +| GJ7547c | Brandwacht | Skemeraand | 6 cardinal mountains → Afrikaans names; city "Ridge Marker" → "Wagpos" | +| GJ528Ac | Klaarstroom | Ouplaas | 4 cardinal mountains → Afrikaans names; city "Ridge Line" → "Klaardorp"; river "Riverbend" → "Die Draai" | +| GJ68f | Winter | Schuilhoek | All 6 cardinal/navigational rivers renamed to Afrikaans; cities "Dust Bowl Flats"/"Barren Meadow" → "Stofkamp"/"Kaalveld" | +| GJ68d | Lente | Schuilhoek | Wrong-type mountain names removed; 3 landscape-desc rivers → Afrikaans; 2 cap cities renamed | +| GJ667Ad | Geelong | New Ballarat | 2 wrong-type mountain names → Anglo-Australian; 2 cap cities → Australian flora names | +| GJ661Ad | Ys | Crown's Hollow | 2 collision city names → Anglo-Saxon unique names | +| GJ15Ac | Gongshu | Lu Ban | Wrong-type mountain; 2 collision cities → institutional core names; 1 collision river | +| GJ879d | Patiala | Singh's Landing | "Billabong" (water concept) + 6 cardinal mountains → Punjabi names; "Dusty Gully" → "Phillaur" | +| GJ556c | Idanha | Recanto | 4 cardinal/wrong-type mountains → Portuguese names; cap city → "Miradouro" | +| GJ138c | Portel | Sertão | Cap city → "Marco Sertão"; 4 concatenated river names → Portuguese | +| GJ174c | Clausthal | Tiefenbach | Cardinal + wrong-type mountains → German names; cap city → "Bergstation"; 3 wrong-type rivers | +| GJ421Bc | Serpa | Pedra Seca | 4 wrong-type mountains (flatland/valley floor used as mountains) → Portuguese names; cap city | +| GJ566Ac | Haodu | Haodu | "Jade Harbor" (worst collision, 20 bodies) → "Lianyun Harbor" | +| GJ674c | Provenance | Provenance | "Capitol Heights" → "Provenance Heights" | +| GJ68c | Zomer | Schuilhoek | 2 collision city names → Afrikaans | + +### Pass 2 — Infrastructure naming (all inhabited bodies) + +All 37 unnamed roads and 37 unnamed railroads across 36+ inhabited bodies were named using the city-pair convention. Bodies touched: GJ71d, GJ144d, GJ144e, GJ725Bc, GJ166Ac, GJ251c, GJ3877c, GJ674c, GJ699b, GJ1286e, GJ15Ac, GJ447c, GJ768f, GJ783Ae, GJ1116Ac, GJ1289c, GJ273c, GJ3325d, GJ3622c, GJ411c, GJ475e, GJ566Ac, GJ667Ad, GJ667Bd, GJ68c, GJ68d, GJ68e, GJ68f, GJ680d, GJ75d, GJ877c, GJ879d, GJ1156d, GJ661Ad, GJ780e, GJ34Ad. + +All bodies re-synced via `generate_atlas.py --body ` and verified in atlas_* tables. + +--- + +## Deferred to Paula (#849) + +- Edict (GJ244Ad / Sirius system): "Westwall" was not present in current markers.json or DB — either removed in a prior pass or the query data was stale. Paula's Sprint 36 pass renamed "Keel Ridge" → "Charter Spur" and "Sanction Ridge" → "The Statute". Edict mountains are clean. "Accord Peaks" cross-reference with Estrade's "Accord Run" river was evaluated and deemed acceptable (different feature types, no collision). +- Sol system bodies: Earth, Luna, Mars, Europa — missing city placements, white-glove treatment needed. +- Lendel (GJ380c / Groombridge system): check for any quality issues. + +--- + +## 9. POI Audit — Sprint 36 (LOW severity) + +**Scope:** `atlas_pois` and `atlas_body_grids` audited post-#838. + +### atlas_body_grids +Pure structural data (body_id, grid_w, grid_h, updated_at). No name column. **Clean — no action required.** + +### atlas_pois +287 total POIs across 267 inhabited bodies. Kind distribution: 267 transit (gate terminals), 15 institutional, 4 commercial, 1 corporate. + +**Zero empty names.** All 267 transit POIs have names. Institutional/commercial/corporate POIs are all hand-authored (template bodies only) and clean. + +**Cross-body duplicates (LOW):** + +| Name | Bodies | Note | +|---|---|---| +| "North Fork" | 5 | Geographic feature name used as transit POI — reads as generic | +| "Transit Hub" | 4 | Generator fallback — no locally grounded name derived | +| "Shizuka Port" | 3 | east_reach name on 3 separate bodies | +| "Ordnungshof" | 3 | west_reach name on 3 separate bodies | +| 8 others | 2 each | Minor | + +**Assessment:** Severity is LOW. Max collision depth is 5 bodies ("North Fork") vs. 20 for worst city collision. No empty names. The non-transit POIs (institutional/commercial/corporate) are entirely hand-authored and show no issues. Transit POIs are the only generator output category — most are correctly named "{Capital} Gate Terminal" or "{Body} Gate Terminal". + +**No hand-fixes required this sprint.** The 4× "Transit Hub" entries are the only meaningful quality gap (generic fallback), but transit POIs are low-visibility in Phase 3 (Phase 1/2 priority). + +**Generator fixes recommended (add to #853):** + +7. **Transit POI deterministic naming** — derive gate terminal name from body's capital city: `{capital_name} Gate Terminal`. Current fallback to "Transit Hub" is a generator gap, same root cause as unnamed roads/railroads. +8. **Cross-system POI dedup** — same approach as city/mountain dedup (global taken set per feature type). + +--- + +## Generator Patches Required (Future Ticket) + +Recommend creating a generator-patch ticket to address: + +1. **Cross-system city name dedup** — seed taken list from global atlas_cities +2. **Cross-system mountain name dedup** — same approach +3. **Empty-name fallback** — retry logic + deterministic fallback when generation fails +4. **Suffix monotony post-filter** — re-query if >40% same suffix per body +5. **Founding culture blend** — parse gttr_hook for cultural cues, blend with corridor palette +6. **River "Flow/Current" filter** — move these to ocean pools, post-gen filter on rivers +7. **Transit POI deterministic naming** — derive from capital city name, eliminate "Transit Hub" fallback +8. **Cross-system POI name dedup** — extend global dedup to atlas_pois +9. **River/ocean cross-body name dedup (secondary/uninhabited bodies)** — Mellanie's Sprint 36 sweep confirmed river/ocean collisions remain on secondary and uninhabited bodies (Rio Grande ×23, Steinbruch ×19, others). Inhabited body rivers were addressed in passes 1–2; uninhabited body rivers require a separate scripted dedup pass. Same root cause as city/mountain: no global taken set in Gemma pipeline. diff --git a/tooling/planet-gen/atlas_quality_analysis.py b/tooling/planet-gen/atlas_quality_analysis.py new file mode 100644 index 000000000..aa93aa00a --- /dev/null +++ b/tooling/planet-gen/atlas_quality_analysis.py @@ -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() diff --git a/tooling/planet-gen/refine_log_849.md b/tooling/planet-gen/refine_log_849.md index 655cee2a8..0ee8727ec 100644 --- a/tooling/planet-gen/refine_log_849.md +++ b/tooling/planet-gen/refine_log_849.md @@ -234,7 +234,37 @@ in log and move on if already above quality bar." #### GJ0d — Earth (8.5B pop, core world) -50 cities from earth_features.json (London, Tokyo, Delhi, Shanghai, etc.). sol_import.py +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. + +**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. @@ -246,7 +276,7 @@ ocean (area_fraction=0.7049) represents Earth's interconnected world ocean. | 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` ✓ +**DB sync:** `generate_atlas.py --body GJ0d` ✓ (11 cities, trimmed from 50) --- @@ -316,20 +346,58 @@ Team-lead confirmed: defer this sprint. Finding documented for future pass. --- -## Audit metrics (before/after comparison) +## Audit metrics — final confirmed delta -All audited systems post-fix: +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`. -| System | Empty names before | After | Lazy outputs before | After | Cardinals before | After | Key issues resolved | -|--------|-------------------|----|--------------------|----|-----------------|-------|-----| -| GJ 144 (Ran) | 0 | 0 | 10+ | 0 | 1 | 0 | Aldren/GJ380c cross-system collision fixed | -| GJ 71 (Tau Ceti) | 0 | 0 | 2 | 0 | 0 | 0 | Concordia, Basilica Nova cross-body collisions | -| GJ 244A (Sirius) | 0 | 0 | 2 | 0 | 3 | 0 | Westwall not present; Cairnside resolved | -| GJ 380 (Groombridge) | 0 | 0 | 2 | 0 | 0 | 0 | Lazy suffixes on two #833 features | -| GJ 699 (Barnard's) | 0 | 0 | 1 | 0 | 0 | 0 | Civic vocab on rivers/oceans; street addresses on moon | -| GJ 0 (Sol) | 33 | 0 | 0 | 0 | 0 | 0 | All auto-detected null-name features named | +### Inhabited body targets (in scope for this ticket) -**Total name edits across all bodies:** 23 (non-Sol) + 33 (Sol) = 56 total. +| 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. + +**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) @@ -378,7 +446,7 @@ SR_DB_PATH="$(pwd)/server/data/systems.db" tooling/db/sqlite-query \ - `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) +- `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) diff --git a/wiki/star-systems/GJ-0/bodies/GJ0d/markers.json b/wiki/star-systems/GJ-0/bodies/GJ0d/markers.json index bbddbe976..0e4fa639d 100644 --- a/wiki/star-systems/GJ-0/bodies/GJ0d/markers.json +++ b/wiki/star-systems/GJ-0/bodies/GJ0d/markers.json @@ -863,33 +863,6 @@ ], "population": 12700000 }, - { - "id": "city_paris", - "name": "Paris", - "center": [ - 80, - 261 - ], - "population": 11000000 - }, - { - "id": "city_berlin", - "name": "Berlin", - "center": [ - 77, - 269 - ], - "population": 3700000 - }, - { - "id": "city_mexico_city", - "name": "Mexico City", - "center": [ - 107, - 101 - ], - "population": 21800000 - }, { "id": "city_new_york", "name": "New York", @@ -899,33 +872,6 @@ ], "population": 20100000 }, - { - "id": "city_los_angeles", - "name": "Los Angeles", - "center": [ - 93, - 95 - ], - "population": 13200000 - }, - { - "id": "city_toronto", - "name": "Toronto", - "center": [ - 84, - 123 - ], - "population": 6200000 - }, - { - "id": "city_chicago", - "name": "Chicago", - "center": [ - 85, - 115 - ], - "population": 9500000 - }, { "id": "city_s\u00e3o_paulo", "name": "S\u00e3o Paulo", @@ -935,60 +881,6 @@ ], "population": 22400000 }, - { - "id": "city_lima", - "name": "Lima", - "center": [ - 133, - 131 - ], - "population": 10700000 - }, - { - "id": "city_bogot\u00e1", - "name": "Bogot\u00e1", - "center": [ - 121, - 135 - ], - "population": 11300000 - }, - { - "id": "city_rio_de_janeiro", - "name": "Rio de Janeiro", - "center": [ - 142, - 168 - ], - "population": 13500000 - }, - { - "id": "city_buenos_aires", - "name": "Buenos Aires", - "center": [ - 151, - 153 - ], - "population": 15200000 - }, - { - "id": "city_lagos", - "name": "Lagos", - "center": [ - 120, - 262 - ], - "population": 15400000 - }, - { - "id": "city_kinshasa", - "name": "Kinshasa", - "center": [ - 124, - 270 - ], - "population": 15600000 - }, { "id": "city_cairo", "name": "Cairo", @@ -998,69 +890,6 @@ ], "population": 21300000 }, - { - "id": "city_johannesburg", - "name": "Johannesburg", - "center": [ - 156, - 279 - ], - "population": 6000000 - }, - { - "id": "city_nairobi", - "name": "Nairobi", - "center": [ - 128, - 293 - ], - "population": 5100000 - }, - { - "id": "city_tehran", - "name": "Tehran", - "center": [ - 92, - 308 - ], - "population": 9000000 - }, - { - "id": "city_baghdad", - "name": "Baghdad", - "center": [ - 94, - 303 - ], - "population": 8100000 - }, - { - "id": "city_riyadh", - "name": "Riyadh", - "center": [ - 103, - 304 - ], - "population": 7700000 - }, - { - "id": "city_ankara", - "name": "Ankara", - "center": [ - 87, - 284 - ], - "population": 5700000 - }, - { - "id": "city_karachi", - "name": "Karachi", - "center": [ - 103, - 327 - ], - "population": 16500000 - }, { "id": "city_tokyo", "name": "Tokyo", @@ -1079,15 +908,6 @@ ], "population": 32900000 }, - { - "id": "city_shanghai", - "name": "Shanghai", - "center": [ - 97, - 387 - ], - "population": 28500000 - }, { "id": "city_beijing", "name": "Beijing", @@ -1097,168 +917,6 @@ ], "population": 21500000 }, - { - "id": "city_mumbai", - "name": "Mumbai", - "center": [ - 107, - 333 - ], - "population": 21700000 - }, - { - "id": "city_jakarta", - "name": "Jakarta", - "center": [ - 120, - 374 - ], - "population": 34500000 - }, - { - "id": "city_dhaka", - "name": "Dhaka", - "center": [ - 103, - 351 - ], - "population": 23000000 - }, - { - "id": "city_manila", - "name": "Manila", - "center": [ - 109, - 388 - ], - "population": 14400000 - }, - { - "id": "city_bangkok", - "name": "Bangkok", - "center": [ - 109, - 370 - ], - "population": 11000000 - }, - { - "id": "city_seoul", - "name": "Seoul", - "center": [ - 90, - 393 - ], - "population": 9800000 - }, - { - "id": "city_osaka", - "name": "Osaka", - "center": [ - 93, - 398 - ], - "population": 19300000 - }, - { - "id": "city_chongqing", - "name": "Chongqing", - "center": [ - 97, - 375 - ], - "population": 17000000 - }, - { - "id": "city_kolkata", - "name": "Kolkata", - "center": [ - 103, - 349 - ], - "population": 15100000 - }, - { - "id": "city_lahore", - "name": "Lahore", - "center": [ - 97, - 336 - ], - "population": 14000000 - }, - { - "id": "city_shenzhen", - "name": "Shenzhen", - "center": [ - 104, - 382 - ], - "population": 13400000 - }, - { - "id": "city_bangalore", - "name": "Bangalore", - "center": [ - 111, - 339 - ], - "population": 13200000 - }, - { - "id": "city_ho_chi_minh_city", - "name": "Ho Chi Minh City", - "center": [ - 113, - 374 - ], - "population": 9300000 - }, - { - "id": "city_luanda", - "name": "Luanda", - "center": [ - 132, - 268 - ], - "population": 9000000 - }, - { - "id": "city_addis_ababa", - "name": "Addis Ababa", - "center": [ - 119, - 292 - ], - "population": 5500000 - }, - { - "id": "city_santiago", - "name": "Santiago", - "center": [ - 147, - 137 - ], - "population": 7000000 - }, - { - "id": "city_taipei", - "name": "Taipei", - "center": [ - 103, - 388 - ], - "population": 7000000 - }, - { - "id": "city_hong_kong", - "name": "Hong Kong", - "center": [ - 104, - 382 - ], - "population": 7500000 - }, { "id": "city_singapore", "name": "Singapore", @@ -1276,15 +934,6 @@ 421 ], "population": 5300000 - }, - { - "id": "city_casablanca", - "name": "Casablanca", - "center": [ - 93, - 249 - ], - "population": 3800000 } ], "railroads": [],