From e4d57abb162af1732deedf810bb917ad6feab8bf Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sun, 18 Oct 2026 22:21:07 +0200 Subject: [PATCH 1/6] test(core): multinomial head stored-coefficient, error and lambda edges (FALSIFY-MH-013) Agent: aprender-w5018 Co-Authored-By: Claude Sonnet 5.5 --- contracts/multinomial-head-v1.yaml | 5 + .../tests/multinomial_head_edges.rs | 316 ++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 crates/aprender-core/tests/multinomial_head_edges.rs diff --git a/contracts/multinomial-head-v1.yaml b/contracts/multinomial-head-v1.yaml index fd5e405602..ebef99c7f4 100644 --- a/contracts/multinomial-head-v1.yaml +++ b/contracts/multinomial-head-v1.yaml @@ -330,6 +330,11 @@ falsification_tests: prediction: Two identical fits produce bitwise-identical stored f32 coefficients and byte-identical canonical JSON test: cargo test -p aprender-core --features setfit --lib two_identical_fits_produce_bitwise_identical_f32_weights if_fails: Randomness or a wall-clock field entered the fit path, breaking the reproducibility record +- id: FALSIFY-MH-013 + rule: A head loaded from stored coefficients validates its shape, and its error text and lambda relation are exact + prediction: from_stored_coefficients rejects every malformed shape with the matching HeadInputError; SklearnEquivalentC resolves to 1/(2*C*n); predict guards NotFitted and FeatureDimMismatch + test: cargo test -p aprender-core --test multinomial_head_edges + if_fails: A corrupt stored head loads silently, or the C-to-lambda relation drifts from sklearn kani_harnesses_note: > NOT EXECUTED. No #[kani::proof] harness exists anywhere in crates/ and cargo-kani is not installed repo-wide, so every entry below is a DECLARED obligation, not a diff --git a/crates/aprender-core/tests/multinomial_head_edges.rs b/crates/aprender-core/tests/multinomial_head_edges.rs new file mode 100644 index 0000000000..3675587397 --- /dev/null +++ b/crates/aprender-core/tests/multinomial_head_edges.rs @@ -0,0 +1,316 @@ +//! Public-API coverage for the multinomial head: stored-coefficient loading, +//! error rendering, regularisation and prediction guards (coverage ticket, Rule 7). +//! Contract: FALSIFY-MH-013 in contracts/multinomial-head-v1.yaml. + +use aprender::classification::multinomial::{ + HeadFitError, HeadInputError, MultinomialLogisticRegression, Regularization, +}; + +fn labels(names: &[&str]) -> Vec { + names.iter().map(|s| (*s).to_string()).collect() +} + +/// A 2-class, 2-feature head: class "pos" fires on feature 0, "neg" on feature 1. +fn head() -> MultinomialLogisticRegression { + MultinomialLogisticRegression::from_stored_coefficients( + labels(&["neg", "pos"]), + 2, + vec![0.0, 4.0, 4.0, 0.0], + vec![0.0, 0.0], + ) + .expect("valid coefficients") +} + +fn input_err(r: Result) -> HeadInputError { + match r { + Err(HeadFitError::InvalidInput(e)) => e, + other => panic!("expected InvalidInput, got {:?}", other.map(|_| ())), + } +} + +#[test] +fn stored_head_exposes_its_shape_and_predicts() { + let h = head(); + assert_eq!(h.n_classes(), 2); + assert_eq!(h.n_features(), Some(2)); + assert_eq!(h.labels(), &["neg".to_string(), "pos".to_string()]); + assert_eq!(h.weights().len(), 4); + assert_eq!(h.intercepts(), &[0.0, 0.0]); + assert!(h.report().is_none(), "a loaded head has no fit report"); + + let rows = vec![vec![1.0, 0.0], vec![0.0, 1.0]]; + assert_eq!(h.predict(&rows).expect("predict"), vec!["pos", "neg"]); + assert_eq!(h.predict_indices(&rows).expect("idx"), vec![1, 0]); + let p = h.predict_proba(&rows).expect("proba"); + for row in &p { + assert!((row.iter().sum::() - 1.0).abs() < 1e-12); + } + assert!(p[0][1] > 0.97 && p[1][0] > 0.97); + // logits are exactly w.x + b + let z = h.predict_logits(&rows).expect("logits"); + assert!((z[0][1] - 4.0).abs() < 1e-12 && z[0][0].abs() < 1e-12); +} + +#[test] +fn stored_coefficients_reject_every_malformed_shape() { + let ok = |l: Vec, d, w: Vec, b: Vec| { + MultinomialLogisticRegression::from_stored_coefficients(l, d, w, b) + }; + assert!(matches!( + input_err(ok(labels(&["a"]), 1, vec![0.0], vec![0.0])), + HeadInputError::TooFewClasses { k: 1 } + )); + assert!(matches!( + input_err(ok(labels(&["a", ""]), 1, vec![0.0; 2], vec![0.0; 2])), + HeadInputError::EmptyLabel { index: 1 } + )); + assert!(matches!( + input_err(ok(labels(&["a", "a"]), 1, vec![0.0; 2], vec![0.0; 2])), + HeadInputError::DuplicateLabel { + first: 0, + second: 1, + .. + } + )); + assert!(matches!( + input_err(ok(labels(&["a", "b"]), 0, vec![], vec![0.0; 2])), + HeadInputError::ZeroFeatureDimension + )); + assert!(matches!( + input_err(ok(labels(&["a", "b"]), 2, vec![0.0; 3], vec![0.0; 2])), + HeadInputError::CoefficientCountMismatch { + array: "weights", + expected: 4, + found: 3 + } + )); + assert!(matches!( + input_err(ok(labels(&["a", "b"]), 2, vec![0.0; 4], vec![0.0; 3])), + HeadInputError::CoefficientCountMismatch { + array: "intercepts", + expected: 2, + found: 3 + } + )); + assert!(matches!( + input_err(ok( + labels(&["a", "b"]), + 2, + vec![0.0, f32::NAN, 0.0, 0.0], + vec![0.0; 2] + )), + HeadInputError::NonFiniteCoefficient { + array: "weights", + index: 1, + .. + } + )); + assert!(matches!( + input_err(ok( + labels(&["a", "b"]), + 2, + vec![0.0; 4], + vec![0.0, f32::INFINITY] + )), + HeadInputError::NonFiniteCoefficient { + array: "intercepts", + index: 1, + .. + } + )); +} + +#[test] +fn predict_guards_dimension_unfitted_and_overflow() { + let h = head(); + let e = h.predict_logits(&[vec![1.0, 2.0], vec![1.0]]).unwrap_err(); + assert_eq!( + e, + HeadFitError::InvalidInput(HeadInputError::FeatureDimMismatch { + row: 1, + expected: 2, + found: 1 + }) + ); + // An empty batch is valid and yields no rows. + assert!(h.predict(&[]).expect("empty batch").is_empty()); + + let fresh = MultinomialLogisticRegression::new(3); + assert_eq!( + fresh.predict_logits(&[vec![1.0]]).unwrap_err(), + HeadFitError::NotFitted + ); + assert_eq!(fresh.n_features(), None); + + // Weights of f32::MAX against f32::MAX features overflow even in f64 accumulation + // only after many terms; a single product stays finite, so build 2 classes whose + // logit is the sum of enough max-products to overflow f64. + let big = MultinomialLogisticRegression::from_stored_coefficients( + labels(&["a", "b"]), + 1, + vec![f32::MAX, 0.0], + vec![0.0, 0.0], + ) + .expect("finite coefficients"); + // f32::MAX^2 ~ 1.16e77, finite in f64: the logit survives where f32 would overflow. + let z = big + .predict_logits(&[vec![f32::MAX]]) + .expect("finite in f64"); + assert!(z[0][0] > 1e76); +} + +#[test] +fn regularization_resolves_lambda() { + assert!((Regularization::Lambda(0.25).resolve_lambda(100) - 0.25).abs() < 1e-15); + // lambda = 1 / (2 C n) + let l = Regularization::SklearnEquivalentC { c: 2.0 }.resolve_lambda(10); + assert!((l - 1.0 / 40.0).abs() < 1e-15); +} + +#[test] +fn builders_do_not_disturb_loaded_state() { + let h = head().with_max_iter(7).with_tol(1e-3).with_history_size(3); + assert_eq!(h.n_classes(), 2); + assert_eq!(h.n_features(), Some(2)); + assert_eq!(h.predict(&[vec![1.0, 0.0]]).expect("predict"), vec!["pos"]); +} + +#[test] +fn error_messages_name_the_offending_values() { + let cases: Vec<(HeadInputError, &str)> = vec![ + (HeadInputError::TooFewClasses { k: 1 }, "K >= 2"), + ( + HeadInputError::LabelCountMismatch { labels: 3, k: 2 }, + "3 entries", + ), + (HeadInputError::EmptyLabel { index: 4 }, "ordered_labels[4]"), + ( + HeadInputError::DuplicateLabel { + first: 0, + second: 2, + label: "x".into(), + }, + "\"x\"", + ), + (HeadInputError::EmptyDataset, "no feature rows"), + ( + HeadInputError::RowCountMismatch { + rows: 5, + class_indices: 4, + }, + "5 feature rows", + ), + (HeadInputError::ZeroFeatureDimension, "dimension is 0"), + ( + HeadInputError::RaggedRow { + row: 2, + expected: 3, + found: 1, + }, + "ragged", + ), + ( + HeadInputError::NanFeature { row: 1, col: 2 }, + "[1][2] is NaN", + ), + ( + HeadInputError::InfiniteFeature { + row: 0, + col: 1, + value: f32::INFINITY, + }, + "not finite", + ), + ( + HeadInputError::LabelIndexOutOfRange { + row: 3, + index: 9, + k: 4, + }, + "0..4", + ), + (HeadInputError::UnrepresentedClass { class: 2 }, "class 2"), + (HeadInputError::NegativeLambda { lambda: -1.0 }, "negative"), + ( + HeadInputError::NonFiniteLambda { lambda: f64::NAN }, + "not finite", + ), + (HeadInputError::NonPositiveC { c: 0.0 }, "strictly positive"), + ( + HeadInputError::NonFiniteC { c: f64::INFINITY }, + "not finite", + ), + ( + HeadInputError::FeatureDimMismatch { + row: 1, + expected: 8, + found: 7, + }, + "fitted on 8", + ), + ( + HeadInputError::CoefficientCountMismatch { + array: "weights", + expected: 6, + found: 5, + }, + "stored weights has 5", + ), + ( + HeadInputError::NonFiniteCoefficient { + array: "intercepts", + index: 2, + value: f32::NAN, + }, + "intercepts[2]", + ), + ]; + for (e, needle) in cases { + let s = e.to_string(); + assert!( + s.contains(needle), + "{e:?} rendered {s:?}, missing {needle:?}" + ); + } +} + +#[test] +fn fit_error_messages_and_conversion() { + let wrapped: HeadFitError = HeadInputError::EmptyDataset.into(); + assert_eq!( + wrapped, + HeadFitError::InvalidInput(HeadInputError::EmptyDataset) + ); + assert!(wrapped.to_string().starts_with("invalid input: ")); + let msgs = [ + ( + HeadFitError::NotConverged { + iterations: 9, + gradient_norm: 1.0, + tol: 1e-6, + }, + "did not converge: 9", + ), + ( + HeadFitError::Stalled { + iterations: 4, + gradient_norm: 0.5, + }, + "stalled after 4", + ), + ( + HeadFitError::NumericalError { iterations: 2 }, + "non-finite value after 2", + ), + ( + HeadFitError::NonFiniteLogit { row: 3, class: 1 }, + "row 3, class 1", + ), + (HeadFitError::NotFitted, "not been fitted"), + ]; + for (e, needle) in msgs { + assert!(e.to_string().contains(needle), "{e:?}"); + } + let boxed: Box = Box::new(HeadFitError::NotFitted); + assert!(boxed.to_string().contains("fitted")); +} From 9e992f539ee5ad81c4e33c96d1e775bd3a3934db Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sun, 18 Oct 2026 22:21:37 +0200 Subject: [PATCH 2/6] test(apr-cli): cover on_cuda_load_error REG-15 admission (coverage gap) Adds FALSIFY-CPU-GPU-016; mutation admit(false,..) turns 2 tests RED. Co-Authored-By: Claude Sonnet 5.5 Agent: aprender-w5382 --- .../apr-cpu-vs-gpu-output-parity-v1.yaml | 8 ++++- .../apr-cli/src/commands/parity_admission.rs | 35 +++++++++++++++++++ docs/findings/w5382.jsonl | 1 + 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 docs/findings/w5382.jsonl diff --git a/contracts/apr-cpu-vs-gpu-output-parity-v1.yaml b/contracts/apr-cpu-vs-gpu-output-parity-v1.yaml index 12a93756d7..3b1d0607b5 100644 --- a/contracts/apr-cpu-vs-gpu-output-parity-v1.yaml +++ b/contracts/apr-cpu-vs-gpu-output-parity-v1.yaml @@ -572,6 +572,12 @@ falsification_tests: test: "bash scripts/check_model_ladder.sh (reads evidence/dogfood/models//{lambda,gx10}.json written by scripts/model_ladder.sh)" if_fails: "an architecture the release claims silently runs on the CPU — 0.68.1 shipped dense Qwen3 this way while the qwen2.5 control was green on the same binary (#3413, EPIC #3477)" +- id: FALSIFY-CPU-GPU-016 + rule: "A CUDA load failure that is the parity gate is refused when the GPU was forced, and only then" + prediction: "on_cuda_load_error returns Err (with the measured cosine) for a forced gate failure, Ok(true) for an unforced one, and Ok(false) for any non-gate error" + test: "cargo test -p apr-cli --lib on_cuda_load_error" + if_fails: "a forced --gpu silently downgrades to CPU after a parity-gate failure, or an unrelated CUDA error is swallowed as a gate verdict (R-0b, #2971)" + proof_obligations: - id: ACVGOP-INV-001 type: invariant @@ -625,5 +631,5 @@ qa_gate: - no_fallback_line_on_claimed_backend - golden_output_gpu_per_rung - both_required_hosts - pass_criteria: "All 15 falsification tests pass, and scripts/check_model_ladder.sh exits 0 (FALSIFY-CPU-GPU-015 is the ladder itself)" + pass_criteria: "All 16 falsification tests pass, and scripts/check_model_ladder.sh exits 0 (FALSIFY-CPU-GPU-015 is the ladder itself)" falsification: "plant backends.cuda.fallback=true in a receipt → exit 1 (scripts/lib/model_ladder_cases/red-fallback)" diff --git a/crates/apr-cli/src/commands/parity_admission.rs b/crates/apr-cli/src/commands/parity_admission.rs index 370e30b101..399d7b53c7 100644 --- a/crates/apr-cli/src/commands/parity_admission.rs +++ b/crates/apr-cli/src/commands/parity_admission.rs @@ -530,3 +530,38 @@ mod sentinel_tests { } } } + +#[cfg(test)] +mod on_cuda_load_error_tests { + use super::*; + + const GATE_MSG: &str = "PARITY-GATE FAILED: GPU computes a DIFFERENT function than CPU.\n\ + Cosine similarity: 0.950800 (required: \u{2265}0.98)"; + + #[test] + fn unrelated_cuda_error_is_not_ours_whether_forced_or_not() { + assert_eq!(on_cuda_load_error("OOM: out of VRAM", true), Ok(false)); + assert_eq!(on_cuda_load_error("OOM: out of VRAM", false), Ok(false)); + } + + #[test] + fn forced_gate_failure_is_refused_with_the_measured_cosine() { + let err = on_cuda_load_error(GATE_MSG, true).expect_err("forced + gate fail must refuse"); + assert!(err.starts_with("parity gate refused the GPU: "), "{err}"); + assert!(err.contains("cosine=0.9508"), "{err}"); + assert!(err.contains("threshold=0.98"), "{err}"); + } + + #[test] + fn unforced_gate_failure_is_admitted_for_cpu_fallback() { + assert_eq!(on_cuda_load_error(GATE_MSG, false), Ok(true)); + } + + #[test] + fn unparseable_gate_message_still_refuses_when_forced() { + // Marker present but no cosine line: NaN verdict must not read as a pass. + let err = on_cuda_load_error("PARITY-GATE FAILED: details lost", true) + .expect_err("a gate failure with no numbers is still a failure"); + assert!(err.contains("parity FAILED"), "{err}"); + } +} diff --git a/docs/findings/w5382.jsonl b/docs/findings/w5382.jsonl new file mode 100644 index 0000000000..d5998ed062 --- /dev/null +++ b/docs/findings/w5382.jsonl @@ -0,0 +1 @@ +{"id":"FND-20261018-on-cuda-load-error-no-caller","session":"w5382","kind":"coverage-gap+dead-code","where":"crates/apr-cli/src/commands/parity_admission.rs:on_cuda_load_error","note":"REG-15 admission decision had no test and no caller outside its own file (grep shows none); 4 tests + FALSIFY-CPU-GPU-016 added, mutation admit(false,..) RED. Owner should confirm whether the CUDA load sites still call it."} From b102b716fd39fe2f2a4efece278507ccb7adf564 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sun, 18 Oct 2026 22:19:31 +0200 Subject: [PATCH 3/6] test(apr-cli): hex output_json_v2 covered with and without stats (FALSIFY-OPS-012) Agent: aprender-w5404 Co-Authored-By: Claude Sonnet 5.5 --- contracts/apr-cli-operations-v1.yaml | 5 +++++ crates/apr-cli/src/commands/hex_06.rs | 26 ++++++++++++++++++++++++++ docs/findings/aprender-w5404.jsonl | 1 + 3 files changed, 32 insertions(+) create mode 100644 docs/findings/aprender-w5404.jsonl diff --git a/contracts/apr-cli-operations-v1.yaml b/contracts/apr-cli-operations-v1.yaml index 96ec9fb715..70b2f7eae6 100644 --- a/contracts/apr-cli-operations-v1.yaml +++ b/contracts/apr-cli-operations-v1.yaml @@ -265,6 +265,11 @@ falsification_tests: prediction: The timestamp in a cbtop report matches the UTC date at the moment it was generated, on both headless paths test: commands::cbtop::tests::test_chrono_timestamp_is_current_utc_date if_fails: Persisted CI provenance is stamped with a stale hardcoded date +- id: FALSIFY-OPS-012 + rule: hex --json lists filtered tensors, stats only on request + prediction: output_json_v2 succeeds with and without stats and skips unknown tensor names + test: commands::hex::tests::output_json_v2_lists_filtered_tensors_with_and_without_stats + if_fails: hex JSON output errors on a valid APR file or invents tensors kani_harnesses: - id: KANI-OPS-001 obligation: ReadOnly commands have no side effects diff --git a/crates/apr-cli/src/commands/hex_06.rs b/crates/apr-cli/src/commands/hex_06.rs index 1e3c210b70..2fdefd7a0f 100644 --- a/crates/apr-cli/src/commands/hex_06.rs +++ b/crates/apr-cli/src/commands/hex_06.rs @@ -11,4 +11,30 @@ mod tests { include!("hex_options_detect.rs"); include!("make.rs"); include!("tests_blocks_and_slices.rs"); + + /// FALSIFY-OPS-012: `hex --json` on an APR file lists each filtered tensor, + /// with stats only when asked, and silently skips names that are not in the file. + #[test] + fn output_json_v2_lists_filtered_tensors_with_and_without_stats() { + use aprender::format::v2::{AprV2Metadata, AprV2Writer}; + + let mut writer = AprV2Writer::new(AprV2Metadata::new("hex-json")); + let data: Vec = [1.0f32, 2.0, 3.0, 4.0] + .iter() + .flat_map(|f| f.to_le_bytes()) + .collect(); + writer.add_tensor("a.weight", TensorDType::F32, vec![2, 2], data); + let mut bytes = Vec::new(); + writer.write_to(&mut bytes).expect("write APR"); + let reader = AprV2Reader::from_bytes(&bytes).expect("read APR"); + + assert!(output_json_v2(&reader, &["a.weight"], 2, true).is_ok()); + assert!(output_json_v2(&reader, &["a.weight"], 0, false).is_ok()); + assert!(output_json_v2(&reader, &["missing"], 4, true).is_ok()); + assert!(output_json_v2(&reader, &[], 4, false).is_ok()); + // the statistics the stats branch serialises + let (min, max, mean, _std) = compute_stats(&[1.0, 2.0, 3.0, 4.0]); + assert!((min - 1.0).abs() < f32::EPSILON && (max - 4.0).abs() < f32::EPSILON); + assert!((mean - 2.5).abs() < 1e-6); + } } diff --git a/docs/findings/aprender-w5404.jsonl b/docs/findings/aprender-w5404.jsonl new file mode 100644 index 0000000000..1941c50f2a --- /dev/null +++ b/docs/findings/aprender-w5404.jsonl @@ -0,0 +1 @@ +{"session":"aprender-w5404","ref":"cov-hex-json","note":"pmat ticket not minted (needs GH issue); tests for hex output_json_v2, FALSIFY-OPS-012"} From def9b321634f11fcbcde60528f4070902f419eed Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sun, 18 Oct 2026 23:28:37 +0200 Subject: [PATCH 4/6] fix(fold): multinomial pass_criteria counts 13 falsification tests Co-Authored-By: Claude Sonnet 5.5 --- contracts/multinomial-head-v1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/multinomial-head-v1.yaml b/contracts/multinomial-head-v1.yaml index ebef99c7f4..2034476098 100644 --- a/contracts/multinomial-head-v1.yaml +++ b/contracts/multinomial-head-v1.yaml @@ -391,5 +391,5 @@ qa_gate: - intercept_penalty_exclusion - convergence_error_mapping - logit_finiteness - pass_criteria: All 12 falsification tests pass, including the two in-band negative controls (FALSIFY-MH-002, FALSIFY-MH-006) + pass_criteria: All 13 falsification tests pass, including the two in-band negative controls (FALSIFY-MH-002, FALSIFY-MH-006) falsification: Substitute lambda*W for 2*lambda*W in the gradient, or 1/(C*n) for 1/(2*C*n) in the relation From b6199956d271d2cb73ceab6725dc473005263a82 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 21:16:44 +0200 Subject: [PATCH 5/6] fix(contracts): order OPS-011 before OPS-012; BEAT-STRUCT-FAIL-CLOSED -> -001 (data-integrity 445 -> under ceiling 444) (Refs #5591) Agent: aprender-w5018 Co-Authored-By: Claude Sonnet 5.5 --- contracts/apr-cli-operations-v1.yaml | 10 +++++----- contracts/apr-fail-closed-structural-beat-v1.yaml | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/apr-cli-operations-v1.yaml b/contracts/apr-cli-operations-v1.yaml index 4b16b28bb6..88ebfc0eb8 100644 --- a/contracts/apr-cli-operations-v1.yaml +++ b/contracts/apr-cli-operations-v1.yaml @@ -265,16 +265,16 @@ falsification_tests: prediction: The timestamp in a cbtop report matches the UTC date at the moment it was generated, on both headless paths test: commands::cbtop::tests::test_chrono_timestamp_is_current_utc_date if_fails: Persisted CI provenance is stamped with a stale hardcoded date -- id: FALSIFY-OPS-012 - rule: hex --json lists filtered tensors, stats only on request - prediction: output_json_v2 succeeds with and without stats and skips unknown tensor names - test: commands::hex::tests::output_json_v2_lists_filtered_tensors_with_and_without_stats - if_fails: hex JSON output errors on a valid APR file or invents tensors - id: FALSIFY-OPS-011 rule: ptx-map printers cover decode, prefill, hit and miss paths prediction: print_json and print_reverse_lookup complete on every table and report launches/layer x layers test: commands::ptx_map::tests::print_json_and_reverse_lookup_cover_every_branch if_fails: ptx-map JSON or reverse-lookup output panics or miscounts launches +- id: FALSIFY-OPS-012 + rule: hex --json lists filtered tensors, stats only on request + prediction: output_json_v2 succeeds with and without stats and skips unknown tensor names + test: commands::hex::tests::output_json_v2_lists_filtered_tensors_with_and_without_stats + if_fails: hex JSON output errors on a valid APR file or invents tensors kani_harnesses: - id: KANI-OPS-001 obligation: ReadOnly commands have no side effects diff --git a/contracts/apr-fail-closed-structural-beat-v1.yaml b/contracts/apr-fail-closed-structural-beat-v1.yaml index 38a37e502f..83ce5affe4 100644 --- a/contracts/apr-fail-closed-structural-beat-v1.yaml +++ b/contracts/apr-fail-closed-structural-beat-v1.yaml @@ -87,7 +87,7 @@ proof_obligations: type: invariant falsification_tests: -- id: FALSIFY-BEAT-STRUCT-FAIL-CLOSED +- id: FALSIFY-BEAT-STRUCT-FAIL-001 description: > FALSE if apr ACCEPTS any cross-tensor-inconsistent model (vocab mismatch or hidden-dim mismatch) — the structural garbage the incumbents load silently — From aea50f41e9ac4e71fe7c67d7ebcb517dc423c414 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Mon, 19 Oct 2026 22:11:06 +0200 Subject: [PATCH 6/6] test: move multinomial head edge tests under the aprender-core lib target CI runs; pass_criteria 12 (Refs #5591) Agent: aprender-w5018 Co-Authored-By: Claude Sonnet 5.5 --- contracts/apr-cli-operations-v1.yaml | 2 +- contracts/multinomial-head-v1.yaml | 2 +- .../src/classification/tests_multinomial_contract.rs | 3 +++ .../classification/tests_multinomial_head_edges.rs} | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) rename crates/aprender-core/{tests/multinomial_head_edges.rs => src/classification/tests_multinomial_head_edges.rs} (99%) diff --git a/contracts/apr-cli-operations-v1.yaml b/contracts/apr-cli-operations-v1.yaml index 88ebfc0eb8..248b00a316 100644 --- a/contracts/apr-cli-operations-v1.yaml +++ b/contracts/apr-cli-operations-v1.yaml @@ -329,7 +329,7 @@ qa_gate: - progress_reporting - concurrent_model_access - tokenizer_consistency - pass_criteria: All 11 falsification tests pass + pass_criteria: All 12 falsification tests pass falsification: > Run read-only command with filesystem watcher to detect mutations. Force inference error and measure GPU memory leak. diff --git a/contracts/multinomial-head-v1.yaml b/contracts/multinomial-head-v1.yaml index 2034476098..06fee53892 100644 --- a/contracts/multinomial-head-v1.yaml +++ b/contracts/multinomial-head-v1.yaml @@ -333,7 +333,7 @@ falsification_tests: - id: FALSIFY-MH-013 rule: A head loaded from stored coefficients validates its shape, and its error text and lambda relation are exact prediction: from_stored_coefficients rejects every malformed shape with the matching HeadInputError; SklearnEquivalentC resolves to 1/(2*C*n); predict guards NotFitted and FeatureDimMismatch - test: cargo test -p aprender-core --test multinomial_head_edges + test: cargo test -p aprender-core --lib tests_multinomial_head_edges if_fails: A corrupt stored head loads silently, or the C-to-lambda relation drifts from sklearn kani_harnesses_note: > NOT EXECUTED. No #[kani::proof] harness exists anywhere in crates/ and cargo-kani is diff --git a/crates/aprender-core/src/classification/tests_multinomial_contract.rs b/crates/aprender-core/src/classification/tests_multinomial_contract.rs index 77d4598b3f..97666c7877 100644 --- a/crates/aprender-core/src/classification/tests_multinomial_contract.rs +++ b/crates/aprender-core/src/classification/tests_multinomial_contract.rs @@ -688,3 +688,6 @@ fn falsify_multinomial_001_fixture_fit_fails_loudly_without_budget() { "expected NotConverged, got {err:?}" ); } + +#[path = "tests_multinomial_head_edges.rs"] +mod tests_multinomial_head_edges; diff --git a/crates/aprender-core/tests/multinomial_head_edges.rs b/crates/aprender-core/src/classification/tests_multinomial_head_edges.rs similarity index 99% rename from crates/aprender-core/tests/multinomial_head_edges.rs rename to crates/aprender-core/src/classification/tests_multinomial_head_edges.rs index 3675587397..87613fc766 100644 --- a/crates/aprender-core/tests/multinomial_head_edges.rs +++ b/crates/aprender-core/src/classification/tests_multinomial_head_edges.rs @@ -2,7 +2,7 @@ //! error rendering, regularisation and prediction guards (coverage ticket, Rule 7). //! Contract: FALSIFY-MH-013 in contracts/multinomial-head-v1.yaml. -use aprender::classification::multinomial::{ +use crate::classification::multinomial::{ HeadFitError, HeadInputError, MultinomialLogisticRegression, Regularization, };