ONNX parity (5 tests)
Source:test_onnx_parity.py::test_onnx_parity (parametrized across 5 models)
Assertion: for each model, |booster.predict(sample) − ort.session.run(sample)| ≤ 1e-5 on both a clean row and a NaN-bearing row
What it catches: divergence between the XGBoost native scorer (used in Python training) and the ONNX runtime (used in the React app). The divergence would be silent the browser would show wrong numbers with no error. The NaN-bearing sample (the ~47% of laps where cliff_candidate_flag or cliff_onset_passed is null) is the critical case: XGBoost learns the optimal missing-value split direction during training, and that direction must survive the ONNX conversion.
- Parity status
- Why atol=1e-5
atol=1e-5 parity, including NaN-bearing samples.Predict schema (3 tests)
Source:test_predict.py (3 test functions)
What these catch: changes to the output parquet that would silently break downstream consumers (the app, the mart, the model card) by dropping a column, changing a type, or violating a business rule.
- Output schema (17 columns)
- Holdout & envelope flags
- Quantile monotonicity & prob normalisation
test_predict.py::test_output_schemaAssertion: the written parquet has exactly the 17 columns in PREDICTIONS_ARROW_SCHEMA, with matching types.The 17 columns are:Target synthesis (1 test)
Source:test_targets.py::test_stint_life_synthesis
Assertion: remaining_stint_life_laps ≥ 0 for all rows; the synthesis stint_length_laps − lap_in_stint is non-negative everywhere.
What it catches: the stint-life target is synthesised in features.py at load time, not read from the mart. A bug in the synthesis (e.g. a subtraction with wrong column alignment) would produce negative “remaining laps”, which has no physical meaning and would cause the regressor to learn from impossible targets. The test gates the synthesised values before any training can start.
Version contract (30 tests)
Source:test_manifest_contract.py (18 test functions; five are parametrized across the five models)
What these catch: the pipeline being consistent with itself but not with the rest of the system. Parity compares each booster with its own ONNX file, and a manifest is valid JSON whatever version it names, so a stale export would pass every other check. These tests compare what the pipeline produced with what the app and the model card expect. Tests that need ml/models/manifest.json, the model card, the app copy or the dbt manifest skip when that file is absent.
Manifest and artefacts
Complete artefact set
Complete artefact set
test_manifest_contract.py::test_manifest_version_has_a_complete_artefact_setAssertion: every production target has both a .bst and a .onnx file on disk for the version the manifest names.Why it matters: a manifest that names a version with missing files would ship a contract the browser cannot load.Manifest ships the expected version
Manifest ships the expected version
test_manifest_contract.py::test_manifest_ships_the_version_the_system_expectsAssertion: if a complete set for MODEL_VERSION_DEFAULT is on disk, the manifest names that version. The test skips when no complete default set exists, as in the CI smoke run.Why it matters: this catches make ml-onnx publishing an old version. make app-models copies by manifest version, so the manifest version is what reaches the browser.Declared input width matches the booster (×5 models)
Declared input width matches the booster (×5 models)
test_manifest_contract.py::test_manifest_input_width_matches_the_actual_booster (parametrized × 5)Assertion: the manifest’s n_features for each model equals num_features() of the booster at that version.Why it matters: the width is written from the feature contract and never read back from the booster, so it is a claim until this test measures it. The contract is per model (39 columns for the cliff classifier, 32 for the others), and the browser builds its feature vector from the declared width.Per-model input block is self-consistent (×5 models)
Per-model input block is self-consistent (×5 models)
test_manifest_contract.py::test_manifest_per_model_input_block_is_self_consistent (parametrized × 5)Assertion: n_features, shape[1] and len(feature_order) are equal for each model.Why it matters: the three fields describe the same width. If they disagree, a consumer reading a different one of them would build a different vector.Feature order matches the masked contract (×5 models)
Feature order matches the masked contract (×5 models)
test_manifest_contract.py::test_manifest_feature_order_matches_the_masked_contract (parametrized × 5)Assertion: each model’s shipped feature_order equals feature_columns_for(target), including the per-model mask.Why it matters: a width check would pass on any 32 columns. Scoring is positional, so the identity and order of the columns have to match what training fitted through.No global feature order
No global feature order
test_manifest_contract.py::test_manifest_does_not_declare_a_global_feature_orderAssertion: manifest_schema_version >= 2, and the top-level input block has neither feature_order nor n_features.Why it matters: a single order cannot describe a per-model contract. Leaving the 39-column superset in place would let a consumer that was not migrated build the wrong vector for four of the five boosters.The copy the browser loads
App manifest equals the ML manifest
App manifest equals the ML manifest
test_manifest_contract.py::test_app_manifest_matches_ml_manifestAssertion: app/public/models/manifest.json exists and is identical to ml/models/manifest.json. The test skips when app/public/models/ is absent.Why it matters: make app-models copies the manifest verbatim. A difference means someone re-exported without re-copying, and the browser is scoring against a stale contract.App ONNX files match the manifest hashes
App ONNX files match the manifest hashes
test_manifest_contract.py::test_app_onnx_files_match_the_manifest_hashesAssertion: each ONNX file in app/public/models/ exists and its sha256 equals the onnx_sha256 recorded in the manifest. The test skips when the directory is absent.Why it matters: the files the browser loads must be the exact bytes the manifest describes, not an older export left by a partial copy.Stint-life survival output
Stint life ships as a survival model
Stint life ships as a survival model
test_manifest_contract.py::test_stint_life_ships_as_a_survival_modelAssertion: the stint_life_regressor entry has kind == "survival", objective == "survival:aft" and headline_metric == "aft_nloglik".Why it matters: an RMSE computed on uncensored stints picks the worst model, so the stint-life headline must be the censoring-aware likelihood.Survival output block is complete
Survival output block is complete
test_manifest_contract.py::test_survival_output_block_is_completeAssertion: margin_offset, label_shift, aft_scale, aft_distribution and quantiles are all present and non-null; label_shift and aft_distribution equal the schema constants; 0 < aft_scale < 5; and the quantiles include p10 and p90.Why it matters: the stint-life graph emits a shifted log-scale margin, and the browser turns it into laps using constants that exist only in the manifest. A missing or null constant would produce numbers that look like laps and are not.Margin offset reconstructs the booster
Margin offset reconstructs the booster
test_manifest_contract.py::test_survival_margin_offset_actually_reconstructs_the_boosterAssertion: on 200 NaN-bearing rows, laps computed from the ONNX margin plus margin_offset match laps computed from the .bst booster within the export tolerance. The test skips when the artefact pair is absent.Why it matters: the export step checks the offset inside the process that wrote it. This test re-reads both files from disk and shows the manifest constants really turn one into the other, which is the property the browser depends on.Predictions schema carries the life band
Predictions schema carries the life band
test_manifest_contract.py::test_predictions_schema_carries_the_life_bandAssertion: predictions_schema includes the stint-life point prediction and its p10 and p90 columns.Why it matters: the parquet and the browser must describe the same object. If the app draws a band the mart does not store, there is nothing to compare it with.Interval and ceiling contract on the model card
Every win carries an interval
Every win carries an interval
test_manifest_contract.py::test_every_beats_baseline_claim_carries_an_intervalAssertion: assert_claims_carry_intervals accepts the model card on disk. Skips when the card is absent.Why it matters: beats_baseline: true was published without an interval for every model. The card writer now refuses a bare claim, and this runs that check on the real card.The interval gate can fail
The interval gate can fail
test_manifest_contract.py::test_the_interval_gate_can_failAssertion: for a model that claims a win, each of three mutations makes assert_claims_carry_intervals raise a ValueError mentioning “un-intervalled”: an interval that failed to compute, an interval with null bounds, and a missing significance verdict.Why it matters: a gate ships with a proof that it fires. The three mutations are three different ways a card could go quiet.Claims inside noise are recorded
Claims inside noise are recorded
test_manifest_contract.py::test_claims_inside_noise_are_recorded_not_deletedAssertion: the card’s validation block has a claims_inside_noise list, and its entries are a subset of the model names on the card.Why it matters: a claim that fails its interval is the most informative line on the card. The key has to exist even when the list is empty, so “every claim survived” and “nobody checked” cannot look the same.Attainable ceiling is published per model
Attainable ceiling is published per model
test_manifest_contract.py::test_attainable_ceiling_is_published_per_modelAssertion: every model on the card has an attainable block with a basis and a fraction_of_attainable (or the in-sample-oracle variant).Why it matters: without a ceiling every headline is a ratio against a perfect score of 1.0. The card is where that stops, and the basis says what the ceiling is a ceiling of.Driver-isolation ratings stay out of the ML contract
No contract mart depends on the isolation ratings
No contract mart depends on the isolation ratings
test_manifest_contract.py::test_no_ml_contract_mart_depends_on_the_isolation_ratingsAssertion: on the dbt manifest, none of fct_cliff_prediction_features, fct_stint_features, fct_lap_residuals or fct_driver_skill_features has a driver-isolation model among its ancestors. The test first requires at least nine isolation models to exist, so it cannot pass on a manifest that predates them. Skips when the dbt manifest is absent.Why it matters: the isolation ratings are functions of the residual trajectory, and the same-race tactical rating is a function of the label’s own trajectory. A ref() added anywhere in a contract mart’s lineage would leak the label, so it fails here rather than in review.The isolation lineage check can fail
The isolation lineage check can fail
test_manifest_contract.py::test_the_isolation_lineage_check_can_failAssertion: in a copy of the dbt manifest where fct_stint_features is made to depend on fct_driver_isolation_race, the leak check reports fct_stint_features.Why it matters: the lineage check passes when nothing leaks, so a broken check would look identical to a clean repo. Injecting the defect shows the check can see it.Relationships
ONNX export
CI overview
Features & Targets
stint_length_laps is never in the feature matrix.