Files
settled-reach/server/tests/window_derivation_golden.rs
T
jpmschweitzerandClaude Fable 5 24fad7090f feat(simulation): lakes from settled hydrology (D-227, T-1184)
Productionizes the T-1177 equilibrium solver: run_layer1 now solves
hydrology once per body (~24ms, mirrors drainage::analyze) and carries
it as TerrainAnalysis.hydrology; run_layer1_with_moisture threads the
real body moisture ceiling (extracted derive_moisture_ceiling_q), with
the T-1177 population-survey default as fallback. The resident rung-0
global tier does not exist yet (T-1181's scope) — hydrology rides
TerrainAnalysis and lands in that tier for free when it is built
(deviation recorded on the ticket).

MorphologyZone::Lake is now sourced from the settled solver at derive
time: a gridunit is Lake when bilinear-sampled filled surface exceeds
bilinear-sampled original elevation at the sample's own (px, py) — the
continuous comparison, so lake edges refine with rung like coastlines;
never a discrete basin-cell projection. The gate sits strictly between
OpenOcean (>= 80) and the old ocean_fraction heuristic (>= 60), which
survives as the derive-fresh fallback when no solve is attached —
byte-identical to pre-T-1184 output in that case. Static
classification, distinct from the sim-state flooded plane; no
endorheic bit (the drains-vs-closed cue is T-1185's outlet-course
presence, per the D-227 amendment (4) sequencing). Zero new wire
bytes.

Acceptance: lake_classification_cache_hit_equals_cache_miss (solve
twice independently, byte-identical zones, non-vacuous Lake hit) plus
hydrology determinism tests. Golden fidelity: the window golden
fixture now builds TerrainAnalysis through the production entry point
(run_layer1_with_moisture, per-body), and a dedicated lake_bowl golden
body pins the hydrology-sourced Lake path (morphology 1 at
ocean_fraction_q 0 — provably not the heuristic); the 108 pre-existing
golden rows are byte-identical (pure append). believability.json moved
by one lake-shaped line (GJ338Bd voxel_relief_m 27->28, a correctly
reclassified lake district leaving the dry-relief sample set).
river_course and derivation-harness goldens unchanged. Full suite:
2114 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-25 02:26:22 +02:00

803 lines
35 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Window-derivation golden regression (T-1162).
//!
//! Pins `derive_at_metres` (District/Quarter rungs) and
//! `derive_orbital_at_metres` (Region rung) output at a fixed
//! (seed, body, coords) sweep, across all three rung cutoffs — the window
//! path `layer_proxy::derive_window_cell` actually calls in production.
//! Mirrors `tests/derivation_harness.rs`'s golden pattern exactly (same
//! regen convention, same double-derive determinism check, same JSON-value
//! comparison so formatting drift doesn't false-positive).
//!
//! **T-1162 regen rationale:** this golden is generated AFTER the T-1162
//! octave-extension changes (extended coast-warp band, sub-district relief at
//! Quarter, vegetation patchiness) — it deliberately pins the NEW output.
//! There is no "pre-T-1162" golden to preserve: this file did not exist
//! before this ticket, so there is nothing to regress against except itself
//! from this point forward. Any future change to the coast warp, the voxel
//! relief band, the vegetation-patchiness field, or the MIN_WL_BANDS_M
//! quantization will change this golden's values — regenerate deliberately
//! (per the asset-pipeline discipline: source changes, not hand-edits).
//!
//! **Body coverage (Tyre, PR #194 review I4):** the sweep runs against THREE
//! bodies, not one — the original temperate/ocean/breathable body (unchanged
//! from the initial T-1162 landing), plus an airless/dry body (exercises
//! `vegetation_invention::VegetationEnvelope`'s `ceiling_q == 0` short
//! circuit at the full derivation-stack level) and a volcanic/high-tectonic
//! coastal body (exercises the ridged-warp/wide-`scatter_floor` branch of
//! `coast_invention`). The two new bodies' rows are APPENDED after the
//! 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_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};
const GOLDEN_FILE: &str = "tests/golden/window_derivation_golden.json";
/// Compact representation of a `DistrictProfile` sample for golden pinning.
/// Integer-discriminant fields only (D-010) — no float equality flakiness.
///
/// **No `body` field (Tyre, PR #194 I4 constraint):** the two new body rows
/// (I4) distinguish themselves via the `label` field's prefix instead of a
/// new struct field — adding a field here would change the JSON shape of
/// EVERY existing row (not just the new ones), which fails I4's explicit
/// "existing rows must stay byte-identical" requirement. `label` was always
/// a free-form string, so `"golden_body/coastal_a"` vs `"airless_dry/coastal_a"`
/// costs nothing structurally and keeps the diff a pure append.
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)]
struct GoldenSample {
label: String,
rung: String,
wx_m: i64,
wy_m: i64,
min_wl_m: i64,
morphology: u8,
tectonic: u8,
glaciation: u8,
precipitation: u8,
slope_q: i32,
elev_q: i32,
ocean_fraction_q: i32,
temperature_dc: i32, // deci-°C, i32::MIN sentinel for None (airless)
moisture_q: i32,
vegetation: u8,
}
fn sample_hm() -> BodyHeightmap {
// Deterministic gradient with enough variance for coast/relief/vegetation
// content to actually differ across the sweep positions — same shape
// convention as district_profile.rs's own test_hm / zoom_ladder_bench's
// bench_hm, sized a bit larger so the sweep coordinates land on distinct
// heightmap cells rather than a single interpolated patch.
let (w, h) = (128u32, 64u32);
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;
// A gentle sine ripple on top of the linear gradient gives the
// coastline invention real slope/ocean-mask variance to warp.
let ripple = (c * std::f32::consts::TAU * 3.0).sin() * 0.08;
(r * 0.55 + c * 0.35 + ripple + 0.05).clamp(0.0, 1.0)
})
.collect();
BodyHeightmap {
body_id: "golden_body".into(),
width: w,
height: h,
data,
sea_level: 0.32,
}
}
/// 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 {
BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("temperate".into()),
body_radius_km: Some(6371.0),
..Default::default()
}
}
/// I4 (Tyre, PR #194 review): airless/dry body params — exercises
/// `vegetation_invention::VegetationEnvelope`'s `ceiling_q == 0` short
/// circuit (no water, no atmosphere → zero patchiness swing, per
/// `envelope_airless_or_dry_body_has_zero_ceiling`'s unit-level proof) at
/// the FULL derivation-stack level, which the unit test alone doesn't pin.
/// Also airless (`atmosphere: "none"`) so `temperature_c`/`vegetation_class`
/// take the `None`/`Absent` branches — a body-envelope regression that
/// invented forest on a bone-dry world would show up here as a NEW
/// non-Barren/non-Absent vegetation discriminant in the golden diff.
fn airless_dry_params() -> BodyParams {
BodyParams {
hydrosphere: Some("none".into()),
atmosphere: Some("none".into()),
planet_class: Some("arid".into()),
body_radius_km: Some(3_390.0), // Mars-scale, deliberately distinct from the wet body
..Default::default()
}
}
/// I4 (Tyre, PR #194 review): volcanic/high-tectonic coastal body params —
/// exercises `coast_invention`'s ridged-warp + wide-`scatter_floor` branch
/// (`TectonicClass::Volcanic` → `tectonic_energy` near its ceiling in
/// `body_coast_envelope`, driving up `roughness`/`warp_amplitude_px`/
/// `scatter_floor` per that function's doc) — the coast-crinkle branch most
/// likely to visibly differ from the temperate body's gentler warp, and thus
/// the branch most likely to silently regress without dedicated coverage.
fn volcanic_coast_params() -> BodyParams {
BodyParams {
hydrosphere: Some("ocean".into()),
atmosphere: Some("breathable".into()),
planet_class: Some("volcanic".into()),
tectonic_activity: Some("volcanic".into()),
body_radius_km: Some(6_000.0),
..Default::default()
}
}
/// 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) = (128u32, 64u32);
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) = (128u32, 64u32);
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,
/// sub-district relief, and vegetation patchiness all at once.
fn sweep_positions() -> Vec<(&'static str, f64, f64)> {
vec![
("coastal_a", 2_000_000.0, 1_500_000.0),
("coastal_b", 2_050_000.0, 1_500_000.0),
("coastal_c", 2_100_000.0, 1_560_000.0),
("inland", 500_000.0, 3_000_000.0),
("high_lat", 1_200_000.0, 8_500_000.0),
// T-1170 A2 Discipline item 4: empirically verified (probe run against
// this fixture, `sample_hm()`/`sample_params()`) that `sample_hm()`
// produces a real river-cell chain around working-grid pixel
// (row=10, col=116) — NONE of the original five sweep positions
// (pixel cols ~1.6-6.7) land anywhere near it. This position converts
// that pixel to world metres (same `world_m_to_pixel` inverse the
// production mapping uses) so the golden sweep also exercises
// `derive_at_metres` genuinely close to invented river geometry —
// closing the "believability expected unchanged, verify, don't
// assume" discipline item for the district-profile-only fields this
// golden already pins (courses themselves are pinned separately
// below, `river_course_golden_regression`, since this sweep's
// `derive_at_metres` calls never touch `RiverCourse` at all).
("river_course", 36_277_344.8, -6_830_545.5),
]
}
fn derive_golden_sample(
label: &str,
rung: &str,
seed: SeedChain,
body_id: &str,
params: &BodyParams,
ta: &TerrainAnalysis,
climate: &ClimateConstants,
wx: f64,
wy: f64,
min_wl_m: f64,
orbital: bool,
) -> GoldenSample {
let prof = if orbital {
derive_orbital_at_metres(seed, body_id, params, ta, wx, wy, climate)
} else {
derive_at_metres(seed, body_id, params, ta, wx, wy, climate, min_wl_m, &[])
};
GoldenSample {
label: label.to_string(),
rung: rung.to_string(),
wx_m: wx as i64,
wy_m: wy as i64,
min_wl_m: min_wl_m as i64,
morphology: prof.morphology_zone as u8,
tectonic: prof.tectonic_class as u8,
glaciation: prof.glaciation_grade as u8,
precipitation: prof.precipitation_class as u8,
slope_q: prof.slope_q,
elev_q: prof.elev_q,
ocean_fraction_q: prof.ocean_fraction_q,
temperature_dc: prof
.temperature_c
.map(|t| (t * 10.0).round() as i32)
.unwrap_or(i32::MIN),
moisture_q: prof.moisture_q,
vegetation: prof.vegetation_class as u8,
}
}
/// District's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s
/// District entry — District's own Nyquist floor, `2 * DISTRICT_M` per the
/// PR #194 I1 dependency-direction fix: the RUNG authors the floor, and
/// `detail_scatter::OCTAVE_WAVELENGTHS_M`'s finest octave coinciding with it
/// is pinned by the `const` drift-guard next to `MIN_WL_BANDS_M`, not the
/// source of the value). A District-rung request in production quantizes to
/// exactly this value, so the golden pins the SAME cutoff a real window
/// request would actually carry; `golden_cutoffs_match_the_scale_ladder`
/// asserts the `2 × spacing` coupling for both this and `QUARTER_MIN_WL_M`.
const DISTRICT_MIN_WL_M: f64 = 4_096.0;
/// Quarter's real quantized `min_wl_m` band (`layer_proxy::MIN_WL_BANDS_M`'s
/// new T-1162 entry — Quarter's own Nyquist floor, `2 * QUARTER_M`).
const QUARTER_MIN_WL_M: f64 = 1_024.0;
/// Run the fixed sweep (every position × the three rung cutoffs — District /
/// Quarter use their REAL production `MIN_WL_BANDS_M` values / Region via
/// `derive_orbital_at_metres`, which takes no cutoff parameter — see its own
/// doc on why) for ONE body. Extracted (Tyre, PR #194 I4) so multiple bodies
/// can share the same sweep logic; `label_prefix` (empty for the original
/// 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,
seed: SeedChain,
body_id: &str,
params: &BodyParams,
ta: &TerrainAnalysis,
climate: &ClimateConstants,
positions: &[(&'static str, f64, f64)],
) -> Vec<GoldenSample> {
let mut out = Vec::new();
for &(label, wx, wy) in positions {
let label = format!("{label_prefix}{label}");
out.push(derive_golden_sample(
&label,
"district",
seed,
body_id,
params,
ta,
climate,
wx,
wy,
DISTRICT_MIN_WL_M,
false,
));
out.push(derive_golden_sample(
&label,
"quarter",
seed,
body_id,
params,
ta,
climate,
wx,
wy,
QUARTER_MIN_WL_M,
false,
));
out.push(derive_golden_sample(
&label, "region", seed, body_id, params, ta, climate, wx, wy, 0.0, true,
));
}
out
}
/// Build the full golden sample set: the ORIGINAL temperate/ocean/breathable
/// 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 climate = ClimateConstants::default();
let mut out = Vec::new();
// 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",
&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_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_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
}
#[test]
fn window_derivation_golden_regression() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let golden_path = manifest.join(GOLDEN_FILE);
// Double-derive determinism check (D-010) before ever touching the golden.
let run1 = golden_samples();
let run2 = golden_samples();
assert_eq!(
run1, run2,
"double-derivation mismatch — determinism is broken (D-010)"
);
let actual_json = serde_json::to_string_pretty(&run1).expect("serialize") + "\n";
if std::env::var("UPDATE_GOLDEN").is_ok() {
std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden");
std::fs::write(&golden_path, &actual_json).expect("write golden");
eprintln!(
"Golden written: {} ({} bytes)",
golden_path.display(),
actual_json.len()
);
return;
}
let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
panic!(
"Golden file not found: {}.\n\
First run: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n{e}",
golden_path.display()
)
});
let actual_v: serde_json::Value = serde_json::from_str(&actual_json).expect("reparse actual");
let golden_v: serde_json::Value = serde_json::from_str(&golden_json).expect("parse golden");
if actual_v != golden_v {
panic!(
"Window-derivation golden mismatch — derivation chain changed.\n\
Update: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n\
Golden: {}\nActual: {}",
golden_json.trim(),
actual_json.trim()
);
}
}
/// Cross-rung coherence sanity check on the golden's own fixed sweep: for
/// each coastal label, the Quarter-rung sample must differ from the
/// District-rung sample at the SAME position (the whole point of the
/// extension — Quarter sees finer content District's coarser cutoff
/// truncates). This is a structural companion to the golden file itself,
/// not a replacement for it — it fails loudly if the golden ever gets
/// regenerated with `min_wl_m` accidentally identical across rungs.
#[test]
fn quarter_and_district_rungs_diverge_at_the_same_position() {
let samples = golden_samples();
let mut any_diverged = false;
for (label, _, _) in sweep_positions() {
let district = samples
.iter()
.find(|s| s.label == label && s.rung == "district")
.unwrap();
let quarter = samples
.iter()
.find(|s| s.label == label && s.rung == "quarter")
.unwrap();
if district.elev_q != quarter.elev_q
|| district.slope_q != quarter.slope_q
|| district.moisture_q != quarter.moisture_q
{
any_diverged = true;
}
}
assert!(
any_diverged,
"no sweep position showed ANY difference between District and Quarter \
rungs — the T-1162 octave extension would be structurally inert"
);
}
/// `scale::DISTRICT_M` / `scale::QUARTER_M` sanity — documents WHY
/// `DISTRICT_MIN_WL_M`/`QUARTER_MIN_WL_M` are the cutoffs used above (each
/// rung's own Nyquist floor, `2 × <rung's spacing>`), so a future
/// scale-ladder change surfaces here. Pins BOTH rungs' coupling (Tyre, PR
/// #194 I1 — District's coupling was previously unpinned; only Quarter's
/// `2 × QUARTER_M` was checked) — this mirrors
/// `layer_proxy::MIN_WL_BANDS_M`'s own direct `2 × DISTRICT_M` / `2 ×
/// QUARTER_M` derivation, not `detail_scatter::OCTAVE_WAVELENGTHS_M`.
#[test]
fn golden_cutoffs_match_the_scale_ladder() {
assert_eq!(scale::DISTRICT_M, 2_048);
assert_eq!(scale::QUARTER_M, 512);
assert_eq!(2 * scale::DISTRICT_M, DISTRICT_MIN_WL_M as i32);
assert_eq!(2 * scale::QUARTER_M, QUARTER_MIN_WL_M as i32);
}
// ---------------------------------------------------------------------------
// River course golden (T-1170 A2, Discipline item 4)
// ---------------------------------------------------------------------------
//
// Empirically verified (probe run against `sample_hm()`/`sample_params()`):
// this fixture body produces a real river-cell chain around working-grid
// pixel (row≈10, col=116) — the "river_course" sweep position above converts
// that pixel to world metres. This section pins the ACTUAL invented course
// geometry for an edge from that chain, at both District and Quarter station
// spacing, so courses themselves — not just the district-profile fields the
// main golden above covers — are regression-pinned.
//
// **Mouth coverage (T-1170 PR #197 review, Hoshe #2):** the sample set below
// also includes a real Mouth-terminus edge from `sample_hm()` (empirically
// probed: 4 mouth edges exist in this fixture) — `invent_course`'s raw output
// for that edge is pinned here (edge_id/class/terminus/points), closing the
// "zero Mouth coverage in any golden" gap at the invention layer. The FULL
// end-to-end path (invent → crop → `resolve_mouth_terminus` →
// `CourseTerminus::Mouth`) is covered separately and permanently by
// `layer_proxy::tests::all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none`
// (in-crate, since `crop_course_to_window`/`resolve_mouth_terminus` are
// private to `layer_proxy.rs` and unreachable from this integration test) —
// that test is the actual Hoshe #1 acceptance criterion (3/3 real mouths);
// this golden's job is regression-pinning the raw invented geometry, not
// re-proving the crop/resolve path.
const RIVER_COURSE_GOLDEN_FILE: &str = "tests/golden/river_course_golden.json";
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Clone)]
struct GoldenCourseSample {
rung: String,
edge_id: u32,
class: u8,
terminus: String,
/// Points rounded to the nearest metre (D-010 integer boundary at the
/// golden-pinning layer — the production wire path itself rounds to
/// `i32` metres, `layer_proxy::crop_course_to_window`).
points: Vec<(i64, i64)>,
}
fn river_course_golden_samples() -> Vec<GoldenCourseSample> {
let hm = sample_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);
let edges = river_course::build_edges(&dr.river_network);
// Pick the interior edge whose upstream cell is closest to (row=10,
// col=116) — deterministic (BTreeMap-free linear scan, fixed tie-break
// by edge_id) rather than hardcoding an index that could silently shift
// if `build_edges`' ordering ever changes.
let target = edges
.iter()
.filter(|e| e.terminus == river_course::EdgeTerminusKind::Interior)
.min_by_key(|e| {
let dr = e.upstream.0 as i64 - 10;
let dc = e.upstream.1 as i64 - 116;
(dr * dr + dc * dc, e.edge_id)
})
.expect("sample_hm() fixture must have at least one interior river edge near (10, 116)");
// Hoshe #2: a real Mouth-terminus edge, deterministically selected as the
// lowest edge_id among the fixture's mouth edges (fixed tie-break, no
// hardcoded index).
let mouth_target = edges
.iter()
.filter(|e| e.terminus == river_course::EdgeTerminusKind::Mouth)
.min_by_key(|e| e.edge_id)
.expect("sample_hm() fixture must have at least one Mouth edge (empirically verified: 4)");
let mut out = Vec::new();
for (rung, spacing_m, min_wl_m) in [
("district", DISTRICT_MIN_WL_M, DISTRICT_MIN_WL_M),
("quarter", QUARTER_MIN_WL_M, QUARTER_MIN_WL_M),
] {
let course = river_course::invent_course(seed, target, &ta, &params, spacing_m, min_wl_m);
out.push(GoldenCourseSample {
rung: rung.to_string(),
edge_id: course.edge_id,
class: course.class,
terminus: format!("{:?}", course.terminus),
points: course
.points
.iter()
.map(|p| (p.0.round() as i64, p.1.round() as i64))
.collect(),
});
let mouth_course =
river_course::invent_course(seed, mouth_target, &ta, &params, spacing_m, min_wl_m);
out.push(GoldenCourseSample {
rung: format!("{rung}_mouth"),
edge_id: mouth_course.edge_id,
class: mouth_course.class,
terminus: format!("{:?}", mouth_course.terminus),
points: mouth_course
.points
.iter()
.map(|p| (p.0.round() as i64, p.1.round() as i64))
.collect(),
});
}
out
}
#[test]
fn river_course_golden_regression() {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let golden_path = manifest.join(RIVER_COURSE_GOLDEN_FILE);
let run1 = river_course_golden_samples();
let run2 = river_course_golden_samples();
assert_eq!(
run1, run2,
"double-derivation mismatch — course invention determinism is broken (D-010/D-227)"
);
let actual_json = serde_json::to_string_pretty(&run1).expect("serialize") + "\n";
if std::env::var("UPDATE_GOLDEN").is_ok() {
std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden");
std::fs::write(&golden_path, &actual_json).expect("write golden");
eprintln!(
"Golden written: {} ({} bytes)",
golden_path.display(),
actual_json.len()
);
return;
}
let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| {
panic!(
"Golden file not found: {}.\n\
First run: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n{e}",
golden_path.display()
)
});
let actual_v: serde_json::Value = serde_json::from_str(&actual_json).expect("reparse actual");
let golden_v: serde_json::Value = serde_json::from_str(&golden_json).expect("parse golden");
if actual_v != golden_v {
panic!(
"River-course golden mismatch — course invention changed.\n\
Update: UPDATE_GOLDEN=1 cargo test --test window_derivation_golden\n\
Golden: {}\nActual: {}",
golden_json.trim(),
actual_json.trim()
);
}
}
/// The course golden's target edge must genuinely differ in point geometry
/// between District and Quarter station spacing (more, finer-spaced stations
/// at Quarter — Ruling 3b's cross-rung invariant) — otherwise the golden
/// would be pinning two identical rungs and the test would give false
/// confidence.
#[test]
fn river_course_rungs_have_different_station_counts() {
let samples = river_course_golden_samples();
let district = samples.iter().find(|s| s.rung == "district").unwrap();
let quarter = samples.iter().find(|s| s.rung == "quarter").unwrap();
assert!(
quarter.points.len() > district.points.len(),
"Quarter's finer station spacing must produce MORE points than District \
(district={}, quarter={})",
district.points.len(),
quarter.points.len()
);
}
/// **T-1170 PR #197 review, Hoshe #2 (golden coverage):** the pinned Mouth
/// edge samples must be genuine, non-degenerate courses (`terminus == "Mouth"`,
/// `points.len() >= 2`) — the direct golden-level check that the Hoshe #1 fix
/// (real seaward chord via `RiverNetwork::river_seaward`) reaches this fixture
/// too, not just the dedicated GJ1c acceptance test.
#[test]
fn river_course_mouth_samples_are_non_degenerate() {
let samples = river_course_golden_samples();
for rung in ["district_mouth", "quarter_mouth"] {
let sample = samples
.iter()
.find(|s| s.rung == rung)
.unwrap_or_else(|| panic!("missing golden sample for rung {rung}"));
assert_eq!(
sample.terminus, "Mouth",
"{rung}: build_edges must produce a Mouth-terminus RiverEdge for the pinned target"
);
assert!(
sample.points.len() >= 2,
"{rung}: Mouth edge invented a degenerate {}-point course — the chord-length fix \
(Hoshe #1) regressed for this fixture",
sample.points.len()
);
}
}