fix(simulation): unify C1 climate gate across Wetland+Forest+Grassland (PR #172 N1)

Tyre's re-review found the C1 !Barren guard only covered the Grassland arm — the Wetland arm was an unconditional => true. Since derive_morphology_zone has no temperature gate, a frozen + wet + flat district still yields Wetland material, so the wetland palette would paint copse/bog onto frozen marsh: the exact C1 symptom via the material path. Not a regression (the arm was unchanged) and unreachable by the 2 harness bodies, but the D-246 amendment + the voxel.rs comment already claim the guarantee universally, so the gate must match the claim.

Gate is now: apply = !Barren && class in {Wetland, Forest, Grassland}. Behavior change is limited to frozen/hyper-arid Wetland districts (a temperate marsh is never Barren-dominant; Forest is Barren-free by construction). believability.json unchanged (neither harness body reaches the frozen-wet case). New regression test mosaic_pass_does_not_revegetate_frozen_wetland; D-246 amendment reworded to the unified gate. Full cargo test green; clippy -D warnings clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 19:12:05 +02:00
co-authored by Claude Opus 4.8
parent 48fb749ee9
commit 895bb83bad
2 changed files with 48 additions and 14 deletions
+47 -13
View File
@@ -537,19 +537,24 @@ pub fn derive_voxel_column(
// boreal / tundra read distinctly (D-246 biome-awareness).
let biome =
mosaic::ClimateBiome::from_climate(district_eff.temperature_c, district_eff.moisture_q);
let apply = match class {
SurfaceClass::Wetland | SurfaceClass::Forest => true,
// Only where the climate already grants cover (C1 fix). A Barren-dominant
// district is Barren for a reason `derive_vegetation` already resolved —
// frozen (< -50 °C = surface ice/geology, D-239 §2), hyper-arid (moisture < 5),
// or cold-dry marginal ground — so the mosaic must NOT re-grant it (else it
// paints tundra scrub onto frozen ice worlds like Edict). Genuine tundra
// (cold + moisture ≥ 15) is already Scrub upstream, so it reaches the biome's
// tundra palette through this same !Barren path — no special cold clause needed.
SurfaceClass::Grassland => !matches!(dominant_veg, Vegetation::Barren),
// Rock / Sand / Gravel / Lava are material-driven — a §8-aware follow-up.
_ => false,
};
// Apply within the vegetated / wet land classes — but ONLY where the climate
// already grants cover (C1 + N1). A Barren-dominant district is Barren for a
// reason `derive_vegetation` already resolved — frozen (< -50 °C = surface
// ice/geology, D-239 §2), hyper-arid (moisture < 5), or cold-dry marginal ground
// — so the mosaic must NOT re-grant it, else it paints tundra scrub onto a frozen
// ice world (Edict, via the Grassland arm) or wetland copse onto a frozen marsh
// (via the Wetland arm — `derive_morphology_zone` has no temperature gate, so
// frozen + wet + flat still yields Wetland material). The `!Barren` guard therefore
// spans all three classes, not just Grassland. Genuine tundra (cold + moisture ≥ 15)
// is Scrub upstream (not Barren), so it still reaches the biome's tundra palette.
// Forest is Barren-free by construction (SurfaceClass::Forest requires
// dominant_veg == Forest). Rock / Sand / Gravel / Lava are material-driven — a
// §8-aware follow-up (D-246 amendment).
let apply = !matches!(dominant_veg, Vegetation::Barren)
&& matches!(
class,
SurfaceClass::Wetland | SurfaceClass::Forest | SurfaceClass::Grassland
);
if apply {
let mosaic_seed = SeedChain::for_body(world_seed, body_id)
.derive(SeedDomain::VoxelMosaic, 0)
@@ -2629,6 +2634,35 @@ mod tests {
}
}
/// N1 regression (PR #172 re-review): the C1 climate gate must hold on the Wetland
/// arm too, not only Grassland. `derive_morphology_zone` takes no temperature, so a
/// FROZEN + wet + flat district maps to Wetland material (`SurfaceClass::Wetland`)
/// while `derive_vegetation` returns Barren — the exact C1 symptom via the material
/// path. The unified `!Barren` gate skips it, so the frozen marsh stays Barren rather
/// than being painted with the wetland palette (copse / meadow / bog / marsh). Before
/// the N1 fix the Wetland arm was an unconditional `=> true`.
#[test]
fn mosaic_pass_does_not_revegetate_frozen_wetland() {
let frozen_wet = DistrictProfile {
vegetation_class: VegetationClass::Barren,
temperature_c: Some(-60.0), // frozen: surface ice = geology (D-239 §2)
moisture_q: 70, // ≥ 60 + flat ⇒ Wetland material (SurfaceClass::Wetland)
morphology_zone: MorphologyZone::Wetland,
slope_q: 3,
..alluvial_district()
};
let chunk = derive_chunk_context(42, "frozenmarsh", &frozen_wet, (10, 20), None);
for tx in [100, 137, 200, 263, 331] {
let col = derive_voxel_column(42, "frozenmarsh", &frozen_wet, &chunk, tx, 90);
assert_eq!(
col.vegetation,
Vegetation::Barren,
"frozen Wetland district must stay Barren (N1), got {:?} at x={tx}",
col.vegetation,
);
}
}
#[test]
fn elevation_is_non_negative() {
let district = alluvial_district();