diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 5d9de3f8c..66bfc5b95 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1118,7 +1118,9 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **Two-tier mismatch flagging:** If a matched city-attractor pair has score < 0.35, log a `WARNING` (below expected quality). If score < 0.15, log an `ERROR` and flag for manual review. Generation proceeds in both cases; the flags are for content auditing, not hard blockers. - **Output:** `Vec` written to `atlas_city_positions` at build time. - **Rationale:** Greedy-first for large/locked cities ensures anchor cities (capitals, corp HQs, wiki-named cities) are placed at terrain features that match their lore role. Hungarian for medium cities finds the globally optimal assignment, not just locally optimal. Synthetic overflow prevents the algorithm from failing on bodies where city count exceeds natural attractor count (dense, flat worlds). The two-tier warning system enables content QA without blocking generation. -- **Amendment (T-1206, 2026-08-06 — step 4 may now legitimately place nothing):** phase 4's synthetic position was derived by pure grid arithmetic with **no terrain input at all**, so it could and did land in open water — a scan of every heightmap body at world seed 42 found **46 of 109 synthetic placements sitting in ocean** (e.g. GJ903c at a genuine polar ocean cell). Synthetic overflow now takes the terrain analysis and land-corrects via a bounded nearest-land ring walk; land positions pass through untouched, so **no existing land placement moves** and this record's seed-derived-position promise is intact (position remains a pure function of seed + terrain — this fulfils the placement intent rather than deviating from it, so no re-decision is required). What **does** change is the record's outcome set: step 4's "cities that cannot be matched receive a synthetic `PlainCenter`" is no longer total — where no land exists within the search bound, the synthetic attractor is **skipped**, defined and deliberate, never a fabricated water position and never a panic. Step 5's name-fulfillment warning consequently fires for a new legitimate reason (a genuinely water-locked body), not only for a pipeline failure. +- **Amendment (T-1206, 2026-08-06 — step 4 may now legitimately place nothing):** phase 4's synthetic position was derived by pure grid arithmetic with **no terrain input at all**, so it could and did land in open water — a scan of every heightmap body at world seed 42 found **46 of 109 synthetic placements sitting in ocean** (e.g. GJ903c at a genuine polar ocean cell). Synthetic overflow now takes the terrain analysis and corrects via a bounded ring walk; land positions pass through untouched, so **no existing land placement moves** and this record's seed-derived-position promise is intact (position remains a pure function of seed + terrain — this fulfils the placement intent rather than deviating from it, so no re-decision is required). What **does** change is the record's outcome set: step 4's "cities that cannot be matched receive a synthetic `PlainCenter`" is no longer total — where the search finds nothing acceptable within its bound, the synthetic attractor is **skipped**, defined and deliberate, never a fabricated water position and never a panic. Step 5's name-fulfillment warning consequently fires for a new legitimate reason, not only for a pipeline failure. + + **Amendment refinement (PR #218 round 2, 2026-08-07 — the walk satisfies BOTH of step 4's promises).** The paragraph above was written when the correction searched for land alone, which quietly spent this step's *other* guarantee: step 4 promises a position that "respects minimum city spacing", and the spacing walk ran *before* the correction, so a candidate cleared against its neighbours could be nudged up to half a grid away onto one of them. The spacing test is now folded **into the same ring walk**, so a synthetic candidate must be both land **and** at least `MIN_SPACING` from every already-placed city, resolved in one search with the same deterministic tie-break. Consequently **SKIP also fires where land exists but none of it clears spacing within the bound** — not only on a genuinely water-locked body. The no-re-decision conclusion is unaffected: position remains a deterministic, non-fabricated function of seed and terrain. Note the spacing promise is step 4's alone — Tier A greedy and Tier B/C Hungarian placements sit on their matched terrain attractor and were never subject to it. - **Ticket:** T-919, T-925, T-1206 (ocean-mask guard amendment) - **Raised by:** Generation cascade workshop (T-897) - **Cross-reference:** D-195 (CompatibilityMatrix), D-207 (atlas_city_names), D-209 (GeographicAttractor — input), [D-210](#d-210) (terrain_modification_cost — input; its 2026-07-26 amendment records the *matched-attractor* half of this same placement-in-water gap, closed and validated under T-1116/T-1206) diff --git a/server/src/atlas/attractor_matching.rs b/server/src/atlas/attractor_matching.rs index 06f22e95c..971160670 100644 --- a/server/src/atlas/attractor_matching.rs +++ b/server/src/atlas/attractor_matching.rs @@ -268,8 +268,10 @@ fn hungarian(cost: &[Vec]) -> Vec { const MIN_SPACING: u16 = 15; /// T-1206 — ocean-mask guard search bound. Bounded ring-expansion search -/// radius (cells) `nearest_land_cell` will walk from a water-arithmetic -/// synthetic position before giving up. +/// radius (cells) [`nearest_cell_matching`] will walk from a water-arithmetic +/// synthetic position before giving up. That is the production consumer — +/// reached from `synthetic_attractor` with the land+spacing predicate, and +/// from the test-only `nearest_land_cell` wrapper with a trivial one. /// /// **Sized empirically, not by analogy.** An early draft mirrored /// `road_graph::COASTAL_ANCHOR_MAX_RING` (3) scaled up an order of magnitude diff --git a/server/src/atlas/road_graph.rs b/server/src/atlas/road_graph.rs index ffe1e2bd0..e3812b2b9 100644 --- a/server/src/atlas/road_graph.rs +++ b/server/src/atlas/road_graph.rs @@ -104,18 +104,23 @@ const MIN_CELL_COST: u32 = RIVER_COST; /// 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 +/// placements), water gets nudged to the nearest acceptable cell via a +/// deterministic ring-walk (`nearest_cell_matching`, 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 such cell within the bounded search radius is SKIPPED +/// entirely (never fabricated, never a panic — reported via the existing +/// Phase-5 name-fulfillment warning). "Acceptable" is BOTH of D-211 step 4's +/// promises since the PR #218 round-2 fix: land AND at least `MIN_SPACING` +/// from every already-placed city, so correcting one can never quietly spend +/// the other. SKIP therefore also fires where land exists but none of it +/// clears spacing within the bound. 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). +/// (T-1206 guarantees the placement pixel is land and correctly spaced, or +/// that the city is skipped; 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