fix(tooling): address PR #129 review — atlas generator, schema, Makefile

- ON DELETE CASCADE added to every atlas_* foreign key (atlas_body_grids,
  atlas_cities, atlas_roads, atlas_railroads, atlas_pois, atlas_rivers,
  atlas_oceans, atlas_mountain_ranges). Previously, deleting a body from
  the bodies table or NULL-ing its terrain_reference would leave orphan
  atlas rows forever — sync_markers_to_db only cleans up for bodies it
  re-processes. The existing atlas tables in systems.db were dropped and
  recreated with the new constraint; FK list now reports CASCADE.

- Atlas DDL deduplicated. systems-schema.sql is now the single source of
  truth, bracketed by `-- BEGIN ATLAS INDEX` / `-- END ATLAS INDEX`
  markers. generate_atlas.py reads that block via `_load_atlas_schema()`
  and applies it at runtime, so there is no second copy of the DDL to
  keep in sync. Adding a column requires one edit, not two.

- Uniqueness guard on city coordinates. `_enforce_unique_city_coords`
  runs at the end of `place_cities` and deterministically perturbs any
  duplicate (row, col) via a fixed spiral walk to the first free
  walkable land cell. Rare in practice but the MST collapses to a
  zero-distance edge otherwise, producing an empty A* path and silently
  dropping the road.

- Grid header validation. `load_markers` now raises `AtlasGridMismatch`
  if the loaded `grid: {w, h}` header does not match `GRID_W`/`GRID_H`.
  Both the incremental-skip path and the regenerate path route through
  this loader, so a hand-authored template shipping a different grid
  size fails loud with a per-body error rather than silently producing
  half-scale coordinates.

- Unused `seed_rng` parameter removed from `_analyse_terrain`. The
  function is RNG-free (continent flood-fill, habitability scoring,
  river-mouth dedup, cost grid — all pure functions of terrain). The
  false API contract made it look like terrain analysis consumed RNG
  state and had to be sequenced with downstream RNG use.

- `_score_capital_sites` river-mouth bonus now builds one sparse
  accumulator with all mouth points set at once and runs a single
  `gaussian_filter` call, instead of O(n_mouths) filter calls over
  single-point images.

- `binary_dilation(analysis["land_mask"] == False)` replaced with the
  idiomatic `~analysis["land_mask"]`, matching the convention used
  elsewhere in the file.

- `atlas-generate` Makefile target now guards on
  `SELECT COUNT(*) FROM bodies WHERE terrain_reference IS NOT NULL`.
  On a fresh DB that count is 0 and the generator previously exited
  "success" after processing zero bodies. The target now fails loud
  with a pointer to `populate_terrain_reference.py`.

- `main.rs` SimRng defensive re-insertion gains a long comment
  explaining the exact plugin-ordering hazard it guards against, so
  future readers don't treat the line as dead code. Tied to #826.
This commit is contained in:
2026-04-15 09:25:46 +02:00
parent 64bb83f749
commit d21c690293
5 changed files with 209 additions and 152 deletions
+27 -22
View File
@@ -339,17 +339,20 @@ CREATE TABLE IF NOT EXISTS corp_lifecycle_events (
created_at TEXT DEFAULT (datetime('now'))
);
-- Atlas index (D-191 §8, #832) — scalar metadata mirror of markers.json files.
-- BEGIN ATLAS INDEX (D-191 §8, #832) -- DO NOT EDIT THE MARKER COMMENTS
-- Atlas index — scalar metadata mirror of markers.json files.
-- Source of truth is wiki/star-systems/.../markers.json (next to the heightmap);
-- these tables exist so the atlas implant app and development queries don't
-- have to scan hundreds of JSON files. Polyline geometry stays in the files —
-- the DB only stores scalar/filterable fields + `point_count` as a rough length
-- proxy. Populated and refreshed by tooling/planet-gen/generate_atlas.py.
-- proxy. Populated and refreshed by tooling/planet-gen/generate_atlas.py, which
-- extracts this entire block (between BEGIN/END ATLAS INDEX markers) from this
-- file at runtime so the DDL lives in exactly one place.
-- Per-body grid dimensions (one row per body that has a markers.json).
-- Lets any query interpret the pixel coordinates below without touching disk.
CREATE TABLE IF NOT EXISTS atlas_body_grids (
body_id TEXT PRIMARY KEY REFERENCES bodies(body_id),
body_id TEXT PRIMARY KEY REFERENCES bodies(body_id) ON DELETE CASCADE,
grid_w INTEGER NOT NULL,
grid_h INTEGER NOT NULL,
updated_at TEXT DEFAULT (datetime('now'))
@@ -357,7 +360,7 @@ CREATE TABLE IF NOT EXISTS atlas_body_grids (
CREATE TABLE IF NOT EXISTS atlas_cities (
city_id TEXT PRIMARY KEY, -- "<body_id>/<local_id>" e.g. "GJ380c/city_0"
body_id TEXT NOT NULL REFERENCES bodies(body_id),
body_id TEXT NOT NULL REFERENCES bodies(body_id) ON DELETE CASCADE,
local_id TEXT NOT NULL, -- "city_0", "city_1" — matches markers.json id
name TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL, -- capital|city
@@ -369,7 +372,7 @@ CREATE TABLE IF NOT EXISTS atlas_cities (
CREATE TABLE IF NOT EXISTS atlas_roads (
road_id TEXT PRIMARY KEY, -- "<body_id>/<local_id>"
body_id TEXT NOT NULL REFERENCES bodies(body_id),
body_id TEXT NOT NULL REFERENCES bodies(body_id) ON DELETE CASCADE,
local_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL, -- commercial|highway|rural|...
@@ -379,7 +382,7 @@ CREATE TABLE IF NOT EXISTS atlas_roads (
CREATE TABLE IF NOT EXISTS atlas_railroads (
railroad_id TEXT PRIMARY KEY, -- "<body_id>/<local_id>"
body_id TEXT NOT NULL REFERENCES bodies(body_id),
body_id TEXT NOT NULL REFERENCES bodies(body_id) ON DELETE CASCADE,
local_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL, -- passenger_freight|freight|maglev|...
@@ -389,7 +392,7 @@ CREATE TABLE IF NOT EXISTS atlas_railroads (
CREATE TABLE IF NOT EXISTS atlas_pois (
poi_id TEXT PRIMARY KEY, -- "<body_id>/<local_id>"
body_id TEXT NOT NULL REFERENCES bodies(body_id),
body_id TEXT NOT NULL REFERENCES bodies(body_id) ON DELETE CASCADE,
local_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL, -- transit|institutional|cultural|...
@@ -400,7 +403,7 @@ CREATE TABLE IF NOT EXISTS atlas_pois (
CREATE TABLE IF NOT EXISTS atlas_rivers (
river_id TEXT PRIMARY KEY, -- "<body_id>/<local_id>"
body_id TEXT NOT NULL REFERENCES bodies(body_id),
body_id TEXT NOT NULL REFERENCES bodies(body_id) ON DELETE CASCADE,
local_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '', -- empty for untitled procedural rivers
point_count INTEGER NOT NULL,
@@ -409,7 +412,7 @@ CREATE TABLE IF NOT EXISTS atlas_rivers (
CREATE TABLE IF NOT EXISTS atlas_oceans (
water_id TEXT PRIMARY KEY, -- "<body_id>/<local_id>"
body_id TEXT NOT NULL REFERENCES bodies(body_id),
body_id TEXT NOT NULL REFERENCES bodies(body_id) ON DELETE CASCADE,
local_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
kind TEXT NOT NULL, -- lake|sea|ocean
@@ -421,7 +424,7 @@ CREATE TABLE IF NOT EXISTS atlas_oceans (
CREATE TABLE IF NOT EXISTS atlas_mountain_ranges (
range_id TEXT PRIMARY KEY, -- "<body_id>/<local_id>"
body_id TEXT NOT NULL REFERENCES bodies(body_id),
body_id TEXT NOT NULL REFERENCES bodies(body_id) ON DELETE CASCADE,
local_id TEXT NOT NULL,
name TEXT NOT NULL DEFAULT '',
center_row INTEGER NOT NULL,
@@ -432,6 +435,19 @@ CREATE TABLE IF NOT EXISTS atlas_mountain_ranges (
updated_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_atlas_cities_body ON atlas_cities(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_cities_kind ON atlas_cities(kind);
CREATE INDEX IF NOT EXISTS idx_atlas_cities_population ON atlas_cities(population);
CREATE INDEX IF NOT EXISTS idx_atlas_cities_name ON atlas_cities(name);
CREATE INDEX IF NOT EXISTS idx_atlas_roads_body ON atlas_roads(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_railroads_body ON atlas_railroads(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_pois_body ON atlas_pois(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_pois_kind ON atlas_pois(kind);
CREATE INDEX IF NOT EXISTS idx_atlas_rivers_body ON atlas_rivers(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_oceans_body ON atlas_oceans(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_mountain_ranges_body ON atlas_mountain_ranges(body_id);
-- END ATLAS INDEX (D-191 §8, #832)
-- Indexes
-- astronomical_id removed: system_id IS the GJ catalog number
CREATE INDEX IF NOT EXISTS idx_star_systems_sector ON star_systems(geographic_sector);
@@ -466,15 +482,4 @@ CREATE INDEX IF NOT EXISTS idx_brand_products_origin ON brand_products(origin_sy
CREATE INDEX IF NOT EXISTS idx_brand_products_tier ON brand_products(brand_tier);
CREATE INDEX IF NOT EXISTS idx_brand_inputs_commodity ON brand_inputs(commodity_id);
CREATE INDEX IF NOT EXISTS idx_corp_lifecycle_events_corp ON corp_lifecycle_events(corp_id);
-- Atlas index (D-191 §8, #832)
CREATE INDEX IF NOT EXISTS idx_atlas_cities_body ON atlas_cities(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_cities_kind ON atlas_cities(kind);
CREATE INDEX IF NOT EXISTS idx_atlas_cities_population ON atlas_cities(population);
CREATE INDEX IF NOT EXISTS idx_atlas_cities_name ON atlas_cities(name);
CREATE INDEX IF NOT EXISTS idx_atlas_roads_body ON atlas_roads(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_railroads_body ON atlas_railroads(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_pois_body ON atlas_pois(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_pois_kind ON atlas_pois(kind);
CREATE INDEX IF NOT EXISTS idx_atlas_rivers_body ON atlas_rivers(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_oceans_body ON atlas_oceans(body_id);
CREATE INDEX IF NOT EXISTS idx_atlas_mountain_ranges_body ON atlas_mountain_ranges(body_id);
-- Atlas index (D-191 §8, #832) — see BEGIN/END ATLAS INDEX block above.