Repository navigation
Backfill skin-temp absolutes for nights the 21-night rescore window never reaches #1851
Description
After #1850 the skin-temp screen leads with the measured temperature whenever any night has one. On a real install that surfaced the next limit: five nights had a temperature and twenty-three had a deviation, so the temperature view spans four days while the baseline view spans three weeks — and #943 then correctly locks 2W/3W/M, because four days of data would redraw the same five points under every chip.
The ask is to show temperatures across the same history the deviation view already covers.
This must be a re-derivation, not a reconstruction
absolute = baseline + deviation is available arithmetic and is wrong. The baseline is a rolling value that drifts over weeks, so applying one night's baseline to another night's delta produces numbers that look like measurements and are not. Same failure class as #194.
The legitimate route is the one the engine already uses: recompute the nightly mean from the raw skinTempSample rows.
The source data is still there
skinTempSample has no age-based retention. The only DELETE is pruneSkinTempByTs(minTs, maxTs), the #547 plausibility quarantine, which removes rows OUTSIDE a valid timestamp window rather than old ones. So every night whose samples were banked can still have its absolute derived.
Why the gap exists
skinTempC is written by the scoring pass (AnalyticsEngine, #1663, 2026-08-27) and analyzeRecent covers a rolling 21-night window. Nights older than that window when the column shipped were never revisited, and nothing walks them since. A night is therefore missing its absolute if it fell outside every window that ran after 27 Aug — regardless of whether its raw samples are sitting on disk.
Shape
A one-off backfill over nights that have a deviation but no skinTempC:
- take the night's stored sleep window from
sleepSession(no re-detection); - read the raw
skinTempSamplerows for that window, plus the HR the worn gate needs; - run the SAME
wornNightlySkinTempCthe engine uses — identical inputs, identical result, no second derivation that could disagree; - persist
skinTempConly; leaveskinTempDevCand every scored value untouched.
Deliberately NOT a full re-score: nothing else about the night should move, and re-scoring 100+ nights is the #836/#841 battery path.
Open questions worth settling before building:
- Trigger. On-demand (Settings or Test Centre) is safest; doing it automatically on upgrade risks a long first launch on a deep history.
- Cost. One sleep-window sample read per night. Bounded and far cheaper than a re-score, but should be measured on a years-deep import before being offered as a one-tap action.
- How far back. Every night with samples, or a capped window.
Diagnostic first
Before building the backfill, a per-night report — does this night have skinTempC, how many raw skinTempSample rows does its sleep window hold, and what did the worn gate return — would establish whether the missing nights actually have recoverable data. On the reporting install, 7–11 Aug filled while 12–25 Aug did not, which pruning cannot explain (it removes older rows first, not newer ones), so the answer may be "no samples were banked" rather than "the backfill never ran".
Activity
ryanbr commentedon Sep 3, 2026
Design constraint found while reading the engine, before writing any of this.
skinAnchorRaw(#938 second capture) is learned window-wide from the scan window's skin samples, and the comment explains why that is safe:That safety argument holds for deviations and does not transfer to absolutes. The offset cancels in a delta; an absolute is offset-sensitive by construction — which is also why the 4.0 mapping is documented as "a defensible worn-range mapping, NOT a claimed-accurate absolute thermometer".
So a backfill that re-learns the anchor from the OLD window it is filling would write those nights on a different offset from the ones the engine already wrote. The chart would then plot two scales as one uniform line — the exact class of error
dominantKind(#1705) exists to prevent, except invisible, because both sides are labelled °C and both look plausible.Constraint: on a WHOOP 4.0 the backfill must use the SAME anchor the engine is currently using, passed in rather than re-derived from the window being filled. If no current anchor is resolvable, it must write nothing for that device rather than write on a guessed offset. WHOOP 5/MG is unaffected —
raw / 100carries no anchor.Also worth stating, since it affects what the feature can promise: a night is only recoverable if
sleepSessionstill holds its window AND its raw rows are present. The backfill must report how many nights it filled and how many it could not, rather than silently doing less than the user asked for.evoveotech commentedon Sep 10, 2026
Verified fixed:
SkinTempBackfillWalkeris re-applied (commit b3b68e1) on both platforms (Swift:Strand/Data/SkinTempBackfillWalker.swift,Packages/StrandAnalytics/Sources/StrandAnalytics/SkinTempBackfill.swift, Kotlin:WhoopDatabase.ktline 918) with tests (SkinTempBackfillTests.swift).