diff --git a/Makefile b/Makefile index 1184b2b55..2474dffe6 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null) .PHONY: help setup build check-protocol client server game stop test lint lint-python setup-venv ci ci-client ci-server clean \ decisions-sync decisions-coverage decisions-active decisions-orphan decisions-orphan-tickets \ db-backup db-install validate-content check-fact-ids setup-hooks install-hooks \ - audit deny atlas-verify economy-db atlas-generate regen-db check-systems-db \ + audit deny atlas-verify economy-db regen-db check-systems-db \ pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \ pre-pr-server pre-pr-client pre-pr-content \ fixtures-client fixtures-gauntlet golden-diff golden-update \ @@ -58,11 +58,9 @@ help: @echo " make star-map-data Regenerate client/data/star_map_data.json from systems.db + wiki" @echo " make check-star-map Assert star_map_data.json is up to date (part of pre-pr-client)" @echo " make economy-db Import economics data into systems.db (TOML/JSON → SQLite)" - @echo " make atlas-generate Generate atlas city/road/rail markers for all inhabited bodies" @echo " make regen-db Regenerate systems.db from all sources + stamp meta table (#855)" @echo " make check-systems-db Verify systems.db meta stamp matches current generator sources" @echo " make install-hooks Install pre-push + pre-commit git hooks (once per clone)" - @echo " make test-atlas-determinism Determinism smoke test for generate_atlas.py (#847)" @echo " make fixtures-client Generate GDScript->Rust cross-encoder fixtures (#475)" @echo " make golden-diff Show diff if golden file output has changed" @echo " make golden-update Regenerate golden file and stage for commit" @@ -232,9 +230,6 @@ test-ipc-integration: test-ipc-benchmark: tests/run-ipc-benchmark -test-atlas-determinism: ## Determinism smoke test for generate_atlas.py (#847) - tests/run-atlas-determinism - # --- Clean --- clean-imports: @@ -345,20 +340,6 @@ economy-db: ## Import economics data (commodities, chains, gate links) into sys @tooling/generate-brands @python3 tooling/economy-db/import_economics.py -atlas-generate: ## Generate atlas markers (cities, roads, rail) for all inhabited bodies (#832) - @# Loud guard: generate_atlas.py reads bodies with a non-NULL terrain_reference. - @# If populate_terrain_reference.py has not run on a fresh DB, the generator - @# silently processes zero bodies and exits 0 — fail fast instead. - @count=$$(python3 -c "import sqlite3; c = sqlite3.connect('server/data/systems.db'); print(c.execute('SELECT COUNT(*) FROM bodies WHERE terrain_reference IS NOT NULL').fetchone()[0])"); \ - if [ "$$count" = "0" ]; then \ - echo "ERROR: no bodies have terrain_reference populated yet."; \ - echo "Run: python3 tooling/planet-gen/populate_terrain_reference.py"; \ - echo "(This is a prerequisite for atlas-generate — see D-191 §9 pipeline order.)"; \ - exit 1; \ - fi; \ - echo " [guard] $$count bodies with terrain_reference — proceeding." - @python3 tooling/planet-gen/generate_atlas.py --seed 42 - regen-db: ## Regenerate systems.db from all sources and stamp meta table (#855, #856) @# Run as a single shell so `set -e` covers all steps. Without this @# each recipe line was a fresh shell and a failure in step 1 did not @@ -366,13 +347,14 @@ regen-db: ## Regenerate systems.db from all sources and stamp meta table (#855, @# (PR #136 review T4). import_economics' exit code 2 is a valid @# coverage-gate-warning state (DB and stamp committed), not an error, @# so it's explicitly tolerated. Any other non-zero exit halts the - @# pipeline immediately. + @# pipeline immediately. The atlas city/road/river geometry generator + @# was retired in #951 (D-223); import_economics now owns the atlas + @# index — it loads the names-only pool into atlas_city_names and empties + @# the geometry tables (the server cascade fills them, Phase 4). @set -e; \ echo " [regen-db] Importing economics data (runs generate_brands internally)..."; \ ec=0; python3 tooling/economy-db/import_economics.py || ec=$$?; \ if [ $$ec -ne 0 ] && [ $$ec -ne 2 ]; then exit $$ec; fi; \ - echo " [regen-db] Running atlas generator..."; \ - python3 tooling/planet-gen/generate_atlas.py --seed 42; \ echo ""; \ echo " regen-db complete — systems.db is up to date and stamped."; \ echo " Stage it with: git add server/data/systems.db" diff --git a/server/src/atlas/heightmap.rs b/server/src/atlas/heightmap.rs index 01bc28685..f64a76260 100644 --- a/server/src/atlas/heightmap.rs +++ b/server/src/atlas/heightmap.rs @@ -13,7 +13,7 @@ use rusqlite::{params, Connection}; use thiserror::Error; -/// Canonical grid dimensions matching the Python pipeline (generate_atlas.py). +/// Canonical grid dimensions matching the Python pipeline (atlas_common.py). pub const GRID_W: u32 = 512; pub const GRID_H: u32 = 256; diff --git a/tests/run-all b/tests/run-all index 9c4b80842..4d198b51b 100755 --- a/tests/run-all +++ b/tests/run-all @@ -1,7 +1,7 @@ #!/usr/bin/env bash # tests/run-all: Run all test suites in order (D-030) # Invokes run-rust, run-godot, run-ipc-fixtures, run-ipc-protocol, -# run-ipc-integration, run-visual, run-atlas-determinism. +# run-ipc-integration, run-visual. # Exit: 0 = all suites pass, non-zero = any suite failed # Stdout: {"suite":"all","total":N,"passed":N,"failed":N,"duration_ms":N,"suites":[...]} set -euo pipefail @@ -27,7 +27,6 @@ SUITES=( run-ipc-protocol run-ipc-integration run-visual - run-atlas-determinism ) START_MS=$(date +%s%3N) diff --git a/tests/run-atlas-determinism b/tests/run-atlas-determinism deleted file mode 100755 index 10e49180e..000000000 --- a/tests/run-atlas-determinism +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash -# tests/run-atlas-determinism: Determinism smoke test for generate_atlas.py (#847) -# -# Imports generate_atlas as a Python module, calls process_body() twice with -# seed=42 and dry_run=True, compares the returned markers dicts as JSON. -# No wiki files are written or modified. -# -# Purpose: cheap guardrail against determinism regressions in terrain analysis, -# city placement, A* road routing, infrastructure MST, and gate terminal -# placement. GJ892f is a domed body (population=300, 1 city) — the smallest -# well-exercised case in the atlas pipeline. -# -# Spec ref: #847 -# Exit: 0 = deterministic (pass), non-zero = failure -# Stdout: {"suite":"atlas-determinism","total":1,"passed":N,"failed":N,"duration_ms":N} -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -START_MS=$(date +%s%3N) - -# Write the comparison script to a real file so that the generate_atlas venv -# bootstrap (os.execv) can re-exec it from the venv Python when needed. -# A heredoc (python3 - <<'EOF') does not work after os.execv because stdin -# has already been consumed. -HELPER=$(mktemp /tmp/atlas_det_helper.XXXXXX.py) -trap "rm -f '$HELPER'" EXIT - -cat > "$HELPER" << 'PYEOF' -"""Atlas determinism helper — called by tests/run-atlas-determinism (#847). - -Imports generate_atlas as a module and calls process_body() twice with -dry_run=True. Compares the returned markers dicts as JSON. Exits 0 if -identical, 1 if they differ, 2 on setup/import failure. -""" -import json -import os -import sqlite3 -import sys -from pathlib import Path - -REPO_ROOT = Path(os.environ["SR_REPO_ROOT"]) -sys.path.insert(0, str(REPO_ROOT / "tooling" / "planet-gen")) - -try: - import generate_atlas -except ImportError as e: - print(f"SKIP: generate_atlas import failed (missing deps?): {e}", file=sys.stderr) - sys.exit(2) - -DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" -BODY_ID = "GJ892f" # domed, pop=300 → exactly 1 city; minimal and fast -SEED = 42 - -if not DB_PATH.exists(): - print(f"SKIP: systems.db not found: {DB_PATH}", file=sys.stderr) - sys.exit(2) - -conn = sqlite3.connect(str(DB_PATH)) -conn.execute("PRAGMA foreign_keys=ON") -row = conn.execute(""" - SELECT b.body_id, b.system_id, b.terrain_reference, b.population, - b.settlement_pattern, b.planet_class, b.economic_role, - COALESCE(b.cultural_corridor, s.cultural_corridor) - FROM bodies b JOIN star_systems s ON b.system_id = s.system_id - WHERE b.body_id = ? -""", (BODY_ID,)).fetchone() -conn.close() - -if not row: - print(f"SKIP: {BODY_ID} not found in systems.db", file=sys.stderr) - sys.exit(2) - -body_info = dict(zip( - ("body_id", "system_id", "terrain_reference", "population", - "settlement_pattern", "planet_class", "economic_role", "cultural_corridor"), - row, -)) -body_info["population"] = body_info["population"] or 0 - -kwargs = dict( - body_info=body_info, - seed=SEED, - noise_factor=0.25, - dry_run=True, # no files written - force=True, # skip the already-populated check - verbose=False, -) - -a = generate_atlas.process_body(**kwargs) -b = generate_atlas.process_body(**kwargs) - -if a["status"] != "generated": - print(f"FAIL: run 1 status={a['status']} — {a.get('message', '')}", file=sys.stderr) - sys.exit(1) -if b["status"] != "generated": - print(f"FAIL: run 2 status={b['status']} — {b.get('message', '')}", file=sys.stderr) - sys.exit(1) - -json_a = json.dumps(a["markers"], indent=2) -json_b = json.dumps(b["markers"], indent=2) - -if json_a == json_b: - print(f"PASS: {BODY_ID} markers identical on two runs (seed={SEED})", file=sys.stderr) - sys.exit(0) -else: - import difflib - diff = "\n".join(list(difflib.unified_diff( - json_a.splitlines(), json_b.splitlines(), lineterm="", - fromfile="run1", tofile="run2", - ))[:40]) - print(f"FAIL: {BODY_ID} markers differ between run 1 and run 2 (seed={SEED})", file=sys.stderr) - print(diff, file=sys.stderr) - sys.exit(1) -PYEOF - -set +e -SR_REPO_ROOT="$REPO_ROOT" python3 "$HELPER" 2>&1 >&2 -EXIT_CODE=$? -set -e - -END_MS=$(date +%s%3N) -DURATION_MS=$((END_MS - START_MS)) - -# Exit code 2 = setup failure / missing deps → count as 0 tests (skip, not fail) -if [[ $EXIT_CODE -eq 0 ]]; then - PASSED=1; FAILED=0; TOTAL=1 -elif [[ $EXIT_CODE -eq 2 ]]; then - PASSED=0; FAILED=0; TOTAL=0 - echo " [atlas-determinism] SKIPPED (import failure or missing DB)" >&2 -else - PASSED=0; FAILED=1; TOTAL=1 -fi - -printf '{"suite":"atlas-determinism","total":%d,"passed":%d,"failed":%d,"duration_ms":%d}\n' \ - "$TOTAL" "$PASSED" "$FAILED" "$DURATION_MS" - -# Exit code 2 = venv/DB missing → treated as skip, not failure (exit 0 for run-all). -# Exit code 1 = determinism failure → exit 1 to fail CI. -[[ $EXIT_CODE -eq 2 ]] && exit 0 || exit $EXIT_CODE diff --git a/tooling/check-systems-db-stamp b/tooling/check-systems-db-stamp index b5caa2791..5129fcc9c 100644 --- a/tooling/check-systems-db-stamp +++ b/tooling/check-systems-db-stamp @@ -39,6 +39,12 @@ DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" # import_economics' source set includes the Rust generate_brands binary it now # invokes as a subroutine (#136 review T2/H3). Keep this list in sync with # IMPORT_ECONOMICS_SOURCES in tooling/economy-db/import_economics.py. +# The atlas geometry generator (generate_atlas) was retired in #951 (D-223). +# import_economics is now the sole regen-db generator that writes systems.db; it +# owns the atlas index (names-only pool + empty geometry tables). The surviving +# planet-gen importers (import_heightmaps, import_province_boundaries) are +# one-time build imports baked into the committed DB, not part of regen-db, so +# they are intentionally not stamped here. GENERATOR_SOURCES: dict[str, list[Path]] = { "import_economics": [ REPO_ROOT / "tooling" / "economy-db" / "import_economics.py", @@ -47,15 +53,6 @@ GENERATOR_SOURCES: dict[str, list[Path]] = { REPO_ROOT / "tooling" / "generate-brands", REPO_ROOT / "tooling" / "schema_version.py", ], - "generate_atlas": [ - REPO_ROOT / "tooling" / "planet-gen" / "generate_atlas.py", - REPO_ROOT / "tooling" / "planet-gen" / "gemma_naming.py", - REPO_ROOT / "tooling" / "planet-gen" / "naming_core.py", - REPO_ROOT / "tooling" / "planet-gen" / "import_city_names.py", - REPO_ROOT / "tooling" / "planet-gen" / "import_heightmaps.py", - REPO_ROOT / "tooling" / "planet-gen" / "import_province_boundaries.py", - REPO_ROOT / "tooling" / "schema_version.py", - ], } diff --git a/tooling/db/backfill_cultural_corridor.py b/tooling/db/backfill_cultural_corridor.py index 5f7cfc686..a1c730cf6 100755 --- a/tooling/db/backfill_cultural_corridor.py +++ b/tooling/db/backfill_cultural_corridor.py @@ -10,7 +10,7 @@ south_reach, east_reach, west_reach, deep_frontier). These two fields refer to the same concept: which arc of the reach the system belongs to. Leaving `cultural_corridor` NULL on 99%+ of rows defeats every downstream consumer that actually wants to filter by corridor -(gemma_naming.py, future atlas UI queries, narrative tools). +(the atlas UI, narrative tools, and the server generation cascade). This script treats `geographic_sector` as the source of truth and copies it into `cultural_corridor`: diff --git a/tooling/planet-gen/apply_name_fixes.py b/tooling/planet-gen/apply_name_fixes.py deleted file mode 100644 index 060bab0e6..000000000 --- a/tooling/planet-gen/apply_name_fixes.py +++ /dev/null @@ -1,300 +0,0 @@ -#!/usr/bin/env python3 -""" -apply_name_fixes.py — Apply curated name replacements to markers.json files. - -Usage: - python3 tooling/planet-gen/apply_name_fixes.py [--dry-run] - -Edits name fields only; all geometry (path, center, area_fraction) is preserved. -After running, call generate_atlas.py --body for each body to sync DB. - -Decisions: D-191 (markers.json format, corridor palettes) -""" -import json -import sys -from pathlib import Path - -REPO_ROOT = (Path(__file__).resolve().parent / ".." / "..").resolve() -WIKI = REPO_ROOT / "wiki" / "star-systems" - -DRY_RUN = "--dry-run" in sys.argv - -# --------------------------------------------------------------------------- -# Name replacement tables per body -# Format: { body_dir_key: { "feature_type": { "old_name": "new_name" } } } -# feature_type: cities | rivers | oceans | mountain_ranges | pois -# --------------------------------------------------------------------------- - -FIXES: dict[str, dict[str, dict[str, str]]] = { - - # ----------------------------------------------------------------------- - # GJ 144 — Ran system - # Verified against actual markers.json files (DB is stale). - # ----------------------------------------------------------------------- - - # Kallast (GJ144d) — 2B pop, agricultural, urban_concentrated, ocean world - # Current state (from markers.json): good Nordic naming pass done; residual - # lazy/generic oceans remain. Fix: rename remaining generics, add cross-ref - # with mountain "Seterfjellet" (ocean → "Seterfjord"), fix POI, and rename - # "Aldren Pass" to avoid cross-system collision with Lendel (GJ380c capital - # "Aldren" and river "The Aldren"). - # Narrative hook: "Rán's Landing" city (names the star), "Rán's Run" and - # "Ranfall Beck" rivers (shared Rán stem = cross-referencing done). Adding - # Seterfjord → Seterfjellet link completes the second cross-reference arc. - "GJ-144/bodies/GJ144d": { - "rivers": { - # Avoid cross-system stem collision with Lendel (GJ380c) "Aldren" - "Aldren Pass": "Randalfoss", - }, - "oceans": { - # Still generic — large ocean near mountains → name ties to Seterfjellet - "Boulder Bluff": "Keldmere", - # "High Mesa" for an ocean is nonsensical - "High Mesa": "Seterfjord", - }, - "pois": { - "Summit Point": "Kallast Gate Terminal", - }, - }, - - # Vethis (GJ144e) — 1.2B pop, agricultural, dispersed_rural - # Current state: partial naming pass done. Remaining: cardinals (West Bend), - # earth-echoes (Blue Ridge), generics (Stone Point, Ironside Run, Golden - # Valley, Broad Fork), "Ash" stem overuse (Ashbluff Sea + Ashstone Ridge + - # Ashvale = 3 features), "Iron" stem overuse (Irongate + Ironside Run = 2). - # Fixes cross-reference existing named features: - # Grey- stem: Greywash + Greywash Fork + Greystone Ridge (intentional arc) - # Thorn- stem: Thorncrests + Thornrun (mountain visible from river) - # Kel- stem: Kelbridge + Kelside Run (river serving the city) - # Veth- stem: Vethis + Veth Delta + Veth Mere (planet name echoed in geography) - "GJ-144/bodies/GJ144e": { - "rivers": { - "West Bend": "Greywash Fork", # cardinal → cross-refs Greywash river - "Blue Ridge": "Ashvale Beck", # earth-echo → cross-refs Ashvale city - "Stone Point": "Thornrun", # generic → cross-refs Thorncrests mtn - "Ironside Run": "Kelside Run", # Iron overuse → cross-refs Kelbridge - }, - "oceans": { - # Ashbluff Sea: "Ash" prefix already on Ashvale city + Ashstone Ridge mtn - "Ashbluff Sea": "Veth Mere", # Ash overuse → cross-refs Veth Delta - }, - "mountain_ranges": { - "Golden Valley": "Greymoor Range", # generic → cross-refs Greywash river - "Broad Fork": "Keld Spur", # generic → Nordic "keld" (spring) - # Ashstone Ridge: "Ash" overuse (Ashvale + Veth Mere rename frees slot) - "Ashstone Ridge": "Greystone Ridge", # Ash overuse → cross-refs Greywash - }, - "pois": { - "Ridge Crossing": "Vethis Gate Terminal", - }, - }, - - # ----------------------------------------------------------------------- - # GJ 71 — Tau Ceti system - # Verified against actual markers.json files (DB is stale). - # ----------------------------------------------------------------------- - - # Threshold (GJ71c) — 600M pop, agricultural, urban_concentrated - # Fix: replace "Aethelred" (Anglo-Saxon; breaks the all-Latin-personal-name - # pattern of the six rivers: Octavius, Septimus, Quintus, Valeria, Marcus). - # Narrative: All six rivers named after the original Commission survey team. - "GJ-71/bodies/GJ71c": { - "rivers": { - "Aethelred": "Gaius", - }, - }, - - # Arden (GJ71d) — 500M pop, agricultural, dispersed_rural - # Fix cross-body stem duplicates: - # "Concordia Hall" city → exact stem match with GJ71c ocean "Concordia" - # "Basilica Nova" river → exact name match with GJ71c POI "Basilica Nova" - # GJ71d-1 has been independently updated — Forum Major no longer conflicts. - # Capital "Palaestra" and other names are clean — keep. - "GJ-71/bodies/GJ71d": { - "cities": { - "Concordia Hall": "The Praxis", # Concordia = GJ71c sea - }, - "rivers": { - "Basilica Nova": "Via Principia", # exact match = GJ71c POI name - }, - }, - - # Verantis (GJ71d-1) — 20M pop, transit moon of Arden - # Already updated in a prior pass (capital=Praetorium, mountains all - # renamed to unique Latin institutional names). No changes needed. - # "GJ-71/bodies/GJ71d-1": {}, # skip - - # ----------------------------------------------------------------------- - # 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() diff --git a/tooling/planet-gen/earth_blocklist.txt b/tooling/planet-gen/earth_blocklist.txt index 954b03319..c024104da 100644 --- a/tooling/planet-gen/earth_blocklist.txt +++ b/tooling/planet-gen/earth_blocklist.txt @@ -1,4 +1,6 @@ -# Earth-name blocklist for gemma_naming.py (#833, D-191 §4). +# Earth-name blocklist (#833, D-191 §4). Retained as curated reference data +# after the Gemma naming pipeline was retired (#951, D-223) — see +# docs/gemma-naming-methodology.md. # # Per memory feedback_earth_echo_names: Earth-sounding names are OK and # expected — the setting is a blended reality. This file is a small, diff --git a/tooling/planet-gen/fix_fewshot_bleed.py b/tooling/planet-gen/fix_fewshot_bleed.py deleted file mode 100644 index 36ea080bf..000000000 --- a/tooling/planet-gen/fix_fewshot_bleed.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env python3 -"""Replace few-shot example names that bled into the output. - -The batch naming prompt uses Scottish Highland and Dutch colonial -examples. The Scottish ones (Glen Moray, Dunvegan Ridge, Torridon, -Cairn Brae, The Kelpie's Spine) leaked into 270 features. This script -replaces them with unique names from a combined Scottish/Welsh/Irish -pool, ensuring no collisions with the existing corpus. -""" - -import json -import sqlite3 -import sys -from collections import defaultdict -from pathlib import Path - -TOOLING_DIR = Path(__file__).resolve().parent -REPO_ROOT = (TOOLING_DIR / ".." / "..").resolve() -DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" -WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems" - -sys.path.insert(0, str(TOOLING_DIR)) -from generate_atlas import sync_markers_to_db - -# The few-shot names to replace -FEWSHOT_NAMES = { - "glen moray", "dunvegan ridge", "torridon", "cairn brae", "the kelpie's spine", - "kloosterbeek", "nieuw rijn", "hoogland run", "van diemen's creek", -} - -# Scottish / Welsh / Irish replacement pool — 300+ names to cover 270 replacements -# with room for Levenshtein filtering. Mix of geographic feature styles. -REPLACEMENT_POOL = [ - # Scottish - "Glenfinnan", "Dalwhinnie Pass", "Cairngorm", "Loch Maree", - "Kinlochleven", "Strathspey", "Brae Morar", "Skye Reach", - "Ardnamurchan", "Kintail", "Glen Affric", "Lochaber", - "Killiecrankie", "Rannoch Moor", "Glen Coe", "Strathnaver", - "Applecross", "Torrisdale", "Durness", "Assynt", - "Coigach", "Inverpolly", "Sandwood", "Cape Wrath", - "Sutherland", "Helmsdale", "Brora", "Golspie", - "Cromarty", "Dornoch", "Nairn", "Forres", - "Culbin", "Findhorn", "Spey Bay", "Buckie", - "Banff", "Fraserburgh", "Peterhead", "Cruden Bay", - "Slains", "Ythan", "Bennachie", "Morven", - "Lochnagar", "Braemar", "Balmoral", "Crathie", - "Ballater", "Dinnet", "Tarland", "Lumphanan", - "Corgarff", "Tomintoul", "Glenlivet", "Dufftown", - "Craigellachie", "Aberlour", "Knockando", "Archiestown", - "Rothes", "Elgin", "Lossiemouth", "Burghead", - "Kinloss", "Alves", "Pluscarden", "Dallas", - # Welsh - "Cwm Idwal", "Beddgelert", "Crib Goch", "Tryfan", - "Ogwen", "Llyn Padarn", "Dolgellau", "Harlech", - "Rhinog", "Cader Idris", "Barmouth", "Aberdovey", - "Tywyn", "Machynlleth", "Pumlumon", "Hafren", - "Elan Valley", "Claerwen", "Llandrindod", "Brecon", - "Pen y Fan", "Corn Du", "Crickhowell", "Llangorse", - "Talgarth", "Hay Bluff", "Mynydd Troed", "Mynydd Llangorse", - "Skirrid", "Blorenge", "Llanfoist", "Govilon", - "Gilwern", "Llangattock", "Crug Hywel", "Cwm Clydach", - "Pontneddfechan", "Ystradfellte", "Sgwd yr Eira", "Henrhyd", - "Carreg Cennen", "Dinefwr", "Llandeilo", "Dryslwyn", - "Tywi Valley", "Carmarthen", "Kidwelly", "Pembrey", - "Gower", "Rhossili", "Oxwich", "Port Eynon", - "Pennard", "Langland", "Caswell", "Mumbles", - "Merthyr Mawr", "Ogmore", "Dunraven", "Llantwit", - "Monknash", "Nash Point", "Aberthaw", "Fonmon", - # Irish - "Glendalough", "Lugnaquilla", "Glen Imaal", "Wicklow Gap", - "Sally Gap", "Kippure", "Djuce", "Maulin", - "Djouce", "Great Sugar Loaf", "Bray Head", "Killiney", - "Dalkey", "Howth", "Lambay", "Ireland's Eye", - "Malahide", "Portmarnock", "Donabate", "Skerries", - "Balbriggan", "Gormanston", "Bettystown", "Laytown", - "Slane", "Newgrange", "Dowth", "Knowth", - "Tara", "Trim", "Navan", "Kells", - "Loughcrew", "Oldcastle", "Castlepollard", "Fore", - "Delvin", "Mullingar", "Kilbeggan", "Tullamore", - "Clara", "Ferbane", "Banagher", "Shannonbridge", - "Clonmacnoise", "Ballinasloe", "Aughrim", "Loughrea", - "Portumna", "Mountshannon", "Killaloe", "Ballina", - "Nenagh", "Roscrea", "Templemore", "Thurles", - "Cashel", "Cahir", "Clonmel", "Carrick-on-Suir", - "Piltown", "Mooncoin", "Waterford", "Tramore", - "Bunmahon", "Ardmore", "Youghal", "Midleton", - "Cobh", "Crosshaven", "Kinsale", "Clonakilty", - "Skibbereen", "Bantry", "Glengarriff", "Kenmare", - "Sneem", "Caherdaniel", "Waterville", "Cahersiveen", - "Valentia", "Portmagee", "Skellig", "Dingle", - "Brandon", "Castlegregory", "Fenit", "Tralee", - "Listowel", "Ballybunion", "Tarbert", "Glin", - "Foynes", "Askeaton", "Adare", "Patrickswell", - # More Scottish/Gaelic to fill - "Stornoway", "Tarbert", "Scalpay", "Eriskay", - "Barra", "Vatersay", "Minguilay", "Pabbay", - "Berneray", "Monach Isles", "Balranald", "Lochmaddy", - "Benbecula", "Grimsay", "Ronay", "Wiay", - "Canna", "Rum", "Eigg", "Muck", - "Ardnish", "Arisaig", "Morar", "Mallaig", - "Knoydart", "Barrisdale", "Arnisdale", "Glenelg", - "Sandaig", "Brochs of Borve", "Callanish", "Garenin", - "Carloway", "Arnol", "Barvas", "Tolsta", - "Ness", "Europie", "Swainbost", "Skigersta", - # Additional Welsh/Irish - "Aberystwyth", "Llanberis", "Betws-y-Coed", "Conwy", - "Caernarfon", "Pwllheli", "Abersoch", "Nefyn", - "Llanbedrog", "Criccieth", "Porthmadog", "Portmeirion", - "Trawsfynydd", "Ffestiniog", "Blaenau", "Llyn Tegid", - "Corwen", "Llangollen", "Chirk", "Oswestry", -] - - -def load_global_names(conn): - """Load all existing names globally for uniqueness checking.""" - names = set() - for table in ['atlas_cities', 'atlas_rivers', 'atlas_mountain_ranges', - 'atlas_oceans', 'atlas_pois']: - rows = conn.execute( - f"SELECT lower(name) FROM {table} WHERE name IS NOT NULL AND name != ''" - ).fetchall() - names.update(r[0] for r in rows) - return names - - -def main(): - conn = sqlite3.connect(str(DB_PATH), timeout=30.0) - conn.execute("PRAGMA journal_mode=WAL") - conn.execute("PRAGMA busy_timeout=15000") - - global_names = load_global_names(conn) - print(f"Loaded {len(global_names)} existing names") - - # Build available replacements (not already in corpus) - available = [n for n in REPLACEMENT_POOL if n.lower() not in global_names] - print(f"Available replacements: {len(available)} (from pool of {len(REPLACEMENT_POOL)})") - - # Find all features that need replacement - replacements_needed = [] - for markers_path in sorted(WIKI_SYSTEMS.glob("*/bodies/*/markers.json")): - body_id = markers_path.parent.name - m = json.loads(markers_path.read_text()) - for section in ("cities", "rivers", "oceans", "mountain_ranges", "pois"): - for feat in m.get(section, []): - name = feat.get("name", "") - if name and name.lower() in FEWSHOT_NAMES: - replacements_needed.append((markers_path, body_id, section, feat)) - - print(f"Features to replace: {len(replacements_needed)}") - - if len(available) < len(replacements_needed): - print(f"WARNING: only {len(available)} replacements for {len(replacements_needed)} features") - print(" some features will keep their few-shot names") - - # Group by file, reload, replace, write - replacement_idx = 0 - used_per_body = defaultdict(set) - changed_bodies = [] - - # Group by file - by_file = defaultdict(list) - for markers_path, body_id, section, feat in replacements_needed: - by_file[markers_path].append((body_id, section, feat["id"] if "id" in feat else None)) - - for markers_path, entries in by_file.items(): - body_id = markers_path.parent.name - m = json.loads(markers_path.read_text()) - changed = False - - for _, section, feat_id in entries: - for feat in m.get(section, []): - name = feat.get("name") or "" - if not name or name.lower() not in FEWSHOT_NAMES: - continue - - assigned = None - for attempt in range(len(available)): - candidate = available[(replacement_idx + attempt) % len(available)] - if candidate.lower() not in used_per_body[body_id]: - assigned = candidate - replacement_idx = (replacement_idx + attempt + 1) % len(available) - break - - if assigned: - feat["name"] = assigned - used_per_body[body_id].add(assigned.lower()) - changed = True - - if changed: - markers_path.write_text(json.dumps(m, indent=2) + "\n") - changed_bodies.append(body_id) - # Sync to DB if body exists - try: - sync_markers_to_db(conn, body_id, m) - except Exception: - pass # orphan body - - conn.commit() - conn.close() - - print(f"\nReplaced few-shot names on {len(changed_bodies)} bodies") - print("Done.") - - -if __name__ == "__main__": - main() diff --git a/tooling/planet-gen/gemma_naming.py b/tooling/planet-gen/gemma_naming.py deleted file mode 100755 index 6a9ba4266..000000000 --- a/tooling/planet-gen/gemma_naming.py +++ /dev/null @@ -1,2537 +0,0 @@ -#!/usr/bin/env python3 -""" -gemma_naming.py — Batch-name every empty name field in the reach's -markers.json files using the Gemma 4 E2B tooling pipeline (#833, D-191 §4). - -Pipeline per body: - 1. Load markers.json; identify feature records whose `name` is empty - or null. Hand-authored names are never overwritten. - 2. Build a short corridor-aware prompt per feature. - 3. Stream the prompts into `sr-voice serve --stdio` (long-lived - subprocess, restarted every --refresh requests to prevent KV-cache - context bleed). - 4. Post-process each response: strip quotes, trim whitespace, reject - blocklisted Earth majors, retry with a bumped seed on collision or - on blocklist hit (up to 3 attempts), fall back to a deterministic - palette-driven name on persistent failure. - 5. Dedup within (cultural_corridor, feature_type) so two bodies in the - same corridor never ship the same river name; cross-corridor - collisions are allowed (two "Aldren"s on opposite arcs is fine). - 6. Write markers.json back (only if any field changed). - 7. Sync every touched body's atlas_* rows in systems.db so - `atlas_cities.name`, `atlas_rivers.name`, etc. pick up the new - strings without needing a follow-up generate_atlas.py pass. - -Usage: - tooling/planet-gen/gemma_naming.py # full batch, real model - tooling/planet-gen/gemma_naming.py --body GJ380c # single body - tooling/planet-gen/gemma_naming.py --limit 5 --verbose # smoke test - tooling/planet-gen/gemma_naming.py --mock # mock-stdio.sh (no model) - tooling/planet-gen/gemma_naming.py \\ - --sr-voice ~/Projects/settled-reach/binaries/sr-voice-tooling \\ - --model ~/Projects/settled-reach/models/gemma-4.gguf - -Exit codes: - 0 pipeline completed (possibly with skipped bodies) - 1 fatal error (subprocess crash, missing binary, missing schema) -""" - -import argparse -import datetime -import hashlib -import json -import os -import re -import signal -import subprocess -import sys -import time -from pathlib import Path - -TOOLING_DIR = Path(__file__).resolve().parent -REPO_ROOT = (TOOLING_DIR / ".." / "..").resolve() - -# Reuse the atlas DB sync logic from generate_atlas.py so there is one -# authoritative path for atlas_* row updates. -sys.path.insert(0, str(TOOLING_DIR)) -from generate_atlas import ( # noqa: E402 - GRID_H, - GRID_W, - ensure_atlas_schema, - sync_markers_to_db, -) - -import sqlite3 # noqa: E402 -from naming_core import ( # noqa: E402 - name_features_batch, - mood_for_body, -) - -DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" -WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems" -BLOCKLIST_PATH = TOOLING_DIR / "earth_blocklist.txt" - -# NOTE: The following are vestigial from the Gemma 2 single-name pipeline. -# The live path uses _batch_fill() → name_features_batch() from naming_core. -# TODO(#833): remove in a cleanup pass. Full dead-code island (~750 lines): -# - _CAPTURE_FILE, --dump-prompts argparse (here + main()) -# - _build_prompt() and its few-shot example pools (~lines 500-929) -# - post_process(), is_placeholder(), _PLACEHOLDER_TOKENS, _LABEL_PREFIX, -# _MD_BOLD, _MD_UNDER (~lines 1040-1100) -# - is_blocked(), load_blocklist() (~lines 1105-1140) -# - fallback_name(), _FALLBACK_STEMS, _FALLBACK_SUFFIXES (~lines 1145-1205) -# - name_feature() with its retry loop and _is_duplicate (~lines 1530-1655) -_CAPTURE_FILE = None # vestigial — see note above - -# Default binary + model paths. The sr-voice binary is platform-specific -# (GPU backend baked in per-build) and lives OUTSIDE any git worktree so -# persistent across worktree cleanup: -# -# ~/Projects/settled-reach/binaries/sr-voice-rocm (AMD / ROCm) -# ~/Projects/settled-reach/binaries/sr-voice-cuda (NVIDIA, future) -# ~/Projects/settled-reach/binaries/sr-voice-vulkan (cross-vendor, future) -# ~/Projects/settled-reach/binaries/sr-voice-cpu (fallback) -# -# See #850 for the multi-backend release-binary ticket. The container -# used to build these is created via the distrobox recipe documented in -# the sr-voice README. -# -# The Gemma 2 model weights live under main/server/models and are shared -# across worktrees (too large to duplicate). -HOME_PROJECTS = Path.home() / "Projects" / "settled-reach" -BINARIES_DIR = HOME_PROJECTS / "binaries" -MODELS_DIR = HOME_PROJECTS / "models" -MAIN_WORKDIR = Path("/var/mnt/data/projects/settled-reach/main") - - -def _find_sr_voice() -> Path: - """Resolve the default sr-voice binary path. - - Preference order: - 1. $HOME/Projects/settled-reach/binaries/sr-voice-tooling — Gemma 4 - tooling binary, preferred for content generation. - 2. $HOME/Projects/settled-reach/binaries/sr-voice-rocm — Gemma 2 - ROCm binary, fallback. - 3. main workdir's target/release/sr-voice — legacy. - """ - tooling_bin = BINARIES_DIR / "sr-voice-tooling" - if tooling_bin.exists(): - return tooling_bin - rocm_bin = BINARIES_DIR / "sr-voice-rocm" - if rocm_bin.exists(): - return rocm_bin - return MAIN_WORKDIR / "server" / "sr-voice" / "target" / "release" / "sr-voice" - - -def _find_default_model() -> Path: - """Resolve the default model path. Prefers Gemma 4 over Gemma 2.""" - gemma4 = MODELS_DIR / "gemma-4.gguf" - if gemma4.exists(): - return gemma4 - return MAIN_WORKDIR / "server" / "models" / "gemma2.gguf" - - -DEFAULT_SR_VOICE = _find_sr_voice() -DEFAULT_MODEL = _find_default_model() -MOCK_STDIO = REPO_ROOT / "server" / "sr-voice" / "mock-stdio.sh" - - -# --------------------------------------------------------------------------- -# Tee logger — stdout + log file in one call -# --------------------------------------------------------------------------- - -class Logger: - """Write lines to stdout AND an optional log file. - - Every message gets a prefix of the form `[HH:MM:SS +00h03m]`: - - HH:MM:SS is wall-clock local time, - - +NNhMMm is the elapsed time since the Logger was constructed. - The elapsed offset tells the user at a glance how long the run has - been going without scrolling back to the banner line. Flushes after - every line so a kill -9 loses at most one entry. - """ - - def __init__(self, log_path: Path | None): - self.log_path = log_path - self.started_at = time.monotonic() - self.fh = None - if log_path is not None: - log_path.parent.mkdir(parents=True, exist_ok=True) - # Truncate on open so each run starts fresh — the user can - # rename an old log before kicking off the next run. - self.fh = log_path.open("w", buffering=1) # line buffered - - def _elapsed(self) -> str: - secs = int(time.monotonic() - self.started_at) - return f"+{secs // 3600:02d}h{(secs % 3600) // 60:02d}m" - - def _prefix(self) -> str: - clock = datetime.datetime.now().strftime("%H:%M:%S") - return f"[{clock} {self._elapsed()}]" - - def __call__(self, msg: str = "") -> None: - line = f"{self._prefix()} {msg}" if msg else "" - print(line, flush=True) - if self.fh is not None: - self.fh.write(line + "\n") - self.fh.flush() - - def raw(self, msg: str = "") -> None: - """Print without the timestamp prefix (for banner lines).""" - print(msg, flush=True) - if self.fh is not None: - self.fh.write(msg + "\n") - self.fh.flush() - - def close(self) -> None: - if self.fh is not None: - self.fh.close() - self.fh = None - - -# --------------------------------------------------------------------------- -# Corridor palettes (D-191 §4, glossary.md §Corridors, decisions/economics.md D-175) -# --------------------------------------------------------------------------- - -# Each palette is the cultural inflection the prompt asks Gemma to -# produce names in. Palette keys match the values of -# `star_systems.geographic_sector` directly — that column is the real -# source of corridor identity in systems.db (cultural_corridor is a -# legacy field that was never populated beyond sol-gateway-axis). -CORRIDOR_SUBSTYLES: dict[str, list[dict[str, str]]] = { - # Each corridor has a list of sub-style inflections. The pipeline - # picks one per body via hash(body_id) so neighbouring bodies on the - # same planet get different registers, and Gemma's narrow per-register - # vocabulary (~15 stems) stays fresh across hundreds of bodies. - # - # IMPORTANT: the inflection is a DOMINANT bias, not a hard lock. - # A British surveyor on an east_reach moon still names a river after - # their aunt in Dorset. Each sub-style explicitly names its register - # AND invites diaspora variety. - "core": [ - {"inflection": "English countryside, rural, agricultural settlers", - "examples": "Thornbury, Bramblewood, Millbrook, Ashford, Weston"}, - {"inflection": "British colonial settlement era", - "examples": "New Bristol, Port Augusta, Kingstown, Admiralty, Georgetown"}, - {"inflection": "American frontier, practical, geographic", - "examples": "Dusty Creek, Twin Oaks, Cedar Flat, Hawk's Hollow, Red Bluff"}, - {"inflection": "American municipal, administrative, cosmopolitan", - "examples": "Prospect Heights, Liberty, Union, Meridian, Commonwealth"}, - {"inflection": "Classical references, institutional, civic", - "examples": "Concordia, Aurelius, Prefecture, Senate Landing, Forum"}, - {"inflection": "Australian and New Zealand settler", - "examples": "Redfern, Glenelg, Wollongong, Kaikoura, Hawke's Bay"}, - ], - "north_reach": [ - {"inflection": "English rural, village and parish names", - "examples": "Wolcott, Mildern, Ashbourne, Briarfell, Tarndale"}, - {"inflection": "Scottish Highland and Lowland place-names", - "examples": "Glenmoray, Dunfermline, Kinross, Brae, Dalwhinnie"}, - {"inflection": "Australian outback, station and property names", - "examples": "Redfern, Birdsville, Tennant, Woomera, Coober"}, - {"inflection": "Irish rural and coastal settlement", - "examples": "Ballymore, Kilrush, Dunmore, Tralee, Skellig"}, - {"inflection": "South African English settler", - "examples": "Grahamstown, Oudtshoorn, Stellenbosch, Graaff, Beaufort"}, - ], - "south_reach": [ - {"inflection": "Portuguese colonial era, Iberian", - "examples": "Monteforte, Serra, Tavira, Oliveira, Porto Novo"}, - {"inflection": "Brazilian interior, frontier settlement", - "examples": "Ribeirão, Campo Largo, Várzea, Ilhabela, Pinheiro"}, - {"inflection": "East African Swahili coastal", - "examples": "Inhambane, Kilimi, Ngola, Manhica, Quelimane"}, - {"inflection": "Cape Verdean and West African", - "examples": "Cabo, Moçambo, Ribeira, Mindelo, Tarrafal"}, - {"inflection": "Angolan and Mozambican settlement", - "examples": "Huambo, Lobito, Nampula, Lichinga, Benguela"}, - ], - "east_reach": [ - {"inflection": "Korean place-name tradition", - "examples": "Hanyang, Seorak, Baektu, Saeyeon, Taegong"}, - {"inflection": "Japanese rural and coastal settlement", - "examples": "Takamine, Ginoza, Tsukuri, Aomori, Fukagawa"}, - {"inflection": "Taiwanese and Hakka settler", - "examples": "Jiufen, Beigang, Hsinchu, Meinong, Tainan"}, - {"inflection": "Filipino settler community", - "examples": "Batangas, Legazpi, Tuguegarao, Zambales, Tarlac"}, - {"inflection": "Mixed East Asian diaspora, cosmopolitan", - "examples": "Naruhan, Morimine, Kōzan, Midori, Kawasaki"}, - ], - "west_reach": [ - {"inflection": "German settlement, orderly and compound names", - "examples": "Altdorf, Drachenberg, Feldberg, Krakenberg, Lüneborg"}, - {"inflection": "Dutch colonial, low-country", - "examples": "Kloosterdam, Hoogland, Oudewater, Nieuwpoort, Voorhout"}, - {"inflection": "Nordic and Scandinavian", - "examples": "Sørholm, Torsfell, Bergfjord, Lindeborg, Nordhölm"}, - {"inflection": "Polish and Czech settler", - "examples": "Krakowice, Bystrica, Wieliczka, Tarnów, Ostrava"}, - {"inflection": "Baltic and Finnish settler", - "examples": "Järvenpää, Tallinna, Pärnu, Turku, Rakvere"}, - ], - "deep_frontier": [ - {"inflection": "frontier founder-name era, surname-first, any Earth culture", - "examples": "Okafor Reach, Stenner Cross, Weller Hold, Pruitt Basin"}, - {"inflection": "frontier descriptive, geographic features named by surveyors", - "examples": "Red Mesa, Dry Fork, Iron Flat, Long Ridge, Dust Basin"}, - {"inflection": "frontier outpost, functional and military", - "examples": "Forward Post, Relay Station, Survey Camp, Waypoint, Anchor"}, - ], -} - -# Legacy aliases -CORRIDOR_SUBSTYLES["sol-gateway-axis"] = CORRIDOR_SUBSTYLES["core"] -CORRIDOR_SUBSTYLES["inner_corridor"] = CORRIDOR_SUBSTYLES["core"] -CORRIDOR_SUBSTYLES["inner_orbit"] = CORRIDOR_SUBSTYLES["core"] -CORRIDOR_SUBSTYLES["frontier"] = CORRIDOR_SUBSTYLES["deep_frontier"] - -DEFAULT_SUBSTYLES = CORRIDOR_SUBSTYLES["core"] - -# Processing order for the main run — core first so those bodies win -# the dedup race and the outer sectors fall into the palette fallback -# path when names collide. -SECTOR_PRIORITY: dict[str, int] = { - "core": 0, - "north_reach": 1, - "south_reach": 2, - "east_reach": 3, - "west_reach": 4, - "deep_frontier": 5, -} - - -def palette_for(corridor: str | None, system_id: str = "") -> dict[str, str]: - """Pick a sub-style for this system within its corridor. - - All bodies in the same system get the same sub-style (consistent - cultural register per star system). Different systems rotate through - the sub-style list via hash(system_id). - - This is the FALLBACK path — the preferred path is select_register() - which asks Gemma to pick the register based on wiki/GTTR content. - """ - substyles = CORRIDOR_SUBSTYLES.get(corridor or "core", DEFAULT_SUBSTYLES) - idx = int(hashlib.sha256(system_id.encode()).hexdigest()[:8], 16) % len(substyles) - return substyles[idx] - - -def _system_slug(system_id: str) -> str: - """Convert system_id ('GJ 411') to wiki directory slug ('GJ-411').""" - if system_id.startswith("GJ "): - return "GJ-" + system_id[3:] - return system_id - - -def load_wiki_context(system_id: str) -> tuple[str | None, str | None]: - """Read index.md and gttr.md for a system from wiki/star-systems/. - - Returns (index_text, gttr_text). Either or both may be None if the - file doesn't exist. - """ - slug = _system_slug(system_id) - sys_dir = WIKI_SYSTEMS / slug - index_path = sys_dir / "index.md" - gttr_path = sys_dir / "gttr.md" - index_text = index_path.read_text() if index_path.exists() else None - gttr_text = gttr_path.read_text() if gttr_path.exists() else None - return index_text, gttr_text - - -def _extract_cultural_lines(wiki_text: str, max_lines: int = 8) -> str: - """Pull the most culturally relevant lines from a wiki index.md. - - Scans for lines mentioning heritage, founding identity, language, - cultural texture, or corridor affiliation. Falls back to the first - prose paragraphs if no keyword hits. Keeps the excerpt short enough - for Gemma 2 2B's 1024-token context. - """ - keywords = ( - "cultural", "heritage", "founding", "settler", "surname", - "language", "tradition", "diaspora", "population carried", - "portuguese", "iberian", "japanese", "korean", "chinese", - "filipino", "german", "dutch", "nordic", "scandinavian", - "polish", "czech", "finnish", "baltic", "swahili", "african", - "angolan", "cape verde", "irish", "scottish", "australian", - "british", "brazilian", "mozambic", "norwegian", "frisian", - "afrikaans", "lusophone", "corridor", - ) - hits: list[str] = [] - prose: list[str] = [] - for line in wiki_text.splitlines(): - stripped = line.strip() - if not stripped or stripped.startswith("#") or stripped.startswith("|") or stripped.startswith("---") or stripped.startswith("