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:
@@ -1927,7 +1927,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser
|
||||
- **Amendment (T-1084, 2026-07-05 — v1 implementation scope, PR #172 H2/C4):** the first implementation lands a **narrower pass than item 5 specifies**, recorded here so the record and code agree (the [D-239](#d-239) T-1080/81/82 postscript pattern):
|
||||
- **Only the Soil-derived vegetated/wet classes take the mosaic** in v1 — Wetland, Forest, Grassland (the `apply` gate in `voxel.rs`). The **material-driven** families (Sand, Rock, Gravel, Lava) are gated `_ => false`, so the material-variant rows of the table above — dune crest/**hardpan**, **scree**/talus, dry-wash Gravel, and every Lava row — are **authored but not yet constructed on any reachable path**, and the two new `TerrainMaterial::{Hardpan, Scree}` values are consequently unreachable in v1. A material change on those families must respect the family's own lithology and carved geometry (shape-aware §8 reconciliation), so they are the deferred follow-up. Item 5's promise that the flat families take the *full* (material + micro-relief) mosaic is **deferred, not delivered** at v1.
|
||||
- **No elevation change in v1.** The item-5 micro-relief nudge (dune crest/slack, bog lows) and the non-structural dramatic-family pass are both deferred — v1 modulates **vegetation + water only**, never `elevation_m`. The D-239 §8 **lithology** law therefore holds trivially: material and shape are left exactly as the family generator set them.
|
||||
- **For the three classes it does touch, the mosaic OWNS the §8 climate→vegetation law** (C4), not merely stays inside it. The **no-skip** half holds — no palette emits `Barren` in a Forest / Wetland / Grassland zone. The **climate-appropriateness** half is enforced by the Grassland apply-gate `!Barren`: a climatically-barren district (frozen `< -50 °C` = surface ice / geology per D-239 §2, or hyper-arid `moisture_q < 5` — both resolved to `VegetationClass::Barren` upstream by `derive_vegetation`) is **skipped**, so the mosaic never re-grants cover the climate withheld (the C1 fix: Edict, a frozen ice world, must not read as vegetated tundra — [D-245](#d-245) §5). The trade: the family generators' *guaranteed* riparian Thicket band is softened to a **probabilistic** wet-habitat clustered near `has_active_channel` chunks (T-1040-gated) — acceptable at v1.
|
||||
- **For the three classes it does touch, the mosaic OWNS the §8 climate→vegetation law** (C4), not merely stays inside it. The **no-skip** half holds — no palette emits `Barren` in a Forest / Wetland / Grassland zone. The **climate-appropriateness** half is enforced by a **unified `!Barren` apply-gate spanning all three classes** (C1 + N1): a climatically-barren district (frozen `< -50 °C` = surface ice / geology per D-239 §2, or hyper-arid `moisture_q < 5` — both resolved to `VegetationClass::Barren` upstream by `derive_vegetation`) is **skipped**, so the mosaic never re-grants cover the climate withheld. The guard must span **Wetland as well as Grassland**: `derive_morphology_zone` has no temperature gate, so a frozen + wet + flat district still yields Wetland material and would otherwise be painted with wetland copse/bog — the same C1 symptom via the material path (a frozen ice world must read as barren ground, not as vegetated tundra *or* marsh — [D-245](#d-245) §5). Forest is `Barren`-free by construction (`SurfaceClass::Forest` requires `dominant_veg == Forest`), so the guard is a no-op there. The trade: the family generators' *guaranteed* riparian Thicket band is softened to a **probabilistic** wet-habitat clustered near `has_active_channel` chunks (T-1040-gated) — acceptable at v1.
|
||||
- **Cross-reference:** [D-228](#d-228) (the axes this modulates — amended 2026-07-02 to add the five values), [D-227](#d-227) (derive-don't-store — the tier the mosaic lives in), [D-245](#d-245) (believability gate — this is condition 3, intra-class variety), [D-239](#d-239) §8 (binding laws — the mosaic stays inside them) / §10 (sub-chunk budget), [D-243](#d-243) §2 (the detail-scatter tier T-1081 and this extend), [D-247](content.md#d-247) (native-mirror vs introduced-Earth **biosphere register** — palette-key input 3; the mosaic is chirality-blind, but its register labels each niche native or introduced), [D-210](#d-210) / [T-948](../../.pql) (SubBiomeVariant — now **palette-key input 2**: the macro biome realized as the finer intra-class distribution, not a single tag). Tickets: T-1084 (this), T-1079 (epic), T-1081 (relief — a conditioning input + the reused machinery), T-1083 (enforcer — measures the mosaic), [Q-123](../questions/architecture.md#q-123) item 3 (calibrates K).
|
||||
- **Dissent:** None
|
||||
|
||||
|
||||
+47
-13
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user