The defect
Sleep consistency reports a typical bedtime that no night is near. On the current 14-night window it renders typical bedtime 20:48 while 0 of 14 nights fall within an hour of it. The percentage built on top of that number is equally meaningless.
Two independent bugs stack, both in AtriaSleepConsistency.result(from:) in Atria/Atria/Sessions.swift.
1. The noon anchor splits a single cluster in half
// Anchor both timings at the night that starts in the evening.
return (night.id, night.day,
bedtime < 12 * 60 ? bedtime + 24 * 60 : bedtime,
wake < 12 * 60 ? wake + 24 * 60 : wake)
The anchor assumes sleep begins in the evening. For any wearer whose main sleep starts near midday, the cluster straddles the anchor:
| Real bedtime |
Anchored minutes |
|
| 11:28 |
2,128 (= 11:28 + 24 h) |
pushed a full day forward |
| 12:08 |
728 |
left alone |
Two nights 40 minutes apart become 23 h 20 m apart. The anchor is not wrong in general — it is wrong for this wearer, and it fails silently.
2. Arithmetic mean over a bimodal set
let typicalBedtime = Int((Double(bedtimes.reduce(0, +)) / Double(bedtimes.count)).rounded())
Once the anchor has scattered the nights to both ends of the axis, the mean lands in the empty middle — which is how a wearer who reliably sleeps in the early afternoon is told their typical bedtime is 20:48. recommendedWindow already uses medianWake; the two typicals do not.
Why the fix is coordinated, not a one-liner
The anchored minute values are load-bearing beyond this struct. NightDeviation.bedtimeMinutes / .wakeMinutes feed the schedule visual, which maps them through AtriaSleepConsistencyStrip.fraction(_:) against axisStartMinutes (AtriaHealthScreen.swift:3323). Changing the anchor without moving the axis moves every night off the strip. AtriaFitnessAge also consumes combinedPercent as a scored factor, so the number changing is a user-visible scoring change, not just a label fix.
Suggested direction
A circular mean on a 24 h clock is the standard fix and is byte-identical for a conventional evening sleeper — worth checking, because it means the change can be validated as a no-op on normal data before it is trusted on shifted data. It alone does not fix this case (still 0% here); the anchor has to stop being a fixed noon constant and become derived from the wearer's own cluster.
Done when
Related: the wearer's shifted schedule is the same condition that made #25 hard.
The defect
Sleep consistency reports a typical bedtime that no night is near. On the current 14-night window it renders
typical bedtime 20:48while 0 of 14 nights fall within an hour of it. The percentage built on top of that number is equally meaningless.Two independent bugs stack, both in
AtriaSleepConsistency.result(from:)inAtria/Atria/Sessions.swift.1. The noon anchor splits a single cluster in half
The anchor assumes sleep begins in the evening. For any wearer whose main sleep starts near midday, the cluster straddles the anchor:
Two nights 40 minutes apart become 23 h 20 m apart. The anchor is not wrong in general — it is wrong for this wearer, and it fails silently.
2. Arithmetic mean over a bimodal set
Once the anchor has scattered the nights to both ends of the axis, the mean lands in the empty middle — which is how a wearer who reliably sleeps in the early afternoon is told their typical bedtime is 20:48.
recommendedWindowalready usesmedianWake; the two typicals do not.Why the fix is coordinated, not a one-liner
The anchored minute values are load-bearing beyond this struct.
NightDeviation.bedtimeMinutes/.wakeMinutesfeed the schedule visual, which maps them throughAtriaSleepConsistencyStrip.fraction(_:)againstaxisStartMinutes(AtriaHealthScreen.swift:3323). Changing the anchor without moving the axis moves every night off the strip.AtriaFitnessAgealso consumescombinedPercentas a scored factor, so the number changing is a user-visible scoring change, not just a label fix.Suggested direction
A circular mean on a 24 h clock is the standard fix and is byte-identical for a conventional evening sleeper — worth checking, because it means the change can be validated as a no-op on normal data before it is trusted on shifted data. It alone does not fix this case (still 0% here); the anchor has to stop being a fixed noon constant and become derived from the wearer's own cluster.
Done when
AtriaFitnessAgefactor stay correct under the new anchor.Related: the wearer's shifted schedule is the same condition that made #25 hard.