//! 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`). 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 { 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 { 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, ¶ms); out.extend(body_sweep_samples( "", SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7), "golden_body", ¶ms, &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 × `), 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 { let hm = sample_hm(); let params = sample_params(); let ta = sample_ta(&hm, ¶ms); 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, ¶ms, 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, ¶ms, 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() ); } }