Skip to content
fitme·story
Case studies
v7.9 · 4 min read · 2026-06-01

Readiness-Aware Training Alert — A Second Observer for Today's Training-Day Decision Aid (C2)

C2 shipped 2026-06-01 in a single session as a 4-phase Enhancement on feature/readiness-aware-training-alert (merged via FT2 PR #560, squash commit dcf30c9). Adds a second notification observer alongside the existing ReadinessAlertObserver (score-crossing). The new ReadinessAwareTrainingObserver fires daily at the user's learned training start time and surfaces one of three smart recommendations on Home AIInsightCard + push + AIIntelligenceSheet Why? affordance — continueAsPlanned, adaptEasierLoad, or restDaySwap. Avatar mode mapping inline shimmer for swap, pulse for adapt, breathe for continue. Pure-function trigger evaluator means 12 of 25 new tests are trivial. C4 (the multi-day trend-alert sibling) ships next, fills the rest-day-pattern gap C2 deliberately excludes.

Learned start time
median over 30d
Evaluate readiness
pure-function trigger
Train now
≥65 + confidence, no flag
Lighten
50–65 or adverse flag
Swap to rest
≤35 + adverse flag
The pure-function trigger fires at the learned training-start time and maps readiness band + flags to one of three CTAs on the Home AIInsightCard.
What do T1 / T2 / T3 mean?
T1Instrumented — from a ledger/commit
T2Declared — stated, not measured
T3Narrative — estimate from memory

Readiness-Aware Training Alert — C2

The gap C2 fills

Before C2 shipped, FitMe had one notification observer:

  • ReadinessAlertObserver (push-notifications-v2, shipped 2026-05-07) — fires when the readiness score crosses the ≥80 high or ≤40 low threshold. A score-change event.

That's an observation, not a decision aid. A user on a stable mid-band score (50-70) gets no nudge — even though that's the band where intraday swings matter most for "train hard / lighten / swap" judgment on a scheduled training day.

C2 adds the second observer: ReadinessAwareTrainingObserver, distinct in three dimensions:

DimensionScore-crossing (existing)C2 (new)
TriggerEvent-driven (≥80 / ≤40)Scheduled (at learned training time)
WindowSingle-day spikeSingle-day decision aid
RecommendationDirection only (high/low)One of three CTAs
Cap-tag routing.critical on low+workout.standard (always advisory)
Re-fire windowPer-direction per-dayPer local day

The three recommendations:

  • Train now (continueAsPlanned) — readiness ≥ 65 + confidence ≥ medium + no adverse flag
  • Lighten (adaptEasierLoad) — readiness 50-65 OR any adverse BodyCompFlag (hydrationWarning / visceralTrend)
  • Swap to rest (restDaySwap) — readiness ≤ 35 AND adverse flag present (the rare combo)

Three design decisions

1. Keep separate from ReadinessAlertObserver. Different trigger semantics → second consumer of NotificationGateway with its own typeIdentifier (readinessAwareTrainingAlert), distinct cap tag, distinct de-dupe key. Composing them into one observer would bloat evaluate() with conflicting trigger logic.

2. Pure-function trigger. ReadinessAwareTrainingTrigger.evaluate(...) takes (ReadinessResult, DayType, suggestedSwapTarget, scheduledTime, generatedAt) and returns ReadinessAlertContext?. No Date() calls, no UserDefaults reads, no side effects. 12 of the 25 new tests are trivial because every input is reproducible.

3. Reuse existing UI primitives. AIIntelligenceSheet.readinessSection already renders HRV/Sleep/Training/RHR bars. The "Why?" affordance is "scroll a bit further in the existing sheet." The new UI is one banner with three CTAs at the top of the sheet — about 50 lines.

What ships in PR #560

LayerNewModified
ModelsReadinessAlertRecommendation.swift (enum + Context)
Services/RemindersReadinessAwareTrainingTrigger (pure-function), TrainingStartTimeLearner (median-from-30d), ReadinessAwareTrainingObserver (gateway wiring), ReadinessAwareAlertStore (UI observable)
Views/AIAIInsightCard (avatar mode + title/subtitle override), AIIntelligenceSheet (3-CTA banner + Why? wiring)
SettingsReminderPreferencesStore.readinessAwareAlertsEnabled + NotificationsSettingsScreen toggle
AnalyticsAnalyticsProvider (4 events + 2 params) + AnalyticsService (4 logHomeReadinessAlert* methods)
Tests3 files, 25 tests

What's deliberately not in C2

  • Multi-day trend pattern — sustained HRV drop over 3+ days. That's C4 (trend-alerts-hrv, slot 41), which fills the gap on rest days when C2 doesn't fire.
  • Behavioral learning of optimal dispatch time per user — C2 uses the median of last-30-day DailyLog.sessionStartTime (fallback 18:00 local). Bayesian per-user posterior is the smart-reminders behavioral-learning PR-2 follow-on (gated on App Store launch — needs real-user data).
  • AIOrchestrator integration — C2's recommendation is computed pure-function-style from ReadinessResult, not from the AI engine. AI engine routing for adaptive recommendations is in the D1 (adaptive-intelligence) docket.

Phase E discipline

C2 shipped during the v7.9 Phase E 14-day soak (2026-05-21 → 2026-06-04). The release adds no enforcement gates — it's a new feature consuming existing v7.8.6 + v7.9 infrastructure exclusively (NotificationGateway, NotificationConsumerRegistry, screen-prefixed analytics convention, Tier 2.2 logging, Mechanism C cache-hit attribution). Phase E compliant.

Cross-references

  • Source case study — docs/case-studies/readiness-aware-training-alert-case-study.md
  • PRD — docs/product/prd/smart-reminders.md (parent)
  • Sibling C4 (slot 41) — sustained-trend HRV observer
  • Phase E context — slot 34 (framework-v7-9-promotion.mdx)
  • v7.8.6 cadence batch — slot 32 (fitme-story-ds-p2-final-sweep.mdx)
Kill criterion · not fired
  • Action-taken rate < 5 percent after 14 days of organic exposure (advisory ignored)
  • User-reported false-positive rate > 20 percent (algorithm too noisy)
  • Daily push-fatigue rate (read-but-no-action) > 60 percent (advisory treated as spam)