Repository navigation
fix(recovery): provisional/final states, no early pins, one headline source (refs #543) #578
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3efb856
71f5e3f
f939cd3
8954914
ae9df7d
6f0780e
49429ce
c1344fd
7b687cf
eb82b17
b77bc13
2742595
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2101,6 +2101,62 @@ bool overnightSettled({ | |
| (nowSec != null && nowSec >= sleepOffsetSec + kOvernightGiveUpSec); | ||
| } | ||
|
|
||
| /// How long the band's data edge must run past a detected wake before that | ||
| /// wake counts as CONFIRMED rather than "where the sync happened to stop". | ||
| /// Mid-drain the stager closes the window at the newest record, so a wake | ||
| /// sitting at the edge is no wake at all. | ||
| // ponytail: fixed 30 min; tune against real drain timings if provisional flickers. | ||
| const int kWakeConfirmMarginSec = 30 * 60; | ||
|
|
||
| /// A pin is only trusted for the night whose wake is within this of its own. | ||
| const int kPinWakeToleranceSec = 60 * 60; | ||
|
|
||
| /// A night whose wake is before this local hour, or that slept less than | ||
| /// [kMinPinSleepSec], is never frozen: the reported 2-instead-of-28 morning was | ||
| /// a pin taken off the first three hours of a night still in progress. | ||
| // ponytail: population heuristics; a night-shift sleeper never pins and reads | ||
| // the live final value instead, which is the safe side. | ||
| const int kMinPinWakeHour = 3; | ||
| const int kMinPinSleepSec = 3 * 60 * 60; | ||
|
|
||
| /// Today's recovery headline, in the one vocabulary every surface reads. | ||
| enum RecoveryState { nightInProgress, provisional, finalReady } | ||
|
|
||
| extension RecoveryStateWire on RecoveryState { | ||
| String get wire => switch (this) { | ||
| RecoveryState.nightInProgress => 'night_in_progress', | ||
| RecoveryState.provisional => 'provisional', | ||
| RecoveryState.finalReady => 'final', | ||
| }; | ||
| } | ||
|
|
||
| /// Where today's night stands: a wake the edge has not yet run | ||
| /// [kWakeConfirmMarginSec] past is still in progress; a confirmed wake whose | ||
| /// window has not settled ([overnightSettled]) is provisional; settled is | ||
| /// final. No window at all and not settled is null: nothing says the user is | ||
| /// asleep (an unworn night reads the same until noon), so no state is claimed. | ||
| RecoveryState? recoveryStateOf({ | ||
| required int? wakeSec, | ||
| required int dataEdgeSec, | ||
| int? nowSec, | ||
| }) { | ||
| if (overnightSettled( | ||
| sleepOffsetSec: wakeSec, dataEdgeSec: dataEdgeSec, nowSec: nowSec)) { | ||
| return RecoveryState.finalReady; | ||
| } | ||
| if (wakeSec == null) return null; | ||
| return dataEdgeSec >= wakeSec + kWakeConfirmMarginSec | ||
| ? RecoveryState.provisional | ||
| : RecoveryState.nightInProgress; | ||
| } | ||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||
|
|
||
| /// Whether a night is a plausible MAIN night to freeze (see [kMinPinWakeHour]). | ||
| bool pinnableNight({required int wakeSec, int? onsetSec}) { | ||
| final wake = DateTime.fromMillisecondsSinceEpoch(wakeSec * 1000); | ||
| if (wake.hour < kMinPinWakeHour) return false; | ||
| return onsetSec == null || wakeSec - onsetSec >= kMinPinSleepSec; | ||
| } | ||
|
|
||
| /// The cross-day inputs an importer fills with its vendor's own scores. | ||
| const _crossDayVendorScoreKeys = [ | ||
| 'rhr', | ||
|
|
@@ -2133,14 +2189,21 @@ const _crossDayVendorScoreKeys = [ | |
| required int? liveReadiness, | ||
| required ({String day, int value, int? wakeSec})? current, | ||
| int? wakeSec, | ||
| int? onsetSec, | ||
| }) { | ||
| // A pin with no wake (written by an older build) cannot say which night it | ||
| // is, so a night that does know its wake replaces it once that night settles. | ||
| final sameNight = current != null && | ||
| current.day == today && | ||
| (current.wakeSec == null || | ||
| wakeSec == null || | ||
| (wakeSec - current.wakeSec!).abs() < _headlineFreezeMarginSec); | ||
| if (sameNight) return current; // pinned; hold | ||
| if (overnightComplete && liveReadiness != null) { | ||
| (wakeSec == null || | ||
| (current.wakeSec != null && | ||
| (wakeSec - current.wakeSec!).abs() < kPinWakeToleranceSec)); | ||
|
sourcery-ai[bot] marked this conversation as resolved.
|
||
| final pinnable = | ||
| wakeSec == null || pinnableNight(wakeSec: wakeSec, onsetSec: onsetSec); | ||
| // A re-derive can shrink the same night below a main night (onset moved); | ||
| // its old pin no longer describes a pinnable night, so it is not held. | ||
| if (sameNight) return pinnable ? current : null; // pinned; hold | ||
|
Comment on lines
+2203
to
+2205
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Clear the stored pin when the same night stops being pinnable.
Return an explicit "release" result, or let the caller call Proposed fix- if (sameNight) return pinnable ? current : null; // pinned; hold
+ if (sameNight) return pinnable ? current : null; // null here = release// in _maybeFreezeHeadlineReadiness, before `if (next == null) return;`
if (next == null) {
if (current != null && current.day == day.date &&
!pinnableNight(wakeSec: day.sleepOffsetSec, onsetSec: day.sleepOnsetSec)) {
await LocalDb.releaseFrozenHeadline(day.date);
}
return;
}🤖 Prompt for AI Agents |
||
| if (overnightComplete && pinnable && liveReadiness != null) { | ||
| // first complete settle of this night → pin | ||
| return (day: today, value: liveReadiness, wakeSec: wakeSec); | ||
| } | ||
|
|
@@ -5924,6 +5987,7 @@ class DerivationEngine { | |
| liveReadiness: readiness?.round(), | ||
| current: current, | ||
| wakeSec: day.sleepOffsetSec, | ||
| onsetSec: day.sleepOnsetSec, | ||
| ); | ||
| if (next == null) return; | ||
| // Already pinned to this exact value → skip the redundant write. | ||
|
|
@@ -6485,14 +6549,16 @@ class DerivationEngine { | |
| // and tested; this only reads its inputs and emits what it returns. | ||
| today ??= LocalDb.localDayLabelNow(); | ||
| final anchor = exceptionAnchor(cd); | ||
| final pin = await LocalDb.frozenHeadline(); | ||
| // Only a pin taken on the anchor day's own night (LocalDb.headlinePinFor). | ||
| final pinValue = | ||
| anchor == null ? null : await LocalDb.headlinePinFor(anchor.date); | ||
| final notices = planExceptionNotices( | ||
| cd, | ||
| today: today, | ||
| irregularFlag: anchor == null | ||
| ? null | ||
| : await LocalDb.metricValueOn(anchor.date, 'irregular_rhythm_flag'), | ||
| pin: pin == null ? null : (day: pin.day, value: pin.value), | ||
| pin: pinValue == null ? null : (day: anchor!.date, value: pinValue), | ||
| storedReadiness: anchor == null | ||
| ? null | ||
| : await LocalDb.metricValueOn(anchor.date, 'readiness'), | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.