Workout Parse — Code Map
API (apps/api/src/ai/parse/)
parse.module.ts— self-contained Nest module. ImportsMembershipsModule,ExercisesModule,WorkoutsModule; re-declaresAnthropicClient,AgentCostTracker,AgentRateLimitServiceas its own providers (stateless wrappers over global singletons) to avoid a circularAiModuleimport.parse.controller.ts— the four routes under/organizations/:orgId/workouts/parse(POST, GET:jobId, POST:jobId/commit, POST:jobId/discard); gate ordering;jobToDraftResponsefallback for jobs without a persisted draft.parse-orchestration.service.ts—runParse: normalize → insert job → read (readWhole: one extraction, orreadDayByParts: day grain with ≥2 parts — per-part extraction, checks, escalation) → meter spend → language gate → multi-workout slicing → shape/superset reconciliation → resolution → assembleParseDraft→ persist + emit.extraction.service.ts—extract(text, call = MODEL_POLICY.parsePaste, { escalation?, cacheScope? }): one tool call (record_extraction,zod → JSON Schemaviaz.toJSONSchema) sent withrequestParams(call)— the call’s model, cap, thinking and tool choice come from the model policy (parsePasteforces the tool with thinking off;parseDayPart/parseEscalationon Sonnet 5.5, which refuses a forced tool, usetool_choice: auto);options.escalation(the re-read, recorded and cached apart),options.cacheScope(kept answers viaAiCacheService); 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)—hashExtractionInputsha256 prefix, the escalation re-read keyed apart). ThrowsParseExtractionError.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.parseDurationhelper.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 exportsattachResolutions(moves “Row or Bike” alternatives onto the prescription) andrunWithConcurrency.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 theirmediaUrl); excluded raw lines appended to section description (“Needs review…” block); carriespart; block text viadraftSectionSource.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 whenRunParseArgs.dayGrain.day-part-extraction.ts—shiftExtraction(part offsets → day offsets),stitchPartExtractions,checkDayPart(the per-part structural checks behind escalation andunstructured_partissues).movement-annotations.ts— link-only lines, “Pick one:” lists and “if you don’t have X → sub with Y” lines:blockAnnotationLines(detection, shared bycheckDayPart,reconcileProgrammingand the represented-source pass),blockAnnotations(draft)(which movement takes each link asmediaUrland each list as alternatives),QUANTIFIED_LINE. Applied inrunParsebefore resolution.reconcile-programming.ts— source-line reconciliation, incl. calories by sex,@heights, scalinglevels,percent_reference, set-load bands and complexes (group_load).../agent/tools/leaves/parse.tools.ts—ParseAgentTools, theworkouts_parsetool (MCP and Spotter; contract in mcp-server).parse.module.tsexportsParseOrchestrationService,ParseCommitService,ParseObservabilityServicefor it.parse-commit.service.ts—commit(create-path vs structure-existing-path viaWorkoutsService),discard, correction delta viadiffSectionsPayload. TKN-320:dto.isLibrarythreads intoWorkoutsService.createon the create path only;dto.publishTo(when present) callsDailyProgrammingService.set(published,entry: 'paste') in the same request — importsDailyProgrammingModule(parse.module.ts).parse-jobs.repository.ts— thin Drizzle access toai_parse_jobs(create,update,findByIdorg-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(+ reservedparse.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 reusesSectionDtofromset-sections.dto.tsverbatim.fixtures/extraction/*.json— recorded extraction fixtures (named byextractionFixtureKey) 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 incapacity-day-captures.unit.spec.tsviacapacity-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 orPARSE_EXTRACTION_FIXTURES=1), writesreports/<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-27full day-grain day,capacity-conditioning-levels-2026-09-28,capacity-build-back-squat-3rm;dayGrain: true, expected sections carrypart).../capacity-day.driver.tsreplays the model’s extraction for them incapacity-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 rolesparsePaste(Sonnet 5, forced tool, thinking off),parseDayPartandparseEscalation(Sonnet 5.5: a plain read, then one reasoned re-read of a failing part),parseMovementRewrite,parseResolutionConfirmation(Haiku 4.5); rates inapps/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 optionalmediaUrl: the demo link under it in its block);superRefinevalidates sectionconfigagainstSECTION_SHAPES[shape].configand the part invariant (findSectionPartIssues). Day-grain fields:section.part,workout.description,parts[{ key, span, noteSpan, escalatedModel? }],structuralIssues[].reason: 'unstructured_part'withpartKey+failedChecks(partCheckValues).draft-parts.ts—draftSectionSource(draft, index):sectionSourceFromDraftclipped at the section’s part end.parse-api.schema.ts—parseRequestSchema,parseCommitRequestSchema(TKN-320:isLibrarydefaulttrue, optionalpublishTo: { 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 theFeatureGate('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’sclassTypeId/date/classTypeName/sessionCount/returnToparams into apublishTarget; non-blocking publish/save bar (contextual label,saveToLibrarycheckbox, “Adjust in builder”) replaces the old blocking commit bar.structure-review-dialog.tsx— the schedule sheets’ in-place variant (full-screenDialog, no route). Same TKN-320 treatment as the page:publishTarget/onAdjustInBuilderprops, non-blocking publish/save bar. Used bysession-detail-sheet.tsxandcreate-session-sheet.tsx’s “Create & structure” flows (CREATE and structure-existing paths);onAdjustInBuildernavigates 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; ownsuseBuilderState. TKN-320’sreadOnlyprop (used by both containers above) swaps the right pane from the editableSectionEditortree to the member board (VerbatimWorkoutviaverbatimSectionsFromBuilder) plus a separate “movements to confirm” panel for outstanding ones —readOnlydefaults tofalse, 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 oncebuildSectionsPayloadfilters it out.source-text-pane.tsx— span-highlighted source text, hover-sync with the tree.draft-to-builder.ts— re-exportsdraftToBuilderSections(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 mapsmovementKey → { resolution, mentionText, rawLine },sectionKey → { shapeSource, shapeConfidence }.confidence-chip.tsx—auto/suggested/unresolvedchips + 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 purebuildSectionsPayload+deriveScoringfrom@taikan/shared(libs/shared/src/lib/workout-builder/section-payload.ts), calls commit/discard, invalidates workout queries, redirects.commit(...)does not gate onoutstandingCount;metacarriesisLibrary/publishTothrough 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.tsfor grammar, superset, extraction, resolution, orchestration, controller, commit (incl. TKN-320’sisLibrary/publishTodescribe 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.tsparses 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 MCPworkouts_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=1set by the spec,ANTHROPIC_API_KEYblanked). 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, needsANTHROPIC_API_KEYinapps/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 astestApi.seedParseJob()inapps/web/e2e/fixtures/test-fixture.ts.