Skip to content

Persist SDNN alongside RMSSD (5-min index); fix HealthKit SDNN export mislabel (rebase of #475) - #1334

Merged
ryanbr merged 1 commit into
mainfrom
pr475-rebase
Aug 15, 2026
Merged

Persist SDNN alongside RMSSD (5-min index); fix HealthKit SDNN export mislabel (rebase of #475)#1334
ryanbr merged 1 commit into
mainfrom
pr475-rebase

Conversation

@ryanbr

@ryanbr ryanbr commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Maintainer rebase of #475 (@vishk23) onto current main — the fork branch could not be pushed to directly (maintainer-edit push returned 403), so the author's commit is carried here unchanged in authorship.

Only change vs #475: the migration is renumbered v32-daily-avg-sdnnv37-daily-avg-sdnn (upstream advanced to v36 while the branch was open), and the grdbMigrations list in both schema_oracle.json copies is updated to match (v37 at position 37, both files byte-identical). All logic — HRVAnalyzer.sdnnIndex, the avgSdnn column/upsert/read, the HealthKit export fix, and the documented daily-avg-sdnn-ios-only divergence — is unchanged from #475.

Closes #475.


Original #475 summary: DailyMetric.avgHrv holds RMSSD for strap rows but SDNN for Apple-Health rows, and the HealthKit write-back exported the strap's RMSSD under .heartRateVariabilitySDNN, mislabeling it in the user's permanent Apple Health record. This persists a real 5-min SDNN index (avgSdnn) and exports that instead, falling back to avgHrv only when SDNN is absent.

… mislabel

Rebased onto current main: migration renumbered v32→v37-daily-avg-sdnn
(upstream advanced to v36 while this branch was open); grdbMigrations
lists updated in both schema_oracle.json copies. Logic unchanged.

avgHrv keeps RMSSD; new nullable dailyMetric.avgSdnn carries the Task
Force 5-min SDNN index. HealthKit write-back now exports avgSdnn under
.heartRateVariabilitySDNN (falling back to avgHrv only when SDNN absent).
Android twin pending; column registered as a documented one-directional
divergence (daily-avg-sdnn-ios-only).
@ryanbr
ryanbr merged commit 758a008 into main Aug 15, 2026
16 checks passed
@ryanbr
ryanbr deleted the pr475-rebase branch August 15, 2026 03:23
DX23876 added a commit to DX23876/noop that referenced this pull request Aug 15, 2026
51 commits since the 10.0.0 release point. The android/ tree they still carry
is dropped wholesale as usual (74 delete/modify conflicts), leaving ~90 files
that actually apply here.

Both trees grew personalized HR zones in the same week (ryanbr#531 upstream), down to
naming the same `hrZoneSet` / `hasCustomHRZones` properties. This fork's version
stays: it has percent AND bpm modes, its own editor screen, coach integration
and the boundary-rounding fix, where upstream's is bpm-only with the editor
inline in Settings. Upstream's `profile.hrZoneThresholds` backup key is kept
whitelisted and imported into the `.bpm` mode when this fork's own keys are
absent, so a `.noopbak` written by their build — or a wearer migrating from it —
does not silently lose its bands. Read-only and idempotent; an edit made here
always wins.

Taken from upstream where their answer was better or theirs to make:
  - ryanbr#1334 SDNN export: HealthKit's HRV field IS SDNN, so the Apple Health export
    now writes the stored 5-min SDNN index instead of recomputing a whole-night
    SD at export time, which reads 2-3x high against Apple's short-window
    samples. The `avgHrv` fallback is NOT taken — RMSSD under an SDNN label is
    the mislabel this is fixing.
  - ryanbr#837 charge/sync indicator in the Liquid Today header, with the fork's coach
    button re-seated at the head of the cluster. `LiquidHeaderMetrics.control` is
    now an alias for the design system's `compactControlSize`: the indicator
    sizes its ring off that token, and one 30pt control beside 36pt siblings
    reads as a mistake.
  - ryanbr#518 HRV tuning moved out of the always-visible Strap card into Advanced,
    with a new `.hrv` search section so the in-app field still finds it.
  - ryanbr#1331 respiratory carry, staleness-bounded, alongside the existing per-field
    SpO2 / skin-temperature carries.
  - ryanbr#1284 / ryanbr#1325 sleep dedup + edit-owner resolution, where both trees had
    independently fixed the same report.
  - Polish localization, 10.1.0, and the v37+ migrations.

Kept here: the fork's Today hero palette and typography, the search-aware
Advanced disclosure, and the strict R-R trust gate on what may be exported.

Upstream's `defaultLowerBounds(maxHR:)` rounding insight survives their API as
`HRZones.defaultBpmLowerBounds(maxHR:)`, pinned by a test that walks every
integer bpm to prove seeding the editor cannot reclassify a sample.

Verified: 1450 StrandAnalytics + 386 WhoopStore + 2074 app tests, macOS and iOS
both build, i18n audit exit 0. Polish for this fork's own ~1300 strings is the
next commit; the gate does not cover pl yet.
simoncad7 pushed a commit to simoncad7/noop that referenced this pull request Aug 17, 2026
… mislabel (ryanbr#1334)

Rebased onto current main: migration renumbered v32→v37-daily-avg-sdnn
(upstream advanced to v36 while this branch was open); grdbMigrations
lists updated in both schema_oracle.json copies. Logic unchanged.

avgHrv keeps RMSSD; new nullable dailyMetric.avgSdnn carries the Task
Force 5-min SDNN index. HealthKit write-back now exports avgSdnn under
.heartRateVariabilitySDNN (falling back to avgHrv only when SDNN absent).
Android twin pending; column registered as a documented one-directional
divergence (daily-avg-sdnn-ios-only).

Co-authored-by: vishk23 <119831996+vishk23@users.noreply.github.com>
ryanbr pushed a commit that referenced this pull request Aug 22, 2026
Android stored nightly RMSSD but had no five-minute SDNN index, the separate
metric iOS has computed and persisted since #1334. This closes the data and
schema gap so native Android nights and cross-platform backups carry the same
distinct value.

`HrvAnalyzer.sdnnIndex` is the Kotlin twin of `HRVAnalyzer.sdnnIndex`: the mean
of per-segment sample-SDNN across consecutive 300-second segments, each cleaned
with the same range + Malik rejection the nightly path uses, segments under the
clean-beat floor skipped, null when none qualifies. It buckets samples in one
pass rather than re-filtering the night per segment. That is equivalent because
the Swift twin filters to the window BEFORE cleaning, so a segment receives the
same intervals in the same order either way -- and it is O(n) instead of
O(n x segments).

Computed from R-R inside the matched sleep sessions using the same half-open
bounds as every other in-bed aggregate.

Storage: `DailyMetric.avgSdnn` (nullable), Room 31 -> 32 with one additive
column, appended last so the entity order matches ALTER TABLE. Existing rows stay
null; there is deliberately no RMSSD-to-SDNN backfill, because avgHrv is RMSSD
for native and Health Connect rows and relabelling it would fabricate the metric.

Apple Health's source statistic IS SDNN, so it fills both avgHrv (existing
recovery behaviour) and avgSdnn, matching what AppleHealthImport.swift already
does on iOS. Health Connect exposes RMSSD and deliberately leaves avgSdnn null.
The value is preserved through the merge, coalescing and spine-union paths, where
a Kotlin data class would otherwise default it to null in silence.

Removes the `daily-avg-sdnn-ios-only` entry from both copies of the shared schema
oracle. That entry named this exact work as its closing condition.

Thanks to @bhelm.

Verified on the merged tree: 4208 Android tests, 0 failures, 6 intentional skips
(--no-build-cache --rerun-tasks, so a stale generated DAO cannot mask the schema
change); SchemaOracleTest and DailySdnnMigrationTest green; test (WhoopStore)
covers the Swift half of the oracle. Migration verification is static plus Room
KSP schema verification: the repository has no Room runtime migration harness, so
an on-device 31-to-32 upgrade is NOT claimed as exercised.
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