Skip to content

fix(hrv): do not replace a refused main night's HRV with the day's naps - #2426

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:fix/hrv-refused-main-night-no-nap-fallback
Sep 24, 2026
Merged

ryanbr merged 1 commit into
ryanbr:mainfrom
pipiche38:fix/hrv-refused-main-night-no-nap-fallback

Conversation

@pipiche38

@pipiche38 pipiche38 commented Sep 23, 2026 •

Copy link
Copy Markdown

Problem

The day's HRV (avgHRVDaily) is the in-bed-weighted mean of every session's HRV. That rests on "the main overnight dominates". When the #1118 over-count gate refuses the main night, its HRV is nil and carries zero weight, so the naps become 100 % of the figure.

Field case, one ring: the main night on 2026-09-20 read 1.31 coverage and was refused. The day's HRV became the mean of two 26-minute naps, (69.51 + 27.90) / 2 = 48.7 ms, which the HRV baseline then folded as a full night (mean 32.2 → 35.6 on the second of the four nights it needs to seed).

Change

  • When a main-night-group session's HRV is nil because the gate refused it, only the main group is pooled. The day holds (nil, skip-and-hold in the baseline) unless another fragment of that night measured cleanly.
  • A main night that simply banked no R-R is not refused, so [hrv] An HR-only night computes a usable HRV and then discards it, leaving Charge unscoreable #1884's fill-in from the day's other sessions is unchanged. The deep-window mode is untouched.
  • The refusal test is one pure helper on both platforms, SleepStager.sessionHrvOverCounted. sessionAvgHRV now gates on it too, so "refused" has one definition that the value and the day-level rule cannot disagree about.

Verification

  • New AnalyticsEngineRefusedMainNightHrvTests (Swift) and its Kotlin twin AnalyticsEngineRefusedMainNightHrvTest, same fixtures and expected values:
  • swift test StrandAnalytics: 2053/0.
  • Kotlin: new twin 4/0, HrvOverCountGateTest 7/0, AnalyticsEngineProvidedSleepTest 5/0. Full Android unit suite: the same 8 failures as untouched upstream/main (RecoveryDrivers, StandardHrSensorFormat, StepsDetailIntegration, TodayExplainability), none in touched code.
  • doc_comment_lint: OK. No UI strings, no app-target Swift.

Independent of #2423. With #2423 in, far fewer ring nights are refused in the first place; this covers the ones that still are, on any strap.

Refs #2425, #1118, #1884

The day's HRV is the in-bed-weighted mean of every session's HRV, on the
premise that the main overnight dominates. When the ryanbr#1118 over-count
gate refuses the main night, its HRV is nil and carries zero weight, so
the naps became 100 % of the figure. Field case: a ring night at 1.31
coverage was refused, and the day's HRV became the mean of two
26-minute naps, (69.51 + 27.90) / 2 = 48.7 ms, which the baseline then
folded as a full night.

Now, when a main-night-group session's HRV is nil BECAUSE the gate
refused it, only the main group is pooled, so the day holds (nil)
unless another fragment of that night measured cleanly. A main night
that simply banked no R-R is not refused, so ryanbr#1884's fill-in from the
day's other sessions is unchanged. The deep-window mode is untouched.

The refusal test is one pure helper on both platforms,
SleepStager.sessionHrvOverCounted, which sessionAvgHRV now gates on too,
so "refused" has a single definition that the value and the day-level
rule cannot disagree about.

Tests: new AnalyticsEngineRefusedMainNightHrvTests (Swift) and its
Kotlin twin, same fixtures and expected values: refused main night +
clean nap -> nil (60.0 without the fix, on both platforms); no-R-R main
night + nap -> 60; clean night + nap -> 41.25 as before; helper agrees
with the value gate. StrandAnalytics 2053/0. Kotlin twin 4/0,
HrvOverCountGateTest 7/0, AnalyticsEngineProvidedSleepTest 5/0; full
Android suite shows the same 8 pre-existing failures as upstream/main,
none in touched code. doc_comment_lint OK.

Refs ryanbr#1118, ryanbr#1884

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvMtCweTWqqM9iyDBz88XX
@pipiche38 pipiche38 changed the title [hrv] When the main night's HRV is refused as over-counted, the day's HRV and the baseline fall back to naps alone fix(hrv): do not replace a refused main night's HRV with the day's naps Sep 23, 2026
@ryanbr
ryanbr merged commit 2e3d1a2 into ryanbr:main Sep 24, 2026
16 checks passed
@pipiche38
pipiche38 deleted the fix/hrv-refused-main-night-no-nap-fallback branch September 24, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants