From 17a582ae4d2c4188f6e1dfd3a8adf680858a95d5 Mon Sep 17 00:00:00 2001 From: Edward Langley Date: Tue, 22 Sep 2026 18:29:35 -0700 Subject: [PATCH] test(gp): pin the FitOutcome::warnings order the doc comment and gp.md both state (improvise-ipqu) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fit_measure` concatenates three warning tiers with two `splice(0..0, ..)` calls. The resulting order — the budget's notices, then the feature table's, then preprocess's — is stated in the field's doc comment and in context/code/gp.md section 14, joined into a user-facing status line, and was checked nowhere: swapping the two statements left all 74 improvise-gp tests green. No pre-existing fixture produced more than one tier, which is why nothing caught it. `all_three_warning_tiers_at_once` adds two things to the wide-category fixture: a `_Measure` item with no cell anywhere (a header column entirely missing in every row, which preprocess warns about) and a `Cost` cell at an unused coordinate (one visible record with no `Score`, which feature_table skips). Neither disturbs the budget arithmetic. The assertion is on the three positions, not on the concatenation containing them, and the helper panics on an absent tier so a tier that stops speaking fails loudly rather than making the ordering trivially true. Two rivals, each moving a different boundary, each caught only by this test. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH --- context/test-evidence/gp.md | 40 ++++++++++++++++++++++++++++++ crates/improvise-gp/src/fit.rs | 45 ++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/context/test-evidence/gp.md b/context/test-evidence/gp.md index e37a400f..9e50f066 100644 --- a/context/test-evidence/gp.md +++ b/context/test-evidence/gp.md @@ -1183,3 +1183,43 @@ The assertion that the ranking is **non-empty** is there so the test cannot pass by reporting nothing at all — which is precisely what the non-finite guard makes `importance_ranking` do, one layer down, if the overflow is ever reintroduced upstream of it. + +### fit::tests::a_budget_notice_precedes_a_table_warning_which_precedes_a_preprocess_one + +improvise-ipqu. `FitOutcome::warnings` is three tiers concatenated by two +`splice(0..0, ..)` calls in `fit_measure`, and the resulting order — the +budget's notices, then the feature table's, then `preprocess`'s — is stated in +the field's own doc comment **and** in `context/code/gp.md` §14, and is joined +into a user-facing status line. It was checked nowhere. Swapping the two +statements left all 74 `improvise-gp` tests green, measured before and after. +This is the shape gotcha 43 names — a rule "held up only by the order of two +statements" — and the repair is the same one +`a_published_basis_follows_the_grid_updates_it_describes` made: inject the swap +and watch a test fail. + +**No pre-existing fixture produced more than one tier**, which is why nothing +caught it, and building one is the whole of the work. `all_three_warning_tiers_at_once` +extends `two_categories_of_different_widths` — 60 `Wide` items against a +64-feature budget, so `Wide` is dropped and a notice is emitted — with two +further things, each aimed at exactly one of the other tiers. A `Blank` item on +`_Measure` carrying **no cell anywhere** is a header column (`raw_measures` +reads item names, not data) that is empty in every row, which is what +`preprocess` warns about. And a `Cost` cell at `(w0, n1)`, a coordinate the base +fixture does not use, is one visible record with no `Score` value, which +`feature_table` skips and warns about. Neither addition disturbs the budget +arithmetic: the skipped record contributes no row, so `Wide` is still 60 wide, +and `Blank` costs 2, leaving `60 + 8 + 1 + 2 = 71` over budget by `Wide` alone. + +Asserting that the concatenation *contains* all three would be vacuous +(`doctrine(fold_tests_must_test_order_not_the_axiom)`), so the assertion is on +the three positions. The helper resolving a tier to its index **panics** when a +tier is absent rather than returning a sentinel, so a future change that stops +one tier speaking fails here loudly instead of making the ordering assertion +trivially true over two elements. + +Two rivals, each moving a different boundary, and each caught only by this test +(74 passed, 1 failed, both times). Swapping the two `splice` statements gives +table → notice → preprocess and fails at `notice < table`. Replacing the first +`splice(0..0, table.warnings)` with `extend(table.warnings)` gives notice → +preprocess → table and fails at `table < preprocessed` — which is the rival that +earns the third tier, since the swap alone never moves `preprocess`'s. diff --git a/crates/improvise-gp/src/fit.rs b/crates/improvise-gp/src/fit.rs index 31cacef8..e5d6a192 100644 --- a/crates/improvise-gp/src/fit.rs +++ b/crates/improvise-gp/src/fit.rs @@ -785,6 +785,51 @@ mod tests { ); } + /// improvise-ipqu: the three warning tiers are concatenated, and the order + /// is user-facing. `context/test-evidence/gp.md`. + #[test] + fn a_budget_notice_precedes_a_table_warning_which_precedes_a_preprocess_one() { + let (model, view) = all_three_warning_tiers_at_once(); + let outcome = fit_measure(&model, &view, "Score", FIT_INTO, &request()) + .expect("dropping the wide column is enough"); + let at = |needle: &str| { + outcome + .warnings + .iter() + .position(|w| w.contains(needle)) + .unwrap_or_else(|| panic!("no {needle:?} warning in {:?}", outcome.warnings)) + }; + let (notice, table, preprocessed) = (at("budget"), at("no Score value"), at("Blank")); + assert!( + notice < table && table < preprocessed, + "budget notice, then the table's, then preprocess's: {:?}", + outcome.warnings + ); + } + + /// [`two_categories_of_different_widths`] plus the two things that make the + /// other two tiers speak. Reasoning: `context/test-evidence/gp.md`. + fn all_three_warning_tiers_at_once() -> (Model, View) { + let (mut model, view) = two_categories_of_different_widths(); + // A measure with no cell anywhere is a header column that is entirely + // missing in every row, which is what `preprocess` warns about. + model + .category_mut(MEASURE) + .expect("_Measure always exists") + .add_item("Blank"); + // A coordinate the base fixture does not use, carrying `Cost` and no + // `Score`: one visible record `feature_table` must skip. + model.set_cell( + CellKey::new(vec![ + ("Wide".to_string(), "w0".to_string()), + ("Narrow".to_string(), "n1".to_string()), + (MEASURE.to_string(), "Cost".to_string()), + ]), + CellValue::Number(1.0), + ); + (model, view) + } + /// 60 `Wide` items and 8 `Narrow` ones over 60 records, so the two columns /// are 60 and 8 and only one of them has to go. fn two_categories_of_different_widths() -> (Model, View) {