fix(simulation): derive_moisture_q canonical hydrosphere vocab (T-1034)

derive_moisture_q matched hydrosphere on 'ocean'/'rivers'/'subsurface', but the
real bodies.hydrosphere vocab in systems.db is dominated by 'liquid_water' (175
bodies) — so most surface-water worlds fell to the '_ => 30' default, propagating
wrong precipitation_class / glaciation_grade / vegetation. Same root cause as
D-240 (code keyed on a vocab the data doesn't use).

Align the match to the canonical vocab, grouped by available surface moisture
(mirrors the [hydrosphere_maritime] table from T-1033): surface liquid
(liquid_water/ocean/ocean-coastal/extensive) -> 80; rivers/rivers-lakes/moderate
-> 55; ice -> 20; subsurface_liquid -> 15; subsurface/subsurface_ice -> 10;
minimal/trace -> 5; none -> 0. Add a liquid_water regression test and broaden the
clamp sweep to the full vocabulary.

Closes T-1034 — last open ticket under epic T-974 (Atlas-to-tile derivation),
which is now complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 23:01:43 +02:00
co-authored by Claude Opus 4.8
parent 839ac18460
commit 4984d59bf4
3 changed files with 108 additions and 6 deletions
+54 -6
View File
@@ -847,16 +847,30 @@ pub fn derive_temperature_c(
///
/// D-239 §2: moisture is derived from hydrosphere + atmosphere; 0 = arid, 100 = saturated.
/// Integer output (D-010).
///
/// The `hydro` arms use the **actual `bodies.hydrosphere` vocabulary in systems.db**
/// — same canonical set the `[hydrosphere_maritime]` table (D-240) keys on — grouped
/// by available surface moisture. Keying on a vocab the data doesn't use (e.g. only
/// `"ocean"`, which is 19 bodies, while 175 use `"liquid_water"`) silently dropped most
/// water worlds to the default (T-1034 — same root cause as D-240).
pub fn derive_moisture_q(params: &BodyParams) -> i32 {
let hydro = params.hydrosphere.as_deref().unwrap_or("none");
let atmo = params.atmosphere.as_deref().unwrap_or("none");
let base: i32 = match hydro {
"none" => 0,
"subsurface" => 10,
// Large surface liquid — saturated.
"liquid_water" | "ocean" | "ocean-coastal" | "extensive" => 80,
// Partial surface water (rivers / lakes / seasonal) — moderate.
"rivers" | "rivers-lakes" | "moderate" => 55,
// Frozen surface water — low available moisture.
"ice" => 20,
"rivers" => 55,
"ocean" => 80,
// Buried water — minimal surface effect.
"subsurface_liquid" => 15,
"subsurface" | "subsurface_ice" => 10,
// Effectively dry.
"minimal" | "trace" => 5,
"none" => 0,
// Unknown vocab — conservative mid-low default.
_ => 30,
};
let atmo_boost: i32 = match atmo {
@@ -1465,6 +1479,23 @@ mod tests {
assert!(q >= 70, "ocean + breathable moisture {q} should be >= 70");
}
#[test]
fn moisture_q_liquid_water_is_high_not_default() {
// T-1034 regression: "liquid_water" is the dominant surface-water vocab in
// systems.db (175 bodies). It must map to the high surface-liquid band, not
// silently fall to the `_ => 30` default like it did before the fix.
let params = BodyParams {
hydrosphere: Some("liquid_water".into()),
atmosphere: Some("standard".into()),
..Default::default()
};
let q = derive_moisture_q(&params);
assert!(
q >= 70,
"liquid_water moisture {q} should be >= 70 (surface-liquid band), not the default 30"
);
}
#[test]
fn moisture_q_airless_is_zero() {
let params = BodyParams {
@@ -1478,8 +1509,25 @@ mod tests {
#[test]
fn moisture_q_clamped_to_range() {
// All combinations must stay in [0, 100].
let hydros = ["none", "subsurface", "ice", "rivers", "ocean", "unknown"];
// All combinations must stay in [0, 100]. Sweep the full canonical
// `bodies.hydrosphere` vocabulary (T-1034) plus an unknown fallback.
let hydros = [
"liquid_water",
"ocean",
"ocean-coastal",
"extensive",
"rivers",
"rivers-lakes",
"moderate",
"ice",
"subsurface_liquid",
"subsurface",
"subsurface_ice",
"minimal",
"trace",
"none",
"unknown",
];
let atmos = [
"none",
"thin",