feat(simulation): climate-derived fields + frozen 17-zone morphology (T-1025, T-1027)

T-1025 (D-239 §2) — climate-derived fields, all from the temperature(+moisture)
primitive:
- precipitation_class + glaciation_grade re-keyed to (temperature_c, moisture_q)
  inputs (were keyed off raw body params); river_threshold signature follows.
- New VegetationClass (Absent/Barren/Scrub/Forest/RiparianScrub/RiparianThicket)
  + derive_vegetation: treeline bands x elevation, structural Forest->Scrub->Barren
  no-skip, riparian 1-3 tile band; airless (temp None) -> Absent.
- 21 tests.

T-1027 (D-239 §5/§6/§7) — morphology classifier:
- FROZEN 17-zone MorphologyZone enum exactly per D-239 §6 (OpenOcean..Wetland,
  repr(u8) pinned). Removed legacy Sea/CoastalLowland/Island/Canyon/Unknown;
  all consumers (skeleton_gen street_topology) remapped to nearest D-239 zone.
- 8-family gated decision tree (LavaField..AlluvialPlain fallback) over integer
  inputs; hard gates: Fjord>=GlaciationGrade2, LavaField=Volcanic tectonic,
  lithology slope/form laws (§8). 4 sub-classification zones (TidalFlat,
  Estuarine, Alpine, Wetland) derived from family + elevation/water.
- Build-time compatibility-matrix invariant tests (§7): MeanderReach<->Volcanic
  and Volcanic<->Fjord forbidden adjacencies structurally prevented by gate order.
- 13 tests incl 17-variant completeness + discriminant pinning.

D-010 integer discipline throughout. cargo test 1534 pass, clippy -D warnings
clean, fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 13:12:34 +02:00
co-authored by Claude Opus 4.8
parent bca80617e6
commit 961855f276
3 changed files with 1066 additions and 144 deletions
File diff suppressed because it is too large Load Diff
+24 -12
View File
@@ -1046,19 +1046,31 @@ enum Topology {
}
fn street_topology(m: &MorphologyZone) -> Topology {
// D-234 morphology-gated trunk topology.
// Updated for D-239 §6 frozen 17-zone vocabulary (T-1027).
match m {
MorphologyZone::Fjord | MorphologyZone::Canyon | MorphologyZone::MountainPass => {
Topology::Ribbon
}
// Ribbon: linear/constrained terrain — follow the single axis.
MorphologyZone::Fjord
| MorphologyZone::CliffCoast
| MorphologyZone::MountainPass
| MorphologyZone::Alpine
| MorphologyZone::ValleyFloor => Topology::Ribbon,
// HubSpoke: water-enclosed or island-like contexts — radiate from a centre.
MorphologyZone::Delta
| MorphologyZone::Estuarine
| MorphologyZone::OpenOcean
| MorphologyZone::Lake
| MorphologyZone::Sea
| MorphologyZone::Island => Topology::HubSpoke,
| MorphologyZone::DuneStrand
| MorphologyZone::TidalFlat => Topology::HubSpoke,
// Mesh: flat/open terrain — any pattern.
MorphologyZone::AlluvialPlain
| MorphologyZone::MeanderReach
| MorphologyZone::CoastalLowland
| MorphologyZone::Unknown => Topology::Mesh,
| MorphologyZone::RiverBank
| MorphologyZone::BraidedPlain
| MorphologyZone::Wetland
| MorphologyZone::Volcanic => Topology::Mesh,
}
}
@@ -1748,7 +1760,7 @@ mod tests {
#[test]
fn hub_spoke_shares_a_common_node() {
let aps = derive_access_points(&[0, 2, 4, 6], &[]);
let corridors = derive_corridors(&aps, &MorphologyZone::Island);
let corridors = derive_corridors(&aps, &MorphologyZone::OpenOcean);
// Every spoke touches the hub.
let hub = central_node(&aps) as u16;
assert!(corridors.iter().all(|c| c.from == hub || c.to == hub));
@@ -1771,14 +1783,14 @@ mod tests {
let inland = subdivide_block_footprints(
70,
&BulkClass::NonPhysical,
&MorphologyZone::CoastalLowland,
&MorphologyZone::RiverBank,
None,
SeedChain::root(3),
);
let quay = subdivide_block_footprints(
70,
&BulkClass::NonPhysical,
&MorphologyZone::CoastalLowland,
&MorphologyZone::RiverBank,
Some(Edge::North),
SeedChain::root(3),
);
@@ -1796,14 +1808,14 @@ mod tests {
let inland = subdivide_block_footprints(
10,
&BulkClass::BulkSolid,
&MorphologyZone::CoastalLowland,
&MorphologyZone::RiverBank,
None,
SeedChain::root(11),
);
let quay = subdivide_block_footprints(
10,
&BulkClass::BulkSolid,
&MorphologyZone::CoastalLowland,
&MorphologyZone::RiverBank,
Some(Edge::North),
SeedChain::root(11),
);