Skip to content

Close the final HR/HRV window on the shipped SleepStager path, and drop the unused RecoveryScorer helpers - #1936

Merged
ryanbr merged 3 commits into
ryanbr:mainfrom
bhelm:feature/noop-p2-hr-twin
Sep 6, 2026
Merged

ryanbr merged 3 commits into
ryanbr:mainfrom
bhelm:feature/noop-p2-hr-twin

Conversation

@bhelm

@bhelm bhelm commented Sep 6, 2026 •

Copy link
Copy Markdown

Follow-up to #1927. That PR closed the final five-minute bin on the window end in RecoveryScorer.restingHR / recoveryIndexSlope — but those two helpers have no production caller. The shipped per-session resting HR and HRV windows come from SleepStager.sessionRestingHR and SleepStager.sessionHrvWindows on both platforms, and they carried the same defect: a closed [start, end] prefilter over [t, t + 300) bins with a loop that stops at end, so a sample or beat sitting exactly on an aligned end is admitted and then lands in no bin.

Part 1 — the same rule on the shipped path. The final bin now closes on end on both platforms ([t, t + 300) for every bin except the last, which is [t, end]; a zero-length window is that single closed bin). Window startTs, the stage-tagging centre and the RMSSD math are unchanged. Because these two functions are shipped, this can move a displayed number on a night whose session length is an exact multiple of 300 s with a sample on end: per-session restingHR/avgHRV and the HRV trace. Stored rows keep the old value until that night is re-scored; no migration.

Paired aligned/non-aligned endpoint tests on both platforms (8 Swift, 8 Kotlin), red before the fix on the same four cases on each side, green after. The 37-value sessionRestingHR sweep is one oracle literal asserted verbatim by both tests. Wider runs unchanged: swift test over SleepStager/AnalyticsEngine/SleepSession (221 passed) and the com.noop.analytics Kotlin package (2019 tests).

Part 2 — dead code out. RecoveryScorer.restingHR and RecoveryScorer.recoveryIndexSlope are removed on both platforms together with their tests and the four constants only they used (restingHRWindowS, restingHRMinBinSamples, restingHRMinPlausibleBpm, recoveryIndexMinBins), after a repo-wide caller grep found no production or public consumer. The recoveryIndexSlope parameter of recovery(...) stays (a caller-supplied scorer input; today only tests supply it). docs/ANALYTICS.md now documents the endpoint rule where the shipped code is; docs/LIBRARY.md no longer lists the removed helper. The dated plan docs/superpowers/plans/2026-06-29-oura-local-ble-architecture.md (lines 15 and 61) still names the old helper and was left as history.

Two things a maintainer may want to weigh: the shipped-number change above, and the constant removal — leaving restingHRMinPlausibleBpm behind would document an artifact gate no code applies.

Observed, not fixed here: the shipped sessionRestingHR has no #686-style artifact gate (no minimum bin sample count, no plausibility floor); a single-beat bin can win the night's floor. Pre-existing on both platforms; worth its own issue.

Limits: Linux host, no Xcode — app targets were not built (app-build.yml is the real check); no full suite on either platform, selection by affected scope. Synthetic fixtures only.

…ed path

SleepStager.sessionRestingHR and SleepStager.sessionHrvWindows prefilter a closed
[start, end] window and then bin it as [t, t + 300) with a loop that stops at
`end` — the same defect RecoveryScorer.restingHR carried before ab14a95. A
sample or beat sitting exactly on an ALIGNED `end` passes the prefilter and then
belongs to no bin: admitted as data, silently ignored.

Unlike the RecoveryScorer helpers, these two are the SHIPPED path. Both platforms
compute the per-session resting HR and avgHRV through them (SleepStager's own
session loops and AnalyticsEngine's fallback), and sessionHrvWindows also feeds
the HRV nightly trace and the last-deep-run comparator. So this moves a displayed
number, in the aligned-endpoint case only.

The final bin now closes on `end` on both platforms ([t, t + 300) for every bin
except the last, which is [t, end]); a zero-length window collapses to that one
closed bin, so a session whose beats all sit on `start == end` yields a window
instead of none. Window startTs, the stage-tagging center and the RMSSD math are
unchanged.

Tests are red first, then green: the four aligned cases fail on the unmodified
source (Swift: resting floor reads 70 instead of 40, the sweep disagrees at
end=600 and end=900, the final HRV window comes back empty, zero-length avgHRV is
nil; Kotlin: the same four). Each is paired with a non-aligned twin that passes
before and after, so the change cannot be mistaken for one to trailing partial
bins. The Kotlin literals are the verbatim stdout of a standalone Swift oracle
over the fixed binning, and SleepStagerTests.swift pins the same literals so the
oracle guards both directions. The HRV cases use a constant 900 ms RR series
whose RMSSD is analytically 0.0 on both platforms, so they pin the BINNING and
not the RMSSD math.

Verification: swift test --filter SleepStagerTests (84 passed) and the wider
SleepStager/AnalyticsEngine/SleepSession selection (221 passed); Gradle
testFullDebugUnitTest for com.noop.analytics (2019 passed, including the 8 new
cases). No strap and no app-target build is involved — this is pure package math.
RecoveryScorer.restingHR and RecoveryScorer.recoveryIndexSlope have no caller
outside their own tests on either platform. The shipped resting HR and HRV come
from SleepStager.sessionRestingHR / sessionHrvWindows (via SleepStager's session
loops and AnalyticsEngine's fallback), which now carry the closed-window endpoint
rule themselves. recovery(...) takes the resting HR and the Recovery-Index slope
as caller-supplied parameters and never called these two.

Confirmed before removing, over non-test sources only (Strand/, StrandiOS*/,
Packages/*/Sources, Tools/, android/app/src/main, docs/, project.yml): no
production call site, no public consumer in the app targets, and the only prose
references were the doc/API mentions updated here. Both same-named `recoveryIndexSlope`
PARAMETERS of recovery(...) and its chargeDrivers overload stay — they are the
scorer's inputs, not this helper — and their doc comments no longer point at the
removed function.

Removed with them: restingHRWindowS, restingHRMinBinSamples,
restingHRMinPlausibleBpm and recoveryIndexMinBins on both platforms. They were
referenced only from inside the two deleted functions, and their doc comments
describe an artifact-hardening rule (ryanbr#686) and a bin gate that no longer exist
anywhere in the tree — leaving them would leave documentation for code that is
gone. Nothing else in either platform reads them.

Tests deleted with the code they covered: the restingHR floor/ryanbr#686 cases and the
recoveryIndexSlope cases in RecoveryScorerTests.swift, RecoveryScorerRestingHrTest.kt
and RecoveryScorerWindowEndpointTest.kt (both whole files), and the three slope
cases plus the now-unused hr/slopeSeries fixtures in
RecoveryIndexActivityBalanceTest.kt. The recovery(...) parameter cases in that
file and in RecoveryScorerTests.swift are untouched: they pin the scorer terms,
which still exist.

docs/ANALYTICS.md now documents the endpoint rule where the shipped code is —
a new "Per-session resting HR and HRV" section under SleepStager — and the
RecoveryScorer section states that recovery(...) takes resting HR as an input
rather than computing one. docs/LIBRARY.md drops restingHR(_:start:end:) from
the RecoveryScorer public-API row.

Verification: swift test --filter '(RecoveryScorerTests|SleepStagerTests)'
(97 passed); Gradle testFullDebugUnitTest for com.noop.analytics (2001 passed,
18 fewer than before, exactly the removed cases); python3 Tools/doc_comment_lint.py
(OK, 24 baselined sites). No behaviour change: nothing that runs called these.
@bhelm
bhelm marked this pull request as ready for review September 6, 2026 16:58
@ryanbr
ryanbr merged commit 965e5dc into ryanbr:main Sep 6, 2026
16 checks passed
@bhelm
bhelm deleted the feature/noop-p2-hr-twin branch September 15, 2026 22:35
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