@@ -28,17 +28,38 @@
//! original body's rows (never interleaved), so the original rows stay
//! byte-identical across the I4 regen — see `body_sweep_samples`'s doc.
//!
//! **T-1184 fixture fidelity fix (lead review, post-hydrology-productionization):**
//! `sample_ta` now builds its `TerrainAnalysis` via `run_layer1_with_moisture`
//! (solving settled hydrology and attaching it, exactly what
//! `TerrainAnalysisCache::get_or_derive` does in production once `body_params`
//! is `Some` — the ONLY path `derive_window_cell`'s callers reach post-T-1184),
//! not the bare `drainage::analyze` + `TerrainAnalysis::analyze` construction
//! this file used before, which silently pinned the fallback heuristic path
//! (`ta.hydrology == None`) that production no longer takes. Each body in the
//! sweep gets its OWN `ta`, solved with ITS OWN moisture ceiling
//! (`derive_moisture_ceiling_q(params)`) — mirrors production's per-`body_id`
//! `TerrainAnalysisCache` keying, since hydrology's endorheic/overflow split
//! (not lake EXTENT, which is moisture-independent) depends on the body's own
//! params. A FOURTH body (`lake_bowl`, a dedicated bowl-shaped heightmap with a
//! real filled basin at its centre) is appended for exactly this reason: the
//! original three bodies' `sample_hm()` gradient-plus-ripple heightmap
//! produces ZERO filled basins anywhere (verified before this fix — see the
//! coordinator's review), so without a dedicated lake fixture the hydrology
//! lake-sourcing gate would be wired but never pinned by any golden row.
//!
//! Run: `cargo test --test window_derivation_golden`
//! Regenerate: `UPDATE_GOLDEN=1 cargo test --test window_derivation_golden`
use std ::path ::PathBuf ;
use settled_reach_server ::atlas ::district_profile ::{
derive_at_metres , derive_orbital_at_metres , BodyParams , ClimateConstants ,
derive_at_metres , derive_moisture_ceiling_q , derive_orbital_at_metres , BodyParams ,
ClimateConstants ,
} ;
use settled_reach_server ::atlas ::drainage ;
use settled_reach_server ::atlas ::features ::TerrainAnalysis ;
use settled_reach_server ::atlas ::heightmap ::BodyHeightmap ;
use settled_reach_server ::atlas ::layer1 ::run_layer1_with_moisture ;
use settled_reach_server ::atlas ::river_course ;
use settled_reach_server ::atlas ::scale ;
use settled_reach_server ::seed ::{ SeedChain , SeedDomain } ;
@@ -101,9 +122,22 @@ fn sample_hm() -> BodyHeightmap {
}
}
fn sample_ta ( hm : & BodyHeightmap ) -> TerrainAnalysis {
let dr = drainage ::analyze ( & hm . data , hm . width , hm . height , hm . sea_level ) ;
TerrainAnalysis ::analyze ( hm , & dr )
/// Builds a `TerrainAnalysis` the PRODUCTION way (T-1184 fixture-fidelity
/// fix): `run_layer1_with_moisture`, solving settled hydrology and attaching
/// it via `with_hydrology`, using the SAME moisture ceiling
/// `TerrainAnalysisCache::get_or_derive` derives from `body_params` in
/// production (`derive_moisture_ceiling_q(params)`) — not the bare
/// `drainage::analyze` + `TerrainAnalysis::analyze` construction this
/// function used before, which left `ta.hydrology == None` and silently
/// pinned the fallback heuristic path production no longer takes once
/// `body_params` is `Some` (the only case `derive_window_cell`'s callers
/// reach: `resolve_settlement_morphology_zone` short-circuits to `None` on a
/// missing `body_params`, and `DeriveWindow`'s `body_params` field is a
/// non-`Option` `Box<BodyParams>`).
fn sample_ta ( hm : & BodyHeightmap , params : & BodyParams ) -> TerrainAnalysis {
let moisture_q = derive_moisture_ceiling_q ( params ) ;
let ( _l1 , ta ) = run_layer1_with_moisture ( hm , moisture_q ) ;
ta
}
fn sample_params ( ) -> BodyParams {
@@ -153,6 +187,88 @@ fn volcanic_coast_params() -> BodyParams {
}
}
/// T-1184 (lead review fixture-fidelity fix): a dedicated bowl-shaped
/// heightmap — high rim (0.9), low centre (0.15), same 128× 64 dims as
/// `sample_hm()` — so the settled-hydrology solver produces a real, single
/// filled lake basin at the bowl's geometric centre. `sea_level = 0.05`
/// (well below the bowl's own lowest point, 0.15) keeps the ENTIRE grid dry
/// by the raw heightmap threshold, so a `Lake` verdict at the centre can only
/// be hydrology-sourced, never the pre-existing `ocean_fraction_q` heuristic.
///
/// Needed because NONE of `sample_hm()`/`airless_dry_params()`'s/
/// `volcanic_coast_params()`'s shared gradient-plus-ripple heightmap ever
/// forms an enclosed depression (verified: `solve()` returns zero basins on
/// it at any moisture input) — without this fixture, T-1184's hydrology
/// lake-sourcing gate would be wired into the derive core but pinned by no
/// golden row at all, exactly the "unpinned production path" gap the review
/// flagged.
fn lake_bowl_hm ( ) -> BodyHeightmap {
let ( w , h ) = ( 128 u32 , 64 u32 ) ;
let n = ( w * h ) as usize ;
let cx = w as f32 / 2.0 ;
let cy = h as f32 / 2.0 ;
let max_r = cx . min ( cy ) ;
let data = ( 0 .. n )
. map ( | i | {
let r = ( i / w as usize ) as f32 ;
let c = ( i % w as usize ) as f32 ;
let d = ( ( ( c - cx ) . powi ( 2 ) + ( r - cy ) . powi ( 2 ) ) . sqrt ( ) / max_r ) . min ( 1.0 ) ;
0.15 + d * 0.75
} )
. collect ( ) ;
BodyHeightmap {
body_id : " golden_body_lake_bowl " . into ( ) ,
width : w ,
height : h ,
data ,
sea_level : 0.05 ,
}
}
/// Params for the lake-bowl body — ocean/breathable/temperate at Earth
/// radius (matches `sample_params()`'s class so the lake row exercises the
/// same moisture ceiling as the original body, isolating the bowl geometry
/// as the one variable).
fn lake_bowl_params ( ) -> BodyParams {
BodyParams {
hydrosphere : Some ( " ocean " . into ( ) ) ,
atmosphere : Some ( " breathable " . into ( ) ) ,
planet_class : Some ( " temperate " . into ( ) ) ,
body_radius_km : Some ( 6371.0 ) ,
.. Default ::default ( )
}
}
/// Pixel → world-metres, reproducing `district_profile::pixel_to_world_m`'s
/// formula (that function is `pub(crate)`, unreachable from this integration
/// test) — the SAME mapping `derive_at_metres`/`derive_orbital_at_metres`
/// compute inline for their own `(px, py)` derivation.
fn golden_pixel_to_world_m ( px : f64 , py : f64 , w : u32 , h : u32 , radius_km : f64 ) -> ( f64 , f64 ) {
let wx = px / w as f64 * ( std ::f64 ::consts ::TAU * radius_km * 1000.0 ) ;
let lat_frac = py / ( h as f64 - 1.0 ) - 0.5 ;
( wx , lat_frac * ( std ::f64 ::consts ::PI * radius_km * 1000.0 ) )
}
/// Sweep positions for the lake-bowl body — just the bowl centre (world
/// metres for pixel (64, 32) at `body_radius_km = 6371.0`) plus a rim point
/// clearly outside the basin, so the golden also pins the "near but not in a
/// lake" boundary case at this fixture. Computed once via
/// `golden_pixel_to_world_m` so the position always matches the ACTUAL bowl
/// centre even if the bowl dimensions ever change, rather than a hand-copied
/// literal.
fn lake_bowl_sweep_positions ( ) -> Vec < ( & 'static str , f64 , f64 ) > {
let ( w , h ) = ( 128 u32 , 64 u32 ) ;
let ( cx , cy ) = ( w as f64 / 2.0 , h as f64 / 2.0 ) ;
let ( centre_x , centre_y ) = golden_pixel_to_world_m ( cx , cy , w , h , 6371.0 ) ;
// A rim pixel (near the bowl edge, outside the filled basin) — same row,
// near the left edge of the bowl's radius.
let ( rim_x , rim_y ) = golden_pixel_to_world_m ( 4.0 , cy , w , h , 6371.0 ) ;
vec! [
( " lake_centre " , centre_x , centre_y ) ,
( " lake_rim " , rim_x , rim_y ) ,
]
}
/// Fixed sweep positions (world metres from origin) — a handful of points
/// spanning a coastal stretch (per the heightmap's ripple) plus a couple of
/// clearly inland/high-latitude points, so the golden exercises coast warp,
@@ -244,6 +360,12 @@ const QUARTER_MIN_WL_M: f64 = 1_024.0;
/// body, non-empty for the I4 additions) is prepended to each row's `label`
/// so multi-body output stays distinguishable without a new struct field
/// (see [`GoldenSample`]'s doc on why no `body` field was added).
///
/// `positions` is now a parameter (T-1184) rather than always calling
/// `sweep_positions()` internally — the lake-bowl body needs its OWN sweep
/// (`lake_bowl_sweep_positions()`, a position that's actually inside its
/// basin), not the original three bodies' coastal/inland/river sweep, which
/// means nothing on the bowl fixture's geometry.
#[ allow(clippy::too_many_arguments) ]
fn body_sweep_samples (
label_prefix : & str ,
@@ -252,9 +374,10 @@ fn body_sweep_samples(
params : & BodyParams ,
ta : & TerrainAnalysis ,
climate : & ClimateConstants ,
positions : & [ ( & 'static str , f64 , f64 ) ] ,
) -> Vec < GoldenSample > {
let mut out = Vec ::new ( ) ;
for ( label , wx , wy ) in sweep_positions ( ) {
for & ( label , wx , wy ) in positions {
let label = format! ( " {label_prefix} {label} " ) ;
out . push ( derive_golden_sample (
& label ,
@@ -290,46 +413,93 @@ fn body_sweep_samples(
}
/// Build the full golden sample set: the ORIGINAL temperate/ocean/breathable
/// body's sweep first (byte-identical inputs to the pre-I4 `golden_samples`
/// — same seed, same `body_id`, same unprefixed labels, so its rows are
/// byte-identical in the regenerated fixture), THEN the two I4 body rows
/// appended after ( never interleaved) so the diff against the pre-I4 golden
/// is a pure append, not a reshuffle.
/// body's sweep first (byte-identical POSITIONS/seed/label to the pre-I4
/// `golden_samples` — the VALUES move under the T-1184 fixture-fidelity fix,
/// see below), THEN the I4 body rows, THEN the T-1184 lake-bowl body
/// ( appended last, never interleaved) so the diff against the pre-fix golden
/// is a pure value-update-plus- append, not a reshuffle.
///
/// **T-1184: each body now gets its OWN `TerrainAnalysis`**, built via
/// `sample_ta(&hm, params)` — production-faithful (`run_layer1_with_moisture`
/// solving hydrology with THIS body's own moisture ceiling), mirroring
/// `TerrainAnalysisCache`'s real per-`body_id` keying. Before this fix all
/// three bodies shared ONE `ta` built from ONE call with no hydrology
/// attached at all — cheap to share when `TerrainAnalysis` was a pure
/// function of the heightmap alone, no longer correct now that hydrology's
/// endorheic/overflow split depends on the body's own `BodyParams`. This
/// doesn't change any EXISTING row's values on `sample_hm()` bodies (that
/// fixture has zero filled basins at any moisture input — hydrology only
/// changes the fallback-vs-sourced CODE PATH taken, not the OUTPUT, when
/// there's nothing to source) but is required for correctness going forward
/// and is what the lake-bowl body's own per-body `ta` depends on.
fn golden_samples ( ) -> Vec < GoldenSample > {
let hm = sample_hm ( ) ;
let ta = sample_ta ( & hm ) ;
let climate = ClimateConstants ::default ( ) ;
let mut out = Vec ::new ( ) ;
// Original body — UNCHANGED inputs from pre-I4 (T-1162 initial landing).
// Original body — same seed/body_id/labels/positions as pre-T-1184; `ta`
// is now built production-faithfully (see doc above) but this fixture has
// no basins, so the row VALUES are unaffected.
let hm = sample_hm ( ) ;
let params = sample_params ( ) ;
let ta = sample_ta ( & hm , & params ) ;
out . extend ( body_sweep_samples (
" " ,
SeedChain ::root ( 0xC0FFEE_ u64 ) . derive ( SeedDomain ::Body , 7 ) ,
" golden_body " ,
& sample_params ( ) ,
& params ,
& ta ,
& climate ,
& sweep_positions ( ) ,
) ) ;
// I4 addition 1: airless/dry — ceiling_q == 0 vegetation short-circuit.
// Same heightmap (sample_hm()) as the original body, but its OWN ta
// (moisture ceiling for an airless/dry body is 0, not the ocean body's
// moisture — matters for endorheic/overflow classification even though,
// again, this fixture has no basins to classify).
let airless_params = airless_dry_params ( ) ;
let airless_ta = sample_ta ( & hm , & airless_params ) ;
out . extend ( body_sweep_samples (
" airless_dry/ " ,
SeedChain ::root ( 0xC0FFEE_ u64 ) . derive ( SeedDomain ::Body , 8 ) ,
" golden_body_airless_dry " ,
& airless_dry_params ( ) ,
& ta ,
& airless_params ,
& airless_ta ,
& climate ,
& sweep_positions ( ) ,
) ) ;
// I4 addition 2: volcanic/high-tectonic coast — ridged warp, wide scatter_floor.
let volcanic_params = volcanic_coast_params ( ) ;
let volcanic_ta = sample_ta ( & hm , & volcanic_params ) ;
out . extend ( body_sweep_samples (
" volcanic_coast/ " ,
SeedChain ::root ( 0xC0FFEE_ u64 ) . derive ( SeedDomain ::Body , 9 ) ,
" golden_body_volcanic_coast " ,
& volcanic_coast_params ( ) ,
& ta ,
& volcanic_params ,
& volcanic_ta ,
& climate ,
& sweep_positions ( ) ,
) ) ;
// T-1184 addition: lake-bowl body — a dedicated heightmap WITH a real
// filled basin, so the settled-hydrology lake-sourcing gate this ticket
// adds is pinned by at least one golden row (the original three bodies'
// shared heightmap has zero basins at any sampled position — see this
// function's and `lake_bowl_hm`'s docs). Own heightmap, own params, own
// seed, own sweep (the bowl centre + a rim point, not the coastal/inland
// positions that mean nothing on this fixture's geometry).
let lake_hm = lake_bowl_hm ( ) ;
let lake_params = lake_bowl_params ( ) ;
let lake_ta = sample_ta ( & lake_hm , & lake_params ) ;
out . extend ( body_sweep_samples (
" lake_bowl/ " ,
SeedChain ::root ( 0xC0FFEE_ u64 ) . derive ( SeedDomain ::Body , 10 ) ,
" golden_body_lake_bowl " ,
& lake_params ,
& lake_ta ,
& climate ,
& lake_bowl_sweep_positions ( ) ,
) ) ;
out
@@ -475,8 +645,8 @@ struct GoldenCourseSample {
fn river_course_golden_samples ( ) -> Vec < GoldenCourseSample > {
let hm = sample_hm ( ) ;
let ta = sample_ta ( & hm ) ;
let params = sample_params ( ) ;
let ta = sample_ta ( & hm , & params ) ;
let seed = SeedChain ::root ( 0xC0FFEE_ u64 ) . derive ( SeedDomain ::Body , 7 ) ;
let dr = drainage ::analyze ( & hm . data , hm . width , hm . height , hm . sea_level ) ;