fix(simulation): synthetic-overflow placements get the ocean-mask guard (T-1206)

synthetic_attractor now takes the terrain analysis and land-corrects
its pure-arithmetic position via a bounded nearest-land ring walk
(T-1116's pattern: row-major tie-break, column wrap, row clamp,
MAX_LAND_SEARCH_RING=128 sized empirically — real polar ocean bands
push nearest land up to 125 cells). Land positions pass through
UNTOUCHED — verified by direct before/after scan of all 267 real
bodies: 63 land-arithmetic placements byte-identical, and every
golden/determinism harness passes unchanged. The gap was real and
widespread: 46 of 109 synthetic-overflow placements sat in open water
at seed 42 (e.g. GJ903c at a genuine polar ocean cell); post-fix zero,
with all 109 preserved (confirmed at a second seed). Degradation is
defined and pinned: no land within the bound -> the synthetic
attractor is skipped and Phase 5's existing not-placed warning
reports it — never a panic, never a fabricated water position (the
bound never triggers on any scanned real body). 9 new unit tests;
road_graph's anchor comment and the D-210 amendment record the gap
CLOSED (validated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 15:27:43 +02:00
co-authored by Claude Fable 5
parent 47279184d2
commit dbd5c6c4f2
4 changed files with 476 additions and 19 deletions
+21 -12
View File
@@ -95,18 +95,27 @@ const MIN_CELL_COST: u32 = RIVER_COST;
/// edge touching it (the documented T-1116 bug: GJ251c land 0.55 / GJ380c land
/// 0.588 — land-majority BODIES with 0 routable edges).
///
/// **Known gap this relaxation also happens to cover, but does not fix at the
/// source:** D-211's Phase-4 **synthetic overflow** path
/// (`attractor_matching.rs::synthetic_attractor`, used when a body has more
/// cities than real attractors) picks a position by pure grid arithmetic
/// (`grid_h/2, grid_w/4` then a fixed spacing-walk) with **no terrain check at
/// all** — it takes no heightmap/`TerrainAnalysis` argument and never reads
/// `ocean_mask`. A synthetic-overflow settlement CAN land in open ocean. This
/// routing relaxation still degrades that case gracefully (anchors to the
/// nearest passable cell within `COASTAL_ANCHOR_MAX_RING`, or leaves it
/// unrouted beyond that), but the placement itself is not guaranteed land —
/// that gap belongs to D-211/Layer 3, not this file, and is reported rather
/// than silently patched over here (flagged in PR #215 review, not yet ticketed).
/// **Gap this relaxation also happened to cover, now CLOSED at the source
/// (T-1206, 2026-07-26):** D-211's Phase-4 **synthetic overflow** path
/// (`attractor_matching.rs::synthetic_attractor`) used to pick a position by
/// pure grid arithmetic (`grid_h/2, grid_w/4` then a fixed spacing-walk) with
/// **no terrain check at all** — a synthetic-overflow settlement could land
/// in open ocean (confirmed on 46 real bodies at world seed 42/"yolo" before
/// the fix). `synthetic_attractor` now takes an `Option<&TerrainAnalysis>`
/// and, when supplied, guards the arithmetic position: already-land stays
/// byte-identical (verified unmoved on all 63 real land-arithmetic
/// placements), water gets nudged to the nearest land cell via a
/// deterministic ring-walk (`nearest_land_cell`, the same tie-break pattern
/// as this file's own `nearest_passable_cell` below, at native/working-grid
/// resolution rather than the routing grid's downsample), and a city with no
/// land within the bounded search radius is SKIPPED entirely (never
/// fabricated, never a panic — reported via the existing Phase-5
/// name-fulfillment warning). This routing relaxation still degrades
/// gracefully for the coastal-cell/downsample case it was built for (anchors
/// to the nearest passable cell within `COASTAL_ANCHOR_MAX_RING`, or leaves
/// it unrouted beyond that) — the two fixes are independent and both apply
/// (T-1206 guarantees the placement pixel is land; this relaxation still
/// covers the routing CELL being water-majority at downsample granularity).
///
/// The fix anchors routing to the NEAREST passable routing cell (ring-expansion
/// search, deterministic tie-break) rather than the settlement's own impassable