From 1acdd4beee989ed4a4051b97b2d496ad74e74dd7 Mon Sep 17 00:00:00 2001 From: Mohammad Abdul Sahil <127765312+abdulsaheel@users.noreply.github.com> Date: Sat, 19 Sep 2026 13:26:37 +0530 Subject: [PATCH] overreaching_conjunction: guard against quantized rhr baseline dispersion alternating whole-bpm rhr baselines (e.g 58/59) give a small nonzero mad that isn't real dispersion, just rounding noise. illness_cusum, readiness and event_detection already guard this exact channel with dispersionBelowQuantum, this one didn't, so a 1bpm rise could clear the gate and fire the conjunction card on nothing. --- .../onehz/human/overreaching_conjunction.dart | 8 ++- test/onehz/overreaching_conjunction_test.dart | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 test/onehz/overreaching_conjunction_test.dart diff --git a/lib/src/onehz/human/overreaching_conjunction.dart b/lib/src/onehz/human/overreaching_conjunction.dart index 62449fb..4cde0c3 100644 --- a/lib/src/onehz/human/overreaching_conjunction.dart +++ b/lib/src/onehz/human/overreaching_conjunction.dart @@ -107,7 +107,13 @@ Metric overreachingConjunction({ final base = robustBaseline(rhrBaselineWindow, minValid: minBaseline); final centre = base.center; final scale = base.scale; - final gate = (scale != null && scale > 0) ? 0.5 * scale : null; + // Nightly RHR is whole-bpm; an alternating baseline (e.g. 58/59) can carry + // a small nonzero MAD that is unresolvable rounding noise, not real + // dispersion — the same guard illness_cusum.dart and readiness_composite.dart + // already apply to this exact channel. + final rhrBelowQuantum = dispersionBelowQuantum(rhrBaselineWindow, 1.0); + final gate = + (!rhrBelowQuantum && scale != null && scale > 0) ? 0.5 * scale : null; if (centre == null || gate == null || !base.sufficient) { return Metric.absent( tier: Tier.estimate, diff --git a/test/onehz/overreaching_conjunction_test.dart b/test/onehz/overreaching_conjunction_test.dart new file mode 100644 index 0000000..b1f457d --- /dev/null +++ b/test/onehz/overreaching_conjunction_test.dart @@ -0,0 +1,51 @@ +// Regression for the whole-bpm RHR quantization false positive: a baseline +// alternating between two adjacent integers carries a small nonzero MAD that +// is unresolvable rounding noise, not real dispersion. Without +// dispersionBelowQuantum this let a 1 bpm rise clear the gate. + +import 'package:test/test.dart'; +import 'package:openstrap_analytics/onehz.dart'; + +void main() { + group('overreachingConjunction RHR quantum guard', () { + final load = Metric( + value: const LoadState(40.0, 65.0, -25.0), // atl/ctl = 1.625 >= 1.5 + confidence: 0.8, + tier: Tier.high, + inputs_used: const ['daily_trimp'], + ); + + test('alternating whole-bpm baseline abstains instead of firing', () { + final baseline = [ + 58, 59, 58, 59, 58, 59, 58, 59, 58, 59, 58, 59, 58, 59, + ]; + // 1 bpm above the median baseline on every recent night — exactly the + // rounding-noise case the guard exists to catch. + final recent = [60.0, 60.0, 60.0, 60.0, 60.0]; + + final m = overreachingConjunction( + load: load, + rhrRecent: recent, + rhrBaselineWindow: baseline, + ); + + expect(m.present, isFalse); + }); + + test('real dispersion in the baseline still lets it fire', () { + final baseline = [ + 54, 56, 58, 60, 55, 57, 59, 61, 54, 58, 60, 56, 59, 55, + ]; + final recent = [65.0, 65.0, 65.0, 65.0, 65.0]; + + final m = overreachingConjunction( + load: load, + rhrRecent: recent, + rhrBaselineWindow: baseline, + ); + + expect(m.present, isTrue); + expect(m.value!.bothPointSameWay, isTrue); + }); + }); +}