Skip to content

fix(recovery): calibrating N tracks real baseline nValid, not a bounds count (#393 follow-up, Bug B) - #449

Merged
ryanbr merged 1 commit into
ryanbr:mainfrom
digitalerdude:fix/recovery-calibration-needsstrap
Jul 15, 2026
Merged

fix(recovery): calibrating N tracks real baseline nValid, not a bounds count (#393 follow-up, Bug B)#449
ryanbr merged 1 commit into
ryanbr:mainfrom
digitalerdude:fix/recovery-calibration-needsstrap

Conversation

@digitalerdude

Copy link
Copy Markdown

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:) (in IntelligenceEngine), 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 returned nil. scoreStateForToday / MetricTileState.resolve then fell through to NeedsStrap, even though the post-recalibration baseline (nValid < 4) was genuinely still cold-starting.

Fix

Compute N as the baseline's real nValid by folding the same day-keyed, epoch-aware history the engine folds. N can then never over-state the seed count, and NeedsStrap is reserved for a truly-established baseline (nValid ≥ seed) with no data today.

  • Swift (RecoveryScorer.calibrationNights): gains dayKeys: + baselineEpoch:, returns foldHistory(...).nValid. TodayView / CoupledView pass repo.days.map(\.day) (epoch defaults to the persisted UserDefaults value, like the engine).
  • Android (recoveryCalibrationNights): gains hrvBaselineEpoch: Double. TodayScreen / CoupledScreen thread NoopPrefs.of(context).getLong(Baselines.hrvBaselineEpochKey, 0L).
  • No structural change to 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 to epoch = 0 (byte-identical to before), plus two recalibration cases each — N reads 2 of 4 after the epoch drops the pre-epoch tail, and stays nil when 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 RecoveryCalibrationTests9/9 pass
  • ./gradlew testFullDebugUnitTest --tests com.noop.ui.RecoveryCalibrationTestBUILD SUCCESSFUL (also compiles the app module)
  • xcodebuild -scheme Strand -destination 'platform=macOS' buildBUILD SUCCEEDED

No hardware path touched (pure analytics + UI wiring).

…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
ryanbr merged commit 9d2ac97 into ryanbr:main Jul 15, 2026
8 checks passed
ryanbr added a commit that referenced this pull request Jul 15, 2026
Rolling testing build off main: #452 i18n gate, #449 calibration count,
#412 carried-Charge source label, #451 Trends l10n, #476 IMU decode-log.
Build numbers bumped (iOS 185→186, Android 274→275); no app-version change.
ryanbr added a commit that referenced this pull request Jul 15, 2026
Rolling testing build off main: #452 i18n gate, #449 calibration count,
#412 carried-Charge source label, #451 Trends l10n, #476 IMU decode-log.
Staging 8.8.0; build numbers 195/284 -> 196/285.
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants