hevy-shared 1.0.1037 → 1.0.1039

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
- import { WeeklyTrainingFrequency, TrainingGoal, TrainingLevel, SimplifiedMuscleGroup, MuscleGroup, LibraryExercise, ExerciseCategory, GranularEquipment, HevyTrainerProgramEquipment, RestTimerLength, CardioPreference } from '.';
1
+ import { WeeklyTrainingFrequency, TrainingGoal, TrainingLevel, SimplifiedMuscleGroup, MuscleGroup, LibraryExercise, ExerciseCategory, GranularEquipment, HevyTrainerProgramEquipment, RestTimerLength, CardioPreference, StrictRepRange, ExerciseType } from '.';
2
2
  export type HevyTrainerExerciseCategory = (typeof hevyTrainerExerciseCategories)[number];
3
- export type HevyTrainerRoutineName = (typeof routineNames)[number];
3
+ export type TrainerWorkoutTemplateName = (typeof workoutTemplateNames)[number];
4
4
  export declare const workoutDurationOptions: readonly [40, 60, 80];
5
5
  export type WorkoutDurationMinutes = (typeof workoutDurationOptions)[number];
6
6
  export declare const trainerGymTypes: readonly ["home_gym", "garage_gym", "commercial_gym", "full_gym"];
@@ -16,12 +16,12 @@ export declare const trainerEquipmentToGranularEquipments: (equipments: HevyTrai
16
16
  export declare const granularEquipmentsToTrainerEquipments: (granularEquipments: GranularEquipment[]) => HevyTrainerProgramEquipment[];
17
17
  export declare const hevyTrainerExerciseCategories: readonly ["compound", "isolation"];
18
18
  export declare const defaultDurationPerFrequency: Record<WeeklyTrainingFrequency, WorkoutDurationMinutes>;
19
- export declare const routineNames: readonly ["full_body_1", "full_body_2_a", "full_body_2_b", "full_body_3_a", "full_body_3_b", "full_body_3_c", "upper_1_a", "lower_1_a", "upper_1_b", "lower_1_b", "push_1", "pull_1", "legs_1", "upper_2", "lower_2", "push_2_a", "pull_2_a", "legs_2_a", "push_2_b", "pull_2_b", "legs_2_b"];
19
+ export declare const workoutTemplateNames: readonly ["full_body_1", "full_body_2_a", "full_body_2_b", "full_body_3_a", "full_body_3_b", "full_body_3_c", "upper_1_a", "lower_1_a", "upper_1_b", "lower_1_b", "push_1", "pull_1", "legs_1", "upper_2", "lower_2", "push_2_a", "pull_2_a", "legs_2_a", "push_2_b", "pull_2_b", "legs_2_b"];
20
20
  export type exerciseId = string;
21
21
  export interface ExerciseSelectionCriteria {
22
22
  exerciseCategory: HevyTrainerExerciseCategory | 'all';
23
23
  equipments: GranularEquipment[];
24
- routineBarbellExerciseCount: number;
24
+ workoutBarbellExerciseCount: number;
25
25
  level: TrainingLevel;
26
26
  goal: TrainingGoal;
27
27
  muscleGroup: MuscleGroup;
@@ -29,7 +29,7 @@ export interface ExerciseSelectionCriteria {
29
29
  }
30
30
  export interface ExerciseSelectionContext {
31
31
  programUsedExerciseIds?: Set<string>;
32
- routineUsedExerciseIds?: Set<string>;
32
+ workoutUsedExerciseIds?: Set<string>;
33
33
  excludedExerciseIds?: Set<string>;
34
34
  }
35
35
  export interface ExerciseSelectionParams<T extends HevyTrainerLibraryExercise> {
@@ -69,7 +69,7 @@ export interface ProgramGenerationParams<T extends HevyTrainerLibraryExercise> {
69
69
  }
70
70
  export interface TemplateExerciseSelectionTraceRecord {
71
71
  traceKind: 'template';
72
- routine: HevyTrainerRoutineName;
72
+ workoutTemplate: TrainerWorkoutTemplateName;
73
73
  prescriptionIndex: number;
74
74
  prescription: ExercisePrescription;
75
75
  resolvedMuscleGroup: MuscleGroup;
@@ -79,7 +79,7 @@ export interface TemplateExerciseSelectionTraceRecord {
79
79
  }
80
80
  export interface CardioExerciseSelectionTraceRecord {
81
81
  traceKind: 'cardio';
82
- routine: HevyTrainerRoutineName;
82
+ workoutTemplate: TrainerWorkoutTemplateName;
83
83
  prescriptionIndex: number;
84
84
  selectedExerciseId?: string;
85
85
  equipments: GranularEquipment[];
@@ -92,19 +92,19 @@ export type TrainerProgramAttemptWithTraces = TrainerProgramAttempt & {
92
92
  };
93
93
  export type FrequencyString = 'one_day' | 'two_days' | 'three_days' | 'four_days' | 'five_days' | 'six_days';
94
94
  export declare const frequencyMap: Record<WeeklyTrainingFrequency, FrequencyString>;
95
- export declare const programSplits: Record<WeeklyTrainingFrequency, HevyTrainerRoutineName[]>;
95
+ export declare const programSplits: Record<WeeklyTrainingFrequency, TrainerWorkoutTemplateName[]>;
96
96
  export type SetsPerGoal = {
97
97
  [key in TrainingGoal]: number;
98
98
  };
99
99
  export type SetsPerFrequency = {
100
100
  [key in FrequencyString]: SetsPerGoal;
101
101
  };
102
- export interface RepRange {
102
+ export interface AlgorithmRepRange {
103
103
  rep_range_start: number;
104
104
  rep_range_end: number;
105
105
  }
106
106
  export type RepRangesPerCategory = {
107
- [key in HevyTrainerExerciseCategory]: RepRange;
107
+ [key in HevyTrainerExerciseCategory]: AlgorithmRepRange;
108
108
  };
109
109
  export type RepRanges = {
110
110
  [key in TrainingGoal]: RepRangesPerCategory;
@@ -135,10 +135,11 @@ export type ExerciseReplacements = {
135
135
  };
136
136
  export interface WorkoutTemplate {
137
137
  exercises: ExercisePrescription[];
138
+ /** @deprecated Because this is english-only and not translatable so we are not using it currently */
138
139
  notes?: string;
139
140
  }
140
141
  export type Templates = {
141
- [key in HevyTrainerRoutineName]: WorkoutTemplate;
142
+ [key in TrainerWorkoutTemplateName]: WorkoutTemplate;
142
143
  };
143
144
  export interface BackofficeTrainerPreset {
144
145
  id?: number;
@@ -157,39 +158,40 @@ export interface TrainerAlgorithmSettings {
157
158
  exercise_notes: ExerciseNotes;
158
159
  exercise_replacements: ExerciseReplacements;
159
160
  }
160
- /** Resistance prescriptions from templates (sets, reps, rest, notes). */
161
161
  export interface TrainerProgramResistanceExercise {
162
162
  kind: 'resistance';
163
- exerciseTemplate: HevyTrainerLibraryExercise;
164
- muscleGroup: MuscleGroup | 'focus_muscle';
165
- category: HevyTrainerExerciseCategory;
166
- sets: number;
167
- warmupSetCount?: number;
168
- repRangeStart: number;
169
- repRangeEnd: number;
170
- restTimerSeconds: number;
171
- notes?: string;
163
+ exercise_template: HevyTrainerLibraryExercise;
164
+ set_count: number;
165
+ warmup_set_count: number;
166
+ rep_range: StrictRepRange;
167
+ rest_seconds: number;
172
168
  }
173
- /** Optional cardio attachment — no prescription semantics beyond exercise choice. */
174
169
  export interface TrainerProgramCardioExercise {
175
170
  kind: 'cardio';
176
- exerciseTemplate: HevyTrainerLibraryExercise;
177
- durationSeconds: number;
171
+ exercise_template: HevyTrainerLibraryExercise;
172
+ set_count: 1;
173
+ duration_seconds: number;
178
174
  }
179
- export type TrainerProgramExercise = TrainerProgramResistanceExercise | TrainerProgramCardioExercise;
175
+ export interface TrainerProgramOtherExercise {
176
+ kind: 'other';
177
+ exercise_template: HevyTrainerLibraryExercise;
178
+ set_count: number;
179
+ }
180
+ export type TrainerProgramExercise = TrainerProgramResistanceExercise | TrainerProgramCardioExercise | TrainerProgramOtherExercise;
180
181
  export declare const isTrainerProgramResistanceExercise: (exercise: TrainerProgramExercise) => exercise is TrainerProgramResistanceExercise;
181
182
  export declare const isTrainerProgramCardioExercise: (exercise: TrainerProgramExercise) => exercise is TrainerProgramCardioExercise;
182
- export interface TrainerProgramRoutine {
183
- name: HevyTrainerRoutineName;
183
+ export declare const isTrainerProgramOtherExercise: (exercise: TrainerProgramExercise) => exercise is TrainerProgramOtherExercise;
184
+ export declare const isTrainerResistanceExerciseType: (exerciseType: ExerciseType) => exerciseType is "weight_reps" | "reps_only" | "bodyweight_reps" | "bodyweight_assisted_reps";
185
+ export declare const isTrainerCardioExerciseType: (exerciseType: ExerciseType) => exerciseType is "duration" | "distance_duration" | "floors_duration" | "steps_duration";
186
+ export interface TrainerProgramWorkoutTemplate {
187
+ name: TrainerWorkoutTemplateName;
184
188
  exercises: TrainerProgramExercise[];
185
- notes?: string;
186
189
  }
187
- export interface TrainerProgram {
188
- name: WeeklyTrainingFrequency;
189
- routines: TrainerProgramRoutine[];
190
+ export interface GeneratedTrainerProgram {
191
+ workoutTemplates: TrainerProgramWorkoutTemplate[];
190
192
  }
191
193
  export declare const getTrainerSetCount: (trainerAlgorithmSettings: TrainerAlgorithmSettings, goal: TrainingGoal, frequency: WeeklyTrainingFrequency) => number;
192
- export declare const getTrainerRepRange: (trainerAlgorithmSettings: TrainerAlgorithmSettings, goal: TrainingGoal, exerciseCategory: HevyTrainerExerciseCategory) => RepRange;
194
+ export declare const getTrainerRepRange: (trainerAlgorithmSettings: TrainerAlgorithmSettings, goal: TrainingGoal, exerciseCategory: HevyTrainerExerciseCategory) => AlgorithmRepRange;
193
195
  export declare const getTrainerRestTimerSeconds: (trainerAlgorithmSettings: TrainerAlgorithmSettings, goal: TrainingGoal, length: RestTimerLength, exerciseCategory: HevyTrainerExerciseCategory) => number;
194
196
  /**
195
197
  * Normalizes the exercise category to a HevyTrainerExerciseCategory
@@ -243,23 +245,14 @@ export interface ExercisePrescriptionError {
243
245
  focusMuscle?: SimplifiedMuscleGroup;
244
246
  };
245
247
  }
246
- export interface TrainerProgramExerciseResult {
247
- success: true;
248
- exercise: TrainerProgramExercise;
249
- }
250
- export interface TrainerProgramExerciseError {
251
- success: false;
252
- error: ExercisePrescriptionError;
253
- }
254
- export type TrainerProgramExerciseAttempt = TrainerProgramExerciseResult | TrainerProgramExerciseError;
255
248
  export interface TrainerProgramResult {
256
249
  success: true;
257
- program: TrainerProgram;
250
+ program: GeneratedTrainerProgram;
258
251
  }
259
252
  export interface TrainerProgramError {
260
253
  success: false;
261
254
  errors: ExercisePrescriptionError[];
262
- partialProgram: TrainerProgram;
255
+ partialProgram: GeneratedTrainerProgram;
263
256
  }
264
257
  export type TrainerProgramAttempt = TrainerProgramResult | TrainerProgramError;
265
258
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS = exports.getPrioritySortedExercises = exports.isEquipmentCompatible = exports.normalizeExerciseCategory = exports.getTrainerRestTimerSeconds = exports.getTrainerRepRange = exports.getTrainerSetCount = exports.isTrainerProgramCardioExercise = exports.isTrainerProgramResistanceExercise = exports.programSplits = exports.frequencyMap = exports.routineNames = exports.defaultDurationPerFrequency = exports.hevyTrainerExerciseCategories = exports.granularEquipmentsToTrainerEquipments = exports.trainerEquipmentToGranularEquipments = exports.granularEquipmentDefaults = exports.trainerGymTypes = exports.workoutDurationOptions = void 0;
3
+ exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS = exports.getPrioritySortedExercises = exports.isEquipmentCompatible = exports.normalizeExerciseCategory = exports.getTrainerRestTimerSeconds = exports.getTrainerRepRange = exports.getTrainerSetCount = exports.isTrainerCardioExerciseType = exports.isTrainerResistanceExerciseType = exports.isTrainerProgramOtherExercise = exports.isTrainerProgramCardioExercise = exports.isTrainerProgramResistanceExercise = exports.programSplits = exports.frequencyMap = exports.workoutTemplateNames = exports.defaultDurationPerFrequency = exports.hevyTrainerExerciseCategories = exports.granularEquipmentsToTrainerEquipments = exports.trainerEquipmentToGranularEquipments = exports.granularEquipmentDefaults = exports.trainerGymTypes = exports.workoutDurationOptions = void 0;
4
4
  exports.pickTrainerExercise = pickTrainerExercise;
5
5
  exports.generateProgram = generateProgram;
6
6
  const _1 = require(".");
@@ -152,7 +152,7 @@ exports.defaultDurationPerFrequency = {
152
152
  5: 40,
153
153
  6: 40,
154
154
  };
155
- exports.routineNames = [
155
+ exports.workoutTemplateNames = [
156
156
  // Full body 1x
157
157
  'full_body_1',
158
158
  // Full body 2x
@@ -201,6 +201,18 @@ const isTrainerProgramResistanceExercise = (exercise) => exercise.kind === 'resi
201
201
  exports.isTrainerProgramResistanceExercise = isTrainerProgramResistanceExercise;
202
202
  const isTrainerProgramCardioExercise = (exercise) => exercise.kind === 'cardio';
203
203
  exports.isTrainerProgramCardioExercise = isTrainerProgramCardioExercise;
204
+ const isTrainerProgramOtherExercise = (exercise) => exercise.kind === 'other';
205
+ exports.isTrainerProgramOtherExercise = isTrainerProgramOtherExercise;
206
+ const isTrainerResistanceExerciseType = (exerciseType) => exerciseType === 'weight_reps' ||
207
+ exerciseType === 'bodyweight_reps' ||
208
+ exerciseType === 'bodyweight_assisted_reps' ||
209
+ exerciseType === 'reps_only';
210
+ exports.isTrainerResistanceExerciseType = isTrainerResistanceExerciseType;
211
+ const isTrainerCardioExerciseType = (exerciseType) => exerciseType === 'distance_duration' ||
212
+ exerciseType === 'duration' ||
213
+ exerciseType === 'floors_duration' ||
214
+ exerciseType === 'steps_duration';
215
+ exports.isTrainerCardioExerciseType = isTrainerCardioExerciseType;
204
216
  const getTrainerSetCount = (trainerAlgorithmSettings, goal, frequency) => {
205
217
  return trainerAlgorithmSettings.sets[exports.frequencyMap[frequency]][goal];
206
218
  };
@@ -291,12 +303,12 @@ const MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK = 3;
291
303
  * - Only enforce the cap when the user has at least one "barbell substitute" equipment available
292
304
  * (otherwise we allow barbell exercises to avoid running out of viable options)
293
305
  */
294
- const isBarbellExerciseAllowed = (exercise, routineBarbellExerciseCount, userEquipments, frequency) => {
306
+ const isBarbellExerciseAllowed = (exercise, workoutBarbellExerciseCount, userEquipments, frequency) => {
295
307
  const isCandidateBarbell = exercise.equipment_category === 'barbell';
296
308
  const isOncePerWeek = frequency === 1;
297
309
  if (!isCandidateBarbell || !isOncePerWeek)
298
310
  return true;
299
- const isAtOrOverLimit = routineBarbellExerciseCount >= MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK;
311
+ const isAtOrOverLimit = workoutBarbellExerciseCount >= MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK;
300
312
  if (!isAtOrOverLimit)
301
313
  return true;
302
314
  const barbellSubstitutes = [
@@ -314,7 +326,7 @@ const isBarbellExerciseAllowed = (exercise, routineBarbellExerciseCount, userEqu
314
326
  const isExerciseUsed = (exercise, context) => {
315
327
  var _a, _b, _c;
316
328
  return (((_a = context.programUsedExerciseIds) === null || _a === void 0 ? void 0 : _a.has(exercise.id)) ||
317
- ((_b = context.routineUsedExerciseIds) === null || _b === void 0 ? void 0 : _b.has(exercise.id)) ||
329
+ ((_b = context.workoutUsedExerciseIds) === null || _b === void 0 ? void 0 : _b.has(exercise.id)) ||
318
330
  ((_c = context.excludedExerciseIds) === null || _c === void 0 ? void 0 : _c.has(exercise.id)) ||
319
331
  false);
320
332
  };
@@ -393,16 +405,16 @@ const getPrioritySortedExercises = (exercisePriorities, exerciseStore) => {
393
405
  return sortedExercises;
394
406
  };
395
407
  exports.getPrioritySortedExercises = getPrioritySortedExercises;
396
- const getMuscleGroup = ({ muscleGroupPrescription, programFocusMuscleExerciseCount, focusMuscle, routineName, }) => {
408
+ const getMuscleGroup = ({ muscleGroupPrescription, programFocusMuscleExerciseCount, focusMuscle, workoutTemplateName, }) => {
397
409
  if (muscleGroupPrescription === 'focus_muscle') {
398
- // If the user has selected a focus muscle and it is allowed for the routine,
410
+ // If the user has selected a focus muscle and it is allowed for the workout template,
399
411
  // we return the focus muscle extra exercise
400
412
  if (!!focusMuscle &&
401
- isFocusMuscleExtraExerciseAllowed(routineName, focusMuscle)) {
413
+ isFocusMuscleExtraExerciseAllowed(workoutTemplateName, focusMuscle)) {
402
414
  const n = _1.simplifiedMuscleGroupToMuscleGroups[focusMuscle].length;
403
415
  return _1.simplifiedMuscleGroupToMuscleGroups[focusMuscle][programFocusMuscleExerciseCount % n];
404
416
  }
405
- // If the user has not selected a focus muscle or it is not allowed for the routine
417
+ // If the user has not selected a focus muscle or it is not allowed for the workout template
406
418
  // we skip this extra exercise for the focus muscle in the program
407
419
  return undefined;
408
420
  }
@@ -420,7 +432,7 @@ const isExerciseMatch = (exercise, criteria) => {
420
432
  const levelMatch = (_b = (_a = exercise.level) === null || _a === void 0 ? void 0 : _a.includes(criteria.level)) !== null && _b !== void 0 ? _b : false;
421
433
  const goalMatch = (_d = (_c = exercise.goal) === null || _c === void 0 ? void 0 : _c.includes(criteria.goal)) !== null && _d !== void 0 ? _d : false;
422
434
  const equipmentMatch = (0, exports.isEquipmentCompatible)(exercise, criteria.equipments);
423
- const barbellLimitOk = isBarbellExerciseAllowed(exercise, criteria.routineBarbellExerciseCount, criteria.equipments, criteria.frequency);
435
+ const barbellLimitOk = isBarbellExerciseAllowed(exercise, criteria.workoutBarbellExerciseCount, criteria.equipments, criteria.frequency);
424
436
  return (categoryMatch && levelMatch && goalMatch && equipmentMatch && barbellLimitOk);
425
437
  };
426
438
  /**
@@ -465,8 +477,8 @@ const pickTrainerExerciseWithTrace = (params) => {
465
477
  programUsedExerciseIds: context.programUsedExerciseIds,
466
478
  excludedExerciseIds: context.excludedExerciseIds,
467
479
  };
468
- const routineContext = {
469
- routineUsedExerciseIds: context.routineUsedExerciseIds,
480
+ const workoutContext = {
481
+ workoutUsedExerciseIds: context.workoutUsedExerciseIds,
470
482
  excludedExerciseIds: context.excludedExerciseIds,
471
483
  };
472
484
  // Pass 1
@@ -480,10 +492,10 @@ const pickTrainerExerciseWithTrace = (params) => {
480
492
  if (exercise)
481
493
  return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 1 }) };
482
494
  // Pass 2
483
- exercise = findMatchingExercise(exercises, criteria, routineContext);
495
+ exercise = findMatchingExercise(exercises, criteria, workoutContext);
484
496
  trace.entries.push({
485
497
  pass: 2,
486
- label: 'exact match; not used in routine (reuse allowed in program)',
498
+ label: 'exact match; not used in the same workout (reuse allowed in program)',
487
499
  candidatePoolSize: exercises.length,
488
500
  selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
489
501
  });
@@ -500,20 +512,20 @@ const pickTrainerExerciseWithTrace = (params) => {
500
512
  if (exercise)
501
513
  return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 3 }) };
502
514
  // Pass 4
503
- exercise = findAlternativeIsolationExercise(exercises, criteria, routineContext);
515
+ exercise = findAlternativeIsolationExercise(exercises, criteria, workoutContext);
504
516
  trace.entries.push({
505
517
  pass: 4,
506
- label: 'handpicked alternative isolation; not used in routine (reuse allowed in program)',
518
+ label: 'handpicked alternative isolation; not used in the same workout (reuse allowed in program)',
507
519
  candidatePoolSize: exercises.length,
508
520
  selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
509
521
  });
510
522
  if (exercise)
511
523
  return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 4 }) };
512
524
  // Pass 5
513
- exercise = findMatchingExercise(exercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), routineContext);
525
+ exercise = findMatchingExercise(exercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), workoutContext);
514
526
  trace.entries.push({
515
527
  pass: 5,
516
- label: 'match regardless of isolation or compound; not used in routine (reuse allowed in program)',
528
+ label: 'match regardless of isolation or compound; not used in the same workout (reuse allowed in program)',
517
529
  candidatePoolSize: exercises.length,
518
530
  selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
519
531
  });
@@ -523,10 +535,10 @@ const pickTrainerExerciseWithTrace = (params) => {
523
535
  const secondaryMuscleExercises = Object.values(sortedExercises)
524
536
  .flat()
525
537
  .filter((e) => e.other_muscles.includes(criteria.muscleGroup));
526
- exercise = findMatchingExercise(secondaryMuscleExercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), routineContext);
538
+ exercise = findMatchingExercise(secondaryMuscleExercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), workoutContext);
527
539
  trace.entries.push({
528
540
  pass: 6,
529
- label: 'secondary muscle match; regardless of isolation or compound; not used in routine (reuse allowed in program)',
541
+ label: 'secondary muscle match; regardless of isolation or compound; not used in the same workout (reuse allowed in program)',
530
542
  candidatePoolSize: secondaryMuscleExercises.length,
531
543
  selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
532
544
  });
@@ -542,11 +554,11 @@ function pickTrainerExercise(params) {
542
554
  }
543
555
  const CARDIO_TRACE_INDEX_BEFORE_TEMPLATE = -1;
544
556
  exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS = 600;
545
- /** Inserts optional cardio before or after template exercises for one routine. */
546
- const attachOptionalCardioToRoutine = ({ cardioPreference, sortedExercises, criteria, debugExerciseSelectionTrace, exerciseSelectionTraces, routine, templatePrescriptionCount, routineExercises, }) => {
557
+ /** Inserts optional cardio before or after template exercises for one workout template. */
558
+ const attachOptionalCardioToWorkoutTemplate = ({ cardioPreference, sortedExercises, criteria, debugExerciseSelectionTrace, exerciseSelectionTraces, workoutTemplate, templatePrescriptionCount, workoutExercises, }) => {
547
559
  if (cardioPreference === 'no-cardio')
548
560
  return;
549
- // Cardio is reusable across routines, so program/routine used-id contexts are
561
+ // Cardio is reusable across workout templates, so program/workoutTemplate used-id contexts are
550
562
  // intentionally omitted — only the global excluded set applies.
551
563
  const selectionParams = {
552
564
  sortedExercises,
@@ -557,7 +569,7 @@ const attachOptionalCardioToRoutine = ({ cardioPreference, sortedExercises, crit
557
569
  equipments: criteria.equipments,
558
570
  muscleGroup: 'cardio',
559
571
  exerciseCategory: 'all',
560
- routineBarbellExerciseCount: 0,
572
+ workoutBarbellExerciseCount: 0,
561
573
  },
562
574
  context: { excludedExerciseIds: criteria.excludedExerciseIds },
563
575
  };
@@ -567,17 +579,18 @@ const attachOptionalCardioToRoutine = ({ cardioPreference, sortedExercises, crit
567
579
  return;
568
580
  const slot = {
569
581
  kind: 'cardio',
570
- exerciseTemplate: cardioExercise,
571
- durationSeconds: exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS,
582
+ exercise_template: cardioExercise,
583
+ duration_seconds: exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS,
584
+ set_count: 1,
572
585
  };
573
586
  if (cardioPreference === 'workout-start')
574
- routineExercises.unshift(slot);
587
+ workoutExercises.unshift(slot);
575
588
  else
576
- routineExercises.push(slot);
589
+ workoutExercises.push(slot);
577
590
  if (debugExerciseSelectionTrace) {
578
591
  exerciseSelectionTraces.push({
579
592
  traceKind: 'cardio',
580
- routine,
593
+ workoutTemplate,
581
594
  prescriptionIndex: cardioPreference === 'workout-start'
582
595
  ? CARDIO_TRACE_INDEX_BEFORE_TEMPLATE
583
596
  : templatePrescriptionCount,
@@ -592,28 +605,27 @@ function generateProgram(params) {
592
605
  var _a, _b;
593
606
  const { trainerAlgorithmSettings, frequency, goal, level, equipments, workoutDurationMinutes, restTimerLength, exerciseStore, focusMuscle, excludedExerciseIds, debugExerciseSelectionTrace, cardioPreference, } = params;
594
607
  const exerciseSelectionTraces = [];
595
- const routines = exports.programSplits[frequency];
608
+ const workoutTemplates = exports.programSplits[frequency];
596
609
  const program = {
597
- name: frequency,
598
- routines: [],
610
+ workoutTemplates: [],
599
611
  };
600
612
  // TODO: Rename sortedExercises to sortedExercisesByMuscleGroup or something similar
601
613
  const sortedExercises = (0, exports.getPrioritySortedExercises)(trainerAlgorithmSettings.exercise_priorities, exerciseStore);
602
614
  const programUsedExerciseIds = new Set();
603
615
  let programFocusMuscleExerciseCount = 0;
604
616
  const allErrors = [];
605
- for (const routine of routines) {
606
- const routineTemplate = trainerAlgorithmSettings.templates[routine];
607
- let routineBarbellExerciseCount = 0;
608
- const routineUsedExerciseIds = new Set();
609
- const routineExercises = [];
610
- for (let prescriptionIndex = 0; prescriptionIndex < routineTemplate.exercises.length; prescriptionIndex++) {
611
- const exercisePrescription = routineTemplate.exercises[prescriptionIndex];
617
+ for (const workout of workoutTemplates) {
618
+ const workoutTemplate = trainerAlgorithmSettings.templates[workout];
619
+ let workoutBarbellExerciseCount = 0;
620
+ const workoutUsedExerciseIds = new Set();
621
+ const workoutExercises = [];
622
+ for (let prescriptionIndex = 0; prescriptionIndex < workoutTemplate.exercises.length; prescriptionIndex++) {
623
+ const exercisePrescription = workoutTemplate.exercises[prescriptionIndex];
612
624
  const muscleGroup = getMuscleGroup({
613
625
  muscleGroupPrescription: exercisePrescription.muscle_group,
614
626
  programFocusMuscleExerciseCount,
615
627
  focusMuscle,
616
- routineName: routine,
628
+ workoutTemplateName: workout,
617
629
  });
618
630
  // If the muscle group is not found, skip the exercise
619
631
  if (!muscleGroup) {
@@ -632,7 +644,7 @@ function generateProgram(params) {
632
644
  exerciseCategory: exercisePrescription.category,
633
645
  equipments,
634
646
  muscleGroup,
635
- routineBarbellExerciseCount,
647
+ workoutBarbellExerciseCount,
636
648
  level,
637
649
  goal,
638
650
  };
@@ -643,7 +655,7 @@ function generateProgram(params) {
643
655
  criteria,
644
656
  context: {
645
657
  programUsedExerciseIds,
646
- routineUsedExerciseIds,
658
+ workoutUsedExerciseIds,
647
659
  excludedExerciseIds,
648
660
  },
649
661
  withTrace: true,
@@ -651,7 +663,7 @@ function generateProgram(params) {
651
663
  exercise = selection.exercise;
652
664
  exerciseSelectionTraces.push({
653
665
  traceKind: 'template',
654
- routine,
666
+ workoutTemplate: workout,
655
667
  prescriptionIndex,
656
668
  prescription: exercisePrescription,
657
669
  resolvedMuscleGroup: muscleGroup,
@@ -666,30 +678,29 @@ function generateProgram(params) {
666
678
  criteria,
667
679
  context: {
668
680
  programUsedExerciseIds,
669
- routineUsedExerciseIds,
681
+ workoutUsedExerciseIds,
670
682
  excludedExerciseIds,
671
683
  },
672
684
  });
673
685
  }
674
686
  if (!!exercise) {
675
687
  programUsedExerciseIds.add(exercise.id);
676
- routineUsedExerciseIds.add(exercise.id);
688
+ workoutUsedExerciseIds.add(exercise.id);
677
689
  if (exercise.equipment_category === 'barbell')
678
- routineBarbellExerciseCount++;
690
+ workoutBarbellExerciseCount++;
679
691
  if (exercisePrescription.muscle_group === 'focus_muscle')
680
692
  programFocusMuscleExerciseCount++;
681
693
  const repRange = (0, exports.getTrainerRepRange)(trainerAlgorithmSettings, goal, exercisePrescription.category);
682
- routineExercises.push({
694
+ workoutExercises.push({
683
695
  kind: 'resistance',
684
- exerciseTemplate: exercise,
685
- muscleGroup: exercisePrescription.muscle_group,
686
- category: exercisePrescription.category,
687
- sets: (0, exports.getTrainerSetCount)(trainerAlgorithmSettings, goal, frequency),
688
- repRangeStart: repRange.rep_range_start,
689
- repRangeEnd: repRange.rep_range_end,
690
- restTimerSeconds: (0, exports.getTrainerRestTimerSeconds)(trainerAlgorithmSettings, goal, restTimerLength, exercisePrescription.category),
691
- warmupSetCount: exercisePrescription.warmup_set_count,
692
- notes: (_b = trainerAlgorithmSettings.exercise_notes[exercise.id]) !== null && _b !== void 0 ? _b : '',
696
+ exercise_template: exercise,
697
+ set_count: (0, exports.getTrainerSetCount)(trainerAlgorithmSettings, goal, frequency),
698
+ warmup_set_count: (_b = exercisePrescription.warmup_set_count) !== null && _b !== void 0 ? _b : 0,
699
+ rep_range: {
700
+ start: repRange.rep_range_start,
701
+ end: repRange.rep_range_end,
702
+ },
703
+ rest_seconds: (0, exports.getTrainerRestTimerSeconds)(trainerAlgorithmSettings, goal, restTimerLength, exercisePrescription.category),
693
704
  });
694
705
  }
695
706
  else {
@@ -707,23 +718,21 @@ function generateProgram(params) {
707
718
  });
708
719
  }
709
720
  }
710
- attachOptionalCardioToRoutine({
721
+ attachOptionalCardioToWorkoutTemplate({
711
722
  cardioPreference,
712
723
  sortedExercises,
713
724
  criteria: { equipments, level, goal, frequency, excludedExerciseIds },
714
725
  debugExerciseSelectionTrace,
715
726
  exerciseSelectionTraces,
716
- routine,
717
- templatePrescriptionCount: routineTemplate.exercises.length,
718
- routineExercises,
727
+ workoutTemplate: workout,
728
+ templatePrescriptionCount: workoutTemplate.exercises.length,
729
+ workoutExercises,
719
730
  });
720
- program.routines.push({
721
- name: routine,
722
- notes: routineTemplate.notes,
723
- exercises: routineExercises,
731
+ program.workoutTemplates.push({
732
+ name: workout,
733
+ exercises: workoutExercises,
724
734
  });
725
735
  }
726
- // Return result based on whether there were errors
727
736
  if (allErrors.length > 0) {
728
737
  const result = {
729
738
  success: false,
package/built/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { InstructionsLanguage } from './exerciseLocaleUtils';
2
- import { WorkoutDurationMinutes } from './hevyTrainer';
2
+ import { TrainerWorkoutTemplateName, WorkoutDurationMinutes } from './hevyTrainer';
3
3
  import { BugReportQuestionSchema, CreateBugReportRequestBodySchema, postEmailBackofficeSchema } from './schemas';
4
4
  import { Language } from './translations';
5
5
  import { Lookup } from './typeUtils';
@@ -302,7 +302,7 @@ export interface BackofficeExistingUserResponse {
302
302
  public_api_key: string | null;
303
303
  limited_discovery: boolean;
304
304
  coach_trial_expire_date?: string;
305
- hevy_trainer_program: HevyTrainerProgram | null;
305
+ hevy_trainer_program: TrainerProgramV3 | null;
306
306
  }
307
307
  export interface BackofficeDeletedUserResponse {
308
308
  state: 'deleted-account';
@@ -747,6 +747,10 @@ export interface RepRange {
747
747
  start: number | null;
748
748
  end: number | null;
749
749
  }
750
+ export interface StrictRepRange extends RepRange {
751
+ start: number;
752
+ end: number;
753
+ }
750
754
  export interface WorkoutComment {
751
755
  id: number;
752
756
  username: string;
@@ -875,12 +879,14 @@ export interface Routine extends BaseRoutine {
875
879
  username: string;
876
880
  coach_id: null;
877
881
  }
882
+ /** @deprecated Use TrainerWorkoutTemplate instead */
878
883
  export interface HevyTrainerRoutine extends Routine {
879
884
  hevy_trainer_program_id: string;
880
885
  program_id: null;
881
886
  coach_force_rpe_enabled: false;
882
887
  folder_id: null;
883
888
  }
889
+ /** @deprecated Use TrainerWorkoutTemplate instead */
884
890
  export declare const isHevyTrainerRoutine: (routine: BaseRoutine) => routine is HevyTrainerRoutine;
885
891
  export type CoachsShallowLibraryRoutine = Omit<BaseRoutine, 'exercises' | 'profile_pic' | 'hevy_trainer_program_id' | 'username'>;
886
892
  export interface CoachesRoutine extends BaseRoutine {
@@ -1049,6 +1055,97 @@ export interface GetTeamInviteResponse {
1049
1055
  export interface OutstandingInvitesForCoachTeamResponse {
1050
1056
  invites: CoachTeamInvite[];
1051
1057
  }
1058
+ export interface TrainerWorkoutTemplateResistanceExercise {
1059
+ kind: 'resistance';
1060
+ id: string;
1061
+ exercise_template_id: string;
1062
+ index: number;
1063
+ set_count: number;
1064
+ warmup_set_count: number;
1065
+ rep_range: StrictRepRange;
1066
+ rest_seconds: number;
1067
+ }
1068
+ export interface TrainerWorkoutTemplateCardioExercise {
1069
+ kind: 'cardio';
1070
+ id: string;
1071
+ exercise_template_id: string;
1072
+ index: number;
1073
+ set_count: 1;
1074
+ duration_seconds: number;
1075
+ }
1076
+ export interface TrainerWorkoutTemplateOtherExercise {
1077
+ kind: 'other';
1078
+ id: string;
1079
+ exercise_template_id: string;
1080
+ index: number;
1081
+ set_count: number;
1082
+ }
1083
+ export type TrainerWorkoutTemplateExercise = TrainerWorkoutTemplateResistanceExercise | TrainerWorkoutTemplateCardioExercise | TrainerWorkoutTemplateOtherExercise;
1084
+ export interface TrainerWorkoutTemplate {
1085
+ id: string;
1086
+ hevy_trainer_program_id: string;
1087
+ index: number;
1088
+ name: TrainerWorkoutTemplateName;
1089
+ exercises: TrainerWorkoutTemplateExercise[];
1090
+ }
1091
+ export interface TrainerProgramV3 {
1092
+ id: string;
1093
+ schema_version: 'v3';
1094
+ created_at: string;
1095
+ updated_at: string;
1096
+ level: TrainingLevel;
1097
+ goal: TrainingGoal;
1098
+ equipments: GranularEquipment[];
1099
+ weekly_frequency: WeeklyTrainingFrequency;
1100
+ templates: TrainerWorkoutTemplate[];
1101
+ focus_muscle?: SimplifiedMuscleGroup;
1102
+ next_workout_index: number;
1103
+ workout_duration_minutes: WorkoutDurationMinutes;
1104
+ rest_timer_length: RestTimerLength;
1105
+ cardio_preference: CardioPreference;
1106
+ }
1107
+ export type TrainerWorkoutTemplateExerciseInput = Omit<TrainerWorkoutTemplateResistanceExercise, 'id'> | Omit<TrainerWorkoutTemplateCardioExercise, 'id'> | Omit<TrainerWorkoutTemplateOtherExercise, 'id'>;
1108
+ export interface PostTrainerWorkoutTemplate {
1109
+ name: TrainerWorkoutTemplateName;
1110
+ index: number;
1111
+ exercises: TrainerWorkoutTemplateExerciseInput[];
1112
+ }
1113
+ export interface UpdateTrainerWorkoutTemplate extends PostTrainerWorkoutTemplate {
1114
+ id: string;
1115
+ }
1116
+ export interface PostTrainerProgramV3RequestBody {
1117
+ program: {
1118
+ version: 3;
1119
+ title: string;
1120
+ level: TrainingLevel;
1121
+ goal: TrainingGoal;
1122
+ equipments: GranularEquipment[];
1123
+ weekly_frequency: WeeklyTrainingFrequency;
1124
+ focus_muscle?: SimplifiedMuscleGroup;
1125
+ next_workout_index?: number;
1126
+ workout_duration_minutes: WorkoutDurationMinutes;
1127
+ rest_timer_length: RestTimerLength;
1128
+ cardio_preference: CardioPreference;
1129
+ templates: PostTrainerWorkoutTemplate[];
1130
+ };
1131
+ }
1132
+ export interface UpdateTrainerProgramV3RequestBody {
1133
+ program: {
1134
+ version: 3;
1135
+ programId: string;
1136
+ level: TrainingLevel;
1137
+ goal: TrainingGoal;
1138
+ equipments: GranularEquipment[];
1139
+ weekly_frequency: WeeklyTrainingFrequency;
1140
+ focus_muscle?: SimplifiedMuscleGroup;
1141
+ next_workout_index?: number;
1142
+ workout_duration_minutes: WorkoutDurationMinutes;
1143
+ rest_timer_length: RestTimerLength;
1144
+ cardio_preference: CardioPreference;
1145
+ templates: UpdateTrainerWorkoutTemplate[];
1146
+ };
1147
+ }
1148
+ /** @deprecated Use TrainerProgramV3 instead */
1052
1149
  export interface HevyTrainerProgram {
1053
1150
  id: string;
1054
1151
  created_at: string;
@@ -1065,6 +1162,7 @@ export interface HevyTrainerProgram {
1065
1162
  rest_timer_length: RestTimerLength;
1066
1163
  cardio_preference: CardioPreference;
1067
1164
  }
1165
+ /** @deprecated Use PostTrainerProgramV3RequestBody instead */
1068
1166
  export interface PostHevyTrainerProgramRequestBody {
1069
1167
  program: {
1070
1168
  version: 1;
@@ -1081,6 +1179,7 @@ export interface PostHevyTrainerProgramRequestBody {
1081
1179
  cardio_preference: CardioPreference;
1082
1180
  };
1083
1181
  }
1182
+ /** @deprecated Use UpdateTrainerProgramV3RequestBody instead */
1084
1183
  export interface UpdateHevyTrainerProgramRequestBody {
1085
1184
  program: {
1086
1185
  version: 1;
@@ -1103,7 +1202,7 @@ export interface UpdateHevyTrainerProgramRequestBody {
1103
1202
  }[];
1104
1203
  };
1105
1204
  }
1106
- /** @deprecated Use HevyTrainerProgram instead */
1205
+ /** @deprecated Use TrainerProgramV3 instead */
1107
1206
  export interface HevyTrainerProgramOld {
1108
1207
  id: string;
1109
1208
  created_at: string;
@@ -1118,7 +1217,7 @@ export interface HevyTrainerProgramOld {
1118
1217
  next_workout_index: number;
1119
1218
  workout_duration_minutes?: WorkoutDurationMinutes;
1120
1219
  }
1121
- /** @deprecated Use PostHevyTrainerProgramRequestBody instead */
1220
+ /** @deprecated Use PostTrainerProgramV3RequestBody instead */
1122
1221
  export interface PostHevyTrainerProgramOldRequestBody {
1123
1222
  program: {
1124
1223
  title: string;
@@ -1132,7 +1231,7 @@ export interface PostHevyTrainerProgramOldRequestBody {
1132
1231
  workout_duration_minutes?: WorkoutDurationMinutes;
1133
1232
  };
1134
1233
  }
1135
- /** @deprecated Use UpdateHevyTrainerProgramRequestBody instead */
1234
+ /** @deprecated Use UpdateTrainerProgramV3RequestBody instead */
1136
1235
  export interface UpdateHevyTrainerProgramOldRequestBody {
1137
1236
  program: {
1138
1237
  programId: string;
package/built/index.js CHANGED
@@ -321,6 +321,7 @@ const isWorkoutBiometrics = (x) => {
321
321
  return caloriesAreValid && heartSamplesAreValid;
322
322
  };
323
323
  exports.isWorkoutBiometrics = isWorkoutBiometrics;
324
+ /** @deprecated Use TrainerWorkoutTemplate instead */
324
325
  const isHevyTrainerRoutine = (routine) => routine.hevy_trainer_program_id !== null;
325
326
  exports.isHevyTrainerRoutine = isHevyTrainerRoutine;
326
327
  exports.measurementsList = [
@@ -236,7 +236,7 @@ const makeSettings = (overrides = {}) => {
236
236
  };
237
237
  return acc;
238
238
  }, {});
239
- const templates = hevyTrainer_1.routineNames.reduce((acc, name) => {
239
+ const templates = hevyTrainer_1.workoutTemplateNames.reduce((acc, name) => {
240
240
  acc[name] = { exercises: [] };
241
241
  return acc;
242
242
  }, {});
@@ -372,7 +372,7 @@ describe('getPrioritySortedExercises', () => {
372
372
  expect(result.quadriceps.map((e) => e.id)).toEqual(['leg-ex']);
373
373
  });
374
374
  });
375
- const baseCriteria = (overrides = {}) => (Object.assign({ exerciseCategory: 'compound', equipments: [], routineBarbellExerciseCount: 0, level: 'beginner', goal: 'strength', muscleGroup: 'chest', frequency: 3 }, overrides));
375
+ const baseCriteria = (overrides = {}) => (Object.assign({ exerciseCategory: 'compound', equipments: [], workoutBarbellExerciseCount: 0, level: 'beginner', goal: 'strength', muscleGroup: 'chest', frequency: 3 }, overrides));
376
376
  /** Produces a `sortedExercises` record where only `chest` is populated. */
377
377
  const chestOnlySorted = (exercises) => {
378
378
  const empty = __1.muscleGroups.reduce((acc, mg) => {
@@ -394,7 +394,7 @@ describe('pickTrainerExercise', () => {
394
394
  });
395
395
  expect(result === null || result === void 0 ? void 0 : result.id).toBe('fresh');
396
396
  });
397
- it('pass 2: falls back to exercises not used in the current routine when all are used in the program', () => {
397
+ it('pass 2: falls back to exercises not used in the current workout when all are used in the program', () => {
398
398
  const onlyExercise = makeExercise({ id: 'only' });
399
399
  const result = (0, hevyTrainer_1.pickTrainerExercise)({
400
400
  sortedExercises: chestOnlySorted([onlyExercise]),
@@ -402,8 +402,8 @@ describe('pickTrainerExercise', () => {
402
402
  context: {
403
403
  // Marked as used in the whole program already.
404
404
  programUsedExerciseIds: new Set(['only']),
405
- // But not used in this routine, so pass 2 should succeed.
406
- routineUsedExerciseIds: new Set(),
405
+ // But not used in this workout, so pass 2 should succeed.
406
+ workoutUsedExerciseIds: new Set(),
407
407
  },
408
408
  });
409
409
  expect(result === null || result === void 0 ? void 0 : result.id).toBe('only');
@@ -506,7 +506,7 @@ describe('pickTrainerExercise', () => {
506
506
  sortedExercises: chestOnlySorted([ex]),
507
507
  criteria: baseCriteria({
508
508
  frequency: 3,
509
- routineBarbellExerciseCount: 10,
509
+ workoutBarbellExerciseCount: 10,
510
510
  equipments: ['dumbbell'],
511
511
  }),
512
512
  context: {},
@@ -519,7 +519,7 @@ describe('pickTrainerExercise', () => {
519
519
  sortedExercises: chestOnlySorted([ex]),
520
520
  criteria: baseCriteria({
521
521
  frequency: 1,
522
- routineBarbellExerciseCount: 2,
522
+ workoutBarbellExerciseCount: 2,
523
523
  equipments: ['dumbbell'],
524
524
  }),
525
525
  context: {},
@@ -532,7 +532,7 @@ describe('pickTrainerExercise', () => {
532
532
  sortedExercises: chestOnlySorted([ex]),
533
533
  criteria: baseCriteria({
534
534
  frequency: 1,
535
- routineBarbellExerciseCount: 3,
535
+ workoutBarbellExerciseCount: 3,
536
536
  // Dumbbell is listed as a barbell substitute, so the cap applies.
537
537
  equipments: ['dumbbell'],
538
538
  }),
@@ -546,7 +546,7 @@ describe('pickTrainerExercise', () => {
546
546
  sortedExercises: chestOnlySorted([ex]),
547
547
  criteria: baseCriteria({
548
548
  frequency: 1,
549
- routineBarbellExerciseCount: 5,
549
+ workoutBarbellExerciseCount: 5,
550
550
  // No substitutes → avoid dead-ends by allowing the barbell.
551
551
  equipments: [],
552
552
  }),
@@ -587,7 +587,7 @@ describe('pickTrainerExercise', () => {
587
587
  describe('generateProgram', () => {
588
588
  const buildSettingsForChestProgram = () => {
589
589
  const settings = makeSettings();
590
- // Put a single chest compound prescription into the 1-day full body routine.
590
+ // Put a single chest compound prescription into the 1-day full body workout.
591
591
  settings.templates.full_body_1 = {
592
592
  exercises: [
593
593
  { muscle_group: 'chest', category: 'compound', warmup_set_count: 2 },
@@ -596,7 +596,7 @@ describe('generateProgram', () => {
596
596
  };
597
597
  return settings;
598
598
  };
599
- it('produces one routine per programSplit and populates exercise fields from the settings', () => {
599
+ it('produces one workout per programSplit and populates exercise fields from the settings', () => {
600
600
  const settings = buildSettingsForChestProgram();
601
601
  const chestExercise = makeExercise({ id: 'bench', muscle_group: 'chest' });
602
602
  const result = (0, hevyTrainer_1.generateProgram)({
@@ -613,27 +613,19 @@ describe('generateProgram', () => {
613
613
  expect(result.success).toBe(true);
614
614
  if (!result.success)
615
615
  return;
616
- expect(result.program.name).toBe(1);
617
- expect(result.program.routines).toHaveLength(hevyTrainer_1.programSplits[1].length);
618
- const [routine] = result.program.routines;
619
- expect(routine.name).toBe('full_body_1');
620
- expect(routine.notes).toBe('Hit the chest hard.');
621
- expect(routine.exercises).toHaveLength(1);
622
- const [exercise] = routine.exercises;
623
- expect(exercise.kind).toBe('resistance');
624
- if (exercise.kind !== 'resistance')
616
+ expect(result.program.workoutTemplates).toHaveLength(hevyTrainer_1.programSplits[1].length);
617
+ const [workoutTemplate] = result.program.workoutTemplates;
618
+ expect(workoutTemplate.name).toBe('full_body_1');
619
+ expect(workoutTemplate.exercises).toHaveLength(1);
620
+ const [exercise] = workoutTemplate.exercises;
621
+ if (!(0, hevyTrainer_1.isTrainerProgramResistanceExercise)(exercise))
625
622
  return;
626
- expect(exercise.exerciseTemplate.id).toBe('bench');
627
- expect(exercise.category).toBe('compound');
628
- expect(exercise.muscleGroup).toBe('chest');
629
- expect(exercise.warmupSetCount).toBe(2);
630
- // strength + one_day -> 1 (see `makeSettings`)
631
- expect(exercise.sets).toBe(1);
632
- // strength + compound -> 1..5
633
- expect(exercise.repRangeStart).toBe(1);
634
- expect(exercise.repRangeEnd).toBe(5);
635
- // strength + medium + compound -> 60
636
- expect(exercise.restTimerSeconds).toBe(60);
623
+ expect(exercise.kind).toBe('resistance');
624
+ expect(exercise.exercise_template.id).toBe('bench');
625
+ expect(exercise.warmup_set_count).toBe(2);
626
+ expect(exercise.set_count).toBe(1);
627
+ expect(exercise.rep_range).toEqual({ start: 1, end: 5 });
628
+ expect(exercise.rest_seconds).toBe(60);
637
629
  });
638
630
  it('returns a partial program and errors when no matching exercise can be found', () => {
639
631
  const settings = buildSettingsForChestProgram();
@@ -656,14 +648,14 @@ describe('generateProgram', () => {
656
648
  type: 'exercise_not_found',
657
649
  muscleGroup: 'chest',
658
650
  });
659
- // The routine structure still exists in the partial program, just without
651
+ // The workout template structure still exists in the partial program, just without
660
652
  // any resolved exercises.
661
- expect(result.partialProgram.routines).toHaveLength(1);
662
- expect(result.partialProgram.routines[0].exercises).toEqual([]);
653
+ expect(result.partialProgram.workoutTemplates).toHaveLength(1);
654
+ expect(result.partialProgram.workoutTemplates[0].exercises).toEqual([]);
663
655
  });
664
656
  describe('min_workout_duration_limit filtering', () => {
665
657
  /**
666
- * Runs a 1-day program containing a single chest compound prescription
658
+ * Runs a 1-day workout program containing a single chest compound prescription
667
659
  * with the provided `min_workout_duration_limit` against the provided
668
660
  * `workoutDurationMinutes`, and returns whether the prescription was
669
661
  * placed.
@@ -690,9 +682,8 @@ describe('generateProgram', () => {
690
682
  exerciseStore: [makeExercise({ id: 'bench' })],
691
683
  cardioPreference: 'no-cardio',
692
684
  });
693
- const routine = (result.success ? result.program : result.partialProgram)
694
- .routines[0];
695
- return routine.exercises.length === 1;
685
+ const workoutTemplate = (result.success ? result.program : result.partialProgram).workoutTemplates[0];
686
+ return workoutTemplate.exercises.length === 1;
696
687
  };
697
688
  it('skips prescriptions whose limit is strictly greater than the selected duration', () => {
698
689
  expect(runDurationProbe({ limit: 80, workoutDurationMinutes: 40 })).toBe(false);
@@ -764,11 +755,11 @@ describe('generateProgram', () => {
764
755
  return;
765
756
  // Only the 40- and 60-minute prescriptions survive; the 80-minute one
766
757
  // is filtered out. Order is preserved.
767
- const placed = result.program.routines[0].exercises;
758
+ const placed = result.program.workoutTemplates[0].exercises;
768
759
  expect(placed.every((e) => e.kind === 'resistance')).toBe(true);
769
760
  expect(placed
770
761
  .filter(hevyTrainer_1.isTrainerProgramResistanceExercise)
771
- .map((e) => e.warmupSetCount)).toEqual([1, 2]);
762
+ .map((e) => e.warmup_set_count)).toEqual([1, 2]);
772
763
  });
773
764
  });
774
765
  it('skips focus_muscle prescriptions when no focusMuscle is provided', () => {
@@ -792,7 +783,7 @@ describe('generateProgram', () => {
792
783
  expect(result.success).toBe(true);
793
784
  if (!result.success)
794
785
  return;
795
- expect(result.program.routines[0].exercises).toEqual([]);
786
+ expect(result.program.workoutTemplates[0].exercises).toEqual([]);
796
787
  });
797
788
  it('routes focus_muscle prescriptions to the requested SimplifiedMuscleGroup', () => {
798
789
  const settings = makeSettings();
@@ -819,12 +810,11 @@ describe('generateProgram', () => {
819
810
  expect(result.success).toBe(true);
820
811
  if (!result.success)
821
812
  return;
822
- const [exercise] = result.program.routines[0].exercises;
813
+ const [exercise] = result.program.workoutTemplates[0].exercises;
823
814
  expect(exercise.kind).toBe('resistance');
824
815
  if (exercise.kind !== 'resistance')
825
816
  return;
826
- expect(exercise.exerciseTemplate.id).toBe('biceps-curl');
827
- expect(exercise.muscleGroup).toBe('focus_muscle');
817
+ expect(exercise.exercise_template.id).toBe('biceps-curl');
828
818
  });
829
819
  it('excludes exercises listed in `excludedExerciseIds`', () => {
830
820
  const settings = buildSettingsForChestProgram();
@@ -845,7 +835,7 @@ describe('generateProgram', () => {
845
835
  expect(result.success).toBe(true);
846
836
  if (!result.success)
847
837
  return;
848
- expect(result.program.routines[0].exercises[0].exerciseTemplate.id).toBe('alt');
838
+ expect(result.program.workoutTemplates[0].exercises[0].exercise_template.id).toBe('alt');
849
839
  });
850
840
  it('attaches exercise selection traces when debugExerciseSelectionTrace is true', () => {
851
841
  const settings = buildSettingsForChestProgram();
@@ -866,7 +856,7 @@ describe('generateProgram', () => {
866
856
  expect(result.exerciseSelectionTraces).toHaveLength(1);
867
857
  const [trace] = result.exerciseSelectionTraces;
868
858
  expect(trace.traceKind).toBe('template');
869
- expect(trace.routine).toBe('full_body_1');
859
+ expect(trace.workoutTemplate).toBe('full_body_1');
870
860
  expect(trace.selectedExerciseId).toBe('bench');
871
861
  expect(trace.trace.selectedPass).toBe(1);
872
862
  });
@@ -938,7 +928,7 @@ describe('generateProgram', () => {
938
928
  expect(cardioTrace.equipments).toEqual([]);
939
929
  expect(cardioTrace.level).toBe('beginner');
940
930
  });
941
- it('uses the same prioritized cardio exercise on every routine (not blocked by program reuse)', () => {
931
+ it('uses the same prioritized cardio exercise on every workout (not blocked by program reuse)', () => {
942
932
  const settings = makeSettings();
943
933
  settings.templates.full_body_2_a = {
944
934
  exercises: [{ muscle_group: 'chest', category: 'compound' }],
@@ -971,13 +961,13 @@ describe('generateProgram', () => {
971
961
  expect(result.success).toBe(true);
972
962
  if (!result.success)
973
963
  return;
974
- expect(result.program.routines[0].exercises[0]).toMatchObject({
964
+ expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
975
965
  kind: 'cardio',
976
- exerciseTemplate: { id: 'run' },
966
+ exercise_template: { id: 'run' },
977
967
  });
978
- expect(result.program.routines[1].exercises[0]).toMatchObject({
968
+ expect(result.program.workoutTemplates[1].exercises[0]).toMatchObject({
979
969
  kind: 'cardio',
980
- exerciseTemplate: { id: 'run' },
970
+ exercise_template: { id: 'run' },
981
971
  });
982
972
  });
983
973
  it('skips cardio exercises that need equipment the user does not have', () => {
@@ -1016,9 +1006,9 @@ describe('generateProgram', () => {
1016
1006
  expect(result.success).toBe(true);
1017
1007
  if (!result.success)
1018
1008
  return;
1019
- expect(result.program.routines[0].exercises[0]).toMatchObject({
1009
+ expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
1020
1010
  kind: 'cardio',
1021
- exerciseTemplate: { id: 'body-run' },
1011
+ exercise_template: { id: 'body-run' },
1022
1012
  });
1023
1013
  });
1024
1014
  it('skips globally excluded cardio and uses the next priority exercise', () => {
@@ -1057,12 +1047,12 @@ describe('generateProgram', () => {
1057
1047
  expect(result.success).toBe(true);
1058
1048
  if (!result.success)
1059
1049
  return;
1060
- expect(result.program.routines[0].exercises[0]).toMatchObject({
1050
+ expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
1061
1051
  kind: 'cardio',
1062
- exerciseTemplate: { id: 'run-b' },
1052
+ exercise_template: { id: 'run-b' },
1063
1053
  });
1064
1054
  });
1065
- it('keeps global trace order aligned with routine generation (no cross-routine unshift)', () => {
1055
+ it('keeps global trace order aligned with workout generation (no cross-workout unshift)', () => {
1066
1056
  const settings = makeSettings();
1067
1057
  settings.templates.full_body_2_a = {
1068
1058
  exercises: [{ muscle_group: 'chest', category: 'compound' }],
@@ -1096,15 +1086,15 @@ describe('generateProgram', () => {
1096
1086
  expect(result.success).toBe(true);
1097
1087
  if (!result.success)
1098
1088
  return;
1099
- const idxA = result.exerciseSelectionTraces.findIndex((t) => t.routine === 'full_body_2_a');
1100
- const idxB = result.exerciseSelectionTraces.findIndex((t) => t.routine === 'full_body_2_b');
1089
+ const idxA = result.exerciseSelectionTraces.findIndex((t) => t.workoutTemplate === 'full_body_2_a');
1090
+ const idxB = result.exerciseSelectionTraces.findIndex((t) => t.workoutTemplate === 'full_body_2_b');
1101
1091
  expect(idxA).toBeGreaterThanOrEqual(0);
1102
1092
  expect(idxB).toBeGreaterThan(idxA);
1103
1093
  });
1104
1094
  describe('barbell cap at frequency === 1 (end-to-end)', () => {
1105
1095
  it('drops the 4th+ barbell prescription when the user has a substitute', () => {
1106
1096
  const settings = makeSettings();
1107
- // 4 identical barbell compound prescriptions in the 1-day routine.
1097
+ // 4 identical barbell compound prescriptions in the 1-day workout.
1108
1098
  settings.templates.full_body_1 = {
1109
1099
  exercises: Array.from({ length: 4 }, () => ({
1110
1100
  muscle_group: 'chest',
@@ -1133,7 +1123,7 @@ describe('generateProgram', () => {
1133
1123
  expect(result.success).toBe(false);
1134
1124
  if (result.success)
1135
1125
  return;
1136
- expect(result.partialProgram.routines[0].exercises).toHaveLength(3);
1126
+ expect(result.partialProgram.workoutTemplates[0].exercises).toHaveLength(3);
1137
1127
  expect(result.errors).toHaveLength(1);
1138
1128
  });
1139
1129
  it('allows 4+ barbell prescriptions at frequency 1 when no substitute is present', () => {
@@ -1164,19 +1154,19 @@ describe('generateProgram', () => {
1164
1154
  expect(result.success).toBe(true);
1165
1155
  if (!result.success)
1166
1156
  return;
1167
- expect(result.program.routines[0].exercises).toHaveLength(4);
1157
+ expect(result.program.workoutTemplates[0].exercises).toHaveLength(4);
1168
1158
  });
1169
1159
  });
1170
1160
  describe('focus_muscle cycling', () => {
1171
1161
  it('cycles through simplifiedMuscleGroupToMuscleGroups as focus_muscle prescriptions are placed', () => {
1172
1162
  // Use a 2-day split so we can place 4 focus_muscle exercises across two
1173
- // routines and confirm the counter survives between routines.
1163
+ // workouts and confirm the counter survives between workouts.
1174
1164
  const settings = makeSettings();
1175
1165
  const focusPrescription = {
1176
1166
  muscle_group: 'focus_muscle',
1177
1167
  category: 'isolation',
1178
1168
  };
1179
- // full_body routines allow any focus muscle. Place 2 per routine so the
1169
+ // full_body workouts allow any focus muscle. Place 2 per workout so the
1180
1170
  // combined count across the program is 4.
1181
1171
  settings.templates.full_body_2_a = {
1182
1172
  exercises: [focusPrescription, focusPrescription],
@@ -1246,7 +1236,7 @@ describe('generateProgram', () => {
1246
1236
  settings.templates.upper_2 = { exercises: [focusPrescription] };
1247
1237
  settings.templates.lower_2 = { exercises: [focusPrescription] };
1248
1238
  // `legs` cycles through ['quadriceps', 'hamstrings', 'calves', 'glutes',
1249
- // 'abductors', 'adductors']. The first two allowed routines (legs_1 and
1239
+ // 'abductors', 'adductors']. The first two allowed workouts (legs_1 and
1250
1240
  // lower_2) should land on quadriceps and hamstrings respectively.
1251
1241
  const exercises = [
1252
1242
  makeExercise({
@@ -1279,10 +1269,7 @@ describe('generateProgram', () => {
1279
1269
  // Only legs_1 and lower_2 run the focus_muscle prescription. Both
1280
1270
  // succeed and should have selected quadriceps then hamstrings in order.
1281
1271
  expect(result.exerciseSelectionTraces).toHaveLength(2);
1282
- expect(result.exerciseSelectionTraces.map((t) => t.routine)).toEqual([
1283
- 'legs_1',
1284
- 'lower_2',
1285
- ]);
1272
+ expect(result.exerciseSelectionTraces.map((t) => t.workoutTemplate)).toEqual(['legs_1', 'lower_2']);
1286
1273
  expect(result.exerciseSelectionTraces
1287
1274
  .filter((t) => t.traceKind === 'template')
1288
1275
  .map((t) => t.resolvedMuscleGroup)).toEqual(['quadriceps', 'hamstrings']);
@@ -1290,14 +1277,14 @@ describe('generateProgram', () => {
1290
1277
  });
1291
1278
  describe('isFocusMuscleExtraExerciseAllowed template compatibility', () => {
1292
1279
  /**
1293
- * Runs a single-routine program that places one focus_muscle
1280
+ * Runs a single-workout template program that places one focus_muscle
1294
1281
  * prescription into the requested template and returns whether the
1295
1282
  * prescription produced an exercise or was skipped.
1296
1283
  */
1297
- const runFocusMuscleProbe = (routineName, focusMuscle, frequency, candidateExercise) => {
1284
+ const runFocusMuscleProbe = (workoutTemplateName, focusMuscle, frequency, candidateExercise) => {
1298
1285
  var _a;
1299
1286
  const settings = makeSettings();
1300
- settings.templates[routineName] = {
1287
+ settings.templates[workoutTemplateName] = {
1301
1288
  exercises: [{ muscle_group: 'focus_muscle', category: 'isolation' }],
1302
1289
  };
1303
1290
  const result = (0, hevyTrainer_1.generateProgram)({
@@ -1313,13 +1300,13 @@ describe('generateProgram', () => {
1313
1300
  debugExerciseSelectionTrace: true,
1314
1301
  cardioPreference: 'no-cardio',
1315
1302
  });
1316
- // Find the routine we targeted (it may not be first in the split).
1317
- const routine = (result.success ? result.program : result.partialProgram).routines.find((r) => r.name === routineName);
1303
+ // Find the workout template we targeted (it may not be first in the split).
1304
+ const workoutTemplate = (result.success ? result.program : result.partialProgram).workoutTemplates.find((w) => w.name === workoutTemplateName);
1318
1305
  return {
1319
- placed: ((_a = routine === null || routine === void 0 ? void 0 : routine.exercises.length) !== null && _a !== void 0 ? _a : 0) > 0,
1306
+ placed: ((_a = workoutTemplate === null || workoutTemplate === void 0 ? void 0 : workoutTemplate.exercises.length) !== null && _a !== void 0 ? _a : 0) > 0,
1320
1307
  };
1321
1308
  };
1322
- it('allows every focus muscle on full_body routines', () => {
1309
+ it('allows every focus muscle on full_body workouts', () => {
1323
1310
  const muscles = ['chest', 'shoulders', 'arms', 'legs', 'back'];
1324
1311
  muscles.forEach((focusMuscle) => {
1325
1312
  // Use a muscle that's in the simplified group: biceps for arms, chest
@@ -155,6 +155,7 @@ class WorkoutBuilder {
155
155
  workoutVisibility: 'public',
156
156
  isWorkoutBiometricsPublic: true,
157
157
  trainerProgramId: undefined,
158
+ trainerWorkoutTemplateId: undefined,
158
159
  shareTo: {
159
160
  strava: false,
160
161
  appleHealth: false,
@@ -11,7 +11,7 @@ export interface WorkoutSync {
11
11
  isMore: boolean;
12
12
  updated_at?: string;
13
13
  }
14
- export type Workout = OwnedWorkout | UserWorkout;
15
- export type WorkoutExercise = OwnedWorkoutExercise | UserWorkoutExercise;
16
- export type WorkoutExerciseSet = OwnedWorkoutExerciseSet | UserWorkoutExerciseSet;
14
+ export type Workout = OwnedWorkout & Partial<UserWorkout>;
15
+ export type WorkoutExercise = OwnedWorkoutExercise & Partial<UserWorkoutExercise>;
16
+ export type WorkoutExerciseSet = OwnedWorkoutExerciseSet & Partial<UserWorkoutExerciseSet>;
17
17
  export declare function isOwnedWorkout(workout: OwnedWorkout | UserWorkout): workout is OwnedWorkout;
@@ -20,7 +20,8 @@ __exportStar(require("./userWorkout"), exports);
20
20
  __exportStar(require("./publicWorkout"), exports);
21
21
  __exportStar(require("./normalizedWorkout"), exports);
22
22
  __exportStar(require("./postWorkoutRequest"), exports);
23
- // Placeholder, will be done better but this works for now
23
+ // Stub
24
24
  function isOwnedWorkout(workout) {
25
- return 'gymId' in workout;
25
+ void workout;
26
+ return true;
26
27
  }
@@ -36,6 +36,7 @@ export interface NormalizedWorkout {
36
36
  clientId: string;
37
37
  biometrics?: WorkoutBiometrics;
38
38
  trainerProgramId: string | undefined;
39
+ trainerWorkoutTemplateId: string | undefined;
39
40
  gymId: string | undefined;
40
41
  }
41
42
  export interface NormalizedSet {
@@ -39,6 +39,7 @@ export interface PostWorkoutRequestWorkout {
39
39
  biometrics?: WorkoutBiometrics;
40
40
  is_biometrics_public: boolean;
41
41
  trainer_program_id: string | undefined;
42
+ trainer_workout_template_id: string | undefined;
42
43
  gym_id: string | undefined;
43
44
  }
44
45
  export interface PostWorkoutRequestExercise {
@@ -25,6 +25,7 @@ export interface UserWorkout {
25
25
  apple_watch: boolean;
26
26
  wearos_watch: boolean;
27
27
  verified: boolean;
28
+ created_at: string;
28
29
  updated_at: string;
29
30
  nth_workout: number;
30
31
  /**
@@ -38,8 +39,14 @@ export interface UserWorkout {
38
39
  */
39
40
  include_warmup_sets: boolean;
40
41
  is_private: boolean;
42
+ /**
43
+ * If applicable, the user ID of the coach who logged this workout
44
+ */
45
+ logged_by_coach_id?: string;
41
46
  biometrics?: WorkoutBiometrics;
42
47
  is_biometrics_public: boolean;
48
+ trainer_program_id: string | undefined;
49
+ trainer_workout_template_id: string | undefined;
43
50
  gym: UserWorkoutGym | undefined;
44
51
  }
45
52
  export interface UserWorkoutExercise {
@@ -28,10 +28,25 @@ export interface OwnedWorkout {
28
28
  created_at: string;
29
29
  updated_at: string;
30
30
  nth_workout: number;
31
+ /**
32
+ * See https://github.com/hevyapp/hevy-backend/pull/193 to understand
33
+ * why we added estimated_volume_kg
34
+ */
35
+ estimated_volume_kg: number;
36
+ /**
37
+ * Whether to include warmup sets in various calculations.
38
+ * https://github.com/hevyapp/hevy-shared/pull/312
39
+ */
40
+ include_warmup_sets: boolean;
31
41
  is_private: boolean;
42
+ /**
43
+ * If applicable, the user ID of the coach who logged this workout
44
+ */
45
+ logged_by_coach_id?: string;
32
46
  biometrics?: WorkoutBiometrics;
33
47
  is_biometrics_public: boolean;
34
48
  trainer_program_id: string | undefined;
49
+ trainer_workout_template_id: string | undefined;
35
50
  gym_id: string | undefined;
36
51
  }
37
52
  export interface TrainerWorkout extends OwnedWorkout {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-shared",
3
- "version": "1.0.1037",
3
+ "version": "1.0.1039",
4
4
  "description": "",
5
5
  "main": "built/index.js",
6
6
  "types": "built/index.d.ts",