You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat(sleep): recalculate a selected past wake date - #2782
Sleep now offers a wake-date picker and Recalculate action on Android, macOS and iOS, including when no sleep session exists. It reruns the existing detector and scorer from persisted inputs for that completed local date, including dates beyond the normal 21-day scan. It reports insufficient data, no detection, deletion suppression, concurrency and failure honestly; it does not fetch strap history or change detector thresholds.
The bounded scan uses calendar-day/DST boundaries and neighboring sample context. It preserves imported rows, user-edited windows and intentional deletion markers, banks selected sleep/day/provenance, motion/state details and computed duplicate cleanup atomically, and refreshes dependent Charge cells only within the shared current 21-day baseline window. A no-data or no-detection scan retains earlier saved sleep and totals together.
This adds the missing-day capability and is separate from the deleted-night management/reporting work in #2660 and #2776.
Type of change
New feature
How it was tested
Production Swift calendar-window oracle covers 25 timezone/date cases, including 23/25-hour days, Lord Howe half-hour transitions and Apia's skipped date; Kotlin pins that output.
Full StrandAnalytics: 2,151 tests passed, including old missing-night detection, authoritative manual windows, calendar boundaries and paired dependent-score oracles using the existing Charge/Baselines/Rest functions.
Full WhoopStore: 628 tests passed. Real SQLite regression coverage exercises selected sleep/day/detail/provenance rollback, cancellation, preserved manual/source/day rows and neighboring context-marker isolation.
Android: 32 focused tests passed with no skips, including production V2 engine missing/incomplete-night scans beyond 21 days, manual/other-day preservation, no-data/no-detection, failed/cancelled transactions, context-marker isolation, legacy R-R provenance, action/concurrency and unchanged JaCoCo budgets. App and unit-test sources compiled locally. The documented local Room snapshot issue required an empty gitignored input plus excluding syncRoomSchemaSnapshot for this focused run; Clean Android CI subsequently passed the APK build and full unit suite at ced6accc7e757209f6ac5ee47b0347fbb229c520, including schema-export coverage.
Parity refresh/ratchet passed against origin/main with unchanged 290 known findings; all 50 parity repository-acceptance tests, 153 core Tools tests and two legacy R-R contracts passed. Final Swift app syntax parsing, source-comment hygiene and whitespace checks passed. Translation diff gate checked locally. UI uses existing design tokens.
All 18 CI checks passed at ced6accc7e757209f6ac5ee47b0347fbb229c520, including the universal macOS app build, macOS StrandTests, iOS Simulator app build, Android APK/full unit suite, Swift packages/tools, parity governance, Tools Python, source hygiene and i18n. Apple app compilation was validated in native CI; the local host is Linux. The iOS leg is compile-only. No BLE code or detector thresholds changed; no hardware validation is claimed.
Checklist
Tests passed for both touched Swift packages.
Full clean Android CI passed at the updated PR head (local focused run timed out).
UI uses existing design tokens.
No BLE commands, hardcoded frames, generated output or secrets committed.
Native macOS/iOS app builds and macOS StrandTests passed at the updated PR head.
The selected-sleep cleanup scope guard now throws an error so the transaction rolls back instead of terminating the app. A real SQLite regression checks preservation of imported sleep rows, scores, metric points and provenance on refusal. Both dependent-score helpers now resolve their seven shared baseline values once before iterating.
At 84398484, the eight selected-sleep store tests and three dependent-score tests (including the existing Swift numeric oracle) passed locally on Linux. A normal merge of current main resolved only a generated parity-map conflict through regeneration; the refreshed ledger passed with 289 known findings. Whitespace checks passed. The cold local Android run completed app Kotlin compilation but timed out before unit tests under the worker's memory constraints; Android test success is not claimed for that local run. All 18 CI checks passed at 843984844c4382223cb789d24e072680bf2cc2ea: native macOS/iOS app builds, macOS StrandTests, Android APK/full unit suite, Swift packages/tools, parity governance, Tools Python, source hygiene and i18n. The final exact-head check API reports 18 checks and zero non-success results, matching the initial registered roster. Earlier full-suite and native Apple CI evidence above belongs to ced6accc; it is not evidence for the updated head.
Read this one closely, both halves, plus the ledger entries. It is carefully built. Two things to change, neither structural.
Verified
The window twins agree. Both take calendar midnights so 23 and 25 hour days survive, both reject 2026-02-30 (Swift via the component round-trip, Kotlin via LocalDate.parse plus the toString() identity check), both reject a day that has not finished, and both derive readStart from the same named lookback constant rather than a literal.
I went looking for an off-by-one in the bounds, since contains is half-open while selectedSleepBounds is a closed range. Swift builds start...(end - 1) and Kotlin builds start until end, which are the same interval, and end - 1 is used consistently at all seven sites across the two platforms. No divergence.
The provenance writes are the same three branches on both sides, with byte-identical SQL, and the recovery == nil delete plus the sourceId upsert line up exactly. Both wrap the whole sequence in one transaction, and both share the empty-dailyMetrics early return.
The claim that imports, manual edits and deleted windows survive is actually enforced, by scoping the stale-session cleanup to the computed -noop device. That is the mechanism, so it is worth keeping the assertion that guards it (see below for how).
Ten new strings, all ten locales each.
The 17 new dispositions are all out_of_scope_twin against Strand/Data/IntelligenceEngine.swift, which really is outside Swift ledger scope, so they are the documented pattern rather than an exemption. The one without a twin_path, deleteRecoveryProvenance, states that Swift performs the identical DELETE inline, and it does, in writeSleepDependentRecovery. IntelligenceEngineJacocoBudgetTest.kt also makes the repeated "instrumentation budget" rationale checkable, so the helper extraction is a requirement rather than a preference.
Test coverage is symmetric across platforms and covers the cases that matter: rollback on a failed daily write, a cancelled write banking nothing, atomic provenance, both DST day lengths, a saved manual window beating a detected onset, and a Swift oracle for the Kotlin side.
Please change: the precondition should throw
precondition(deviceId.hasSuffix("-noop")) traps the process in a release build. The Kotlin twin guards the same invariant with require(...), which throws and rolls the transaction back, and two lines above it in the same Swift function the sibling scope violation already throws an NSError. A user-triggered recalculation should refuse the write, not terminate the app, and since the check sits before any delete the transaction unwinds cleanly either way. This is also the first precondition in WhoopStore.
Please change: hoist the loop-invariant work
In SleepRecalculationDependents.updates, seven values inside the per-row loop do not depend on row: resolved (a full 21-day resolve), sourceDays, respEpoch, respBase, skinCfg, skinHistory and skinBase. Two of those are foldHistory calls. With refreshBaseline true the loop runs up to 21 times, so that is roughly 21 times the baseline work for identical results. Same shape on both platforms. A plain hoist above the loop, no behaviour change.
Not blocking
The Kotlin skinDev expression reads like a bug but is not. I checked all three cases against the Swift if/else and they agree; the trailing ?: if (row.skinTempC == null) row.skinTempDevC else null exists only because ?.let collapses "absent" and "returned null". A when (row.skinTempC) would say that out loud.
Good call using explicit half-away-from-zero rounding on the Kotlin side instead of Math.round, given skin temp deviations are frequently negative.
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
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.
What this PR does
Sleep now offers a wake-date picker and Recalculate action on Android, macOS and iOS, including when no sleep session exists. It reruns the existing detector and scorer from persisted inputs for that completed local date, including dates beyond the normal 21-day scan. It reports insufficient data, no detection, deletion suppression, concurrency and failure honestly; it does not fetch strap history or change detector thresholds.
The bounded scan uses calendar-day/DST boundaries and neighboring sample context. It preserves imported rows, user-edited windows and intentional deletion markers, banks selected sleep/day/provenance, motion/state details and computed duplicate cleanup atomically, and refreshes dependent Charge cells only within the shared current 21-day baseline window. A no-data or no-detection scan retains earlier saved sleep and totals together.
This adds the missing-day capability and is separate from the deleted-night management/reporting work in #2660 and #2776.
Type of change
How it was tested
StrandAnalytics: 2,151 tests passed, including old missing-night detection, authoritative manual windows, calendar boundaries and paired dependent-score oracles using the existing Charge/Baselines/Rest functions.WhoopStore: 628 tests passed. Real SQLite regression coverage exercises selected sleep/day/detail/provenance rollback, cancellation, preserved manual/source/day rows and neighboring context-marker isolation.syncRoomSchemaSnapshotfor this focused run; Clean Android CI subsequently passed the APK build and full unit suite atced6accc7e757209f6ac5ee47b0347fbb229c520, including schema-export coverage.origin/mainwith unchanged 290 known findings; all 50 parity repository-acceptance tests, 153 core Tools tests and two legacy R-R contracts passed. Final Swift app syntax parsing, source-comment hygiene and whitespace checks passed. Translation diff gate checked locally. UI uses existing design tokens.All 18 CI checks passed at
ced6accc7e757209f6ac5ee47b0347fbb229c520, including the universal macOS app build, macOSStrandTests, iOS Simulator app build, Android APK/full unit suite, Swift packages/tools, parity governance, Tools Python, source hygiene and i18n. Apple app compilation was validated in native CI; the local host is Linux. The iOS leg is compile-only. No BLE code or detector thresholds changed; no hardware validation is claimed.Checklist
Related issues
Refs #515
Review follow-up
The selected-sleep cleanup scope guard now throws an error so the transaction rolls back instead of terminating the app. A real SQLite regression checks preservation of imported sleep rows, scores, metric points and provenance on refusal. Both dependent-score helpers now resolve their seven shared baseline values once before iterating.
At
84398484, the eight selected-sleep store tests and three dependent-score tests (including the existing Swift numeric oracle) passed locally on Linux. A normal merge of current main resolved only a generated parity-map conflict through regeneration; the refreshed ledger passed with 289 known findings. Whitespace checks passed. The cold local Android run completed app Kotlin compilation but timed out before unit tests under the worker's memory constraints; Android test success is not claimed for that local run. All 18 CI checks passed at843984844c4382223cb789d24e072680bf2cc2ea: native macOS/iOS app builds, macOS StrandTests, Android APK/full unit suite, Swift packages/tools, parity governance, Tools Python, source hygiene and i18n. The final exact-head check API reports 18 checks and zero non-success results, matching the initial registered roster. Earlier full-suite and native Apple CI evidence above belongs toced6accc; it is not evidence for the updated head.