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:
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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, ¶ms, 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user