fix(recovery): calibrating N tracks real baseline nValid, not a bounds count (#393 follow-up, Bug B) - #449
Merged
ryanbr merged 1 commit intoJul 15, 2026
Conversation
…s count (ryanbr#393 follow-up, Bug B) The recovery engine folds the HRV baseline with the epoch-aware Baselines.foldHistory(_:dayKeys:cfg:baselineEpoch:), which DROPS every night dated before a manual "Recalibrate HRV baseline" epoch. But the calibrating helper counted all in-range nights. After a recalibration, a user's pre-epoch nights pushed the count past minNightsSeed, so the helper returned nil and the Today score side fell through to NeedsStrap while the post-recalibration baseline was genuinely still cold-starting. Compute N as the baseline's real nValid by folding the SAME day-keyed, epoch-aware history the engine folds, so N can never over-state the seed count and NeedsStrap is reserved for a truly-established baseline (nValid >= seed) with no data today. Byte-identical Swift/Kotlin; identical user-facing strings. - Swift: RecoveryScorer.calibrationNights gains dayKeys + baselineEpoch and returns foldHistory(...).nValid; TodayView/CoupledView pass repo.days.map(\.day). - Android: recoveryCalibrationNights gains hrvBaselineEpoch; TodayScreen/ CoupledScreen thread NoopPrefs hrvBaselineEpochKey. - Extend RecoveryCalibrationTest(s) on both platforms with recalibration cases. Verified: swift test RecoveryCalibrationTests (9/9), Android testFullDebugUnitTest --tests RecoveryCalibrationTest, macOS Strand build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ryanbr
added a commit
that referenced
this pull request
Jul 15, 2026
ryanbr
added a commit
that referenced
this pull request
Jul 15, 2026
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…s count (ryanbr#393 follow-up, Bug B) (ryanbr#449) The recovery engine folds the HRV baseline with the epoch-aware Baselines.foldHistory(_:dayKeys:cfg:baselineEpoch:), which DROPS every night dated before a manual "Recalibrate HRV baseline" epoch. But the calibrating helper counted all in-range nights. After a recalibration, a user's pre-epoch nights pushed the count past minNightsSeed, so the helper returned nil and the Today score side fell through to NeedsStrap while the post-recalibration baseline was genuinely still cold-starting. Compute N as the baseline's real nValid by folding the SAME day-keyed, epoch-aware history the engine folds, so N can never over-state the seed count and NeedsStrap is reserved for a truly-established baseline (nValid >= seed) with no data today. Byte-identical Swift/Kotlin; identical user-facing strings. - Swift: RecoveryScorer.calibrationNights gains dayKeys + baselineEpoch and returns foldHistory(...).nValid; TodayView/CoupledView pass repo.days.map(\.day). - Android: recoveryCalibrationNights gains hrvBaselineEpoch; TodayScreen/ CoupledScreen thread NoopPrefs hrvBaselineEpochKey. - Extend RecoveryCalibrationTest(s) on both platforms with recalibration cases. Verified: swift test RecoveryCalibrationTests (9/9), Android testFullDebugUnitTest --tests RecoveryCalibrationTest, macOS Strand build. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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.
Bug B (#393 follow-up)
While the recovery HRV baseline is genuinely still calibrating (
nValid < Baselines.minNightsSeed), the Today screen could show "Needs the strap / Was your strap worn?" instead of "Calibrating N of 4".Cause
The recovery engine folds the HRV baseline with the epoch-aware
Baselines.foldHistory(_:dayKeys:cfg:baselineEpoch:)(inIntelligenceEngine), which drops every night dated before a manual "Recalibrate HRV baseline" epoch. But the calibrating helper —RecoveryScorer.calibrationNights/recoveryCalibrationNights— counted all in-range nights.After a recalibration, a user's old pre-epoch nights pushed that per-night bounds count past
minNightsSeed, so the helper returnednil.scoreStateForToday/MetricTileState.resolvethen fell through toNeedsStrap, even though the post-recalibration baseline (nValid < 4) was genuinely still cold-starting.Fix
Compute N as the baseline's real
nValidby folding the same day-keyed, epoch-aware history the engine folds. N can then never over-state the seed count, andNeedsStrapis reserved for a truly-established baseline (nValid ≥ seed) with no data today.RecoveryScorer.calibrationNights): gainsdayKeys:+baselineEpoch:, returnsfoldHistory(...).nValid.TodayView/CoupledViewpassrepo.days.map(\.day)(epoch defaults to the persistedUserDefaultsvalue, like the engine).recoveryCalibrationNights): gainshrvBaselineEpoch: Double.TodayScreen/CoupledScreenthreadNoopPrefs.of(context).getLong(Baselines.hrvBaselineEpochKey, 0L).ScoreState/scoreStateForToday/MetricTileState— the fix flows through the single calibration-count source.Byte-identical Swift/Kotlin logic; user-facing strings unchanged (byte-parity contract holds). Kept separate from #421 (one concern per PR).
Tests
Extended
RecoveryCalibrationTest(s)on both platforms: existing cases pinned toepoch = 0(byte-identical to before), plus two recalibration cases each — N reads2 of 4after the epoch drops the pre-epoch tail, and staysnilwhen enough post-epoch nights cross the seed gate.Verification
App-target Swift + Android UI have no default CI, so built/tested locally on top of
upstream/main:swift test --filter RecoveryCalibrationTests→ 9/9 pass./gradlew testFullDebugUnitTest --tests com.noop.ui.RecoveryCalibrationTest→ BUILD SUCCESSFUL (also compiles the app module)xcodebuild -scheme Strand -destination 'platform=macOS' build→ BUILD SUCCEEDEDNo hardware path touched (pure analytics + UI wiring).