feat(simulation): T-1170 A2/A3 + T-1168 A5 — course inventor, coast termination, riparian signal

A2 (river_course.rs): Stage A rung-independent valley-seeking control
path (chord/8 stations, k=5 bilinear-scored candidates + continuity
penalty); Stage B rung-indexed perpendicular warp on GLOBAL arc-length
(window-independence, Ruling 1e), band chord/2 down to min_wavelength_m
hard-truncate, sine taper to zero at anchors, amplitude min(8% chord,
half-cell) slope/class-scaled. SeedDomain::RiverCourse=17, distinct
salt. Wire: RiverCourse{edge_id,class,points,terminus} on
DistrictWindowLayer.courses (serde-default); bbox-culled, window-
cropped +1 station. TerrainAnalysisCache retains Layer1Output (the
gen_queue:626 discard, Ruling 4b). A3: mouth termination walks
stations sampling the window's OWN rung-consistent morphology verdict,
6-iteration bisect; land-at-anchor probes one segment then None;
EdgeDrain never probes. A5: near_perennial_water point-to-segment
predicate (D-239 §8 governed bands 1-3m class-scaled) threaded through
both batch and window paths; Region always false; never touches
moisture_q. Discipline closed: dormant zoom-ladder bench run + numbers
recorded in the design doc (District 3.009/1.785, Quarter 1.823
us/cell, Region window 0.617ms); course-cost bench CAUGHT a real
+12-36% per-cell riparian scan regression -> precomputed bbox O(1)
reject (60ns->2.2ns/call), final delta +3.4-6.9% at budget; three
determinism tests (overlapping-window byte-identity, cross-rung
amplitude bound, warp-stream cross-correlation r<0.3); goldens: window
sweep gained a verified course-bearing position (pure append), new
river_course golden at both rungs, believability verified unchanged.
Revert-verification discovered the pole-row branch is structurally
unreachable (flow_direction bounds-check) — the real edge-drain path
is k<0 flat-plateau; test fixture rewritten to exercise reality.
scale.rs stale comment fixed. Full cargo test green.

Tickets: T-1170, T-1168

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:36:16 +02:00
co-authored by Claude Fable 5
parent c31cc6220e
commit 4320df9b60
16 changed files with 4353 additions and 78 deletions
+60 -20
View File
@@ -43,6 +43,18 @@ const D8: [(i32, i32); 8] = [
(1, -1), // SW
];
/// `(dr, dc)` for D8 direction index `k` (0-7) — the same fixed priority-order
/// table [`extract_river_network`]/`flow_direction` use internally, exposed
/// `pub(crate)` so downstream consumers of [`crate::atlas::body_world_state::
/// RiverNetwork::river_downstream`] (T-1170's river course inventor) can walk
/// a river cell's D8 pointer without duplicating the table. Panics on an
/// out-of-range index — callers must check against the
/// `RIVER_DOWNSTREAM_MOUTH`/`RIVER_DOWNSTREAM_EDGE_DRAIN`/
/// `RIVER_DOWNSTREAM_TERMINAL` sentinels (values ≥ 8) before calling this.
pub(crate) fn d8_offset(k: u8) -> (i32, i32) {
D8[k as usize]
}
/// Result of the full D8 drainage analysis for one body.
#[derive(Debug, Clone)]
pub struct DrainageResult {
@@ -1132,40 +1144,68 @@ mod tests {
#[test]
fn pole_edge_drains_are_not_mouths() {
// Ruling 3f, binding: a river that flows off the grid's polar edge is
// a grid artifact, not a river-meets-sea event. Build a tiny grid that
// slopes toward the north pole (row 0) with no ocean anywhere, so any
// river cell reaching row 0 must exit via EDGE_DRAIN, never MOUTH —
// and must never land in `mouths`.
let (w, h) = (16usize, 32usize);
// Ruling 3f, binding: a river cell with no valid D8 outflow is a grid
// artifact, not a river-meets-sea event.
//
// **Why this test targets the flat/no-outflow (`k < 0`) case, not a
// literal "flow direction points past row 0" case:** `flow_direction`
// (this file, `fn flow_direction`) bounds-checks every D8 candidate
// BEFORE comparing drops (`if nr < 0 || nr >= h { continue; }`) — a
// row-0 cell can therefore never even be ASSIGNED a north-pointing
// `fdir` in the first place; the off-grid-direction branch in
// `extract_river_network`'s `river_downstream` computation exists as
// correct defensive code but is structurally unreachable given this
// invariant. The real, reachable "pole-edge-drain" case (confirmed
// against the committed GJ1c golden fixture, which has EDGE_DRAIN
// cells at several rows including row 0) is `k < 0`: a cell with NO
// neighbor at a strictly lower elevation — most commonly a flat
// plateau at the grid's fringe, which the depression-fill/flow
// algorithm cannot route off of. This fixture constructs exactly
// that: a perfectly flat plateau at row 0 (identical elevation
// across the whole top row, so no cell in it has a positive-drop
// neighbor and `flow_direction` assigns `k=-1` — verified this
// reproduces before ever reasoning about mouths) that river cells
// from a converging valley drain into, with no ocean anywhere.
let (w, h) = (64usize, 64usize);
let n = w * h;
// Slope: elevation decreases toward row 0 (the north pole), giving a
// clean, deterministic downhill flow off the top edge. sea_level below
// everything so no cell is ever ocean.
let center_col = (w / 2) as f32;
let elev: Vec<f32> = (0..n)
.map(|i| {
let r = i / w;
0.2 + (r as f32 / h as f32) * 0.7
let c = (i % w) as f32;
if r == 0 {
return 0.15; // flat plateau — no cell here has a strictly lower neighbor
}
// V-shaped valley converging on center_col, sloping down
// toward row 0 (but never reaching the plateau's own
// elevation until row 1, so row-1 cells drain INTO the flat
// row-0 plateau and then have nowhere further to go).
let dist_from_center = (c - center_col).abs() / center_col;
let valley = 0.15 + dist_from_center * 0.6;
let pole_gradient = (r as f32 / h as f32) * 0.25;
(valley + pole_gradient).clamp(0.0, 1.0)
})
.collect();
let result = analyze(&elev, w as u32, h as u32, 0.0);
let rn = &result.river_network;
if rn.river_cells.is_empty() {
// Too small a grid to clear RIVER_THRESHOLD — nothing to assert,
// but not a test failure (the threshold is a fixed constant this
// synthetic tiny grid isn't guaranteed to reach).
return;
}
assert!(
rn.mouths.is_empty(),
"an all-land, pole-draining world must have zero mouths — got {:?}",
rn.mouths
!rn.river_cells.is_empty(),
"the converging-valley fixture must clear RIVER_THRESHOLD — if this starts \
failing, the fixture (not the production code) needs retuning, since an \
empty river_cells silently no-ops every assertion below"
);
assert!(
rn.river_downstream
.iter()
.any(|&v| v == RIVER_DOWNSTREAM_EDGE_DRAIN),
"expected at least one EDGE_DRAIN-sentinel river cell on a pole-draining world"
"expected at least one EDGE_DRAIN-sentinel river cell (the flat-plateau case) \
on this fixture — downstream values were {:?}",
rn.river_downstream
);
assert!(
rn.mouths.is_empty(),
"an all-land, pole-draining world must have zero mouths — got {:?}",
rn.mouths
);
assert!(
!rn.river_downstream