Files
geniusrun/docs/superpowers/specs/2026-07-17-profile-taxonomy-analysis-engine-design.md
Christophe Vila f9d85e16ef Rename smartrun to geniusrun throughout the codebase
Updates the Go module path, cmd/smartrund -> cmd/geniusrund, the
smartrun-dev skill, .gitignore, and every reference in docs/CLAUDE.md
to match.
2026-07-24 21:08:07 +02:00

12 KiB
Raw Blame 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 ReferenceAreas, 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.