Draft for approval — RecoveryDeck v1.3. Docs-only until you request build.
Product Requirements Document — RecoveryDeck
Product name: RecoveryDeck Owner: Zachery Ringstrom Status: Draft for approval (no implementation until approved) Version: 1.3 Date: 2026-08-02 Changelog 1.2: Rename RecoveryDeck; local analytics; 1–7 scales; Alan + WHOOP-style questionnaire; optional display name; TP/Intervals future; evidence notes. Changelog 1.3: Keep short orthostatic; G/Y/R reframed to Couzens 7d vs 60d ±1 SD; optional caffeine time/amount + last meal time; HealthKit not v3 confirmed. Companion: TECH_SPEC.md · REVIEW_NOTES_RESPONSE.md
1. Summary
RecoveryDeck is a privacy-first, local-first iOS app for a single primary user (Zach) that runs a fixed morning ritual:
- Forced questionnaire (cannot skip into measurements)
- Polar H10 measurements that replace Elite HRV (rMSSD) and add an orthostatic HR test
- Simple history + traffic-light context vs personal baselines
The app supports Couzens-style recovery-on-demand decisions: composite signals (subjective + HRV + RHR + orthostatic), not a black-box commercial “readiness score.”
Scope for first ship: everything through v3 (questionnaire gate + H10 rMSSD + H10 orthostatic + local history). Optional App Store distribution for friends is a future phase and must not drive v1–v3 feature bloat.
2. Problem
| Pain | Today |
|---|
| Subjective readiness never logged | No daily fatigue/mood/soreness/stress answers |
| Recovery signals fragmented | Oura (sleep/RHR/HRV), Elite HRV (1 min rMSSD), training feel — separate apps |
| Elite HRV is an extra hop | User wants one morning app that owns rMSSD |
| Orthostatic not automated | Couzens-valued test; no easy 2–3 min guided flow with H10 |
| Commercial readiness scores untrusted | Oura Readiness etc. not anchored to “ready for what” (Couzens critique) |
Cost of status quo: decisions about down weeks / easy days under-weight subjective state; orthostatic unused; habit friction.
3. Goals and non-goals
3.1 Goals (v3 = “done for personal daily use”)
- Questionnaire-first gate every local calendar morning before any measurement or stats that reveal trends.
- Replace Elite HRV with in-app ~60 s rMSSD via Polar H10.
- Guided orthostatic test (~2–3 min) via H10; store lying avg HR, standing avg HR, peak standing HR, and gaps.
- RHR defined as lying average HR from the orthostatic (or dedicated lying segment) — primary RHR for the day.
- Local-first storage; no account required; no third-party cloud analytics SDK; no required cloud.
- On-device analytics (adherence, quality fails, completion time, etc.) stored only on the phone.
- History of questionnaire + metrics with simple vs personal baseline indicators (green / yellow / red) — heuristic, not clinical cutoffs (see §6.6).
- Installable on owner’s iPhone (Xcode / TestFlight to self).
- Architecture that does not block later App Store, optional display name, or optional TrainingPeaks / Intervals export.
- Optional display name for greetings (not an account).
3.2 Non-goals (v3)
- Oura API integration (optional later; notes OK)
- TrainingPeaks / Intervals.icu sync in v3 — deferred to v4+ optional, schema should stay export-friendly
- HealthKit read/write in v3 (optional later; see REVIEW_NOTES)
- Coaching dashboard, multi-athlete, teams
- Diagnosing medical conditions or “overtraining syndrome” as a clinical claim
- AI / ML readiness models
- Android
- Background continuous HR monitoring
- Social features, login accounts, ads
- Third-party analytics SDKs that phone home
- Perfect reproduction of Elite HRV’s proprietary pipeline
3.3 Success metrics (personal)
| Metric | Target (first 30 days after install) |
|---|
| Morning completion rate | ≥ 80% of days with full questionnaire + rMSSD |
| Orthostatic adherence | ≥ 60% of days (skippable after confirm per locked O1; still count as completed morning if rMSSD saved) |
| Elite HRV usage | Zero intentional uses after parallel validation week |
| Subjective capture | 100% of measurement days have all required questionnaire fields |
| Trust | User prefers this app’s morning flow over prior stack |
4. Users
| Persona | Role |
|---|
| Primary: Zach | Endurance athlete (~18–22 h/wk, AeT-capped base). Owns Oura + Polar H10. Trains with Couzens-informed principles. |
| Future: friends | Possible App Store users; same solo morning ritual; no coach portal in v3. |
Assumptions: iPhone recent enough for current iOS LTS−1; Polar H10; English UI; America/Los_Angeles local calendar days.
5. Product principles
- Gate first, measure second — No peeking at trends to bias answers.
- Composite, not oracle — Show numbers + simple baselines; user decides training.
- Privacy first — Data never leaves the device unless user explicitly exports.
- Repeatable protocol — Same posture, timing, and order every day.
- Couzens-aligned language — Ready to load vs ready to recover; low HRV ≠ ban all easy work.
- Boring reliability — Prefer a rock-solid H10 connection over flashy charts.
- Personal now, public-ready later — No hard-coded secrets; no medical overclaim.
6. User experience
6.1 Happy path (full morning, ~4–6 minutes)
v3 primary UX = one combined H10 session after the questionnaire (not two separate “Start HRV” / “Start Orthostatic” products). Separate re-run of orthostatic-only is allowed only as a recovery path after a failed standing segment.
Launch app
→ If questionnaire incomplete for today:
Questionnaire screen (blocking)
→ Home / Today (post-questionnaire):
[Start morning measurement] → combined: settle → rMSSD → lying HR → stand → done
optional: [Skip orthostatic today] only as confirm during/after rMSSD path (see O1)
→ Results for today + optional “What this might mean” plain-language tips
→ History available only after questionnaire submitted for today
6.2 Gating rules (normative)
| Destination | Allowed if questionnaire not done today? |
|---|
| Questionnaire | Yes |
| Start morning measurement (combined session) | No |
| Today’s metric numbers (after measure) | Only after questionnaire; metrics appear as completed |
| History / charts / baselines | No until questionnaire submitted for today |
| Settings | Yes (always) |
| Export | Yes (always); History blocked until questionnaire done |
Day boundary: localDate in device timezone (default America/Los_Angeles, user-overridable in Settings).
Resubmit questionnaire: Allowed; overwrites today’s answers; does not delete measurements already taken (flag questionnaireEditedAfterMeasure if edit after first measurement).
Remeasure (same day): At most one active measurement bundle per localDate. User may re-run the combined session (or HRV-only if orthostatic skipped) only after confirm overwrite. Overwrite replaces the previous bundle for that day (no multi-version history in v3).
6.3 Questionnaire (v3 fields)
Block A — required (Alan Couzens core), scale 1–7
Higher = better (consistent polarity). Midpoint = 4.
| Field | 1 | 7 |
|---|
| Fatigue | Exhausted | Fresh |
| Mood | Very low | Great |
| Soreness / heavy legs | Very sore | None |
| Life stress | Very high | Very low |
Rationale: Couzens morning inventory / dashboard features use Fatigue, Mood, Soreness, Stress (+ measured HRV & Pulse).
Block B — required (short), scale 1–7
| Field | 1 | 7 |
|---|
| Sleep quality (how it felt) | Terrible | Excellent |
Rationale: Captures “slept long but feel wrecked”; complements Oura without API.
Block C — optional context (skippable fields; not required for gate)
Shown on the same questionnaire screen; each can be left blank / “Skip.”
| Field | Type | Notes |
|---|
| Last caffeine time (previous day → morning) | Time of day (or “none”) | When caffeine was last consumed before this check-in |
| Caffeine amount | Optional amount | Free number + unit picker default mg, or coarse chips (e.g. none / small / medium / large) if exact mg unknown |
| Last meal time | Time of day | When the last meal/substantial snack was finished |
Rationale: Timing/dose of caffeine and last meal affect sleep, overnight HRV proxies, morning RHR, and how “fasted” the measurement is—useful context without forcing perfection.
Block D — optional habit chips (WHOOP-inspired), Yes / No
Defaults off until user enables in Settings:
- Alcohol last night
- Hard training yesterday
- Travel / late night
- Feeling sick / under the weather
(Late caffeine is largely covered by Block C time field.)
Block E — optional free-text Notes
Submit enabled when Block A + B complete (5 scores on 1–7). Blocks C–E optional.
6.4 Measurement: rMSSD (replaces Elite HRV)
| Item | Spec |
|---|
| Hardware | Polar H10 via Bluetooth LE |
| Posture | Lying supine, still, normal breathing (aligned with orthostatic lying; one posture for whole session) |
| Duration | 60 seconds of accepted RR intervals after a short settle |
| Settle | 15 s countdown after “connected & still” before RR window starts |
| Output | rMSSD (ms), mean HR (bpm), artifact %, sample count |
| Failures | Disconnect, too many artifacts → prompt retry; do not save junk without confirmation |
Caffeine / timing guidance (in-app copy, not enforced): before coffee when possible; same time of day; bathroom OK; after waking ~5–15 min.
6.5 Measurement: orthostatic (Couzens-style)
Science reference (Couzens): classic standalone orthostatic is often described as ~60 s lying + ~60 s standing, tracking avg lying, avg standing, peak standing, and especially peak − lying.
v3 normative product protocol (combined morning session — locked): One lie-down; rMSSD and orthostatic share the session. Orthostatic lying sample is 30 s after rMSSD (not a second full 60 s lie), then 60 s standing. This is an intentional time tradeoff; standing duration and peak−lying emphasis stay Couzens-faithful.
| Phase | Duration | User action | Record |
|---|
| Prep | — | Connect H10; lie down | — |
| Settle | 15 s | Stay lying, still | discard for rMSSD |
| rMSSD | ~60 s RR accumulation | Stay lying | rMSSD, mean HR |
| Orthostatic lying | 30 s | Stay lying | Avg lying HR (primary RHR for the day) |
| Cue | 3 s | “Stand up now” (haptic) | — |
| Standing | 60 s | Stand still | Avg standing HR, Peak standing HR |
Derived (store all):
gap_avg = avg_standing − avg_lying
gap_peak = peak_standing − avg_lying
Primary gap in UI: gap_peak (Couzens). Show gap_avg secondary.
Total guided time: ~15 + 60 + 30 + 3 + 60 ≈ ~3 min + connection → product copy “about 3 minutes.”
Standalone classic 60+60 is not a v3 UI mode (may be added later). Skip-orthostatic path: settle + rMSSD only (see O1).
6.6 Results interpretation (non-prescriptive)
What Couzens actually does (dashboard / Ch.13 materials)—not a branded G/Y/R product table:
- Long-term norm ≈ 60-day rolling mean (starting point; window can be experimented with).
- Acute signal ≈ 7-day rolling mean (less noisy than raw daily).
- Normal band ≈ ±1 standard deviation around the long-term mean (he plots this as a pale green “normal range”).
- Flag when the athlete is outside their own range.
- Combine HRV, pulse, fatigue, mood, soreness, stress; be skeptical of commercial single “readiness scores.”
- “Red light” language for serious overtraining-type states (e.g. orthostatic Q4)—not every slightly off morning.
RecoveryDeck mapping: full rules in TECH_SPEC.md §5.4 (Couzens-aligned):
| Light | Meaning |
|---|
| Green | Today (acute) inside personal 60-day mean ± 1 SD band, or still building history |
| Yellow | Mildly outside that band (~1–1.5 SD) |
| Red | Clearly outside (~>1.5 SD), especially in a concerning direction |
UI must label direction (“RHR higher than your usual range”, “orthostatic gap smaller than usual”), not only color. Not a medical diagnosis.
Plain-language tips (examples, not medical advice):
- Low rMSSD + high RHR + small gap → bias easy / recovery; avoid intensity.
- Low rMSSD but good subjective + large gap → still prefer easy if unsure; easy AeT work may be OK.
- Heavy legs (soreness 1–2 on 1–7 scale) → respect muscular recovery even if HRV “looks fine.”
Never: “You have overtraining syndrome.” Never: auto-change training calendar.
6.7 History
- List by day: scores + rMSSD + RHR + gaps
- Simple charts: 14–28 day rMSSD, RHR, gap_avg, subjective average
- Filter: incomplete days visible as partial
6.8 Notifications
- One daily local notification (default 06:30 local, user-editable)
- Title: “Morning check-in”
- Body: “Questionnaire first — then H10.”
- Tapping opens app to questionnaire if incomplete
6.9 Settings
- Display name (optional; greetings only)
- Notification time on/off
- Timezone / day-boundary note
- Baseline window (7 vs 14 days)
- Habit chips enable/disable (Block D)
- H10 pairing / BLE troubleshooting help
- Export JSON/CSV
- Local analytics summary (adherence, etc.)
- Delete all data
- About / disclaimer
- Orthostatic always skippable with confirm (O1)
- Onboarding (first launch): purpose, privacy, H10 requirement, disclaimer, optional display name, notification permission
- Questionnaire (blocking when incomplete)
- Today hub (post-questionnaire): Start morning measurement + today’s results
- Morning measurement session (combined guided flow; skip-orthostatic variant)
- History (gated)
- Day detail
- Settings
8. Privacy and trust
| Rule | Detail |
|---|
| Local first | All data in app sandbox |
| No account | Optional display name only |
| No third-party cloud analytics SDK | None in v3 |
| First-party local analytics | Yes — events/metrics on device only |
| No ads | None |
| Network | None required for core flow; BLE only |
| Export | User-initiated share sheet |
| Delete | One-tap wipe |
| Future App Store | Privacy Nutrition Labels: Data Not Collected (if still true); privacy policy URL when shipping publicly |
Disclaimer (onboarding + About): Not a medical device. Not for diagnosis or treatment. For personal training journaling only. If you have a heart condition or dizziness on standing, consult a clinician before orthostatic testing; stop if lightheaded.
9. Hardware and environment
| Item | Requirement |
|---|
| Phone | iPhone, iOS version per tech spec |
| Strap | Polar H10 (BLE Heart Rate + RR if available) |
| Optional | Oura — not integrated in v3; user may type RHR in notes if desired |
10. Rollout plan
| Phase | Deliverable | Exit criteria |
|---|
| Docs | PRD + TECH_SPEC approved | Zach sign-off |
| v1 vertical | Questionnaire gate + local store + Today shell | Forced gate works |
| v2 | H10 connect + 60 s rMSSD + history | Replaces Elite for 7-day parallel check |
| v3 | Orthostatic + baselines + notifications + export | Daily driver |
| v3.1 (optional) | Polish, widgets, CSV to Files | Nice-to-have |
| v4+ | Optional TP/Intervals export, Oura import, HealthKit read (maybe), longer orthostatic mode, App Store | Separate PRD addendum |
Parallel validation (recommended): For 7 mornings, run Elite HRV after this app’s rMSSD (or vice versa, fixed order) and note both values in Notes to sanity-check magnitude — not for pixel-perfect match.
11. Future: App Store for friends
Does not block v3. When considered:
| Area | Implication |
|---|
| Legal | Privacy policy, stronger medical disclaimer, maybe “wellness” not “diagnosis” |
| Support | Friends will break BLE; need troubleshooting screen |
| Signing | Apple Developer Program, review |
| Features to avoid until ready | HealthKit write of “clinical” types without care; unsubstantiated readiness claims |
| Features that help later | Clean architecture, settings-driven thresholds, export already built |
12. Product decisions (locked for v3)
| ID | Question | Options | Recommendation |
|---|
| O1 | Can user skip orthostatic after HRV? | (a) Required daily (b) Skippable with confirm (c) Required 4×/week | LOCKED (b) Skippable with confirm |
| O2 | Lying HR duration in combined flow | (a) 60 s after rMSSD (b) reuse rMSSD mean HR (c) 30 s after rMSSD | LOCKED (c) 30 s dedicated lying avg |
| O3 | Orthostatic gap primary display | gap_peak vs gap_avg | LOCKED both stored; gap_peak primary in UI |
| O4 | Strict history gate | Block history until questionnaire | LOCKED Yes |
| O5 | rMSSD posture | Lie vs sit | LOCKED Lie |
13. Risks and mitigations
| Risk | Mitigation |
|---|
| BLE flaky on iOS | Clear reconnect UX; foreground-only session; tested checklist |
| rMSSD ≠ Elite exactly | Document method; parallel week; don’t chase identity |
| User stands too slow/fast | Large cues, haptics, invalidation if peak too late |
| Dizziness | Warning in onboarding; cancel button always visible |
| Habit drop | Notification + <5 min full path |
| Scope creep (Oura, TP, AI) | Non-goals list; new PRD revision required |
14. Out of scope copy / compliance
- Do not claim FDA clearance or medical accuracy.
- Do not use the word “diagnose.”
- Prefer “training journal,” “morning check-in,” “personal baselines.”
15. Approval
| Role | Name | Status |
|---|
| Product owner | Zach | Pending |
| Spec author | Grok (session 2026-08-01) | Complete for review |
Next step after approval: implement per TECH_SPEC.md milestones; no code until Zach says go.
Technical Specification — RecoveryDeck (iOS)
Companion: PRD.md · REVIEW_NOTES_RESPONSE.md Product name: RecoveryDeck Version: 1.3 Date: 2026-08-02 Status: Draft for approval (no implementation until approved) Changelog 1.2: Rename RecoveryDeck; local analytics; 1–7 questionnaire + habits; displayName; export schema. Changelog 1.3: Baselines Couzens-style 7d vs 60d ±1 SD; caffeine time/amount + last meal fields; short orthostatic kept.
1. Overview
Native iOS application (Swift + SwiftUI) that:
- Enforces a per-local-day questionnaire gate
- Streams Polar H10 BLE heart rate / RR intervals
- Computes rMSSD (Elite HRV replacement)
- Runs a guided orthostatic protocol
- Persists all data on-device only
Target: personal sideload / TestFlight to owner. Architecture must not preclude App Store later.
| Choice | Decision |
|---|
| Language | Swift 5.9+ |
| UI | SwiftUI |
| Min iOS | iOS 17 (adjust to owner’s phone if needed; document at kickoff) |
| Architecture | MVVM + protocol-oriented services |
| Concurrency | Swift async/await + AsyncStream for HR samples |
| Persistence | SwiftData (preferred) or Core Data; v3 single store |
| BLE | CoreBluetooth |
| Package manager | Xcode SPM only if needed (prefer zero deps v3) |
| Bundle ID (suggested) | com.zringstrom.recoverydeck (final at Xcode create) |
| Display name | RecoveryDeck |
2.1 Xcode project layout (suggested)
RecoveryDeck/
App/
RecoveryDeckApp.swift
Features/
Onboarding/
Questionnaire/
Today/
MorningMeasurement/ # combined rMSSD + orthostatic session
History/
Settings/
Services/
Bluetooth/
HeartRateClient.swift
PolarH10Identifiers.swift
Metrics/
RMSSDCalculator.swift
OrthostaticCalculator.swift
BaselineCalculator.swift
Persistence/
Models.swift
DayRepository.swift
Notifications/
NotificationScheduler.swift
Resources/
Info.plist keys (BLE usage strings)
Tests/
RMSSDCalculatorTests.swift
OrthostaticCalculatorTests.swift
BaselineCalculatorTests.swift
GateLogicTests.swift
3. Permissions and Info.plist
| Key | Purpose | User-facing string (draft) |
|---|
NSBluetoothAlwaysUsageDescription | H10 | “Bluetooth is used to connect to your Polar H10 for morning heart-rate measurements.” |
NSBluetoothPeripheralUsageDescription | if required by target | same spirit |
| Notifications | morning reminder | Standard request at end of onboarding |
Not used in v3: HealthKit, Motion (unless later for stand detect — out of scope), Background Modes for BLE (sessions are foreground only).
4. Bluetooth / Polar H10
4.1 Services and characteristics
Use standard BLE SIG profiles (H10 supports these):
| Item | UUID (standard) |
|---|
| Heart Rate Service | 0x180D |
| Heart Rate Measurement | 0x2A37 |
| Body Sensor Location | 0x2A38 (optional) |
| Battery Service | 0x180F (optional UX) |
RR intervals: Parse HR Measurement flags per Bluetooth HR Profile:
- If RR-Interval bit present, extract RR values in 1/1024 s units → convert to ms:
rr_ms = rr_raw * 1000 / 1024
If a firmware/connection path yields HR-only without RR:
- Cannot compute true rMSSD from integer BPM alone.
- UX: error “RR intervals not available — ensure H10 is snug, charged, and not connected exclusively elsewhere; prefer H10 in heart rate mode.”
- Optional degraded mode: store mean HR only; mark
rmssd null — do not fake rMSSD from BPM.
4.2 Connection UX
- User taps Connect Polar H10
- Scan for peripherals advertising HR service (or known Polar name prefix
Polar H10)
- Connect + subscribe to HR Measurement notifications
- Show live BPM + “RR OK” indicator when RR present
- On disconnect mid-test: pause, offer Resume/Restart
- Only one session owns the peripheral at a time
Note: iPhone may already be bonded; handle reconnect. Instruct user to disconnect H10 from other apps (Elite HRV, Polar Beat) during measurement.
4.3 Sample model
struct HRSample: Sendable {
let timestamp: Date
let bpm: Double? // from HR field when present
let rrIntervalsMs: [Double] // zero or more RR since last notification
}
Feed AsyncStream<HRSample> into session VMs.
5. Metric algorithms
5.1 rMSSD
Definition: Root mean square of successive differences of valid NN (RR) intervals.
For successive valid intervals \(RR_i\), \(RR_{i+1}\) (ms):
\[ rMSSD = \sqrt{ \frac{1}{N-1} \sum_{i=1}^{N-1} (RR_{i+1} - RR_i)^2 } \]
Window (normative): accumulate accepted RR until sum(RR) ≥ 60_000 ms, with min 45 accepted intervals and max wall-clock 90 s after settle; fail quality if either floor not met. (Physiological 60 s of RR, not wall-clock-only.)
Settle: 15.0 s wall-clock after user confirms still; discard RR during settle for rMSSD (may still show live HR).
Artifact rejection (v3, simple and documented):
- Drop RR < 300 ms or > 2000 ms
- Drop RR where successive difference > 20% of previous RR (ectopic/motion heuristic)
- Track
artifactRatio = rejected / (accepted+rejected)
- If
artifactRatio > 0.20 or accepted count < 45 → quality fail, prompt retry
Also store: mean RR, mean HR = 60000/meanRR, SDNN optional (not primary UI).
Unit tests: golden vectors of RR lists → known rMSSD (hand-calculated fixtures).
5.2 Mean HR over a phase
For orthostatic lying/standing phases of wall duration T:
- Prefer mean of BPM samples in window, or 60000/mean(RR) if RR dense
- Peak standing HR: max BPM over the full 60 s standing window (includes early rise; Couzens peak response)
5.3 Orthostatic derived
avgLyingHR
avgStandingHR
peakStandingHR
gapAvg = avgStandingHR - avgLyingHR
gapPeak = peakStandingHR - avgLyingHR
5.4 Baselines and green / yellow / red (Couzens-aligned)
Couzens dashboard approach (Ch.13 materials): 7-day rolling mean (acute) vs 60-day rolling mean (norm), with a normal band ≈ ±1 SD around the long-term mean. Values inside the band look “normal for this athlete”; outside is highlighted.
Per metric series (rMSSD, avgLyingHR, gapPeak, gapAvg, subjectiveMean):
| Quantity | Definition |
|---|
normMean | Rolling mean of last 60 complete days (excluding today); require ≥ 14 prior days else baselineStatus = .building |
normSD | Sample SD over same 60-day window (min floor: rMSSD 1 ms, HR 1 bpm, gap 1 bpm, subjective 0.25) |
acute | Prefer today’s value for daily UI; optional secondary display of 7-day rolling mean (Couzens acute line) |
| Normal band | [normMean − 1×normSD, normMean + 1×normSD] |
Traffic lights (normative):
| Light | Rule |
|---|
| Green | acute inside normal band or still .building |
| Yellow | outside band by ≤ 0.5×normSD beyond the edge (i.e. between 1.0 and 1.5 SD from mean) |
| Red | outside band by > 0.5×normSD beyond the edge (beyond ~1.5 SD from mean) |
Concerning directions (for tips / emphasis, not for ignoring the opposite side):
| Series | Emphasize when… |
|---|
| rMSSD | Low vs band |
| avgLyingHR | High vs band (elevated); very low + poor subjective → tip “deep fatigue pattern?” |
| gapPeak | Smaller gap vs band (blunted stand response) |
| Subjective mean (5× 1–7) | Low scores (worse feel) |
Absolute subjective fallback if not enough history: mean ≥ 5.0 green-ish, ≥ 3.5 yellow-ish, else red-ish (same polarity as before).
Composite chip (optional): majority of channel lights; ties → yellow. Label “rough signal — not medical.”
Not clinical cutoffs; personal statistical bands after Couzens’ visualization pattern.
6. Session state machines
6.1 Combined morning measurement flow (v3 primary — locked)
Single CTA: Start morning measurement. Matches PRD §6.1 / §6.5.
State: Idle
→ Connecting
→ ConnectedLive (show BPM)
→ User taps Start (must be post-questionnaire)
→ SettleLying (15 s)
→ CaptureRMSSD (accumulate RR ~60 s sum)
→ CaptureLyingHR (30 s wall, mean HR) // orthostatic lying / daily RHR
→ PromptStand (3 s countdown + haptic)
→ CaptureStanding (60 s wall)
→ Complete (persist MeasurementBundle; overwrite if same localDate confirm)
→ Error/Cancel from any state
Total: 15 + 60 + 30 lying + 3 cue + 60 standing ≈ ~3 min + connect.
Remeasure: If MeasurementRecord exists for localDate, require confirm → overwrite same row (v3: no version history).
6.2 HRV-only path
If orthostatic skipped (PRD O1b):
Settle 15 s → RMSSD 60 s → persist HRV partial → Today shows orthostatic missing
Skip requires confirm: “Skip orthostatic today?”
6.3 Gate logic (pure function — unit test)
func canAccessHistory(today: LocalDate, questionnaire: DayRecord?) -> Bool
func canStartMeasurement(today: LocalDate, questionnaire: DayRecord?) -> Bool
Rules per PRD §6.2.
7. Data model
7.1 SwiftData entities (conceptual)
DayRecord
| Field | Type | Notes |
|---|
| id | UUID | |
| localDate | String yyyy-MM-dd | unique |
| timezoneIdentifier | String | |
| fatigue | Int 1…7 | |
| mood | Int 1…7 | |
| soreness | Int 1…7 | |
| lifeStress | Int 1…7 | |
| sleepQuality | Int 1…7 | |
| lastCaffeineAt | Date? | optional; local time of last caffeine |
| caffeineAmountMg | Double? | optional; mg if known |
| caffeineAmountBand | String? | optional: none/small/medium/large if mg unknown |
| lastMealAt | Date? | optional; time last meal finished |
| habitAlcohol | Bool? | optional chip |
| habitHardTrainingYesterday | Bool? | |
| habitTravelLate | Bool? | |
| habitSick | Bool? | |
| notes | String? | |
| questionnaireCompletedAt | Date | |
| questionnaireEditedAfterMeasure | Bool | default false |
MeasurementRecord
| Field | Type | Notes |
|---|
| id | UUID | |
| localDate | String | FK logical to day |
| measuredAt | Date | |
| protocolVersion | String | e.g. "v3.0" |
| rmssdMs | Double? | |
| meanHrBpm | Double? | during RMSSD |
| rrAcceptedCount | Int? | |
| artifactRatio | Double? | |
| hrvQuality | enum ok/fail | |
| avgLyingHr | Double? | |
| avgStandingHr | Double? | |
| peakStandingHr | Double? | |
| gapAvg | Double? | |
| gapPeak | Double? | |
| orthostaticSkipped | Bool | |
| orthostaticQuality | enum ok/fail/skipped | |
| deviceName | String? | |
| rawDebugPath | String? | optional debug only, off by default |
AppSettings
| Field | Type | Default |
|---|
| displayName | String? | optional greeting |
| notificationHour/Minute | Int | 6:30 |
| notificationsEnabled | Bool | true |
| normWindowDays | Int | 60 (Couzens long-term norm) |
| acuteWindowDays | Int | 7 (optional chart line) |
| dayStartTimezone | String | auto |
| enabledHabitChips | [String] | which Block D chips show |
7.3 Local analytics (on-device only)
Store lightweight events in the same sandbox (separate entity or append-only log), e.g.:
morning_completed, orthostatic_skipped, hrv_quality_fail, session_duration_ms, ble_connect_fail
No network upload. Surface a simple Settings → Analytics summary. Export may include analytics if user exports “full backup.”
7.4 Future export (v4+ — not implemented)
Stable JSON protocolVersion so a later module can map fields → TrainingPeaks metrics / Intervals wellness without schema thrash.
7.2 Completeness
day.isComplete = questionnaire done && rmssd present && quality ok && (orthostatic ok || skipped).
8. Persistence and privacy
| Topic | Spec |
|---|
| Location | App sandbox only |
| Encryption | iOS Data Protection default; no custom crypto required v3 |
| iCloud | Off |
| Backup | Participates in iCloud device backup unless excluded — v3 accept default; document that iCloud backup may include data if user backs up phone |
| Export | JSON (full) + CSV (days flat) via UIActivityViewController |
| Delete all | Wipe SwiftData store + reset onboarding flag optional keep |
No network client in v3 targets. CI should fail if new URLSession analytics appear.
9. UI specifications (behavioral)
9.1 Questionnaire
- Five required steppers / tappable 1–7 chips (Block A+B)
- Optional Block C: last caffeine time, caffeine amount, last meal time (skippable)
- Optional Block D habit chips + Block E notes
- Large touch targets; submit sticky
- Cannot navigate to History via Tab until required scores submitted
9.2 Session UI
- Large phase title
- Countdown / progress
- Live BPM
- Cancel always available
- Success checkmark → auto-return Today
9.3 Accessibility
- Dynamic Type support for questionnaire
- VoiceOver labels on scores
- Haptics for stand cue (respect reduce motion: visual flash)
9.4 Appearance
- System light/dark
- Calm, minimal; no gamification streaks required in v3 (optional later)
10. Notifications
UNUserNotificationCenter
- Daily calendar trigger
- ID:
morning-checkin-daily
- Reschedule on settings change
- No critical alerts
11. Testing strategy
11.1 Unit tests (required before calling v3 done)
| Module | Cases |
|---|
| RMSSDCalculator | empty, single, monotonic, known fixture, artifact heavy |
| OrthostaticCalculator | gaps, peak |
| BaselineCalculator | building, green/yellow/red bands |
| GateLogic | before/after questionnaire, day rollover |
11.2 Manual test checklist (H10)
- Fresh install → onboarding → notification permission
- Questionnaire gate blocks History and Start
- Connect H10; RR indicator green
- Full combined session completes; values plausible (rMSSD typically tens of ms; not 0; not 500)
- Kill app mid-session; no corrupt day
- Skip orthostatic path
- Second launch same day: questionnaire done; can remeasure with confirm overwrite
- Next calendar day: gate resets
- Export JSON non-empty
- Delete all data
11.3 Parallel Elite HRV (user)
7 days: record both rMSSD; expect similar order of magnitude and co-movement, not equality.
12. Implementation milestones
| Milestone | Scope | Done when |
|---|
| M0 | Xcode project, folder structure, SwiftData models, gate + questionnaire + Today shell | UI flow without BLE |
| M1 | HeartRateClient + live BPM | Connect H10 reliably |
| M2 | RMSSD session + persist + History | Elite replacement usable |
| M3 | Combined orthostatic + baselines + notifications + export + tests | v3 complete |
Estimated effort (solo, familiar with iOS): M0–M1 1–2 days, M2 1–2 days, M3 1–2 days — ~1 week calendar part-time, not a commitment.
13. Error catalog (user-visible)
| Code | Message (draft) |
|---|
| BT_POWER | Turn on Bluetooth |
| BT_DENIED | Allow Bluetooth in Settings |
| H10_NOT_FOUND | Polar H10 not found — wear it, press button, close other HR apps |
| H10_NO_RR | Connected but no RR intervals — cannot compute HRV |
| H10_DROP | Connection lost — retry |
| QUALITY_HRV | Too much noise — lie still and retry |
| QUALITY_ORTHO | Standing segment invalid — retry orthostatic |
| GATE | Complete today’s questionnaire first |
14. Security notes
- No API keys in repo
- No logging of health samples to third parties
- Debug RR dumps off by default; if enabled, local file only + setting
15. App Store readiness (non-blocking notes)
When/if public:
- Privacy policy URL
- App Review notes: Polar H10 required for full function; demo mode with simulator RR fixture for reviewers
- Simulator demo mode: inject synthetic RR for UI review without hardware
- Avoid HealthKit until justified
- Medical disclaimer in App Description
v3 demo mode: compile-flag DEMO_RR for SwiftUI previews and unit-free UI dev without strap.
16. PRD decisions (locked for v3)
| PRD | Implementation |
|---|
| O1 skip orthostatic | Allow skip with confirm → HRV-only path §6.2 |
| O2 lying duration | 30 s lying HR after rMSSD |
| O3 gap display | gapPeak primary, gapAvg secondary |
| O4 history gate | Block until questionnaire |
| O5 posture | Lying for rMSSD |
| Remeasure | Overwrite after confirm |
17. Explicit non-implementations
- Frequency-domain HRV (LF/HF) — Couzens notes need ~5 min for LF; v3 stays rMSSD + orthostatic
- ECG authentication modes beyond standard HR BLE
- Auto-stand detection via accelerometer
- Cloud sync
18. Acceptance criteria (v3)
- Questionnaire required before measurements and history.
- rMSSD computed from H10 RR with documented artifact rules; saved per day.
- Orthostatic produces lying avg, standing avg, peak, gaps (or skip recorded).
- Baselines:
.building until ≥14 complete days; then G/Y/R vs 60-day ±1 SD band.
- No network calls in core paths.
- Export and delete work.
- Unit tests for calculators and gate pass.
- Manual H10 checklist completed by owner.
19. References (product science)
- Alan Couzens, readiness composite (HRV, RHR, fatigue, mood, soreness, stress) — training-plan dashboard / Ch.13 materials in local archive
- Alan Couzens, orthostatic 60s+60s, peak vs lying gap —
taking-your-understanding-of-hrv.md
- Alan Couzens, HRV alone insufficient; commercial readiness skepticism — distilled monitoring notes
- Bluetooth SIG Heart Rate Profile (RR intervals)
20. Approval
| Role | Status |
|---|
| Engineering spec author | Complete for review |
| Owner (Zach) | Pending |
No implementation until PRD + this spec are approved and Zach requests build.
Response to product-review notes (2026-08-02)
Companion to PRD.md / TECH_SPEC.md. Product name going forward: RecoveryDeck.
1. How long should the orthostatic test be? Research vs our protocol
What research / clinical guidelines use
| Context | Typical protocol | Purpose |
|---|
| Orthostatic hypotension (clinical) | ~5 min supine, then BP/HR at ~3 min standing (sometimes also 1 min) | Diagnose sustained BP drop / HR response |
| Active stand / POTS screening | Often 5–10 min supine, then up to 10 min standing with serial HR/BP | Diagnose postural tachycardia (≥30 bpm rise sustained) |
| Autonomic lab studies | Sometimes 5+5 min or longer (10 min stand) | Research-grade comparability to tilt |
| Couzens (coaching tool) | ~60 s lie + 60 s stand; track lying avg, standing avg, peak standing, gaps | Daily readiness / ANS quadrant, not clinical diagnosis |
Clinical orthostatic tests are longer because they are optimizing diagnostic sensitivity (POTS, OH), often with BP cuffs, not a 3-minute morning habit with a chest strap.
Our current v3 protocol
~15 s settle + 60 s rMSSD + 30 s lying HR + 60 s stand ≈ 3 min total.
- Aligned with Couzens on the idea (quick daily orthostatic, peak−lying gap).
- Not aligned with clinical 3–10 min stand standards.
- Shorter lying sample (30 s) after rMSSD is a habit tradeoff, not a research gold standard.
Recommendation
| Goal | Protocol |
|---|
| Daily habit (v3 default) | Keep ~60 s stand + peak/avg gaps; keep combined rMSSD session. Accept that this is a trend tool, not a POTS test. |
| Better Couzens alignment | Prefer 60 s lying HR (not 30 s) after rMSSD if you can spare ~30 s more on the floor. |
| Optional “long stand” mode (later) | Settings: 60 s / 3 min stand for occasional deeper check (still not a medical diagnosis). |
| Do not | Claim clinical OH/POTS diagnosis from this app. |
Bottom line: Research supports minutes, not seconds, for clinical orthostatic testing. For daily coaching readiness, Couzens-style ~1+1 min is a deliberate lightweight proxy. We should document that honestly and optionally offer a longer stand later—not silently claim clinical equivalence.
2. Evidence behind green / yellow / red signals
Honest answer (current v1.3)
RecoveryDeck no longer uses arbitrary ±10% / ±3 bpm tables as the primary rule.
Couzens’s actual method (dashboard): compare acute (≈7-day) metrics to a long-term norm (≈60-day mean) and shade a normal range ≈ ±1 SD. Outside that band is “not normal for this athlete.” He does not publish a universal green/yellow/red product matrix.
RecoveryDeck G/Y/R (TECH §5.4) implements that idea: green inside ±1 SD, yellow ~1–1.5 SD, red beyond ~1.5 SD, with concerning-direction labels.
What is evidence-backed (directionally)
| Idea | Support |
|---|
| Compare to personal baseline, not population | Couzens 7d vs 60d; HRV practice generally |
| ±1 SD as “normal range” visualization | Couzens dashboard figures (explicit green band) |
| HRV related to readiness for higher intensity | Couzens / Vesterinen et al. type findings |
| RHR + orthostatic response context | Couzens quadrants; Hedelin et al. overreaching |
| Subjective fatigue/mood/soreness/stress | Couzens Ch.13; Morgan/Hooper swimming staleness work |
| Composite > single commercial score | Couzens critique of unanchored readiness apps |
What is not proven
- That 1.0 / 1.5 SD yellow/red edges are optimal for you (tunable later)
- That a single traffic light predicts adaptation that day
Recommendation
Keep lights as personal-band cues, not medical scores; show direction in words; allow tuning after you have months of data.
3. Analytics: “no SDK” vs in-app analytics
Clarification
| Kind | v3 intent |
|---|
| Third-party analytics SDK (Amplitude, Firebase Analytics cloud, Mixpanel, etc.) | No — privacy-first, no phone-home |
| On-device analytics / instrumentation | Yes — local event + metric store so you can see adherence, completion, quality fails, time-to-complete |
Examples of local analytics (stay on phone):
- Days completed / streak
- % mornings with orthostatic skipped
- HRV quality fail rate
- Median time for full session
- Questionnaire item distributions over time
We will update the PRD wording from “no analytics” → “no third-party cloud analytics SDK; first-party local analytics OK.”
4. TrainingPeaks / Intervals.icu sync later
Agreed: not in v3, but optional future (v4+), not a hard forever-no.
Architecture implication (tech): keep DayRecord / MeasurementRecord export-friendly (stable JSON schema) so a later exporter can push:
- Morning metrics as TrainingPeaks “metrics” style fields, or
- Intervals wellness / notes / custom fields
No implementation now.
5. Scale 1–5 vs 1–7
Research (psychometrics, brief)
- 5- and 7-point Likert scales are both widely used and considered adequate.
- 7-point often gives slightly higher reliability / more variance (more discrimination); diminishing returns above ~7–11.
- 5-point is faster and often better for general populations / less fatigue.
- “Weird numbers” (e.g. 1–10, 0–100) help mainly by more resolution, not magic—odd midpoints (5 or 7) give a true center.
Recommendation for RecoveryDeck
Default to 1–7 for core feeling items (athlete daily use, high motivation, want nuance).
| Scale | When |
|---|
| 1–7 | Fatigue, mood, soreness, stress, sleep quality (core) |
| Yes / No | WHOOP-style habit tags (alcohol, late caffeine, etc.) |
Update traffic-light subjective bands when switching (e.g. green mean ≥ 5.0, yellow ≥ 3.5 on 1–7).
6. Questionnaire: Alan + WHOOP-inspired
What Alan actually uses / models (archive)
From Couzens dashboard / readiness materials, morning metrics fed into readiness include:
| Field | Role |
|---|
| HRV | Measured (not questionnaire) |
| Pulse / RHR | Measured |
| Fatigue | Subjective |
| Mood | Subjective |
| Soreness | Subjective (incl. “heavy legs”) |
| Stress (life stress) | Subjective |
He emphasizes: multi-input composite; low HRV ≠ ban all easy work; soreness = muscular readiness; life stress often early warning.
WHOOP Journal (what you likely liked)
WHOOP separates:
- How you feel / recovery-adjacent ratings (mood, etc., product evolves)
- Behavior tags (yes/no) correlated later with recovery: e.g. alcohol, caffeine, shared bed, meditation, late meals, etc.
The power is habit ↔ recovery association, not a long feelings essay.
Proposed RecoveryDeck questionnaire (v3)
Block A — required (Alan core), scale 1–7 Higher = better for all feeling items (consistent polarity).
- Fatigue — Exhausted → Fresh
- Mood — Very low → Great
- Soreness / heavy legs — Very sore → None
- Life stress — Very high → Very low
Block B — required short (WHOOP-inspired sleep feel)
- Sleep quality (subjective) — Terrible → Excellent
(complements Oura; captures “I slept 8h but feel wrecked”)
Block C — optional habit chips (WHOOP-style, yes/no, default off until user enables)
Suggested starter set (user can toggle in Settings later):
- Alcohol last night
- Caffeine after 2pm (or “late caffeine”)
- Hard training yesterday (self-tag)
- Travel / late night
- Illness / sick feel
Block D — optional free text Notes
Gate: Block A + B required before measurements. Block C can be same screen, optional.
7. Name without account
Yes — optional display name in onboarding/Settings.
- Stored only on device
- Used for “Good morning, Zach”
- Not identity verification; no login
- Default: empty → “Good morning”
8. “Friends will break BLE” — what that means
Not “Bluetooth is nonstandard.” BLE HR is exactly what Elite HRV, HRV4Training, Polar Beat use.
It means support reality when people other than you use the app:
| Issue | Why friends hit it |
|---|
| H10 connected to two apps | iOS often only streams cleanly to one |
| Wrong strap fit / dry electrodes | No RR → no rMSSD |
| Bluetooth permission denied | Silent fail |
| Backgrounded mid-test | Session dies (we’re foreground-only by design) |
| Non-H10 strap | May send HR without RR |
| “It worked yesterday” | Watch OS / phone OS updates |
Elite HRV has years of edge-case UX and support docs. For you, fine. For App Store friends, expect support load unless troubleshooting is excellent.
9. HealthKit — would it ever make sense?
| Write to HealthKit | Sense? |
|---|
| Heart rate samples during test | Low value (transient; Apple already has workouts from watch) |
| HRV (SDNN) | Possible; Apple’s type is often SDNN-oriented, not rMSSD—mapping is messy |
| Mindful / sleep | Not our domain |
| Read: sleep analysis, resting HR from watch | Maybe later if you drop Oura manual path |
| Read: workouts | Could contextualize “hard day yesterday” without Intervals |
Recommendation:
- v3: No HealthKit (simpler privacy story, fewer review issues).
- Later (optional):
- Read last-night sleep duration / workouts for context chips - Write optional “mindful session” or a single daily summary only if users want Apple Health graphs
- Avoid writing clinical-sounding “recovery diagnosis.”
10. App name: RecoveryDeck
Approved as product name. Bundle ID suggestion: com.zringstrom.recoverydeck.
Product decisions (updated 2026-08-02 follow-up)
| ID | Decision |
|---|
| Name | RecoveryDeck |
| Local analytics | Yes (on-device only) |
| Cloud analytics SDK | No (v3) |
| TP / Intervals | v4+ optional, not v3 |
| Likert | 1–7 for feeling items |
| Questionnaire | Alan core + sleep quality; optional last caffeine time + amount; optional last meal time; optional habit chips |
| Display name | Optional onboarding field |
| Orthostatic duration | Keep short daily proxy (v3 combined ~60 s stand) |
| HealthKit | Not v3 (confirmed) |
| G/Y/R | Couzens-style: acute vs 60-day mean ± 1 SD band (see TECH §5.4)—not arbitrary ±10% |
Couzens on green / yellow / red (short)
He does not ship a branded traffic-light product table. He:
- Compares 7-day (acute) metrics to a 60-day norm
- Shades a normal range ≈ ±1 SD (green band in his dashboard figures)
- Flags when the athlete is outside their own range
- Uses “red light” for serious overtraining-type states (e.g. orthostatic Q4), not every off morning
RecoveryDeck’s G/Y/R is an implementation of that normal-band idea, not a quote of Couzens cutoffs.
BLE “friends will break it” (plainer)
Bluetooth itself is fine—Elite HRV uses it too. The hard part is everything around the radio:
Imagine a friend installs RecoveryDeck, wears an H10, and taps Start:
- Polar Beat is still connected in the background → phone won’t give clean RR to RecoveryDeck → “no HRV.”
- They didn’t enable Bluetooth permission → blank scan.
- Strap is loose / dry skin → HR jumps, RR missing → quality fail.
- They switch to Messages mid-test → app backgrounds → connection drops.
- They use a generic $30 strap that only sends BPM, not RR → we refuse to invent rMSSD.
- They’re in a gym full of BLE devices → slow pairing, flaky connect.
You already know “close other apps, wet electrodes, stay in the app.” Friends often don’t—so they think the app is broken when it’s the setup. That’s all “friends will break BLE” meant: support/education, not “BLE is wrong.”
Docs updated in PRD/TECH to reflect the above (see changelog).
RecoveryDeck
Privacy-first iOS morning recovery check-in (personal): forced questionnaire → Polar H10 rMSSD (Elite HRV replacement) → orthostatic test → local history + on-device analytics.
Formerly working title: “Morning Readiness.”
Documents
open docs/index.html
Status
Docs only — not implemented until approved and build is requested.
Hardware
Privacy
Local-first; optional display name; no third-party cloud analytics SDK; yes first-party on-device analytics.