Files
geniusrun/docs/superpowers/specs/2026-07-17-profile-taxonomy-analysis-engine-design.md

238 lines
12 KiB
Markdown
Raw Normal View History

# Profile, workout taxonomy & analysis engine foundations
**Status:** Draft — approved by user, pending implementation planning
**Date:** 2026-07-17
## Context
geniusrun 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 5060, Z2 6070, Z3 7080,
Z4 8090, Z5 90100) 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 `GENIUSRUN_ADDR`,
`GENIUSRUN_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 0100%.
**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:ssm: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
(01) 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
0100% 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.