Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions Strand/Screens/ChargeBreakdownFormat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,18 @@ struct ChargeBreakdownSection: View {
let confidence: ScoreConfidence
var skinTempRel: SkinTempRelative? = nil

/// The label `RecoveryScorer.chargeDrivers` gives its skin-temperature row, pinned against the scorer's
/// own output by `ChargeBreakdownSkinTempOnceTests`.
static let skinTempDriverLabel = "Skin temperature"

/// Whether the separate relative skin-temperature row adds anything. `RecoveryScorer.chargeDrivers`
/// lists a skin-temperature driver for every night that carries a deviation, read from the same
/// `skinTempDevC` the callers pass as `skinTempRel`, so drawing `SkinTempDeviationRow` as well printed one
/// reading twice ("two readouts of one fact", AGENTS.md). Android's sheet draws the driver row only.
static func showsSeparateSkinTempRow(drivers: [ChargeDriver]) -> Bool {
!drivers.contains { $0.label == skinTempDriverLabel }
}

var body: some View {
VStack(alignment: .leading, spacing: NoopMetrics.cardInnerSpacing) {
Divider().overlay(StrandPalette.hairline)
Expand All @@ -314,8 +326,9 @@ struct ChargeBreakdownSection: View {
}
}
// A5 , the night's relative skin-temp marker, tagged REL., shown as a deviation from the
// personal normal rather than a fake clinical absolute. Only when the night carries one.
if let rel = skinTempRel {
// personal normal rather than a fake clinical absolute. Only when the night carries one AND no
// driver row above already reports it.
if let rel = skinTempRel, Self.showsSeparateSkinTempRow(drivers: drivers) {
SkinTempDeviationRow(rel: rel)
.padding(.top, NoopMetrics.space1)
}
Expand Down
2 changes: 1 addition & 1 deletion Strand/Screens/IntelligenceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ struct IntelligenceView: View {
// A1: "What shaped it" , one row per engine-supplied Charge driver. Gated on a non-empty
// list so a cold-start / calibrating night (no real contributions to attribute) shows
// nothing here rather than a fake breakdown. A5's relative skin-temp marker rides at the
// foot when the night carries one.
// foot only when no driver row already reports the night's skin temperature.
if !d.drivers.isEmpty {
ChargeBreakdownSection(drivers: d.drivers,
confidence: d.confidence,
Expand Down
36 changes: 36 additions & 0 deletions StrandTests/ChargeBreakdownSkinTempOnceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import XCTest
import StrandAnalytics
@testable import Strand

/// The Charge breakdown must report a night's skin temperature once. The scorer's driver list already
/// carries a skin-temperature row whenever the night has a deviation, so the separate relative row is drawn
/// only when that driver is absent. These run the real `RecoveryScorer.chargeDrivers`, so a renamed driver
/// label fails here instead of silently bringing the second row back.
final class ChargeBreakdownSkinTempOnceTests: XCTestCase {

private var trustedHrvFixture: BaselineState {
BaselineState(baseline: 55, spread: 12 / 1.253, nValid: 20, nightsSinceUpdate: 0, status: .trusted)
}

private func driverRows(skinTempDev: Double?) -> [ChargeDriver] {
RecoveryScorer.chargeDrivers(hrv: 58, rhr: 54, resp: nil,
hrvBaseline: trustedHrvFixture, rhrBaseline: nil,
respBaseline: nil, sleepPerf: 0.85, skinTempDev: skinTempDev)
}

func testANightWithADeviationReportsSkinTemperatureOnlyAsADriverRow() {
for deviation in [0.4, -0.6, 0.0] {
let drivers = driverRows(skinTempDev: deviation)
XCTAssertTrue(drivers.contains { $0.label == ChargeBreakdownSection.skinTempDriverLabel },
"The scorer must list skin temperature for a deviation of \(deviation).")
XCTAssertFalse(ChargeBreakdownSection.showsSeparateSkinTempRow(drivers: drivers),
"A deviation of \(deviation) would be printed twice.")
}
}

func testANightWithoutADeviationHasNoSkinTemperatureDriver() {
let drivers = driverRows(skinTempDev: nil)
XCTAssertFalse(drivers.contains { $0.label == ChargeBreakdownSection.skinTempDriverLabel })
XCTAssertTrue(ChargeBreakdownSection.showsSeparateSkinTempRow(drivers: drivers))
}
}
Loading