Conversation
|
Verified both claims against the code, and the PR is underselling itself.
But this is not only a reading problem. The same column reaches the Charge SCORE, at // RecoveryScorer.swift:361, skinTempScaleC = 1.0, wSkinTemp = 0.05
if let dev = skinTempDev {
terms.append((-abs(dev) / skinTempScaleC, wSkinTemp))
}At the 33.5 you measured that is a term of about -33, against other terms that live in single digits. So imported nights have not merely displayed a wrong number, they have been SCORED with a large negative skin-temp penalty for as long as the import has existed. That makes the one-time repair considerably more valuable than "nobody has to re-import", and it is worth saying in the PR body, because a reviewer skimming for risk will otherwise read this as cosmetic. Three things I would want settled before this lands:
Design looks right otherwise. Deriving the deviation from the PRECEDING nights is the correct call: folding the night into its own baseline would leak the value into its own deviation, and matching On the Android note in the description, that matches what I would expect, though it is worth confirming rather than assuming since the Kotlin importer may not share this shape. Thanks @Iskrata, this is a good catch and a well-scoped change. |
41afb7d to
f7713d4
Compare
|
Thanks. One correction first, because it changes what the repair has to do. The stored Charge on imported nights was not scored with the penalty. Imported rows keep the export's own recovery.
The description now says this, so it doesn't read as cosmetic or as a scoring bug.
Rebased onto current main with no conflicts; the ratchet reports 0 errors. Android: I'll check the Kotlin importer for the same shape once my Android toolchain is set up, and add the matching fix here if it has it, rather than guess. |
f7713d4 to
cab7884
Compare
|
Checked the Kotlin importer as promised: it has the same shape ( |
cab7884 to
cb4b6ba
Compare
|
Reviewed at cb4b6ba. A one-shot repair that rewrites stored rows deserves more than a read-through, so here is what I actually checked rather than assumed. All of it came back clean:
The finding: the Kotlin rounding reverses a decision this codebase already madeThe new // Round HALF-AWAY-FROM-ZERO to 2 dp to match Swift's Double.rounded()
// (IntelligenceEngine.swift:291). Math.round() is half-UP and would diverge on negative
// .5 ties (e.g. -2.5 -> -2 here vs Swift's -3). (Cross-platform parity.)
val r = if (scaled >= 0) Math.floor(scaled + 0.5) else Math.ceil(scaled - 0.5)Same metric, same 2 dp, same signed column, and the new path uses precisely the function that comment exists to warn against. Confirmed on a JDK rather than reasoned about: a deviation of -0.005 gives +0.00 in Kotlin against -0.01 in Swift; -0.015 gives -0.01 against -0.02. Positives agree. The description says the value is "rounded to 0.01 °C like the on-device deviation", which is true on positives only, and a skin-temp deviation is negative about half the time by construction. What makes this worth fixing before merge rather than after: the repair is one-shot behind While you are in there, it is probably worth extracting that rounding into one shared helper that both call, so a third copy cannot drift the same way. Two smaller notes
On the two local Approving once the Kotlin rounding uses the existing half-away-from-zero form and the branch is rebased. The analysis in the description is unusually good, particularly tracing why the stored Charge score never reads the column; I checked that claim and it holds. |
…t as the deviation The WHOOP export ships absolute skin temperature (°C). The importer wrote it into `skinTempDevC`, the deviation column, with a NOTE saying so. Several screens sniff the magnitude and cope (`VitalBands.isAbsoluteSkinTemp`), but callers that trust the column do not: the Charge breakdown feeds `row.skinTempDevC` to `RecoveryScorer.skinTempRelative` and the Trends report charts it, so an imported night read as a +33 °C deviation. On one install the imported rows averaged 33.5 in that column against ~0.0 for computed nights. Imported rows now carry the absolute in `skinTempC`, and `skinTempDevC` is that absolute minus the personal baseline folded over the nights before it (`Baselines.metricCfg["skin_temp"]`, the absolute-°C config), rounded like the on-device deviation and nil until the baseline is usable. A one-shot launch repair moves already-imported absolutes out of the deviation column and recomputes them, so existing installs don't need to re-import.
… absolute (never the deviation) as Skin temp (celsius) on both platforms
…ugh one shared helper Math.round rounds negative ties up, so a -0.005 C deviation stored +0.00 on Android against -0.01 on iOS, and the one-shot repair would have kept it. Baselines.roundedDelta2dp now carries the half-away-from-zero form recomputeSkinTempDev already used, and both callers go through it. Notes on both platforms that the column mixes two baselines.
cb4b6ba to
e28bd24
Compare
|
Thanks. Both done in
Locally: |
Apply the Apple changes from ryanbr/noop PR ryanbr#2285 by @Iskrata (e28bd24). Increment the import mapping revision and verify unrelated metrics and other sources survive repair. Android and parity metadata are intentionally unchanged under the requested iPhone-only scope.
What this PR does
The WHOOP export ships absolute skin temperature (°C), but
WhoopImporterwrote it intoskinTempDevC, the deviation column, with aNOTEacknowledging the mismatch. Screens that check the magnitude cope with this (VitalBands.isAbsoluteSkinTemp), but callers that trust the column don't:row.skinTempDevCinto the drivers (ChargeBreakdownWiring.swift:49), so an imported day's "why" showed a skin-temp driver of about −33;AICoach.swift:1215).So an imported night reads as a +33 °C deviation. On one install the imported rows averaged 33.5 in that column, against ~0.0 for NOOP-computed nights.
What it does not reach is the stored Charge score. Imported rows keep the export's own recovery.
recomputeRecovery/recomputeChargeDrivers(IntelligenceEngine.swift:3094,:3112,:3131) run only for the nights NOOP scores itself (recomputeRecoveryDailyat:2065, drivers at:2080, both inside thescoredNightsloop). There the deviation is recomputed from nightly skin samples against an on-device-only baseline (:1875).This PR fixes it in three parts:
skinTempC.skinTempDevCbecomes that absolute minus the personal baseline folded over the preceding nights (Baselines.metricCfg["skin_temp"], the absolute-°C config). It is rounded to 0.01 °C like the on-device deviation (recomputeSkinTempDev), and nil until the baseline is usable.repairAbsoluteSkinTempIfNeeded) moves already-imported absolutes out of the deviation column and recomputes the deviations, so nobody has to re-import. No re-score is needed, since stored scores never read the column (above); the repair refreshes the repository so the breakdown, Trends and Coach read the corrected values straight away.upsertDailyMetricsfollowed by the flag. A kill before the upsert commits changes nothing, and the repair runs again. A kill between the upsert and the flag means the next launch finds the moved rows (skinTempCset, so the guard skips them) and recomputes identical deviations. It changes nothing and then sets the flag.VitalBands.isAbsoluteSkinTempstays for now. It is how the repair finds old rows, and it covers rows imported on a build without this fix until the repair has run. The rule is physically safe: no wrist skin temperature is below 20 °C, and no nightly deviation reaches ±20 °C. It can be removed once that window has passed.Export. The WHOOP CSV exporter wrote
skinTempDevCinto itsSkin temp (celsius)column, so a computed night exported its deviation (~0.2) as a skin temperature, and a re-import then stored it as the absolute. Both exporters now writeskinTempC, falling back toskinTempDevConly when it is itself an absolute (the shape older imports stored). A true deviation is never written as a temperature. The newexportedSkinTempCelsiushelper is the same on both platforms.Android. The Kotlin importer has the same bug:
WhoopCsvImporter.parseCycleswroteskin_temp_celsiusintoskinTempDevC(WhoopCsvImporter.kt:354→:387). This PR adds the twin:withSkinTempDeviationsfills the deviation from prior nights, andparseCyclesnow writesskinTempC;skinTempRepair/repairAbsoluteSkinTempIfNeededis the one-time repair, run fromAppViewModelbefore the Effort rescore behindNoopPrefs.KEY_SKIN_TEMP_REPAIR_DONE, with the same interrupted-repair semantics as Swift;WhoopCsvExporter.exportedSkinTempCelsiusis the export fix.Recorded in
parity_dispositions.json: the KotlinrepairAbsoluteSkinTempIfNeededtwins a Swift function in the app layer (Strand/Data/WhoopImporter.swift), outside the governed roots.Type of change
How it was tested
StrandTests/WhoopImportSkinTempTests(3 tests), plusWhoopCsvExporterTests.testTheSkinColumnCarriesTheAbsoluteNeverTheDeviation.compileFullDebugKotlin+testFullDebugUnitTest):WhoopCsvImporterTesthas 3 new tests (deviation from prior nights and never the absolute; a row without an absolute keeps its deviation; the repair moves an imported absolute once and is idempotent). The two Importing a Fahrenheit WHOOP export stores the °F number as if it were °C #1849 °F/°C tests now assertskinTempC.WhoopCsvExporterTesthas a new test for the absolute-never-deviation column.SkinTempFallbackNoteTeststill passes.RecoveryDriversTest(-0.4999999999999929vs-0.5half-tie rounding). They fail identically with this change stashed, so they are unrelated; possibly a JDK or architecture difference on this Mac, given your run passed.doc_comment_lint.pyis clean.Checklist
android/(see the two unrelatedRecoveryDriversTestfailures above)docs/CONTRIBUTING.mdStrand.xcodeproj/) or any secrets/keystores