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
@@ -258,3 +258,6 @@ WORKFLOW REMINDERS for the resumer: lead drives all git; agents run lean (don''t
fmt/clippy/test — the pre-push gate enforces them + cargo test now); review caught real
bugs on EVERY PR this session (hollow tests, a cache-coherence bug hidden by a tautological
test, the +356C climate bug) — keep reviewing + verifying agent output, don''t trust "all green".', NULL, '2026-06-08 17:22:10', '2026-06-08 17:22:10', '2026-06-08 17:22:10', NULL, 'e497455bce9ece94481dcc060d7db1c0', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1031', 'status', 'in_progress', 'done', NULL, '2026-06-08 20:38:59', '2026-06-08 20:38:59', '2026-06-08 20:38:59', NULL, 'db4d162c8bddb6ead74d6d6458c1a41c', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1034', 'status', 'backlog', 'done', NULL, '2026-06-08 21:01:34', '2026-06-08 21:01:34', '2026-06-08 21:01:34', NULL, '03c07199221bba3a0305d5e0e2d81ec5', 1) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-974', 'status', 'backlog', 'done', NULL, '2026-06-08 21:01:34', '2026-06-08 21:01:34', '2026-06-08 21:01:34', NULL, 'eb367d867a3258091fc50d8ad6743748', 1) ON CONFLICT(hash) DO NOTHING;
+51
View File
@@ -679,3 +679,54 @@ WORKFLOW REMINDERS for the resumer: lead drives all git; agents run lean (don''t
fmt/clippy/test — the pre-push gate enforces them + cargo test now); review caught real
bugs on EVERY PR this session (hollow tests, a cache-coherence bug hidden by a tautological
test, the +356C climate bug) — keep reviewing + verifying agent output, don''t trust "all green".', 'in_progress', 'high', NULL, 'server', 'D-239', '2026-06-07 10:34:11', '2026-06-08 17:22:10', NULL, '32ac92fca534508f3387c87bc54d9fae', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1031', 'task', 'T-974', 'Believability + determinism + <5ms budget verification harness', 'D-239 sections 1,8,9. Golden-seed regression for the binding laws (drainage monotonicity, lithology->landform, glaciation->form with no glacial-U on GlaciationGrade 0, climate->vegetation no-skip + riparian band), per-family <5ms/chunk budget assertion, and the validation-case bodies: Kallast=alluvial plain/Soil, Velen=tidal-flat/dune coast (VERIFY it has a moon param for the tidal term first), Cygni B=volcanic immature drainage, Ross 128=ocean island terrain, Gruenfeld=marginal Gravel/Scrub. If a body reads wrong, fix its params — never patch the derivation (D-239 section 1). Blocked by the family generators.
---
Note from T-1028 review (Tyre, 2026-06-08): VoxelCache LRU eviction is an O(capacity) linear scan for the minimum access_gen (default cap 8192). Latent — at the D-239 §10 ~2.2-4.2 ms/chunk budget (4096 voxels) an 8k scan is measurable noise, not a blocker. If it shows up in this ticket''s profiling, the fix is a secondary BTreeMap<(access_gen, CacheKey), ()> eviction index (stays D-010-compliant). Scope before the budget test runs.
---
RESUME NOTE (2026-06-08, context reset). Where T-974 stands and how to finish it.
STATUS: Epic T-974 (Atlas-to-tile derivation) is ~done. Merged this session:
T-1023/1024/1025/1026/1027/1028/1029/1030/1032 + T-1033 (class-envelope temperature,
D-240). Only T-1031 (this ticket) + T-1034 remain to close T-974.
D-240 (just landed): registry orbit/star data is non-canonical placeholder; temperature
derives from planet_class envelope + atmosphere + hydrosphere(maritime moderation) +
seed — NO orbit/star inputs. This FIXED the +356C "temperate" worlds. BodyParams was
stripped of orbital_period_days / spectral_class / star_type / axial_tilt_deg.
T-1031 IS PARKED in a worktree at .worktrees/verification-harness (branch
verification-harness), created BEFORE T-1033. The harness (server/tests/derivation_harness.rs)
is built: golden-seed determinism + §8 law sweeps + per-family budget + validation bodies
(Kallast pass; Velen/Gruenfeld skipped-not-in-wiki). It also made derive_morphology_zone pub.
TO FINISH T-1031 (3 steps):
1. Rebase/merge current main into the verification-harness worktree. It will then FAIL TO
COMPILE: the harness test fixtures construct BodyParams with the now-removed fields
(orbital_period_days/spectral_class/star_type/axial_tilt_deg) — strip those from the
fixtures. Also re-check any validation-body temperature assertions against the NEW
class-envelope temps (the harness previously noted Marevna -56C as a param issue — that
may now read differently).
2. Generate the golden against the CORRECTED temps: from the worktree,
`cd server && UPDATE_GOLDEN=1 cargo test --test derivation_harness -- golden_seed_determinism_regression`
then commit tests/golden/derivation_harness.json + the harness + the region_profile.rs
pub-visibility change together.
3. Lead verify (cargo test/clippy -D warnings/fmt — the push gate runs all), then /pr-review
(Hoshe+Tyre) -> merge -> mark T-1031 done -> T-974 CLOSES.
ALSO PENDING (separate, fast-follow):
- T-1034 (high, under T-974): derive_moisture_q hydrosphere vocab mismatch — expects ''ocean''
but DB uses ''liquid_water'' (175 bodies) -> wrong moisture/precip/glaciation/vegetation.
The T-1033 [hydrosphere_maritime] table in climate_constants.toml has the real vocab as a
reference. NOTE: this is now important (water worlds get right temp, wrong moisture).
- Flaky test (no ticket yet, offer to file): voice::lookup::cache_miss_returns_base_text
intermittently fails the full-suite push gate under parallelism (passes in isolation;
unrelated to climate work). CI-hygiene.
WORKFLOW REMINDERS for the resumer: lead drives all git; agents run lean (don''t pre-run
fmt/clippy/test — the pre-push gate enforces them + cargo test now); review caught real
bugs on EVERY PR this session (hollow tests, a cache-coherence bug hidden by a tautological
test, the +356C climate bug) — keep reviewing + verifying agent output, don''t trust "all green".', 'done', 'high', NULL, 'server', 'D-239', '2026-06-07 10:34:11', '2026-06-08 20:38:59', NULL, '1120adccd354f2864b061fa8dba28b1b', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-1034', 'bug', 'T-974', 'derive_moisture_q hydrosphere vocab mismatch — liquid_water etc. fall to default', 'derive_moisture_q (region_profile.rs) matches hydrosphere on ''ocean''/''rivers''/''ice''/''subsurface''/''none'', but the actual bodies.hydrosphere vocab in systems.db is: liquid_water (175 bodies), ice (962), rivers (31), moderate (21), ocean (only 19), minimal (10), trace (8), subsurface_liquid (7), rivers-lakes (4), extensive (4), subsurface_ice (3), ocean-coastal (1), plus NULL (1649). So ''liquid_water'' and most surface-water worlds fall to the ''_ => 30'' default instead of high moisture — propagating wrong precipitation_class / glaciation_grade / vegetation_class for ~175+ water worlds. Same root cause as D-240 (code keying on a vocab the data doesn''t use). Fix: align derive_moisture_q (and audit any other hydrosphere consumer) to the real vocab — surface liquid (liquid_water/ocean/ocean-coastal/extensive) -> high; rivers/rivers-lakes/moderate -> mid; ice/subsurface* -> low; minimal/trace/none/NULL -> arid. T-1033''s maritime_factor table already maps the real vocab and can be the reference. Found during T-1033 verification (2026-06-08).', 'done', 'high', NULL, 'server', 'D-240', '2026-06-08 17:00:07', '2026-06-08 21:01:34', NULL, '99d8cef76a78b012c9838f1dd033fa62', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-974', 'epic', 'T-750', 'Atlas-to-tile derivation model', 'Foundational world-derivation model from the atlas-derivation workshop. D-227 (deterministic-rebuild world model: pure function of seed+atlas, derived on demand + cached, only tile mutators persisted, fully volumetric voxel/subvoxel, vertical extent physical not floor-count, variable-height floors) + D-228 (composite tile schema: orthogonal axes TerrainMaterial/FloorMaterial/Vegetation/Water/elevation, derived shape/tactical-form, region-level morphology zones, cohesion matrix, dynamic clock-bound region seasonal state). Most implementation lands as the cascade reaches the tile layers (Phase 4+). Open design tracked in Q-100 (biome authority), Q-101 (refinement contract + body-class river density + ocean mask), Q-102 (cohesion-matrix algorithm), Q-103 (mutator op schema), Q-104 (floor-to-voxel-z mapping), Q-105 (region seasonal/clock state: water/snow/weather/crops).', 'done', 'high', NULL, 'server', 'D-228', '2026-05-25 10:51:49', '2026-06-08 21:01:34', NULL, '1c48b836fb7b63e287ab13872c6f0aca', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
+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",