fix(simulation): address PR #172 review — C1 over-vegetation + H1 biosphere gate (T-1084/T-1085)

Hoshe + Tyre review of the micro-habitat-mosaic branch requested changes on two
data-confirmed correctness bugs plus follow-through. All addressed.

Blocking:
- C1 (voxel.rs): the mosaic re-granted vegetation the climate withheld — a frozen
  ice world (Edict) read 61% vegetated tundra. The Grassland apply-gate now fires
  only where the climate already grants cover (!Barren); a climatically-barren
  district (frozen < -50C = surface ice/geology D-239 §2, or hyper-arid moisture<5,
  both resolved to VegetationClass::Barren upstream) is skipped. Genuine tundra
  (cold + moisture>=15) is Scrub upstream and still reaches the tundra palette via
  the same !Barren path. believability.json regenerated: Edict vegetated_districts
  39->0, Arbour 20->8 (its real cold-pole/arid districts revert to barren);
  intra-class variety preserved (Arbour vegetation_classes 3, micro_habitat_distinct 2).
- H1 (bodies.py): biosphere Gate 0 keyed on `inhabited`, conflating settlement with
  biosphere and force-Airless'ing uninhabited-but-alive worlds (D-247: worlds were
  alive before humans). Gate 0 is now settlement-independent — keyed on
  atmosphere/hydrosphere/planet_class. 24 uninhabited-alive worlds now classified
  (was 0); GJ0g-1 (dense atm + rivers) -> NativeMirror. Split 122 Compatible :
  121 Mirror; all 9 canon exemplars hold.

Follow-through:
- C2 (bodies.py + D-247): Gate 0 now reads planet_class (barren -> Airless) as
  D-247's "+ planet_class" phrasing names. D-247 amended to record the actual
  two-gate signal set (atm/hydro/planet_class habitability; economic_base_primary
  chirality; settlement-independence).
- C3 (believability.rs): micro_habitat_distinct criterion relaxed to `== 0 || >= 2`
  — the min-over-patches estimator makes K=3 unreachable against a dominant-entry
  palette; K=2 provisional, Q-123 calibrates.
- C4 + H2 (D-246): amended to record the v1 scope cut — only the Soil-derived
  vegetated/wet classes take the mosaic; material-driven families are `_ => false`
  so Hardpan/Scree are authored but unreachable in v1; no elevation change in v1;
  the mosaic OWNS the §8 climate->vegetation law for the classes it touches.
- H3 (voxel.rs): three dedicated mosaic-pass regression tests — C1 barren-respect,
  §8 material-untouched + forest-textured, T-1040 no-water-without-channel.
- H4 (voxel.rs): relief_signal comment corrected to ~[-1,+1].
- H5 (bodies.py): frontmatter override now strips quotes + validates against the
  4-value enum (warn-and-skip on typo).

systems.db regenerated + stamped (H1). believability golden regenerated (C1).
Full cargo test green; clippy -D warnings + ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 17:46:19 +02:00
co-authored by Claude Opus 4.8
parent b71df601b2
commit c17b4f5442
9 changed files with 239 additions and 41 deletions
+16 -6
View File
@@ -113,10 +113,16 @@ pub struct ContrastMetrics {
/// traverses — not the single 64 m sample chunk, which is too narrow to register it.
pub voxel_relief_m: i32,
/// Minimum distinct micro-habitat axis-triples (terrain × vegetation × water) found
/// in any single sampled VEGETATED-land district (T-1084, D-246). The D-245 intra-class
/// variety screen: a vegetated patch must show ≥K distinct micro-features, never 100%
/// one — near 1 on the pre-mosaic uniform world ("Wetland 100%"), ≥3 once the mosaic
/// textures the class. Barren / water patches are excluded (legitimately uniform).
/// in any single sampled VEGETATED-land district (T-1084, D-246); `0` = no vegetated
/// patch was sampled (barren / ocean world). The D-245 intra-class variety screen: a
/// vegetated patch must show ≥K distinct micro-features, never 100% one — near 1 on the
/// pre-mosaic uniform world ("Wetland 100%"). Barren / water patches are excluded
/// (legitimately uniform). NOTE (PR #172, C3): this is a *minimum over patches* against
/// palettes with a dominant entry (e.g. dense stand 40/90), so a single 64 m chunk
/// often sits in the dominant bucket → the provisional floor is K=2 ("no vegetated
/// patch is 100% one micro-habitat"), the achievable, meaningful bar. The estimator
/// (min vs mean/median), the final K, and same-class-only pooling are Q-123's to
/// calibrate against the enforcer's baselines.
pub micro_habitat_distinct: usize,
}
@@ -375,9 +381,13 @@ pub fn evaluate_criteria(r: &BelievabilityReport) -> Vec<Criterion> {
},
Criterion {
name: "intra-class variety",
pass: c.micro_habitat_distinct >= 3,
// 0 = no vegetated patch sampled (barren/ocean world — legitimately nothing to
// vary). Otherwise the provisional floor is K=2 = "no vegetated patch is 100%
// one micro-habitat" — the achievable bar for a min-over-patches estimator;
// ≥3 + the estimator are Q-123's to calibrate (PR #172, C3).
pass: c.micro_habitat_distinct == 0 || c.micro_habitat_distinct >= 2,
detail: format!(
"min {} distinct micro-habitats per vegetated patch",
"min {} distinct micro-habitats per vegetated patch (K≥2 provisional; Q-123 calibrates)",
c.micro_habitat_distinct
),
},
+111 -17
View File
@@ -509,7 +509,7 @@ pub fn derive_voxel_column(
);
let relief_m = (relief * VOXEL_RELIEF_SPAN_M as f64) as i32; // truncate (D-010)
column.elevation_m = (column.elevation_m + relief_m).max(0);
relief_signal = relief; // ~[-0.5, +0.5]; negative = mid-scale valley (wetter)
relief_signal = relief; // ~[-1, +1] (envelope clamps to 1); negative = valley (wetter)
}
// ── 3c. Intra-class micro-habitat mosaic (T-1084, D-246) ──────────────
@@ -520,12 +520,15 @@ pub fn derive_voxel_column(
// (mid-scale relief thalweg + district moisture) so wet habitats settle in the
// lows. This is the motivating fix for the probe's uniform "Wetland 100%".
//
// v1 scope: terrain MATERIAL is left exactly as the family generator set it, so
// the D-239 §8 lithology law holds untouched. The material-variant micro-habitats
// (hardpan / scree / dry-wash gravel) and the extreme classes (Rock / Sand /
// Gravel / Lava) need shape-aware §8 reconciliation and are a follow-up. A
// climatically-barren district stays barren (D-245 §5 climate-appropriateness).
// Within these classes this supersedes the per-voxel `scatter_vegetation` scatter.
// v1 scope: terrain MATERIAL is left exactly as the family generator set it, so the
// D-239 §8 *lithology* law holds untouched; the material-variant micro-habitats
// (hardpan / scree / dry-wash gravel) and the extreme classes (Rock / Sand / Gravel /
// Lava) need shape-aware §8 reconciliation and are a follow-up (D-246 amendment). The
// mosaic now OWNS the §8 *climate→vegetation* law for these classes: the no-skip half
// holds (no palette emits Barren in a Forest zone), and a climatically-barren district
// stays barren (D-245 §5 — the mosaic never re-grants cover the climate withheld). The
// family's guaranteed riparian band is softened to a probabilistic wet-habitat near
// channels — acceptable at v1. Within these classes this supersedes `scatter_vegetation`.
if !matches!(family, MorphologyFamily::WaterBody) {
use crate::atlas::mosaic::{self, SurfaceClass};
let dominant_veg = Vegetation::from_vegetation_class(district_eff.vegetation_class);
@@ -536,16 +539,14 @@ pub fn derive_voxel_column(
mosaic::ClimateBiome::from_climate(district_eff.temperature_c, district_eff.moisture_q);
let apply = match class {
SurfaceClass::Wetland | SurfaceClass::Forest => true,
// A vegetated grassland always; a barren-dominant one only when the biome is
// cold (tundra / boreal), where the sparse lichen/scrub mosaic is climate-
// appropriate — a hot barren district (desert on Soil) stays bare.
SurfaceClass::Grassland => {
!matches!(dominant_veg, Vegetation::Barren)
|| matches!(
biome,
mosaic::ClimateBiome::Tundra | mosaic::ClimateBiome::Boreal
)
}
// 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,
};
@@ -2535,6 +2536,99 @@ mod tests {
}
}
// -----------------------------------------------------------------------
// Intra-class micro-habitat mosaic — dedicated integration surface (T-1084,
// D-246, PR #172 H3). Locks the three load-bearing invariants of the §3c pass
// that no other test targets directly: the C1 climate-authority gate, the §8
// lithology hold, and the T-1040 hydrology guard.
// -----------------------------------------------------------------------
/// C1 regression: the mosaic must NOT re-grant vegetation the climate withheld.
/// A climatically-barren district — a frozen ice world (like Edict) or a
/// hyper-arid margin, both resolved to `VegetationClass::Barren` upstream by
/// `derive_vegetation` — reaches the Grassland surface-class (AlluvialPlain →
/// Soil, not temperature-gated), but the apply-gate is `!Barren`, so the mosaic
/// is skipped and every voxel stays Barren. Before the fix,
/// `ClimateBiome::from_climate` read the sub-zero temp as Tundra and painted
/// tundra scrub onto the ice — Edict read 61 % vegetated (D-245 §5 violation).
#[test]
fn mosaic_pass_does_not_revegetate_barren_climate() {
let frozen_barren = DistrictProfile {
vegetation_class: VegetationClass::Barren,
temperature_c: Some(-60.0), // frozen: surface ice = geology (D-239 §2)
moisture_q: 5, // also hyper-arid; keeps terrain Soil (< 60)
..alluvial_district()
};
let chunk = derive_chunk_context(42, "GJ244Ad", &frozen_barren, (10, 20), None);
for tx in [100, 137, 200, 263, 331, 404] {
let col = derive_voxel_column(42, "GJ244Ad", &frozen_barren, &chunk, tx, 90);
assert_eq!(
col.vegetation,
Vegetation::Barren,
"frozen/hyper-arid barren district must stay Barren (C1), got {:?} at x={tx}",
col.vegetation,
);
}
}
/// Positive path + §8 lithology hold: a vegetated Forest district IS textured
/// by the mosaic (≥2 distinct vegetation micro-habitats, never Barren per the
/// §8 no-skip law), while the terrain MATERIAL is left exactly as the family
/// generator set it (Soil). The mosaic owns the climate→vegetation half of §8
/// but never the lithology half (D-246 v1 scope).
#[test]
fn mosaic_pass_textures_forest_without_touching_material() {
let district = alluvial_district(); // Forest, temperate, moisture 55 → Soil
let chunk = alluvial_chunk(&district);
let mut veg_seen = std::collections::BTreeSet::new();
for tx in 100..160 {
let col = derive_voxel_column(42, "GJ1c", &district, &chunk, tx, 100);
assert_eq!(
col.terrain,
TerrainMaterial::Soil,
"mosaic must not change terrain material (§8 lithology), got {:?} at x={tx}",
col.terrain,
);
assert_ne!(
col.vegetation,
Vegetation::Barren,
"Forest zone never renders Barren (§8 no-skip), at x={tx}",
);
veg_seen.insert(format!("{:?}", col.vegetation));
}
assert!(
veg_seen.len() >= 2,
"mosaic must texture a Forest district into ≥2 micro-habitats, saw {veg_seen:?}",
);
}
/// T-1040 hydrology guard: the mosaic adds standing water ONLY where the
/// chunk's hydrology gate allows it. In a channel-free chunk
/// (`ocean_fraction_q < 10` ⇒ `has_active_channel == false`) the wet-habitat
/// water is suppressed — every voxel stays Dry — even though the Forest palette
/// contains wet habitats.
#[test]
fn mosaic_pass_adds_no_water_without_active_channel() {
let dry_forest = DistrictProfile {
ocean_fraction_q: 0, // < 10 ⇒ has_active_channel == false
..alluvial_district()
};
let chunk = derive_chunk_context(42, "GJ1c", &dry_forest, (10, 20), None);
assert!(
!chunk.has_active_channel,
"test precondition: chunk must have no active channel",
);
for tx in 100..160 {
let col = derive_voxel_column(42, "GJ1c", &dry_forest, &chunk, tx, 100);
assert_eq!(
col.water,
Water::Dry,
"no standing water may appear in a channel-free chunk (T-1040), got {:?} at x={tx}",
col.water,
);
}
}
#[test]
fn elevation_is_non_negative() {
let district = alluvial_district();