Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWorkout ParseWorkout Parse — Code Map

Workout Parse — Code Map

API (apps/api/src/ai/parse/)

  • parse.module.ts — self-contained Nest module. Imports MembershipsModule, ExercisesModule, WorkoutsModule; re-declares AnthropicClient, AgentCostTracker, AgentRateLimitService as its own providers (stateless wrappers over global singletons) to avoid a circular AiModule import.
  • parse.controller.ts — the four routes under /organizations/:orgId/workouts/parse (POST, GET :jobId, POST :jobId/commit, POST :jobId/discard); gate ordering; jobToDraftResponse fallback for jobs without a persisted draft.
  • parse-orchestration.service.ts — runParse: normalize → insert job → read (readWhole: one extraction, or readDayByParts: day grain with ≥2 parts — per-part extraction, checks, escalation) → meter spend → language gate → multi-workout slicing → shape/superset reconciliation → resolution → assemble ParseDraft → persist + emit.
  • extraction.service.ts — extract(text, call = MODEL_POLICY.parsePaste, { escalation?, cacheScope? }): one tool call (record_extraction, zod → JSON Schema via z.toJSONSchema) sent with requestParams(call) — the call’s model, cap, thinking and tool choice come from the model policy (parsePaste forces the tool with thinking off; parseDayPart / parseEscalation on Sonnet 5.5, which refuses a forced tool, use tool_choice: auto); options.escalation (the re-read, recorded and cached apart), options.cacheScope (kept answers via AiCacheService); one validation retry (a thinking model’s turn is replayed whole), span sanitization (sanitizeExtractionResult, which also narrows a multi-line movement to its mention line), fixture playback (PARSE_EXTRACTION_FIXTURES=1, extractionFixtureKey(input, escalation) — hashExtractionInput sha256 prefix, the escalation re-read keyed apart). Throws ParseExtractionError.
  • extraction.prompt.ts — EXTRACTION_SYSTEM_PROMPT (adapted from the Spotter cookbook, extraction-only rules) + EXTRACTION_TOOL_NAME. System block cached { type: 'ephemeral', ttl: '1h' }.
  • grammar/shape-grammar.ts — detectShape(headerText, bodyLines, hasMention): pure, deterministic canonical-shape detector (amrap/emom/for_time/rounds/tabata/rep_scheme/intervals) incl. parseDuration helper.
  • grammar/superset-signal.ts — detectSupersetGroups: explicit-signal-only superset grouping.
  • resolution.service.ts — resolveMentions(orgId, mentions, { strict, dayGrain }): cleanMention / normalizeMentionKey (exported — orchestration uses them for lookup parity), dedupe, 4-way concurrency pool, D6 confidence mapping, per-mention error isolation. resolveAlternatives(orgId, options, { primaryExerciseId, existing, strict }): the exact, selecting matches among offered options (“or Bike”, a “Pick one:” list, a swap line), at most four. Also exports attachResolutions (moves “Row or Bike” alternatives onto the prescription) and runWithConcurrency.
  • resolution-vocabulary.ts — the day-grain folds: MOVEMENT_ABBREVIATIONS, MOVEMENT_NOTE_MODIFIERS, non-exercises (isNonExerciseMention), mentionVariants, splitMovementOptions.
  • resolution-confirmation.service.ts — ResolutionConfirmationService.confirm(items): one batched Haiku call that confirms a suggested candidate or none.
  • draft-to-payload.ts — draftToSectionsPayload(draft): auto-only movements (with their mediaUrl); excluded raw lines appended to section description (“Needs review…” block); carries part; block text via draftSectionSource.
  • day-parts.ts — day grain: parsePartHeading, detectDayParts (headings, clock windows, durations, optional, part notes), partSlices (each part’s text for its own extraction), scopeSectionsToParts (split/trim blocks, block notes, tidyPartBlocks: empty/duplicate/mid-line/cut-off blocks), dayBrief. Used only when RunParseArgs.dayGrain.
  • day-part-extraction.ts — shiftExtraction (part offsets → day offsets), stitchPartExtractions, checkDayPart (the per-part structural checks behind escalation and unstructured_part issues).
  • movement-annotations.ts — link-only lines, “Pick one:” lists and “if you don’t have X → sub with Y” lines: blockAnnotationLines (detection, shared by checkDayPart, reconcileProgramming and the represented-source pass), blockAnnotations(draft) (which movement takes each link as mediaUrl and each list as alternatives), QUANTIFIED_LINE. Applied in runParse before resolution.
  • reconcile-programming.ts — source-line reconciliation, incl. calories by sex, @ heights, scaling levels, percent_reference, set-load bands and complexes (group_load).
  • ../agent/tools/leaves/parse.tools.ts — ParseAgentTools, the workouts_parse tool (MCP and Spotter; contract in mcp-server). parse.module.ts exports ParseOrchestrationService, ParseCommitService, ParseObservabilityService for it.
  • parse-commit.service.ts — commit (create-path vs structure-existing-path via WorkoutsService), discard, correction delta via diffSectionsPayload. TKN-320: dto.isLibrary threads into WorkoutsService.create on the create path only; dto.publishTo (when present) calls DailyProgrammingService.set (published, entry: 'paste') in the same request — imports DailyProgrammingModule (parse.module.ts).
  • parse-jobs.repository.ts — thin Drizzle access to ai_parse_jobs (create, update, findById org-scoped).
  • parse-observability.service.ts — emit(stage, props) fan-out to Pino + Sentry breadcrumb + PostHog (production only), scrub() blocklist (no raw text/mentions/titles ever leave the server). Event taxonomy: parse.requested|completed|blocked|failed|committed|discarded (+ reserved parse.exercise_created, currently unused).
  • parse-errors.ts — ParseHttpError ({ code, message, ...extra } wire shape).
  • dto/parse.dto.ts, dto/parse-commit.dto.ts — class-validator DTOs mirroring the shared Zod contracts; commit reuses SectionDto from set-sections.dto.ts verbatim.
  • fixtures/extraction/*.json — recorded extraction fixtures (named by extractionFixtureKey) for playback mode, incl. the per-part recordings of Capacity’s four days.
  • fixtures/day-grain-captures/*.json — live Sonnet 5 whole-day extractions of Capacity’s days that came back wrong (collapsed into one block, folded into the warm-up, multi-line movements, split complex…) plus correct ones; replayed part by part in capacity-day-captures.unit.spec.ts via capacity-day.driver.ts (dayGrainCapture, extractionForPart).
  • capacity-links.driver.ts + capacity-links.unit.spec.ts — Capacity’s blocks with a demo link and offered alternatives (the Lift + Successory of the day refused in production, jobs 43ef0ea0/dfbac910; the hamstring and dips/sled blocks) with a model’s extraction of them, through the harness.

Eval harness (apps/api/src/ai/parse/eval/)

  • run-eval.ts — pnpm parse-eval --org … --user … [--clerk …] [--filter …]; runs the real pipeline per golden fixture (direct service invocation, live or PARSE_EXTRACTION_FIXTURES=1), writes reports/<timestamp>.{md,json} (gitignored).
  • eval-metrics.ts — goldenFixtureSchema, summarizeEval, formatEvalMarkdown.
  • golden/*.golden.json — fixtures incl. one multi-workout and Capacity’s real days (capacity-day-2026-09-27 full day-grain day, capacity-conditioning-levels-2026-09-28, capacity-build-back-squat-3rm; dayGrain: true, expected sections carry part). ../capacity-day.driver.ts replays the model’s extraction for them in capacity-day.unit.spec.ts.
  • README.md — format, recording instructions, how to add real (Erez) content.

Shared (libs/shared/src/lib/parse-schemas/)

  • constants.ts — PARSE_MAX_INPUT_CHARS (10,000), PARSE_AUTO_ACCEPT_SCORE (0.6), PARSE_AMBIGUITY_GAP (0.1), PARSE_CANDIDATES_PER_MENTION (5), PARSE_MAX_SECTIONS (20), PARSE_MAX_MOVEMENTS_PER_SECTION (50), PARSE_PART_CONCURRENCY (4).
  • apps/api/src/ai/model-policy.ts — the model standard (ADR-0022), shared with Spotter: parse roles parsePaste (Sonnet 5, forced tool, thinking off), parseDayPart and parseEscalation (Sonnet 5.5: a plain read, then one reasoned re-read of a failing part), parseMovementRewrite, parseResolutionConfirmation (Haiku 4.5); rates in apps/api/src/ai/cost.util.ts.
  • extraction.schema.ts — extractionResultSchema (the forced tool output), spanSchema.
  • parse-draft.schema.ts — parseDraftSchema + section/movement/candidate/resolution sub-schemas (a movement’s optional mediaUrl: the demo link under it in its block); superRefine validates section config against SECTION_SHAPES[shape].config and the part invariant (findSectionPartIssues). Day-grain fields: section.part, workout.description, parts[{ key, span, noteSpan, escalatedModel? }], structuralIssues[].reason: 'unstructured_part' with partKey + failedChecks (partCheckValues).
  • draft-parts.ts — draftSectionSource(draft, index): sectionSourceFromDraft clipped at the section’s part end.
  • parse-api.schema.ts — parseRequestSchema, parseCommitRequestSchema (TKN-320: isLibrary default true, optional publishTo: { classTypeId, date }), workoutsParseInputSchema (MCP tool input), parseErrorCodeValues, response type aliases.
  • payload-diff.ts — diffSectionsPayload(a, b): PayloadDelta (the correction-delta / zero-edit metric).
  • index.ts — barrel; re-exported from @taikan/shared.

DB

  • libs/db/src/lib/schema/ai-parse.ts — ai_parse_jobs + relations. See data-model.md.
  • Migration generated via pnpm db:generate (applied by CI/deploy, never by hand).

Web (apps/web/src/components/overview/workouts/paste-workout/)

  • Route: apps/web/src/app/[lang]/(protected)/dashboard/workouts/new/page.tsx — freeform default plus the FeatureGate('workout_builder')-wrapped parse state (?mode=translate / ?jobId=).
  • paste-workout-view.tsx — container: compose state, ?jobId= load, handoff intake, resolved-actions tracking. TKN-320: reads the board’s classTypeId/date/classTypeName/sessionCount/returnTo params into a publishTarget; non-blocking publish/save bar (contextual label, saveToLibrary checkbox, “Adjust in builder”) replaces the old blocking commit bar.
  • structure-review-dialog.tsx — the schedule sheets’ in-place variant (full-screen Dialog, no route). Same TKN-320 treatment as the page: publishTarget/onAdjustInBuilder props, non-blocking publish/save bar. Used by session-detail-sheet.tsx and create-session-sheet.tsx’s “Create & structure” flows (CREATE and structure-existing paths); onAdjustInBuilder navigates to the session-slot builder route.
  • paste-input.tsx — textarea (dir="auto"), char counter, submit, per-code inline errors, multi-workout warning banner.
  • parse-preview.tsx — split view; owns useBuilderState. TKN-320’s readOnly prop (used by both containers above) swaps the right pane from the editable SectionEditor tree to the member board (VerbatimWorkout via verbatimSectionsFromBuilder) plus a separate “movements to confirm” panel for outstanding ones — readOnly defaults to false, so the original interactive tree still exists, just unused by any paste-first caller. Collapsed unparsed-remainder block either way.
  • draft-to-whiteboard.ts — needsConfirmation(key, parseMeta, resolvedActions): the outstanding-movement predicate the read-only review and the containers share.
  • finalize-for-publish.ts — TKN-320. autoDropUnresolvedMovements(sections, parseMeta): right before a non-blocking commit, folds any still-null-exercise movement’s raw line into its section’s description (mirrors the manual “drop to note” action) so nothing silently vanishes once buildSectionsPayload filters it out.
  • source-text-pane.tsx — span-highlighted source text, hover-sync with the tree.
  • draft-to-builder.ts — re-exports draftToBuilderSections(draft): SectionState[] from @taikan/shared (libs/shared/src/lib/workout-builder/draft-to-builder.ts) (shape/config → builder timer config, resolution → exercise selection / placeholder).
  • parse-meta.ts — buildParseMeta(draft): sidecar maps movementKey → { resolution, mentionText, rawLine }, sectionKey → { shapeSource, shapeConfidence }.
  • confidence-chip.tsx — auto/suggested/unresolved chips + section low-confidence chip.
  • movement-resolution-popover.tsx — candidate pick / library search / create org-local exercise / drop-to-note.
  • use-parse-commit.ts — builds the final payload via the pure buildSectionsPayload + deriveScoring from @taikan/shared (libs/shared/src/lib/workout-builder/section-payload.ts), calls commit/discard, invalidates workout queries, redirects. commit(...) does not gate on outstandingCount; meta carries isLibrary/publishTo through to the request. The offline eval also uses this same no-edit serializer, then reads the committed workout back for strict golden fixtures.
  • paste-handoff.ts — sessionStorage handoff (writePasteHandoff / readAndClearPasteHandoff) for the “Convert to structured workout” entry.

Entry points: the unified root creator renders freeform-workout-form.tsx (structure-this-button), with ?mode=translate used by contextual schedule links. add-workout-popover.tsx and day-sheet.tsx continue to expose Paste in their own contexts; freeform-workout-form.tsx hands off into the root creator.

i18n: workouts.parse.* namespace in libs/shared/src/lib/i18n/dictionaries/{en,he,ru}.json.

Testing

  • API unit: colocated *.unit.spec.ts for grammar, superset, extraction, resolution, orchestration, controller, commit (incl. TKN-320’s isLibrary/publishTo describe blocks), observability, draft-to-payload, eval-metrics; shared specs next to each parse-schema file. Safety net: apps/api/src/workouts/workouts.service.unit.spec.ts (Task C — guards the commit write path).
  • Web: paste-input.int.spec.tsx, parse-preview.int.spec.tsx, movement-resolution-popover.int.spec.tsx, structure-review-dialog.int.spec.tsx (each with a colocated *.driver.tsx), draft-to-builder.unit.spec.ts, draft-to-whiteboard.unit.spec.ts, use-parse-commit.unit.spec.ts. Entry-point ordering is covered where each chooser/popover already has int coverage: session-detail-sheet.int.spec.tsx (add-workout popover). The day sheet’s entry-point ordering coverage (programming-week-board.int.spec.tsx) was removed along with the standalone programming board — see daily-programming.
  • E2E: apps/web/e2e/specs/paste-workout.spec.ts + apps/web/e2e/drivers/paste-workout.driver.ts.
  • API E2E (day grain): apps/api/test/e2e/day-grain-parse.e2e.spec.ts parses Capacity’s four real days (test/e2e/fixtures/capacity-days.ts — Daily/Build 2026-09-27 and 2026-09-28, three <<>>-delimited, Daily 09-28 plain headings) through MCP workouts_parse, saves, publishes once and reads the athlete’s assignment. It replays the real models’ extractions — one per part, plus any escalated re-read (src/ai/parse/fixtures/extraction/<key>.json, PARSE_EXTRACTION_FIXTURES=1 set by the spec, ANTHROPIC_API_KEY blanked). Re-record after changing the prompt, the texts, the part split or the models (parses each day once live with Sonnet 5.5, a reasoned re-read recorded apart, needs ANTHROPIC_API_KEY in apps/api/.env; writes every extraction the parse asks for, then commit them): CAPACITY_PARSE_RECORD=1 DATABASE_URL=postgresql://postgres:postgres@localhost:55443/taikan_test REDIS_URL=redis://localhost:56390 pnpm -C apps/api exec vitest run --config vitest.e2e.config.ts test/e2e/day-grain-parse.e2e.spec.ts
  • Seed: TestingService.seedParseJob (POST /testing/seed/parse-job) — deterministic draft job (grammar-high AMRAP w/ 2 auto movements, low-confidence section, suggested + unresolved movements, remainder) + guaranteed exercises; exposed as testApi.seedParseJob() in apps/web/e2e/fixtures/test-fixture.ts.