You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Close final gaps: computeWins, respiratory rate, cycle reports, profile cleanup, MetricScoreCard states
G1: Complete computeWins with baselines_ready, back_in_range win types,
30-day per-metric cooldown for personal bests, and AsyncStorage
persistence for previous statuses and win dates.
G2: Starter pack engine now extracts respiratory_rate from WHOOP and Oura
sleep session vendor_metadata, includes it in daily_history and
metric averages.
G3: Cycle confirmations now insert rows into user_cycle_reports and
compute rolling average cycle_length_days from last 3 reported cycles.
G4: Profile "Turn off cycle tracking" now triggers compute-baselines
recomputation, deletes user_cycle_reports, and invalidates cache.
G5: MetricScoreCard shows "Building your cycle-aware ranges..." interim
text and "Back in your range" transition label.
G6: CycleInsightCard date picker replaced with real TextInput field.
G7: CycleConfirmationPrompt once-per-cycle guard via AsyncStorage.
G9: 4 new integration test cases for interim states, red color absence,
and respiratory rate rendering.
Tests: 2818 mobile (0 fail) + 92 Edge Function (0 fail)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
**Status of computeWins today:** Implements `personal_best` and `all_in_range` only. Missing `back_in_range` (needs prior state), `streak` (needs weekly history), `baselines_ready` (needs first-time flag), and per-metric 30-day cooldown.
1116
+
1117
+
**Changes to `mobile/src/utils/experiments/computeWins.ts`:**
1118
+
1119
+
1. **`baselines_ready`** — Accept an `isFirstBaseline: boolean` parameter. When true and at least one metric has `personal_status != null`, emit a one-time win. The caller (`my-body.tsx`) tracks this via AsyncStorage key `baselines_ready_win_shown`.
1120
+
1121
+
2. **Cooldown** — Accept `recentWinDates: Record<string, string>` (metric_key → ISO date of last win). Skip personal_best for a metric if its last win was within 30 days. The caller persists this in AsyncStorage.
1122
+
1123
+
3. **`back_in_range`** — Accept optional `previousStatuses: Record<string, string | null>` (metric_key → previous personal_status). When a metric was `'outside_bad'` and is now `'within'` or `'outside_good'`, emit a win. The caller persists the previous scorecard's statuses in AsyncStorage after each render.
1124
+
1125
+
4. **`streak`** — Defer. Requires weekly aggregation of trend data across multiple sessions, which we don't have. Leave the TODO. This is the lowest-value win type.
**Change:** Extract respiratory_rate from `sleep_sessions.vendor_metadata` the same way the compute-baselines function does. WHOOP stores it at `vendor_metadata.respiratory_rate`, Oura at `vendor_metadata.breath.average`. Average across sessions per day, include in `metricAverages` and `dailyHistory`.
1163
+
1164
+
**Tests (add to `starter-pack.test.ts`):**
1165
+
-`daily_history.respiratory_rate` has entries when sleep sessions contain respiratory rate data
1166
+
- Metric averages include `respiratory_rate` when data exists
1167
+
- WHOOP respiratory_rate extracted from `vendor_metadata.respiratory_rate`
1168
+
- Oura respiratory_rate extracted from `vendor_metadata.breath.average`
1169
+
1170
+
**Test count:**~4
1171
+
1172
+
---
1173
+
1174
+
### G3: Insert user_cycle_reports on confirm + refine cycle_length_days
1175
+
1176
+
**File:**`mobile/src/app/(tabs)/my-body.tsx`
1177
+
1178
+
**Changes to `onCycleConfirm` callback:**
1179
+
1. After `updateProfile`, insert a row into `user_cycle_reports` via `supabase.from('user_cycle_reports').insert({ user_id, period_start_date, source: 'user_input' })`
1180
+
2. Query the last 3 `user_cycle_reports` rows for this user, compute the rolling average of intervals between consecutive `period_start_date` values, and update `cycle_length_days` on user_profiles.
1181
+
1182
+
**Changes to `onCycleSubmit` callback (CycleInsightCard):**
1183
+
1. If a `last_period_date` is provided, also insert into `user_cycle_reports`.
1184
+
1185
+
**Tests (add to `cycleConfirmDismiss.test.ts` or new file):**
1186
+
- After onCycleConfirm, a row is inserted into user_cycle_reports
1187
+
- After 3 confirmations, cycle_length_days is updated to the rolling average
1188
+
- After onCycleSubmit with a date, a row is inserted into user_cycle_reports
1189
+
1190
+
**Test count:**~3
1191
+
1192
+
---
1193
+
1194
+
### G4: Profile "Turn off" triggers recomputation and deletes cycle_reports
1195
+
1196
+
**File:**`mobile/src/app/(tabs)/profile.tsx`
1197
+
1198
+
**Changes to the "Turn off cycle tracking" handler:**
1199
+
1. After clearing cycle fields on user_profiles, call `supabase.functions.invoke('compute-baselines', { body: { user_id } })` to rebuild all-phase baselines.
1200
+
2. Delete all `user_cycle_reports` rows for this user: `supabase.from('user_cycle_reports').delete().eq('user_id', userId)`.
1201
+
3. Invalidate personalBaselines query cache.
1202
+
1203
+
**Tests (add to `ProfileAboutYou.test.tsx`):**
1204
+
- Turning off cycle tracking calls compute-baselines
1205
+
- Turning off cycle tracking deletes user_cycle_reports rows
1206
+
- (These are callback verification tests — mock supabase.functions.invoke and supabase.from().delete())
1207
+
1208
+
**Test count:**~2
1209
+
1210
+
---
1211
+
1212
+
### G5: MetricScoreCard interim text and transition label
1.**"Building cycle-aware ranges..."** — When the score has `cycle_phase` set but `personal_status` is null (cycle tracking opted in but phase-specific baselines not ready yet), show: "Building your cycle-aware ranges... we need one full cycle of data."
1219
+
1220
+
2.**"Back in your range"** — When the score has a new field `returned_to_range: boolean` (set by computeWins or buildMetricScorecard when detecting the transition), show "Back in your range" label.
1221
+
1222
+
For (2), add `returned_to_range?: boolean` to MetricScore in types.ts and set it in computeWins or in my-body.tsx by comparing current vs previous statuses.
1223
+
1224
+
**Tests (integration test additions to `health-snapshot-integration.test.tsx`):**
1225
+
- MetricScoreCard shows "Building your cycle-aware ranges" when cycle_phase set but personal_status null
1226
+
- MetricScoreCard shows "Back in your range" when returned_to_range is true
**Change:** Replace the stub date picker (auto-sets to 14 days ago) with a TextInput date field matching the pattern used in CycleConfirmationPrompt. User types YYYY-MM-DD or taps to select. Keep "I'm not sure" option.
1237
+
1238
+
**Tests (integration test covers this):** Existing integration test verifies submit callback fires with the right values. No new tests needed — just implementation.
1239
+
1240
+
**Test count:** 0
1241
+
1242
+
---
1243
+
1244
+
### G7: CycleConfirmationPrompt "once per cycle" guard
**Change:** Add `lastShownCycleDate: string | null` prop. If the current `estimatedDueDate` matches `lastShownCycleDate` (same cycle), return null. The caller persists this in AsyncStorage after the prompt is shown.
1249
+
1250
+
**File:**`mobile/src/app/(tabs)/my-body.tsx` — read/write `cycle_confirm_last_shown_due_date` from AsyncStorage.
1251
+
1252
+
**Tests:**
1253
+
- Prompt does not render when `lastShownCycleDate` matches current `estimatedDueDate`
1254
+
- Prompt renders when `lastShownCycleDate` is from a prior cycle
**Change:** The improving metric sentence currently says "Your HRV has been improving." To add duration ("for 3 weeks"), `MetricScore` would need a `trend_duration_weeks` field. This requires the trend computation to output duration, which requires weekly aggregated data we don't have.
1265
+
1266
+
**Decision:** Defer. The current sentence is accurate without the duration. Adding "for 3 weeks" requires changes to trendComputation, buildMetricScorecard, and the starter pack's daily history format. Low value relative to complexity.
1267
+
1268
+
**Test count:** 0
1269
+
1270
+
---
1271
+
1272
+
### G9: Test gaps — MetricScoreCard and HealthSnapshot Phase 6/8 tests
1273
+
1274
+
The plan specified 16 MetricScoreCard tests and 10 HealthSnapshot tests in Phases 6 and 8. These live in `.test.tsx` files in `__tests__/components/` which have a pre-existing Babel JSX parse issue. The integration test file (`health-snapshot-integration.test.tsx`) covers most of these paths end-to-end but runs via a separate command.
1275
+
1276
+
**Fix:** Add the missing test assertions to the integration test file (which uses proper JSX compilation) rather than the broken `.test.tsx` files:
1277
+
1278
+
- MetricScoreCard: "Building your personal baseline... X of 30 days" renders when daysOfData < 30
1279
+
- MetricScoreCard: "Building cycle-aware ranges..." renders during interim
1280
+
- MetricScoreCard: population safety callout not shown when flag is false (already covered)
1281
+
- MetricScoreCard: never renders red color (verify no element with red color style)
1282
+
- HealthSnapshot: shows CycleInsightCard with privacy text for qualifying user (already covered)
1283
+
- HealthSnapshot: does not show CycleInsightCard for male user (already covered)
1284
+
- HealthSnapshot: respiratory_rate card appears for WHOOP/Oura data (needs respiratory_rate in test data after G2)
0 commit comments