Tracking issue for the remediation programme that comes out of the T20 findings (#389) and
the statistics defects in #390. Plan document: plans/FIX-PLAN-T20-2026-08-23.md.
What this is fixing
Three facts drive the whole thing.
selection_mode="xai" never uses saliency for arbitration. It is a greedy argmax on
selection-validation accuracy, and the benchmarked contrast was accuracy-argmax vs random.
It is null at n=10 across two datasets and three protocols.
- That criterion is close to orthogonal to the objective. On Waterbirds it sits at ~97% while
the objective sits at 59%, and candidate accuracies differ by fractions of a point.
- The equal-compute protocol costs the deployed model two thirds of its budget. Matching
deployed epochs closes 4.46 pp to 0.09 pp on Imagewoof.
Plus a family of silent-failure defects found at source.
Phase 0, independent fixes, all parallel
Phase 1, turn saliency into measurements
Phase 2, selection refactor
Phase 3, record and observe
Phase 4, search policy
Block 7, calibration and falsification
Sequencing that matters
Phase 3 before Phase 4. Shadow mode (#411) records the diagnosis on every run
without letting it decide anything. It is what makes #417 possible: from the moment
it lands, every run that was going to happen anyway becomes a calibration sample at zero extra
GPU cost. Without it the only way to get thresholds is to guess, which is the mistake this
whole programme exists to undo.
Thresholds stay None. #405 makes the library refuse diagnostic mode until they
are supplied, and #417 is the only issue allowed to supply them.
Defaults do not move until Block 7 says so. #413 ships the policies with
exhaustive still default.
Kill criterion, stated up front. If the calibrated rule does not beat metric_argmax on
held-out seeds, diagnosis_single does not become the default and the result is published as a
negative result, the same way T20's null was.
Schedule
| Week |
Dates |
Content |
| CW9 |
Aug 25-31 |
#394 to #398 in parallel, #399 alongside, #402 starts |
| CW10 |
Sep 1-7 |
#399 to #401 finish, Phase 1, #406 starts |
| CW11 |
Sep 8-14 |
Phase 2, Phase 3, shadow runs start collecting |
| CW12 |
Sep 15-21 |
Phase 4, #416, #417 running |
| CW13 |
Sep 22-30 |
Buffer. #418, defaults decision, docs, retro |
Roughly 200h of estimates against roughly 195h of capacity, so the plan is full. If it slips,
#412 drops first, then Block 7 becomes a post-cohort study with the shadow data
already collected.
Out of scope for the cohort, deliberately: augmentation_schedule (sequential ICD then AICD,
stochastic mix, Proposal B). It opens a new untested axis and is not needed to close either
defect the benchmarks identified.
Decisions already taken
Status, 2026-08-30
Phase 0 dev items, Phase 1 and Phase 2 are complete. Phase 3 is half done.
Shadow mode is live. Since #411 landed, every run with XAI enabled writes shadow_records.jsonl: the saliency statistics and robustness metrics per candidate, with the arm that was kept flagged. That is the calibration set #417 needs, accumulating at no extra GPU cost from runs that were going to happen anyway. The sooner the team's benchmark runs go through a current build, the larger that set is when #417 starts.
Two decisions taken while implementing, both flagged in their PRs and reversible:
One deviation from an issue's steps: #408 step 5 asks to reuse the paired estimator from benchmarks/stats.py (#398). That file does not exist yet, so the estimator lives in bnnr/training/paired.py and the convention difference (sample-level mean vs seed-level median, and why) is documented. Aligning them is a follow-up once #398 lands.
Also fixed, filed off the back of #396: #421 (DifPresets/ProCAM CPU/GPU parity) is implemented in #429 but not merged — it changes numpy-path colour output and is waiting on a call about which channel order to keep.
Tracking issue for the remediation programme that comes out of the T20 findings (#389) and
the statistics defects in #390. Plan document:
plans/FIX-PLAN-T20-2026-08-23.md.What this is fixing
Three facts drive the whole thing.
selection_mode="xai"never uses saliency for arbitration. It is a greedy argmax onselection-validation accuracy, and the benchmarked contrast was accuracy-argmax vs random.
It is null at n=10 across two datasets and three protocols.
the objective sits at 59%, and candidate accuracies differ by fractions of a point.
deployed epochs closes 4.46 pp to 0.09 pp on Imagewoof.
Plus a family of silent-failure defects found at source.
Phase 0, independent fixes, all parallel
benchmarks/stats.py, fix rank-biserial and the paired bootstrap ([Bug]: - rank-biserial, paired Δ/CI, and tie handling are incorrect in benchmarks/summarize_grand.py #390)Phase 1, turn saliency into measurements
analysis/saliency_stats.pyanalysis/diagnosis.py, AttentionRegime and the decision rulehard_quantile_accandrobustness_gapPhase 2, selection refactor
training/selection.py, CandidateSelector and SELECTORSreason="indistinguishable"Phase 3, record and observe
Phase 4, search policy
search_policy: exhaustive, diagnosis_single, successive_halvingBlock 7, calibration and falsification
Sequencing that matters
Phase 3 before Phase 4. Shadow mode (#411) records the diagnosis on every run
without letting it decide anything. It is what makes #417 possible: from the moment
it lands, every run that was going to happen anyway becomes a calibration sample at zero extra
GPU cost. Without it the only way to get thresholds is to guess, which is the mistake this
whole programme exists to undo.
Thresholds stay
None. #405 makes the library refuse diagnostic mode until theyare supplied, and #417 is the only issue allowed to supply them.
Defaults do not move until Block 7 says so. #413 ships the policies with
exhaustivestill default.Kill criterion, stated up front. If the calibrated rule does not beat
metric_argmaxonheld-out seeds,
diagnosis_singledoes not become the default and the result is published as anegative result, the same way T20's null was.
Schedule
Roughly 200h of estimates against roughly 195h of capacity, so the plan is full. If it slips,
#412 drops first, then Block 7 becomes a post-cohort study with the shadow data
already collected.
Out of scope for the cohort, deliberately:
augmentation_schedule(sequential ICD then AICD,stochastic mix, Proposal B). It opens a new untested axis and is not needed to close either
defect the benchmarks identified.
Decisions already taken
FIX-namespace, separate from the cohort T ladder.raise when the range looks normalised and they are not.
Status, 2026-08-30
Phase 0 dev items, Phase 1 and Phase 2 are complete. Phase 3 is half done.
Shadow mode is live. Since #411 landed, every run with XAI enabled writes
shadow_records.jsonl: the saliency statistics and robustness metrics per candidate, with the arm that was kept flagged. That is the calibration set #417 needs, accumulating at no extra GPU cost from runs that were going to happen anyway. The sooner the team's benchmark runs go through a current build, the larger that set is when #417 starts.Two decisions taken while implementing, both flagged in their PRs and reversible:
UNSTRUCTUREDconfidence is1 - max(other two); a tie goes to the regime whose weakest clause has the larger margin.hard_quantile_qonBNNRConfigrather than moving it intoDiagnosisConfig, because the metrics it produces are worth watching with no diagnosis configured.One deviation from an issue's steps: #408 step 5 asks to reuse the paired estimator from
benchmarks/stats.py(#398). That file does not exist yet, so the estimator lives inbnnr/training/paired.pyand the convention difference (sample-level mean vs seed-level median, and why) is documented. Aligning them is a follow-up once #398 lands.Also fixed, filed off the back of #396: #421 (DifPresets/ProCAM CPU/GPU parity) is implemented in #429 but not merged — it changes numpy-path colour output and is waiting on a call about which channel order to keep.