Exercises — code map
API
Controllers
| File | Base path | Notes |
|---|---|---|
apps/api/src/exercises/exercises.controller.ts | /organizations/:orgId/exercises | Standard CRUD + library + overrides |
apps/api/src/exercises/exercise-search.controller.ts | /exercises | GET /search — hybrid search, optional ?orgId |
apps/api/src/exercises/admin-canonical.controller.ts | /admin/exercises/canonical | Platform-admin: seed, enrich, video-discover, video-refresh, video-rank, cues, jobs, video verify/demote |
Services
| File | Responsibility |
|---|---|
apps/api/src/exercises/exercises.service.ts | CRUD, library list, single-read |
apps/api/src/exercises/exercise-overrides.service.ts | Upsert/reset overrides, setMedia (org-scoped demo media; library ids go through the org fork); exports mergeExerciseWithOverride |
apps/api/src/exercises/exercise-media.ts | Effective movement media per org: pickExerciseMedia (the rules) + batched loadExerciseMedia |
apps/api/src/common/workout-section.ts | Member-facing section serializer; movementMedia (per-occurrence media first), sectionExerciseIds (input to one batched media lookup) |
apps/api/src/common/is-media-url.validator.ts | @IsMediaUrl() — https, ≤2048, for DTOs |
apps/api/src/exercises/exercise-search.service.ts | RRF lexical + trgm + semantic search |
apps/api/src/exercises/canonical-seed.service.ts | R2 JSON → canonical upsert + Voyage embed |
apps/api/src/exercises/canonical-seed.processor.ts | BullMQ processor for seed jobs |
apps/api/src/exercises/canonical-enrich.service.ts | Anthropic Haiku Hebrew/Russian alias enrichment |
apps/api/src/exercises/canonical-enrich.processor.ts | BullMQ processor for enrichment jobs |
apps/api/src/exercises/canonical-utils.ts | Slug / equipment / muscle normalization helpers |
apps/api/src/exercises/movement-content-model.ts | MovementContentModel provider interface (TKN-318) + Anthropic implementation; DI token MOVEMENT_CONTENT_MODEL |
apps/api/src/exercises/video-search-provider.ts | VideoSearchProvider interface (TKN-318) + plain-fetch YouTube Data API v3 implementation; DI token VIDEO_SEARCH_PROVIDER; exports parseIso8601DurationSeconds |
apps/api/src/exercises/video-trusted-channels.ts | VIDEO_TRUSTED_CHANNELS — editable, empty-by-default channel allowlist consumed by ranking (prompt hint) and discovery (extra query) |
apps/api/src/exercises/canonical-video-discovery.service.ts | YouTube search pass, appends video_candidates; exports buildDiscoveryQueries, dedupeDiscoveredIds, withinDurationRange |
apps/api/src/exercises/canonical-video-discovery.processor.ts | BullMQ processor for the discovery job; enqueues the ranking job on completion when thenRank: true |
apps/api/src/exercises/canonical-video-ranking.service.ts | Video candidate scoring pass (scores the union of existing + discovered candidates); exports pickBestCandidate, orderByReferenced, excludeManualRows, VIDEO_SCORE_THRESHOLD |
apps/api/src/exercises/canonical-video-ranking.processor.ts | BullMQ processor for the video ranking job |
apps/api/src/exercises/canonical-cues.service.ts | Cues/faults/scaling generation pass; exports needsCuesRebuild |
apps/api/src/exercises/canonical-cues.processor.ts | BullMQ processor for the cues job |
apps/api/src/exercises/canonical-video-moderation.service.ts | verify/demote (admin) + report (org member; one report per user per url, library auto-demote needs reporters from 2+ orgs) |
apps/api/src/exercises/canonical-duplicates.service.ts | Duplicate-candidate detection (embedding + same pattern), alias-collision report (lookup normalization via normalizeExerciseIdentity), variant-qualifier exclusion, suggested-winner ranking; exports findAliasCollisions, isVariantPair, VARIANT_QUALIFIERS, findDuplicateCandidatePairs, suggestWinnerId, DUPLICATE_EMBEDDING_THRESHOLD |
apps/api/src/exercises/canonical-duplicates.processor.ts | BullMQ processor for the duplicates job |
apps/api/src/exercises/canonical-merge.service.ts | Merges an approved duplicate pair — repoints referencing tables, moves/drops overrides, soft-deletes the loser; exports mergeAliasLists, pickWinnerVideoUrl, partitionPersonalRecordConflicts, partitionOverrideConflicts |
DTOs (apps/api/src/exercises/dto/)
create-exercise.dto.ts,update-exercise.dto.ts,upsert-exercise.dto.tscanonical-seed.dto.ts,canonical-enrich.dto.tscanonical-video-ranking.dto.ts,canonical-cues.dto.ts,canonical-video-discovery.dto.tscanonical-duplicates.dto.ts,canonical-merge.dto.ts
Routes
| Route | Method |
|---|---|
GET /organizations/:orgId/exercises | findAll (legacy org-only list) |
GET /organizations/:orgId/exercises/library | findAllForLibrary (merged + paginated) |
GET /organizations/:orgId/exercises/library/:id | findByIdForLibrary |
GET /organizations/:orgId/exercises/:id | findById (accepts canonical) |
POST /organizations/:orgId/exercises | create org-custom |
PATCH /organizations/:orgId/exercises/:id | update org-custom — returns the merged EffectiveExercise shape, same as the library endpoints |
DELETE /organizations/:orgId/exercises/:id | remove org-custom (soft) |
POST /organizations/:orgId/exercises/upsert | upsert by case-insensitive name |
PUT /organizations/:orgId/exercises/:id/override | upsertOverride |
DELETE /organizations/:orgId/exercises/:id/override | deleteOverride (reset) |
POST /organizations/:orgId/exercises/:id/video/report | reportVideo — any org member |
GET /exercises/search | Hybrid search; ?orgId optional |
POST /admin/exercises/canonical/seed | Platform admin |
POST /admin/exercises/canonical/enrich | Platform admin |
POST /admin/exercises/canonical/video/discover | Platform admin |
POST /admin/exercises/canonical/video/refresh | Platform admin — chains discover → rank |
POST /admin/exercises/canonical/video/rank | Platform admin |
POST /admin/exercises/canonical/cues | Platform admin |
POST /admin/exercises/canonical/:id/video/verify | Platform admin |
POST /admin/exercises/canonical/:id/video/demote | Platform admin |
GET /admin/exercises/canonical/jobs | Platform admin |
GET /admin/exercises/canonical/jobs/:queue/:id | Platform admin |
POST /admin/exercises/canonical/duplicates | Platform admin — always dry run |
POST /admin/exercises/canonical/merge | Platform admin — synchronous, { pairs, dryRun } |
Web
Movements is its own destination (/dashboard/movements, sidebar item next to
Workouts) rather than a second tab on the Workouts page — ?tab=library on
/dashboard/workouts redirects there for old links. exercise-autocomplete.tsx
(hybrid search against /exercises/search) is the only picker: the builder, PR
entry, goal creation, and the paste-flow resolution popover all use it.
exercise-library-browser.tsx (legacy org-only list) and exercise-video-dialog.tsx
(its preview dialog) were deleted once nothing imported them.
Components
apps/web/src/app/[lang]/(protected)/dashboard/movements/page.tsx— the routeapps/web/src/components/overview/movements/movements-library-view.tsx— search, “My gym’s” chip, Category/Equipment filtersapps/web/src/components/overview/movements/movements-data-table.tsx— thumbnail/name/equipment/video-status columnsapps/web/src/components/overview/movements/movement-detail-sheet.tsx— single-screen sheet (no tabs): video top, Replace/Report video actions, Cues/Scale it/Common faults, inline “Customize”, collapsed “Advanced”, canonical-only “Reset to Taikan default”apps/web/src/components/overview/movements/movement-video-status.tsx— derives verified/unverified/no-video fromvideoStatus/videoUrlapps/web/src/components/overview/movements/movement-source-badge.tsx—sourceFor/isGymRow+ the small “Your gym” row badgeapps/web/src/components/overview/workouts/exercise-autocomplete.tsx— the one picker (hybrid search)apps/web/src/components/overview/workouts/paste-workout/movement-resolution-popover.tsx— paste-flow resolution; its “search library” action opensexercise-autocomplete.tsx
Admin
apps/admin/src/pages/canonical/show.tsx— seed/enrich/video/cues/thumbnails job cards + jobs table; the “Find duplicates” card (dry run) whose pairs open the merge review.apps/admin/src/pages/canonical/merge-review-modal.tsx— dry-runs every pair againstPOST /admin/exercises/canonical/merge, lets the admin choose the kept movement, merges onlyplannedpairs.apps/admin/src/pages/canonical/alias-collisions-panel.tsx—GET alias-collisionstable; per-term resolve (keep on one movement / remove from all) with dry run, viaPOST alias-change.
E2E
apps/web/e2e/specs/coach-movements-library.spec.ts— movements browse + customize flow.
DB
libs/db/src/lib/schema/workouts.ts (same file as workouts):
exercises— both canonical (org_id null) and org rows.exercise_org_overrides— JSONB diff per (org, exercise).exercise_video_reports— one row per (exercise, user, video url) member report; uniqueexercise_video_reports_reporter_unique.workout_movements.media_url— per-occurrence demo media.exercise_comments— comments on aworkout_movement(lives incomments.ts).
Indexes (exercises):
| Index | Columns / clause |
|---|---|
exercises_slug_unique_idx | UNIQUE on slug WHERE slug IS NOT NULL AND organization_id IS NULL |
exercises_search_tsv_idx | GIN on search_tsv |
exercises_name_trgm_idx | GIN on name gin_trgm_ops (pg_trgm, migration 0039) |
exercises_embedding_idx | HNSW on embedding (vector_cosine_ops) |
exercises_movement_pattern_idx | partial on movement_pattern WHERE not null |
exercises_org_id_idx | on organization_id |
exercises_forked_from_id_idx | partial WHERE not null |
Indexes (exercise_org_overrides):
exercise_org_overrides_org_exercise_uniqueUNIQUE on(organization_id, exercise_id)exercise_org_overrides_org_idxonorganization_id
Every table with an exercise_id FK (the set canonical-merge.service.ts
repoints on a merge): workout_movements.exercise_id,
workout_set_results.exercise_id, personal_records.exercise_id (nullable —
exactly one of exercise_id/library_workout_id set), goals.exercise_id
(nullable — exactly one of exercise_id/metric_type set),
exercise_org_overrides.exercise_id (moved, not repointed — see
behavior.md).
personal_records additionally carries a partial UNIQUE on (user_id, exercise_id) WHERE deleted_at IS NULL, which is what forces the
retire-instead-of-repoint conflict rule on merge.
Shared
libs/shared/src/lib/schemas/exercise.schema.ts— Zod schemas for read responses, category/kind enum values.libs/shared/src/lib/media-kind.ts—MediaKind,MediaSource,EffectiveMedia,mediaKindOf,isValidMediaUrl,MEDIA_URL_MAX_LENGTH,toEffectiveMedia(mobile mirrors it atsrc/lib/media-kind.ts).libs/shared/src/lib/schemas/workout.schema.ts—mediaUrlSchema,effectiveMediaSchema;mediaUrl/mediaon movement schemas.libs/shared/src/lib/agent-schemas/exercises.ts—exercisesSetMediaInputSchema,mediaUrlonexercisesCreateCustomInputSchema.libs/shared/src/lib/exercise-overrides.ts—overridableExerciseFieldKeys(the allowlist),OverridableExerciseFieldstype,EffectiveExercisetype,MovementSourceFilterenum. TODO: verify exact filename.libs/shared/src/lib/agent-schemas/read.ts— agent-facing exercise search query schema (exerciseSearchQuerySchema).
Tests
| File | Type |
|---|---|
apps/api/src/exercises/exercise-overrides.service.unit.spec.ts | Unit — merge logic, override upsert |
apps/api/src/exercises/exercise-search.int.spec.ts | Integration — RRF, lexical/semantic/hybrid modes |
apps/api/src/exercises/canonical-video-ranking.service.unit.spec.ts | Unit — threshold selection, referenced-first ordering, manual-row exclusion |
apps/api/src/exercises/canonical-cues.service.unit.spec.ts | Unit — rebuild gating |
apps/api/src/exercises/canonical-video-moderation.service.unit.spec.ts | Unit — verify/demote fall-through, report dedupe + auto-demote rules |
apps/api/src/exercises/exercise-media.unit.spec.ts | Unit — effective media rules |
libs/shared/src/lib/media-kind.unit.spec.ts | Unit — mediaKindOf, isValidMediaUrl |
apps/api/src/exercises/movement-media.int.spec.ts (+ .int.driver.ts) | Integration — org override isolation, clear → live library fallback, per-occurrence media through set_sections + release, setMedia fork reuse / not-found, report dedupe and the 2-org rule |
apps/api/src/exercises/canonical-video-discovery.service.unit.spec.ts | Unit — dedup vs existing/rejected ids, duration filter, quota stop + resumable report, no-op when key missing, dry run |
apps/api/src/exercises/canonical-video-discovery.processor.unit.spec.ts | Unit — refresh chains discover → rank |
apps/api/src/exercises/canonical-duplicates.service.unit.spec.ts | Unit — normalization, variant-qualifier exclusion, alias/embedding pairing, suggested-winner rule, service wiring |
apps/api/src/exercises/canonical-merge.service.unit.spec.ts | Unit — alias/video merge rules, PR/override conflict partitioning, repoints per referencing table, idempotency, dry run |
apps/api/src/exercises/canonical-merge.service.driver.ts | Driver backing the merge spec — models the Drizzle transaction chain |
apps/web/e2e/specs/coach-movements-library.spec.ts | E2E — library browse + override |