Files
settled-reach/tooling/domains/atlas/planet/atlas_common.py
T
jpmschweitzerandClaude Opus 5.5 668772075c refactor(tooling): T-1288 — planet-gen becomes reach atlas planet
The 30-file tree moves under atlas as its third rung (D-243), ten verbs
fronting it. Each verb restates its module's options so `--help` describes
something; tooling/test_planet_router.py hands every declared option to the
module's own argparse and fails on drift, and now runs in make test-tooling.

The 2026-09-02 half of this move had converted the top-level imports and the
repo roots. Finishing it found what the half-move left:

- Lazy in-function imports, and all of sol_data/, still named siblings bare.
  They resolved only through sys.path.insert hacks, so under reach the first
  globe render in generate, batch or sol-import would have raised
  ModuleNotFoundError. Qualified; the hacks are gone.
- 247 print() calls and a stdout progress writer that fired once per 8 KB
  block. Report verbs (audit, quality) write through console.out, progress
  through console.event, and download progress is throttled to 10% steps
  so a job log is not tens of thousands of lines.
- Every error exit raises ReachError with a fix.

Two checks that could not fail:

- batch --verify-determinism printed a warning and exited 0 on a mismatch.
- import-provinces exited 0 with errors > 0.

Both now raise. The 271-body bake is only safe to re-run because the first
one holds.

sol-import --body is action="append" in the module but the router took one
value, so --body GJ0d --body GJ0e kept one. Now repeatable, and _flags repeats
list options.

test_conformance walked one level, so a nested group was reported as a verb
missing @command and its ten verbs were never checked. It recurses now;
proven by stripping @command from `planet quality` and watching it fail.

Stray PNGs from the 2026-09-03 runaway router-test run are parked in
.cache/t1288-stray-pngs/, not committed. Their reliefmaps differ from HEAD
while the heightmap regenerated byte-identical — filed as T-1291.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:08:02 +02:00

168 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""
atlas_common.py — shared atlas-DB utilities for the atlas planet importers.
Extracted from the retired generate_atlas.py (D-223, #951). The atlas city/road/
river geometry *generator* was retired when authored geometry was dropped in
favour of the deterministic server-side cascade (Phase 4); markers.json is now a
flavoured name pool only. What remained were the schema/DB helpers shared by the
surviving build-time importers (heightmaps, province boundaries, city names) and
the naming pipeline. Those live here so there is a single home for them with no
dependency on the deleted geometry-production code.
This is a library module — it does NOT re-exec the venv. The importing scripts
(import_heightmaps.py, import_province_boundaries.py, import_city_names.py,
gemma_naming.py) own the venv bootstrap before importing this module.
Decisions: D-223 (authored content as flavoured name pool), D-191 (atlas index).
"""
import sqlite3
from pathlib import Path
from tooling.core import config
import yaml
# ---------------------------------------------------------------------------
# Paths and grid constants
# ---------------------------------------------------------------------------
PLANET_DIR = Path(__file__).resolve().parent
REPO_ROOT = config.repo_root()
DB_PATH = REPO_ROOT / "server" / "data" / "systems.db"
WIKI_SYSTEMS = REPO_ROOT / "wiki" / "star-systems"
SYSTEMS_SCHEMA_PATH = REPO_ROOT / "server" / "data" / "systems-schema.sql"
# Heightmap pixel-space grid (D-202). Geometry derives from this resolution.
GRID_W = 512
GRID_H = 256
# Delimiters for the single canonical atlas_* DDL block in
# server/data/systems-schema.sql. `_load_atlas_schema()` extracts everything
# between these markers at runtime so this file does not duplicate the schema
# (and drift from it).
_ATLAS_SCHEMA_BEGIN_MARKER = "-- BEGIN ATLAS INDEX"
_ATLAS_SCHEMA_END_MARKER = "-- END ATLAS INDEX"
# ---------------------------------------------------------------------------
# Schema
# ---------------------------------------------------------------------------
def _load_atlas_schema() -> str:
"""Return the atlas_* DDL block from systems-schema.sql.
systems-schema.sql is the single source of truth for the atlas index
tables (see the BEGIN ATLAS INDEX / END ATLAS INDEX markers). We extract
just that block and run it through `executescript` so callers work against
any DB state (fresh or partially-migrated) without maintaining a second
copy of the DDL here.
"""
if not SYSTEMS_SCHEMA_PATH.exists():
raise RuntimeError(
f"systems-schema.sql not found at {SYSTEMS_SCHEMA_PATH} — "
"atlas importers cannot proceed without the canonical schema."
)
text = SYSTEMS_SCHEMA_PATH.read_text()
try:
start = text.index(_ATLAS_SCHEMA_BEGIN_MARKER)
end = text.index(_ATLAS_SCHEMA_END_MARKER, start)
except ValueError as e:
raise RuntimeError(
f"systems-schema.sql is missing the {_ATLAS_SCHEMA_BEGIN_MARKER}/"
f"{_ATLAS_SCHEMA_END_MARKER} block — has the schema been "
"restructured?"
) from e
return text[start:end]
def ensure_atlas_schema(conn: sqlite3.Connection) -> None:
"""Apply the canonical atlas_* DDL from systems-schema.sql.
Idempotent: all statements inside the block use CREATE TABLE / INDEX
IF NOT EXISTS, so running this on an already-migrated DB is a no-op.
Also ensures the meta stamp table exists (#855, #856).
"""
conn.executescript(_load_atlas_schema())
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS meta (
generator_name TEXT PRIMARY KEY,
schema_version TEXT NOT NULL,
schema_sha TEXT,
generator_sha TEXT NOT NULL,
generated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
"""
)
# Add schema_sha column to existing DBs that pre-date #888 (#888 migration).
try:
conn.execute("ALTER TABLE meta ADD COLUMN schema_sha TEXT")
except sqlite3.OperationalError as e:
if "duplicate column" not in str(e).lower():
raise
# ---------------------------------------------------------------------------
# Body definition loading
# ---------------------------------------------------------------------------
def load_body_def(body_dir: Path) -> dict | None:
"""Load body definition from body index.md frontmatter."""
index_md = body_dir / "index.md"
if not index_md.exists():
return None
content = index_md.read_text()
if not content.startswith("---"):
return None
try:
end = content.index("---", 3)
bd = yaml.safe_load(content[3:end])
except (ValueError, yaml.YAMLError):
return None
if not bd or "id" not in bd or "planet_class" not in bd:
return None
return bd
# ---------------------------------------------------------------------------
# DB queries
# ---------------------------------------------------------------------------
def query_inhabited_bodies(conn: sqlite3.Connection) -> list[dict]:
"""Query systems.db for all inhabited bodies with terrain_reference set."""
rows = 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) AS cultural_corridor
FROM bodies b
JOIN star_systems s ON b.system_id = s.system_id
WHERE b.inhabited = 1
AND b.terrain_reference IS NOT NULL
AND b.population > 0
ORDER BY b.system_id, b.body_id
""").fetchall()
return [
{
"body_id": row[0],
"system_id": row[1],
"terrain_reference": row[2],
"population": row[3] or 0,
"settlement_pattern": row[4],
"planet_class": row[5],
"economic_role": row[6],
"cultural_corridor": row[7],
}
for row in rows
]