Workout Parse (paste to structure)
The freeform → structured workout transformer (FIT-245 / ADR-0014). A coach pastes workout text they already have (Excel, notes, Instagram captions) and gets the exact set_sections payload the structured builder produces — with a side-by-side preview and a movement-adjustment step before anything is committed.
What
A dedicated, non-conversational parse pipeline in apps/api/src/ai/parse/ (deliberately not the Spotter chat orchestrator — no conversation state, no SSE, no confirm cards). One synchronous POST /organizations/:orgId/workouts/parse call runs:
paste text
→ LLM extraction (Sonnet, ONE forced-tool call): movement mentions + section
structure + char spans. No exercise IDs. No authority over section shape.
→ deterministic grammar: detects canonical shapes (amrap/emom/for_time/tabata/
rep_scheme/rounds/intervals); reconciles with the LLM's guess — grammar wins
→ embedding resolution: each mention → ExerciseSearchService.search (hybrid RRF)
→ top-N candidates + confidence (auto / suggested / unresolved)
→ ParseDraft (IR) persisted to ai_parse_jobs, returned to the client
→ web preview: original text ↔ the real builder canvas, side by side
→ adjust: pick candidate / search library / create org-local exercise / drop-to-note
(decisions persist per job in sessionStorage and carry onto a re-convert)
→ commit: existing WorkoutsService write path; correction delta recorded on the jobCore doctrine: the LLM does the least it can. Under-structure, never mis-structure. Everything deduced is an editable suggestion; unclaimed text becomes notes, never a fabricated section; supersets only on explicit signal (A1/A2, the literal word “superset”).
Why
- Programming is the most time-consuming task in a gym day, and most coaches already have the content as text. Re-typing it into the builder is slow enough that they paste into the freeform field instead — losing shape detection, exercise linking, and analytics.
- The correction delta recorded at commit (
payload-diff.ts) turns “is the parse good?” into a computable product metric: % zero-edit commits, mean edit distance. - The preview reuses the builder’s own editing surface (
SectionEditor+useBuilderState), so committed workouts are indistinguishable from hand-built ones and there is no second editing UI to maintain.
Who
- Owner / admin / coach — full access. Member role is 403’d at the controller.
- Gated by tier: the
workout_buildertier feature (Pro/Elite). See rollout-runbook.md.
Capabilities
- One creator route (D12):
/dashboard/workouts/newopens the freeform form directly. Its “Convert to structured workout” button hands the current description and optional workout id to the parse flow in place viasessionStorage; preview URLs use/dashboard/workouts/new?jobId=. - Server-persisted drafts — every parse leaves an
ai_parse_jobsaudit row with the normalized input text, the IR draft, the auto-resolved payload, cost, and stage timings. Preview survives refresh via?jobId=+ the GET endpoint. - Any-language paste — extraction detects the language and records it on the job row (
language); it never rejects on language. Therejected_non_englishstatus andparse_english_onlycode survive only on historical rows from the English-only v1 gate (D3). - Multi-workout paste: detect + warn + parse first (D4) —
workoutCount > 1parses only the first workout’s span and shows a warning banner. - Same spend meter as Spotter (D9) —
AgentRateLimitService.preCheckbefore the LLM call,AgentCostTracker.recordTurnafter; budget breach → 429ai_budget_exceeded. Spend lands inai_usage_daily. - Fixture playback —
PARSE_EXTRACTION_FIXTURES=1replays recorded extraction outputs (sha256-keyed) so unit tests and e2e never depend on a live model call. - Eval harness — golden corpus + metrics runner (
apps/api/src/ai/parse/eval/,pnpm parse-eval) measuring % resolved, % correct shape, % zero-edit, mean edit distance, latency, and cost per parse. See its README. - Enabling fix — org-local exercise create/update via
POST /exercises/PATCHenqueues embedding enrichment (D10). The adjustment panel creates throughPOST /exercises/upsert(find-or-create by name), which does not enqueue enrichment, so a later parse finds that exercise through the exact-name lookup and the lexical search legs, not the semantic one. - Same rules before and after the server — the review validates the commit payload with the server’s own section validator (
validateSectionsPayload), downgrades timer-only failures to untimed text, and names the section or movement behind any refused commit (reasonon 409parse_review_required).
Related features
workouts— the commit path (WorkoutsService.create/setSections) and the builder UI the preview reuses.exercises— hybrid search (ExerciseSearchService) powering resolution; org-local create for the “create new exercise” adjustment action.spotter-agent— shared building blocks:AnthropicClient, cost tracker, rate limiter,ai_usage_daily.event-tracking— PostHog facade used for theparse.*event taxonomy.
Status
- FIT-245 — shipped for every
workout_builderorg. API parse module + commit/discard, web paste flow, eval harness, e2e coverage all in place. Open: build the dashboard, collect real golden content, run the live eval. See rollout-runbook.md.
Gaps
- Hebrew programming has no eval signal — Hebrew/Russian structure words, units and superset words are covered by deterministic unit specs (structure lexicon, grammar, reconciliation, resolution), and the Capacity goldens carry Hebrew notes, but no golden fixture is a workout programmed in Hebrew, so the eval harness does not measure non-English parse accuracy.
- Multi-workout import (a week’s programming in one paste) is deferred (FIT-38); v1 parses the first workout only.
- Sync POST holds the connection for the whole extraction (one model call, or one per part for a multi-part paste, plus any corrective or escalation call); if volume grows this moves to async + polling. A re-sent unchanged paste is answered from the per-org extraction cache.
- Confidence thresholds (
0.6score /0.1gap) are seeded from the Spotter resolve-batch heuristic, not yet tuned against real content.