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:
2026-07-23 15:02:16 +02:00
co-authored by Claude Fable 5
parent 581be31549
commit d256233faf
10 changed files with 2325 additions and 53 deletions
+26
View File
@@ -30,6 +30,32 @@
//! sea-adjacent mouths (`RIVER_DOWNSTREAM_MOUTH`). `river_cells`/`attractors`
//! counts are unchanged (93/256) — this is a pure re-classification + one new
//! additive field, not a drainage-algorithm change.
//!
//! **Attractor cascade correction (T-1170 PR #197 review, Hoshe #4):** the A1
//! commit's "pure reclassification" framing overclaimed — `attractors` is
//! count-PARITY (256/256), not byte-identical. Mouths dropping 19→3 shrinks
//! `TerrainAnalysis::water_dist`'s seed set (`compute_water_dist` seeds from
//! `river_network.mouths`), which shifts the water-distance field, which
//! feeds `RawAttractor::strength` scoring in `features::extract_attractors`.
//! This is a principled, in-scope cascade (the pole-edge cells genuinely
//! aren't water-distance sources anymore) — not a bug — but it is a REAL
//! field-level change, not a no-op re-tagging. Restated accurately here so
//! the record doesn't imply byte-identical attractor output.
//!
//! **Second deliberate re-pin (T-1170 PR #197 review, Hoshe #1):**
//! `RiverNetwork` gained a second additive field, `river_seaward:
//! Vec<(u16,u16)>` — the real seaward neighbor position for MOUTH-sentinel
//! river cells, captured in the SAME `extract_river_network` pass (the
//! elevation check that already computes `(nr, nc)` to decide the MOUTH
//! sentinel). Fixes a second instance of the discard-then-need-it-later
//! anti-pattern Ruling 2b's `river_downstream` field fixed for interior D8
//! pointers: `river_course::build_edges` needs a real seaward cell to give
//! Mouth edges a non-degenerate chord (see `river_course.rs`'s
//! `build_edges` doc and `layer_proxy::tests::
//! all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none` for the full
//! bug/fix story). `river_cells`/`mouths`/`attractors` counts unchanged by
//! this second re-pin (93/3/256) — purely the new parallel array, 3 non-
//! placeholder entries (one per real mouth).
use std::path::PathBuf;
+1
View File
@@ -559,6 +559,7 @@ fn generate_atlas_layer_response_fixtures() {
mouths: vec![(12, 58)],
river_class: vec![1, 2],
river_downstream: vec![2, 8], // 2=E direction; 8=MOUTH sentinel
river_seaward: vec![(0, 0), (12, 60)], // meaningful only for the MOUTH entry
},
drainage_basins: vec![DrainageBasin {
basin_id: 1,
+374
View File
@@ -8342,6 +8342,380 @@
9,
2,
8
],
"river_seaward": [
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
39,
47
],
[
38,
99
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
0,
0
],
[
125,
240
]
]
}
},
File diff suppressed because it is too large Load Diff
+63
View File
@@ -444,6 +444,20 @@ fn golden_cutoffs_match_the_scale_ladder() {
// geometry for an edge from that chain, at both District and Quarter station
// spacing, so courses themselves — not just the district-profile fields the
// main golden above covers — are regression-pinned.
//
// **Mouth coverage (T-1170 PR #197 review, Hoshe #2):** the sample set below
// also includes a real Mouth-terminus edge from `sample_hm()` (empirically
// probed: 4 mouth edges exist in this fixture) — `invent_course`'s raw output
// for that edge is pinned here (edge_id/class/terminus/points), closing the
// "zero Mouth coverage in any golden" gap at the invention layer. The FULL
// end-to-end path (invent → crop → `resolve_mouth_terminus` →
// `CourseTerminus::Mouth`) is covered separately and permanently by
// `layer_proxy::tests::all_real_gj1c_mouths_resolve_to_mouth_terminus_not_none`
// (in-crate, since `crop_course_to_window`/`resolve_mouth_terminus` are
// private to `layer_proxy.rs` and unreachable from this integration test) —
// that test is the actual Hoshe #1 acceptance criterion (3/3 real mouths);
// this golden's job is regression-pinning the raw invented geometry, not
// re-proving the crop/resolve path.
const RIVER_COURSE_GOLDEN_FILE: &str = "tests/golden/river_course_golden.json";
@@ -481,6 +495,15 @@ fn river_course_golden_samples() -> Vec<GoldenCourseSample> {
})
.expect("sample_hm() fixture must have at least one interior river edge near (10, 116)");
// Hoshe #2: a real Mouth-terminus edge, deterministically selected as the
// lowest edge_id among the fixture's mouth edges (fixed tie-break, no
// hardcoded index).
let mouth_target = edges
.iter()
.filter(|e| e.terminus == river_course::EdgeTerminusKind::Mouth)
.min_by_key(|e| e.edge_id)
.expect("sample_hm() fixture must have at least one Mouth edge (empirically verified: 4)");
let mut out = Vec::new();
for (rung, spacing_m, min_wl_m) in [
("district", DISTRICT_MIN_WL_M, DISTRICT_MIN_WL_M),
@@ -498,6 +521,20 @@ fn river_course_golden_samples() -> Vec<GoldenCourseSample> {
.map(|p| (p.0.round() as i64, p.1.round() as i64))
.collect(),
});
let mouth_course =
river_course::invent_course(seed, mouth_target, &ta, &params, spacing_m, min_wl_m);
out.push(GoldenCourseSample {
rung: format!("{rung}_mouth"),
edge_id: mouth_course.edge_id,
class: mouth_course.class,
terminus: format!("{:?}", mouth_course.terminus),
points: mouth_course
.points
.iter()
.map(|p| (p.0.round() as i64, p.1.round() as i64))
.collect(),
});
}
out
}
@@ -567,3 +604,29 @@ fn river_course_rungs_have_different_station_counts() {
quarter.points.len()
);
}
/// **T-1170 PR #197 review, Hoshe #2 (golden coverage):** the pinned Mouth
/// edge samples must be genuine, non-degenerate courses (`terminus == "Mouth"`,
/// `points.len() >= 2`) — the direct golden-level check that the Hoshe #1 fix
/// (real seaward chord via `RiverNetwork::river_seaward`) reaches this fixture
/// too, not just the dedicated GJ1c acceptance test.
#[test]
fn river_course_mouth_samples_are_non_degenerate() {
let samples = river_course_golden_samples();
for rung in ["district_mouth", "quarter_mouth"] {
let sample = samples
.iter()
.find(|s| s.rung == rung)
.unwrap_or_else(|| panic!("missing golden sample for rung {rung}"));
assert_eq!(
sample.terminus, "Mouth",
"{rung}: build_edges must produce a Mouth-terminus RiverEdge for the pinned target"
);
assert!(
sample.points.len() >= 2,
"{rung}: Mouth edge invented a degenerate {}-point course — the chord-length fix \
(Hoshe #1) regressed for this fixture",
sample.points.len()
);
}
}