Repository navigation
rr: label WHOOP 4 type-47 history and score one R-R stream per window - #2478
Merged
Merged
Conversation
… WHOOP 4 rebase The branch was written against a base that predates #2423 and #2457, and both touch the ONE read it changes. A literal conflict resolution would have dropped one or both silently, so this records what the merged read has to keep. `rrIntervals` is now a three-way choice rather than two: WHOOP 5 picks the lowest scorable transport for the interval; a WHOOP 4 picks its labelled type-47 history, or its unlabelled standard-BLE rows when the window has no history; everything else takes the Oura branch from #2423, one beat channel per window, the fuller of green and the amplitude family. `OuraRedrainCollapse.withoutRedrainedRuns` still wraps the result on BOTH platforms, which the Kotlin conflict would also have removed from `WhoopRepository`. Parity, which CI would not have caught here: `parity-governance.yml` filters on `Tools/parity_*`, and this branch touches `Packages/**` and `android/**`, so that job never runs on it. Run locally instead, and the ratchet named five findings. `extractHistoricalStreams` gained a declared twin reference rather than a disposition: the Kotlin twin exists, and adding `family` only moved the Swift identity, which the inventory keys on arity. Calling that platform-specific would have written a false rationale into the registry permanently. The three Room DAO methods and their two `@Query` constants took `platform_specific` dispositions. Room needs a compile-time constant string and a generated method; the Swift store issues the same SQL inline in `Reads.swift` and `StreamStore.swift`, which is why the WHOOP 5 siblings are one-sided for the same reason. Hashes came from `parity_ledger._canonical_sha256`, not by hand. `--migrate-authority` was deliberately not used. It waives whether the BASE's stored manifest reproduces, and reaching for it to absorb debt a branch introduces is laundering; the ratchet asks for an issue-bound authority change instead. Two smaller things: `isWhoop4RRSource` gained the doc its siblings have, including that `confirmedRegistryFamily` returns nil for an unknown registry and that nil must not be read as "a WHOOP 4"; and the promote path names `RRSourceChannel.whoop4Historical` instead of force-unwrapping the optional its own pattern match already proved non-nil. The derived authority is refreshed here too, which the first push missed. The ratchet went green on the dispositions and I took that as the whole verdict, but the ledger had already said `twin-map-authority-drift|functions`: pairing `extractHistoricalStreams` and adding five functions moved the derived inventory, so the stored snapshot no longer reproduced. CI caught it as `test_checked_metadata_is_compact_v3_and_expands_losslessly`, stored 4486 against a derived 4491. Refreshed with `--refresh-derived` and NOT `--migrate-authority`: the base authority reproduces fine, only this branch's derived counts moved, and migration would have waived a check that was not the thing failing. Findings held at 292.
ryanbr
force-pushed
the
rb-2441-whoop4-hrv
branch
from
September 26, 2026 01:20
e7f4462 to
7fa4d3a
Compare
This was referenced Sep 26, 2026
Merged
DX23876
added a commit
to DX23876/noop
that referenced
this pull request
Sep 28, 2026
…e AI-12) The reconciler matched each beat against the nearest beat of a better path within 3 s. That only pairs the two copies of a beat while the strap's clock (history) and the phone's receipt clock (standard 0x2A37) agree to within the radius, and they drift. On one WHOOP 5/MG, nights 2026-09-09 to 09-26, the standard copy sat most often 1-2 s after its history twin at the start and 5-6 s on 09-26. The radius still held only because the history train is dense enough that some beat always lies nearby; the seams it spliced into the scored train rose from 12-48 a night to 146 and 233. Each five-minute UTC segment is now read from one path: the highest-ranked (history > standard > realtime) that occupies at least four fifths of the seconds the fullest path occupies there. Occupancy counts seconds, not rows. Two labelled paths never share a segment. Rows without provenance fill only outside the chosen path's stretch, so an upgrade or a partial offload never blanks the older beats beside it. Only the owner family's labels name a path. rrIntervals reads the segments a window touches in full and cuts back afterwards, so a five-minute read selects what a whole-night read does; the day witness covers the same whole segments and both witness prefixes bump. Upstream (ryanbr#2478) picks one source per UTC hour, where an hour holding a little history still hides a complete live train; the completeness bar closes that residual too. Checked on the 18 real nights above, old reconciler as the oracle against the new one through HRVAnalyzer: no coverage verdict changes, coverage moves by at most 0.008, RMSSD by at most 0.2 ms, seams fall to 0-2 a night. The hrv diag gains an "rr paths" line (segments per path, seams) and, for the newest night, "rr paths clock" with the standard-minus-history offset, so the drift the segment choice guards against stays visible in the log. Analysis migration required: yes. Recipe AI-12 re-scores the standard 21-day window without day reuse. No raw row is rewritten or deleted. Verified: WhoopStore 792 and StrandAnalytics 2776 tests pass, Strand (macOS) and NOOPiOS build, StrandTests 3652 pass after the recipe pin fix. Refs ryanbr#1118, ryanbr#2478.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@rodrigosa7's work from #2441, rebased onto current main with the conflicts worked through and the parity gate satisfied. Both of their commits are unchanged. This branch exists only because a push to their fork is refused (
maintainer_can_modifyreports true, the push does not), so the follow-up could not be added to their PR. Merging with rebase so all three commits keep their own author.Raised in #2335, #1118, #2336.
Their change
WHOOP 4.0 type-47 historical R-R gets its own label (code 8), so it can be told apart from the overlapping unlabelled live BLE feed. HRV reads then select ONE stream for the whole window, history when the window has any, the unlabelled rows otherwise. Exact-key historical replays promote existing legacy rows, and the analysis fingerprints bump so cached nights rescore.
The diagnosis is right and it matches the shape #2423 settled on for Oura: when two sources cover the same beats, score one of them for the interval rather than splicing both and reading 1.0+ coverage. Here the pair read as
crossSecondOverCountand the #1118 gate withheld a plausible whole-night RMSSD.Checked rather than assumed: the
family: DeviceFamily = .whoop4default that labels unlabelled frames looks risky in isolation, but it mirrors the Kotlin twin, where that exact default has been on main at three signatures already, and both production call sites (Backfiller,RawHistoryArchive) pass it explicitly.What the follow-up does
The rebase, which is the substantive part. The base predates #2423 and #2457 and both touch the one read this changes, so taking either side literally would have dropped one of them without saying so.
rrIntervalsis now a three-way choice: WHOOP 5 takes the lowest scorable transport, a WHOOP 4 takes its type-47 history (or its unlabelled rows when the window has none), and everything else takes the Oura branch, one channel per window.OuraRedrainCollapse.withoutRedrainedRunsstill wraps the result on both platforms; the Kotlin conflict would have removed it fromWhoopRepositorytoo.The parity gate.
parity-governance.ymlfilters onTools/parity_*, and the branch as SUBMITTED touched onlyPackages/**andandroid/**, so the gate would not have run on it and the findings below would have reached main ungoverned. It runs on this branch only because the follow-up commit touchesTools/parity_dispositions.json, which matches that filter. Found by running it locally, and each of the five findings got the remedy that fits it:extractHistoricalStreamstook a declared twin reference, NOT a disposition. The Kotlin twin exists; addingfamilyonly moved the Swift identity, which the inventory keys on arity. Aplatform_specificrationale there would have been false, and permanent.@Queryconstants tookplatform_specificdispositions. Room needs a generated method and a compile-time constant string; the Swift store issues the same SQL inline inReads.swiftandStreamStore.swift, which is why the WHOOP 5 siblings are one-sided for the same reason. Hashes came fromparity_ledger._canonical_sha256.--migrate-authoritywas deliberately not used. It waives whether the BASE's stored manifest reproduces, so using it to absorb debt a branch introduces would be laundering.Two small things:
isWhoop4RRSourcegained the doc its siblings have, including thatconfirmedRegistryFamilyreturns nil for an unknown registry and nil must not be read as "a WHOOP 4"; and the promote path names the enum case instead of force-unwrapping an optional its own pattern match already proved non-nil.Verification
swift testWhoopProtocol: 803 tests, 0 failures, 1 corpus-dependent skipswift buildWhoopStore: clean, with the merged three-way predicateswift testWhoopStore: cannot link on Linux (sqlite3_snapshot_cmp), so its new selection tests are CI's to runparity_ratchet.py: errors=0 locally, andparity-governanceis in this PR's own check rosterparity_ledger.py: the three DAO findings remain listed as inventory drift and are dispositioned, which is what the ratchet checksThe two Android failures are
DebugExportSchedulerTest.targetEarlierTodayRollsToTomorrowandtargetEqualToNowRollsToTomorrow, which fail identically on untouched main. They are #2477, a DST-boundary bug in that test, and they clear by themselves after 2026-09-28.One known trade, worth stating rather than discovering later
Promotion relabels
NULLto code 8 only on exact key matches, while the read is all-or-nothing per window. A PARTIAL historical offload therefore promotes part of a night, and the read then drops the un-promoted unlabelled rows, lowering coverage, which is what the #1118 gate refuses on. So an interrupted offload could newly withhold an RMSSD that today's over-counted read produces.Merged as-is on purpose. The alternative, preferring history and filling gaps from the live feed, re-introduces the overlap at the seam, which is the bug. The direction of failure here is a refusal to score rather than a wrong number, and a refusal is visible in the
hrv diagcoverage line.Hardware proof is owed, as the description says: a whole-night RMSSD on a 4.0 whose window carries both a type-47 offload and live BLE, with
hrv diagshowing coverage at or below 1.0 rather thancrossSecondOverCount.