Summary
immobilityMask / detectNaps guard the trailing edge of the record but not the leading edge, so edge has to guess at the boundary with a wall-clock heuristic instead of being told.
The asymmetry
nap.dart handles a bout running past the end of the record properly — it marks it unfinished and defers it, and propagates that backward through napChainGapSec so a chained fragment is deferred too. That is exactly right.
At the other end there is no equivalent, because stillAt short-circuits its own discontinuity check at k == 0:
bool stillAt(int k) =>
mask.deltaDeg[k] < thr && (k == 0 || absAt(k) - absAt(k - 1) == 1);
So a bout that was already in progress when the record opened is emitted as if it started there, with no way for the caller to tell the difference.
Why it matters downstream
edge slices a day's nap window at local midnight, so index 0 is the day boundary. A nap straddling midnight is emitted whole by the day that owns it and then re-detected as a fresh bout at index 0 by the next day — double-counted, with its minutes subtracted from both days' sleep need.
edge currently works around this (OpenStrap/edge#204) by dropping an index-0 bout when the record is contiguous into midnight, judged by a 60-second timestamp tolerance. That is a guess, and it has a known residual: if the band only starts recording at midnight, the previous day never saw the bout either, so both days drop it and a real nap is lost.
What would fix it properly
Expose leading-edge-unfinished the same way the trailing edge already is — e.g. a startsAtRecordEdge flag on NapWindow, or extend the existing unfinished concept to both ends. The caller can then defer on evidence rather than infer from a timestamp tolerance, and the residual disappears.
ImmobilityMask already carries immobileUnknown for undecidable seconds, which is the same idea — index 0 is undecidable in exactly that sense.
Related
Summary
immobilityMask/detectNapsguard the trailing edge of the record but not the leading edge, so edge has to guess at the boundary with a wall-clock heuristic instead of being told.The asymmetry
nap.darthandles a bout running past the end of the record properly — it marks itunfinishedand defers it, and propagates that backward throughnapChainGapSecso a chained fragment is deferred too. That is exactly right.At the other end there is no equivalent, because
stillAtshort-circuits its own discontinuity check atk == 0:So a bout that was already in progress when the record opened is emitted as if it started there, with no way for the caller to tell the difference.
Why it matters downstream
edge slices a day's nap window at local midnight, so index 0 is the day boundary. A nap straddling midnight is emitted whole by the day that owns it and then re-detected as a fresh bout at index 0 by the next day — double-counted, with its minutes subtracted from both days' sleep need.
edge currently works around this (OpenStrap/edge#204) by dropping an index-0 bout when the record is contiguous into midnight, judged by a 60-second timestamp tolerance. That is a guess, and it has a known residual: if the band only starts recording at midnight, the previous day never saw the bout either, so both days drop it and a real nap is lost.
What would fix it properly
Expose leading-edge-unfinished the same way the trailing edge already is — e.g. a
startsAtRecordEdgeflag onNapWindow, or extend the existingunfinishedconcept to both ends. The caller can then defer on evidence rather than infer from a timestamp tolerance, and the residual disappears.ImmobilityMaskalready carriesimmobileUnknownfor undecidable seconds, which is the same idea — index 0 is undecidable in exactly that sense.Related