fix(readiness): freeze the morning headline once the overnight is complete#132
Merged
abdulsaheel merged 1 commit intoJul 22, 2026
Merged
Conversation
…plete Readiness is meant to be a stable morning score, but a day stays recomputable for ~48h and every re-derive overwrites the persisted readiness scalar. As the night's flash finishes draining and the trailing 28-day baseline shifts, the surfaced value legitimately drifts through the day (OpenStrap#128: "morning it was 49, now 45"). PR OpenStrap#121's `settledReadinessScore` only WITHHELD readiness while the overnight was still building; once `overnight_state == 'ready'` it passed the latest re-derived value straight through, so it could not stop a ready→ready drift. Freeze the headline instead: once today's overnight is genuinely COMPLETE — the drained data edge has moved a margin PAST the sleep offset (wake), distinct from `ready` which flips on the first sleep-bearing row mid-drain — pin the first such readiness and surface it for the Today hero and the once-a-morning recovery story. Later re-derives keep updating the full day_result, baselines, trends and finalisation; only the headline number is pinned. Day-tagged so it survives restarts and resets on a new day. The freeze decision (`nextFrozenHeadline`) is a pure function covered by a ready→ready + new-day unit test. Closes OpenStrap#128
📝 WalkthroughWalkthroughThe change freezes Today’s morning readiness headline after genuine overnight completion, stores the pinned day and value in LocalDb, applies matching pins in ChangesReadiness headline freezing
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant DerivationEngine
participant LocalDb
participant LocalRepository
DerivationEngine->>LocalDb: Read frozen headline after derived-day persistence
DerivationEngine->>DerivationEngine: Pin completed current-day readiness
DerivationEngine->>LocalDb: Write changed day/value pin
LocalRepository->>LocalDb: Read frozen headline for Today
LocalDb-->>LocalRepository: Return matching pinned value
Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem (#128, user RR)
Readiness DRIFTS during the day — "morning it was 49, now 45." It's meant to be a stable morning score.
A day stays recomputable for ~48h (
_finalizationSec) andselectLightDeriveDaysre-targets today on every BLE drain; each derive OVERWRITES the persistedreadinessscalar. As the night's overnight flash finishes draining and the trailing 28-day baseline shifts, the value legitimately moves.PR #121's
TodayData.settledReadinessScoreonly withheld the number while the overnight was still building. Onceovernight_state == 'ready'it passed the latest re-derived value straight through, so it could not stop a ready→ready drift.Fix (Option A — freeze the headline)
Once today's overnight is genuinely COMPLETE, pin the first settled readiness and surface that pinned value for the two headline surfaces — the orbit hero (
today_screen) and the once-a-morning recovery story. Later re-derives keep updating the fullday_result, baselines, trends and finalisation exactly as before; only the headline number is pinned."Complete" ≠
overnight_state == 'ready'readyflips as soon as the first sleep-bearing row lands (mid-drain), so pinning on it could freeze a partial-night value. Instead the freeze trigger requires the drained data edge to have moved a margin past the sleep offset (wake):This is the same "edge past the window" model the engine already uses for finalisation (
day.endSec + 48h < dataNowSec), anchored at wake+margin instead of wake+48h: the whole sleep window is then decoded and the segmentation-placed wake is settled, so the overnight inputs are final. Below that margin we might still be looking at a partial night, so we do not pin.Persistence + reset
The pin is a day-tagged cursor (
frozen_headline={day, value}) via the existingsync_cursorstore — survives restarts; a new day's first complete overnight overwrites it, andgetTodayonly surfaces a pin whosedaymatches today.Same-day improvement → does NOT move the headline
Default and shipped behaviour: once pinned, a later same-day re-derive that would RAISE the score is ignored, same as one that lowers it. Stability is the whole point. Flagged as a product call for RR.
Files
lib/compute/derivation_engine.dart—_headlineFreezeMarginSec, the purenextFrozenHeadlinedecision (@visibleForTesting), and_maybeFreezeHeadlineReadinesscalled at the end of_derivePreparedDay(writes the pin on the first complete-overnight settle for today; rescan/baseline re-derives hit the "already pinned" guard and hold).lib/data/db.dart—frozenHeadline()/setFrozenHeadline()cursor accessors.lib/data/local_repository_impl.dart—getTodaysurfaces the pin as thereadiness/recoveryheadline metric when it belongs to today's own overnight (overnight_state == 'ready', matching day). Both the hero and the story read it viasettledReadinessScore.test/readiness_freeze_test.dart— ready→ready with a changing value → headline holds; a new day resets; a partial night settles to the complete-night value.Unverified without a device
No Flutter/Dart SDK or hardware in this environment — verified by inspection + the pure-function unit test only. On-device confirmation needed that: the freeze fires in the morning (the data edge reaches wake+60min on a normal post-wake sync), the pinned hero/story stay put across the day while trends/baselines keep moving, and the pin resets cleanly the next morning. The 60-minute margin is the tunable conservatism knob if the freeze proves too early/late in the field.
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Closes #128