Stop dropping REM sleep, and let the coach say so - #120
Open
ak710 wants to merge 1 commit into
Open
Conversation
The Colmi big-data timeline (stage 0x04) and the YCBT timeline (tag 3) both report REM, and both decoders have always stored it as a SleepStageBlock. It just never got any further: SleepSummary carried light/deep/awake only, so REM never reached the sleep score, the Sleep tab, or the coach — which was still hard-coded to tell the model "light/deep/awake only, no REM" on every ring. Carry remMinutes through SleepSummary, collapseByDay and averageStages, and report it as remPct. Nil rather than zero throughout when the ring reported no REM stage at all: a jring's 0x11 timeline genuinely has none, and "absent" and "you slept no REM" are different claims. This also fixes a scoring side-effect. hasAwakeSignal falls back to asking whether the stage timeline accounted for essentially the whole night, and REM was excluded from that sum — so a fully described REM night looked only ~80% covered, failed the 0.95 gate, and had its awake reading thrown away as "no signal", costing it 45% of the awake sub-score. REM now counts toward coverage. REM is measured but still not scored: the light band (ideal 50-60%) is calibrated for a no-REM decoder that lumps REM into light, so re-weighting the score would move every stored night and needs the versioned recompute readiness does. Noted in SleepScore.calculate for the sleep-score rework. The coach's caveat is now chosen per night from that night's own blocks rather than from the connected ring's capabilities — stored nights outlive the ring that recorded them, so switching rings must not retro-actively disclaim last week's REM. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 3, 2026
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.
Closes #111
Merge order: 1 of 9. Branches off
main, depends on nothing.What
The Colmi big-data timeline (stage
0x04) and the YCBT timeline (tag3) both report REM, and both decoders have always stored it. It just never got any further —SleepSummarycarried light/deep/awake only, so REM never reached the sleep score, the Sleep tab, or the coach, which was hard-coded to tell the model "light/deep/awake only, no REM" on every ring.Also fixes a scoring bug
hasAwakeSignalfalls back to asking whether the stage timeline accounted for essentially the whole night. REM was excluded from that sum, so a fully-described REM night looked ~80% covered, failed the 0.95 gate, and had its awake reading thrown away as "no signal" — costing it 45% of the awake sub-score.Test:
testFullyDescribedRemNightKeepsItsAwakeSignal, withtestPartiallyDescribedNightStillWithholdsAwakeSignalas the complement so the fix didn't just make the gate unconditionally true.Design notes
0x11timeline genuinely has no REM stage; "absent" and "you slept no REM" are different claims.remPct,SleepContext.remMinandAverageStages.remare all optional.SleepScore.calculate, and done in [Feature]: Sleep score scores time in bed as if it were time asleep #115.averageStagesbecame a struct rather than a 4-tuple, which trips SwiftLint'slarge_tuple. That's a net −1 on the lint baseline.Testing
PulseLoopTests/SleepRemStageTests.swift— 10 new tests. Full suite green (881). SwiftLint introduces no new violation types.Demo data already generates REM blocks, so
-seedDemo YESexercises this without hardware.