fix(simulation): PR #197 review round — real seaward chord for Mouth edges (Hoshe #1/#2, Tyre 1/2)
The batch's real bug: extract_river_network computed the seaward neighbor (nr,nc) to decide the MOUTH sentinel then discarded it, and build_edges placeholder-pointed mouth edges at themselves — zero chord, invent_course's degenerate 1-point return, resolve_mouth_ terminus dead code on real data, ALL real mouths resolving None, and (because Ruling 3g retired the D/Q clip on the promise of real termini) mouths vanishing at District/Quarter. The second instance of the threw-away-the-answer anti-pattern Ruling 2b fixed for interior pointers. Fix: river_seaward: Vec<(u16,u16)> on RiverNetwork (additive, serde-default, parallel array; meaningful only at MOUTH entries), captured in the same extraction pass; build_edges gives Mouth edges the real one-D8-step chord. Permanent acceptance: all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none — 3/3, revert- verified failing at the golden's first mouth (38,47). Mouth golden coverage added (river_course_golden gains district_mouth/quarter_mouth samples + non-degeneracy test; the previously Interior-only filter gap closed). Tyre 1: the land-probe's dead dx/len*len arithmetic replaced — station_spacing_m threaded through the crop path, probe steps one real cell spacing, comment reconciled. Tyre 2: boxing comment reattributed to variant-size balancing (Layer1Output retention lives on TerrainAnalysisCache, not BodyWorldState). Hoshe #4: cascade_golden's doc now states the attractor cascade accurately (count-parity, not byte-identity — water_dist seeds from mouths). Full cargo test green; goldens re-pinned deliberately; bench +3.0%. Tickets: T-1170 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -176,18 +176,25 @@ pub fn pack_cell_id(cell: RiverCell) -> u32 {
|
||||
/// always be exactly parallel to `river_cells`, but a mismatched-length
|
||||
/// legacy payload must degrade to "no edges" rather than panic).
|
||||
///
|
||||
/// For `Mouth`/`EdgeDrain` termini there is no real downstream river cell to
|
||||
/// point at — the caller's termination logic (Ruling 3e/3f) walks or
|
||||
/// truncates from the upstream anchor using the edge's own direction, not a
|
||||
/// second real cell. This function still records SOME `downstream` position
|
||||
/// for those edges — one D8 step from the upstream cell in its own flow
|
||||
/// direction, when that direction is known (i.e. `river_downstream < 8`, the
|
||||
/// `Mouth` sentinel case where the OLD code already had a real direction
|
||||
/// before classifying the neighbor as sub-sea-level) — falling back to the
|
||||
/// upstream cell itself (a zero-length probe anchor) for `EdgeDrain`, where
|
||||
/// no direction survived at all. Both are legitimate: Stage A/B degrade
|
||||
/// gracefully to a near-zero-length or short edge, and A3's termination logic
|
||||
/// (layer_proxy) is what actually resolves the real geometry from here.
|
||||
/// **Mouth edges get a REAL seaward chord (T-1170 PR #197 review, Hoshe #1),
|
||||
/// not a same-cell placeholder.** The former code set `downstream = upstream`
|
||||
/// for `Mouth` edges — a zero-length chord (`chord_m < 1.0` in
|
||||
/// [`invent_course`]) that silently tripped the degenerate single-point
|
||||
/// return, which made [`crate::atlas::layer_proxy::resolve_mouth_terminus`]'s
|
||||
/// station walk a no-op (a 1-point course can't even reach that function's
|
||||
/// `pts.len() >= 2` fallback probe) — every Mouth edge resolved
|
||||
/// `CourseTerminus::None` instead of `Mouth`, DESPITE `extract_river_network`
|
||||
/// having already computed the real seaward neighbor to decide the sentinel
|
||||
/// in the first place. `RiverNetwork::river_seaward` (additive, captured in
|
||||
/// the SAME extraction pass) now carries that neighbor through; this
|
||||
/// function reads it for `Mouth` edges, giving them a genuine ~one-cell chord
|
||||
/// toward the raw sea so Stage A/B actually have something to invent and the
|
||||
/// termination walk is reachable.
|
||||
///
|
||||
/// `EdgeDrain` termini still use the upstream cell itself as a zero-length
|
||||
/// placeholder — there is no seaward neighbor for a grid-artifact exit, and
|
||||
/// none is needed: A3's `EdgeDrain` handling (Ruling 3f) never probes for
|
||||
/// water, it just ends the course at the last in-grid station.
|
||||
pub fn build_edges(rn: &RiverNetwork) -> Vec<RiverEdge> {
|
||||
let mut edges = Vec::with_capacity(rn.river_cells.len());
|
||||
for (i, &upstream) in rn.river_cells.iter().enumerate() {
|
||||
@@ -203,14 +210,17 @@ pub fn build_edges(rn: &RiverNetwork) -> Vec<RiverEdge> {
|
||||
let downstream = step_cell(upstream, dr, dc);
|
||||
(downstream, EdgeTerminusKind::Interior)
|
||||
} else if sentinel == RIVER_DOWNSTREAM_MOUTH {
|
||||
// The mouth sentinel is recorded once the D8 walk already found a
|
||||
// real neighbor cell (see `extract_river_network`); we don't have
|
||||
// that direction anymore at this layer, so use the upstream cell
|
||||
// itself as a zero-length placeholder — A3's mouth-walk logic
|
||||
// (layer_proxy, Ruling 3e) resolves the real terminus by sampling
|
||||
// the morphology water verdict outward from `upstream`, not by
|
||||
// trusting this placeholder position.
|
||||
(upstream, EdgeTerminusKind::Mouth)
|
||||
// Real seaward neighbor (Hoshe #1) — falls back to the upstream
|
||||
// cell (the old placeholder) ONLY on a legacy/pre-fix payload
|
||||
// where `river_seaward` is absent or the specific entry is the
|
||||
// unset `(0, 0)` fill AND that happens to differ from a genuine
|
||||
// seaward cell at (0,0) (an acceptable, vanishingly rare
|
||||
// degradation at the map's literal origin — never hit on any
|
||||
// real body, since (0,0) is a pole/edge pixel, never sub-sea
|
||||
// adjacent to an actual river mouth in practice).
|
||||
let seaward = rn.river_seaward.get(i).copied().unwrap_or((0, 0));
|
||||
let downstream = if seaward == (0, 0) { upstream } else { seaward };
|
||||
(downstream, EdgeTerminusKind::Mouth)
|
||||
} else {
|
||||
debug_assert_eq!(sentinel, RIVER_DOWNSTREAM_EDGE_DRAIN);
|
||||
(upstream, EdgeTerminusKind::EdgeDrain)
|
||||
|
||||
Reference in New Issue
Block a user