# Profile, workout taxonomy & analysis engine foundations **Status:** Draft — approved by user, pending implementation planning **Date:** 2026-07-17 ## Context smartrun currently classifies runs into arbitrary, user-created "workout kinds" using absolute pace/HR thresholds, and has no concept of a user profile — Garmin credentials are passed as environment variables at process start, and there is exactly one hardcoded set of engine parameters. This spec refines that into a fixed running-specific taxonomy, replaces absolute-pace classification with structural and relative-to-self signals, adds warm-up/work/cool-down phase detection with per-phase heart-rate analysis, and introduces a single-profile settings model so all of this is tunable without redeploying. Two related features are explicitly **out of scope** for this spec, per the user's direction to discuss them later once this foundation exists: - **Deep per-phase workout analysis** beyond the HR metrics described in section 5 (the user has more detailed expectations to describe once phase segmentation exists to hang them on). - **Adaptive pace recommendation** ("delta" between the user's declared pace ranges and workout-derived ones) — still undecided whether this needs an AI component or can be fully deterministic. The pace ranges and expected HR zones introduced here exist as **data capture** for that future delta feature; nothing in this spec computes or displays a delta. ## Goals 1. A single-profile settings model: Garmin credentials + all tunable engine parameters, stored in SQLite instead of environment variables, edited through one settings screen. 2. A fixed, 7-type running workout taxonomy replacing the current user-created "workout kinds," each with a user-editable target pace range (no history — the synced activity log *is* the history). 3. Classification driven by structure and by an activity's *relative* standing among its own recent history — never by matching the user's declared target pace ranges. 4. Deterministic warm-up/work/cool-down phase detection per activity, with per-phase heart-rate analysis (zone, drift, basic recovery signal), configurable per workout type. 5. A reusable "classification preview" — every activity scored against every workout type at once — as the shared tool for iteratively tuning natural-language rule definitions against real history. 6. A "recompute" action that re-runs phase detection and reclassification across all activities, since relative metrics and phase parameters can change what a past activity should have been classified as. ## Non-goals - Multi-profile switching / multi-tenant data (explicitly declined — one active profile at a time; the schema does not need per-row profile scoping, a singleton row is sufficient). - The delta/recommendation calculation itself (section above). - Deep per-phase analysis beyond HR zone/drift/basic recovery signal. - Any AI/LLM involvement at runtime — classification and phase detection are fully deterministic, tuned offline against real data before being encoded as rules/parameters. ## 1. Profile A new **`profile`** table, a singleton row (`id = 1`, following the existing `sync_state` pattern), replacing environment-variable Garmin credentials and consolidating all tunable engine parameters: - `garmin_email`, `garmin_password` — moved out of env vars; entered once via the profile screen. - `rolling_window_days` (default `90`) — the population window for relative-classification metrics (section 3). - `max_heart_rate`, `resting_heart_rate` — inputs to the Heart Rate Reserve (Karvonen) calculation used for zone detection (section 5). - Five HR zone ranges, each a `(min_pct, max_pct)` pair of "% of heart rate reserve," seeded with standard defaults (Z1 50–60, Z2 60–70, Z3 70–80, Z4 80–90, Z5 90–100) and user-editable. - Per-workout-type phase-detection parameters (section 4) — e.g. `easy_warmup_minutes`, `easy_cooldown_minutes`, `interval_warmup_minutes`, one pair per non-lap-based type. `internal/config` (env-var loading) shrinks to just `SMARTRUN_ADDR`, `SMARTRUN_DB_PATH`, and the mcp-garmin subprocess paths — everything runtime-tunable moves into `profile`. **Validation** (rejected at save time, not silently accepted): resting HR < max HR; zone ranges ascending and non-overlapping, collectively covering 0–100%. **Credential updates & re-authentication.** `garmin.Client` currently takes a fixed `Config` (including email/password) at construction and lazily spawns the mcp-garmin subprocess on first use — credentials are only ever whatever the process was launched with. Saving new Garmin credentials via the profile screen must: 1. Update the running `garmin.Client`'s credentials in memory. 2. Reset its "started" state and terminate any already-spawned subprocess (which would otherwise still be running under the *old* credentials). The next "Connect to Garmin" click — the connect button and MFA-code entry UI already built for the previous single-env-var-credential model — then spawns a fresh subprocess with the newly-saved credentials and proceeds through the existing authenticate/MFA flow unchanged. No new UI is needed; saving the profile just makes that existing flow reachable at any time, not only right after process launch. ## 2. Workout taxonomy & pace ranges The existing `workout_kinds` table is reseeded with exactly seven fixed rows and loses its "create new" / "delete" affordances in the UI — only `rule_json` (and the new phase/HR fields below) remain editable per type: `Easy Run`, `Long Run`, `Threshold 30'`, `Threshold 60'`, `Tempo`, `Interval`, `MAS Test`. A new **`workout_type_paces`** table, one row per workout kind: - `pace_min_sec_per_km`, `pace_max_sec_per_km` — entered/displayed as `m:ss–m:ss`. - `expected_hr_zone` — which of the 5 zones this type should predominantly sit in (e.g. Easy Run → Zone 2), used later by the per-phase HR check (section 5), not by classification. Neither field has history — overwritten in place when the user updates them, per the user's explicit direction (the activity log is the history). ## 3. Classification: relative & structural metrics New metrics available to `internal/classify`'s condition-tree engine, computed fresh at classification time (no cached/materialized column): - **`distance_percentile`, `duration_percentile`** — this activity's rank (0–1) among all *running* activities (any type except MAS Test) whose start date falls within `rolling_window_days` of *this activity's own date* (not "today" — so reclassifying an old activity is stable and doesn't depend on when reclassification happens to run). Computed via a straightforward SQL ranking query over `activities`. - **`lap_pace_consistency`** — coefficient of variation of pace across the work phase specifically (depends on section 4's phase boundaries), a low value indicating "constant pace" — the structural signal the user described for Long Run, replacing any pace-target comparison. - Existing metrics (`lap_interval_pattern`, `lap_hr_drift_bpm_per_min`, `lap_hr_recovery_bpm_per_min`, `aerobic_training_effect`, etc.) remain available unchanged. **`ReclassifyAll`**: a new bulk operation alongside the existing per-activity `ClassifyActivity`, iterating every stored activity and re-evaluating it. Necessary because relative metrics depend on the whole population — a rolling-window setting change (or a new activity altering others' percentile rank) can change what an old activity should be classified as, not just new ones. Per-activity failures are logged and counted, not fatal to the batch. **Classification preview**: a new read path (`GET /api/classification-preview` or similar) returning, for every activity, *every* workout type's evaluation — matched or not, with score — not just the ones that crossed the confidence threshold (unlike `kind_assignments`, which only records matched candidates). This is the shared tool for the natural-language rule-tuning sessions: change a rule, hit preview, see the whole table shift. ## 4. Phase segmentation A new **`activity_phases`** table: one row per detected phase per activity — `activity_id`, `phase_label` (`warmup` / `work` / `cooldown` for continuous types; `warmup` / `work_1` / `rest_1` / `work_2` / ... / `cooldown` for Interval), `start_elapsed_seconds`, `end_elapsed_seconds`. Two phase-detection strategies, selected per workout type (not one one-size-fits-all algorithm): - **`fixed_duration`** (Easy, Long, Tempo, Threshold-30/60, first pass at MAS Test): warm-up = first *N* configured minutes (`profile`'s per-type setting), cool-down = last *M* configured minutes, work = everything between. - **`lap_intensity`** (Interval): reuses the existing Garmin lap `IntensityType` tagging (ACTIVE/REST) built earlier — warm-up = before the first ACTIVE lap, work = the ACTIVE/REST lap sequence itself (each rep an individually labeled phase), cool-down = after the last one. Phases are computed as part of the existing post-sync detail-fill step (`internal/sync`), stored once, and only recomputed via the explicit recompute action (section 6) — the same lifecycle as classification. **Per-phase heart-rate analysis**, computed from `activity_samples` within each phase's elapsed-time window: - `avg_hr`, `max_hr` over the phase. - `hr_zone` — the phase's average HR mapped to one of the 5 Karvonen zones from `profile`, compared against that workout type's `expected_hr_zone` (section 2) so a mismatch (e.g. an Easy Run run in Zone 4) is visible. - `hr_drift_bpm_per_min` — reuses the existing lap-level drift regression (already generic over any sample window), applied to the phase's window instead of a lap's. - A basic recovery signal for cool-down/rest phases (reusing the existing HR-recovery regression). The more nuanced version the user described — correlating HR fall against recovery *pace*, not HR alone — captures its raw ingredients here (per-phase HR trend + per-phase pace) but the composite "recovery quality" metric itself is deferred to the future deep per-phase analysis discussion. ## 5. Settings panel & frontend - **Profile screen**: Garmin credentials, rolling window, max/resting HR, the 5 zone ranges, per-type phase parameters, and (from section 2) per-type pace ranges + expected HR zone. One screen, one save, validated per section 1. - **Recompute action**: re-runs phase segmentation then `ReclassifyAll` across all activities. Reuses the existing sync-progress-banner pattern (live done/total) since this walks the whole history. - **Classification preview page**: the table from section 3. - **Activity detail page** (new — no per-activity view exists today): pace/HR-vs-time chart (Recharts) with phase bands overlaid as `ReferenceArea`s, plus a per-phase readout (HR zone, drift, avg pace). ## 6. Testing & error handling - Phase detection and the new classification metrics are pure functions tested against fixture sample/lap data, independent of the database or Garmin — same pattern as the existing `internal/classify` tests. - `ReclassifyAll` and the phase-recompute pass are per-activity fault isolated: one activity's failure is logged and counted, not fatal to the batch; the recompute status reports a failure count alongside progress. - Profile save validates HR zone ranges (ascending, non-overlapping, full 0–100% coverage) and resting-HR-less-than-max-HR before persisting, returning a specific error message rather than accepting invalid state. ## Open questions carried forward (not blocking this spec) - Exact per-type natural-language rule definitions and phase parameters — to be tuned interactively against the user's real Garmin history using the classification preview (section 3) once this foundation is built. - The deep per-phase analysis feature beyond HR zone/drift/recovery. - Whether the pace/HR-zone delta recommendation is AI-assisted or fully deterministic.