Repository navigation
Baselines: imported rows with nil values no longer shadow computed nights - #401
Merged
Merged
Conversation
added 2 commits
July 13, 2026 12:23
…ghts The nightly-into-history merge only checked KEY presence before letting a computed night in. A Health Connect / backup import that creates a row for a day with a nil HRV/RHR value therefore shadowed the real strap-computed night forever: zero nights reached the baseline fold, the baseline never left CALIBRATING, and Today rendered "No Data" / "Needs the strap" despite scored sleep (the reported bug). Fix, per-field and identical on both platforms: imported non-nil wins; a nil (or absent) slot is backfilled by the computed value. - Swift: hist[day] == nil on [String: Double?] is only true for an absent key (.some(.none) passed the old check). Also mark the helper nonisolated so it is testable off the main actor. - Kotlin: day !in hist had the same shape; hist[day] == null covers both absent keys and null values. Tests (RED first on both platforms): IntelligenceBaselineShadowTests (Swift) and IntelligenceBaselineShadowTest (Kotlin) cover imported-non-nil wins, nil backfilled, no-computed stays missing, and the 7/7 strap-night starvation end-to-end shape.
The carriedLastNight titles are now interpolated ("Last night · %@")
so the string-catalog extractor sees the format key (ryanbr#779), which makes
LocalizedStringKey equality against a literal key always false. Resolve
the key + arguments via Mirror and pin the rendered user-visible copy.
TheBoroer
marked this pull request as ready for review
July 13, 2026 17:47
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
From #34 (alarm not buzzing): the strap consistently reported a STALE armed time regardless of what NOOP sent (its clock was reset) — the ryanbr#401 readback caught it, but nothing surfaced it and the log even claimed 'armed' while disconnected. #3 — Alarm block in the debug export (both platforms): the configured wake + the last arm's sent-vs-strap-reports epochs with a MISMATCH flag, so a 'didn't buzz' report is decidable at a glance. armStrapAlarm persists the sent epoch (+ when + connected); the GET_ALARM_TIME readback persists what the strap reports; the export composes them (best-effort, guarded). #4 — don't log 'Alarm: armed' when the strap isn't connected (the SET was dropped). Log 'queued — will send on next connect' instead, so the log stops claiming an arm that never transmitted. Both platforms (iOS BLEManager/FrameRouter/DebugDataDiagnostics; Android WhoopBleClient/ AndroidDiagnostics). Android compiles clean; iOS gated by CI.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…ghts (ryanbr#401) * Baselines: imported rows with nil values no longer shadow computed nights The nightly-into-history merge only checked KEY presence before letting a computed night in. A Health Connect / backup import that creates a row for a day with a nil HRV/RHR value therefore shadowed the real strap-computed night forever: zero nights reached the baseline fold, the baseline never left CALIBRATING, and Today rendered "No Data" / "Needs the strap" despite scored sleep (the reported bug). Fix, per-field and identical on both platforms: imported non-nil wins; a nil (or absent) slot is backfilled by the computed value. - Swift: hist[day] == nil on [String: Double?] is only true for an absent key (.some(.none) passed the old check). Also mark the helper nonisolated so it is testable off the main actor. - Kotlin: day !in hist had the same shape; hist[day] == null covers both absent keys and null values. Tests (RED first on both platforms): IntelligenceBaselineShadowTests (Swift) and IntelligenceBaselineShadowTest (Kotlin) cover imported-non-nil wins, nil backfilled, no-computed stays missing, and the 7/7 strap-night starvation end-to-end shape. * Tests: render LocalizedStringKey titles instead of comparing keys The carriedLastNight titles are now interpolated ("Last night · %@") so the string-catalog extractor sees the format key (ryanbr#779), which makes LocalizedStringKey equality against a literal key always false. Resolve the key + arguments via Mirror and pin the rendered user-visible copy. --------- Co-authored-by: Boro Vukovic <hello@loudtronix.ca>
simoncad7
pushed a commit
to simoncad7/noop
that referenced
this pull request
Aug 17, 2026
…ghts (ryanbr#401) * Baselines: imported rows with nil values no longer shadow computed nights The nightly-into-history merge only checked KEY presence before letting a computed night in. A Health Connect / backup import that creates a row for a day with a nil HRV/RHR value therefore shadowed the real strap-computed night forever: zero nights reached the baseline fold, the baseline never left CALIBRATING, and Today rendered "No Data" / "Needs the strap" despite scored sleep (the reported bug). Fix, per-field and identical on both platforms: imported non-nil wins; a nil (or absent) slot is backfilled by the computed value. - Swift: hist[day] == nil on [String: Double?] is only true for an absent key (.some(.none) passed the old check). Also mark the helper nonisolated so it is testable off the main actor. - Kotlin: day !in hist had the same shape; hist[day] == null covers both absent keys and null values. Tests (RED first on both platforms): IntelligenceBaselineShadowTests (Swift) and IntelligenceBaselineShadowTest (Kotlin) cover imported-non-nil wins, nil backfilled, no-computed stays missing, and the 7/7 strap-night starvation end-to-end shape. * Tests: render LocalizedStringKey titles instead of comparing keys The carriedLastNight titles are now interpolated ("Last night · %@") so the string-catalog extractor sees the format key (ryanbr#779), which makes LocalizedStringKey equality against a literal key always false. Resolve the key + arguments via Mirror and pin the rendered user-visible copy. --------- Co-authored-by: Boro Vukovic <hello@loudtronix.ca>
7 of 12 tasks
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.
What this PR does
Fixes a "Needs the strap" / "No Data" false negative on Today and Recovery when the user does have sleep data for the day.
When a day had both an imported row (e.g. a Whoop/backup import) and an on-device computed row, the merge in
IntelligenceEnginelet the imported row win the whole day even when its individual fields werenil. Thosenilfields then shadowed real computed values, so recovery/sleep scoring saw an empty night and the UI fell back to "Needs the strap" / "No Data" despite a scored sleep session existing.The merge now treats imported precedence per-field: an imported
nilno longer blanket-wins the day, and computed values fill any fields the import doesn't actually carry. Applied identically to the Swift engine (Strand/Data/IntelligenceEngine.swift) and its Kotlin twin (android/.../com/noop/analytics/IntelligenceEngine.kt) so the two lanes stay in step.Also updates
TodayExplainabilityTeststo renderLocalizedStringKeytitles instead of comparing keys: the carried-night titles are built with interpolation ("Last night · %@") so the string-catalog extractor sees the format key, which makes literal-key equality always false. The tests now resolve the key + arguments and pin the exact user-visible copy.Type of change
How it was tested
Analytics/merge change only — no BLE path touched.
nilfields + computed sibling for the same day):StrandTests/IntelligenceBaselineShadowTests.swiftandandroid/.../IntelligenceBaselineShadowTest.kt.xcodebuild test -project Strand.xcodeproj -scheme Strand -destination 'platform=macOS,arch=arm64'—IntelligenceBaselineShadowTestsand the fullTodayExplainabilityTestssuite pass (TEST SUCCEEDED)../gradlew :app:testDemoDebugUnitTest(the demo flavor is the unit-test variant in this checkout) — shadow test plus the existing baseline/recalibration tests pass, BUILD SUCCESSFUL.nilrow blocking the computed night before the fix and the recovered score after it.Checklist
Packages/<name>not touched; change is in theStrandapp target)android/(ran:app:testDemoDebugUnitTest; notestFullDebugUnitTesttask in this project's flavor setup)docs/CONTRIBUTING.mdStrand.xcodeproj/) or any secrets/keystoresRelated issues
No upstream issue filed — fix comes from a user report (screenshots showed "Needs the strap" + Recovery "No Data" on a day with a 5h 24m scored sleep in the same backup).