Skip to content

WHOOP 4 R-R source per UTC hour, with the ingress test and extractStreams pair repaired - #2515

Merged
ryanbr merged 5 commits into
mainfrom
pr2496
Sep 27, 2026
Merged

ryanbr merged 5 commits into
mainfrom
pr2496

Conversation

@ryanbr

@ryanbr ryanbr commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Merges @rodrigosa7's #2496 for #1118 / #2335, rebased onto current main with one change on top.

What it fixes

A WHOOP 4 scoring read chose type-47 history for its whole multi-night interval the moment any historical row existed, so a partial offload left the remaining hours reading nothing and nights without the coverage to score HRV. The decision is per UTC hour now, so hours history never covered fall back to the live stream. testWhoop4PartialHistoryKeepsUnlabelledRowsFromOtherHours pins that, and it is the actual defect.

Underneath it, WHOOP 4 R-R gains three labelled transports where live traffic was previously all NULL: type-47 history (8), type-40 realtime (9) and standard BLE (10). That is the direct follow-on from #2478, which found history and live BLE both carrying srcChannel NULL was what made them indistinguishable. Legacy NULL rows still participate, at the lowest rank, so existing installs benefit rather than only future data. Both platforms bump their read cache keys (v4 to v5, s3 to s4), which is easy to forget and would otherwise have served stale memoised reads straight past the change.

The policy, stated because the description says otherwise

The merged behaviour is provenance order, not the fuller stream: history, then realtime, then standard BLE, then legacy, chosen per hour. Commits one to three selected by count with history breaking ties, which is what the PR description and commit one's title describe. Commit four replaced that with MIN(rank) and rewrote the tests to pin it, so the code and tests agree and it is the prose that is stale.

Keeping provenance order. Type-47 carries the strap's own device timestamps where standard BLE is stamped on receipt, and receipt stamping is where #1008's cross-second over-count came from, so letting the better-timestamped stream win is the safer read even when it is sparser. The residual is worth naming: an hour holding sparse history still prefers it over dense live beats, which is the reported failure mode surviving at hour granularity rather than interval granularity. If coverage should win instead, that is a scoring decision rather than a review note.

The change on top

The ingress test still pinned the old NULL for a live WHOOP 4 standard-BLE beat, so build (Strand, macOS) was red. Labelling that beat is the point of the change, so the assertion moves to [.whoop5Standard, .whoop4Standard, nil] rather than losing coverage. Only a beat with no family stays nil, and the test's name, RrFamilyIsCapturedAtIngress, reads truer than before.

extractStreams gained family on both platforms at once, which changed the arity on both sides together and unpaired them in the ledger even though they moved as one. Declared twin references pair them again, and the derived authority is refreshed: baseline holds at 292 known findings, no disposition touched.

The Kotlin query now records why it is not AS MATERIALIZED like its Swift twin: the hint wants SQLite 3.35 and minSdk is 26, so it is a syntax error on most supported devices. The cost is the per-row re-evaluation the Swift comment names, and the note exists so the gap is not closed by adding the keyword.

Verification

  • Rebased onto 77d7325; all four contributor commits keep their authorship and dates.
  • parity_ledger.py and parity_ratchet.py: clean, errors=0, twin references 1018 to 1020.
  • WhoopProtocol: 804 tests, 0 failures. Kotlin: Whoop5RRSqliteTest 24, FramingTest 39, Whoop5RRTest 5, HrFingerprintTest 4, all 0 failures under --no-build-cache --rerun-tasks with result XML checked fresh against the clock.
  • Tools/doc_comment_lint.py clean, baseline unchanged at 24.
  • WhoopStore tests cannot link on Linux (GRDB needs sqlite3_snapshot_*) and StrandTests runs only on the macOS leg, so both of those, including the repinned test, are CI's to confirm.
  • Not verified on hardware, and this is the part that matters: whether a real partially-offloaded WHOOP 4 night now scores HRV. That wants a 4.0 with a partial type-47 offload.

Because this carries Tools/parity_twin_map.json, parity-governance runs on it. Worth noting the original branch could not have caught the parity break: the changed files sit inside the parity globs while the workflow triggers only on Tools/parity_*.

Refs #2496, #1118, #2335

rodrigosa7 and others added 5 commits September 27, 2026 13:12
Labelling live WHOOP 4 beats is the point of the change, so the ingress test
moves to the new shape rather than losing the assertion: a standard-BLE beat is
`whoop4Standard` now where it was left unlabelled, and that NULL is exactly what
let a partial type-47 offload hide live intervals. Only a beat with no family
stays nil. The test's own name reads truer for it.

`extractStreams` gained `family` on both platforms in the same change, which
changed the arity on both sides at once and unpaired them in the ledger even
though they moved together. Declared twin references pair them again, and the
derived authority is refreshed for it: the baseline holds at 292 known findings
and no disposition is touched.

The Kotlin query now says why it is not `AS MATERIALIZED` like the Swift twin.
That hint wants SQLite 3.35 and `minSdk` is 26, so it is a syntax error on most
supported devices; the cost is the per-row re-evaluation the Swift comment names,
and the note exists so nobody closes the gap by adding the keyword.

Worth stating because the PR description says otherwise: the merged policy is
provenance order, not the fuller stream. History, then type-40 realtime, then
standard BLE, then legacy rows, chosen per UTC hour. Commits one to three
selected by count; commit four replaced that with `MIN(rank)` and rewrote the
tests to pin it. Keeping it: type-47 carries the strap's own device timestamps
where standard BLE is stamped on receipt, which is where #1008's cross-second
over-count came from, so the better-timestamped stream winning is the safer read.
The residual is that an hour holding sparse history still prefers it over dense
live beats.

Refs #2496, #1118, #2335
@ryanbr
ryanbr merged commit 12ca2f0 into main Sep 27, 2026
18 checks passed
@ryanbr
ryanbr deleted the pr2496 branch September 27, 2026 02:38
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