Skip to content

fix(recovery): provisional/final states, no early pins, one headline source (refs #543) - #578

Open
abdulsaheel wants to merge 7 commits into
mainfrom
fix/recovery-states
Open

abdulsaheel wants to merge 7 commits into
mainfrom
fix/recovery-states

Conversation

@abdulsaheel

@abdulsaheel abdulsaheel commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

User description

Refs #543 and the "Home shows 2 while Coach says 27.6" report.

Root cause: Home showed a frozen headline pinned from a night still in progress (04:04 local, night 01:05-07:56), and the first pin of the day stuck; Coach read the stored day result.

  • Three recovery states computed in one place: night in progress (no number, "Sleeping…"), provisional (grey number, "Finishing up"), final (colour). Only a final night is frozen; pins before 03:00 local or after under 3 h of sleep are refused.
  • Read side trusts a pin only when its wake matches tonight's; old pins without a wake time yield to the live value.
  • One headline source (todayHeadlineOf) for Home, Readiness detail, Health caption, briefing, notifications, widgets (provisional drawn muted) and Coach (get_today; v_daily/v_metric serve today's readiness through the same rule).
  • "Updated 8:12 · 2 → 28" note when the shown final value changes; manual re-analyse releases today's pin and lists days whose recovery changed.
  • No kAlgoVersion change; no stored derived output changes. Russian strings for new keys (machine translated, worth a native check).

Tests: recovery_states, recovery_headline_ui, overnight_settled, widget_service_sentinels, coach_views, low_readiness tests.

🤖 Generated with Claude Code

Summary by Sourcery

Unify today’s recovery headline around explicit settlement states while preventing incomplete nights and stale pins from producing incorrect scores.

New Features:

  • Add explicit night-in-progress, provisional, and final recovery states with consistent presentation across the app.
  • Provide Coach with a dedicated get_today source and align today’s SQL readiness views with the Home headline.
  • Show recovery update notes after a final value changes and report recovery changes after manual re-analysis.

Bug Fixes:

  • Prevent incomplete or implausibly short nights from freezing recovery headlines and reject pins that do not match the current night’s wake.
  • Ensure stale or legacy pins yield to the live final recovery value and prevent held-over recovery data from appearing as today’s result.

Enhancements:

  • Centralize today’s recovery headline selection for Home, Readiness, Health, briefing, notifications, widgets, and Coach.
  • Render provisional recovery values as muted and expose the corresponding state messaging consistently across user surfaces.

Documentation:

  • Update Coach guidance to use get_today for current recovery, strain, and sleep.

Tests:

  • Add coverage for recovery state transitions, pin validation, headline consistency, Coach views, overnight settling, provisional widgets, and recovery update behavior.

Chores:

  • Add localized strings for the new recovery states and update messages.

PR Type

Bug fix, Enhancement


Description

  • Unify today's recovery headline handling across Home, Coach, Widgets, and Briefing

  • Introduce explicit recovery states: night_in_progress, provisional, and final

  • Prevent early pins from freezing the headline and trust pins only for matching wake times

  • Show an update note when a final recovery value changes after being displayed

  • Add get_today tool to Coach and shadow v_daily/v_metric to align with the headline


Diagram Walkthrough

flowchart LR
  A["Derivation Engine"] -- "recoveryStateOf" --> B["LocalDb"]
  B -- "todayHeadlineOf" --> C["Home Screen"]
  B -- "todayHeadlineOf" --> D["Coach Engine"]
  B -- "todayHeadlineOf" --> E["Widgets & Briefing"]
Loading

File Walkthrough

Relevant files
Enhancement
12 files
briefing_engine.dart
Update briefing to use todayHeadlineOf for readiness         
+9/-1     
coach_db.dart
Shadow v_daily and v_metric with temp views for today's readiness
+33/-1   
coach_engine.dart
Add get_today tool and pass today's readiness to runCoachSql
+30/-1   
local_repository_impl.dart
Update getToday to include provisional readiness and update notes
+26/-4   
payloads.dart
Add todayHeadlineOf to centralize recovery headline logic
+64/-4   
data.dart
Show changed recovery days after manual re-analysis           
+23/-1   
health_screen.dart
Add recoveryStateLine to show recovery status                       
+8/-0     
home_screen.dart
Handle provisional state and show recovery update notes   
+71/-4   
readiness_detail.dart
Update to use todayHeadlineOf and handle provisional state
+24/-3   
widget_service.dart
Handle provisional readiness state for widgets                     
+11/-1   
app_en.arb
Add English strings for recovery states and updates           
+28/-0   
app_ru.arb
Add Russian strings for recovery states and updates           
+18/-1   
Documentation
1 files
coach_prompt.dart
Update prompt to instruct using get_today() for today's recovery
+3/-0     
Bug fix
3 files
derivation_engine.dart
Add RecoveryState enum, recoveryStateOf, and pinnableNight logic
+68/-6   
db.dart
Add headlinePinFor and noteHeadlineShown to manage recovery pins
+75/-1   
app_state.dart
Release frozen headline during manual re-analysis               
+4/-0     
Tests
4 files
coach_views_test.dart
Add tests for v_daily and v_metric shadowing                         
+37/-0   
recovery_headline_ui_test.dart
Add UI tests for recovery states and update notes               
+154/-0 
recovery_states_test.dart
Add tests for recoveryStateOf and pin guards                         
+178/-0 
widget_service_sentinels_test.dart
Add test for provisional night in widgets                               
+22/-0   
Additional files
3 files
low_readiness_agreement_test.dart +13/-8   
low_readiness_notification_test.dart +15/-2   
overnight_settled_test.dart +43/-2   

…e source

Freeze only final nights (wake before 03:00 or under 3h of sleep never pin),
trust a pin only for the night whose wake it matches, and route Home, Coach
get_today, the morning briefing and the low-readiness notification through
todayHeadlineOf so they cannot disagree. Home notes a changed final number
(Updated 8:12 · 2 → 28); re-analysis lists the days whose recovery moved.
# Conflicts:
#	lib/compute/derivation_engine.dart
#	lib/l10n/app_en.arb
#	test/low_readiness_notification_test.dart
Readiness detail greys a provisional number and shows the update note;
Health names today's recovery state above the overnight rows (one shared
recoveryStateLine). A manual re-analyse releases today's pin so the ring
re-pins to the re-derived value. Widgets draw a provisional night in the
muted calibrating state; Watch/Siri keep -1 until final. run_sql shadows
v_daily/v_metric with temp views whose today readiness is the final
headline or NULL. Russian strings for the new keys; the low-readiness
agreement fixture pins on the night's own wake.
@sourcery-ai

sourcery-ai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Centralizes today’s recovery headline around explicit in-progress, provisional, and final states; prevents premature or stale pins; and routes Home, Coach, Readiness, Health, briefing, notifications, and widgets through the same value and state, with UI change notices and broad regression coverage.

State diagram for today’s recovery headline

stateDiagram-v2
    [*] --> night_in_progress
    night_in_progress --> provisional: wake confirmed
    provisional --> finalReady: overnightSettled
    night_in_progress --> finalReady: overnightSettled
    finalReady --> finalReady: final value served

    state night_in_progress {
        [*] --> no_number
        no_number: No recovery number
        no_number: Sleeping…
    }
    state provisional {
        [*] --> muted_number
        muted_number: Today's number
        muted_number: Finishing up
    }
    state finalReady {
        [*] --> frozen_number
        frozen_number: Colour number
        frozen_number: Eligible for pin
    }
Loading

Flow diagram for the shared recovery headline

flowchart LR
    derived["Derived today data"] --> headline[todayHeadlineOf]
    headline --> home[Home]
    headline --> readiness[Readiness detail]
    headline --> health[Health]
    headline --> briefing[Briefing]
    headline --> widgets[Widgets]
    headline --> coach[get_today]
    headline --> views["v_daily / v_metric today"]
    views --> coachSql[Coach run_sql]
    coachSql --> coach
Loading

File-Level Changes

Change Details Files
Introduces a single recovery-state model and headline selection rule across derivation, persistence, read APIs, and all consumer surfaces.
  • Classifies today as night in progress, provisional, or final based on wake confirmation and overnight settlement.
  • Allows headline pins only for settled, plausible main nights and validates pins against the current night's wake.
  • Exposes a shared headline payload with state, recovery value, provisional value, and post-display changes.
lib/compute/derivation_engine.dart
lib/data/db.dart
lib/data/local_repository_impl.dart
lib/models/payloads.dart
Aligns Home and related presentation surfaces with the shared headline, including explicit provisional and in-progress UI.
  • Shows Sleeping… without a score during an active night and a muted score with Finishing up while data drains.
  • Uses the same recovery value and state in Home, Readiness detail, Health, briefing, and widgets.
  • Displays an Updated time and old-to-new value when a final headline changes.
lib/ui2/screens/home_screen.dart
lib/ui2/screens/readiness_detail.dart
lib/ui2/screens/health_screen.dart
lib/ai/briefing_engine.dart
lib/widget/widget_service.dart
lib/l10n/app_en.arb
lib/l10n/app_ru.arb
Makes Coach consume the same today headline rather than potentially stale derived rows.
  • Adds get_today for the exact Home headline and documents when to use it.
  • Shadows today’s v_daily and v_metric readiness through temporary views for run_sql, returning null until final or the headline value when final.
  • Adds test-only tool execution support and status messaging.
lib/coach/coach_db.dart
lib/coach/coach_engine.dart
lib/coach/coach_prompt.dart
Adds re-analysis and notification/read-side handling for corrected or changed recovery values.
  • Releases today’s pin before manual re-analysis so a fresh final value can be pinned.
  • Tracks the first displayed final headline and reports subsequent changes.
  • Validates pins for exception notices and other reads against the current night’s wake.
lib/state/app_state.dart
lib/ui2/profile/data.dart
lib/data/db.dart
lib/data/local_repository_impl.dart
lib/compute/derivation_engine.dart
Expands automated coverage for state transitions, pin safety, UI consistency, Coach views, widgets, and low-readiness behavior.
  • Covers wake confirmation, settlement, pre-03:00 and short-sleep pin refusal, legacy pins, and wake mismatches.
  • Verifies Home/Coach/Readiness/Health agreement and provisional rendering.
  • Tests widget sentinels, overnight settlement, notifications, and re-analysis behavior.
test/recovery_states_test.dart
test/recovery_headline_ui_test.dart
test/overnight_settled_test.dart
test/coach_views_test.dart
test/widget_service_sentinels_test.dart
test/low_readiness_agreement_test.dart
test/low_readiness_notification_test.dart

Possibly linked issues

  • #Readiness score drifts during the day: PR freezes the finalized headline while later derives update underlying metrics, preventing ready-state daytime readiness drift.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 7 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: OpenStrap/edge/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 399b19df-e460-475e-87d2-b39daa1cbf07

📥 Commits

Reviewing files that changed from the base of the PR and between c7af737 and 49429ce.


⛔ Files ignored due to path filters (8)
  • test/coach_views_test.dart is excluded by !test/**
  • test/low_readiness_agreement_test.dart is excluded by !test/**
  • test/low_readiness_notification_test.dart is excluded by !test/**
  • test/overnight_settled_test.dart is excluded by !test/**
  • test/readiness_freeze_test.dart is excluded by !test/**
  • test/recovery_headline_ui_test.dart is excluded by !test/**
  • test/recovery_states_test.dart is excluded by !test/**
  • test/widget_service_sentinels_test.dart is excluded by !test/**

📒 Files selected for processing (16)
  • lib/ai/briefing_engine.dart
  • lib/coach/coach_db.dart
  • lib/coach/coach_engine.dart
  • lib/coach/coach_prompt.dart
  • lib/compute/derivation_engine.dart
  • lib/data/db.dart
  • lib/data/local_repository_impl.dart
  • lib/l10n/app_en.arb
  • lib/l10n/app_ru.arb
  • lib/models/payloads.dart
  • lib/state/app_state.dart
  • lib/ui2/profile/data.dart
  • lib/ui2/screens/health_screen.dart
  • lib/ui2/screens/home_screen.dart
  • lib/ui2/screens/readiness_detail.dart
  • lib/widget/widget_service.dart


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 7 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="lib/coach/coach_engine.dart" line_range="805" />
<code_context>
+        readiness: h['recovery_state'] == 'final' ? h['recovery'] as num? : null,
+      );
+    } catch (_) {
+      return null;
+    }
+  }
</code_context>
<issue_to_address>
**Coach exposes partial readiness**

When `api.getToday()` throws while Coach handles a `run_sql` request, `_todayReadiness` returns null, and `_serveToday` treats null as no override, leaving the stored views exposed. Coach SQL can return provisional readiness despite the final-only contract.

Represent a failed lookup separately from a no-override result and mask today’s readiness in the temporary views when the lookup fails.

Also at `lib/coach/coach_db.dart:492`, `lib/coach/coach_db.dart:525`.
</issue_to_address>

### Comment 2
<location path="lib/coach/coach_db.dart" line_range="502-505" />
<code_context>
+            : '"${r['name']}"',
+    ];
+    await db.execute(
+        'CREATE TEMP VIEW v_daily AS SELECT ${cols.join(', ')} FROM main.v_daily');
+    await db.execute('CREATE TEMP VIEW v_metric AS SELECT date, key, '
+        "CASE WHEN date = '$day' AND key = 'readiness' THEN $v ELSE value END "
+        'AS value FROM main.v_metric');
+  }
+
</code_context>
<issue_to_address>
**Pinned recovery disappears from Coach**

When a final headline pin exists but today’s readiness row is absent from the underlying views, `_serveToday` only rewrites rows returned by `main.v_daily` and `main.v_metric`; it does not add the missing row. Coach SQL omits the recovery that Home and `get_today` show.

Add today’s pinned readiness row to the temporary views when the underlying row is absent.
</issue_to_address>

### Comment 3
<location path="lib/compute/derivation_engine.dart" line_range="2176-2180" />
<code_context>
 }) {
+  // 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);
+      (wakeSec == null ||
+          (current.wakeSec != null &&
+              (wakeSec - current.wakeSec!).abs() < kPinWakeToleranceSec));
   if (sameNight) return current; // pinned; hold
-  if (overnightComplete && liveReadiness != null) {
</code_context>
<issue_to_address>
**Ineligible nights keep old pins**

When a later derive makes a night ineligible while its wake remains within the pin tolerance, `nextFrozenHeadline` returns a same-wake pin before checking `pinnableNight`, and `headlinePinFor` validates only the wake match. The old pin remains the headline for an ineligible night, so Home and other pin readers keep showing its score.

Check the newly derived night’s eligibility before retaining a pin, and apply the eligibility check when reading a pin.

Also at `lib/compute/derivation_engine.dart:2181`, `lib/data/db.dart:3801-3804`.
</issue_to_address>

### Comment 4
<location path="lib/compute/derivation_engine.dart" line_range="2123-2131" />
<code_context>
+  required int dataEdgeSec,
+  int? nowSec,
+}) {
+  if (overnightSettled(
+      sleepOffsetSec: wakeSec, dataEdgeSec: dataEdgeSec, nowSec: nowSec)) {
+    return RecoveryState.finalReady;
+  }
+  if (wakeSec != null && dataEdgeSec >= wakeSec + kWakeConfirmMarginSec) {
+    return RecoveryState.provisional;
</code_context>
<issue_to_address>
**Awake mornings say sleeping**

When a today row exists without a sleep window and its data edge has not yet met the no-window settle condition, `recoveryStateOf` treats a missing `wakeSec` as `night_in_progress` whenever `overnightSettled` is false, and `refreshComputeFreshness` stores that state for any today row. `todayHeadlineOf` then tells Home and Coach the user is sleeping even when no sleep window has been detected.

Represent an absent sleep window separately from an in-progress night, and only report `night_in_progress` when there is evidence that sleep is underway.

Also at `lib/data/db.dart:10847-10853`.
</issue_to_address>

### Comment 5
<location path="lib/data/db.dart" line_range="3795-3800" />
<code_context>
+    final pin = await frozenHeadline();
+    final pinWake = pin?.wakeSec;
+    if (pin == null || pin.day != day || pinWake == null) return null;
+    int? wake;
+    try {
+      final w = jsonDecode(await sleepWindowJsonFor(day) ?? '{}');
+      final ms = w is Map ? w['offset_ms'] : null;
+      wake = ms is num ? ms ~/ 1000 : null;
+    } catch (_) {/* malformed window → no wake → no pin */}
+    if (wake == null || (wake - pinWake).abs() >= kPinWakeToleranceSec) {
+      return null;
</code_context>
<issue_to_address>
**Valid pins are rejected**

When the stored `window_json` has an outer `value` map containing `offset_ms`, `headlinePinFor` reads `offset_ms` only at the top level of `window_json`. When the stored window uses the `{value: {offset_ms: ...}}` envelope, it treats the wake as missing and rejects a valid pin, so Home falls back to a live readiness value that can drift after later derives.

Unwrap the `value` map when reading the wake, as `storedWindowSpan` does, before checking pin tolerance.
</issue_to_address>

### Comment 6
<location path="lib/state/app_state.dart" line_range="2268-2271" />
<code_context>
     reanalyzeProgress = 'Analyzing…';
     notifyListeners();
     try {
+      // The user asked for a fresh answer: today's morning pin would otherwise
+      // hold the old number in the ring while the re-analysis summary reports
+      // the new one. The re-derive below re-pins once the night is final.
+      await LocalDb.releaseFrozenHeadline(LocalDb.localDayLabelNow());
       final n = await _derive.run(
         _profile,
</code_context>
<issue_to_address>
**Re-analysis skips its fresh pass**

When a sync or background derivation already holds the derivation latch when the user starts re-analysis, `reanalyzeAll` releases today’s pin before `_derive.run`, which returns `0` when another derive holds the latch. The requested pass is skipped after the pin is removed, so the headline can remain unpinned or an active pass can restore its older value while the UI reports completion.

Ensure re-analysis waits for or queues a derive that will run before releasing today’s pin.
</issue_to_address>

### Comment 7
<location path="lib/data/db.dart" line_range="3817-3841" />
<code_context>
+  /// has not changed since it was first shown.
+  static Future<Map<String, int>?> noteHeadlineShown(
+      String day, int value) async {
+    Map? cur;
+    try {
+      final d = jsonDecode(await getCursor(kHeadlineShownCursor) ?? 'null');
+      if (d is Map && d['day'] == day && d['value'] is num) cur = d;
+    } catch (_) {/* malformed → start over */}
+    if (cur != null && (cur['value'] as num).round() == value) {
+      final prev = cur['prev'], at = cur['at'];
+      return prev is num && at is num
+          ? {'from': prev.round(), 'to': value, 'at': at.toInt()}
+          : null;
+    }
+    final now = DateTime.now().millisecondsSinceEpoch;
+    await setCursor(
</code_context>
<issue_to_address>
**Recovery update cursor goes backward**

When overlapping `getToday` calls observe different final values during a re-analysis or late sync, `noteHeadlineShown` reads and later writes the shared cursor without a transaction or lock. An older snapshot can overwrite a newer cursor, so the next recovery update note reports an incorrect `from` value.

Make the cursor read-and-write atomic so stale snapshots cannot overwrite newer values.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 7 findings to address first, and if the new state and pin guards are wrong, the app can persist or serve an incorrect recovery headline and may trigger a notification based on that value; reverting will not undo already-written pins, cursors, or notifications. The stored headline data is bounded and can be recomputed, but externally shown or sent notifications cannot be fully retracted.

Blocking findings: lib/coach/coach_engine.dart:805, lib/coach/coach_db.dart:505, lib/compute/derivation_engine.dart:2180, lib/compute/derivation_engine.dart:2131, lib/data/db.dart:3800, and 2 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread lib/coach/coach_engine.dart Outdated
Comment thread lib/coach/coach_db.dart Outdated
Comment thread lib/compute/derivation_engine.dart
Comment thread lib/compute/derivation_engine.dart
Comment thread lib/data/db.dart Outdated
Comment thread lib/state/app_state.dart Outdated
Comment thread lib/data/db.dart Outdated
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

PR Code Suggestions ✨

Latest suggestions up to ae9df7d

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix invalid Dart syntax in map literal

The syntax ?readinessUpdate is invalid in Dart map literals and will cause a
compilation error. Use if (readinessUpdate != null) to conditionally include the
key-value pair.

lib/data/local_repository_impl.dart [460-462]

         'readiness_provisional': _scalarMetric(provisionalReadiness, 'HIGH'),
-      'readiness_update': ?readinessUpdate,
+      if (readinessUpdate != null) 'readiness_update': readinessUpdate,
       'resting_hr': _scalarMetric(
Suggestion importance[1-10]: 10

__

Why: The syntax ?readinessUpdate is invalid in Dart map literals and will cause a compilation error. The suggestion correctly replaces it with a valid conditional map entry.

High
Fix invalid Dart syntax in map literals

The syntax ?_status is invalid in Dart map literals and will prevent compilation.
You can safely pass _status directly, as map values can be null.

lib/models/payloads.dart [146-158]

     final h = todayHeadlineOf(
-        {'daily': _daily, 'sleep': _sleep, 'status': ?_status});
+        {'daily': _daily, 'sleep': _sleep, 'status': _status});
     return h['recovery_state'] == 'final'
         ? (h['recovery'] as num?)?.round()
         : null;
   }
 
   /// Today's own number while its night is `provisional` (wake confirmed, the
   /// band still draining past it), else null. Surfaces that can grey a value
   /// show it greyed; the rest show nothing until it is final.
   int? get provisionalReadinessScore {
     final h = todayHeadlineOf(
-        {'daily': _daily, 'sleep': _sleep, 'status': ?_status});
+        {'daily': _daily, 'sleep': _sleep, 'status': _status});
Suggestion importance[1-10]: 10

__

Why: The syntax ?_status is invalid in Dart map literals and will prevent compilation. The suggestion correctly advises passing _status directly, which is valid since map values can be null.

High

Previous suggestions

Suggestions up to commit f939cd3
CategorySuggestion                                                                                                                                    Impact
Possible issue
Fix invalid map literal syntax

Dart does not support the ?variable syntax in map literals. Use an if collection
element instead to conditionally include the key, otherwise this will cause a
compilation error.

lib/data/local_repository_impl.dart [457-462]

       'readiness': readinessMetric,
       'recovery': readinessMetric,
       if (provisionalReadiness != null)
         'readiness_provisional': _scalarMetric(provisionalReadiness, 'HIGH'),
-      'readiness_update': ?readinessUpdate,
+      if (readinessUpdate != null) 'readiness_update': readinessUpdate,
       'resting_hr': _scalarMetric(
Suggestion importance[1-10]: 9

__

Why: The ?variable syntax is not valid in Dart map literals and will cause a compilation error. Using an if collection element is the correct approach to conditionally include a key-value pair.

High
Parse day labels as local time

DateTime.tryParse parses date-only strings (like "YYYY-MM-DD") as UTC. When
formatted by MaterialLocalizations (which uses local time), this can shift the date
to the previous day in timezones behind UTC. Append T00:00:00 to force parsing as
local time.

lib/ui2/profile/data.dart [189-193]

     ].reversed.take(5).map((c) {
-      final d = DateTime.tryParse(c.day);
+      final d = DateTime.tryParse('${c.day}T00:00:00');
       final label = d == null ? c.day : ml.formatShortMonthDay(d);
       return '$label ${c.from} → ${c.to}';
     }).join(', ');
Suggestion importance[1-10]: 8

__

Why: Dart parses date-only strings as UTC, which can cause the date to shift to the previous day when formatted in local timezones behind UTC. Appending T00:00:00 correctly forces local time parsing, preventing this subtle bug.

Medium

@abdulsaheel

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

- no sleep window yet claims no state instead of "Sleeping..."
- headlinePinFor unwraps a value-enveloped window_json
- Coach SQL masks today's readiness when getToday fails, and shows a
  pinned headline even when metric_series has no row for the day
- re-analysis waits out a running derive before releasing the pin
- the shown-headline cursor is read and written in one transaction
- freeze test uses local wall-clock wakes (the 03:00 pin gate is local)
@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵⚪⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Syntax Error

The PR introduces hallucinated Dart syntax ?readinessUpdate inside a map literal. Dart does not support ?variable for conditionally adding map entries or passing null-aware values. This will cause a compilation error. Use if (readinessUpdate != null) 'readiness_update': readinessUpdate instead. This same syntax error is also present in lib/models/payloads.dart (?_status) and test/overnight_settled_test.dart (?rmssd, ?readiness).

if (provisionalReadiness != null)
  'readiness_provisional': _scalarMetric(provisionalReadiness, 'HIGH'),
'readiness_update': ?readinessUpdate,
'resting_hr': _scalarMetric(
Concurrency Race Condition

_serveToday drops and creates temp.v_daily and temp.v_metric views on the shared database connection outside of a transaction. If the LLM makes concurrent run_sql tool calls (e.g., via parallel tool calling), they will race to drop and recreate these views, potentially causing queries to fail or read the wrong today value. runCoachSql should wrap the view creation and rawQuery execution in a db.transaction.

static Future<void> _serveToday(
    Database db, ({String day, num? readiness})? today) async {
  await db.execute('DROP VIEW IF EXISTS temp.v_daily');
  await db.execute('DROP VIEW IF EXISTS temp.v_metric');
  if (today == null) return;
  final day = today.day.replaceAll("'", "''");
  final v = today.readiness?.toString() ?? 'NULL';
  final names = [
    for (final r in await db.rawQuery('PRAGMA main.table_info(v_daily)'))
      '${r['name']}',
  ];
  final cols = [
    for (final n in names)
      n == 'readiness'
          ? "CASE WHEN date = '$day' THEN $v ELSE readiness END AS readiness"
          : '"$n"',
  ];
  // A pinned headline whose day has no stored readiness (the live composite
  // abstained on a later derive) still has to read the same as Home.
  final added = [
    for (final n in names)
      n == 'date' ? "'$day'" : (n == 'readiness' ? v : 'NULL'),
  ];
  await db.execute(
      'CREATE TEMP VIEW v_daily AS SELECT ${cols.join(', ')} FROM main.v_daily '
      "UNION ALL SELECT ${added.join(', ')} WHERE $v IS NOT NULL AND NOT "
      "EXISTS (SELECT 1 FROM main.v_daily WHERE date = '$day')");
  await db.execute('CREATE TEMP VIEW v_metric AS SELECT date, key, '
      "CASE WHEN date = '$day' AND key = 'readiness' THEN $v ELSE value END "
      'AS value FROM main.v_metric '
      "UNION ALL SELECT '$day', 'readiness', $v WHERE $v IS NOT NULL AND NOT "
      "EXISTS (SELECT 1 FROM main.v_metric WHERE date = '$day' "
      "AND key = 'readiness')");
}

A re-derive that shrinks the same night below a main night (same wake,
later onset) no longer keeps its old pin, and headlinePinFor checks the
stored window's eligibility before returning one.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant