fix(simulation): one absolute-metre derive core (D-256, T-1174)

derive_district_profile is now a thin wrapper over the shared
derive_at_metres_with_riparian core at survey-cell-centre world metres
— one derive core, two position sets. The batch pseudo-grid and the
true D-243 district grid were two coordinate systems sharing one bare
(i32,i32) type; the new SurveyCellPos newtype re-keys every batch
product (BodyWorldState.districts, Layer1Output.survey_basin_dirs) so
the compiler rejects cross-namespace passing.

Fixes two latent same-position divergences the T-1174 investigation
surfaced: three inconsistent latitude conventions collapse into the
core's single inverse mapping, and the region-climate baseline now
floor-divides true world metres instead of collapsing the whole body
onto region (0,0)'s baseline — batch climate becomes latitude/region
graded (D-245 direction: every changed believability metric increased).

Binding preservations per D-256(c): basin_direction rides a post-call
override with the true L1 D8 survey-cell aggregate (layer1's map
re-keyed to SurveyCellPos, identity lookup — a floor-divide lookup
against the pseudo-keyed map would have silently defaulted every cell
North); the riparian verdict comes from near_perennial_water_at, never
the empty-slice default (which would have flipped riverside
vegetation_class).

Quarter-skeleton morphology_zone now resolves at the settlement's
exact world position via derive_at_metres at work-item execution
(where TerrainAnalysisCache lives), replacing the survey-cell-centre
map lookup (D-256(d)); settlement_district_pos fixed to true-district
floor-division in passing (same doc/impl mismatch class). Second
pixel-vs-metre conflation fixed in aliveness_probe's anchor-walk math.

Window path byte-unchanged (window_derivation_golden 6/6 byte-
identical); derivation_harness golden untouched; believability golden
regenerated. Full lib + integration suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 00:40:13 +02:00
co-authored by Claude Fable 5
parent 35a4137304
commit 933e1f4ee4
15 changed files with 1272 additions and 479 deletions
+33 -10
View File
@@ -24,13 +24,13 @@ use crate::atlas::attractor_matching::{
match_cities, territorial_status_from_faction, CityPlacement, CityRecord,
};
use crate::atlas::body_world_state::{BodyWorldState, RiverNetwork};
use crate::atlas::district_profile::{self, BodyParams, DistrictPos, DistrictProfile};
use crate::atlas::district_profile::{self, BodyParams, DistrictProfile};
use crate::atlas::features::TerrainAnalysis;
use crate::atlas::heightmap::{self, BodyHeightmap, HeightmapLoadError};
use crate::atlas::layer1::{self, Layer1Output};
use crate::atlas::region_profile::{self, RegionProfile};
use crate::atlas::road_graph::{self, RoadGraph};
use crate::atlas::scale::{self, RegionPos};
use crate::atlas::scale::{self, RegionPos, SurveyCellPos};
use crate::seed::SeedChain;
use crate::simulation::generator::{CompatibilityMatrix, GeographicAttractor, TerritorialStatus};
@@ -105,11 +105,11 @@ pub struct CascadeSnapshot {
pub terrain_analysis: Option<TerrainAnalysis>,
}
/// DistrictProfile layer output (T-1023, D-239 §1): per-district (~1 km) terrain
/// profiles covering the whole body. Stored in `BodyWorldState.districts`.
/// DistrictProfile layer output (T-1023, D-239 §1): per-survey-cell (D-256(b))
/// terrain profiles covering the whole body. Stored in `BodyWorldState.districts`.
#[derive(Debug, Clone, Default)]
pub struct LayerDistrictOutput {
pub districts: std::collections::BTreeMap<DistrictPos, DistrictProfile>,
pub districts: std::collections::BTreeMap<SurveyCellPos, DistrictProfile>,
}
/// Region climate layer output (T-1113, D-243 §3): per-region (~205 km) climate
@@ -120,6 +120,12 @@ pub struct LayerDistrictOutput {
/// cache pads a neighbour ring because its edge-fuzz blend samples across
/// boundaries; that padding is a blend implementation detail, not part of the
/// body's own region grid, and a dense Atlas wire encoding wants exact dims.)
///
/// **D-256(f):** the build still keys off the SurveyCellPos-shaped pseudo-grid
/// dims rather than the true D-243 district grid — a fenced, deliberate defer
/// to T-1181's rung-0 Global canvas, not a fix this ticket makes. Verified
/// safe: the sole reader is the `region_grid` Atlas overlay, which reads no
/// `DistrictProfile` climate to disagree with.
#[derive(Debug, Clone, Default)]
pub struct LayerRegionOutput {
pub regions: std::collections::BTreeMap<RegionPos, RegionProfile>,
@@ -154,11 +160,13 @@ impl CascadeSnapshot {
let road_graph = self.road_graph.unwrap_or_default();
// terrain_analysis (transient) is intentionally dropped here.
let _ = self.terrain_analysis;
let sea_level = self.heightmap.sea_level;
BodyWorldState {
body_id: self.body_id,
heightmap: self.heightmap.data,
heightmap_width: self.heightmap.width,
heightmap_height: self.heightmap.height,
sea_level,
river_network,
drainage_basins,
attractors,
@@ -301,10 +309,13 @@ pub fn run_cascade_from_heightmap(
// domain separation — derive_all_districts builds the region
// cache internally.
//
// district_basin_dirs from Layer1Output threads the true D8
// thalweg direction into each DistrictProfile.basin_direction
// (T-1047). Pass the map through derive_all_districts.
let basin_dirs = snapshot.layer1.as_ref().map(|l1| &l1.district_basin_dirs);
// Layer1Output.survey_basin_dirs threads the true D8 thalweg
// direction into each DistrictProfile.basin_direction (T-1047)
// — the VALUES are true D8 aggregates, the KEYS are survey
// cells (D-256(b); `derive_all_districts` looks it up by
// identity, matching this map's own key space). Pass the map
// through derive_all_districts.
let basin_dirs = snapshot.layer1.as_ref().map(|l1| &l1.survey_basin_dirs);
// T-1168 Ruling 4c: same `snapshot.layer1` source as
// `basin_dirs`/`river_cells` (the `road_graph` precedent
// below) — the river network for the batch-path riparian
@@ -372,6 +383,18 @@ pub fn run_cascade_from_heightmap(
// no TerrainAnalysis needed, so it runs outside the transient-borrow block
// above. Gates on body_params like the DistrictProfile layer (no params →
// no climate inputs → the layer skips, `regions` stays empty).
//
// D-256(f) FENCED, NOT FIXED BY THIS TICKET: this block still treats the
// survey-raster dims (`district_cols`/`district_rows` below, really
// SurveyCellPos counts) as if they were true district counts and maps
// them straight through `scale::district_to_region` — the SAME pseudo-grid
// collapse D-256(c) fixed for `DistrictProfile`'s own region baseline.
// Deliberately deferred to T-1181's rung-0 Global canvas (D-256 ruling's
// tripwire, verified: the sole production reader of `LayerRegionOutput`/
// `regions` is the `region_grid` body-view overlay — no consumer reads it
// against `DistrictProfile` climate, so this collapse never disagrees
// with anything this ticket's scope touches). The overlay stays visibly
// stale until T-1181 replaces it — accepted, noted, not silently ignored.
if up_to >= CascadeLayer::Region {
if let Some(params) = body_params {
// The covering region grid: the same district dims the district
@@ -589,7 +612,7 @@ mod tests {
assert_eq!(p1.river_threshold, p2.river_threshold);
assert_eq!(p1.tectonic_class, p2.tectonic_class);
assert_eq!(p1.glaciation_grade, p2.glaciation_grade);
// basin_direction threads run_layer1 -> district_basin_dirs -> here;
// basin_direction threads run_layer1 -> survey_basin_dirs -> here;
// guard the full chain's determinism (T-1047).
assert_eq!(p1.basin_direction, p2.basin_direction);
}