//! Atlas layer-stream proxy handler (#969, D-225). //! //! Serves a body's generation-cascade layer data to the client, compute-on- //! demand and mod-first: //! - **Cache hit** → serialize the cached `Layer1Output` and reply `Ready`. //! - **Cache miss** → resolve the body's source heightmap ([`BodySourceResolver`]), //! enqueue an `Immediate` `AnalyzeBody` on the background queue (#968), and //! reply `Pending` (the client re-requests; the drain system populates the //! cache, so a later request hits). //! //! Pure handler logic; the bridge wiring (message routing) is the proxy's other //! half. No baking — the heightmap is the only source of truth (D-225). use bevy_ecs::prelude::Resource; use serde::{Deserialize, Serialize}; use crate::atlas::body_params_reader::BodyParamsReader; use crate::atlas::body_world_state::{BodyWorldState, BodyWorldStateCache, RiverNetwork, SimTick}; use crate::atlas::cascade::CascadeLayer; use crate::atlas::city_context_reader::CityContextReader; use crate::atlas::district_profile::{BodyParams, DistrictPos}; use crate::atlas::gen_queue::{GenPriority, GenWorkItem, GenerationQueue}; use crate::atlas::layer1::Layer1Output; use crate::atlas::river_course::{self, EdgeTerminusKind, InventedCourse}; use crate::atlas::road_graph::RoadNodeKind; use crate::atlas::scale::DISTRICT_M; use crate::atlas::source_resolver::{BodySourceResolver, SourceResolveError}; use crate::bridge::ConnectionId; use crate::seed::{SeedChain, SeedDomain}; use crate::simulation::generator::{AttractorType, DistrictType, MaintenanceAuthority, ZoningType}; /// Fallback sea level when the heightmap PNG carries no `sea_level` tEXt chunk /// (the loader prefers the chunk; this is only the floor). pub(crate) const DEFAULT_SEA_LEVEL: f32 = 0.3; /// Hard server-side clamp on [`AtlasLayerRequest::window_n`] (D-226 T-1124 /// amendment §4, binding numbers). 64×64 districts ≈ 131 km per side — the /// same window size `aliveness_probe --render`'s default already proved out /// server-side (T-1123). **Never trust `window_n` from the wire** — every /// caller clamps to `[1, DISTRICT_WINDOW_MAX_N]` before deriving. /// /// **Finer-than-district / district rungs ONLY (T-1152).** This ceiling was /// calibrated for district spacing; applying it unchanged to `Region` /// requests would be nonsensical — see [`DISTRICT_WINDOW_MAX_N_REGION`]'s doc /// for why `Region` needs its own, much larger per-axis ceiling on the SAME /// `n` (window extent in districts). pub const DISTRICT_WINDOW_MAX_N: u32 = 64; /// Per-axis ceiling on [`AtlasLayerRequest::window_n`] for /// [`WindowGranularity::Region`] requests ONLY (T-1152 step 4: "work out what /// n means at region granularity ... and document it"). `n` is always the /// window extent in DISTRICTS regardless of rung (T-1150 design doc §2, /// unchanged) — but [`DISTRICT_WINDOW_MAX_N`] (64 districts ≈ 131 km) was /// sized for the district rung's own per-cell world extent, and reusing it /// unchanged for `Region` would clamp every region window to well under /// ONE region's own 100-district side ([`crate::atlas::scale::DISTRICTS_PER_REGION`]), /// making [`WindowGranularity::cell_grid_side`] round every legal `n` down /// to a degenerate 1×1 cell grid — a "region view" that can never show more /// than one region cell is not a useful rung. /// /// Derived (not a new magic number, per the D-243 instruction): the largest /// `n` for which `cell_grid_side(n) == sqrt(WIRE_CAP_CELLS)` (64 region /// cells across, the same [`WIRE_CAP_CELLS`]-derived side length district /// mode already reaches at its own cap) is /// `sqrt(WIRE_CAP_CELLS) * DISTRICTS_PER_REGION = 64 * 100 = 6,400` /// districts (≈13,100 km — comfortably covering a planetary hemisphere's /// worth of region tiles in one capped request). The WIRE-SIZE ceiling /// ([`WIRE_CAP_CELLS`] via [`clamp_window_n_v2`]) is still the actual /// enforcement point (never trusted from the wire) — this constant only /// widens the PER-AXIS ceiling far enough that the wire-size math has room /// to matter for `Region`, exactly mirroring how [`DISTRICT_WINDOW_MAX_N`] /// relates to the wire-size ceiling at district granularity (see /// `clamp_window_n`'s doc: "Applied AFTER the per-axis clamp so a request /// that already satisfies [the per-axis cap] still shrinks further"). pub const DISTRICT_WINDOW_MAX_N_REGION: u32 = WIRE_CAP_CELLS_SQRT * crate::atlas::scale::DISTRICTS_PER_REGION as u32; /// `WIRE_CAP_CELLS`'s integer square root (64) — computed once as a `const` /// so [`DISTRICT_WINDOW_MAX_N_REGION`]'s derivation is checkable at compile /// time rather than repeating the literal `64` as an uncommented magic /// number. `WIRE_CAP_CELLS = 4_096 = 64²` exactly (see that constant's own /// doc), so this is exact integer arithmetic, not an approximation. const WIRE_CAP_CELLS_SQRT: u32 = 64; const _: () = assert!(WIRE_CAP_CELLS_SQRT * WIRE_CAP_CELLS_SQRT == WIRE_CAP_CELLS); /// Finer-than-district spacing multipliers (T-1150, zoom ladder design doc /// §3/§5): the number of derived cells per district side. `1` = district /// spacing (2,048 m/cell, [`DISTRICT_WINDOW_MAX_N`]'s existing behavior). /// `4` = quarter spacing (512 m/cell, D-243) — Option B from the design doc: /// full reclassification at the finer spacing via `derive_at_metres`, not a /// coarser-cell interpolation. /// /// **T-1159:** these used to also be legal VALUES of the wire-facing /// `AtlasLayerRequest::window_granularity: u32` field (resolved via the /// now-removed `resolve_window_granularity`) — that field is retired, fully /// shadowed by [`WindowGranularity`] since T-1152. These constants remain as /// internal spacing-multiplier values (see [`WindowGranularity::spacing_multiplier`], /// [`clamp_window_n`]). pub const WINDOW_GRANULARITY_DISTRICT: u32 = 1; pub const WINDOW_GRANULARITY_QUARTER: u32 = 4; /// Server-side wire-size ceiling (T-1150, design doc §3 "Cell-count cap"): /// `window_n² × granularity² ≤ WIRE_CAP_CELLS`. At `WIRE_CAP_CELLS = 4,096`, /// district `n=64` (the existing [`DISTRICT_WINDOW_MAX_N`] cap) sits exactly /// at the ceiling (64² × 1² = 4,096), and quarter mode is clamped to /// `n=16` districts across (16² × 4² = 4,096 — matching the design doc's /// worked example, "Quarter, capped to same cell budget (n≈16 districts /// across)") — this is the "extent shrinks as granularity refines, payload /// stays ~constant" rule the design doc requires, enforced here (never /// trusted from the wire) rather than merely asserted. pub const WIRE_CAP_CELLS: u32 = 4_096; // --------------------------------------------------------------------------- // WindowGranularity (T-1152, R5 redesign — D-226 T-1143-rulings amendment's // "Wire-contract note (T-1150, PR #191 review — Tyre)") // --------------------------------------------------------------------------- /// The full window-derivation-granularity vocabulary (T-1152), superseding /// `window_granularity: u32`'s finer-than-district-only ceiling (Tyre's /// wire-contract note, D-226 T-1143-rulings amendment: "the `u32` multiplier /// field expresses finer-than-district integer multiples only ... a new /// magic value is not the path"). This is the R5 redesign the note demands: /// an explicit **named-variant enum**, the same wire pattern [`RoadNodeKind`] /// already uses on this carrier (a plain `#[derive(Serialize, Deserialize)]` /// enum with no `#[repr]`/manual impl serializes as its variant name over /// `rmp_serde`, not an integer discriminant — deliberately NOT the /// `repr(u8)`-cast-to-`Vec` convention the six dense per-cell arrays use; /// this field is a scalar tag, not a bulk payload, so the string-tag /// legibility is worth the few extra wire bytes one field costs). /// /// **Why an enum and not a signed/log-scale int (the R5 alternative the risk /// row named):** a log-scale `i32` still needs a lookup table to turn back /// into a spacing, and a "cannot express" bug (someone passing `-2` and /// expecting quarter-of-quarter) is silent at the type level. An enum with an /// exhaustive match in [`WindowGranularity::spacing_m`] makes "this variant /// has no defined spacing" a compile error, not a runtime surprise — the same /// reasoning `MorphologyZone`'s exhaustive-match discipline already /// established for this codebase (D-239 §6). /// /// **Resolution (T-1152, simplified T-1159):** [`AtlasLayerRequest::window_granularity_v2`] /// resolves directly via [`resolve_window_granularity_v2`], the single /// widening point for this enum. Absent (`#[serde(default)]`, `None`) /// resolves to `District`. /// /// **T-1159:** this used to also reconcile against a legacy /// `AtlasLayerRequest::window_granularity: u32` field (whenever THIS field /// was absent) — that field is retired, fully shadowed since T-1152 and /// never sent as anything but its byte-compatible default by any caller in /// this codebase (no external client exists, single-repo client/server pair). /// /// **Unknown → District** at every resolution boundary (never trust the /// wire) — same posture as every other wire-decoded enum in this module. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub enum WindowGranularity { /// 512 m/cell (D-243 `QUARTER_M`) — finer than district, T-1150 Option B. Quarter, /// 2,048 m/cell (D-243 `DISTRICT_M`) — the original, still-default rung. District, /// 204,800 m/cell (D-243 `REGION_M`) — the coarser-than-district rung R5 /// flagged as unrepresentable in the `u32` multiplier encoding. Serves /// BOTH the design doc's named "Region" row and the "Orbital/planetary" /// row (R4: "Acceptable if the planetary rung's tiling effectively /// subsumes it" — they share one derivation mode, `derive_orbital_at_metres`, /// and one spacing; there is no third, coarser rung to distinguish them /// by today, so one variant covers both named rows without inventing a /// spacing the design doc never specified). Region, } impl WindowGranularity { /// Cell spacing in metres — the single source of truth every caller /// (server derive loop, cap math, client mirror) must read through, /// rather than re-deriving the metre value from the variant name. /// Sourced from `scale::` (D-243), never a magic number local to this /// module (T-1152 instruction: "Update the D-243-derived spacing /// constants from `scale::` rather than new magic numbers"). pub fn spacing_m(self) -> f64 { match self { WindowGranularity::Quarter => crate::atlas::scale::QUARTER_M as f64, WindowGranularity::District => DISTRICT_M as f64, WindowGranularity::Region => crate::atlas::scale::REGION_M as f64, } } /// The finer-than-district spacing multiplier ([`WINDOW_GRANULARITY_DISTRICT`] /// / [`WINDOW_GRANULARITY_QUARTER`]) this variant corresponds to — `None` /// for `Region`, which has no such multiplier (its spacing is coarser, /// not a finer subdivision of a district). Used only by /// [`clamp_window_n_v2`]'s District/Quarter branch to reuse /// [`clamp_window_n`]'s per-axis-cap math rather than re-deriving it. /// /// **T-1159:** this used to double as the wire-back-compat value for the /// now-retired `window_granularity: u32` echo (`DistrictWindowLayer.granularity`) /// — that role, and the `key_u32()` method that served it, are gone; this /// is purely an internal spacing-multiplier lookup now. fn spacing_multiplier(self) -> Option { match self { WindowGranularity::District => Some(WINDOW_GRANULARITY_DISTRICT), WindowGranularity::Quarter => Some(WINDOW_GRANULARITY_QUARTER), WindowGranularity::Region => None, } } /// The derived cell-grid side length (in CELLS, at this granularity) for /// a window whose extent is `n` DISTRICTS (T-1152 step 4: "work out what /// n means at region granularity against D-243's region=100-district /// side and document it" — this is that answer, made a total function /// instead of inline arithmetic at each call site). /// /// `n` is ALWAYS the window extent in DISTRICTS regardless of /// granularity (T-1150 design doc §2, unchanged by this ticket: "the /// window's `n` stays the DISTRICT extent"). The derived grid's side /// length scales by [`Self::spacing_m`] relative to [`DISTRICT_M`]: /// /// - `Quarter` (512 m, 4 cells/district side): `side = n * 4` — MORE /// cells than districts requested (T-1150's existing behavior, /// unchanged). /// - `District` (2,048 m, 1:1): `side = n` — unchanged. /// - `Region` (204,800 m = 100 districts/side, D-243 /// `DISTRICTS_PER_REGION`): `side = round(n / 100)`, minimum 1. A /// region-granularity request with the SAME `n` as a district request /// derives a FAR SMALLER cell grid (a region window with `n=64` /// districts — the per-axis cap — derives only a 1×1 region cell, /// since 64 districts is well under one region's 100-district side). /// This inversion (finer rungs MULTIPLY cell count by `n`; `Region` /// DIVIDES it) is why [`clamp_window_n`]'s wire-cap math needs a /// region-aware branch too (see that function) — a flat /// `n² × multiplier² ≤ WIRE_CAP_CELLS` formula would make `n` for /// `Region` requests nonsensically tiny if `multiplier` were naively /// `1/100`. Rounding (not floor/ceil) keeps the mapping the closest /// integer approximation of the true ratio; minimum 1 so an `n` smaller /// than one region never derives a degenerate empty grid. pub fn cell_grid_side(self, n: u32) -> i32 { match self { WindowGranularity::Quarter => (n * WINDOW_GRANULARITY_QUARTER) as i32, WindowGranularity::District => n as i32, WindowGranularity::Region => { let dpr = crate::atlas::scale::DISTRICTS_PER_REGION as f64; ((n as f64 / dpr).round() as i32).max(1) } } } } /// Resolve a request's granularity to a [`WindowGranularity`] — **the single /// widening point for the full (finer- and coarser-than-district) vocabulary** /// (T-1152). Absent (`None`, `#[serde(default)]`) resolves to /// [`WindowGranularity::District`] — unknown/malformed variants can't reach /// this function at all (`rmp_serde` rejects an unrecognized enum variant /// name at decode time, so "unknown" for this field means "absent", never /// "present but garbage"). /// /// **T-1159:** this function used to fall back to the legacy `window_granularity: u32` /// field (via the now-removed `resolve_window_granularity`) when /// `window_granularity_v2` was absent — that fallback is retired along with /// the field itself (fully shadowed since T-1152, no pre-T-1152 client /// exists). A future coarser-than-region rung is added by widening this /// function's match AND [`WindowGranularity`]'s variant list together. fn resolve_window_granularity_v2(req: &AtlasLayerRequest) -> WindowGranularity { req.window_granularity_v2 .unwrap_or(WindowGranularity::District) } /// Clamp `window_n` against BOTH the existing per-axis cap /// ([`DISTRICT_WINDOW_MAX_N`]) and the granularity-aware wire-size ceiling /// ([`WIRE_CAP_CELLS`]) — `window_n² × granularity² ≤ WIRE_CAP_CELLS` (T-1150 /// design doc §3). Applied AFTER the per-axis clamp so a request that already /// satisfies `DISTRICT_WINDOW_MAX_N` still shrinks further at granularity 4. /// /// **This clamp is echoed, not silently applied** — `serve_district_window` /// puts the CLAMPED `n` into `DistrictWindowLayer.n`, so a client that /// requests an oversized `n` gets back a smaller one. Any client-side /// staleness guard comparing its own requested `n` against the echo MUST /// mirror this exact function first (PR #191 review, Tyre C1) — see /// `atlas_window_request.gd`'s `_clamp_window_n_mirror()`, which matches this /// function bit-for-bit, the same load-bearing-mirror pattern /// `canonicalize_district_center()` (`atlas_descend_geometry.gd`) already /// uses for `normalize_window_center`. fn clamp_window_n(raw_n: u32, granularity: u32) -> u32 { let n = raw_n.clamp(1, DISTRICT_WINDOW_MAX_N); let g = granularity.max(1); let cap_n = (WIRE_CAP_CELLS as f64).sqrt() / g as f64; n.min(cap_n.floor().max(1.0) as u32) } /// [`WindowGranularity`]-aware twin of [`clamp_window_n`] (T-1152 step 4) — /// the SAME two-stage discipline (per-axis clamp, THEN the wire-size /// ceiling on the DERIVED cell count, never trusted from the wire), but /// computed through [`WindowGranularity::cell_grid_side`] so it is correct /// for BOTH directions (finer multiplies cell count; `Region` divides it — /// see that method's doc) instead of assuming the finer-only /// `n × multiplier` relationship [`clamp_window_n`] hard-codes. /// /// - **`District`/`Quarter`:** per-axis cap is [`DISTRICT_WINDOW_MAX_N`] /// (64, unchanged) — byte-identical clamped `n` to [`clamp_window_n`] for /// every input these two variants can produce (verified by /// `clamp_window_n_v2_delegates_to_legacy_for_district_and_quarter`, /// below). /// - **`Region`:** per-axis cap is [`DISTRICT_WINDOW_MAX_N_REGION`] (6,400 — /// see that constant's doc for the derivation), then a halving loop walks /// `n` back if `cell_grid_side(n)` would still exceed `sqrt(WIRE_CAP_CELLS)` /// region cells across. /// /// **This loop is defensive, not currently reachable — stated plainly, not /// left implicit.** `DISTRICT_WINDOW_MAX_N_REGION` is DERIVED as /// `sqrt(WIRE_CAP_CELLS) * DISTRICTS_PER_REGION` specifically so the /// per-axis clamp alone already forecloses the loop's trigger condition: a /// brute-force sweep of every `raw_n` in `[1, DISTRICT_WINDOW_MAX_N_REGION]` /// shows `cell_grid_side(n)` never exceeds `sqrt(WIRE_CAP_CELLS)` (64), so /// `n /= 2` never executes for any input the per-axis clamp lets through — /// verified by `clamp_window_n_v2_region_per_axis_cap_alone_satisfies_wire_cap_for_all_inputs`, /// which pins BOTH the invariant (`cell_grid_side(result)² ≤ WIRE_CAP_CELLS`) /// AND the loop's current no-op status (`result == raw_n.clamp(1, /// DISTRICT_WINDOW_MAX_N_REGION)` for every swept input). The loop is kept /// anyway as the general, correct algorithm (region's `cell_grid_side` is a /// ROUNDING division, not the finer rungs' exact multiplication, so there /// is no closed-form inverse the way `cap_n = sqrt(WIRE_CAP_CELLS) / g` is /// for the finer case) — it is the safety net for a FUTURE cap derivation /// that doesn't land exactly on the boundary (a new rung from a later /// measurement pass, or a `WIRE_CAP_CELLS` retune that isn't a perfect /// square times `DISTRICTS_PER_REGION`). If a future constant change makes /// the loop actually fire, the pinned no-op assertion above breaks loudly, /// forcing a deliberate look rather than a silent behavior change. fn clamp_window_n_v2(raw_n: u32, granularity: WindowGranularity) -> u32 { match granularity { WindowGranularity::District | WindowGranularity::Quarter => clamp_window_n( raw_n, granularity .spacing_multiplier() .unwrap_or(WINDOW_GRANULARITY_DISTRICT), ), WindowGranularity::Region => { let mut n = raw_n.clamp(1, DISTRICT_WINDOW_MAX_N_REGION); while granularity.cell_grid_side(n).pow(2) as u32 > WIRE_CAP_CELLS && n > 1 { n /= 2; } n.max(1) } } } /// Quantized `window_min_wl_m` bands (T-1150, zoom ladder design doc §5; /// retuned T-1162 part (c); band 4 re-derived per Tyre's PR #194 review I1). /// `0` (no cutoff) plus the two coarse legacy bands /// [`crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M`]`[0..2]` (32,768/ /// 16,384/8,192 m — carried over unchanged, no rung claims these as its own /// Nyquist floor), plus **District's own Nyquist floor** computed directly /// from the rung's spacing (`2 × DISTRICT_M = 4,096` m), plus **Quarter's own /// Nyquist floor** (`2 × QUARTER_M = 1,024` m). /// /// **Dependency direction (Tyre, PR #194 I1):** the cutoff's semantic job is /// Nyquist truncation — a property of the RUNG (its sample spacing), never of /// any one invention field's octave choices. Band 4 is therefore derived from /// `scale::DISTRICT_M` directly, NOT from /// `detail_scatter::OCTAVE_WAVELENGTHS_M[3]` (the old T-1162 wiring) — that /// coupling ran backwards: a future `detail_scatter` retune could silently /// redefine the Atlas rung floor, and the coast warp already extends its OWN /// octaves below `OCTAVE_WAVELENGTHS_M`'s range (T-1162 part a), proving no /// single invention field's array is entitled to dictate the floor. The two /// values happen to coincide today (4,096 = `2 × DISTRICT_M` = also /// `OCTAVE_WAVELENGTHS_M[3]`) — the `const _: () = assert!(...)` immediately /// below pins that coincidence so a future drift on EITHER side breaks the /// build loudly instead of silently decoupling the two meanings. /// /// **Pre-T-1162 bands unchanged in VALUE** (32,768/16,384/8,192/4,096 stay /// exactly where they were) — but **NOT unchanged in behavior at 8,192/4,096 /// specifically**: `terrain_detail`'s own octave array is untouched by this /// ticket, so THAT contribution is byte-identical at every pre-existing band, /// but the coast warp's octave array gained genuinely new 8,192/4,096 m /// entries (T-1162 part a) that a District-rung request quantizing to those /// bands now legitimately admits — District's real Nyquist floor (4,096 m) /// is coarse enough to resolve that content, so this is intended enrichment /// at District too, not a leak (see `coast_invention::WARP_OCTAVE_WAVELENGTHS_M`'s /// doc for the full admit/exclude table). Only the 32,768/16,384 bands are /// truly inert-to-this-ticket (no octave in any extended array falls in that /// range). The sub-district relief band ([`crate::atlas::detail_scatter::VOXEL_OCTAVE_WAVELENGTHS_M`], /// all ≤1,024 m) and the coast warp's two finest additions (2,048/1,024 m) /// are genuinely excluded at every pre-existing band (all ≥4,096) — those are /// Quarter-exclusive, admitted only by the new `1,024` band. The new band is /// additive at the END of the array — appending, not reordering, keeps every /// existing index-based reference to the first four entries valid. /// /// Descending order except the leading `0.0` sentinel, matched by /// `quantize_min_wl_m`'s scan below. const MIN_WL_BANDS_M: [f64; 6] = [ 0.0, crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[0], crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[1], crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[2], 2.0 * DISTRICT_M as f64, 2.0 * crate::atlas::scale::QUARTER_M as f64, ]; /// Direction-agnostic guard (Tyre, PR #194 I1): `MIN_WL_BANDS_M`'s District /// band is now derived from `2 × DISTRICT_M`, deliberately decoupled from /// `detail_scatter::OCTAVE_WAVELENGTHS_M[3]` (see the doc above). The two /// values are expected to keep coinciding — `terrain_detail`'s finest octave /// IS meant to sit at District's Nyquist floor — but nothing in the type /// system enforces that anymore now that the dependency runs one direction /// only. This assert exists so that if either side ever drifts (a /// `detail_scatter` octave retune, or a `DISTRICT_M` scale-ladder change). /// the build breaks loudly and a human decides deliberately whether the /// coincidence should be restored or the two meanings were meant to diverge /// — never a silent redefinition of the Atlas rung floor. const _: () = assert!(crate::atlas::detail_scatter::OCTAVE_WAVELENGTHS_M[3] as i64 == 2 * DISTRICT_M as i64); /// Snap a wire-supplied `window_min_wl_m` to the nearest fixed band in /// [`MIN_WL_BANDS_M`] (T-1150, design doc §5's gap-fix): "as specified, /// `window_min_wl_m` is viewport-continuous while the cache key/echo tuple is /// `(body, center, n, granularity)` — same key, different `min_wl`, would /// silently collide. Fix: quantize `min_wl_m` to a small fixed set of bands /// ... and add the quantized band to both the echo and the cache key." This /// is that quantization, applied unconditionally to every request before it /// touches either the cache key or the `DeriveWindow` work item — **never /// the raw wire value past this point**, same discipline as `window_n`'s /// clamp and `window_center`'s normalization. Nearest-band snap (ties round /// to the coarser/lower band, i.e. `<=` on the running best distance) keeps /// the mapping total and deterministic for any `u32` input, including values /// far outside the octave range (e.g. `u32::MAX` snaps to the coarsest band). pub(crate) fn quantize_min_wl_m(raw: u32) -> u32 { let raw_f = raw as f64; let mut best = MIN_WL_BANDS_M[0]; let mut best_dist = (raw_f - best).abs(); for &band in &MIN_WL_BANDS_M[1..] { let dist = (raw_f - band).abs(); if dist < best_dist { best = band; best_dist = dist; } } best as u32 } /// A client request for a body's generation layers (D-225), extended with an /// optional district-resolution window query (D-226 T-1124 amendment §1, T-1137). /// /// `up_to` is a forward-compat seam that is **not yet honored**: `run_work_item` /// (`gen_queue.rs`) currently runs the cascade through `CascadeLayer::Region` /// (the terminal layer, T-1113) unconditionally on every request, ignoring this /// field. Wiring per-request depth (and the partial caching it implies) is /// deferred to #1021. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AtlasLayerRequest { pub body_id: String, pub up_to: CascadeLayer, /// District-window centre (D-226 T-1124 amendment §1, T-1137). `None` = no /// window requested (whole-body layers only — today's behavior, byte-unchanged /// for every existing caller thanks to `#[serde(default)]`). #[serde(default)] pub window_center: Option, /// Window side length in districts. Ignored when `window_center` is `None`. /// Clamped server-side to `[1, DISTRICT_WINDOW_MAX_N]` — **never trusted /// from the wire** (D-226 T-1124 amendment §4). #[serde(default)] pub window_n: u32, /// Window derivation granularity (T-1152, R5 redesign — see /// [`WindowGranularity`]'s doc for the full rationale). `#[serde(default)]` /// (`None`) resolves to [`WindowGranularity::District`] (today's default /// behavior, byte-compatible with every pre-T-1150 caller) via /// [`resolve_window_granularity_v2`] — **never trusted from the wire**, /// unrecognized/absent values fall back to district. /// /// **T-1159:** the legacy `window_granularity: u32` field this superseded /// (T-1150's finer-than-district-only encoding) is retired — this enum /// fully shadowed it since T-1152 landed, and no pre-T-1152 client exists /// (single-repo client/server pair). See D-255(c): the `district_window` /// carrier itself stays alive byte-unchanged for its existing consumer; /// only the redundant `u32` alongside this enum is gone. #[serde(default)] pub window_granularity_v2: Option, /// Octave cutoff for the invented-terrain scatter (T-1149's /// `min_wavelength_m`), in whole metres. `0` (absent) = no cutoff = the /// pre-T-1150 behavior. /// /// **Quantization contract (Hoshe 1 / Tyre C3, PR #191 review; design doc /// §5):** the wire value here is an UNQUANTIZED, unclamped raw passthrough /// — client codecs may send any `u32`. The SERVER is the one place /// quantization happens: `serve_district_window` snaps every request's /// value to the nearest fixed band in /// [`MIN_WL_BANDS_M`] via [`quantize_min_wl_m`] BEFORE it ever touches /// the cache key or the `DeriveWindow` work item, and the QUANTIZED value /// (not this raw field) is what gets echoed back on /// `DistrictWindowLayer.min_wl_m` and used as the cache key component. /// This closes the §5 gap: without quantization, two requests differing /// only in a continuous-valued `min_wl_m` would silently miss each /// other's cache entries (the unbounded-key-space problem §5 exists to /// close) — the client is free to send a viewport-continuous estimate; /// the server's quantization is what makes the key space bounded again. #[serde(default)] pub window_min_wl_m: u32, } /// Status of a layer response (D-225). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum AtlasLayerStatus { /// Layer data is ready (`layer1` is populated). Ready, /// Analysis was enqueued; the client should re-request shortly. Pending, /// The body is unknown or has no source terrain — re-requesting won't help. NotFound, /// Resolution / IO failure (message for the client log). Error(String), } /// The cascade's coarse district grid, surfaced for the Atlas generation overlay /// (T-1046, D-226). This is the **planetary-scale** view — one cell per coarse /// grid square (`grid_w/cols` heightmap pixels) — not the on-demand 2 km districts /// (those derive only when a player enters a settlement, Phase 5). `morphology` /// and `elev_q` are row-major (`rows × cols`); `morphology[i]` is a `MorphologyZone` /// discriminant (D-239 §6, `repr(u8)`), `elev_q[i]` is 0–100 elevation for relief /// shading. The client maps `cols × rows` onto the displayed heightmap. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DistrictGridLayer { pub cols: u32, pub rows: u32, pub morphology: Vec, pub elev_q: Vec, } /// A layer response: the computed `Layer1Output` + the coarse district grid /// (D-225, T-1046) + the road-graph and settlement overlays (T-960 §1/§2) + /// the region climate grid (T-1113) + the quarter-footprint overlay (T-1112, /// T-1119) + the district-resolution window query (T-1124, T-1137), or a /// non-ready status. /// /// Growth ceiling (governance-bounded): the one-`Option`-field-per-layer /// pattern tops out at six fields for the **dense whole-body layer family** /// (`district_grid`, `road_graph`, `settlements`, `region_grid`, /// `quarter_footprints` — each a compute-once, cache-per-body snapshot) — /// D-226's 2026-07-13 amendment (d) rules out any L5/tile Atlas layer ever, /// and `quarter_footprints` below is the last candidate the 2026-07-16 T-1112 /// amendment named. **That budget is now consumed:** a seventh *whole-body* /// field is not a naming exercise like the six before it — a future /// generation-layer addition needs its own governance, not a drive-by field. /// /// The D-226 T-1124 amendment (2026-07-18) RESOLVED what carries the next /// addition, and it is NOT this family: a **windowed viewport query** is a /// categorically different payload (keyed on the *request* `(body, center, n)`, /// re-fetched per pan, not a per-body snapshot). `district_window` (wired here, /// T-1137) rides on `AtlasLayerResponse` but is explicitly OUTSIDE the /// whole-body family and does not count against the six-field ceiling above /// (D-226 T-1124 §2). The windowed family has its own hard cap: exactly ONE /// windowed-query field; a second windowed query (a second viewport, a /// windowed chunk-preview) is a dedicated response message by rule, not a /// second `Option` here (D-226 T-1124 §2, symmetric with the request-side /// five-shape demux ceiling in `bridge/mod.rs`). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AtlasLayerResponse { pub body_id: String, pub status: AtlasLayerStatus, pub layer1: Option, /// The coarse district/morphology grid for the Atlas overlay (T-1046). /// `Some` on a cache hit once the DistrictProfile layer has run; `None` otherwise. pub district_grid: Option, /// The inter-settlement road/rail graph overlay (T-960 §1, T-1038). /// `Some` on a cache hit once the RoadGraph layer has run; `None` otherwise /// (including a body with zero placed settlements — an empty graph has no /// nodes to draw, so it collapses to `None` the same way `district_grid` /// does for an unrun layer). pub road_graph: Option, /// The settlement-placement overlay (T-960 §2, #955). `Some` on a cache hit /// once the Settlement layer has placed at least one city; `None` otherwise. pub settlements: Option, /// The region climate grid for the Atlas overlay (D-243 §3, T-1113). /// `Some` on a cache hit once the Region layer has run; `None` otherwise. pub region_grid: Option, /// The requested district window (D-226 T-1124 amendment, T-1137), or /// `None` when the request carried no `window_center` / no window data is /// cached yet for a pending derive. Distinct from the five layers above: /// keyed on the REQUEST `(body, center, n)`, not on the body alone — see /// the struct-level doc. pub district_window: Option, /// The quarter-footprint overlay (D-226 T-1112 amendment, T-1119). `Some` /// on a cache hit once at least one settlement's quarter skeleton has been /// generated (`state.quarters` non-empty); `None` otherwise, including a /// body with placed settlements whose quarters haven't finished the async /// `GenerateSkeleton` pass yet (skeleton generation runs at `Low` priority /// after the body's own `Ready` snapshot is cached — see `plugin.rs`). pub quarter_footprints: Option, } /// Build the coarse [`DistrictGridLayer`] from a body's cached state (T-1046). /// Returns `None` when the DistrictProfile layer has not run (empty `districts`). /// The grid is dense `[0, cols) × [0, rows)` (the cascade tiles the full /// heightmap), so the extent comes from the maximum `SurveyCellPos` (D-256(b) /// — this is the coarse survey raster, not the true district grid). pub fn build_district_grid( state: &crate::atlas::body_world_state::BodyWorldState, ) -> Option { if state.districts.is_empty() { return None; } let cols = state.districts.keys().map(|p| p.0).max().unwrap_or(0) as u32 + 1; let rows = state.districts.keys().map(|p| p.1).max().unwrap_or(0) as u32 + 1; let n = (cols * rows) as usize; let mut morphology = vec![0u8; n]; let mut elev_q = vec![0u8; n]; for (pos, profile) in &state.districts { let (x, y) = (pos.0, pos.1); if x < 0 || y < 0 { continue; } let i = (y as u32 * cols + x as u32) as usize; if i < n { morphology[i] = profile.morphology_zone as u8; elev_q[i] = profile.elev_q.clamp(0, 100) as u8; } } Some(DistrictGridLayer { cols, rows, morphology, elev_q, }) } // --------------------------------------------------------------------------- // RegionGridLayer (T-1113, D-243 §3) // --------------------------------------------------------------------------- /// The ~205 km region climate grid for the Atlas overlay (T-1113), dense /// row-major like [`DistrictGridLayer`] (the T-1046 encoding precedent). /// Serves the **mean-state** `RegionClock` fields only — the Q-105 tick-phase /// callbacks are deferred, so what ships is the static climate context. /// /// Wire encoding is all-integer (D-010 wire discipline): /// - `season[i]` / `weather[i]` — the `repr(u8)` discriminants of /// `SeasonPhase` / `WeatherState` (pinned, append-only). /// - `mean_temp_dc[i]` — mean-annual temperature baseline in **deci-°C** /// (×10, `round`ed; 0.1 °C is ample for a map overlay). `i16::MIN` is the /// sentinel for "no atmosphere → no temperature" (airless bodies carry /// `mean_temp_c: None`); real values are class-band-clamped far inside /// i16 range. /// - `moisture_q[i]` — the 0–100 region moisture primitive. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RegionGridLayer { pub cols: u32, pub rows: u32, pub season: Vec, pub weather: Vec, pub mean_temp_dc: Vec, pub moisture_q: Vec, } /// Sentinel for "airless body — no temperature baseline" in /// [`RegionGridLayer::mean_temp_dc`]. pub const REGION_TEMP_NONE_DC: i16 = i16::MIN; /// Build the [`RegionGridLayer`] from a body's cached state (T-1113). /// Returns `None` when the Region layer has not run (empty `regions`). /// The stored region set is the dense covering grid `[0, cols) × [0, rows)` /// (see `cascade::LayerRegionOutput` — no blend-padding ring), so the extent /// comes from the maximum `RegionPos`, mirroring [`build_district_grid`]. pub fn build_region_grid( state: &crate::atlas::body_world_state::BodyWorldState, ) -> Option { if state.regions.is_empty() { return None; } let cols = state.regions.keys().map(|(x, _)| *x).max().unwrap_or(0) as u32 + 1; let rows = state.regions.keys().map(|(_, y)| *y).max().unwrap_or(0) as u32 + 1; let n = (cols * rows) as usize; let mut season = vec![0u8; n]; let mut weather = vec![0u8; n]; let mut mean_temp_dc = vec![REGION_TEMP_NONE_DC; n]; let mut moisture_q = vec![0u8; n]; for (&(x, y), profile) in &state.regions { if x < 0 || y < 0 { continue; } let i = (y as u32 * cols + x as u32) as usize; if i < n { season[i] = profile.clock.season as u8; weather[i] = profile.clock.weather as u8; mean_temp_dc[i] = match profile.clock.mean_temp_c { Some(t) => { ((t * 10.0).round() as i32).clamp(i16::MIN as i32 + 1, i16::MAX as i32) as i16 } None => REGION_TEMP_NONE_DC, }; moisture_q[i] = profile.moisture_q.clamp(0, 100) as u8; } } Some(RegionGridLayer { cols, rows, season, weather, mean_temp_dc, moisture_q, }) } // --------------------------------------------------------------------------- // DistrictWindowLayer (D-226 T-1124 amendment, T-1137) // --------------------------------------------------------------------------- /// The requested district window: an `n × n` grid of TRUE 2 km districts /// (or, at `granularity = 4`, an effective `(4n) × (4n)` grid of 512 m /// quarters covering the SAME world extent — see `granularity` doc below), /// derived on-demand via `district_profile::derive_district`/`derive_at_metres` /// (D-226 T-1124 amendment §2, T-1150). **Echoes `center`/`n`/`granularity` /// back** — this is the client's race-condition guard, not a convenience /// field: because the derivation is pure and deterministic (D-227), the same /// `(center, n, granularity, min_wl_m)` query always yields the same payload, /// so the echoed tuple *is* the cache/staleness key the client compares /// against its most recently requested window (`body_id` disambiguation /// rides the enclosing `AtlasLayerResponse`, not the echo — see the /// amendment). /// /// All six arrays are dense row-major (`i = row * side + col`, where `side` /// is `n` at district granularity or `4n` at quarter granularity), matching /// the `DistrictGridLayer`/`RegionGridLayer` indexing convention. Per-cell /// wire cost is 7 bytes (1+1+2+1+1+1) before MessagePack framing overhead /// (D-226 T-1124 amendment §4). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct DistrictWindowLayer { pub center: DistrictPos, /// Window extent in DISTRICTS — this does NOT change with granularity /// (T-1150 design doc §2: "the window's `n` stays the DISTRICT extent"). /// The derived cell grid's actual side length is `n * granularity`. pub n: u32, /// The R5-redesigned granularity (T-1152) — see [`WindowGranularity`]'s /// doc. Always populated (never `None`): the server always resolves a /// concrete rung internally via [`resolve_window_granularity_v2`]. /// /// **T-1159:** the legacy `granularity: u32` echo this field used to sit /// alongside (a wire-back-compat value for pre-T-1152 clients, which do /// not exist — single-repo client/server pair) is retired. This enum /// echo has been the sole granularity signal since T-1152. pub granularity_v2: WindowGranularity, /// The `min_wavelength_m` octave cutoff (T-1149) this window was derived /// with, in whole metres (`0` = no cutoff). Echoed for the same reason as /// `granularity` — two windows at identical `(center, n, granularity)` /// but different cutoffs are NOT the same payload and must not alias. pub min_wl_m: u32, /// `MorphologyZone` discriminant, the frozen 17-zone vocabulary (D-239 §6). pub morphology: Vec, /// 0-100, matches `DistrictGridLayer.elev_q` encoding. pub elev_q: Vec, /// Deci-°C, [`REGION_TEMP_NONE_DC`] sentinel — the SAME scheme as /// `RegionGridLayer.mean_temp_dc`, deliberately not a separate /// district-tier quantization (one temperature colorizer spans both zoom /// levels, D-226 T-1124 amendment §2). pub temp_dc: Vec, /// 0-100, matches `DistrictGridLayer` precedent. pub moisture_q: Vec, /// `VegetationClass` discriminant, 0-6 including `Marine = 6` (T-1126) — /// any client palette MUST be exhaustive over `Marine` (D-226 T-1124 /// amendment §3, non-negotiable — the ocean-blind-vegetation bug this /// field caught at the district tier). pub vegetation: Vec, /// `GlaciationGrade` discriminant, 0-4 (T-1127). pub glaciation: Vec, /// Invented river course polylines intersecting this window (T-1170, /// Ruling 1b/1c/3h). **Not part of the windowed-family ceiling** (D-226 /// T-1124 §2 [HARD]) — that ceiling counts windowed-QUERY fields; this is /// content of the ONE existing windowed payload, arriving on the same /// echo key with the same staleness semantics as the six dense arrays /// above (governance capture: `governance/decisions/architecture.md`, /// D-226 amendment 2026-07-23, course-invention carrier note). /// `#[serde(default)]` — the additive T-1124 §1 pattern: a pre-T-1170 /// payload/fixture decodes to an empty `Vec`, never an error. #[serde(default)] pub courses: Vec, } /// One invented river course polyline intersecting a window (T-1170, Ruling /// 3h). Only edges whose amplitude-inflated chord bounding box intersects the /// window ship; `points` are cropped to the window plus one station beyond /// each edge of it (so client-side polyline drawing has continuity into the /// next window without needing to stitch across a request boundary). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RiverCourse { /// The packed upstream-cell id (`river_course::pack_cell_id`) — the /// edge's stable identity (Ruling 2d), stable across every window/rung /// that ships this same edge. pub edge_id: u32, /// `river_class` at the edge's upstream cell (0=stream, 1=tributary, /// 2=trunk) — the SAME vocabulary `RiverNetwork.river_class` uses, so /// client-side per-rung/per-class filtering (Araminta's presentation /// tables, Ruling 5c) reuses the existing decode path. pub class: u8, /// Points along the course, in absolute world metres, cropped to this /// window (+ one station beyond each edge, Ruling 3h). pub points: Vec<(i32, i32)>, /// How this course's downstream end resolves (Ruling 3e/3f) — `None` when /// the course's true downstream terminus (whether `Mouth` or /// `ContinuesBeyondWindow`) falls outside this window's cropped point /// range, so nothing about the terminus can be asserted from this /// payload alone. pub terminus: CourseTerminus, } /// [`RiverCourse::terminus`] — the course's downstream-end classification on /// the wire (Ruling 3h). #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum CourseTerminus { /// The course's downstream end is not within this window's cropped point /// range — the real terminus (whatever it is) lies in a different window. None, /// The course reaches a real sea/lake crossing within this window (Ruling /// 3e) — the last point in `points` is the resolved invented-coast /// terminus. Mouth, /// The course reaches a grid-edge drain (Ruling 3f) — a grid artifact, /// not a mouth; the last point in `points` is the last in-grid station, /// with no mouth marker implied. EdgeDrain, /// The course's downstream end is a real river cell beyond this window's /// crop range — i.e. an `Interior`-terminus edge whose full extent is /// wider than what got cropped in. The client draws the polyline without /// a terminus marker and expects it to continue in an adjacent window. ContinuesBeyondWindow, } /// Key for the server-side window derive cache (T-1137, extended T-1150, /// extended again T-1152): `(body_id, center, n, granularity, min_wl_m)`. /// D-227 purity means a cached window is valid forever for a given /// body+seed — no staleness/TTL invalidation is needed, only a bound on /// unbounded growth (see [`DistrictWindowCache`]). `granularity`/`min_wl_m` /// MUST be part of the key — the design doc's aliasing risk (§3): a /// granularity-4 request at the same `(body, center, n)` as a granularity-1 /// request is a DIFFERENT payload and must land in a different cache slot, /// never silently overwrite or be served by the other. /// /// **The granularity slot is [`WindowGranularity`] itself (T-1152), not the /// legacy `u32`** — carrying the full enum here (rather than relying on /// [`WindowGranularity::key_u32`]'s reserved-sentinel trick alone) is what /// makes a `Region` window's cache slot structurally distinct from a /// `District`/`Quarter` one, satisfying the "carry the new representation" /// requirement at the cache-key touch point directly rather than through an /// encoding side-channel. `WindowGranularity`'s `Ord` derive (declaration /// order: `Quarter < District < Region`) makes this legal as a `BTreeMap` /// key (D-010 determinism — ordered iteration, no `HashMap`). pub type DistrictWindowKey = (String, DistrictPos, u32, WindowGranularity, u32); /// Bounded LRU-ish cache of completed district-window derives (T-1137), a /// sibling to [`BodyWorldStateCache`] rather than a field on it: windows are /// keyed on the *request* `(body, center, n)`, not the body alone (see the /// struct-level doc on [`AtlasLayerResponse`]), so they don't fit the /// per-body cache's keying at all. Eviction is capacity-only FIFO-by-insertion /// (not access-recency LRU like `BodyWorldStateCache`) — window requests are /// comparatively rare and cheap to re-derive on a genuine miss (a background /// re-submit, never a stall), so exact recency tracking isn't worth the /// bookkeeping; a simple bound against unbounded growth is enough. #[derive(Resource, Debug, Default)] pub struct DistrictWindowCache { entries: std::collections::BTreeMap, /// Insertion order, oldest first — the eviction queue. order: std::collections::VecDeque, capacity: usize, } /// Default capacity for [`DistrictWindowCache`] — generous relative to /// `BodyWorldStateCache::CACHE_CAPACITY` (50 bodies) since each entry here is /// far smaller (a handful of `Vec`/`Vec` at `n ≤ 64`, ≤ 28 KiB raw vs. /// `BodyWorldState`'s full heightmap + districts + regions), and several /// windows can legitimately be live per body (a player panning around). pub const DISTRICT_WINDOW_CACHE_CAPACITY: usize = 256; impl DistrictWindowCache { pub fn new(capacity: usize) -> Self { Self { entries: std::collections::BTreeMap::new(), order: std::collections::VecDeque::new(), capacity, } } /// Look up a cached window by its full key. Never mutates — window /// validity has no time component (D-227), so there is nothing to bump. pub fn get(&self, key: &DistrictWindowKey) -> Option<&DistrictWindowLayer> { self.entries.get(key) } /// Insert a completed window derive, evicting the oldest entry first if /// at capacity. Re-inserting an existing key replaces the value without /// moving it in the eviction order (D-227: the value can only ever be /// identical, so this is a no-op in practice, but stays correct either way). pub fn insert(&mut self, key: DistrictWindowKey, layer: DistrictWindowLayer) { if !self.entries.contains_key(&key) { if self.entries.len() >= self.capacity { if let Some(victim) = self.order.pop_front() { self.entries.remove(&victim); } } self.order.push_back(key.clone()); } self.entries.insert(key, layer); } pub fn len(&self) -> usize { self.entries.len() } pub fn is_empty(&self) -> bool { self.entries.is_empty() } } /// One derived cell's packed wire fields — the per-cell output of the window /// loop body, shared between the serial and parallel builders (T-1151) so the /// packing logic can never drift between them. struct WindowCell { morphology: u8, elev_q: u8, temp_dc: i16, moisture_q: u8, vegetation: u8, glaciation: u8, } /// Derive one window cell at `(row, col)` and pack its wire fields. Pure /// (D-227) — the whole reason row-chunked `par_iter` (T-1151) is safe: every /// cell is an independent function of its own world-metre position, nothing /// shared mutably. /// /// `granularity` (T-1150, widened to [`WindowGranularity`] T-1152) selects /// BOTH the metre spacing between cells ([`WindowGranularity::spacing_m`]) /// AND the derivation function: finer-than-district rungs (`District`, /// `Quarter`) call `derive_at_metres` (full derivation, `invent_primitives` /// included) exactly as T-1150 shipped; `Region` calls /// [`crate::atlas::district_profile::derive_orbital_at_metres`] instead — the /// design doc §2/§4 orbital row's region-baseline-blend-only path, no /// `invent_primitives` call. This is the ONE place the two derivation /// functions fork based on rung — everything else in the window-building /// pipeline (`scatter_row`, the six-array packing, the cache/echo plumbing) /// is identical regardless of which function ran, because both produce a /// `DistrictProfile` and this function's WindowCell-packing tail (below) is /// shared. /// /// `half_cells` is HALF the cell-grid side (`side / 2`, already in the /// caller's cell units, not districts), so `center` (a `DistrictPos`, always /// district-scale) is converted to a world-metre origin once by the caller /// and offset here in `spacing_m()` units — this is what makes the quarter /// grid cover the SAME world rect as the district grid at 4x the cell /// density (design doc §2 Option B), and what makes a region-granularity /// window cover a proportionally larger world rect at 1/100x the cell /// density (D-243: `DISTRICTS_PER_REGION = 100`). #[allow(clippy::too_many_arguments)] fn derive_window_cell( seed: SeedChain, body_id: &str, params: &crate::atlas::district_profile::BodyParams, ta: &crate::atlas::features::TerrainAnalysis, climate: &crate::atlas::district_profile::ClimateConstants, center_world_m: (f64, f64), half_cells: i32, granularity: WindowGranularity, min_wavelength_m: f64, row: i32, col: i32, nearby_courses: &[InventedCourse], ) -> WindowCell { // Row 0 = northmost, matching aliveness_probe's render_window_panels // (derive_at_metres maps negative wy to negative lat_frac = north). let step_m = granularity.spacing_m(); let wx = center_world_m.0 + (col - half_cells) as f64 * step_m; let wy = center_world_m.1 + (row - half_cells) as f64 * step_m; let prof = match granularity { WindowGranularity::Region => crate::atlas::district_profile::derive_orbital_at_metres( seed, body_id, params, ta, wx, wy, climate, ), WindowGranularity::District | WindowGranularity::Quarter => { crate::atlas::district_profile::derive_at_metres( seed, body_id, params, ta, wx, wy, climate, min_wavelength_m, nearby_courses, ) } }; WindowCell { morphology: prof.morphology_zone as u8, elev_q: prof.elev_q.clamp(0, 100) as u8, temp_dc: match prof.temperature_c { Some(t) => { ((t * 10.0).round() as i32).clamp(i16::MIN as i32 + 1, i16::MAX as i32) as i16 } None => REGION_TEMP_NONE_DC, }, moisture_q: prof.moisture_q.clamp(0, 100) as u8, vegetation: prof.vegetation_class as u8, glaciation: prof.glaciation_grade as u8, } } /// Scatter a computed row of [`WindowCell`]s into the six flat output arrays /// at row-major offset `row * side`. #[allow(clippy::too_many_arguments)] fn scatter_row( row_cells: &[WindowCell], row: i32, side: i32, morphology: &mut [u8], elev_q: &mut [u8], temp_dc: &mut [i16], moisture_q: &mut [u8], vegetation: &mut [u8], glaciation: &mut [u8], ) { let base = (row * side) as usize; for (col, cell) in row_cells.iter().enumerate() { let i = base + col; morphology[i] = cell.morphology; elev_q[i] = cell.elev_q; temp_dc[i] = cell.temp_dc; moisture_q[i] = cell.moisture_q; vegetation[i] = cell.vegetation; glaciation[i] = cell.glaciation; } } /// Resolve `center` (a district-grid position) to its world-metre origin — /// shared by both window builders so the district->metres convention can /// never drift between them. Mirrors `derive_district`'s own quantization /// (`district_profile.rs`) exactly: `dm = DISTRICT_M`, `(dx*dm, dy*dm)`. fn center_to_world_m(center: DistrictPos) -> (f64, f64) { let dm = DISTRICT_M as f64; (center.0 as f64 * dm, center.1 as f64 * dm) } /// Peak Stage-B course amplitude never exceeds this fraction of an edge's /// chord (mirrors `river_course::STAGE_B_PEAK_FRACTION_OF_CHORD` — kept as an /// independent constant here, not a re-export, so the culling inflation and /// the actual amplitude cap can never silently decouple through a shared /// mutable import path; a `const _: () = assert!(...)` below pins the two /// values equal). Used to inflate an edge's chord bounding box before the /// window-intersection cull (Ruling 3h: "amplitude-inflated chord bbox"). const COURSE_BBOX_INFLATION_FRACTION: f64 = 0.08; const _: () = assert!( (COURSE_BBOX_INFLATION_FRACTION * 1_000_000.0) as i64 == (crate::atlas::river_course::STAGE_B_PEAK_FRACTION_OF_CHORD * 1_000_000.0) as i64 ); /// The window's world-metre rect, `(x0, y0, x1, y1)` — the SAME convention /// [`derive_window_cell`] uses to place cells: `step = granularity.spacing_m()`, /// `[center_world_m - half*step, center_world_m + (side-half)*step)` on each /// axis. Shared by [`invent_courses_near_window`] and [`crop_courses_for_wire`] /// so the rect can never drift between the two. fn window_world_rect( center_world_m: (f64, f64), half_cells: i32, side: i32, step_m: f64, ) -> (f64, f64, f64, f64) { ( center_world_m.0 - half_cells as f64 * step_m, center_world_m.1 - half_cells as f64 * step_m, center_world_m.0 + (side - half_cells) as f64 * step_m, center_world_m.1 + (side - half_cells) as f64 * step_m, ) } /// Invent every river course whose amplitude-inflated chord bounding box /// intersects this window (T-1170 A2, Ruling 1b/3h/4b) — the FULL-precision /// [`InventedCourse`] list, NOT yet cropped to the window or converted to the /// wire [`RiverCourse`] shape. This is the single source both consumers read /// from: [`derive_window_cell`]'s per-cell riparian test (T-1168, Ruling 4b: /// "in the window path, T-1170's already-invented courses") and /// [`crop_courses_for_wire`]'s wire packing — computed ONCE per window, /// before the per-cell derive loop, rather than twice or per-cell. /// /// Pure function of `(seed, body, river_network, window rect, granularity, /// min_wavelength_m)` — independent of whether the caller derives cells /// serially or in parallel, which is why both [`build_district_window_layer`] /// and its `#[cfg(test)]` serial twin call this SAME function. /// /// Region granularity draws courses via the whole-body skeleton path (Ruling /// 5a — the rung-truncated course degenerates to the straight chord at /// Region spacing, so the skeleton dots/chords ARE the course there). No /// windowed course invention at Region — an empty result here is correct, /// not a gap. #[allow(clippy::too_many_arguments)] fn invent_courses_near_window( seed: SeedChain, params: &crate::atlas::district_profile::BodyParams, ta: &crate::atlas::features::TerrainAnalysis, river_network: &RiverNetwork, window_rect: (f64, f64, f64, f64), granularity: WindowGranularity, min_wavelength_m: f64, ) -> Vec { if granularity == WindowGranularity::Region { return Vec::new(); } let step_m = granularity.spacing_m(); let (win_x0, win_y0, win_x1, win_y1) = window_rect; let edges = river_course::build_edges(river_network); let mut courses = Vec::new(); for edge in &edges { let anchor_a = crate::atlas::district_profile::pixel_to_world_m( edge.upstream.1 as f64, edge.upstream.0 as f64, ta.w, ta.h, params.body_radius_km, ); let anchor_b = crate::atlas::district_profile::pixel_to_world_m( edge.downstream.1 as f64, edge.downstream.0 as f64, ta.w, ta.h, params.body_radius_km, ); let chord_m = ((anchor_a.0 - anchor_b.0).powi(2) + (anchor_a.1 - anchor_b.1).powi(2)).sqrt(); let inflate_m = chord_m * COURSE_BBOX_INFLATION_FRACTION; let (bx0, bx1) = ( anchor_a.0.min(anchor_b.0) - inflate_m, anchor_a.0.max(anchor_b.0) + inflate_m, ); let (by0, by1) = ( anchor_a.1.min(anchor_b.1) - inflate_m, anchor_a.1.max(anchor_b.1) + inflate_m, ); // Bbox-vs-window intersection cull — most edges cull to zero for any // given window (Ruling 4b's "most cells cull to zero edges" applies // symmetrically here: most EDGES cull out of any one window). if bx1 < win_x0 || bx0 > win_x1 || by1 < win_y0 || by0 > win_y1 { continue; } courses.push(river_course::invent_course( seed, edge, ta, params, step_m, min_wavelength_m, )); } courses } /// Crop the window's already-invented courses ([`invent_courses_near_window`]) /// to the wire [`RiverCourse`] shape (Ruling 3h) — window rect + one station /// beyond each edge, terminus resolution (A3, Ruling 3e/3f). /// /// `station_spacing_m` is the rung's own cell spacing (`granularity.spacing_m()` /// — District 2,048 m / Quarter 512 m) — threaded to [`resolve_mouth_terminus`]'s /// land-at-final-anchor probe, which extends "one cell length" (Ruling 3e's own /// words), not one Stage-B segment length (Tyre, PR #197 review issue 1). #[allow(clippy::too_many_arguments)] fn crop_courses_for_wire( invented: &[InventedCourse], window_rect: (f64, f64, f64, f64), seed: SeedChain, body_id: &str, params: &crate::atlas::district_profile::BodyParams, ta: &crate::atlas::features::TerrainAnalysis, climate: &crate::atlas::district_profile::ClimateConstants, min_wavelength_m: f64, station_spacing_m: f64, ) -> Vec { invented .iter() .filter_map(|course| { crop_course_to_window( course, window_rect, seed, body_id, params, ta, climate, min_wavelength_m, station_spacing_m, ) }) .collect() } /// Crop an [`InventedCourse`]'s full-edge point list to `window_rect` (+ one /// station beyond each edge, Ruling 3h) and resolve its wire [`CourseTerminus`] /// (A3, Ruling 3e/3f). Returns `None` when the course has zero points inside /// (or adjacent to) the window — the caller's cull is a cheap bbox pre-filter, /// this is the exact per-point check. #[allow(clippy::too_many_arguments)] fn crop_course_to_window( course: &InventedCourse, window_rect: (f64, f64, f64, f64), seed: SeedChain, body_id: &str, params: &crate::atlas::district_profile::BodyParams, ta: &crate::atlas::features::TerrainAnalysis, climate: &crate::atlas::district_profile::ClimateConstants, min_wavelength_m: f64, station_spacing_m: f64, ) -> Option { let (x0, y0, x1, y1) = window_rect; let inside = |p: &(f64, f64)| p.0 >= x0 && p.0 <= x1 && p.1 >= y0 && p.1 <= y1; let n = course.points.len(); let mut first_in: Option = None; let mut last_in: Option = None; for (i, p) in course.points.iter().enumerate() { if inside(p) { first_in.get_or_insert(i); last_in = Some(i); } } let (first_in, last_in) = match (first_in, last_in) { (Some(a), Some(b)) => (a, b), _ => return None, // no point of this course falls inside the window }; // Crop range: one station beyond each edge (Ruling 3h), clamped to the // course's own point range. let lo = first_in.saturating_sub(1); let hi = (last_in + 1).min(n.saturating_sub(1)); let points: Vec<(i32, i32)> = course.points[lo..=hi] .iter() .map(|p| (p.0.round() as i32, p.1.round() as i32)) .collect(); // Terminus resolution (A3, Ruling 3e/3f): only meaningful if the // course's TRUE downstream end (the last point of the full, uncropped // course) is within this cropped range — otherwise the real terminus // lies in a different window and this one just sees a mid-course // passthrough. let true_end_included = hi == n.saturating_sub(1); let terminus = if !true_end_included { CourseTerminus::ContinuesBeyondWindow } else { match course.terminus { EdgeTerminusKind::EdgeDrain => CourseTerminus::EdgeDrain, EdgeTerminusKind::Interior => CourseTerminus::ContinuesBeyondWindow, EdgeTerminusKind::Mouth => { match resolve_mouth_terminus( course, seed, body_id, params, ta, climate, min_wavelength_m, station_spacing_m, ) { Some(mouth_point) => { // Replace the cropped course's tail with the resolved // mouth point (bisected against the last land // station) so the wire polyline ends exactly at the // invented-coast crossing, not at the raw upstream // anchor placeholder `build_edges` recorded. let mut pts = points; if let Some(last) = pts.last_mut() { *last = (mouth_point.0.round() as i32, mouth_point.1.round() as i32); } return Some(RiverCourse { edge_id: course.edge_id, class: course.class, points: pts, terminus: CourseTerminus::Mouth, }); } None => CourseTerminus::None, // degenerate: never found water (Ruling 3e land-at-anchor case) } } } }; Some(RiverCourse { edge_id: course.edge_id, class: course.class, points, terminus, }) } /// Number of bisection iterations for the mouth-terminus search (Ruling 3e, /// binding: "fixed 6 iterations"). const MOUTH_BISECT_ITERATIONS: u32 = 6; /// Walk a `Mouth`-terminus course's stations upstream→downstream, sampling /// the SAME rung-consistent morphology water verdict the window's own cells /// use (`derive_at_metres(...).morphology_zone` — Ruling 3e, binding: "never /// raw `ocean_frac`"). First water station found → bisect against the /// previous land station (fixed [`MOUTH_BISECT_ITERATIONS`]) → the resolved /// terminus point. If no station (including one D8-direction cell-length /// probe past the final anchor) samples water, returns `None` — the /// degenerate "drawn coast receded past this edge" case (Ruling 3e), which /// the caller renders with no mouth flag. /// /// `station_spacing_m` is the rung's own cell spacing — the probe extends /// exactly "one cell length" past the final anchor (Ruling 3e's own words), /// in the direction of the final Stage-B segment, but scaled to /// `station_spacing_m` rather than that segment's own (possibly much /// shorter, near-zero at a taper-to-zero anchor) length. fn resolve_mouth_terminus( course: &InventedCourse, seed: SeedChain, body_id: &str, params: &crate::atlas::district_profile::BodyParams, ta: &crate::atlas::features::TerrainAnalysis, climate: &crate::atlas::district_profile::ClimateConstants, min_wavelength_m: f64, station_spacing_m: f64, ) -> Option<(f64, f64)> { let is_water = |p: (f64, f64)| -> bool { // `&[]`: the mouth-termination water-verdict probe has no use for // the riparian signal (it only reads `morphology_zone`, never // `vegetation_class`) — an empty course slice is a correct, cheap // no-op here (T-1168's `nearby_courses` param never affects // morphology, only vegetation, so this can never mis-terminate). let prof = crate::atlas::district_profile::derive_at_metres( seed, body_id, params, ta, p.0, p.1, climate, min_wavelength_m, &[], ); matches!( prof.morphology_zone, crate::simulation::generator::MorphologyZone::OpenOcean | crate::simulation::generator::MorphologyZone::Lake ) }; let pts = &course.points; if pts.is_empty() { return None; } // Walk upstream -> downstream (points are already stored in that order). let mut prev_land = pts[0]; for &p in pts.iter() { if is_water(p) { return Some(bisect_to_waterline(prev_land, p, is_water)); } prev_land = p; } // Final anchor still land: extend ONE CELL LENGTH (`station_spacing_m` — // Ruling 3e's own words, "up to one cell length probing", not one // Stage-B segment length, which can be much shorter near a // taper-to-zero anchor — Tyre, PR #197 review issue 1) along the final // segment's own direction, as a single probe. if pts.len() >= 2 { let a = pts[pts.len() - 2]; let b = pts[pts.len() - 1]; let (dx, dy) = (b.0 - a.0, b.1 - a.1); let len = (dx * dx + dy * dy).sqrt(); if len > 1e-6 { let (ux, uy) = (dx / len, dy / len); // unit direction of the final segment let probe = (b.0 + ux * station_spacing_m, b.1 + uy * station_spacing_m); if is_water(probe) { return Some(bisect_to_waterline(b, probe, is_water)); } } } None // degenerate: still land — terminate with no mouth flag (caller's job) } /// Bisect between a known-land point and a known-water point for /// [`MOUTH_BISECT_ITERATIONS`] iterations, returning the point closest to the /// water side of the crossing. fn bisect_to_waterline( land: (f64, f64), water: (f64, f64), is_water: impl Fn((f64, f64)) -> bool, ) -> (f64, f64) { let mut lo = land; // land let mut hi = water; // water for _ in 0..MOUTH_BISECT_ITERATIONS { let mid = ((lo.0 + hi.0) * 0.5, (lo.1 + hi.1) * 0.5); if is_water(mid) { hi = mid; } else { lo = mid; } } hi } /// Build a [`DistrictWindowLayer`] by deriving every cell in the window /// around `center` (T-1137, extended T-1150). Mirrors /// `aliveness_probe::render_window_panels`'s derive loop exactly (the probe /// this design promotes to a served layer, D-226 T-1124 amendment §2) — same /// row-major indexing, same per-cell derive call. /// /// `n` MUST already be clamped by the caller ([`clamp_window_n`], /// `[1, DISTRICT_WINDOW_MAX_N]` AND the granularity-aware `WIRE_CAP_CELLS` /// ceiling) — this function trusts it verbatim (the clamp is /// `handle_atlas_request`'s job, applied once at the wire boundary, not /// re-checked on every internal caller per the existing codebase convention /// of clamping at the edge). /// /// `n` is always the window extent in DISTRICTS (design doc §2 Option B: "the /// window's `n` stays the DISTRICT extent"). The derived cell-grid side /// length is [`WindowGranularity::cell_grid_side`] — `n × 4` at `Quarter`, /// `n` at `District`, `round(n / 100)` at `Region` (T-1152, D-243's /// `DISTRICTS_PER_REGION = 100`; see that method's doc for the full /// finer-multiplies/coarser-divides rationale). Full reclassification at the /// rung's own spacing (`derive_at_metres`/`derive_orbital_at_metres` per /// [`derive_window_cell`]'s dispatch), never a coarser-cell interpolation. /// /// **Row-chunked `par_iter` (T-1151):** each cell is a pure function of its /// own position (D-227), so rows can derive in parallel with no shared /// mutable state. Chunking by ROW (not per-cell) amortizes Rayon's own /// task-dispatch overhead against the ~1.2 µs/cell derive cost (design doc /// §7: naive per-cell parallelization risks the dispatch overhead itself /// costing more than the work) — one Rayon task per row means `side` tasks of /// `side` cells each, not `side²` tasks of one cell each. This applies /// unchanged to the `Region` rung (T-1152 step 4: "progressive capped-density /// tiling on the SAME carrier ... No new message shape") — the SAME row-chunked /// parallel loop, cache, and coalescing machinery serve every rung. /// [`build_district_window_layer_serial`] is kept alongside this as the /// golden-comparison baseline (T-1151 acceptance: bit-identical serial vs. /// parallel output, exact row-major array ordering preserved either way). #[allow(clippy::too_many_arguments)] pub fn build_district_window_layer( seed: SeedChain, body_id: &str, params: &crate::atlas::district_profile::BodyParams, ta: &crate::atlas::features::TerrainAnalysis, river_network: &RiverNetwork, center: DistrictPos, n: u32, climate: &crate::atlas::district_profile::ClimateConstants, granularity: WindowGranularity, min_wl_m: u32, ) -> DistrictWindowLayer { use rayon::prelude::*; let side = granularity.cell_grid_side(n); let half = side / 2; let min_wavelength_m = min_wl_m as f64; let center_world_m = center_to_world_m(center); let cells = (side * side) as usize; let mut morphology = vec![0u8; cells]; let mut elev_q = vec![0u8; cells]; let mut temp_dc = vec![REGION_TEMP_NONE_DC; cells]; let mut moisture_q = vec![0u8; cells]; let mut vegetation = vec![0u8; cells]; let mut glaciation = vec![0u8; cells]; // T-1170 A2/T-1168 A5: invent this window's river courses ONCE, before // the per-cell derive loop — this is the single source both the per-cell // riparian test (T-1168, threaded into `derive_window_cell` below) and // the wire course packing (crop step, after the loop) read from. Doing // this first (not per-cell, not twice) is what keeps the window-cost // delta close to the Discipline item 2 ~5% budget. let step_m = granularity.spacing_m(); let window_rect = window_world_rect(center_world_m, half, side, step_m); let invented_courses = invent_courses_near_window( seed, params, ta, river_network, window_rect, granularity, min_wavelength_m, ); // One Rayon task per row: derive_window_cell(row, ..) for every col, then // scatter that row's results into the flat arrays. Row order in the // output collection is preserved by `par_iter` (it yields in index // order), so the scatter below reproduces the exact row-major layout the // serial loop produces. let rows: Vec> = (0..side) .into_par_iter() .map(|row| { (0..side) .map(|col| { derive_window_cell( seed, body_id, params, ta, climate, center_world_m, half, granularity, min_wavelength_m, row, col, &invented_courses, ) }) .collect() }) .collect(); for (row, row_cells) in rows.into_iter().enumerate() { scatter_row( &row_cells, row as i32, side, &mut morphology, &mut elev_q, &mut temp_dc, &mut moisture_q, &mut vegetation, &mut glaciation, ); } let courses = crop_courses_for_wire( &invented_courses, window_rect, seed, body_id, params, ta, climate, min_wavelength_m, step_m, ); DistrictWindowLayer { center, n, granularity_v2: granularity, min_wl_m, morphology, elev_q, temp_dc, moisture_q, vegetation, glaciation, courses, } } /// Serial twin of [`build_district_window_layer`] (T-1151) — the pre-parallel /// row/col double loop, kept ONLY as the golden-comparison baseline for the /// bit-identical serial-vs-parallel test. Not used by production callers. #[cfg(test)] #[allow(clippy::too_many_arguments)] fn build_district_window_layer_serial( seed: SeedChain, body_id: &str, params: &crate::atlas::district_profile::BodyParams, ta: &crate::atlas::features::TerrainAnalysis, river_network: &RiverNetwork, center: DistrictPos, n: u32, climate: &crate::atlas::district_profile::ClimateConstants, granularity: WindowGranularity, min_wl_m: u32, ) -> DistrictWindowLayer { let side = granularity.cell_grid_side(n); let half = side / 2; let min_wavelength_m = min_wl_m as f64; let center_world_m = center_to_world_m(center); let cells = (side * side) as usize; let mut morphology = vec![0u8; cells]; let mut elev_q = vec![0u8; cells]; let mut temp_dc = vec![REGION_TEMP_NONE_DC; cells]; let mut moisture_q = vec![0u8; cells]; let mut vegetation = vec![0u8; cells]; let mut glaciation = vec![0u8; cells]; let step_m = granularity.spacing_m(); let window_rect = window_world_rect(center_world_m, half, side, step_m); let invented_courses = invent_courses_near_window( seed, params, ta, river_network, window_rect, granularity, min_wavelength_m, ); for row in 0..side { let row_cells: Vec = (0..side) .map(|col| { derive_window_cell( seed, body_id, params, ta, climate, center_world_m, half, granularity, min_wavelength_m, row, col, &invented_courses, ) }) .collect(); scatter_row( &row_cells, row, side, &mut morphology, &mut elev_q, &mut temp_dc, &mut moisture_q, &mut vegetation, &mut glaciation, ); } let courses = crop_courses_for_wire( &invented_courses, window_rect, seed, body_id, params, ta, climate, min_wavelength_m, step_m, ); DistrictWindowLayer { center, n, granularity_v2: granularity, min_wl_m, morphology, elev_q, temp_dc, moisture_q, vegetation, glaciation, courses, } } // --------------------------------------------------------------------------- // QuarterFootprintLayer (D-226 T-1112 amendment, T-1119) // --------------------------------------------------------------------------- /// Per-settlement aggregate over one quarter's 4×4 `BlockSkeleton` grid, for /// the Atlas quarter-footprint overlay (D-226 T-1112 amendment §1). Five /// scalar fields earn their place per the amendment's hard ceiling (§2): no /// per-block zoning/street/tag detail ever reaches the wire, and no /// chunk/tile/voxel data is touched. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct QuarterFootprintEntry { pub city_id: u64, /// Basis-point mean of `BlockSkeleton.density_pct` across the 16 blocks /// (integer division, D-010 — no `f32` on the wire). pub density_avg_pct: u8, /// Mode `DistrictType` across the 16 blocks; ties resolve to the lowest /// declaration-order variant (the `Ord` derive on `DistrictType`, T-994). pub dominant_district_type: DistrictType, /// Mode `ZoningType` across the 16 blocks; same tie rule (the `Ord` /// derive added on `ZoningType` for this ticket, T-1119). pub dominant_zoning: ZoningType, /// Count of blocks with `landmark: Some(_)` across the 16 blocks (max 16). /// Tooltip/sidebar-only per the D-226(d) ceiling — never a map-visible /// channel (§2). pub landmark_count: u8, /// `QuarterSkeleton.corridors.len()`, clamped to `u8`. Tooltip/sidebar-only, /// same ceiling as `landmark_count`. pub corridor_count: u8, } /// The quarter-footprint overlay for one body (D-226 T-1112 amendment §1), /// keyed by `city_id` — a quarter carries no independent spatial position of /// its own (`QuarterId` is a content-addressable hash, not a coordinate), so /// the layer anchors at the existing L3 settlement position client-side and /// this map only needs to answer "does this settlement have quarter data, and /// if so what does it aggregate to". #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct QuarterFootprintLayer { /// `BTreeMap` for D-010 determinism, matching `RegionGridLayer`'s and the /// source `QuarterWorldState.block_tags`' own `BTreeMap` precedent. pub entries: std::collections::BTreeMap, } /// Aggregate one quarter's 16 `BlockSkeleton`s into a [`QuarterFootprintEntry`] /// for `city_id`. fn aggregate_quarter_footprint( city_id: u64, skeleton: &crate::simulation::generator::QuarterSkeleton, ) -> QuarterFootprintEntry { let blocks: Vec<&crate::simulation::generator::BlockSkeleton> = skeleton.blocks.iter().flatten().collect(); let n = blocks.len() as u32; // always 16 (the fixed 4×4 grid) — computed // rather than hardcoded so the mean formula // stays correct if the grid shape ever changes. let density_sum: u32 = blocks.iter().map(|b| b.density_pct as u32).sum(); let density_avg_pct = if n == 0 { 0 } else { (density_sum / n) as u8 }; let dominant_district_type = mode_by_declaration_order(blocks.iter().map(|b| &b.district_type)) .cloned() .unwrap_or_default(); let dominant_zoning = mode_by_declaration_order(blocks.iter().map(|b| &b.zoning)) .cloned() .unwrap_or_default(); let landmark_count = blocks.iter().filter(|b| b.landmark.is_some()).count() as u8; let corridor_count = skeleton.corridors.len().min(u8::MAX as usize) as u8; QuarterFootprintEntry { city_id, density_avg_pct, dominant_district_type, dominant_zoning, landmark_count, corridor_count, } } /// Mode of an `Ord` value over an iterator, tie-broken by lowest declaration /// order (i.e. the `Ord`-smallest value among the tied-for-max-count values). /// `None` for an empty iterator. /// /// **Not** `counts.into_iter().max_by_key(...)`: `Iterator::max_by_key` /// returns the *last* maximum on a tie (documented behaviour), which is the /// opposite of what's needed here. `BTreeMap` iterates keys in ascending /// `Ord` order (= declaration order for these enums), so walking forward and /// only replacing the running best on a *strictly greater* count keeps the /// first-seen — i.e. lowest-declaration-order — winner on every tie. fn mode_by_declaration_order<'a, T: Ord + 'a>( values: impl Iterator, ) -> Option<&'a T> { let mut counts: std::collections::BTreeMap<&'a T, u32> = std::collections::BTreeMap::new(); for v in values { *counts.entry(v).or_insert(0) += 1; } let mut best: Option<(&'a T, u32)> = None; for (v, count) in counts { match best { Some((_, best_count)) if count <= best_count => {} _ => best = Some((v, count)), } } best.map(|(v, _)| v) } /// Build the [`QuarterFootprintLayer`] from a body's cached state (T-1119). /// Returns `None` when the Quarter-skeleton layer has not run for any /// settlement (empty `state.quarters`). /// /// `state.quarters` carries no independent spatial position — the only /// spatial anchor a quarter has is the `city_id` it was generated for /// (D-226 T-1112 amendment §1). So this recomputes the same deterministic /// `QuarterId` derivation the L3→L4 dispatch path uses /// (`SeedChain::for_body(world_seed, body_id).derive(SeedDomain::Layer4Quarter, /// city_id).seed()`, `plugin.rs::build_skeleton_work_item`) for every placed /// settlement and looks it up in `state.quarters`. A placement whose derived /// id isn't found (skeleton generation is async, dispatched at `Low` priority /// after the body's `Ready` snapshot is already cached — `plugin.rs`) is /// skipped, not defaulted: an absent quarter is not a zero-footprint quarter. pub fn build_quarter_footprint_layer( state: &BodyWorldState, world_seed: u64, ) -> Option { if state.quarters.is_empty() { return None; } let body_chain = SeedChain::for_body(world_seed, &state.body_id); let mut entries = std::collections::BTreeMap::new(); for placement in &state.placements { let quarter_id = body_chain .derive(SeedDomain::Layer4Quarter, placement.city_id) .seed(); if let Some(quarter_state) = state.quarters.get(&quarter_id) { entries.insert( placement.city_id, aggregate_quarter_footprint(placement.city_id, &quarter_state.skeleton), ); } } Some(QuarterFootprintLayer { entries }) } // --------------------------------------------------------------------------- // RoadGraphLayer (T-960 §1, T-1038) // --------------------------------------------------------------------------- /// One node in the [`RoadGraphLayer`] overlay — a settlement junction or a /// waypoint. Trimmed from the internal [`crate::atlas::road_graph::RoadNode`]: /// `degree` and `parent_edge` are internal bookkeeping a planetary-map overlay /// doesn't need (degree is trivially re-derivable client-side by counting /// edges per node index if a renderer wants junction highlighting). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoadGraphNode { /// Position in working-heightmap-grid coordinates `(row, col)` — the same /// space as `Layer1Output` attractors/rivers and `SettlementLayer` positions. pub position: (u16, u16), pub kind: RoadNodeKind, /// The settlement's `city_id` (cross-references `SettlementLayer`), or /// `None` for a waypoint. pub city_id: Option, } /// One edge in the [`RoadGraphLayer`] overlay — a routed road or rail segment. /// Trimmed from [`crate::atlas::road_graph::RoadEdge`]: `length_cells` is an /// internal A* routing-grid measure with no meaning outside that grid's scale /// (the polyline `path` is what an overlay actually draws). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoadGraphEdge { /// `nodes` indices of the endpoint settlements (`from < to`). pub from: usize, pub to: usize, /// Routed polyline in working-heightmap-grid coordinates `(row, col)`. pub path: Vec<(u16, u16)>, pub maintenance: MaintenanceAuthority, /// `true` if this edge is a railroad; `false` is a road. pub is_rail: bool, /// Joined `systems.db` named-route id, if any (empty pool today — D-223). pub named_route_id: Option, } /// The inter-settlement road/rail graph, trimmed for the Atlas planetary-map /// overlay (T-960 §1, D-211, T-1038). See [`RoadGraphNode`]/[`RoadGraphEdge`] /// for what was dropped from the internal `RoadGraph`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RoadGraphLayer { pub nodes: Vec, pub edges: Vec, } /// Build the [`RoadGraphLayer`] from a body's cached state (T-960 §1). /// Returns `None` when the RoadGraph layer has not run, which coincides /// exactly with "no settlements placed" (`build_road_graph` returns an empty /// graph for zero placements, and every placement yields at least one node). pub fn build_road_graph_layer(state: &BodyWorldState) -> Option { if state.road_graph.nodes.is_empty() { return None; } let nodes = state .road_graph .nodes .iter() .map(|n| RoadGraphNode { position: n.position, kind: n.kind, city_id: n.city_id, }) .collect(); let edges = state .road_graph .edges .iter() .map(|e| RoadGraphEdge { from: e.from, to: e.to, path: e.path.clone(), maintenance: e.maintenance, is_rail: e.is_rail, named_route_id: e.named_route_id.clone(), }) .collect(); Some(RoadGraphLayer { nodes, edges }) } // --------------------------------------------------------------------------- // SettlementLayer (T-960 §2, #955) // --------------------------------------------------------------------------- /// Coarse settlement size class for the Atlas overlay (T-960 §2), derived from /// raw population using the same Tier A/B population cutoffs the D-211 /// placement pipeline already uses (`attractor_matching::match_cities`): /// Tier A (≥ 1,000,000 or `NameLocked`) settlements are `Major`, Tier B /// (50,000–999,999) are `Standard`, and everything else (Tier C / synthetic /// overflow) is `Minor`. A display bucket, not new simulation truth. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SettlementSizeClass { Major, Standard, Minor, } impl SettlementSizeClass { /// Bucket a raw population using the D-211 Tier A/B cutoffs. pub fn from_population(population: i64) -> Self { if population >= 1_000_000 { SettlementSizeClass::Major } else if population >= 50_000 { SettlementSizeClass::Standard } else { SettlementSizeClass::Minor } } } /// One placed settlement in the [`SettlementLayer`] overlay (T-960 §2). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SettlementEntry { pub city_id: u64, pub name: String, /// Position in working-heightmap-grid coordinates `(row, col)` — the same /// space as `Layer1Output` attractors/rivers (T-960 §2: match the /// coordinate convention layer1 features already use so the client /// transforms identically). pub position: (u16, u16), pub size_class: SettlementSizeClass, /// Authored `atlas_city_names.kind == 'capital'` (not population-derived). pub is_capital: bool, /// Cheap derived flag: `true` if the settlement's anchoring attractor is /// water-adjacent (`CoastalAccess` / `RiverMouth` / `LakeShore`). pub is_port: bool, } /// The settlement-placement overlay for one body (T-960 §2, #955, D-211). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SettlementLayer { pub settlements: Vec, } /// `true` for the water-adjacent attractor types a settlement counts as a /// "port" for cheaply (T-960 §2). No "foothold" flag: unlike `is_port`, there /// is no existing concept in the placement data this could derive from /// without inventing new business logic — left out (see the T-960 report). fn is_port_attractor(at: AttractorType) -> bool { matches!( at, AttractorType::CoastalAccess | AttractorType::RiverMouth | AttractorType::LakeShore ) } /// Build the [`SettlementLayer`] from a body's cached state (T-960 §2). /// Returns `None` when the Settlement layer has not placed any city yet. pub fn build_settlement_layer(state: &BodyWorldState) -> Option { if state.placements.is_empty() { return None; } let settlements = state .placements .iter() .map(|p| SettlementEntry { city_id: p.city_id, name: p.name.clone(), position: p.position, size_class: SettlementSizeClass::from_population(p.population), is_capital: p.is_capital, is_port: is_port_attractor(p.attractor_type), }) .collect(); Some(SettlementLayer { settlements }) } /// Normalize a wire-supplied district-window centre against the body's /// physical geometry (T-1142 — a letterbox-click bug sent `window_center` /// wildly out of a body's valid range; the server accepted it, derived /// clamped garbage inside `derive_district`, and CACHED that garbage under /// the raw un-normalized key). Mirrors `district_profile::derive_district`'s /// own forward mapping (`district_profile.rs:1426-1451`) EXACTLY, so a window /// centre that survives this normalization derives identically to how /// `derive_district` would have resolved it anyway — this function only /// closes the gap between "derive_district silently clamps/wraps internally" /// and "the SERVING path (cache key, coalescing key) saw the raw value". /// /// **Column (longitude) wraps** — `rem_euclid` against the body's /// circumference in districts, mirroring the forward map's /// `(wx / circumference_m).rem_euclid(1.0)` (longitude is periodic; a click /// at column 12276 on a body whose circumference is a few hundred districts /// wide is the SAME point as some small in-range column, not garbage). /// /// **Row (latitude) clamps** — to `±half_meridian_districts`, mirroring the /// forward map's `(wy / meridian_m).clamp(-0.5, 0.5)` (latitude is NOT /// periodic; it terminates at the poles, so out-of-range rows collapse to the /// nearest pole rather than wrapping — same asymmetry `derive_district` /// itself already encodes). /// /// Both bounds are derived from `body_radius_km` via the SAME `DISTRICT_M` /// (2 048 m, D-243) constant the forward map uses — no independent magic /// numbers that could silently drift out of sync with `derive_district`. /// /// **No radius (`body_radius_km` absent/non-positive — tiny test bodies /// only, per `BodyParams`'s own doc: "e.g. tiny test bodies"; every real /// `systems.db` body row carries a radius):** identity, no wrap/clamp. The /// forward map's own no-radius branch has no periodicity concept either (it /// clamps the FRACTIONAL PIXEL position directly against the heightmap's /// working-grid dimensions, which aren't known at request-serving time — only /// inside the Rayon work item once the heightmap is loaded); a real letterbox /// click can never hit this branch, so it is out of this fix's scope. fn normalize_window_center(params: &BodyParams, center: DistrictPos) -> DistrictPos { let (dx, dy) = center; match params.body_radius_km { Some(r_km) if r_km > 0.0 => { let circumference_m = std::f64::consts::TAU * r_km * 1000.0; let meridian_m = std::f64::consts::PI * r_km * 1000.0; // Whole districts per full circumference / per half-meridian — // rounded (not truncated) so the bound matches the forward map's // continuous fraction as closely as an integer district grid can. let districts_per_circumference = (circumference_m / DISTRICT_M as f64).round().max(1.0) as i32; let half_meridian_districts = (meridian_m / DISTRICT_M as f64 / 2.0).round() as i32; let wrapped_dx = dx.rem_euclid(districts_per_circumference); let clamped_dy = dy.clamp(-half_meridian_districts, half_meridian_districts); (wrapped_dx, clamped_dy) } // No radius: derive_district's own fallback has no wrap/clamp concept // at the DistrictPos level (see the doc above) — identity. _ => center, } } /// Resolve `req`'s district-window query, if any (D-226 T-1124 amendment, /// T-1137). Returns `None` immediately when `req.window_center` is absent (no /// window requested — the common case, zero cost). /// /// **Independent of the whole-body cache state** (the amendment is explicit: /// "the window derivation depends only on `TerrainAnalysis` + `BodyParams` /// being resolvable for the body ... not on which whole-body layers the /// cascade has cached") — so this runs whether `handle_atlas_request` is /// about to take its cache-hit or cache-miss branch, sharing neither's control /// flow. /// /// **`window_center` is normalized via [`normalize_window_center`] BEFORE the /// `DistrictWindowCache` key AND the `submit_window` coalescing key are built** /// (T-1142) — this is why `body_params` is read HERE, unconditionally, /// rather than only inside the former miss-branch: normalization needs /// `body_radius_km` to compute the wrap/clamp bounds, and it must happen /// before either key exists, or an insane request and its sane normalized /// twin would land in different cache entries (exactly the bug this fix /// closes — a garbage `window_center` was cached standalone instead of /// collapsing onto its valid twin). The coalescing key itself is /// `(ConnectionId, body_id)` — it never carried `center`, so coalescing /// was never at risk of diverging per-center; normalizing before /// `submit_window` matters only so the work item DERIVES (and echoes) the /// canonical center. The one-time cost /// (a single indexed `bodies` row read) is paid on every window request now, /// not just on a cache miss — a request whose normalized center hits the /// cache still needed this read to know WHICH key to check. /// /// Cache hit (`(body_id, normalized_center, n)` already in `window_cache`) → /// `Some` immediately, no queue submission (D-227: a previously-derived /// window for this body+seed is valid forever, no staleness check needed). /// Cache miss → submit a `DeriveWindow` work item (queue-based, per the /// amendment's binding serving model — never inline here) and return `None`; /// the *next* request for this `(body, normalized_center, n)` re-checks the /// cache and finds it populated once `drain_generation_completions` has /// processed the completion (the existing D-225 poll-and-recheck-cache /// pattern every other layer already uses, not a push). /// /// `window_n` is clamped to `[1, DISTRICT_WINDOW_MAX_N]` (or /// `DISTRICT_WINDOW_MAX_N_REGION` at `Region` granularity, T-1152) AND the /// granularity-aware `WIRE_CAP_CELLS` ceiling here — the ONE place that clamp /// is applied; nothing downstream re-checks the wire value. The granularity /// itself is resolved via [`resolve_window_granularity_v2`] at the same /// boundary (T-1150, widened T-1152 — see that function's doc for the /// legacy-`u32`-vs-`window_granularity_v2` precedence rule). #[allow(clippy::too_many_arguments)] fn serve_district_window( req: &AtlasLayerRequest, window_cache: &mut DistrictWindowCache, queue: &GenerationQueue, resolver: &BodySourceResolver, body_params_reader: Option<&BodyParamsReader>, world_seed: u64, conn_id: ConnectionId, ) -> Option { let raw_center = req.window_center?; let granularity = resolve_window_granularity_v2(req); let n = clamp_window_n_v2(req.window_n, granularity); // T-1150 design doc §5: quantize BEFORE either the cache key or the // DeriveWindow work item sees it — the raw wire value never reaches // either (same discipline as window_n's clamp above and // normalize_window_center's wrap/clamp below). let min_wl_m = quantize_min_wl_m(req.window_min_wl_m); // body_params is needed to normalize the centre BEFORE either cache key // exists (T-1142) — read it first, unconditionally (not gated on a cache // miss like the former structure). A read failure here can't distinguish // "insane vs. sane center" for the key, so it's a hard skip for the whole // window (window stays None on this response), same failure posture the // former miss-only read already had. let Some(reader) = body_params_reader else { tracing::warn!( body_id = %req.body_id, "district window request: no body_params_reader wired — window stays None" ); return None; }; let body_params = match reader.read_body_params(&req.body_id) { Ok(p) => p, Err(e) => { tracing::warn!( body_id = %req.body_id, error = %e, "district window request: body_params read failed — window stays None" ); return None; } }; let center = normalize_window_center(&body_params, raw_center); if center != raw_center { tracing::debug!( body_id = %req.body_id, raw = ?raw_center, normalized = ?center, "district window request: out-of-range window_center normalized (T-1142)" ); } // T-1150: granularity + min_wl_m are part of the cache key — a // granularity-4 request at the same (body, center, n) as a granularity-1 // request is a DIFFERENT payload and must never alias onto the same slot // (design doc §3's aliasing risk, the mandatory regression test below). let key: DistrictWindowKey = (req.body_id.clone(), center, n, granularity, min_wl_m); if let Some(layer) = window_cache.get(&key) { return Some(layer.clone()); } // Miss — resolve the heightmap and submit a background derive. // Read/resolve failures are non-fatal for the window (log + skip): the // window simply stays None on this response, same as an unrun whole-body // layer, rather than failing the entire AtlasLayerResponse. let heightmap_path = match resolver.resolve(&req.body_id) { Ok(p) => p, Err(e) => { tracing::warn!( body_id = %req.body_id, error = %e, "district window request: heightmap resolve failed — window stays None" ); return None; } }; queue.submit_window( GenWorkItem::DeriveWindow { body_id: req.body_id.clone(), conn_id, heightmap_path, sea_level: DEFAULT_SEA_LEVEL, body_seed: SeedChain::for_body(world_seed, &req.body_id), body_params: Box::new(body_params), center, n, granularity, min_wl_m, }, GenPriority::Immediate, ); None } /// Serve one layer request (D-225). `current_tick` stamps the cache LRU on hit; /// `world_seed` derives the body's `SeedChain` for the enqueued analysis. /// /// `city_reader` supplies the body's settlements for Layer-3 placement (#955), /// read on a cache miss. `None` (or a read failure) places no cities — the /// cascade still runs Layer 1; the body just gets no settlement placements. /// /// `body_params_reader` supplies the body's physical parameters for the /// DistrictProfile carrier layer (T-1032, D-239 §1), read on a cache miss. /// `None` (or a read failure) passes `body_params: None` to the work item, /// causing the cascade to stop at `CascadeLayer::Settlement` (pre-T-1032 /// behaviour). A successful read passes `Some(Box::new(params))`, enabling /// the full `CascadeLayer::DistrictProfile` path. /// /// `window_cache` + `conn_id` serve the optional district-window query /// (D-226 T-1124 amendment, T-1137) via [`serve_district_window`] — see that /// function for the caching/coalescing model. `conn_id` is used ONLY as the /// window request's coalescing key; nothing else in this function is /// connection-aware (the D-254 §2 convention this proxy already follows). #[allow(clippy::too_many_arguments)] pub fn handle_atlas_request( req: &AtlasLayerRequest, cache: &mut BodyWorldStateCache, window_cache: &mut DistrictWindowCache, queue: &GenerationQueue, resolver: &BodySourceResolver, city_reader: Option<&CityContextReader>, body_params_reader: Option<&BodyParamsReader>, world_seed: u64, current_tick: SimTick, conn_id: ConnectionId, ) -> AtlasLayerResponse { let district_window = serve_district_window( req, window_cache, queue, resolver, body_params_reader, world_seed, conn_id, ); // Cache hit — serve immediately. if let Some(state) = cache.get(&req.body_id, current_tick) { let layer1 = Layer1Output { body_id: state.body_id.clone(), river_network: state.river_network.clone(), drainage_basins: state.drainage_basins.clone(), attractors: state.attractors.clone(), // The cascade ran on the downsampled heightmap, so its dims are the // working grid all Layer-1 positions are expressed in (#960). grid_w: state.heightmap_width, grid_h: state.heightmap_height, // survey_basin_dirs is transient — it is aggregated during run_layer1 // and consumed by derive_all_districts before being stored on // BodyWorldState. When reconstructing Layer1Output from the cache for // the client response, the per-survey-cell direction is already encoded // in DistrictProfile.basin_direction (BodyWorldState.districts) and is // not needed again here. Supply an empty map. survey_basin_dirs: std::collections::BTreeMap::new(), // T-1169: mirrors state.attractors.clone() above — feature_names // is stored on BodyWorldState (see cascade::into_body_world_state) // and cloned back out here, same reconstruction discipline as // every other Layer1Output field on this cache-hit path. feature_names: state.feature_names.clone(), }; let district_grid = build_district_grid(state); let road_graph = build_road_graph_layer(state); let settlements = build_settlement_layer(state); let region_grid = build_region_grid(state); let quarter_footprints = build_quarter_footprint_layer(state, world_seed); return AtlasLayerResponse { body_id: req.body_id.clone(), status: AtlasLayerStatus::Ready, layer1: Some(layer1), district_grid, road_graph, settlements, region_grid, district_window, quarter_footprints, }; } // Miss — resolve the source heightmap and enqueue background analysis. match resolver.resolve(&req.body_id) { Ok(heightmap_path) => { // Pre-resolve this body's settlements + system faction so the Rayon // work item stays DB-free (#955/#956, D-225). Read failures are // non-fatal: log and fall back (no cities / no faction → frontier). let (cities, dominant_faction) = match city_reader { Some(reader) => { let cities = reader .read_body_settlements(&req.body_id) .unwrap_or_else(|e| { tracing::warn!( body_id = %req.body_id, error = %e, "settlement read failed; placing no cities" ); Vec::new() }); let faction = reader .read_body_dominant_faction(&req.body_id) .unwrap_or_else(|e| { tracing::warn!( body_id = %req.body_id, error = %e, "dominant_faction read failed; defaulting to frontier" ); None }); (cities, faction) } None => (Vec::new(), None), }; // Pre-resolve this body's reserved river/mountain name pools so // the Rayon work item stays DB-free (T-1169, D-223, same D-225 // pattern as `cities` above). Read failure is non-fatal: log and // fall back to no names (attach_feature_names degrades gracefully // — every attractor simply gets no name). let (river_names, mountain_names) = match city_reader { Some(reader) => match reader.read_body_feature_names(&req.body_id) { Ok(rows) => { let mut rivers = Vec::new(); let mut mountains = Vec::new(); for row in rows { match row.feature_type.as_str() { "river" => rivers.push(row.name), "mountain" => mountains.push(row.name), _ => {} } } (rivers, mountains) } Err(e) => { tracing::warn!( body_id = %req.body_id, error = %e, "feature name read failed; attaching no names" ); (Vec::new(), Vec::new()) } }, None => (Vec::new(), Vec::new()), }; // Pre-resolve body physical params so the Rayon work item stays // DB-free (D-225 pattern). Read failures are non-fatal: log and // fall back to None (cascade stops at Settlement, pre-T-1032 // behaviour, rather than aborting the entire analysis). let body_params = match body_params_reader { Some(reader) => reader .read_body_params(&req.body_id) .map(|p| Some(Box::new(p))) .unwrap_or_else(|e| { tracing::warn!( body_id = %req.body_id, error = %e, "body_params read failed; district layer skipped" ); None }), None => None, }; queue.submit( GenWorkItem::AnalyzeBody { body_id: req.body_id.clone(), heightmap_path, sea_level: DEFAULT_SEA_LEVEL, body_seed: SeedChain::for_body(world_seed, &req.body_id), cities, dominant_faction, body_params, river_names, mountain_names, }, GenPriority::Immediate, ); AtlasLayerResponse { body_id: req.body_id.clone(), status: AtlasLayerStatus::Pending, layer1: None, district_grid: None, road_graph: None, settlements: None, region_grid: None, district_window, quarter_footprints: None, } } // Unknown / no terrain → re-requesting won't help. Err(SourceResolveError::UnknownBody(_)) | Err(SourceResolveError::NoTerrainReference { .. }) => AtlasLayerResponse { body_id: req.body_id.clone(), status: AtlasLayerStatus::NotFound, layer1: None, district_grid: None, road_graph: None, settlements: None, region_grid: None, district_window, quarter_footprints: None, }, Err(e) => AtlasLayerResponse { body_id: req.body_id.clone(), status: AtlasLayerStatus::Error(e.to_string()), layer1: None, district_grid: None, road_graph: None, settlements: None, region_grid: None, district_window, quarter_footprints: None, }, } } #[cfg(test)] mod tests { use super::*; use crate::atlas::body_world_state::{BodyWorldState, RiverNetwork, CACHE_CAPACITY}; use crate::atlas::gen_queue::GenCompletion; use rusqlite::Connection; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicU32, Ordering}; use std::time::Duration; static SEQ: AtomicU32 = AtomicU32::new(0); #[test] fn district_grid_built_from_cached_districts() { use crate::atlas::district_profile::{ DistrictProfile, GlaciationGrade, PrecipitationClass, TectonicClass, VegetationClass, }; use crate::simulation::generator::MorphologyZone; let dp = |zone: MorphologyZone, elev: i32| DistrictProfile { morphology_zone: zone, tectonic_class: TectonicClass::Stable, glaciation_grade: GlaciationGrade::None, precipitation_class: PrecipitationClass::Arid, slope_q: 0, elev_q: elev, ocean_fraction_q: 0, lake_margin_q: 0, river_threshold: 200, temperature_c: Some(10.0), moisture_q: 50, vegetation_class: VegetationClass::Barren, basin_direction: crate::atlas::scale::BasinDirection::default(), }; let mut state = BodyWorldState { body_id: "GJ1c".into(), heightmap: vec![], heightmap_width: 16, heightmap_height: 8, sea_level: 0.3, river_network: RiverNetwork::default(), drainage_basins: vec![], attractors: vec![], feature_names: vec![], placements: vec![], road_graph: crate::atlas::road_graph::RoadGraph::default(), quarters: std::collections::BTreeMap::new(), districts: std::collections::BTreeMap::new(), regions: std::collections::BTreeMap::new(), last_accessed: 0, }; // 3×2 grid with two distinct zones at the corners. state.districts.insert( crate::atlas::scale::SurveyCellPos(0, 0), dp(MorphologyZone::AlluvialPlain, 10), ); state.districts.insert( crate::atlas::scale::SurveyCellPos(2, 1), dp(MorphologyZone::Alpine, 90), ); let grid = build_district_grid(&state).expect("districts present → Some grid"); assert_eq!((grid.cols, grid.rows), (3, 2)); assert_eq!(grid.morphology.len(), 6); assert_eq!(grid.morphology[0], MorphologyZone::AlluvialPlain as u8); assert_eq!( grid.morphology[grid.cols as usize + 2], MorphologyZone::Alpine as u8 ); assert_eq!(grid.elev_q[grid.cols as usize + 2], 90); // Empty districts → None (DistrictProfile layer hasn't run). state.districts.clear(); assert!(build_district_grid(&state).is_none()); } /// T-1113: the region climate grid mirrors the district-grid encoding — /// `None` when the Region layer hasn't run; dense row-major with the /// integer wire quantization (deci-°C temp, `i16::MIN` airless sentinel) /// when it has. #[test] fn build_region_grid_encodes_dense_quantized_climate() { use crate::atlas::region_profile::{RegionClock, RegionProfile, SeasonPhase, WeatherState}; let mut state = blank_state("GJ1c"); // Empty regions → None (the Region layer hasn't run). assert!(build_region_grid(&state).is_none()); // A 2×1 covering grid: one temperate region, one airless-style region // (mean_temp_c = None → the sentinel). state.regions.insert( (0, 0), RegionProfile { pos: (0, 0), clock: RegionClock { season: SeasonPhase::Summer, weather: WeatherState::Clear, mean_temp_c: Some(12.34), }, latitude_deg: 45.0, moisture_q: 80, }, ); state.regions.insert( (1, 0), RegionProfile { pos: (1, 0), clock: RegionClock { season: SeasonPhase::Winter, weather: WeatherState::Snow, mean_temp_c: None, }, latitude_deg: -10.0, moisture_q: 5, }, ); let grid = build_region_grid(&state).expect("regions present → Some grid"); assert_eq!((grid.cols, grid.rows), (2, 1)); assert_eq!(grid.season.len(), 2); assert_eq!(grid.season[0], SeasonPhase::Summer as u8); assert_eq!(grid.weather[0], WeatherState::Clear as u8); // 12.34 °C → 123 deci-°C (rounded). assert_eq!(grid.mean_temp_dc[0], 123); assert_eq!(grid.moisture_q[0], 80); assert_eq!(grid.season[1], SeasonPhase::Winter as u8); assert_eq!(grid.weather[1], WeatherState::Snow as u8); assert_eq!( grid.mean_temp_dc[1], REGION_TEMP_NONE_DC, "airless None maps to the sentinel" ); assert_eq!(grid.moisture_q[1], 5); } // ----------------------------------------------------------------------- // DistrictWindowLayer (D-226 T-1124 amendment, T-1137) // ----------------------------------------------------------------------- /// Minimal deterministic heightmap fixture, mirroring /// `district_profile::tests::test_hm` (T-1137: the window path shares the /// same on-demand `derive_district` call, so it earns the same fixture /// shape). fn window_test_hm() -> crate::atlas::heightmap::BodyHeightmap { use crate::atlas::heightmap::BodyHeightmap; let (w, h) = (64u32, 32u32); let n = (w * h) as usize; let data = (0..n) .map(|i| { let r = (i / w as usize) as f32 / h as f32; let c = (i % w as usize) as f32 / w as f32; (r * 0.6 + c * 0.4).min(1.0) }) .collect(); BodyHeightmap { body_id: "test".into(), width: w, height: h, data, sea_level: 0.3, } } fn window_test_ta( hm: &crate::atlas::heightmap::BodyHeightmap, ) -> crate::atlas::features::TerrainAnalysis { use crate::atlas::drainage; use crate::atlas::features::TerrainAnalysis; let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level); TerrainAnalysis::analyze(hm, &dr) } /// T-1170: the `RiverNetwork` companion to [`window_test_ta`] — most /// existing window-builder tests don't care about courses at all (this /// synthetic gradient fixture may have zero river cells), so an empty /// default is the common case; call sites that DO care about courses use /// a real fixture (`window_test_gj1c_network`) instead. fn window_test_river_network( hm: &crate::atlas::heightmap::BodyHeightmap, ) -> crate::atlas::body_world_state::RiverNetwork { use crate::atlas::drainage; drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level).river_network } fn window_test_params() -> crate::atlas::district_profile::BodyParams { crate::atlas::district_profile::BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() } } /// `build_district_window_layer` produces a dense `n × n` row-major grid /// (the `DistrictGridLayer`/`RegionGridLayer` indexing convention) whose /// cell count and per-array lengths match `n`, and whose values are /// pulled straight from the corresponding `derive_district` profile field /// (T-1137). #[test] fn build_district_window_layer_produces_dense_n_by_n_grid() { let hm = window_test_hm(); let ta = window_test_ta(&hm); let rn = window_test_river_network(&hm); let params = window_test_params(); let climate = crate::atlas::district_profile::ClimateConstants::default(); let seed = SeedChain::root(42).derive(SeedDomain::Body, 1); let n = 4u32; let layer = build_district_window_layer( seed, "test_body", ¶ms, &ta, &rn, (10, -5), n, &climate, WindowGranularity::District, 0, ); assert_eq!(layer.center, (10, -5)); assert_eq!(layer.n, n); assert_eq!(layer.granularity_v2, WindowGranularity::District); assert_eq!(layer.min_wl_m, 0); let cells = (n * n) as usize; assert_eq!(layer.morphology.len(), cells); assert_eq!(layer.elev_q.len(), cells); assert_eq!(layer.temp_dc.len(), cells); assert_eq!(layer.moisture_q.len(), cells); assert_eq!(layer.vegetation.len(), cells); assert_eq!(layer.glaciation.len(), cells); // Spot-check one cell against a direct derive_district call — the // window builder must not transform the profile's values, only pack // them (row 0, col 0 → district (center.0 - n/2, center.1 - n/2)). let half = (n / 2) as i32; let dp = (10 - half, -5 - half); let prof = crate::atlas::district_profile::derive_district( seed, "test_body", ¶ms, &ta, dp, &climate, ); assert_eq!(layer.morphology[0], prof.morphology_zone as u8); assert_eq!(layer.elev_q[0], prof.elev_q.clamp(0, 100) as u8); assert_eq!(layer.moisture_q[0], prof.moisture_q.clamp(0, 100) as u8); assert_eq!(layer.vegetation[0], prof.vegetation_class as u8); assert_eq!(layer.glaciation[0], prof.glaciation_grade as u8); } /// T-1170/T-1168 A5 integration, strengthened by D-256: the batch path /// (`derive_district_profile`, sourcing courses via `near_perennial_water_at` /// on demand) and the window path (`build_district_window_layer`, /// sourcing courses via the pre-invented `Vec`) must /// resolve the SAME riparian verdict for the SAME world position — /// Ruling 4b's "batch and window paths can never silently disagree" /// binding requirement, checked end to end (not just at the /// `near_perennial_water`/`near_perennial_water_at` unit level). /// /// **Design note (D-256):** pre-D-256 this test deliberately did NOT /// compare the batch and window paths' full `DistrictProfile` output for /// "the same district" — `derive_district_profile`'s cell-aggregate-centre /// sampling and the window path's district-origin sampling resolved /// genuinely different world positions for a shared nominal `DistrictPos` /// (the batch namespace collision D-256 rules on). With sampling unified /// (one derive core, D-256(c)), that framing is now FALSE: batch and /// window positions agree BY CONSTRUCTION — `derive_district_profile` is /// a thin wrapper over the exact same [`crate::atlas::district_profile::derive_at_metres`] /// family the window path calls, at the survey cell's own D-256(b) centre /// world metres. `full_district_profile_matches_derive_at_metres_at_shared_survey_cell_centre` /// below is the strengthened full-profile bit-identical check this design /// note used to explicitly rule out; this test keeps the riparian-signal /// check as a focused shared-exact-position regression (the ONE hand-wired /// signal T-1168 added, worth its own targeted assertion). #[test] fn window_and_batch_paths_agree_on_riparian_signal_near_a_real_river_edge() { use crate::atlas::drainage; use crate::atlas::heightmap::load_heightmap_png; use crate::atlas::river_course; let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"); let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap"); let small = heightmap.downsample(256, 128); let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level); let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr); let rn = &dr.river_network; assert!( !rn.river_cells.is_empty(), "GJ1c downsample must have river cells for this test to be meaningful" ); let params = crate::atlas::district_profile::BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() }; let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1); let station_spacing_m = DISTRICT_M as f64; // Invent a real edge and sample a point exactly on its course. let edges = river_course::build_edges(rn); let edge = edges .iter() .find(|e| e.terminus == river_course::EdgeTerminusKind::Interior) .expect("GJ1c should have an interior river edge"); let course = river_course::invent_course(seed, edge, &ta, ¶ms, station_spacing_m, 0.0); let on_course = course.points[course.points.len() / 2]; // Batch path: near_perennial_water_at (invents nearby edges on demand // from `rn` directly). let batch_signal = river_course::near_perennial_water_at( seed, &ta, ¶ms, rn, on_course, station_spacing_m, 0.0, ); // Window path: invent_courses_near_window (the SAME pre-invention step // `build_district_window_layer` uses) around a window rect containing // `on_course`, then near_perennial_water against that pre-invented list. let window_rect = ( on_course.0 - 10_000.0, on_course.1 - 10_000.0, on_course.0 + 10_000.0, on_course.1 + 10_000.0, ); let invented = invent_courses_near_window( seed, ¶ms, &ta, rn, window_rect, WindowGranularity::District, 0.0, ); let window_signal = river_course::near_perennial_water(on_course, &invented); assert!( batch_signal, "a point exactly on an invented course must read near_perennial_water_at == true (batch path)" ); assert_eq!( batch_signal, window_signal, "batch (near_perennial_water_at) and window (invent_courses_near_window + \ near_perennial_water) paths must agree on the riparian verdict at the SAME \ world position {on_course:?}" ); } /// D-256(c): the batch survey-cell profile == `derive_at_metres` at the /// SAME survey-cell-centre world metres — bit-identical, field for field /// (basin overridden on the expectation since the core always returns the /// default and the wrapper post-call-overrides it; riparian threaded /// equivalently through both paths' own mechanism). This is the /// strengthened replacement for the pre-D-256 "legitimate different /// positions" design note on the riparian-only test above — positions now /// agree by construction, so the FULL profile must too. #[test] fn full_district_profile_matches_derive_at_metres_at_shared_survey_cell_centre() { use crate::atlas::district_profile::{self, ClimateConstants}; use crate::atlas::drainage; use crate::atlas::heightmap::load_heightmap_png; use crate::atlas::scale::{self, BasinDirection, SurveyCellPos}; use std::collections::BTreeMap; let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"); let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap"); let small = heightmap.downsample(256, 128); let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level); let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr); let rn = &dr.river_network; let params = crate::atlas::district_profile::BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() }; let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1); let climate = ClimateConstants::default(); let gcpr = scale::HEIGHTMAP_CELLS_PER_DISTRICT; // An interior survey cell (not clamped at the grid edge — that case // is covered by the dedicated edge-truncation test below). let cell = SurveyCellPos(10, 6); let basin = BasinDirection::East; let batch_profile = district_profile::derive_district_profile( seed, ¶ms, &ta, cell, gcpr, &climate, "GJ1c", &BTreeMap::new(), basin, Some(rn), ); // The SAME survey-cell-centre world metres, resolved via the SAME // D-256(b) bridge function derive_district_profile uses internally. let (world_x_m, world_y_m) = district_profile::survey_cell_centre_world_m( cell, gcpr, ta.w, ta.h, params.body_radius_km, ); // Riparian equivalent: near_perennial_water_at against the same // RiverNetwork, same station spacing/cutoff derive_district_profile // uses internally — the SAME on-demand course invention, not the // window path's pre-invented slice (this is the batch-vs-on-demand // core identity, not the batch-vs-window riparian check above). // // derive_at_metres's PUBLIC signature only accepts a pre-invented // `nearby_courses` slice (never a raw bool — Ruling 4a), so to force // the SAME riparian verdict through the public path a single-point // synthetic course exactly at (world_x_m, world_y_m) is threaded when // the on-demand signal is true; an empty slice when false. Either way // `near_perennial_water((wx,wy), courses)` evaluates to the identical // bool the batch core read. let near_perennial_water = river_course::near_perennial_water_at( seed, &ta, ¶ms, rn, (world_x_m, world_y_m), DISTRICT_M as f64, 0.0, ); let riparian_equivalent: Vec = if near_perennial_water { vec![InventedCourse { edge_id: 0, class: 0, terminus: EdgeTerminusKind::Interior, points: vec![(world_x_m, world_y_m)], bbox: (world_x_m, world_y_m, world_x_m, world_y_m), }] } else { Vec::new() }; let mut expected = district_profile::derive_at_metres( seed, "GJ1c", ¶ms, &ta, world_x_m, world_y_m, &climate, 0.0, &riparian_equivalent, ); // basin_direction (inert field, D-256(c) proof) — the core always // returns the default; the wrapper post-call-overrides it. expected.basin_direction = basin; assert_eq!( batch_profile.morphology_zone as u8, expected.morphology_zone as u8 ); assert_eq!( batch_profile.tectonic_class as u8, expected.tectonic_class as u8 ); assert_eq!( batch_profile.glaciation_grade as u8, expected.glaciation_grade as u8 ); assert_eq!( batch_profile.precipitation_class as u8, expected.precipitation_class as u8 ); assert_eq!(batch_profile.slope_q, expected.slope_q); assert_eq!(batch_profile.elev_q, expected.elev_q); assert_eq!(batch_profile.ocean_fraction_q, expected.ocean_fraction_q); assert_eq!(batch_profile.river_threshold, expected.river_threshold); assert_eq!(batch_profile.temperature_c, expected.temperature_c); assert_eq!(batch_profile.moisture_q, expected.moisture_q); assert_eq!( batch_profile.vegetation_class as u8, expected.vegetation_class as u8 ); assert_eq!( batch_profile.basin_direction as u8, expected.basin_direction as u8 ); } /// D-256(c) invariant: the wrapper (`derive_district_profile`) ≡ core /// (`derive_at_metres`) identity holds at survey-grid edge cases — the /// anti-meridian column (`px` near the wrap boundary), the pole rows /// (latitude clamp), and edge-truncated cells (the covering pixel block /// itself clamped at `w`/`h`, not just the resulting position). No /// riparian signal threaded here (both sides get `river_network: None` / /// `nearby_courses: &[]` — the riparian equivalence is the DEDICATED /// concern of the test above; this one isolates the geometric position /// mapping across the grid's hard edges). #[test] fn wrapper_matches_core_at_survey_grid_edge_cases() { use crate::atlas::district_profile::{self, ClimateConstants}; use crate::atlas::heightmap::load_heightmap_png; use crate::atlas::scale::{self, BasinDirection, SurveyCellPos}; use std::collections::BTreeMap; // The committed GJ1c heightmap (same fixture the other integration // tests in this module use), downsampled to the standard 256×128 // working grid so the survey raster is a real body's shape. let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"); let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap"); let hm = heightmap.downsample(256, 128); let dr = crate::atlas::drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level); let ta = crate::atlas::features::TerrainAnalysis::analyze(&hm, &dr); let params = crate::atlas::district_profile::BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() }; let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 2); let climate = ClimateConstants::default(); let gcpr = scale::HEIGHTMAP_CELLS_PER_DISTRICT; let survey_cols = (hm.width as usize).div_ceil(gcpr) as i32; // 32 let survey_rows = (hm.height as usize).div_ceil(gcpr) as i32; // 16 let cases: &[(&str, SurveyCellPos)] = &[ ("west edge / row 0 corner", SurveyCellPos(0, 0)), ( "anti-meridian column (max col)", SurveyCellPos(survey_cols - 1, survey_rows / 2), ), ("north pole row", SurveyCellPos(survey_cols / 2, 0)), ( "south pole row", SurveyCellPos(survey_cols / 2, survey_rows - 1), ), ( "edge-truncated SE corner", SurveyCellPos(survey_cols - 1, survey_rows - 1), ), ]; for &(label, cell) in cases { let batch_profile = district_profile::derive_district_profile( seed, ¶ms, &ta, cell, gcpr, &climate, "GJ1c", &BTreeMap::new(), BasinDirection::North, // default — no override under test here None, ); let (world_x_m, world_y_m) = district_profile::survey_cell_centre_world_m( cell, gcpr, ta.w, ta.h, params.body_radius_km, ); let expected = district_profile::derive_at_metres( seed, "GJ1c", ¶ms, &ta, world_x_m, world_y_m, &climate, 0.0, &[], ); assert_eq!( batch_profile.morphology_zone as u8, expected.morphology_zone as u8, "[{label}] morphology_zone mismatch at {cell:?}" ); assert_eq!( batch_profile.elev_q, expected.elev_q, "[{label}] elev_q mismatch at {cell:?}" ); assert_eq!( batch_profile.slope_q, expected.slope_q, "[{label}] slope_q mismatch at {cell:?}" ); assert_eq!( batch_profile.ocean_fraction_q, expected.ocean_fraction_q, "[{label}] ocean_fraction_q mismatch at {cell:?}" ); assert_eq!( batch_profile.temperature_c, expected.temperature_c, "[{label}] temperature_c mismatch at {cell:?}" ); assert_eq!( batch_profile.moisture_q, expected.moisture_q, "[{label}] moisture_q mismatch at {cell:?}" ); } } /// Discipline item 3(a), mandatory: two overlapping windows sharing a /// stretch of the same edge must produce BYTE-IDENTICAL course points /// for that shared stretch (Ruling 1e, the window-independence /// invariant — "stations are generated at deterministic global /// arc-length positions along the edge; the window crops, it never /// re-parametrizes"). Two windows at different centers, both containing /// the same real GJ1c edge, must report the identical `RiverCourse` for /// that edge wherever both windows' cropped ranges overlap. #[test] fn overlapping_windows_produce_byte_identical_course_points() { use crate::atlas::drainage; use crate::atlas::heightmap::load_heightmap_png; use crate::atlas::river_course; let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"); let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap"); let small = heightmap.downsample(256, 128); let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level); let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr); let rn = &dr.river_network; let params = crate::atlas::district_profile::BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() }; let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1); let station_spacing_m = DISTRICT_M as f64; let edges = river_course::build_edges(rn); // Find the LONGEST interior edge (by point count) so the two windows // below can each cover a genuine, well-inside-their-bounds stretch — // a short edge's course could produce degenerate/edge-of-range // overlaps that don't actually exercise the invariant. let edge = edges .iter() .filter(|e| e.terminus == river_course::EdgeTerminusKind::Interior) .max_by_key(|e| { let course = river_course::invent_course(seed, e, &ta, ¶ms, station_spacing_m, 0.0); course.points.len() }) .expect("GJ1c should have an interior river edge"); let full_course = river_course::invent_course(seed, edge, &ta, ¶ms, station_spacing_m, 0.0); assert!( full_course.points.len() >= 4, "need a course with enough stations to construct two overlapping windows" ); // A midpoint on the course — the shared stretch two different // windows will both cover. let mid = full_course.points[full_course.points.len() / 2]; // Two DIFFERENT window rects, both containing `mid` well inside // their bounds (so both windows' crop ranges include the shared // stretch, not just a single boundary point). let window_a = ( mid.0 - 20_000.0, mid.1 - 20_000.0, mid.0 + 5_000.0, mid.1 + 5_000.0, ); let window_b = ( mid.0 - 5_000.0, mid.1 - 5_000.0, mid.0 + 20_000.0, mid.1 + 20_000.0, ); let invented_a = invent_courses_near_window( seed, ¶ms, &ta, rn, window_a, WindowGranularity::District, 0.0, ); let invented_b = invent_courses_near_window( seed, ¶ms, &ta, rn, window_b, WindowGranularity::District, 0.0, ); let course_a = invented_a .iter() .find(|c| c.edge_id == edge.edge_id) .expect("edge must be invented for window A"); let course_b = invented_b .iter() .find(|c| c.edge_id == edge.edge_id) .expect("edge must be invented for window B"); // Ruling 1e's actual invariant: invent_courses_near_window returns // the FULL invented course for any edge that culls in — never // window-cropped or re-parametrized at this layer (cropping happens // later, in crop_courses_for_wire). So the two windows' invented // points for the SAME edge must be byte-identical in full, not just // over some overlap region — this is the direct proof that // invention is independent of the window rect entirely. assert_eq!( course_a.points, course_b.points, "the same edge invented from two different windows must be byte-identical (D-227/Ruling 1e)" ); } /// Discipline item 3(b), mandatory: Quarter course points must stay /// within the truncated-octave amplitude bound of the District course at /// the same world position (Ruling 3b's cross-rung invariant — "the /// Quarter course is the District course plus octaves in the (1,024 /// m..4,096 m) band"). Checked via the perpendicular deviation between /// the two rungs' station lists never exceeding the District-rung peak /// amplitude cap by more than a small tolerance (Quarter's extra octaves /// can only ADD bounded displacement on top of the District shape, never /// diverge unboundedly). #[test] fn quarter_course_stays_within_district_amplitude_bound() { use crate::atlas::drainage; use crate::atlas::heightmap::load_heightmap_png; use crate::atlas::river_course; let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"); let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap"); let small = heightmap.downsample(256, 128); let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level); let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr); let rn = &dr.river_network; let params = crate::atlas::district_profile::BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() }; let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1); let edges = river_course::build_edges(rn); let edge = edges .iter() .find(|e| e.terminus == river_course::EdgeTerminusKind::Interior) .expect("GJ1c should have an interior river edge"); let district_course = river_course::invent_course( seed, edge, &ta, ¶ms, DISTRICT_M as f64, 2.0 * DISTRICT_M as f64, // District's real Nyquist-floor cutoff ); let quarter_course = river_course::invent_course( seed, edge, &ta, ¶ms, crate::atlas::scale::QUARTER_M as f64, 2.0 * crate::atlas::scale::QUARTER_M as f64, // Quarter's real cutoff ); // For each District station, find the nearest Quarter station (by // arc-length proxy: nearest point in world space) and confirm the // deviation stays within the District-rung amplitude cap (Stage B's // own hard cap, Ruling 3c) plus a small numeric tolerance — Quarter // must refine the shape, never blow past the amplitude budget the // SAME peak-fraction-of-chord cap governs at every rung. let anchor_a = district_course.points[0]; let anchor_b = *district_course.points.last().unwrap(); let chord_m = ((anchor_a.0 - anchor_b.0).powi(2) + (anchor_a.1 - anchor_b.1).powi(2)).sqrt(); let cap_m = (chord_m * river_course::STAGE_B_PEAK_FRACTION_OF_CHORD) .min(crate::atlas::scale::QUARTER_M as f64 * 0.5) * 1.35; // widest class_scale entry (trunk) for &dp in &district_course.points { let nearest_q = quarter_course .points .iter() .min_by(|a, b| { let da = (a.0 - dp.0).powi(2) + (a.1 - dp.1).powi(2); let db = (b.0 - dp.0).powi(2) + (b.1 - dp.1).powi(2); da.partial_cmp(&db).unwrap() }) .unwrap(); let dist = ((nearest_q.0 - dp.0).powi(2) + (nearest_q.1 - dp.1).powi(2)).sqrt(); assert!( dist <= cap_m + 50.0, // small slack for nearest-station (not exact arc-length) matching "Quarter course deviates {dist} m from the nearest District station — \ exceeds the {cap_m} m amplitude bound (Ruling 3b cross-rung invariant)" ); } } /// **T-1170 PR #197 review, Hoshe #1 acceptance test (blocking, permanent /// — not a throwaway probe).** Every real Mouth edge on the GJ1c golden /// fixture (256×128 downsample, the SAME fixture `cascade_golden.rs` /// pins — 3 mouths: `[(38,47), (38,98), (124,239)]`) must resolve /// `CourseTerminus::Mouth`, not `CourseTerminus::None`. /// /// **What this guards:** before the fix, `build_edges` set /// `downstream = upstream` for every Mouth edge (a same-cell /// placeholder — the SAME discard-then-need-it-later anti-pattern /// Ruling 2b's `river_downstream` field fixed for interior pointers, /// applied a second time to the seaward neighbor `extract_river_network` /// already computes and then threw away). That zeroed the chord /// (`chord_m < 1.0`), which tripped `invent_course`'s degenerate /// single-point return, which made `resolve_mouth_terminus`'s station /// walk a no-op (a 1-point course can't reach the `pts.len() >= 2` /// fallback probe either) — all 3 real GJ1c mouths silently resolved /// `CourseTerminus::None` instead of `Mouth`, and since Ruling 3g retired /// the District/Quarter draw-time clip on the promise of real termini, /// mouths would have disappeared entirely at those rungs. The fix: /// `RiverNetwork::river_seaward` (additive, captured in the same /// `extract_river_network` pass) carries the real seaward neighbor /// through to `build_edges`, giving Mouth edges a genuine ~one-cell /// chord to invent a course along. #[test] fn all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none() { use crate::atlas::drainage; use crate::atlas::heightmap::load_heightmap_png; use crate::atlas::river_course; let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"); let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap"); let small = heightmap.downsample(256, 128); let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level); let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr); let rn = &dr.river_network; let params = crate::atlas::district_profile::BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() }; let climate = crate::atlas::district_profile::ClimateConstants::default(); let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1); let station_spacing_m = DISTRICT_M as f64; let edges = river_course::build_edges(rn); let mouth_edges: Vec<_> = edges .iter() .filter(|e| e.terminus == river_course::EdgeTerminusKind::Mouth) .collect(); assert_eq!( mouth_edges.len(), rn.mouths.len(), "build_edges must produce exactly one Mouth edge per RiverNetwork.mouths entry" ); assert_eq!( mouth_edges.len(), 3, "GJ1c at this downsample is expected to have 3 real mouths (matches the \ committed cascade_golden fixture) — if this count changes, re-verify against \ tests/golden/cascade_layer1.json before updating this assertion" ); let mut resolved_mouth_count = 0; for edge in &mouth_edges { // Sanity: the fix means Mouth edges get a real, non-degenerate // chord toward the seaward neighbor — never upstream==downstream. assert_ne!( edge.upstream, edge.downstream, "Mouth edge {:?} still has a same-cell placeholder downstream — \ river_seaward threading regressed", edge.edge_id ); let course = river_course::invent_course(seed, edge, &ta, ¶ms, station_spacing_m, 0.0); assert!( course.points.len() >= 2, "Mouth edge {:?} invented a degenerate {}-point course — the chord-length \ fix regressed", edge.edge_id, course.points.len() ); // Window rect generous enough to contain the whole short mouth // course (mouths are ~one cell chord, so a wide margin is cheap). let (min_x, max_x) = course .points .iter() .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| { (lo.min(p.0), hi.max(p.0)) }); let (min_y, max_y) = course .points .iter() .fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), p| { (lo.min(p.1), hi.max(p.1)) }); let margin = 50_000.0; let window_rect = ( min_x - margin, min_y - margin, max_x + margin, max_y + margin, ); let wire = crop_course_to_window( &course, window_rect, seed, "GJ1c", ¶ms, &ta, &climate, 0.0, station_spacing_m, ) .unwrap_or_else(|| { panic!( "Mouth edge {:?} cropped to nothing in its own window", edge.edge_id ) }); assert_eq!( wire.terminus, CourseTerminus::Mouth, "Mouth edge {:?} (upstream {:?}, downstream {:?}) resolved {:?} instead of \ CourseTerminus::Mouth", edge.edge_id, edge.upstream, edge.downstream, wire.terminus ); resolved_mouth_count += 1; } assert_eq!( resolved_mouth_count, 3, "acceptance criterion (Hoshe #1): all 3 real GJ1c mouths must resolve \ CourseTerminus::Mouth" ); } /// **T-1170 PR #197 review round 2 (coordinator's live GJ380c/Lendel /// repro) — the upstream-cut coverage gap in the test above, closed.** /// /// `all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none` derives its /// window rect from the invented course's OWN min/max point bbox, so by /// construction that window always contains the WHOLE course (both /// `first_in == 0` and `last_in == n-1`) — it never exercises a window /// that cuts the UPSTREAM anchor while the true downstream terminus /// still falls inside. `crop_course_to_window`'s terminus branch is /// driven entirely by `last_in`/`hi` (the downstream side); this test is /// the direct proof that an upstream cut (`first_in > 0`, i.e. `lo > 0`) /// does NOT collapse the terminus flag to `ContinuesBeyondWindow` — the /// coordinator's hypothesis (a) from the live-server investigation, /// falsified here as a permanent regression case rather than only a /// throwaway probe (`mouth_repro_probe.rs`, deleted after this landed). /// /// Window construction: take one real GJ1c mouth edge's full invented /// course, find the TRUE (uncropped) terminus point, then build a window /// rect deliberately offset upstream along the course's own tail /// direction so its near edge sits well past the upstream anchor (cutting /// it out of range) while its far edge still comfortably contains the /// true terminus. #[test] fn mouth_terminus_survives_an_upstream_only_crop() { use crate::atlas::drainage; use crate::atlas::heightmap::load_heightmap_png; use crate::atlas::river_course; let src = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"); let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode committed GJ1c heightmap"); let small = heightmap.downsample(256, 128); let dr = drainage::analyze(&small.data, small.width, small.height, small.sea_level); let ta = crate::atlas::features::TerrainAnalysis::analyze(&small, &dr); let rn = &dr.river_network; let params = crate::atlas::district_profile::BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() }; let climate = crate::atlas::district_profile::ClimateConstants::default(); let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 1); let station_spacing_m = DISTRICT_M as f64; let edges = river_course::build_edges(rn); let edge = edges .iter() .find(|e| e.terminus == river_course::EdgeTerminusKind::Mouth) .expect("GJ1c fixture must have at least one Mouth edge"); let course = river_course::invent_course(seed, edge, &ta, ¶ms, station_spacing_m, 0.0); assert!( course.points.len() >= 2, "need a non-degenerate course to construct a meaningful upstream cut" ); let upstream_anchor = course.points[0]; let true_end = *course.points.last().unwrap(); // Window offset upstream along the course's own tail direction (the // last segment), far enough that the upstream anchor falls outside // the window but the true terminus stays comfortably inside. let second_last = course.points[course.points.len().saturating_sub(2)]; let (dx, dy) = (true_end.0 - second_last.0, true_end.1 - second_last.1); let len = (dx * dx + dy * dy).sqrt().max(1e-9); let (ux, uy) = (dx / len, dy / len); // Half the upstream->downstream distance keeps the window's near // edge well clear of the upstream anchor for any real mouth chord // (mouth edges are short, ~one D8 step), while the far edge margin // below still comfortably covers the terminus. let chord_m = ((true_end.0 - upstream_anchor.0).powi(2) + (true_end.1 - upstream_anchor.1).powi(2)) .sqrt(); let offset_m = (chord_m * 0.5).max(5_000.0); let center = (true_end.0 - ux * offset_m, true_end.1 - uy * offset_m); let half_extent_m = (chord_m * 0.5).max(5_000.0); let window_rect = ( center.0 - half_extent_m, center.1 - half_extent_m, center.0 + half_extent_m, center.1 + half_extent_m, ); // Sanity on the window construction itself (not the code under // test): the upstream anchor must genuinely be cropped out, and the // true terminus must genuinely be inside — otherwise this test // isn't exercising the branch it claims to. let inside = |p: (f64, f64)| { p.0 >= window_rect.0 && p.0 <= window_rect.2 && p.1 >= window_rect.1 && p.1 <= window_rect.3 }; assert!( !inside(upstream_anchor), "test construction error: upstream anchor {upstream_anchor:?} must be OUTSIDE \ the window {window_rect:?} for this to be a real upstream-cut case" ); assert!( inside(true_end), "test construction error: true terminus {true_end:?} must be INSIDE the \ window {window_rect:?} for this to test the terminus-survives claim" ); let wire = crop_course_to_window( &course, window_rect, seed, "GJ1c", ¶ms, &ta, &climate, 0.0, station_spacing_m, ) .unwrap_or_else(|| panic!("course cropped to nothing despite containing the terminus")); assert_eq!( wire.terminus, CourseTerminus::Mouth, "an upstream-only crop (anchor cut, true terminus still in-window) must NOT \ collapse the terminus flag — got {:?} for edge {:?}", wire.terminus, edge.edge_id ); } /// **T-1170 live GJ380c/Lendel reconciliation dossier (coordinator's /// request) — walk/paint agreement invariant, pinned permanently.** /// /// Live capture at GJ380c Quarter n=32 (server-clamped to n=16 — see /// below), district (13195,-2383), reported the ENTIRE visible course /// (44 wire points, `terminus=Mouth`) painting as water tones, with the /// resolved terminus apparently landing deep in open water. Investigated /// via an instrumented reproduction of `resolve_mouth_terminus`'s exact /// walk (temporary probe, deleted after this landed) — DETERMINATION: /// this is not a bug. The window's world-metre rect /// (`x∈[27006976,27039744]`) simply sits ~60+ km from the course's /// upstream land anchor and close to the resolved coastal terminus, so /// the overwhelming majority of what's IN FRAME is genuinely /// Lake/OpenOcean-painted — confirmed by hand-computing the exact RGB /// (`MORPHOLOGY_RGB_OPAQUE` × elevation lightness) for the coordinator's /// sampled pixel colors, which matched Lake/OpenOcean to the rounding /// digit. The walk's own station-by-station classification (178 land /// stations, then 35 water stations, ZERO flip-flops) is perfectly /// monotonic and the resolved terminus sits ~240 m from the true /// land→water crossing — not "tens of km past coast". /// /// **The invariant this test pins, since the raw "N consecutive water /// stations" framing turned out not to be the real signal:** the wire /// course's resolved `Mouth` terminus point must land in a window CELL /// whose PAINTED morphology is genuinely `OpenOcean`/`Lake` (never a /// land-family zone) — i.e. the termination walk and the window's own /// per-cell classification, sampled independently via the SAME /// `derive_at_metres` call, must agree at the terminus. This is the /// walk/paint reconciliation the dossier was asked to determine, made /// permanent and mechanism-agnostic (it would catch a REAL divergence — /// wrap slip, sign flip, station-order bug — regardless of which /// specific window happens to expose it). #[test] fn mouth_terminus_lands_in_a_painted_water_cell_on_real_gj380c() { use crate::atlas::body_params_reader::BodyParamsReader; use crate::atlas::drainage; use crate::atlas::heightmap::{load_heightmap_png, GRID_H, GRID_W}; use crate::atlas::river_course; let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let src = manifest.join("../wiki/star-systems/GJ-380/bodies/GJ380c/heightmap.png"); let systems_db = manifest.join("data/systems.db"); let params_reader = BodyParamsReader::open(&systems_db).expect("open committed systems.db"); let params = params_reader .read_body_params("GJ380c") .expect("read GJ380c body params from committed systems.db"); let heightmap = load_heightmap_png(&src, "GJ380c", 0.3).expect("decode GJ380c heightmap"); let working = if heightmap.width > GRID_W || heightmap.height > GRID_H { heightmap.downsample(GRID_W, GRID_H) } else { heightmap }; let dr = drainage::analyze( &working.data, working.width, working.height, working.sea_level, ); let ta = crate::atlas::features::TerrainAnalysis::analyze(&working, &dr); let rn = &dr.river_network; // The exact real mouth cell from the live capture. let target_px: (u16, u16) = (63, 354); let edges = river_course::build_edges(rn); let edge = edges .iter() .find(|e| e.edge_id == river_course::pack_cell_id(target_px)) .expect("GJ380c must have a Mouth edge at the live-captured cell"); assert_eq!( edge.edge_id, 4129122, "must be the same edge the live capture reported" ); let seed = SeedChain::for_body(0, "GJ380c"); // production default world_seed let climate = crate::atlas::district_profile::ClimateConstants::default(); // The EXACT live window shape: district (13195,-2383), REQUESTED // n=32, Quarter granularity — server-side clamps n=32 down to n=16 // at Quarter granularity (`clamp_window_n`: cap_n = sqrt(4096)/4 = // 16), which `build_district_window_layer` does NOT do itself (it // trusts n verbatim by its own doc) — the request-handling layer // (`handle_atlas_request`) applies `clamp_window_n_v2` BEFORE // calling it. Pass the already-clamped n=16 here to match what a // real client request actually receives. let center = (13195, -2383); let clamped_n = 16u32; let layer = build_district_window_layer( seed, "GJ380c", ¶ms, &ta, rn, center, clamped_n, &climate, WindowGranularity::Quarter, 0, ); let wire = layer .courses .iter() .find(|c| c.edge_id == edge.edge_id) .expect("edge 4129122 must ship in this window, matching the live capture"); assert_eq!( wire.points.len(), 44, "point count must match the live capture exactly, confirming this IS the \ reported window" ); assert_eq!(wire.terminus, CourseTerminus::Mouth); // The invariant: the terminus point's containing Quarter cell, read // from THIS SAME layer's own painted `morphology` array, must be // OpenOcean or Lake. let side = WindowGranularity::Quarter.cell_grid_side(clamped_n); let half = side / 2; let step_m = WindowGranularity::Quarter.spacing_m(); let center_world_m = ( center.0 as f64 * DISTRICT_M as f64, center.1 as f64 * DISTRICT_M as f64, ); let (tx, ty) = *wire.points.last().unwrap(); let col = ((tx as f64 - center_world_m.0) / step_m + half as f64).round() as i32; let row = ((ty as f64 - center_world_m.1) / step_m + half as f64).round() as i32; assert!( row >= 0 && row < side && col >= 0 && col < side, "terminus point ({tx},{ty}) must map to an in-window cell, got (row={row},col={col})" ); let cell_idx = (row * side + col) as usize; let painted = layer.morphology[cell_idx]; assert!( painted == crate::simulation::generator::MorphologyZone::OpenOcean as u8 || painted == crate::simulation::generator::MorphologyZone::Lake as u8, "walk/paint DIVERGENCE: the resolved Mouth terminus ({tx},{ty}) landed in a cell \ painted with morphology discriminant {painted} (not OpenOcean=0 or Lake=1) — the \ termination walk's own water verdict must agree with the window's painted \ classification at the SAME position" ); } /// Clamped-window edge: `n = 1` is the minimum valid window (a single /// district) — no panic, no empty output, exactly one cell per array. #[test] fn build_district_window_layer_handles_n_equals_one() { let hm = window_test_hm(); let ta = window_test_ta(&hm); let rn = window_test_river_network(&hm); let params = window_test_params(); let climate = crate::atlas::district_profile::ClimateConstants::default(); let seed = SeedChain::root(1).derive(SeedDomain::Body, 1); let layer = build_district_window_layer( seed, "test_body", ¶ms, &ta, &rn, (0, 0), 1, &climate, WindowGranularity::District, 0, ); assert_eq!(layer.n, 1); assert_eq!(layer.morphology.len(), 1); assert_eq!(layer.elev_q.len(), 1); assert_eq!(layer.temp_dc.len(), 1); assert_eq!(layer.moisture_q.len(), 1); assert_eq!(layer.vegetation.len(), 1); assert_eq!(layer.glaciation.len(), 1); } /// Determinism spot-check (D-010, T-1123 precedent promoted to a real /// test per the ticket): two full derive passes over the SAME window are /// byte-identical, at a window size large enough to exercise many cells /// (mirrors `aliveness_probe --render`'s own two-pass proof, now pinned /// as a unit test rather than a probe-only demonstration). #[test] fn build_district_window_layer_two_passes_are_byte_identical() { let hm = window_test_hm(); let ta = window_test_ta(&hm); let rn = window_test_river_network(&hm); let params = window_test_params(); let climate = crate::atlas::district_profile::ClimateConstants::default(); let seed = SeedChain::root(7).derive(SeedDomain::Body, 3); let n = 8u32; let first = build_district_window_layer( seed, "test_body", ¶ms, &ta, &rn, (3, -2), n, &climate, WindowGranularity::District, 0, ); let second = build_district_window_layer( seed, "test_body", ¶ms, &ta, &rn, (3, -2), n, &climate, WindowGranularity::District, 0, ); assert_eq!( first, second, "two full derive passes over the same (center, n) must be byte-identical (D-010/D-227)" ); } /// T-1151 acceptance: the row-chunked `par_iter` window build /// ([`build_district_window_layer`]) must be bit-identical to the /// pre-parallel serial baseline ([`build_district_window_layer_serial`]) /// — same inputs, same output, exact row-major array ordering preserved. /// Run at a window size large enough (16×16 = 256 cells) to actually /// exercise multiple Rayon-dispatched rows, not just n=1. #[test] fn build_district_window_layer_parallel_matches_serial() { let hm = window_test_hm(); let ta = window_test_ta(&hm); let rn = window_test_river_network(&hm); let params = window_test_params(); let climate = crate::atlas::district_profile::ClimateConstants::default(); let seed = SeedChain::root(13).derive(SeedDomain::Body, 4); let n = 16u32; let center = (5, -9); let parallel = build_district_window_layer( seed, "test_body", ¶ms, &ta, &rn, center, n, &climate, WindowGranularity::District, 0, ); let serial = build_district_window_layer_serial( seed, "test_body", ¶ms, &ta, &rn, center, n, &climate, WindowGranularity::District, 0, ); assert_eq!( parallel, serial, "row-chunked par_iter window build must be bit-identical to the serial baseline" ); // Row-major ordering check, explicit (not just struct equality): the // parallel path collects one Vec per row via par_iter, // which preserves index order (`par_iter().map(...).collect()` is // order-preserving), but pin the ordering assumption directly too. assert_eq!(parallel.morphology.len(), (n * n) as usize); assert_eq!(parallel.center, center); assert_eq!(parallel.n, n); } /// FULL-PATH determinism (PR #187 review — Tyre C3, binding, load-bearing /// for save-file lineage under D-227): the test above reuses ONE `ta` for /// both passes, which only proves `build_district_window_layer` (the /// packer) is a pure function of its arguments — it says nothing about /// whether re-running `run_layer1` itself (the D8 drainage pass + /// `TerrainAnalysis::analyze`) is deterministic, which is exactly what the /// production `DeriveWindow` path depends on (T-1137's `TerrainAnalysis` /// re-derive / `TerrainAnalysisCache::get_or_derive` on a cache miss, and /// `aliveness_probe --render`'s workaround before it). /// /// This test runs `run_layer1` TWICE, independently, from the SAME /// `(seed, heightmap)` inputs — no shared `ta` — and asserts the two /// COMPLETE `DistrictWindowLayer` outputs (derive AND pack) are /// byte-identical. D-227's save-file guarantee ("same seed/body/position → /// same derived output, always") is only as strong as the weakest link in /// that chain; this closes the gap the packer-only test left open. #[test] fn full_path_two_independent_run_layer1_passes_produce_identical_window() { let hm = window_test_hm(); let params = window_test_params(); let climate = crate::atlas::district_profile::ClimateConstants::default(); let seed = SeedChain::root(11).derive(SeedDomain::Body, 5); let n = 6u32; let center = (4, -1); // Two INDEPENDENT calls to run_layer1 — each re-runs D8 drainage + // TerrainAnalysis::analyze from scratch on the SAME heightmap, exactly // mirroring what a cold TerrainAnalysisCache miss does on the real // DeriveWindow path (or a second body eviction re-pay). let (l1_pass1, ta_pass1) = crate::atlas::layer1::run_layer1(&hm); let (l1_pass2, ta_pass2) = crate::atlas::layer1::run_layer1(&hm); // Confirm the two independent TerrainAnalysis derivations themselves // agree field-by-field — a precise failure signal if drainage/analyze // ever introduces nondeterminism (unordered iteration, uninitialized // memory, etc.) BEFORE the packer even runs. assert_eq!(ta_pass1.ocean_mask, ta_pass2.ocean_mask); assert_eq!(ta_pass1.lake_mask, ta_pass2.lake_mask); assert_eq!(ta_pass1.water_dist, ta_pass2.water_dist); assert_eq!(ta_pass1.slope_deg, ta_pass2.slope_deg); assert_eq!(ta_pass1.elev_pct, ta_pass2.elev_pct); // T-1184: two independent hydrology solves (each run_layer1 call // solves fresh — no shared HydrologyResult) must also agree // byte-for-byte, extending this test's own "weakest link in the // determinism chain" rationale to the newest field on TerrainAnalysis. let hydro1 = ta_pass1 .hydrology .as_ref() .expect("run_layer1 must populate hydrology"); let hydro2 = ta_pass2 .hydrology .as_ref() .expect("run_layer1 must populate hydrology"); assert_eq!(hydro1.elevation, hydro2.elevation); assert_eq!(hydro1.filled, hydro2.filled); // Now the FULL path: pack a DistrictWindowLayer from each independent // TerrainAnalysis and confirm the complete served payload agrees. let window_from_pass1 = build_district_window_layer( seed, "test_body", ¶ms, &ta_pass1, &l1_pass1.river_network, center, n, &climate, WindowGranularity::District, 0, ); let window_from_pass2 = build_district_window_layer( seed, "test_body", ¶ms, &ta_pass2, &l1_pass2.river_network, center, n, &climate, WindowGranularity::District, 0, ); assert_eq!( window_from_pass1, window_from_pass2, "two independent run_layer1 derivations from the same (seed, heightmap) \ must pack to a byte-identical DistrictWindowLayer end to end (D-227)" ); } /// [`DistrictWindowCache`] insert/get round-trips, and a capacity-1 cache /// evicts the oldest entry FIFO — mirroring `BodyWorldStateCache`'s own /// `evicts_lru_on_overflow` precedent, adapted to this cache's /// capacity-only insertion-order eviction (no access-recency tracking, /// per the struct doc: D-227 means a cached window has no staleness to /// track, only unbounded growth to bound). #[test] fn district_window_cache_insert_get_and_evict() { let mut cache = DistrictWindowCache::new(2); let key_a: DistrictWindowKey = ("Alpha".into(), (0, 0), 4, WindowGranularity::District, 0); let key_b: DistrictWindowKey = ("Beta".into(), (1, 1), 4, WindowGranularity::District, 0); let key_c: DistrictWindowKey = ("Gamma".into(), (2, 2), 4, WindowGranularity::District, 0); let mk = |center, n| DistrictWindowLayer { center, n, granularity_v2: WindowGranularity::District, min_wl_m: 0, morphology: vec![0; (n * n) as usize], elev_q: vec![0; (n * n) as usize], temp_dc: vec![REGION_TEMP_NONE_DC; (n * n) as usize], moisture_q: vec![0; (n * n) as usize], vegetation: vec![0; (n * n) as usize], glaciation: vec![0; (n * n) as usize], courses: Vec::new(), }; assert!(cache.get(&key_a).is_none()); cache.insert(key_a.clone(), mk((0, 0), 4)); cache.insert(key_b.clone(), mk((1, 1), 4)); assert_eq!(cache.len(), 2); assert!(cache.get(&key_a).is_some()); assert!(cache.get(&key_b).is_some()); // Cache at capacity (2): inserting a third entry evicts key_a (oldest). cache.insert(key_c.clone(), mk((2, 2), 4)); assert_eq!(cache.len(), 2); assert!( cache.get(&key_a).is_none(), "key_a should have been evicted" ); assert!(cache.get(&key_b).is_some()); assert!(cache.get(&key_c).is_some()); } /// `handle_atlas_request`'s window branch clamps `window_n` server-side to /// `[1, DISTRICT_WINDOW_MAX_N]` — a request claiming an oversized `n` on /// the wire never reaches `build_district_window_layer` un-clamped. This /// exercises the full request→submit→drain→cache→re-request loop with an /// out-of-range `window_n`, confirming the CACHED layer (once the /// background derive completes) carries the CLAMPED `n`, not the /// requested one. #[test] fn handle_atlas_request_clamps_oversized_window_n() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader("GJ1c"); let queue = GenerationQueue::with_threads(1); let oversized_req = AtlasLayerRequest { body_id: "GJ1c".to_string(), up_to: CascadeLayer::Topography, window_center: Some((0, 0)), window_n: DISTRICT_WINDOW_MAX_N * 10, // wildly over the wire — must clamp, not trust window_granularity_v2: None, window_min_wl_m: 0, }; let resp = handle_atlas_request( &oversized_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); // First request: window not yet cached → None, but a DeriveWindow // must have been submitted (checked via the drain below). assert!(resp.district_window.is_none()); // Wait for the Rayon DeriveWindow work item to complete. std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); let window_completion = completions.into_iter().find_map(|c| { if let GenCompletion::WindowDerived { body_id, layer } = c { if body_id == "GJ1c" { return Some(layer); } } None }); let layer = window_completion.expect("DeriveWindow must complete for GJ1c"); assert_eq!( layer.n, DISTRICT_WINDOW_MAX_N, "server must clamp window_n to DISTRICT_WINDOW_MAX_N, never trust the wire value" ); } /// **Contract-pinning test (PR #191 review, Tyre C1):** a quarter /// (granularity=4) request for `n=32` echoes the WIRE-CAP-CLAMPED `n=16`, /// not the requested 32 — `32² × 4² = 16,384` cells, 4x over /// `WIRE_CAP_CELLS`. This is the exact scenario the review flagged as /// silently breaking the client the moment T-1153 requests quarter at /// n=32: the server echoes a DIFFERENT `n` than what was asked for, and /// any client staleness guard comparing raw `_n` against the echo must /// already know this will happen (see /// `atlas_window_request.gd::_clamp_window_n_mirror()`, the client-side /// fix landed alongside this test). #[test] fn quarter_n32_request_echoes_clamped_n16() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader("GJ1c"); let queue = GenerationQueue::with_threads(1); let quarter_n32_req = AtlasLayerRequest { body_id: "GJ1c".to_string(), up_to: CascadeLayer::Topography, window_center: Some((0, 0)), window_n: 32, window_granularity_v2: Some(WindowGranularity::Quarter), window_min_wl_m: 0, }; let resp = handle_atlas_request( &quarter_n32_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); assert!(resp.district_window.is_none(), "first request — cache miss"); std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); let window_completion = completions.into_iter().find_map(|c| { if let GenCompletion::WindowDerived { body_id, layer } = c { if body_id == "GJ1c" { return Some(layer); } } None }); let layer = window_completion.expect("DeriveWindow must complete for GJ1c"); assert_eq!( layer.granularity_v2, WindowGranularity::Quarter, "granularity must echo back as requested (4 is within budget on its own)" ); assert_eq!( layer.n, 16, "a quarter n=32 request must echo the wire-cap-clamped n=16, not the requested 32" ); } // ------------------------------------------------------------------- // clamp_window_n (T-1150) // ------------------------------------------------------------------- /// District granularity: the per-axis DISTRICT_WINDOW_MAX_N cap alone /// governs (64² × 1² = 4,096 = WIRE_CAP_CELLS exactly, so the cap is /// never tighter than DISTRICT_WINDOW_MAX_N at granularity 1). #[test] fn clamp_window_n_district_granularity_uses_per_axis_cap() { assert_eq!( clamp_window_n(DISTRICT_WINDOW_MAX_N * 10, WINDOW_GRANULARITY_DISTRICT), DISTRICT_WINDOW_MAX_N ); assert_eq!(clamp_window_n(32, WINDOW_GRANULARITY_DISTRICT), 32); assert_eq!(clamp_window_n(0, WINDOW_GRANULARITY_DISTRICT), 1); } /// Quarter granularity: the wire-size ceiling bites BEFORE the per-axis /// cap — a request for n=64 at granularity 4 would derive 256×256=65,536 /// cells (16x over budget), so it must clamp down to n=16 /// (16² × 4² = 4,096 = WIRE_CAP_CELLS exactly — matching the design /// doc's §3 worked example, "Quarter, capped to same cell budget /// (n≈16 districts across)"), never to the raw DISTRICT_WINDOW_MAX_N=64. #[test] fn clamp_window_n_quarter_granularity_uses_wire_cap_not_per_axis_cap() { let clamped = clamp_window_n(DISTRICT_WINDOW_MAX_N, WINDOW_GRANULARITY_QUARTER); assert_eq!( clamped, 16, "quarter granularity must clamp n to keep (n*granularity)^2 <= WIRE_CAP_CELLS" ); assert!( clamped * clamped * WINDOW_GRANULARITY_QUARTER * WINDOW_GRANULARITY_QUARTER <= WIRE_CAP_CELLS, "clamped cell count must never exceed WIRE_CAP_CELLS" ); } /// A small requested `n` at quarter granularity is left unclamped when it /// already fits the budget (the cap must not be a flat floor/ceiling /// substitution — only trims when the request would actually overflow). #[test] fn clamp_window_n_quarter_granularity_leaves_small_n_unclamped() { assert_eq!(clamp_window_n(8, WINDOW_GRANULARITY_QUARTER), 8); } // ------------------------------------------------------------------- // clamp_window_n_v2 (T-1152; PR #192 review — Hoshe, coordinator ruling // 2026-07-22: test 2 reframed per the brute-force finding that the // Region halving loop is unreachable at the CURRENT constants — see // clamp_window_n_v2's doc comment for the full rationale) // ------------------------------------------------------------------- /// The exact boundary: `n = DISTRICT_WINDOW_MAX_N_REGION` (6,400) is the /// largest per-axis-legal `n`, and it lands EXACTLY on the wire-size /// ceiling (`cell_grid_side(6400) = 64 = sqrt(WIRE_CAP_CELLS)`, /// `64² = 4,096 = WIRE_CAP_CELLS`) — uncontested, meaning the request is /// NOT further reduced by the halving loop; the per-axis clamp alone is /// already exact at this boundary. #[test] fn clamp_window_n_v2_region_exact_boundary_n6400_uncontested() { let result = clamp_window_n_v2(DISTRICT_WINDOW_MAX_N_REGION, WindowGranularity::Region); assert_eq!( result, DISTRICT_WINDOW_MAX_N_REGION, "n=6400 must pass through unmodified — it already lands exactly on the ceiling" ); let side = WindowGranularity::Region.cell_grid_side(result) as u32; assert_eq!( side * side, WIRE_CAP_CELLS, "n=6400's cell_grid_side must land EXACTLY on WIRE_CAP_CELLS, not under or over it" ); } /// **Reframed per the coordinator's 2026-07-22 ruling (PR #192 review — /// Hoshe).** The originally-briefed name/shape /// (`clamp_window_n_v2_region_halving_loop_fires_above_boundary`, e.g. /// n=6450) does not hold: `raw_n.clamp(1, DISTRICT_WINDOW_MAX_N_REGION)` /// runs BEFORE the halving loop's condition is ever checked, so any /// `raw_n > DISTRICT_WINDOW_MAX_N_REGION` is clamped to exactly 6,400 — /// the SAME uncontested boundary the test above proves — before /// `cell_grid_side` ever sees the raw value. A brute-force sweep (done /// by hand before writing this test, see `clamp_window_n_v2`'s doc /// comment) confirms `cell_grid_side(n)` never exceeds `sqrt(WIRE_CAP_CELLS)` /// for ANY `n` in `[1, DISTRICT_WINDOW_MAX_N_REGION]` — so the halving /// loop is unreachable at the CURRENT constant derivation, not a bug to /// manufacture a test around (coordinator's option 1, not option 2). /// /// This test proves the ACTUAL property: the per-axis cap ALONE already /// satisfies the wire-size ceiling for every reachable input, and pins /// the loop's current no-op status explicitly — swept across /// `[1, 2 × DISTRICT_WINDOW_MAX_N_REGION]` (double the legal range, so /// wildly-oversized wire values are covered too, never trusting the /// wire). If a FUTURE constant change (a new rung, a `WIRE_CAP_CELLS` /// retune) ever makes the loop fire, the second assertion below breaks /// LOUDLY — forcing a deliberate look rather than a silent behavior /// change (exactly the safety-net role the loop exists for). #[test] fn clamp_window_n_v2_region_per_axis_cap_alone_satisfies_wire_cap_for_all_inputs() { for raw_n in 1..=(2 * DISTRICT_WINDOW_MAX_N_REGION) { let result = clamp_window_n_v2(raw_n, WindowGranularity::Region); let side = WindowGranularity::Region.cell_grid_side(result) as u32; assert!( side * side <= WIRE_CAP_CELLS, "raw_n={raw_n}: clamped result {result} (side {side}) exceeds WIRE_CAP_CELLS" ); assert_eq!( result, raw_n.clamp(1, DISTRICT_WINDOW_MAX_N_REGION), "raw_n={raw_n}: the halving loop must be a no-op at current constants — \ the per-axis clamp alone must already be the final answer" ); } } /// `District`/`Quarter` through `clamp_window_n_v2` must be BYTE-IDENTICAL /// to the legacy `clamp_window_n` for every input either variant can /// legally carry — `clamp_window_n_v2` is documented as delegating to the /// legacy function unchanged for these two rungs, this pins that claim /// with a sweep rather than a handful of spot values. #[test] fn clamp_window_n_v2_delegates_to_legacy_for_district_and_quarter() { // Sweep well past DISTRICT_WINDOW_MAX_N so the "never trust the wire" // oversized-input case is covered too, not just in-range values. for raw_n in 0..=(DISTRICT_WINDOW_MAX_N * 3) { assert_eq!( clamp_window_n_v2(raw_n, WindowGranularity::District), clamp_window_n(raw_n, WINDOW_GRANULARITY_DISTRICT), "District: clamp_window_n_v2 must match clamp_window_n exactly at raw_n={raw_n}" ); assert_eq!( clamp_window_n_v2(raw_n, WindowGranularity::Quarter), clamp_window_n(raw_n, WINDOW_GRANULARITY_QUARTER), "Quarter: clamp_window_n_v2 must match clamp_window_n exactly at raw_n={raw_n}" ); } } // ------------------------------------------------------------------- // quantize_min_wl_m (T-1150, PR #191 review — Hoshe 1 / Tyre C3, design doc §5) // ------------------------------------------------------------------- #[test] fn quantize_min_wl_m_exact_band_values_are_stable() { for &band in &MIN_WL_BANDS_M { assert_eq!(quantize_min_wl_m(band as u32), band as u32); } } #[test] fn quantize_min_wl_m_zero_stays_zero() { assert_eq!(quantize_min_wl_m(0), 0); } /// A value nearer to 0 than to the finest real octave band (T-1162: now /// 1,024, the Quarter-rung floor) snaps to 0 (no cutoff) — the band set /// includes 0 as a real, selectable band, not just a special-cased default. #[test] fn quantize_min_wl_m_small_value_snaps_to_zero_band() { assert_eq!(quantize_min_wl_m(500), 0); } /// A value between two real octave bands snaps to the NEAREST one, not /// always up or always down. #[test] fn quantize_min_wl_m_mid_value_snaps_to_nearest_band() { // Between 4,096 and 8,192: 5,000 is nearer 4,096 (dist 904 vs 3,192). assert_eq!(quantize_min_wl_m(5_000), 4_096); // 7,500 is nearer 8,192 (dist 692 vs 3,404). assert_eq!(quantize_min_wl_m(7_500), 8_192); } /// T-1162: a value between the new 1,024 band and 0 snaps to whichever is /// nearer — exercises the new finest band specifically, not just the /// pre-existing four. #[test] fn quantize_min_wl_m_snaps_to_new_quarter_floor_band() { // Nearer 1,024 than 0 (dist 224 vs 800). assert_eq!(quantize_min_wl_m(800), 1_024); // Nearer 1,024 than 4,096 (dist 476 vs 2,596). assert_eq!(quantize_min_wl_m(1_500), 1_024); } /// A value far above the coarsest band snaps to the coarsest band, never /// panics or overflows — quantization must be a TOTAL function over all /// u32 input (never trust the wire). #[test] fn quantize_min_wl_m_huge_value_snaps_to_coarsest_band() { assert_eq!(quantize_min_wl_m(u32::MAX), 32_768); assert_eq!(quantize_min_wl_m(1_000_000), 32_768); } /// The mandatory §5 aliasing-closing test: two requests differing only in /// an UNQUANTIZED `min_wl_m` that both fall in the SAME band must share /// ONE cache entry, not two — this is the exact gap §5 flags ("same key, /// different min_wl, would silently collide" becomes "same key, same /// quantized min_wl, correctly coalesce"). #[test] fn two_requests_in_same_min_wl_band_share_one_cache_entry() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader_with_radius("BandBody", 6371.0); let queue = GenerationQueue::with_threads(2); // Both values are nearer 4,096 than any other band (4,000 and 4,300 // both round to 4,096 — see the mid-value test above for the // distance math), so they must land in the SAME quantized band. let req_a = AtlasLayerRequest { body_id: "BandBody".to_string(), up_to: CascadeLayer::Topography, window_center: Some((10, -5)), window_n: 4, window_granularity_v2: None, window_min_wl_m: 4_000, }; let req_b = AtlasLayerRequest { body_id: "BandBody".to_string(), up_to: CascadeLayer::Topography, window_center: Some((10, -5)), window_n: 4, window_granularity_v2: None, window_min_wl_m: 4_300, }; handle_atlas_request( &req_a, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); handle_atlas_request( &req_b, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); for c in completions { if let GenCompletion::WindowDerived { body_id, layer } = c { if body_id == "BandBody" { window_cache.insert( ( body_id, layer.center, layer.n, layer.granularity_v2, layer.min_wl_m, ), *layer, ); } } } assert_eq!( window_cache.len(), 1, "two requests in the SAME quantized min_wl_m band at identical \ (body, center, n, granularity) must share ONE cache entry, not two" ); // Re-request both — each must hit the SAME cached entry and echo the // QUANTIZED band (4,096), not either raw wire value. let resp_a = handle_atlas_request( &req_a, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 2, test_conn_id(), ); let resp_b = handle_atlas_request( &req_b, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 2, test_conn_id(), ); let layer_a = resp_a.district_window.expect("req_a must hit the cache"); let layer_b = resp_b.district_window.expect("req_b must hit the cache"); assert_eq!(layer_a.min_wl_m, 4_096, "echo must be the QUANTIZED band"); assert_eq!(layer_b.min_wl_m, 4_096, "echo must be the QUANTIZED band"); assert_eq!( layer_a, layer_b, "both requests must resolve to the identical cached layer" ); } // ------------------------------------------------------------------- // normalize_window_center (T-1142 — letterbox-click out-of-range bug) // ------------------------------------------------------------------- /// A small-body fixture (500 km radius) whose bounds are hand-checkable: /// `districts_per_circumference = round(TAU*500_000/2048) = 1534`, /// `half_meridian_districts = round(PI*500_000/2048/2) = 383`. The /// literal column/row this fixture uses (`12276`, `3021`) are the exact /// values the reported T-1142 letterbox-click bug sent — at THIS radius /// they genuinely overflow both bounds (at Earth radius, coincidentally, /// they wouldn't — the bounds are tens of thousands of districts wide), /// so this is a faithful small-body reproduction, not just an /// arbitrarily-chosen out-of-range pair. fn small_body_params() -> BodyParams { BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(500.0), ..Default::default() } } /// Out-of-range COLUMN wraps (longitude is periodic) to its in-range /// canonical twin via `rem_euclid` — mirroring `derive_district`'s own /// `(wx / circumference_m).rem_euclid(1.0)` forward map. #[test] fn normalize_window_center_wraps_out_of_range_column() { let params = small_body_params(); // districts_per_circumference = 1534 (hand-computed above). // 12276 rem_euclid 1534 = 4 (12276 = 8*1534 + 4). assert_eq!( 12276_i32.rem_euclid(1534), 4, "sanity: the hand-computed wrap" ); let (nx, ny) = normalize_window_center(¶ms, (12276, 0)); assert_eq!( nx, 4, "out-of-range column wraps to its canonical in-range twin" ); assert_eq!(ny, 0, "an in-range row is untouched"); // The canonical twin normalizes to itself (idempotent). let (nx2, _) = normalize_window_center(¶ms, (4, 0)); assert_eq!(nx2, 4); // Negative columns wrap too (rem_euclid, not truncating rem) — // longitude has no sign discontinuity. let (nx3, _) = normalize_window_center(¶ms, (-1, 0)); assert_eq!( nx3, 1533, "negative column wraps to the top of the range, not a negative remainder" ); } /// Beyond-pole ROW clamps (latitude terminates, does not wrap) to /// `±half_meridian_districts` — mirroring `derive_district`'s own /// `(wy / meridian_m).clamp(-0.5, 0.5)` forward map. This is the /// asymmetry the coordinator's fix explicitly calls out: columns wrap, /// rows clamp — never the other way around. #[test] fn normalize_window_center_clamps_beyond_pole_row() { let params = small_body_params(); // half_meridian_districts = 383 (hand-computed above). let (_, ny) = normalize_window_center(¶ms, (0, 3021)); assert_eq!( ny, 383, "beyond-pole row clamps to the pole boundary, not wraps" ); let (_, ny_neg) = normalize_window_center(¶ms, (0, -9000)); assert_eq!(ny_neg, -383, "clamping is symmetric at both poles"); // A row exactly at the boundary is untouched. let (_, ny_boundary) = normalize_window_center(¶ms, (0, 383)); assert_eq!(ny_boundary, 383); } /// An in-range center (well inside both bounds) is returned UNCHANGED — /// normalization must be a no-op for the overwhelming common case (every /// legitimate click), not just a defensive clamp that happens to also /// preserve valid input. #[test] fn normalize_window_center_leaves_in_range_center_unchanged() { let params = small_body_params(); let center = (100, -50); assert_eq!(normalize_window_center(¶ms, center), center); // (0, 0) — the origin — is always in range regardless of body size. assert_eq!(normalize_window_center(¶ms, (0, 0)), (0, 0)); } /// No-radius bodies (tiny test-body fallback, `body_radius_km: None`) get /// IDENTITY — `derive_district`'s own no-radius branch has no /// wrap/clamp-in-district-space concept (see the function doc); an /// extreme center here is out of this fix's scope by design, not an /// oversight. #[test] fn normalize_window_center_no_radius_is_identity() { let params = BodyParams::default(); // body_radius_km: None let extreme = (999_999, -999_999); assert_eq!(normalize_window_center(¶ms, extreme), extreme); } /// End-to-end (the coordinator's core ask): an out-of-range /// `window_center` and its already-normalized twin, requested through the /// REAL `handle_atlas_request` path, land in the SAME `DistrictWindowCache` /// entry and produce a byte-identical `DistrictWindowLayer` — normalization /// happens BEFORE the cache key is built, so an insane request and its /// sane twin never diverge into separate cache entries (the bug this fix /// closes: the insane request was cached STANDALONE). #[test] fn insane_and_sane_twin_requests_share_one_cache_entry() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader_with_radius("SmallMoon", 500.0); let queue = GenerationQueue::with_threads(1); // The insane request — the reported T-1142 letterbox-click values. let insane_req = AtlasLayerRequest { body_id: "SmallMoon".to_string(), up_to: CascadeLayer::Topography, window_center: Some((12276, 3021)), window_n: 4, window_granularity_v2: None, window_min_wl_m: 0, }; let resp1 = handle_atlas_request( &insane_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); assert!( resp1.district_window.is_none(), "first request — cache miss, DeriveWindow submitted" ); // Wait for the background derive to complete and drain it into the cache // (mirrors handle_atlas_request_clamps_oversized_window_n's pattern). std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); for c in completions { if let GenCompletion::WindowDerived { body_id, layer } = c { if body_id == "SmallMoon" { window_cache.insert( ( body_id, layer.center, layer.n, layer.granularity_v2, layer.min_wl_m, ), *layer, ); } } } // Exactly ONE entry must exist in the window cache after the insane // request's derive completes — normalization means it was keyed on // the canonical (4, 383), not the raw (12276, 3021). assert_eq!( window_cache.len(), 1, "the insane request's derive must be cached under its NORMALIZED key" ); // The "sane twin" — the already-normalized canonical center — hits // the SAME cache entry the insane request just populated. let sane_twin_req = AtlasLayerRequest { body_id: "SmallMoon".to_string(), up_to: CascadeLayer::Topography, window_center: Some((4, 383)), // the hand-computed canonical twin window_n: 4, window_granularity_v2: None, window_min_wl_m: 0, }; let resp2 = handle_atlas_request( &sane_twin_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 2, test_conn_id(), ); let twin_layer = resp2.district_window.expect( "the sane twin must hit the cache the insane request populated — no new derive needed", ); assert_eq!( window_cache.len(), 1, "the sane twin must NOT create a second cache entry" ); // Re-request the ORIGINAL insane center too — it must ALSO now hit // the same populated cache entry (both requests normalize to the // same key). let resp3 = handle_atlas_request( &insane_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 3, test_conn_id(), ); let insane_layer_second_try = resp3 .district_window .expect("the insane request, re-requested, must ALSO hit the shared cache entry"); assert_eq!( twin_layer, insane_layer_second_try, "the insane request and its sane twin must resolve to a BYTE-IDENTICAL layer" ); assert_eq!( window_cache.len(), 1, "still exactly one entry — neither re-request created a second one" ); } /// **MANDATORY aliasing regression (T-1150, design doc §3's flagged /// aliasing risk):** a granularity-4 (quarter) request and a /// granularity-1 (district) request at the IDENTICAL `(body, center, n)` /// must produce DISTINCT `DistrictWindowCache` entries and correct /// per-granularity payloads — never silently alias onto the same slot /// and serve one rung's data for the other's request. #[test] fn granularity_4_and_granularity_1_requests_produce_distinct_cache_entries() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader_with_radius("AliasBody", 6371.0); // 3 threads: BOTH DeriveWindow items (district + quarter) need to // dispatch concurrently with the AnalyzeBody item the first request's // whole-body cache miss also enqueues (handle_atlas_request always // fires an AnalyzeBody alongside the window derive on a cold body) — // 2 threads left one DeriveWindow stuck behind AnalyzeBody within the // single drain_completions() call below. let queue = GenerationQueue::with_threads(3); let center = Some((10, -5)); let n = 4u32; let district_req = AtlasLayerRequest { body_id: "AliasBody".to_string(), up_to: CascadeLayer::Topography, window_center: center, window_n: n, window_granularity_v2: None, window_min_wl_m: 0, }; let quarter_req = AtlasLayerRequest { body_id: "AliasBody".to_string(), up_to: CascadeLayer::Topography, window_center: center, window_n: n, window_granularity_v2: Some(WindowGranularity::Quarter), window_min_wl_m: 0, }; // Fire both requests — same (body, center, n), different granularity. handle_atlas_request( &district_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); handle_atlas_request( &quarter_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); for c in completions { if let GenCompletion::WindowDerived { body_id, layer } = c { if body_id == "AliasBody" { window_cache.insert( ( body_id, layer.center, layer.n, layer.granularity_v2, layer.min_wl_m, ), *layer, ); } } } assert_eq!( window_cache.len(), 2, "district and quarter requests at the SAME (body, center, n) must occupy \ TWO distinct cache entries, not alias onto one" ); // Re-request both — each must now hit ITS OWN cached entry and return // the CORRECT per-granularity payload (not the other rung's data). let district_resp = handle_atlas_request( &district_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 2, test_conn_id(), ); let quarter_resp = handle_atlas_request( &quarter_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 2, test_conn_id(), ); let district_layer = district_resp .district_window .expect("district request must hit its own cached entry"); let quarter_layer = quarter_resp .district_window .expect("quarter request must hit its own cached entry"); assert_eq!(district_layer.granularity_v2, WindowGranularity::District); assert_eq!(quarter_layer.granularity_v2, WindowGranularity::Quarter); // n echoes the DISTRICT extent unchanged at both granularities // (design doc §2: "the window's n stays the DISTRICT extent"). assert_eq!(district_layer.n, n); assert_eq!(quarter_layer.n, n); // The derived CELL GRID differs: n×n at district, (4n)×(4n) at quarter. assert_eq!(district_layer.morphology.len(), (n * n) as usize); assert_eq!( quarter_layer.morphology.len(), (n * WINDOW_GRANULARITY_QUARTER * n * WINDOW_GRANULARITY_QUARTER) as usize ); // Correct per-granularity payload, not the other rung's data reused: // the quarter grid must show finer per-cell VARIATION than a naive // 4x-repeat of the district grid would (Option B — full // reclassification at 512 m, not a coarser-cell interpolation). let quarter_elev_range = { let min = quarter_layer.elev_q.iter().min().copied().unwrap_or(0); let max = quarter_layer.elev_q.iter().max().copied().unwrap_or(0); max - min }; assert!( quarter_elev_range > 0, "quarter-granularity window must show real sub-district elevation \ variation, not a blocky repeat of the district cells" ); } /// **MANDATORY aliasing regression for the T-1152 coarser rung** — the /// SAME discipline `granularity_4_and_granularity_1_requests_produce_distinct_cache_entries` /// established for finer-than-district rungs, extended to `Region` /// (T-1150 design doc §3's aliasing risk, generalized by the T-1152 /// wire-contract note: "mirror the T-1150 aliasing tests for at least /// one coarser rung"). A `Region`-granularity request and a /// `District`-granularity request at the IDENTICAL `(body, center, n)` /// must produce DISTINCT cache entries, distinct payload shapes, and the /// clamp/echo contract must hold at the coarse rung too (PR #191 C1 /// lesson generalized: the client's mirror of `clamp_window_n_v2` MUST /// be derivable from the same constants this test exercises). #[test] fn region_and_district_requests_produce_distinct_cache_entries() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader_with_radius("OrbitalAliasBody", 6371.0); // See the district/quarter alias test above for the thread-count // rationale (AnalyzeBody + two DeriveWindow items must all be able to // dispatch concurrently within one drain_completions() call). let queue = GenerationQueue::with_threads(3); let center = Some((10, -5)); let n = 4u32; let district_req = AtlasLayerRequest { body_id: "OrbitalAliasBody".to_string(), up_to: CascadeLayer::Topography, window_center: center, window_n: n, window_granularity_v2: None, window_min_wl_m: 0, }; let region_req = AtlasLayerRequest { body_id: "OrbitalAliasBody".to_string(), up_to: CascadeLayer::Topography, window_center: center, window_n: n, window_granularity_v2: Some(WindowGranularity::Region), window_min_wl_m: 0, }; handle_atlas_request( &district_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); handle_atlas_request( ®ion_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); for c in completions { if let GenCompletion::WindowDerived { body_id, layer } = c { if body_id == "OrbitalAliasBody" { window_cache.insert( ( body_id, layer.center, layer.n, layer.granularity_v2, layer.min_wl_m, ), *layer, ); } } } assert_eq!( window_cache.len(), 2, "district and region requests at the SAME (body, center, n) must occupy \ TWO distinct cache entries, not alias onto one" ); let district_resp = handle_atlas_request( &district_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 2, test_conn_id(), ); let region_resp = handle_atlas_request( ®ion_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 2, test_conn_id(), ); let district_layer = district_resp .district_window .expect("district request must hit its own cached entry"); let region_layer = region_resp .district_window .expect("region request must hit its own cached entry"); assert_eq!(district_layer.granularity_v2, WindowGranularity::District); assert_eq!(region_layer.granularity_v2, WindowGranularity::Region); assert_ne!(region_layer.granularity_v2, district_layer.granularity_v2); // n echoes the DISTRICT extent unchanged (design doc §2), same as // every other rung — the derived CELL GRID is what differs. assert_eq!(district_layer.n, n); assert_eq!(region_layer.n, n); assert_eq!(district_layer.morphology.len(), (n * n) as usize); // n=4 districts is far under one region's 100-district side, so // cell_grid_side rounds down to the minimum 1x1 region cell — // exercising the "region divides, doesn't multiply" cell-count // relationship WindowGranularity::cell_grid_side documents. assert_eq!( region_layer.morphology.len(), 1, "n=4 districts is far under one region's 100-district side; \ cell_grid_side must round down to a single region cell, not zero \ and not a district-sized grid" ); } /// The clamp/echo contract at the coarse (Region) rung (T-1152, the PR /// #191 C1 lesson generalized): a region request whose `n` would derive /// MORE than `sqrt(WIRE_CAP_CELLS)` region cells across must clamp `n` /// down and echo the CLAMPED value — Stig's client-side mirror of /// `clamp_window_n_v2` must be derivable from /// `DISTRICT_WINDOW_MAX_N_REGION`/`WIRE_CAP_CELLS`/`DISTRICTS_PER_REGION` /// alone, exactly as `_clamp_window_n_mirror()` already mirrors /// `clamp_window_n` for the finer rungs. #[test] fn region_request_oversized_n_clamps_and_echoes_clamped_n() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader_with_radius("OrbitalCapBody", 6371.0); let queue = GenerationQueue::with_threads(1); // Far over DISTRICT_WINDOW_MAX_N_REGION (6,400) — must clamp, never // trust the wire (same discipline as the district-rung oversized-n test). let oversized_region_req = AtlasLayerRequest { body_id: "OrbitalCapBody".to_string(), up_to: CascadeLayer::Topography, window_center: Some((0, 0)), window_n: DISTRICT_WINDOW_MAX_N_REGION * 10, window_granularity_v2: Some(WindowGranularity::Region), window_min_wl_m: 0, }; let resp = handle_atlas_request( &oversized_region_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); assert!(resp.district_window.is_none(), "first request — cache miss"); std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); let window_completion = completions.into_iter().find_map(|c| { if let GenCompletion::WindowDerived { body_id, layer } = c { if body_id == "OrbitalCapBody" { return Some(layer); } } None }); let layer = window_completion.expect("DeriveWindow must complete for OrbitalCapBody"); assert_eq!(layer.granularity_v2, WindowGranularity::Region); assert!( layer.n <= DISTRICT_WINDOW_MAX_N_REGION, "echoed n must be clamped to DISTRICT_WINDOW_MAX_N_REGION, not the raw oversized value" ); let side = layer.granularity_v2.cell_grid_side(layer.n); assert!( (side as u32) * (side as u32) <= WIRE_CAP_CELLS, "clamped cell count must never exceed WIRE_CAP_CELLS at Region granularity either" ); } /// The echoed `center` on `DistrictWindowLayer` is the NORMALIZED value, /// not the raw wire value — the client's D-227 staleness guard (D-226 /// T-1124 amendment §2) must see what was ACTUALLY derived, so it can /// correctly match this response against its own (now also normalized, /// per the T-1142 fix note to the client team) cache key. #[test] fn echoed_center_is_the_normalized_value_not_the_raw_wire_value() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader_with_radius("SmallMoon2", 500.0); let queue = GenerationQueue::with_threads(1); let insane_req = AtlasLayerRequest { body_id: "SmallMoon2".to_string(), up_to: CascadeLayer::Topography, window_center: Some((12276, 3021)), // raw, out-of-range window_n: 4, window_granularity_v2: None, window_min_wl_m: 0, }; handle_atlas_request( &insane_req, &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); let window_completion = completions.into_iter().find_map(|c| { if let GenCompletion::WindowDerived { body_id, layer } = c { if body_id == "SmallMoon2" { return Some(layer); } } None }); let layer = window_completion.expect("DeriveWindow must complete"); assert_eq!( layer.center, (4, 383), "the completed/echoed layer.center is the NORMALIZED value, not the raw (12276, 3021)" ); assert_ne!( layer.center, (12276, 3021), "the raw out-of-range wire value must never be echoed back" ); } /// `serve_district_window` returns `None` (no window requested) when the /// request carries no `window_center` — the common case, and the ONLY /// path every pre-T-1137 caller takes (wire back-compat: an old client's /// `{body_id, up_to}` frame decodes with `window_center: None` via /// `#[serde(default)]`). #[test] fn handle_atlas_request_no_window_center_leaves_district_window_none() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let (_db, resolver) = empty_resolver(); let queue = GenerationQueue::with_threads(1); let resp = handle_atlas_request( &req("GJ1c"), // window_center: None &mut cache, &mut window_cache, &queue, &resolver, None, None, 42, 1, test_conn_id(), ); assert!(resp.district_window.is_none()); assert!( window_cache.is_empty(), "no window requested → no DeriveWindow submitted, cache stays empty" ); } /// `AtlasLayerResponse.district_window` survives a MessagePack round trip /// (mirrors the existing `atlas_layer_response_with_new_layers_round_trips_msgpack` /// precedent) — the wire shape every field the amendment specifies: /// echoed `center`/`n`, all six parallel arrays including the /// `REGION_TEMP_NONE_DC` sentinel and `VegetationClass::Marine = 6`. #[test] fn district_window_layer_round_trips_msgpack_inside_response() { let window = DistrictWindowLayer { center: (10, -5), n: 2, granularity_v2: WindowGranularity::District, min_wl_m: 0, morphology: vec![0, 8, 14, 16], elev_q: vec![0, 45, 98, 60], temp_dc: vec![205, 150, REGION_TEMP_NONE_DC, 80], moisture_q: vec![90, 55, 0, 100], vegetation: vec![6, 3, 0, 5], // includes Marine = 6 glaciation: vec![0, 0, 4, 1], courses: Vec::new(), }; let resp = AtlasLayerResponse { body_id: "GJ1c".into(), status: AtlasLayerStatus::Ready, layer1: None, district_grid: None, road_graph: None, settlements: None, region_grid: None, district_window: Some(window.clone()), quarter_footprints: None, }; let bytes = rmp_serde::to_vec_named(&resp).expect("encode"); let decoded: AtlasLayerResponse = rmp_serde::from_slice(&bytes).expect("decode"); let dw = decoded .district_window .expect("district_window survives round trip"); assert_eq!(dw, window); assert_eq!(dw.center, (10, -5)); assert_eq!(dw.n, 2); assert_eq!( dw.temp_dc[2], REGION_TEMP_NONE_DC, "airless sentinel preserved" ); assert_eq!(dw.vegetation[0], 6, "Marine discriminant preserved"); } /// Wire back-compat (D-226 T-1124 amendment §1): a pre-T-1137 request /// frame carrying only `{body_id, up_to}` — no `window_center`/`window_n` /// keys at all — decodes cleanly via `#[serde(default)]`, byte-unchanged /// for every existing caller. #[test] fn old_request_frame_without_window_fields_decodes_with_none() { #[derive(serde::Serialize)] struct OldAtlasLayerRequest { body_id: String, up_to: CascadeLayer, } let old = OldAtlasLayerRequest { body_id: "GJ1c".into(), up_to: CascadeLayer::Topography, }; let bytes = rmp_serde::to_vec_named(&old).expect("encode old-shape frame"); let decoded: AtlasLayerRequest = rmp_serde::from_slice(&bytes).expect("decode"); assert_eq!(decoded.body_id, "GJ1c"); assert_eq!(decoded.up_to, CascadeLayer::Topography); assert_eq!(decoded.window_center, None); assert_eq!(decoded.window_n, 0); assert_eq!( decoded.window_granularity_v2, None, "T-1152/T-1159: absent window_granularity_v2 decodes to None (district), byte-compatible" ); assert_eq!( decoded.window_min_wl_m, 0, "T-1150: absent window_min_wl_m decodes to 0 (no cutoff), byte-compatible" ); } /// T-1119: `build_quarter_footprint_layer` returns `None` when no /// settlement's quarter skeleton has been generated (`state.quarters` /// empty), mirroring `build_district_grid`/`build_region_grid`'s /// "unrun layer → None" contract. #[test] fn quarter_footprint_layer_none_when_quarters_empty() { let state = blank_state("GJ1c"); assert!(build_quarter_footprint_layer(&state, 42).is_none()); } /// A `BlockSkeleton` fixture builder for quarter-footprint tests — only /// the fields the aggregate reads are wired; the rest default. fn block( zoning: crate::simulation::generator::ZoningType, district_type: DistrictType, density_pct: u8, landmark: Option, ) -> crate::simulation::generator::BlockSkeleton { crate::simulation::generator::BlockSkeleton { zoning, district_type, density_pct, landmark, ..Default::default() } } /// T-1119: a populated quarter aggregates correctly — the density mean, /// the dominant-mode fields (including a tie resolving to the lowest /// declaration-order variant per the D-226 T-1112 amendment §1), the /// landmark count, and the corridor count. #[test] fn quarter_footprint_layer_aggregates_populated_quarter() { use crate::atlas::attractor_matching::CityPlacement; use crate::simulation::generator::{ ArrangementPattern, FoundingOrientation, PoliticalArchetype, ZoningType, }; let mut state = blank_state("GJ1c"); let placement = CityPlacement { city_id: 7, name: "Millbrook".into(), position: (30, 40), attractor_type: AttractorType::ValleyFloor, score: 500, synthetic: false, political_archetype: PoliticalArchetype::Commission, arrangement_pattern: ArrangementPattern::RadialCore, founding_orientation: FoundingOrientation::Cardinal, population: 200_000, is_capital: false, is_standalone_hq: false, }; state.placements = vec![placement.clone()]; let world_seed = 42; let quarter_id = SeedChain::for_body(world_seed, "GJ1c") .derive(SeedDomain::Layer4Quarter, placement.city_id) .seed(); // 16 blocks: 10 Commercial/Commercial, 6 Industrial/Industrial — a // clean (non-tied) mode on both district_type and zoning, plus a // known density mean and landmark/corridor counts. let mut blocks: [[crate::simulation::generator::BlockSkeleton; 4]; 4] = Default::default(); let mut flat: Vec<&mut crate::simulation::generator::BlockSkeleton> = blocks.iter_mut().flatten().collect(); for (i, b) in flat.iter_mut().enumerate() { if i < 10 { **b = block(ZoningType::Commercial, DistrictType::Commercial, 60, None); } else { **b = block( ZoningType::Industrial, DistrictType::Industrial, 20, Some("landmark".to_string()), ); } } // 3 landmarks among the Industrial blocks (indices 10, 11, 12). *flat[10] = block( ZoningType::Industrial, DistrictType::Industrial, 20, Some("A".to_string()), ); *flat[11] = block( ZoningType::Industrial, DistrictType::Industrial, 20, Some("B".to_string()), ); *flat[12] = block( ZoningType::Industrial, DistrictType::Industrial, 20, Some("C".to_string()), ); for b in flat.iter_mut().skip(13) { **b = block(ZoningType::Industrial, DistrictType::Industrial, 20, None); } // (10 * 60 + 6 * 20) / 16 = 720 / 16 = 45. state.quarters.insert( quarter_id, crate::simulation::generator::QuarterWorldState { skeleton: crate::simulation::generator::QuarterSkeleton { quarter_id, blocks, corridors: vec![ crate::simulation::generator::CorridorSpine { from: 0, to: 1, path: vec![(0, 0), (4, 4)], }, crate::simulation::generator::CorridorSpine { from: 1, to: 2, path: vec![(4, 4), (8, 8)], }, ], ..Default::default() }, block_tags: Default::default(), }, ); let layer = build_quarter_footprint_layer(&state, world_seed).expect("populated quarters → Some"); let entry = layer.entries.get(&7).expect("city_id 7 entry present"); assert_eq!(entry.city_id, 7); assert_eq!(entry.density_avg_pct, 45); assert_eq!(entry.dominant_district_type, DistrictType::Commercial); assert_eq!(entry.dominant_zoning, ZoningType::Commercial); assert_eq!(entry.landmark_count, 3); assert_eq!(entry.corridor_count, 2); } /// T-1119: the mode tie-break resolves to the lowest declaration-order /// variant (the `Ord` derive), per the D-226 T-1112 amendment §1's /// explicit tie rule — this is the reason `ZoningType` gained /// `PartialOrd`/`Ord` in this same ticket. #[test] fn quarter_footprint_layer_tie_breaks_by_declaration_order() { use crate::atlas::attractor_matching::CityPlacement; use crate::simulation::generator::{ ArrangementPattern, FoundingOrientation, PoliticalArchetype, ZoningType, }; let mut state = blank_state("GJ1c"); let placement = CityPlacement { city_id: 3, name: "Farmstead Rell".into(), position: (50, 60), attractor_type: AttractorType::PlainCenter, score: 100, synthetic: false, political_archetype: PoliticalArchetype::Commission, arrangement_pattern: ArrangementPattern::RadialCore, founding_orientation: FoundingOrientation::Cardinal, population: 8_000, is_capital: false, is_standalone_hq: false, }; state.placements = vec![placement.clone()]; let world_seed = 99; let quarter_id = SeedChain::for_body(world_seed, "GJ1c") .derive(SeedDomain::Layer4Quarter, placement.city_id) .seed(); // 8 blocks Industrial, 8 blocks Commercial — an exact tie. Declaration // order on both DistrictType and ZoningType lists Commercial before // Industrial, so the tie-broken dominant must be Commercial on both. let mut blocks: [[crate::simulation::generator::BlockSkeleton; 4]; 4] = Default::default(); for (i, b) in blocks.iter_mut().flatten().enumerate() { *b = if i < 8 { block(ZoningType::Industrial, DistrictType::Industrial, 50, None) } else { block(ZoningType::Commercial, DistrictType::Commercial, 50, None) }; } state.quarters.insert( quarter_id, crate::simulation::generator::QuarterWorldState { skeleton: crate::simulation::generator::QuarterSkeleton { quarter_id, blocks, ..Default::default() }, block_tags: Default::default(), }, ); let layer = build_quarter_footprint_layer(&state, world_seed).expect("populated quarters → Some"); let entry = layer.entries.get(&3).expect("city_id 3 entry present"); assert_eq!( entry.dominant_district_type, DistrictType::Commercial, "tie resolves to Commercial (declared before Industrial)" ); assert_eq!( entry.dominant_zoning, ZoningType::Commercial, "tie resolves to Commercial (declared before Industrial)" ); } /// T-1119: a placement whose deterministically-derived `quarter_id` is /// NOT yet in `state.quarters` (skeleton generation is async, dispatched /// after the body's own snapshot is cached — D-226 T-1112 amendment §1) /// is skipped, not defaulted. `entries` is a subset of `placements`. #[test] fn quarter_footprint_layer_skips_placement_without_matching_quarter() { use crate::atlas::attractor_matching::CityPlacement; use crate::simulation::generator::{ ArrangementPattern, FoundingOrientation, PoliticalArchetype, ZoningType, }; let mut state = blank_state("GJ1c"); let has_quarter = CityPlacement { city_id: 1, name: "Port Aldren".into(), position: (12, 58), attractor_type: AttractorType::CoastalAccess, score: 1000, synthetic: false, political_archetype: PoliticalArchetype::Commission, arrangement_pattern: ArrangementPattern::RadialCore, founding_orientation: FoundingOrientation::Cardinal, population: 2_000_000, is_capital: true, is_standalone_hq: false, }; let no_quarter_yet = CityPlacement { city_id: 2, name: "Farmstead Rell".into(), ..has_quarter.clone() }; state.placements = vec![has_quarter.clone(), no_quarter_yet]; let world_seed = 42; let quarter_id = SeedChain::for_body(world_seed, "GJ1c") .derive(SeedDomain::Layer4Quarter, has_quarter.city_id) .seed(); let mut blocks: [[crate::simulation::generator::BlockSkeleton; 4]; 4] = Default::default(); for b in blocks.iter_mut().flatten() { *b = block(ZoningType::Mixed, DistrictType::MixedUse, 10, None); } state.quarters.insert( quarter_id, crate::simulation::generator::QuarterWorldState { skeleton: crate::simulation::generator::QuarterSkeleton { quarter_id, blocks, ..Default::default() }, block_tags: Default::default(), }, ); let layer = build_quarter_footprint_layer(&state, world_seed).expect("populated quarters → Some"); assert_eq!( layer.entries.len(), 1, "only the placement with a matching quarter gets an entry" ); assert!(layer.entries.contains_key(&1)); assert!( !layer.entries.contains_key(&2), "city_id 2 has no generated quarter yet — must be absent, not defaulted" ); } /// T-1119 (D-010): building the layer twice from identical state produces /// byte-identical output — the `city_id → quarter_id` derivation and the /// mode aggregation are pure functions of their inputs. #[test] fn quarter_footprint_layer_is_deterministic() { use crate::atlas::attractor_matching::CityPlacement; use crate::simulation::generator::{ ArrangementPattern, FoundingOrientation, PoliticalArchetype, ZoningType, }; let mut state = blank_state("GJ1c"); let placement = CityPlacement { city_id: 5, name: "Groombridge".into(), position: (1, 1), attractor_type: AttractorType::PlainCenter, score: 300, synthetic: false, political_archetype: PoliticalArchetype::Commission, arrangement_pattern: ArrangementPattern::RadialCore, founding_orientation: FoundingOrientation::Cardinal, population: 60_000, is_capital: false, is_standalone_hq: false, }; state.placements = vec![placement.clone()]; let world_seed = 7; let quarter_id = SeedChain::for_body(world_seed, "GJ1c") .derive(SeedDomain::Layer4Quarter, placement.city_id) .seed(); let mut blocks: [[crate::simulation::generator::BlockSkeleton; 4]; 4] = Default::default(); for (i, b) in blocks.iter_mut().flatten().enumerate() { *b = block( ZoningType::Residential, DistrictType::Residential, (i as u8) * 5, None, ); } state.quarters.insert( quarter_id, crate::simulation::generator::QuarterWorldState { skeleton: crate::simulation::generator::QuarterSkeleton { quarter_id, blocks, ..Default::default() }, block_tags: Default::default(), }, ); let a = build_quarter_footprint_layer(&state, world_seed); let b = build_quarter_footprint_layer(&state, world_seed); assert_eq!(a, b, "identical state must produce identical output"); } /// A blank `BodyWorldState` for tests that only care about one field — /// callers overwrite `placements`/`road_graph`/etc. as needed. fn blank_state(body_id: &str) -> BodyWorldState { BodyWorldState { body_id: body_id.into(), heightmap: vec![], heightmap_width: 16, heightmap_height: 8, sea_level: 0.3, river_network: RiverNetwork::default(), drainage_basins: vec![], attractors: vec![], feature_names: vec![], placements: vec![], road_graph: crate::atlas::road_graph::RoadGraph::default(), quarters: std::collections::BTreeMap::new(), districts: std::collections::BTreeMap::new(), regions: std::collections::BTreeMap::new(), last_accessed: 0, } } /// T-960 §1: `build_road_graph_layer` trims the internal `RoadGraph` (drops /// `degree`/`parent_edge`/`length_cells`) while keeping everything a /// planetary-map overlay needs (positions, kind, polyline, maintenance, /// rail flag, named-route id). #[test] fn road_graph_layer_built_from_cached_state() { use crate::atlas::road_graph::{RoadEdge, RoadGraph, RoadNode}; use crate::simulation::generator::MaintenanceAuthority; let mut state = blank_state("GJ1c"); state.road_graph = RoadGraph { nodes: vec![ RoadNode { city_id: Some(1), position: (10, 20), kind: RoadNodeKind::Settlement, degree: 1, parent_edge: None, is_hub: false, }, RoadNode { city_id: None, position: (15, 25), kind: RoadNodeKind::Waypoint, degree: 0, parent_edge: Some(0), is_hub: false, }, ], edges: vec![RoadEdge { from: 0, to: 1, path: vec![(10, 20), (15, 25)], length_cells: 20, // internal routing-grid measure — dropped maintenance: MaintenanceAuthority::Trade, named_route_id: Some("split/hwy-1".into()), is_rail: true, }], }; let layer = build_road_graph_layer(&state).expect("populated road_graph → Some"); assert_eq!(layer.nodes.len(), 2); assert_eq!(layer.nodes[0].position, (10, 20)); assert_eq!(layer.nodes[0].kind, RoadNodeKind::Settlement); assert_eq!(layer.nodes[0].city_id, Some(1)); assert_eq!(layer.nodes[1].kind, RoadNodeKind::Waypoint); assert_eq!(layer.nodes[1].city_id, None); assert_eq!(layer.edges.len(), 1); assert_eq!(layer.edges[0].path, vec![(10, 20), (15, 25)]); assert_eq!(layer.edges[0].maintenance, MaintenanceAuthority::Trade); assert!(layer.edges[0].is_rail); assert_eq!( layer.edges[0].named_route_id.as_deref(), Some("split/hwy-1") ); // Layer hasn't run (or zero settlements) → None, mirroring district_grid. let unrun = blank_state("GJ1c"); assert!(build_road_graph_layer(&unrun).is_none()); } /// T-960 §2: `build_settlement_layer` derives `size_class` from population /// using the D-211 Tier A/B cutoffs, threads `is_capital` straight through, /// and derives `is_port` cheaply from the anchoring attractor type. #[test] fn settlement_layer_built_from_cached_placements() { use crate::atlas::attractor_matching::CityPlacement; use crate::simulation::generator::{ ArrangementPattern, FoundingOrientation, PoliticalArchetype, }; let mk = |city_id: u64, name: &str, pos: (u16, u16), population: i64, is_capital: bool, attractor_type: AttractorType| CityPlacement { city_id, name: name.to_string(), position: pos, attractor_type, score: 1000, synthetic: false, political_archetype: PoliticalArchetype::Commission, arrangement_pattern: ArrangementPattern::RadialCore, founding_orientation: FoundingOrientation::Cardinal, population, is_capital, is_standalone_hq: false, }; let mut state = blank_state("GJ1c"); state.placements = vec![ mk( 1, "Port Aldren", (12, 58), 2_000_000, true, AttractorType::CoastalAccess, ), mk( 2, "Millbrook", (30, 40), 200_000, false, AttractorType::ValleyFloor, ), mk( 3, "Farmstead Rell", (50, 60), 8_000, false, AttractorType::PlainCenter, ), ]; let layer = build_settlement_layer(&state).expect("populated placements → Some"); assert_eq!(layer.settlements.len(), 3); let capital = layer.settlements.iter().find(|s| s.city_id == 1).unwrap(); assert_eq!(capital.name, "Port Aldren"); assert_eq!(capital.position, (12, 58)); assert_eq!(capital.size_class, SettlementSizeClass::Major); assert!(capital.is_capital); assert!(capital.is_port, "CoastalAccess must read as a port"); let mid = layer.settlements.iter().find(|s| s.city_id == 2).unwrap(); assert_eq!(mid.size_class, SettlementSizeClass::Standard); assert!(!mid.is_capital); assert!(!mid.is_port, "ValleyFloor is not a port attractor"); let small = layer.settlements.iter().find(|s| s.city_id == 3).unwrap(); assert_eq!(small.size_class, SettlementSizeClass::Minor); assert!(!small.is_port); // No placements → None. let unrun = blank_state("GJ1c"); assert!(build_settlement_layer(&unrun).is_none()); } /// T-960 / T-1119: the new layers survive a MessagePack round trip inside /// `AtlasLayerResponse` — the same wire path the bridge uses /// (`rmp_serde::to_vec_named` / `from_slice`, matching `layer1`/ /// `district_grid`'s existing serialization). #[test] fn atlas_layer_response_with_new_layers_round_trips_msgpack() { use crate::atlas::attractor_matching::CityPlacement; use crate::atlas::road_graph::{RoadEdge, RoadGraph, RoadNode}; use crate::simulation::generator::{ ArrangementPattern, BlockSkeleton, DistrictType, FoundingOrientation, MaintenanceAuthority, PoliticalArchetype, QuarterSkeleton, QuarterWorldState, ZoningType, }; let mut state = blank_state("GJ1c"); state.placements = vec![CityPlacement { city_id: 1, name: "Port Aldren".into(), position: (12, 58), attractor_type: AttractorType::CoastalAccess, score: 1000, synthetic: false, political_archetype: PoliticalArchetype::Commission, arrangement_pattern: ArrangementPattern::RadialCore, founding_orientation: FoundingOrientation::Cardinal, population: 2_000_000, is_capital: true, is_standalone_hq: false, }]; state.road_graph = RoadGraph { nodes: vec![RoadNode { city_id: Some(1), position: (12, 58), kind: RoadNodeKind::Settlement, degree: 0, parent_edge: None, is_hub: false, }], edges: vec![RoadEdge { from: 0, to: 0, path: vec![(12, 58)], length_cells: 0, maintenance: MaintenanceAuthority::Administrative, named_route_id: None, is_rail: false, }], }; let world_seed = 42; let quarter_id = SeedChain::for_body(world_seed, "GJ1c") .derive(SeedDomain::Layer4Quarter, 1) .seed(); let mut block = BlockSkeleton { zoning: ZoningType::Commercial, district_type: DistrictType::Commercial, density_pct: 40, ..Default::default() }; block.position = (0, 0); state.quarters.insert( quarter_id, QuarterWorldState { skeleton: QuarterSkeleton { quarter_id, blocks: std::array::from_fn(|_| std::array::from_fn(|_| block.clone())), ..Default::default() }, block_tags: Default::default(), }, ); let resp = AtlasLayerResponse { body_id: "GJ1c".into(), status: AtlasLayerStatus::Ready, layer1: None, district_grid: None, road_graph: build_road_graph_layer(&state), settlements: build_settlement_layer(&state), region_grid: build_region_grid(&state), district_window: None, quarter_footprints: build_quarter_footprint_layer(&state, world_seed), }; let bytes = rmp_serde::to_vec_named(&resp).expect("encode"); let decoded: AtlasLayerResponse = rmp_serde::from_slice(&bytes).expect("decode"); assert_eq!(decoded.body_id, "GJ1c"); let rg = decoded.road_graph.expect("road_graph survives round trip"); assert_eq!(rg.nodes[0].position, (12, 58)); assert_eq!( rg.edges[0].maintenance, MaintenanceAuthority::Administrative ); let qf = decoded .quarter_footprints .expect("quarter_footprints survives round trip"); let entry = qf.entries.get(&1).expect("city_id 1 entry present"); assert_eq!(entry.density_avg_pct, 40); assert_eq!(entry.dominant_district_type, DistrictType::Commercial); assert_eq!(entry.dominant_zoning, ZoningType::Commercial); let settlements = decoded .settlements .expect("settlements survives round trip"); assert_eq!(settlements.settlements[0].name, "Port Aldren"); assert_eq!( settlements.settlements[0].size_class, SettlementSizeClass::Major ); assert!(settlements.settlements[0].is_capital); assert!(settlements.settlements[0].is_port); } fn req(body_id: &str) -> AtlasLayerRequest { AtlasLayerRequest { body_id: body_id.to_string(), up_to: CascadeLayer::Topography, window_center: None, window_n: 0, window_granularity_v2: None, window_min_wl_m: 0, } } fn test_conn_id() -> ConnectionId { ConnectionId(1) } const REL: &str = "wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"; /// systems.db with one bodies row, + a base root containing a tiny 16-bit /// heightmap PNG at the body's terrain_reference. Returns (db, resolver). fn resolver_with_body(body_id: &str) -> (PathBuf, BodySourceResolver) { let n = SEQ.fetch_add(1, Ordering::Relaxed); let db = std::env::temp_dir().join(format!("sr_proxy_{}_{n}.db", std::process::id())); let _ = std::fs::remove_file(&db); let conn = Connection::open(&db).unwrap(); conn.execute( "CREATE TABLE bodies (body_id TEXT PRIMARY KEY, terrain_reference TEXT)", [], ) .unwrap(); conn.execute( "INSERT INTO bodies (body_id, terrain_reference) VALUES (?1, ?2)", rusqlite::params![body_id, REL], ) .unwrap(); let root = std::env::temp_dir().join(format!("sr_proxyroot_{}_{n}", std::process::id())); write_tiny_heightmap(&root.join(REL)); let resolver = BodySourceResolver::open(&db, vec![root]).unwrap(); (db, resolver) } fn write_tiny_heightmap(path: &Path) { use std::io::BufWriter; std::fs::create_dir_all(path.parent().unwrap()).unwrap(); let file = std::fs::File::create(path).unwrap(); let mut enc = png::Encoder::new(BufWriter::new(file), 32, 16); enc.set_color(png::ColorType::Grayscale); enc.set_depth(png::BitDepth::Sixteen); let mut w = enc.write_header().unwrap(); let data: Vec = (0..32u32 * 16) .flat_map(|i| (((i * 600) % 65536) as u16).to_be_bytes()) .collect(); w.write_image_data(&data).unwrap(); } fn empty_resolver() -> (PathBuf, BodySourceResolver) { let n = SEQ.fetch_add(1, Ordering::Relaxed); let db = std::env::temp_dir().join(format!("sr_proxye_{}_{n}.db", std::process::id())); let _ = std::fs::remove_file(&db); let conn = Connection::open(&db).unwrap(); conn.execute( "CREATE TABLE bodies (body_id TEXT, terrain_reference TEXT)", [], ) .unwrap(); let resolver = BodySourceResolver::open(&db, vec![std::env::temp_dir()]).unwrap(); (db, resolver) } #[test] fn cache_hit_is_ready() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); cache.insert(BodyWorldState { body_id: "GJ1c".into(), heightmap: vec![0.0; 4], heightmap_width: 2, heightmap_height: 2, sea_level: 0.3, river_network: RiverNetwork::default(), drainage_basins: vec![], attractors: vec![], feature_names: vec![], placements: vec![], road_graph: crate::atlas::road_graph::RoadGraph::default(), quarters: std::collections::BTreeMap::new(), districts: std::collections::BTreeMap::new(), regions: std::collections::BTreeMap::new(), last_accessed: 0, }); let (_db, resolver) = empty_resolver(); let queue = GenerationQueue::with_threads(1); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let resp = handle_atlas_request( &req("GJ1c"), &mut cache, &mut window_cache, &queue, &resolver, None, None, 42, 1, test_conn_id(), ); assert_eq!(resp.status, AtlasLayerStatus::Ready); assert_eq!(resp.layer1.expect("layer1").body_id, "GJ1c"); } #[test] fn cache_miss_enqueues_and_pends_then_analyzes() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let (_db, resolver) = resolver_with_body("GJ1c"); let queue = GenerationQueue::with_threads(1); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let resp = handle_atlas_request( &req("GJ1c"), &mut cache, &mut window_cache, &queue, &resolver, None, None, 42, 1, test_conn_id(), ); assert_eq!(resp.status, AtlasLayerStatus::Pending); assert!(resp.layer1.is_none()); // The enqueued analysis runs the real cascade and completes. std::thread::sleep(Duration::from_millis(150)); let completions = queue.drain_completions(); assert!( completions.iter().any( |c| matches!(c, GenCompletion::BodyAnalyzed { body_id, .. } if body_id == "GJ1c") ), "miss should enqueue an AnalyzeBody that completes: {completions:?}" ); } #[test] fn unknown_body_is_not_found() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let (_db, resolver) = empty_resolver(); let queue = GenerationQueue::with_threads(1); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let resp = handle_atlas_request( &req("ghost"), &mut cache, &mut window_cache, &queue, &resolver, None, None, 42, 1, test_conn_id(), ); assert_eq!(resp.status, AtlasLayerStatus::NotFound); } /// Build a DB with the columns needed by both `BodySourceResolver` and /// `BodyParamsReader` for the same body (Earth radius, 6371 km), plus a /// tiny heightmap root. /// /// Returns (db_path, resolver, body_params_reader, _root_kept_alive). fn resolver_and_params_reader( body_id: &str, ) -> ( PathBuf, BodySourceResolver, crate::atlas::body_params_reader::BodyParamsReader, PathBuf, // root dir — must stay alive for the test duration ) { resolver_and_params_reader_with_radius(body_id, 6371.0) } /// Same as [`resolver_and_params_reader`] with a caller-chosen /// `body_radius_km` (T-1142: the window-centre normalization tests need a /// SMALL body — at Earth radius the district-circumference/half-meridian /// bounds are tens of thousands of districts wide, too large for a /// hand-checkable out-of-range test value). fn resolver_and_params_reader_with_radius( body_id: &str, r_km: f64, ) -> ( PathBuf, BodySourceResolver, crate::atlas::body_params_reader::BodyParamsReader, PathBuf, // root dir — must stay alive for the test duration ) { let n = SEQ.fetch_add(1, Ordering::Relaxed); let db = std::env::temp_dir().join(format!("sr_proxybp_{}_{n}.db", std::process::id())); let _ = std::fs::remove_file(&db); let conn = Connection::open(&db).unwrap(); conn.execute_batch( "CREATE TABLE star_systems ( system_id TEXT PRIMARY KEY, spectral_class TEXT, star_type TEXT ); CREATE TABLE bodies ( body_id TEXT PRIMARY KEY, system_id TEXT, terrain_reference TEXT, hydrosphere TEXT, atmosphere TEXT, planet_class TEXT, body_radius_km REAL, orbital_period_days REAL, axial_tilt_deg REAL );", ) .unwrap(); conn.execute( "INSERT INTO star_systems (system_id, spectral_class, star_type) VALUES ('GJ-1', 'G', 'main_sequence')", [], ) .unwrap(); conn.execute( "INSERT INTO bodies (body_id, system_id, terrain_reference, hydrosphere, atmosphere, planet_class, body_radius_km, orbital_period_days, axial_tilt_deg) VALUES (?1, 'GJ-1', ?2, 'ocean', 'breathable', 'temperate', ?3, 365.25, 23.5)", rusqlite::params![body_id, REL, r_km], ) .unwrap(); drop(conn); let root = std::env::temp_dir().join(format!("sr_proxybproot_{}_{n}", std::process::id())); write_tiny_heightmap(&root.join(REL)); let resolver = BodySourceResolver::open(&db, vec![root.clone()]).unwrap(); let params_reader = crate::atlas::body_params_reader::BodyParamsReader::open(&db).unwrap(); (db, resolver, params_reader, root) } /// With body_params_reader wired, a cache miss enqueues an AnalyzeBody that /// completes with populated `districts` (DistrictProfile layer ran) AND /// populated `regions` (Region layer ran — the production terminal, /// T-1113). Then the completed state served back through /// `handle_atlas_request` carries a `region_grid` — closing the full /// dispatch → Ready → region_grid loop (PR #179 F4). #[test] fn body_params_reader_wired_produces_populated_regions() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let (_db, resolver, params_reader, _root) = resolver_and_params_reader("GJ1c"); let queue = GenerationQueue::with_threads(1); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let resp = handle_atlas_request( &req("GJ1c"), &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 1, test_conn_id(), ); assert_eq!(resp.status, AtlasLayerStatus::Pending); // Wait for the Rayon work item to complete. std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); let body_state = completions .into_iter() .find_map(|c| { if let GenCompletion::BodyAnalyzed { body_id, state } = c { if body_id == "GJ1c" { return Some(state); } } None }) .expect("AnalyzeBody must complete for GJ1c"); assert!( !body_state.districts.is_empty(), "districts must be populated when body_params_reader is wired (T-1032 dispatch path)" ); assert!( !body_state.regions.is_empty(), "regions must be populated when body_params_reader is wired (T-1113 dispatch path)" ); // Serve the completed state back through the proxy: the cache-hit // branch must build and include the region grid. cache.insert(*body_state); let ready = handle_atlas_request( &req("GJ1c"), &mut cache, &mut window_cache, &queue, &resolver, None, Some(¶ms_reader), 42, 2, test_conn_id(), ); assert_eq!(ready.status, AtlasLayerStatus::Ready); assert!( ready.region_grid.is_some(), "a Ready response for a Region-populated body must carry region_grid" ); } /// Without body_params_reader (None), districts is empty — pre-T-1032 behaviour. #[test] fn no_body_params_reader_leaves_regions_empty() { let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY); let (_db, resolver) = resolver_with_body("GJ1c"); let queue = GenerationQueue::with_threads(1); let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY); let resp = handle_atlas_request( &req("GJ1c"), &mut cache, &mut window_cache, &queue, &resolver, None, None, // no body_params_reader 42, 1, test_conn_id(), ); assert_eq!(resp.status, AtlasLayerStatus::Pending); std::thread::sleep(Duration::from_millis(300)); let completions = queue.drain_completions(); let body_state = completions .into_iter() .find_map(|c| { if let GenCompletion::BodyAnalyzed { body_id, state } = c { if body_id == "GJ1c" { return Some(state); } } None }) .expect("AnalyzeBody must complete for GJ1c"); assert!( body_state.districts.is_empty(), "districts must remain empty when no body_params_reader is wired" ); assert!( body_state.regions.is_empty(), "regions must remain empty when no body_params_reader is wired \ (the Region layer gates on body_params, T-1113)" ); } }