Repository navigation
ble: extract dataRangeOldestUnix into WhoopProtocol.DataRange + pin the aligned-scan protection (#286 follow-up) - #308
Merged
Conversation
…he aligned-scan protection (#286 follow-up) Second sibling in the GET_DATA_RANGE parser. Like dataRangeNewestUnix (extracted in #307), dataRangeOldestUnix was inline in app-target BLEManager/WhoopBleClient with NO test on either platform — a parity-critical decoder (it reports the banked span = backlog DEPTH a deep oldest-first drain must cover, #364). Re-review of #307 surfaced that the oldest read must NOT be "made consistent" with the newest by scanning every offset. The minimum is fragile where the maximum is not: a real WHOOP 4.0 frame (aa10..2263) carries a spurious plausible straddle at byte offset 6 (1754857506, ~11 months BEFORE the true newest at offset 8). An any-offset MIN would grab it and report a bogus ~11-month backlog; the aligned- from-7 grid skips it. So this extracts the aligned scan VERBATIM (no behaviour change) into WhoopProtocol.DataRange.oldestUnix + com.noop.protocol twin, keeps the app-target methods as thin wrappers, and adds Swift+Kotlin tests that PIN the protection (real frame -> nil; grid-aligned words -> min) so a future refactor can't quietly reintroduce the false-backlog bug. WhoopProtocol swift test 262/262 (+3 oldest); Android compileFullDebugKotlin + DataRangeScanTest green. BLEManager compile: app-build.
This was referenced Jul 12, 2026
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…he aligned-scan protection (ryanbr#286 follow-up) (ryanbr#308) Second sibling in the GET_DATA_RANGE parser. Like dataRangeNewestUnix (extracted in ryanbr#307), dataRangeOldestUnix was inline in app-target BLEManager/WhoopBleClient with NO test on either platform — a parity-critical decoder (it reports the banked span = backlog DEPTH a deep oldest-first drain must cover, ryanbr#364). Re-review of ryanbr#307 surfaced that the oldest read must NOT be "made consistent" with the newest by scanning every offset. The minimum is fragile where the maximum is not: a real WHOOP 4.0 frame (aa10..2263) carries a spurious plausible straddle at byte offset 6 (1754857506, ~11 months BEFORE the true newest at offset 8). An any-offset MIN would grab it and report a bogus ~11-month backlog; the aligned- from-7 grid skips it. So this extracts the aligned scan VERBATIM (no behaviour change) into WhoopProtocol.DataRange.oldestUnix + com.noop.protocol twin, keeps the app-target methods as thin wrappers, and adds Swift+Kotlin tests that PIN the protection (real frame -> nil; grid-aligned words -> min) so a future refactor can't quietly reintroduce the false-backlog bug. WhoopProtocol swift test 262/262 (+3 oldest); Android compileFullDebugKotlin + DataRangeScanTest green. BLEManager compile: app-build.
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…he aligned-scan protection (ryanbr#286 follow-up) (ryanbr#308) Second sibling in the GET_DATA_RANGE parser. Like dataRangeNewestUnix (extracted in ryanbr#307), dataRangeOldestUnix was inline in app-target BLEManager/WhoopBleClient with NO test on either platform — a parity-critical decoder (it reports the banked span = backlog DEPTH a deep oldest-first drain must cover, ryanbr#364). Re-review of ryanbr#307 surfaced that the oldest read must NOT be "made consistent" with the newest by scanning every offset. The minimum is fragile where the maximum is not: a real WHOOP 4.0 frame (aa10..2263) carries a spurious plausible straddle at byte offset 6 (1754857506, ~11 months BEFORE the true newest at offset 8). An any-offset MIN would grab it and report a bogus ~11-month backlog; the aligned- from-7 grid skips it. So this extracts the aligned scan VERBATIM (no behaviour change) into WhoopProtocol.DataRange.oldestUnix + com.noop.protocol twin, keeps the app-target methods as thin wrappers, and adds Swift+Kotlin tests that PIN the protection (real frame -> nil; grid-aligned words -> min) so a future refactor can't quietly reintroduce the false-backlog bug. WhoopProtocol swift test 262/262 (+3 oldest); Android compileFullDebugKotlin + DataRangeScanTest green. BLEManager compile: app-build.
This was referenced Sep 7, 2026
ryanbr
added a commit
that referenced
this pull request
Sep 7, 2026
A night whose sleep runs are all shorter than minSleepMin yields no session at all, however much sleep they add up to. From the report in #1937: sleep-detect summary: sleepRuns=5 droppedMinSleep=5 kept=0 detectedSpanMin=298 survivingSpanMin=0 sparse=false The detector found 298 minutes of sleep across five runs and dropped every one, because each run individually fell about a minute under the 60-minute floor. The bridge that exists to re-stitch fragments was switched off, because it is gated on the night being sparse (#308, so a dense night kept its original path byte-for-byte) and the night was dense: gravity coverage on the reporting device was 99.9%. The rescue was declined precisely because the data was good. So the bridge now also runs when the night is fragmented to nothing. The gate is deliberately "today's answer is zero": if any single run already clears the floor the predicate is false, the sparse rule decides exactly as before, and a night that currently scores cannot change. It can only turn nothing into something. Requiring at least two runs keeps one short fragment from qualifying, and requiring their sum to clear the floor keeps a handful of brief stirs from becoming a night. The bridge's own rules still apply underneath: a gap over sparseBridgeGapMin, an intervening active run that is too long, or HR above the sleep band all still refuse. This enables the attempt, it does not weaken what the attempt checks. The predicate is deliberately not passed to buildRuns, which takes its own sparse flag for the HR-vouched gap rule. That decides how runs are formed, so widening it would change the input to everything downstream including nights that currently score. The E10 trace line now names which gate opened the bridge, so a dense night rescued by this path does not print sparse=true and contradict the summary line beside it. Swift and Kotlin twins with matching tests on both sides.
ryanbr
added a commit
that referenced
this pull request
Sep 7, 2026
A night whose sleep runs are all shorter than minSleepMin yields no session at all, however much sleep they add up to. From the report in #1937: sleep-detect summary: sleepRuns=5 droppedMinSleep=5 kept=0 detectedSpanMin=298 survivingSpanMin=0 sparse=false The detector found 298 minutes of sleep across five runs and dropped every one, because each run individually fell about a minute under the 60-minute floor. The bridge that exists to re-stitch fragments was switched off, because it is gated on the night being sparse (#308, so a dense night kept its original path byte-for-byte) and the night was dense: gravity coverage on the reporting device was 99.9%. The rescue was declined precisely because the data was good. So the bridge now also runs when the night is fragmented to nothing. The gate is deliberately "today's answer is zero": if any single run already clears the floor the predicate is false, the sparse rule decides exactly as before, and a night that currently scores cannot change. It can only turn nothing into something. Requiring at least two runs keeps one short fragment from qualifying, and requiring their sum to clear the floor keeps a handful of brief stirs from becoming a night. The bridge's own rules still apply underneath: a gap over sparseBridgeGapMin, an intervening active run that is too long, or HR above the sleep band all still refuse. This enables the attempt, it does not weaken what the attempt checks. The predicate is deliberately not passed to buildRuns, which takes its own sparse flag for the HR-vouched gap rule. That decides how runs are formed, so widening it would change the input to everything downstream including nights that currently score. The E10 trace line now carries both gates as their own k=v keys, sparse= and fragmentedToNothing=, rather than one flag naming the winner. It used to hardcode sparse=true, which since this change could contradict the sparse=false on the summary line beside it. Emitting both keeps the key set identical on every night, which is what a reader diffing two nights, and any k=v parser of the detail field, needs. Swift and Kotlin twins with matching tests on both sides, including an end-to-end one that runs the reported shape (five 59-minute runs, dense gravity, four stirs the bridge must chain through) all the way through detectSleep and asserts both that a session appears and that the rescue is what produced it.
ryanbr
added a commit
that referenced
this pull request
Sep 8, 2026
A night whose sleep runs are all shorter than minSleepMin yields no session at all, however much sleep they add up to. From the report in #1937: sleep-detect summary: sleepRuns=5 droppedMinSleep=5 kept=0 detectedSpanMin=298 survivingSpanMin=0 sparse=false The detector found 298 minutes of sleep across five runs and dropped every one, because each run individually fell about a minute under the 60-minute floor. The bridge that exists to re-stitch fragments was switched off, because it is gated on the night being sparse (#308, so a dense night kept its original path byte-for-byte) and the night was dense: gravity coverage on the reporting device was 99.9%. The rescue was declined precisely because the data was good. So the bridge now also runs when the night is fragmented to nothing. The gate is deliberately "today's answer is zero": if any single run already clears the floor the predicate is false, the sparse rule decides exactly as before, and a night that currently scores cannot change. It can only turn nothing into something. Requiring at least two runs keeps one short fragment from qualifying, and requiring their sum to clear the floor keeps a handful of brief stirs from becoming a night. The bridge's own rules still apply underneath: a gap over sparseBridgeGapMin, an intervening active run that is too long, or HR above the sleep band all still refuse. This enables the attempt, it does not weaken what the attempt checks. The predicate is deliberately not passed to buildRuns, which takes its own sparse flag for the HR-vouched gap rule. That decides how runs are formed, so widening it would change the input to everything downstream including nights that currently score. The E10 trace line now carries both gates as their own k=v keys, sparse= and fragmentedToNothing=, rather than one flag naming the winner. It used to hardcode sparse=true, which since this change could contradict the sparse=false on the summary line beside it. Emitting both keeps the key set identical on every night, which is what a reader diffing two nights, and any k=v parser of the detail field, needs. Swift and Kotlin twins with matching tests on both sides, including an end-to-end one that runs the reported shape (five 59-minute runs, dense gravity, four stirs the bridge must chain through) all the way through detectSleep and asserts both that a session appears and that the rescue is what produced it.
ryanbr
added a commit
that referenced
this pull request
Sep 8, 2026
A night whose sleep runs are all shorter than minSleepMin yielded no session at all, however much sleep they added up to: five runs, 298 detected minutes, every one about a minute under the floor, reported in #1937. The bridge that re-stitches fragments was gated on the night being sparse (#308), and the reporting device was 99.9% dense, so the rescue was declined precisely because the data was good. It now also runs when the night is fragmented to nothing, a gate that can only turn nothing into something: if any single run already clears the floor the sparse rule decides exactly as before. Swift and Kotlin twins, 7 tests each, including an end-to-end pass over the reported shape with an anti-vacuity assertion.
5 tasks done
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.
Follow-up to #307. Second (and last) sibling in the GET_DATA_RANGE parser.
Why
Like
dataRangeNewestUnix(extracted in #307),dataRangeOldestUnixwas inline in app-targetBLEManager/WhoopBleClientwith no test on either platform — yet it's parity-critical: it reports the banked span = backlog DEPTH a deep oldest-first drain must cover (#364).The re-review catch (why this isn't a mechanical copy of #307)
The oldest read must NOT be "made consistent" with the newest by scanning every offset. The minimum is fragile where the maximum is not. On a real WHOOP 4.0 frame:
newestUnixkeeps the max → the real newest dominates; the straddle is ignored. That's why it can scan every offset (the ble: extract GET_DATA_RANGE newest-parse into WhoopProtocol so both platforms CI-test the parity (#286 follow-up) #307 fix).oldestUnixkeeps the min → an any-offset scan would return the offset-6 straddle → a bogus ~11-month backlog. The aligned-from-7 grid skips it (real frame → nil).What
WhoopProtocol.DataRange.oldestUnix(Swift) +com.noop.protocol.DataRange.oldestUnix(Kotlin), byte-identical twins.newestUnixis documented in both sources.Verification
swift test: 262/262 (+3 oldest) — run locally on Linux.compileFullDebugKotlin+DataRangeScanTestgreen (wrapper → pure).BLEManagercompile: app-build.