diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index f881ebbd1..96b9d8f09 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1771,6 +1771,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser - **Raised by:** tile-derivation-contract workshop (Tyre — refinement chain, warp, determinism; Gestalt — 8 families, game-feel; Troblum — feasibility, warp precision, scale corrections; Miri — believability laws, vocabulary, lore reconciliation), lead-interviewed decisions + an adversarial verification pass, 2026-06-07. - **Dissent:** Tyre's initial cross-family elevation-blend was resolved against (prevent-at-source). Early-integer-truncation of the warp (raised against Gestalt's `ElevationDelta` ranges and by Tyre) was resolved against in favour of f64-to-voxel. - **Implementation note (T-1024, 2026-06-07):** §1 per-body `RIVER_THRESHOLD` is now a derived field (`derive_river_threshold`) on `RegionProfile`. §2 district temperature is a nullable `f32` on `RegionProfile` sampled from a per-region climate derivation (`derive_temperature_c`); moisture is a separate integer primitive (`derive_moisture_q`, 0–100). Both populate the `RegionProfile` carrier during the L4 cascade run. Climate inputs use a **hybrid strategy**: stellar luminosity and spectral class are sourced from the import pipeline (`bodies.axial_tilt_deg`, `star_systems.spectral_class`); greenhouse offsets and diurnal amplitudes are tunable at runtime via `server/data/climate_constants.toml` (source-canonical TOML, not hardcoded). +- **Implementation note (T-1025/T-1027, 2026-06-08):** §6's frozen 17-zone `MorphologyZone` enum is realised in `server/src/simulation/generator.rs` (`repr(u8)`, discriminant-pinned). The §5 8-family gated classifier + §7 compatibility-matrix invariants live in `derive_morphology_zone` (`region_profile.rs`). **16 of the 17 zones are reachable at RegionProfile scale; `BraidedPlain` is the exception** — distinguishing it from `Delta` needs a lithology signal (§8 Gravel→braided) that `RegionProfile` does not carry, so `BraidedPlain` is **deferred to ChunkContext** sub-classification. The §7 compatibility invariant is enforced as a **classifier-gate-ordering** property (a build-time test that a single region cannot yield a forbidden pair), per §7's "build-time test" language; genuine cross-region sharp seams (cliff↔fjord, lithology faults) remain permitted. §2 climate-derived fields (`precipitation_class`, `glaciation_grade`, `vegetation_class`) all derive from the temperature(+moisture) primitive (T-1025). - **Cross-reference:** [D-227](#d-227) (derive-don't-store voxel model), [D-228](#d-228) (composite tile axes / cohesion / seasonal state), [D-210](#d-210) (temperature proxy — formalised), [D-203](#d-203) (BodyWorldState cache), [D-206](#d-206) (background analysis pass), [D-208](#d-208) (drainage / D8), [D-010](#d-010) (determinism), [D-234](#d-234) (street/footprint geometry — consumes morphology), [D-142](content.md#d-142) (zone types), [D-217](#d-217) (tile condition), [Q-102](../questions/architecture.md#q-102) (cohesion = the warp), [Q-103](../questions/architecture.md#q-103) (mutator schema — open), [Q-105](../questions/architecture.md#q-105) (seasonal/clock state — temperature/ElevationDelta forward contract) --- diff --git a/server/src/atlas/region_profile.rs b/server/src/atlas/region_profile.rs index 132d75d47..77c503ed0 100644 --- a/server/src/atlas/region_profile.rs +++ b/server/src/atlas/region_profile.rs @@ -366,11 +366,16 @@ pub fn derive_vegetation( }; let temp_i = temp_c as i32; - // Hyper-arid or extreme cold → Barren regardless of elevation. - // These are the absolute limits; no vegetation survives. - if moisture_q < 5 || temp_i < -50 { - return if near_perennial_water && moisture_q >= 5 { - VegetationClass::RiparianScrub // water creates a micro-oasis even in arid extremes + // Extreme cold → Barren regardless of water proximity: below ~−50 °C mean + // there is no liquid surface water, so a perennial-waterway micro-oasis is + // physically impossible (surface ice is geology per D-227, not vegetation). + if temp_i < -50 { + return VegetationClass::Barren; + } + // Hyper-arid → Barren, but a perennial waterway sustains a riparian micro-oasis. + if moisture_q < 5 { + return if near_perennial_water { + VegetationClass::RiparianScrub } else { VegetationClass::Barren }; @@ -479,11 +484,17 @@ pub fn derive_river_threshold(tectonic: TectonicClass, precip: PrecipitationClas /// 8. MeanderReach — gate: gentle slope + some water presence /// 9. AlluvialPlain — fallback (§5) /// -/// Sub-classifications (§6): after family selection, apply: -/// - Alpine: IncisedGorge family + elev_q ≥ ALPINE_ELEV_THRESHOLD -/// - Wetland: AlluvialPlain/Delta family + slope_q ≤ 5 + moisture ≥ 60 (§8 Wetland ≤5°) -/// - TidalFlat: AlluvialPlain/CliffCoast family + ocean_fraction_q ≥ TIDAL_OCEAN_MIN + very low elev -/// - Estuarine: Delta family + ocean signal (brackish tidal zone at river mouth) +/// Sub-classifications (§6): after family selection, apply. NOTE: the §6 "parent +/// family" names are descriptive of the typical source, not exhaustive — these +/// zones derive from morphological conditions (low elev + tidal signal; flat + +/// moist) that legitimately overlap several families, so a zone can arise from +/// more than one family context. The actual emission sites: +/// - Alpine: IncisedGorge family + elev_q ≥ ALPINE_ELEV_THRESHOLD. +/// - Wetland: flat (slope_q ≤ 5) + moisture ≥ 60; emitted from the AlluvialPlain fallback AND within the MeanderReach gate (§8 Wetland ≤5° flats). +/// - TidalFlat: very low elev + ocean signal; emitted from the BraidedDelta and DuneStrand gates AND a standalone low-coast gate after Family 5. +/// - Estuarine: Delta family + strong ocean signal (brackish tidal zone, river mouth). +/// - ValleyFloor and RiverBank are their own gates (ValleyFloor between IncisedGorge and MeanderReach; RiverBank within the MeanderReach gate). +/// - BraidedPlain (§6) is NOT emitted at region scale — distinguishing it from Delta needs a lithology signal (§8 Gravel→braided) RegionProfile lacks; deferred to ChunkContext (see the D-239 §6 implementation note). /// /// D-010: all gates are integer comparisons. No float arithmetic in this function. fn derive_morphology_zone( @@ -1609,9 +1620,10 @@ mod tests { } #[test] - fn morphology_zone_has_exactly_17_variants() { - // Enumerate all reachable zones through the classifier across a - // representative grid of inputs to verify all 17 are reachable. + fn morphology_zone_region_scale_emits_16_of_17() { + // Enumerate the zones the region-scale classifier can emit across a + // representative input grid. 16 of the 17 frozen zones are reachable here; + // BraidedPlain is the lone exception — see the assertion comment below. use std::collections::BTreeSet; let mut seen: BTreeSet = BTreeSet::new(); @@ -1636,13 +1648,15 @@ mod tests { } } } - // BraidedPlain is not yet emitted by classifier (it's a future ChunkContext - // sub-classification at finer scale); all others must be reachable. - // Assert at least 16 of 17 zones are reachable via the region-scale classifier. - assert!( - seen.len() >= 16, - "only {}/17 zones reachable by classifier: {seen:?}", - seen.len() + // BraidedPlain (discriminant 11) is the ONE frozen zone the region-scale + // classifier never emits: distinguishing it from Delta needs a lithology + // signal (§8 Gravel→braided) that RegionProfile doesn't carry yet, so it is + // deferred to ChunkContext (D-239 §6 implementation note). All other 16 are + // reachable. (Lake IS reachable — ocean_fraction 60–79 → Lake, ≥80 → OpenOcean.) + assert_eq!( + seen.len(), + 16, + "expected exactly 16/17 region-reachable zones (BraidedPlain deferred), got: {seen:?}" ); }