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:

  1. Forced questionnaire (cannot skip into measurements)
  2. Polar H10 measurements that replace Elite HRV (rMSSD) and add an orthostatic HR test
  3. 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

PainToday
Subjective readiness never loggedNo daily fatigue/mood/soreness/stress answers
Recovery signals fragmentedOura (sleep/RHR/HRV), Elite HRV (1 min rMSSD), training feel — separate apps
Elite HRV is an extra hopUser wants one morning app that owns rMSSD
Orthostatic not automatedCouzens-valued test; no easy 2–3 min guided flow with H10
Commercial readiness scores untrustedOura 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”)

  1. Questionnaire-first gate every local calendar morning before any measurement or stats that reveal trends.
  2. Replace Elite HRV with in-app ~60 s rMSSD via Polar H10.
  3. Guided orthostatic test (~2–3 min) via H10; store lying avg HR, standing avg HR, peak standing HR, and gaps.
  4. RHR defined as lying average HR from the orthostatic (or dedicated lying segment) — primary RHR for the day.
  5. Local-first storage; no account required; no third-party cloud analytics SDK; no required cloud.
  6. On-device analytics (adherence, quality fails, completion time, etc.) stored only on the phone.
  7. History of questionnaire + metrics with simple vs personal baseline indicators (green / yellow / red) — heuristic, not clinical cutoffs (see §6.6).
  8. Installable on owner’s iPhone (Xcode / TestFlight to self).
  9. Architecture that does not block later App Store, optional display name, or optional TrainingPeaks / Intervals export.
  10. 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 v3deferred 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)

MetricTarget (first 30 days after install)
Morning completion rate80% of days with full questionnaire + rMSSD
Orthostatic adherence60% of days (skippable after confirm per locked O1; still count as completed morning if rMSSD saved)
Elite HRV usageZero intentional uses after parallel validation week
Subjective capture100% of measurement days have all required questionnaire fields
TrustUser prefers this app’s morning flow over prior stack

4. Users

PersonaRole
Primary: ZachEndurance athlete (~18–22 h/wk, AeT-capped base). Owns Oura + Polar H10. Trains with Couzens-informed principles.
Future: friendsPossible 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

  1. Gate first, measure second — No peeking at trends to bias answers.
  2. Composite, not oracle — Show numbers + simple baselines; user decides training.
  3. Privacy first — Data never leaves the device unless user explicitly exports.
  4. Repeatable protocol — Same posture, timing, and order every day.
  5. Couzens-aligned language — Ready to load vs ready to recover; low HRV ≠ ban all easy work.
  6. Boring reliability — Prefer a rock-solid H10 connection over flashy charts.
  7. 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)

DestinationAllowed if questionnaire not done today?
QuestionnaireYes
Start morning measurement (combined session)No
Today’s metric numbers (after measure)Only after questionnaire; metrics appear as completed
History / charts / baselinesNo until questionnaire submitted for today
SettingsYes (always)
ExportYes (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.

Field17
FatigueExhaustedFresh
MoodVery lowGreat
Soreness / heavy legsVery soreNone
Life stressVery highVery low

Rationale: Couzens morning inventory / dashboard features use Fatigue, Mood, Soreness, Stress (+ measured HRV & Pulse).

Block B — required (short), scale 1–7

Field17
Sleep quality (how it felt)TerribleExcellent

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.”

FieldTypeNotes
Last caffeine time (previous day → morning)Time of day (or “none”)When caffeine was last consumed before this check-in
Caffeine amountOptional amountFree number + unit picker default mg, or coarse chips (e.g. none / small / medium / large) if exact mg unknown
Last meal timeTime of dayWhen 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)

ItemSpec
HardwarePolar H10 via Bluetooth LE
PostureLying supine, still, normal breathing (aligned with orthostatic lying; one posture for whole session)
Duration60 seconds of accepted RR intervals after a short settle
Settle15 s countdown after “connected & still” before RR window starts
OutputrMSSD (ms), mean HR (bpm), artifact %, sample count
FailuresDisconnect, 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.

PhaseDurationUser actionRecord
PrepConnect H10; lie down
Settle15 sStay lying, stilldiscard for rMSSD
rMSSD~60 s RR accumulationStay lyingrMSSD, mean HR
Orthostatic lying30 sStay lyingAvg lying HR (primary RHR for the day)
Cue3 s“Stand up now” (haptic)
Standing60 sStand stillAvg 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:

  1. Long-term norm60-day rolling mean (starting point; window can be experimented with).
  2. Acute signal7-day rolling mean (less noisy than raw daily).
  3. Normal band±1 standard deviation around the long-term mean (he plots this as a pale green “normal range”).
  4. Flag when the athlete is outside their own range.
  5. Combine HRV, pulse, fatigue, mood, soreness, stress; be skeptical of commercial single “readiness scores.”
  6. “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):

LightMeaning
GreenToday (acute) inside personal 60-day mean ± 1 SD band, or still building history
YellowMildly outside that band (~1–1.5 SD)
RedClearly 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)

7. Information architecture (screens)

  1. Onboarding (first launch): purpose, privacy, H10 requirement, disclaimer, optional display name, notification permission
  2. Questionnaire (blocking when incomplete)
  3. Today hub (post-questionnaire): Start morning measurement + today’s results
  4. Morning measurement session (combined guided flow; skip-orthostatic variant)
  5. History (gated)
  6. Day detail
  7. Settings

8. Privacy and trust

RuleDetail
Local firstAll data in app sandbox
No accountOptional display name only
No third-party cloud analytics SDKNone in v3
First-party local analyticsYes — events/metrics on device only
No adsNone
NetworkNone required for core flow; BLE only
ExportUser-initiated share sheet
DeleteOne-tap wipe
Future App StorePrivacy 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

ItemRequirement
PhoneiPhone, iOS version per tech spec
StrapPolar H10 (BLE Heart Rate + RR if available)
OptionalOura — not integrated in v3; user may type RHR in notes if desired

10. Rollout plan

PhaseDeliverableExit criteria
DocsPRD + TECH_SPEC approvedZach sign-off
v1 verticalQuestionnaire gate + local store + Today shellForced gate works
v2H10 connect + 60 s rMSSD + historyReplaces Elite for 7-day parallel check
v3Orthostatic + baselines + notifications + exportDaily driver
v3.1 (optional)Polish, widgets, CSV to FilesNice-to-have
v4+Optional TP/Intervals export, Oura import, HealthKit read (maybe), longer orthostatic mode, App StoreSeparate 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:

AreaImplication
LegalPrivacy policy, stronger medical disclaimer, maybe “wellness” not “diagnosis”
SupportFriends will break BLE; need troubleshooting screen
SigningApple Developer Program, review
Features to avoid until readyHealthKit write of “clinical” types without care; unsubstantiated readiness claims
Features that help laterClean architecture, settings-driven thresholds, export already built

12. Product decisions (locked for v3)

IDQuestionOptionsRecommendation
O1Can user skip orthostatic after HRV?(a) Required daily (b) Skippable with confirm (c) Required 4×/weekLOCKED (b) Skippable with confirm
O2Lying HR duration in combined flow(a) 60 s after rMSSD (b) reuse rMSSD mean HR (c) 30 s after rMSSDLOCKED (c) 30 s dedicated lying avg
O3Orthostatic gap primary displaygap_peak vs gap_avgLOCKED both stored; gap_peak primary in UI
O4Strict history gateBlock history until questionnaireLOCKED Yes
O5rMSSD postureLie vs sitLOCKED Lie

13. Risks and mitigations

RiskMitigation
BLE flaky on iOSClear reconnect UX; foreground-only session; tested checklist
rMSSD ≠ Elite exactlyDocument method; parallel week; don’t chase identity
User stands too slow/fastLarge cues, haptics, invalidation if peak too late
DizzinessWarning in onboarding; cancel button always visible
Habit dropNotification + <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

RoleNameStatus
Product ownerZachPending
Spec authorGrok (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:

  1. Enforces a per-local-day questionnaire gate
  2. Streams Polar H10 BLE heart rate / RR intervals
  3. Computes rMSSD (Elite HRV replacement)
  4. Runs a guided orthostatic protocol
  5. Persists all data on-device only

Target: personal sideload / TestFlight to owner. Architecture must not preclude App Store later.


2. Platform and project setup

ChoiceDecision
LanguageSwift 5.9+
UISwiftUI
Min iOSiOS 17 (adjust to owner’s phone if needed; document at kickoff)
ArchitectureMVVM + protocol-oriented services
ConcurrencySwift async/await + AsyncStream for HR samples
PersistenceSwiftData (preferred) or Core Data; v3 single store
BLECoreBluetooth
Package managerXcode SPM only if needed (prefer zero deps v3)
Bundle ID (suggested)com.zringstrom.recoverydeck (final at Xcode create)
Display nameRecoveryDeck

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

KeyPurposeUser-facing string (draft)
NSBluetoothAlwaysUsageDescriptionH10“Bluetooth is used to connect to your Polar H10 for morning heart-rate measurements.”
NSBluetoothPeripheralUsageDescriptionif required by targetsame spirit
Notificationsmorning reminderStandard 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):

ItemUUID (standard)
Heart Rate Service0x180D
Heart Rate Measurement0x2A37
Body Sensor Location0x2A38 (optional)
Battery Service0x180F (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

  1. User taps Connect Polar H10
  2. Scan for peripherals advertising HR service (or known Polar name prefix Polar H10)
  3. Connect + subscribe to HR Measurement notifications
  4. Show live BPM + “RR OK” indicator when RR present
  5. On disconnect mid-test: pause, offer Resume/Restart
  6. 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):

  1. Drop RR &lt; 300 ms or &gt; 2000 ms
  2. Drop RR where successive difference &gt; 20% of previous RR (ectopic/motion heuristic)
  3. Track artifactRatio = rejected / (accepted+rejected)
  4. If artifactRatio > 0.20 or accepted count &lt; 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):

QuantityDefinition
normMeanRolling mean of last 60 complete days (excluding today); require ≥ 14 prior days else baselineStatus = .building
normSDSample SD over same 60-day window (min floor: rMSSD 1 ms, HR 1 bpm, gap 1 bpm, subjective 0.25)
acutePrefer 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):

LightRule
Greenacute inside normal band or still .building
Yellowoutside band by ≤ 0.5×normSD beyond the edge (i.e. between 1.0 and 1.5 SD from mean)
Redoutside 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):

SeriesEmphasize when…
rMSSDLow vs band
avgLyingHRHigh vs band (elevated); very low + poor subjective → tip “deep fatigue pattern?”
gapPeakSmaller 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

FieldTypeNotes
idUUID
localDateString yyyy-MM-ddunique
timezoneIdentifierString
fatigueInt 1…7
moodInt 1…7
sorenessInt 1…7
lifeStressInt 1…7
sleepQualityInt 1…7
lastCaffeineAtDate?optional; local time of last caffeine
caffeineAmountMgDouble?optional; mg if known
caffeineAmountBandString?optional: none/small/medium/large if mg unknown
lastMealAtDate?optional; time last meal finished
habitAlcoholBool?optional chip
habitHardTrainingYesterdayBool?
habitTravelLateBool?
habitSickBool?
notesString?
questionnaireCompletedAtDate
questionnaireEditedAfterMeasureBooldefault false

MeasurementRecord

FieldTypeNotes
idUUID
localDateStringFK logical to day
measuredAtDate
protocolVersionStringe.g. "v3.0"
rmssdMsDouble?
meanHrBpmDouble?during RMSSD
rrAcceptedCountInt?
artifactRatioDouble?
hrvQualityenum ok/fail
avgLyingHrDouble?
avgStandingHrDouble?
peakStandingHrDouble?
gapAvgDouble?
gapPeakDouble?
orthostaticSkippedBool
orthostaticQualityenum ok/fail/skipped
deviceNameString?
rawDebugPathString?optional debug only, off by default

AppSettings

FieldTypeDefault
displayNameString?optional greeting
notificationHour/MinuteInt6:30
notificationsEnabledBooltrue
normWindowDaysInt60 (Couzens long-term norm)
acuteWindowDaysInt7 (optional chart line)
dayStartTimezoneStringauto
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

TopicSpec
LocationApp sandbox only
EncryptioniOS Data Protection default; no custom crypto required v3
iCloudOff
BackupParticipates in iCloud device backup unless excluded — v3 accept default; document that iCloud backup may include data if user backs up phone
ExportJSON (full) + CSV (days flat) via UIActivityViewController
Delete allWipe 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)

ModuleCases
RMSSDCalculatorempty, single, monotonic, known fixture, artifact heavy
OrthostaticCalculatorgaps, peak
BaselineCalculatorbuilding, green/yellow/red bands
GateLogicbefore/after questionnaire, day rollover

11.2 Manual test checklist (H10)

  1. Fresh install → onboarding → notification permission
  2. Questionnaire gate blocks History and Start
  3. Connect H10; RR indicator green
  4. Full combined session completes; values plausible (rMSSD typically tens of ms; not 0; not 500)
  5. Kill app mid-session; no corrupt day
  6. Skip orthostatic path
  7. Second launch same day: questionnaire done; can remeasure with confirm overwrite
  8. Next calendar day: gate resets
  9. Export JSON non-empty
  10. 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

MilestoneScopeDone when
M0Xcode project, folder structure, SwiftData models, gate + questionnaire + Today shellUI flow without BLE
M1HeartRateClient + live BPMConnect H10 reliably
M2RMSSD session + persist + HistoryElite replacement usable
M3Combined orthostatic + baselines + notifications + export + testsv3 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)

CodeMessage (draft)
BT_POWERTurn on Bluetooth
BT_DENIEDAllow Bluetooth in Settings
H10_NOT_FOUNDPolar H10 not found — wear it, press button, close other HR apps
H10_NO_RRConnected but no RR intervals — cannot compute HRV
H10_DROPConnection lost — retry
QUALITY_HRVToo much noise — lie still and retry
QUALITY_ORTHOStanding segment invalid — retry orthostatic
GATEComplete 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:

  1. Privacy policy URL
  2. App Review notes: Polar H10 required for full function; demo mode with simulator RR fixture for reviewers
  3. Simulator demo mode: inject synthetic RR for UI review without hardware
  4. Avoid HealthKit until justified
  5. 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)

PRDImplementation
O1 skip orthostaticAllow skip with confirm → HRV-only path §6.2
O2 lying duration30 s lying HR after rMSSD
O3 gap displaygapPeak primary, gapAvg secondary
O4 history gateBlock until questionnaire
O5 postureLying for rMSSD
RemeasureOverwrite 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)

  1. Questionnaire required before measurements and history.
  2. rMSSD computed from H10 RR with documented artifact rules; saved per day.
  3. Orthostatic produces lying avg, standing avg, peak, gaps (or skip recorded).
  4. Baselines: .building until ≥14 complete days; then G/Y/R vs 60-day ±1 SD band.
  5. No network calls in core paths.
  6. Export and delete work.
  7. Unit tests for calculators and gate pass.
  8. 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

RoleStatus
Engineering spec authorComplete 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

ContextTypical protocolPurpose
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 screeningOften 5–10 min supine, then up to 10 min standing with serial HR/BPDiagnose postural tachycardia (≥30 bpm rise sustained)
Autonomic lab studiesSometimes 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, gapsDaily 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

GoalProtocol
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 alignmentPrefer 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 notClaim 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)

IdeaSupport
Compare to personal baseline, not populationCouzens 7d vs 60d; HRV practice generally
±1 SD as “normal range” visualizationCouzens dashboard figures (explicit green band)
HRV related to readiness for higher intensityCouzens / Vesterinen et al. type findings
RHR + orthostatic response contextCouzens quadrants; Hedelin et al. overreaching
Subjective fatigue/mood/soreness/stressCouzens Ch.13; Morgan/Hooper swimming staleness work
Composite > single commercial scoreCouzens 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

Kindv3 intent
Third-party analytics SDK (Amplitude, Firebase Analytics cloud, Mixpanel, etc.)No — privacy-first, no phone-home
On-device analytics / instrumentationYes — 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).

ScaleWhen
1–7Fatigue, mood, soreness, stress, sleep quality (core)
Yes / NoWHOOP-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:

FieldRole
HRVMeasured (not questionnaire)
Pulse / RHRMeasured
FatigueSubjective
MoodSubjective
SorenessSubjective (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:

  1. How you feel / recovery-adjacent ratings (mood, etc., product evolves)
  2. 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).

  1. Fatigue — Exhausted → Fresh
  2. Mood — Very low → Great
  3. Soreness / heavy legs — Very sore → None
  4. Life stress — Very high → Very low

Block B — required short (WHOOP-inspired sleep feel)

  1. 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:

IssueWhy friends hit it
H10 connected to two appsiOS often only streams cleanly to one
Wrong strap fit / dry electrodesNo RR → no rMSSD
Bluetooth permission deniedSilent fail
Backgrounded mid-testSession dies (we’re foreground-only by design)
Non-H10 strapMay 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 HealthKitSense?
Heart rate samples during testLow value (transient; Apple already has workouts from watch)
HRV (SDNN)Possible; Apple’s type is often SDNN-oriented, not rMSSD—mapping is messy
Mindful / sleepNot our domain
Read: sleep analysis, resting HR from watchMaybe later if you drop Oura manual path
Read: workoutsCould 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)

IDDecision
NameRecoveryDeck
Local analyticsYes (on-device only)
Cloud analytics SDKNo (v3)
TP / Intervalsv4+ optional, not v3
Likert1–7 for feeling items
QuestionnaireAlan core + sleep quality; optional last caffeine time + amount; optional last meal time; optional habit chips
Display nameOptional onboarding field
Orthostatic durationKeep short daily proxy (v3 combined ~60 s stand)
HealthKitNot v3 (confirmed)
G/Y/RCouzens-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:

  1. Polar Beat is still connected in the background → phone won’t give clean RR to RecoveryDeck → “no HRV.”
  2. They didn’t enable Bluetooth permission → blank scan.
  3. Strap is loose / dry skin → HR jumps, RR missing → quality fail.
  4. They switch to Messages mid-test → app backgrounds → connection drops.
  5. They use a generic $30 strap that only sends BPM, not RR → we refuse to invent rMSSD.
  6. 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

DocPurpose
docs/index.htmlEasy reading site (open in browser)
PRD.mdProduct requirements
TECH_SPEC.mdEngineering
REVIEW_NOTES_RESPONSE.mdAnswers to review notes (research + decisions)
open docs/index.html

Status

Docs only — not implemented until approved and build is requested.

Hardware

  • iPhone
  • Polar H10

Privacy

Local-first; optional display name; no third-party cloud analytics SDK; yes first-party on-device analytics.