hevy-shared 1.0.1016 → 1.0.1018

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 } 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[];
@@ -87,24 +87,24 @@ export interface CardioExerciseSelectionTraceRecord {
87
87
  trace: ExerciseSelectionTrace;
88
88
  }
89
89
  export type ProgramExerciseSelectionTraceRecord = TemplateExerciseSelectionTraceRecord | CardioExerciseSelectionTraceRecord;
90
- export type TrainerProgramAttemptWithTraces = TrainerProgramAttempt & {
90
+ export type TrainerProgramAttemptWithTraces<T> = TrainerProgramAttempt<T> & {
91
91
  exerciseSelectionTraces: ProgramExerciseSelectionTraceRecord[];
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,38 @@ 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
- export interface TrainerProgramResistanceExercise {
161
+ export interface TrainerProgramResistanceExercise<T> {
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: T;
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
- export interface TrainerProgramCardioExercise {
169
+ export interface TrainerProgramCardioExercise<T> {
175
170
  kind: 'cardio';
176
- exerciseTemplate: HevyTrainerLibraryExercise;
177
- durationSeconds: number;
171
+ exercise_template: T;
172
+ set_count: 1;
173
+ duration_seconds: number;
178
174
  }
179
- export type TrainerProgramExercise = TrainerProgramResistanceExercise | TrainerProgramCardioExercise;
180
- export declare const isTrainerProgramResistanceExercise: (exercise: TrainerProgramExercise) => exercise is TrainerProgramResistanceExercise;
181
- export declare const isTrainerProgramCardioExercise: (exercise: TrainerProgramExercise) => exercise is TrainerProgramCardioExercise;
182
- export interface TrainerProgramRoutine {
183
- name: HevyTrainerRoutineName;
184
- exercises: TrainerProgramExercise[];
185
- notes?: string;
175
+ export interface TrainerProgramOtherExercise<T> {
176
+ kind: 'other';
177
+ exercise_template: T;
178
+ set_count: number;
179
+ }
180
+ export type TrainerProgramExercise<T> = TrainerProgramResistanceExercise<T> | TrainerProgramCardioExercise<T> | TrainerProgramOtherExercise<T>;
181
+ export declare const isTrainerProgramResistanceExercise: <T>(exercise: TrainerProgramExercise<T>) => exercise is TrainerProgramResistanceExercise<T>;
182
+ export declare const isTrainerProgramCardioExercise: <T>(exercise: TrainerProgramExercise<T>) => exercise is TrainerProgramCardioExercise<T>;
183
+ export declare const isTrainerProgramOtherExercise: <T>(exercise: TrainerProgramExercise<T>) => exercise is TrainerProgramOtherExercise<T>;
184
+ export interface TrainerProgramWorkoutTemplate<T> {
185
+ name: TrainerWorkoutTemplateName;
186
+ exercises: TrainerProgramExercise<T>[];
186
187
  }
187
- export interface TrainerProgram {
188
- name: WeeklyTrainingFrequency;
189
- routines: TrainerProgramRoutine[];
188
+ export interface GeneratedTrainerProgram<T> {
189
+ workoutTemplates: TrainerProgramWorkoutTemplate<T>[];
190
190
  }
191
191
  export declare const getTrainerSetCount: (trainerAlgorithmSettings: TrainerAlgorithmSettings, goal: TrainingGoal, frequency: WeeklyTrainingFrequency) => number;
192
- export declare const getTrainerRepRange: (trainerAlgorithmSettings: TrainerAlgorithmSettings, goal: TrainingGoal, exerciseCategory: HevyTrainerExerciseCategory) => RepRange;
192
+ export declare const getTrainerRepRange: (trainerAlgorithmSettings: TrainerAlgorithmSettings, goal: TrainingGoal, exerciseCategory: HevyTrainerExerciseCategory) => AlgorithmRepRange;
193
193
  export declare const getTrainerRestTimerSeconds: (trainerAlgorithmSettings: TrainerAlgorithmSettings, goal: TrainingGoal, length: RestTimerLength, exerciseCategory: HevyTrainerExerciseCategory) => number;
194
194
  /**
195
195
  * Normalizes the exercise category to a HevyTrainerExerciseCategory
@@ -243,25 +243,16 @@ export interface ExercisePrescriptionError {
243
243
  focusMuscle?: SimplifiedMuscleGroup;
244
244
  };
245
245
  }
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
- export interface TrainerProgramResult {
246
+ export interface TrainerProgramResult<T> {
256
247
  success: true;
257
- program: TrainerProgram;
248
+ program: GeneratedTrainerProgram<T>;
258
249
  }
259
- export interface TrainerProgramError {
250
+ export interface TrainerProgramError<T> {
260
251
  success: false;
261
252
  errors: ExercisePrescriptionError[];
262
- partialProgram: TrainerProgram;
253
+ partialProgram: GeneratedTrainerProgram<T>;
263
254
  }
264
- export type TrainerProgramAttempt = TrainerProgramResult | TrainerProgramError;
255
+ export type TrainerProgramAttempt<T> = TrainerProgramResult<T> | TrainerProgramError<T>;
265
256
  /**
266
257
  * Generates a complete training program based on the provided parameters
267
258
  * - debugExerciseSelectionTrace: true -> includes exercise selection trace breadcrumbs in the result for debugging.
@@ -269,6 +260,6 @@ export type TrainerProgramAttempt = TrainerProgramResult | TrainerProgramError;
269
260
  */
270
261
  export declare function generateProgram<T extends HevyTrainerLibraryExercise>(params: ProgramGenerationParams<T> & {
271
262
  debugExerciseSelectionTrace: true;
272
- }): TrainerProgramAttemptWithTraces;
273
- export declare function generateProgram<T extends HevyTrainerLibraryExercise>(params: ProgramGenerationParams<T>): TrainerProgramAttempt;
263
+ }): TrainerProgramAttemptWithTraces<T>;
264
+ export declare function generateProgram<T extends HevyTrainerLibraryExercise>(params: ProgramGenerationParams<T>): TrainerProgramAttempt<T>;
274
265
  export {};
@@ -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.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,8 @@ 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;
204
206
  const getTrainerSetCount = (trainerAlgorithmSettings, goal, frequency) => {
205
207
  return trainerAlgorithmSettings.sets[exports.frequencyMap[frequency]][goal];
206
208
  };
@@ -291,12 +293,12 @@ const MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK = 3;
291
293
  * - Only enforce the cap when the user has at least one "barbell substitute" equipment available
292
294
  * (otherwise we allow barbell exercises to avoid running out of viable options)
293
295
  */
294
- const isBarbellExerciseAllowed = (exercise, routineBarbellExerciseCount, userEquipments, frequency) => {
296
+ const isBarbellExerciseAllowed = (exercise, workoutBarbellExerciseCount, userEquipments, frequency) => {
295
297
  const isCandidateBarbell = exercise.equipment_category === 'barbell';
296
298
  const isOncePerWeek = frequency === 1;
297
299
  if (!isCandidateBarbell || !isOncePerWeek)
298
300
  return true;
299
- const isAtOrOverLimit = routineBarbellExerciseCount >= MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK;
301
+ const isAtOrOverLimit = workoutBarbellExerciseCount >= MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK;
300
302
  if (!isAtOrOverLimit)
301
303
  return true;
302
304
  const barbellSubstitutes = [
@@ -314,7 +316,7 @@ const isBarbellExerciseAllowed = (exercise, routineBarbellExerciseCount, userEqu
314
316
  const isExerciseUsed = (exercise, context) => {
315
317
  var _a, _b, _c;
316
318
  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)) ||
319
+ ((_b = context.workoutUsedExerciseIds) === null || _b === void 0 ? void 0 : _b.has(exercise.id)) ||
318
320
  ((_c = context.excludedExerciseIds) === null || _c === void 0 ? void 0 : _c.has(exercise.id)) ||
319
321
  false);
320
322
  };
@@ -393,16 +395,16 @@ const getPrioritySortedExercises = (exercisePriorities, exerciseStore) => {
393
395
  return sortedExercises;
394
396
  };
395
397
  exports.getPrioritySortedExercises = getPrioritySortedExercises;
396
- const getMuscleGroup = ({ muscleGroupPrescription, programFocusMuscleExerciseCount, focusMuscle, routineName, }) => {
398
+ const getMuscleGroup = ({ muscleGroupPrescription, programFocusMuscleExerciseCount, focusMuscle, workoutTemplateName, }) => {
397
399
  if (muscleGroupPrescription === 'focus_muscle') {
398
- // If the user has selected a focus muscle and it is allowed for the routine,
400
+ // If the user has selected a focus muscle and it is allowed for the workout template,
399
401
  // we return the focus muscle extra exercise
400
402
  if (!!focusMuscle &&
401
- isFocusMuscleExtraExerciseAllowed(routineName, focusMuscle)) {
403
+ isFocusMuscleExtraExerciseAllowed(workoutTemplateName, focusMuscle)) {
402
404
  const n = _1.simplifiedMuscleGroupToMuscleGroups[focusMuscle].length;
403
405
  return _1.simplifiedMuscleGroupToMuscleGroups[focusMuscle][programFocusMuscleExerciseCount % n];
404
406
  }
405
- // If the user has not selected a focus muscle or it is not allowed for the routine
407
+ // If the user has not selected a focus muscle or it is not allowed for the workout template
406
408
  // we skip this extra exercise for the focus muscle in the program
407
409
  return undefined;
408
410
  }
@@ -420,7 +422,7 @@ const isExerciseMatch = (exercise, criteria) => {
420
422
  const levelMatch = (_b = (_a = exercise.level) === null || _a === void 0 ? void 0 : _a.includes(criteria.level)) !== null && _b !== void 0 ? _b : false;
421
423
  const goalMatch = (_d = (_c = exercise.goal) === null || _c === void 0 ? void 0 : _c.includes(criteria.goal)) !== null && _d !== void 0 ? _d : false;
422
424
  const equipmentMatch = (0, exports.isEquipmentCompatible)(exercise, criteria.equipments);
423
- const barbellLimitOk = isBarbellExerciseAllowed(exercise, criteria.routineBarbellExerciseCount, criteria.equipments, criteria.frequency);
425
+ const barbellLimitOk = isBarbellExerciseAllowed(exercise, criteria.workoutBarbellExerciseCount, criteria.equipments, criteria.frequency);
424
426
  return (categoryMatch && levelMatch && goalMatch && equipmentMatch && barbellLimitOk);
425
427
  };
426
428
  /**
@@ -465,8 +467,8 @@ const pickTrainerExerciseWithTrace = (params) => {
465
467
  programUsedExerciseIds: context.programUsedExerciseIds,
466
468
  excludedExerciseIds: context.excludedExerciseIds,
467
469
  };
468
- const routineContext = {
469
- routineUsedExerciseIds: context.routineUsedExerciseIds,
470
+ const workoutContext = {
471
+ workoutUsedExerciseIds: context.workoutUsedExerciseIds,
470
472
  excludedExerciseIds: context.excludedExerciseIds,
471
473
  };
472
474
  // Pass 1
@@ -480,10 +482,10 @@ const pickTrainerExerciseWithTrace = (params) => {
480
482
  if (exercise)
481
483
  return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 1 }) };
482
484
  // Pass 2
483
- exercise = findMatchingExercise(exercises, criteria, routineContext);
485
+ exercise = findMatchingExercise(exercises, criteria, workoutContext);
484
486
  trace.entries.push({
485
487
  pass: 2,
486
- label: 'exact match; not used in routine (reuse allowed in program)',
488
+ label: 'exact match; not used in the same workout (reuse allowed in program)',
487
489
  candidatePoolSize: exercises.length,
488
490
  selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
489
491
  });
@@ -500,20 +502,20 @@ const pickTrainerExerciseWithTrace = (params) => {
500
502
  if (exercise)
501
503
  return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 3 }) };
502
504
  // Pass 4
503
- exercise = findAlternativeIsolationExercise(exercises, criteria, routineContext);
505
+ exercise = findAlternativeIsolationExercise(exercises, criteria, workoutContext);
504
506
  trace.entries.push({
505
507
  pass: 4,
506
- label: 'handpicked alternative isolation; not used in routine (reuse allowed in program)',
508
+ label: 'handpicked alternative isolation; not used in the same workout (reuse allowed in program)',
507
509
  candidatePoolSize: exercises.length,
508
510
  selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
509
511
  });
510
512
  if (exercise)
511
513
  return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 4 }) };
512
514
  // Pass 5
513
- exercise = findMatchingExercise(exercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), routineContext);
515
+ exercise = findMatchingExercise(exercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), workoutContext);
514
516
  trace.entries.push({
515
517
  pass: 5,
516
- label: 'match regardless of isolation or compound; not used in routine (reuse allowed in program)',
518
+ label: 'match regardless of isolation or compound; not used in the same workout (reuse allowed in program)',
517
519
  candidatePoolSize: exercises.length,
518
520
  selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
519
521
  });
@@ -523,10 +525,10 @@ const pickTrainerExerciseWithTrace = (params) => {
523
525
  const secondaryMuscleExercises = Object.values(sortedExercises)
524
526
  .flat()
525
527
  .filter((e) => e.other_muscles.includes(criteria.muscleGroup));
526
- exercise = findMatchingExercise(secondaryMuscleExercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), routineContext);
528
+ exercise = findMatchingExercise(secondaryMuscleExercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), workoutContext);
527
529
  trace.entries.push({
528
530
  pass: 6,
529
- label: 'secondary muscle match; regardless of isolation or compound; not used in routine (reuse allowed in program)',
531
+ label: 'secondary muscle match; regardless of isolation or compound; not used in the same workout (reuse allowed in program)',
530
532
  candidatePoolSize: secondaryMuscleExercises.length,
531
533
  selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
532
534
  });
@@ -542,11 +544,11 @@ function pickTrainerExercise(params) {
542
544
  }
543
545
  const CARDIO_TRACE_INDEX_BEFORE_TEMPLATE = -1;
544
546
  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, }) => {
547
+ /** Inserts optional cardio before or after template exercises for one workout template. */
548
+ const attachOptionalCardioToWorkoutTemplate = ({ cardioPreference, sortedExercises, criteria, debugExerciseSelectionTrace, exerciseSelectionTraces, workoutTemplate, templatePrescriptionCount, workoutExercises, }) => {
547
549
  if (cardioPreference === 'no-cardio')
548
550
  return;
549
- // Cardio is reusable across routines, so program/routine used-id contexts are
551
+ // Cardio is reusable across workout templates, so program/workoutTemplate used-id contexts are
550
552
  // intentionally omitted — only the global excluded set applies.
551
553
  const selectionParams = {
552
554
  sortedExercises,
@@ -557,7 +559,7 @@ const attachOptionalCardioToRoutine = ({ cardioPreference, sortedExercises, crit
557
559
  equipments: criteria.equipments,
558
560
  muscleGroup: 'cardio',
559
561
  exerciseCategory: 'all',
560
- routineBarbellExerciseCount: 0,
562
+ workoutBarbellExerciseCount: 0,
561
563
  },
562
564
  context: { excludedExerciseIds: criteria.excludedExerciseIds },
563
565
  };
@@ -567,17 +569,18 @@ const attachOptionalCardioToRoutine = ({ cardioPreference, sortedExercises, crit
567
569
  return;
568
570
  const slot = {
569
571
  kind: 'cardio',
570
- exerciseTemplate: cardioExercise,
571
- durationSeconds: exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS,
572
+ exercise_template: cardioExercise,
573
+ duration_seconds: exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS,
574
+ set_count: 1,
572
575
  };
573
576
  if (cardioPreference === 'workout-start')
574
- routineExercises.unshift(slot);
577
+ workoutExercises.unshift(slot);
575
578
  else
576
- routineExercises.push(slot);
579
+ workoutExercises.push(slot);
577
580
  if (debugExerciseSelectionTrace) {
578
581
  exerciseSelectionTraces.push({
579
582
  traceKind: 'cardio',
580
- routine,
583
+ workoutTemplate,
581
584
  prescriptionIndex: cardioPreference === 'workout-start'
582
585
  ? CARDIO_TRACE_INDEX_BEFORE_TEMPLATE
583
586
  : templatePrescriptionCount,
@@ -592,28 +595,27 @@ function generateProgram(params) {
592
595
  var _a, _b;
593
596
  const { trainerAlgorithmSettings, frequency, goal, level, equipments, workoutDurationMinutes, restTimerLength, exerciseStore, focusMuscle, excludedExerciseIds, debugExerciseSelectionTrace, cardioPreference, } = params;
594
597
  const exerciseSelectionTraces = [];
595
- const routines = exports.programSplits[frequency];
598
+ const workoutTemplates = exports.programSplits[frequency];
596
599
  const program = {
597
- name: frequency,
598
- routines: [],
600
+ workoutTemplates: [],
599
601
  };
600
602
  // TODO: Rename sortedExercises to sortedExercisesByMuscleGroup or something similar
601
603
  const sortedExercises = (0, exports.getPrioritySortedExercises)(trainerAlgorithmSettings.exercise_priorities, exerciseStore);
602
604
  const programUsedExerciseIds = new Set();
603
605
  let programFocusMuscleExerciseCount = 0;
604
606
  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];
607
+ for (const workout of workoutTemplates) {
608
+ const workoutTemplate = trainerAlgorithmSettings.templates[workout];
609
+ let workoutBarbellExerciseCount = 0;
610
+ const workoutUsedExerciseIds = new Set();
611
+ const workoutExercises = [];
612
+ for (let prescriptionIndex = 0; prescriptionIndex < workoutTemplate.exercises.length; prescriptionIndex++) {
613
+ const exercisePrescription = workoutTemplate.exercises[prescriptionIndex];
612
614
  const muscleGroup = getMuscleGroup({
613
615
  muscleGroupPrescription: exercisePrescription.muscle_group,
614
616
  programFocusMuscleExerciseCount,
615
617
  focusMuscle,
616
- routineName: routine,
618
+ workoutTemplateName: workout,
617
619
  });
618
620
  // If the muscle group is not found, skip the exercise
619
621
  if (!muscleGroup) {
@@ -632,7 +634,7 @@ function generateProgram(params) {
632
634
  exerciseCategory: exercisePrescription.category,
633
635
  equipments,
634
636
  muscleGroup,
635
- routineBarbellExerciseCount,
637
+ workoutBarbellExerciseCount,
636
638
  level,
637
639
  goal,
638
640
  };
@@ -643,7 +645,7 @@ function generateProgram(params) {
643
645
  criteria,
644
646
  context: {
645
647
  programUsedExerciseIds,
646
- routineUsedExerciseIds,
648
+ workoutUsedExerciseIds,
647
649
  excludedExerciseIds,
648
650
  },
649
651
  withTrace: true,
@@ -651,7 +653,7 @@ function generateProgram(params) {
651
653
  exercise = selection.exercise;
652
654
  exerciseSelectionTraces.push({
653
655
  traceKind: 'template',
654
- routine,
656
+ workoutTemplate: workout,
655
657
  prescriptionIndex,
656
658
  prescription: exercisePrescription,
657
659
  resolvedMuscleGroup: muscleGroup,
@@ -666,30 +668,29 @@ function generateProgram(params) {
666
668
  criteria,
667
669
  context: {
668
670
  programUsedExerciseIds,
669
- routineUsedExerciseIds,
671
+ workoutUsedExerciseIds,
670
672
  excludedExerciseIds,
671
673
  },
672
674
  });
673
675
  }
674
676
  if (!!exercise) {
675
677
  programUsedExerciseIds.add(exercise.id);
676
- routineUsedExerciseIds.add(exercise.id);
678
+ workoutUsedExerciseIds.add(exercise.id);
677
679
  if (exercise.equipment_category === 'barbell')
678
- routineBarbellExerciseCount++;
680
+ workoutBarbellExerciseCount++;
679
681
  if (exercisePrescription.muscle_group === 'focus_muscle')
680
682
  programFocusMuscleExerciseCount++;
681
683
  const repRange = (0, exports.getTrainerRepRange)(trainerAlgorithmSettings, goal, exercisePrescription.category);
682
- routineExercises.push({
684
+ workoutExercises.push({
683
685
  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 : '',
686
+ exercise_template: exercise,
687
+ set_count: (0, exports.getTrainerSetCount)(trainerAlgorithmSettings, goal, frequency),
688
+ warmup_set_count: (_b = exercisePrescription.warmup_set_count) !== null && _b !== void 0 ? _b : 0,
689
+ rep_range: {
690
+ start: repRange.rep_range_start,
691
+ end: repRange.rep_range_end,
692
+ },
693
+ rest_seconds: (0, exports.getTrainerRestTimerSeconds)(trainerAlgorithmSettings, goal, restTimerLength, exercisePrescription.category),
693
694
  });
694
695
  }
695
696
  else {
@@ -707,23 +708,21 @@ function generateProgram(params) {
707
708
  });
708
709
  }
709
710
  }
710
- attachOptionalCardioToRoutine({
711
+ attachOptionalCardioToWorkoutTemplate({
711
712
  cardioPreference,
712
713
  sortedExercises,
713
714
  criteria: { equipments, level, goal, frequency, excludedExerciseIds },
714
715
  debugExerciseSelectionTrace,
715
716
  exerciseSelectionTraces,
716
- routine,
717
- templatePrescriptionCount: routineTemplate.exercises.length,
718
- routineExercises,
717
+ workoutTemplate: workout,
718
+ templatePrescriptionCount: workoutTemplate.exercises.length,
719
+ workoutExercises,
719
720
  });
720
- program.routines.push({
721
- name: routine,
722
- notes: routineTemplate.notes,
723
- exercises: routineExercises,
721
+ program.workoutTemplates.push({
722
+ name: workout,
723
+ exercises: workoutExercises,
724
724
  });
725
725
  }
726
- // Return result based on whether there were errors
727
726
  if (allErrors.length > 0) {
728
727
  const result = {
729
728
  success: false,
package/built/index.d.ts CHANGED
@@ -1,8 +1,8 @@
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
- import { Lookup } from './typeUtils';
5
+ import { Lookup, TODO } from './typeUtils';
6
6
  import { z } from 'zod';
7
7
  export * from './schemas';
8
8
  export * from './constants';
@@ -300,7 +300,7 @@ export interface BackofficeExistingUserResponse {
300
300
  public_api_key: string | null;
301
301
  limited_discovery: boolean;
302
302
  coach_trial_expire_date?: string;
303
- hevy_trainer_program: HevyTrainerProgram | null;
303
+ hevy_trainer_program: TrainerProgramV3 | null;
304
304
  }
305
305
  export interface BackofficeDeletedUserResponse {
306
306
  state: 'deleted-account';
@@ -745,6 +745,10 @@ export interface RepRange {
745
745
  start: number | null;
746
746
  end: number | null;
747
747
  }
748
+ export interface StrictRepRange extends RepRange {
749
+ start: number;
750
+ end: number;
751
+ }
748
752
  export interface WorkoutExerciseSet {
749
753
  /**
750
754
  * id can be a string or a number because we used
@@ -888,7 +892,8 @@ export interface Workout {
888
892
  biometrics?: WorkoutBiometrics;
889
893
  is_biometrics_public: boolean;
890
894
  trainer_program_id: string | undefined;
891
- gym: Gym | undefined;
895
+ gym: TODO<Gym> | undefined;
896
+ trainer_workout_template_id: string | undefined;
892
897
  }
893
898
  export interface CustomExerciseImage {
894
899
  type: 'image';
@@ -968,7 +973,8 @@ export interface PostWorkoutRequestWorkout {
968
973
  biometrics?: WorkoutBiometrics;
969
974
  is_biometrics_public: boolean;
970
975
  trainer_program_id: string | undefined;
971
- gym_id: string | undefined;
976
+ gym: TODO<Gym> | undefined;
977
+ trainer_workout_template_id: string | undefined;
972
978
  }
973
979
  export type WorkoutDataImporterReport = {
974
980
  state: 'idle';
@@ -1096,12 +1102,14 @@ export interface Routine extends BaseRoutine {
1096
1102
  username: string;
1097
1103
  coach_id: null;
1098
1104
  }
1105
+ /** @deprecated Use TrainerWorkoutTemplate instead */
1099
1106
  export interface HevyTrainerRoutine extends Routine {
1100
1107
  hevy_trainer_program_id: string;
1101
1108
  program_id: null;
1102
1109
  coach_force_rpe_enabled: false;
1103
1110
  folder_id: null;
1104
1111
  }
1112
+ /** @deprecated Use TrainerWorkoutTemplate instead */
1105
1113
  export declare const isHevyTrainerRoutine: (routine: BaseRoutine) => routine is HevyTrainerRoutine;
1106
1114
  export type CoachsShallowLibraryRoutine = Omit<BaseRoutine, 'exercises' | 'profile_pic' | 'hevy_trainer_program_id' | 'username'>;
1107
1115
  export interface CoachesRoutine extends BaseRoutine {
@@ -1270,6 +1278,97 @@ export interface GetTeamInviteResponse {
1270
1278
  export interface OutstandingInvitesForCoachTeamResponse {
1271
1279
  invites: CoachTeamInvite[];
1272
1280
  }
1281
+ export interface TrainerWorkoutTemplateResistanceExercise {
1282
+ kind: 'resistance';
1283
+ id: string;
1284
+ exercise_template_id: string;
1285
+ index: number;
1286
+ set_count: number;
1287
+ warmup_set_count: number;
1288
+ rep_range: StrictRepRange;
1289
+ rest_seconds: number;
1290
+ }
1291
+ export interface TrainerWorkoutTemplateCardioExercise {
1292
+ kind: 'cardio';
1293
+ id: string;
1294
+ exercise_template_id: string;
1295
+ index: number;
1296
+ set_count: 1;
1297
+ duration_seconds: number;
1298
+ }
1299
+ export interface TrainerWorkoutTemplateOtherExercise {
1300
+ kind: 'other';
1301
+ id: string;
1302
+ exercise_template_id: string;
1303
+ index: number;
1304
+ set_count: number;
1305
+ }
1306
+ export type TrainerWorkoutTemplateExercise = TrainerWorkoutTemplateResistanceExercise | TrainerWorkoutTemplateCardioExercise | TrainerWorkoutTemplateOtherExercise;
1307
+ export interface TrainerWorkoutTemplate {
1308
+ id: string;
1309
+ hevy_trainer_program_id: string;
1310
+ index: number;
1311
+ name: TrainerWorkoutTemplateName;
1312
+ exercises: TrainerWorkoutTemplateExercise[];
1313
+ }
1314
+ export interface TrainerProgramV3 {
1315
+ id: string;
1316
+ schema_version: 'v3';
1317
+ created_at: string;
1318
+ updated_at: string;
1319
+ level: TrainingLevel;
1320
+ goal: TrainingGoal;
1321
+ equipments: GranularEquipment[];
1322
+ weekly_frequency: WeeklyTrainingFrequency;
1323
+ templates: TrainerWorkoutTemplate[];
1324
+ focus_muscle?: SimplifiedMuscleGroup;
1325
+ next_workout_index: number;
1326
+ workout_duration_minutes: WorkoutDurationMinutes;
1327
+ rest_timer_length: RestTimerLength;
1328
+ cardio_preference: CardioPreference;
1329
+ }
1330
+ export type TrainerWorkoutTemplateExerciseInput = Omit<TrainerWorkoutTemplateResistanceExercise, 'id'> | Omit<TrainerWorkoutTemplateCardioExercise, 'id'> | Omit<TrainerWorkoutTemplateOtherExercise, 'id'>;
1331
+ export interface PostTrainerWorkoutTemplate {
1332
+ name: TrainerWorkoutTemplateName;
1333
+ index: number;
1334
+ exercises: TrainerWorkoutTemplateExerciseInput[];
1335
+ }
1336
+ export interface UpdateTrainerWorkoutTemplate extends PostTrainerWorkoutTemplate {
1337
+ id: string;
1338
+ }
1339
+ export interface PostTrainerProgramV3RequestBody {
1340
+ program: {
1341
+ version: 3;
1342
+ title: string;
1343
+ level: TrainingLevel;
1344
+ goal: TrainingGoal;
1345
+ equipments: GranularEquipment[];
1346
+ weekly_frequency: WeeklyTrainingFrequency;
1347
+ focus_muscle?: SimplifiedMuscleGroup;
1348
+ next_workout_index?: number;
1349
+ workout_duration_minutes: WorkoutDurationMinutes;
1350
+ rest_timer_length: RestTimerLength;
1351
+ cardio_preference: CardioPreference;
1352
+ templates: PostTrainerWorkoutTemplate[];
1353
+ };
1354
+ }
1355
+ export interface UpdateTrainerProgramV3RequestBody {
1356
+ program: {
1357
+ version: 3;
1358
+ programId: string;
1359
+ level: TrainingLevel;
1360
+ goal: TrainingGoal;
1361
+ equipments: GranularEquipment[];
1362
+ weekly_frequency: WeeklyTrainingFrequency;
1363
+ focus_muscle?: SimplifiedMuscleGroup;
1364
+ next_workout_index?: number;
1365
+ workout_duration_minutes: WorkoutDurationMinutes;
1366
+ rest_timer_length: RestTimerLength;
1367
+ cardio_preference: CardioPreference;
1368
+ templates: UpdateTrainerWorkoutTemplate[];
1369
+ };
1370
+ }
1371
+ /** @deprecated Use TrainerProgramV3 instead */
1273
1372
  export interface HevyTrainerProgram {
1274
1373
  id: string;
1275
1374
  created_at: string;
@@ -1286,6 +1385,7 @@ export interface HevyTrainerProgram {
1286
1385
  rest_timer_length: RestTimerLength;
1287
1386
  cardio_preference: CardioPreference;
1288
1387
  }
1388
+ /** @deprecated Use PostTrainerProgramV3RequestBody instead */
1289
1389
  export interface PostHevyTrainerProgramRequestBody {
1290
1390
  program: {
1291
1391
  version: 1;
@@ -1302,6 +1402,7 @@ export interface PostHevyTrainerProgramRequestBody {
1302
1402
  cardio_preference: CardioPreference;
1303
1403
  };
1304
1404
  }
1405
+ /** @deprecated Use UpdateTrainerProgramV3RequestBody instead */
1305
1406
  export interface UpdateHevyTrainerProgramRequestBody {
1306
1407
  program: {
1307
1408
  version: 1;
@@ -1324,7 +1425,7 @@ export interface UpdateHevyTrainerProgramRequestBody {
1324
1425
  }[];
1325
1426
  };
1326
1427
  }
1327
- /** @deprecated Use HevyTrainerProgram instead */
1428
+ /** @deprecated Use TrainerProgramV3 instead */
1328
1429
  export interface HevyTrainerProgramOld {
1329
1430
  id: string;
1330
1431
  created_at: string;
@@ -1339,7 +1440,7 @@ export interface HevyTrainerProgramOld {
1339
1440
  next_workout_index: number;
1340
1441
  workout_duration_minutes?: WorkoutDurationMinutes;
1341
1442
  }
1342
- /** @deprecated Use PostHevyTrainerProgramRequestBody instead */
1443
+ /** @deprecated Use PostTrainerProgramV3RequestBody instead */
1343
1444
  export interface PostHevyTrainerProgramOldRequestBody {
1344
1445
  program: {
1345
1446
  title: string;
@@ -1353,7 +1454,7 @@ export interface PostHevyTrainerProgramOldRequestBody {
1353
1454
  workout_duration_minutes?: WorkoutDurationMinutes;
1354
1455
  };
1355
1456
  }
1356
- /** @deprecated Use UpdateHevyTrainerProgramRequestBody instead */
1457
+ /** @deprecated Use UpdateTrainerProgramV3RequestBody instead */
1357
1458
  export interface UpdateHevyTrainerProgramOldRequestBody {
1358
1459
  program: {
1359
1460
  programId: string;
@@ -1633,7 +1734,6 @@ export type HomeGym = {
1633
1734
  type: 'home';
1634
1735
  };
1635
1736
  export type Gym = CommercialGym | HomeGym;
1636
- export type GymType = Gym['type'];
1637
1737
  export interface StripePrice {
1638
1738
  product_id: string;
1639
1739
  billing_period: 'month' | 'year' | 'pay-once';
package/built/index.js CHANGED
@@ -324,6 +324,7 @@ const isWorkoutBiometrics = (x) => {
324
324
  return caloriesAreValid && heartSamplesAreValid;
325
325
  };
326
326
  exports.isWorkoutBiometrics = isWorkoutBiometrics;
327
+ /** @deprecated Use TrainerWorkoutTemplate instead */
327
328
  const isHevyTrainerRoutine = (routine) => routine.hevy_trainer_program_id !== null;
328
329
  exports.isHevyTrainerRoutine = isHevyTrainerRoutine;
329
330
  exports.measurementsList = [
@@ -1,4 +1,4 @@
1
- import { ExerciseType, Gym, RPE, SetType, ShareToPlatform, WorkoutBiometrics, WorkoutMedia, WorkoutVisibility } from '.';
1
+ import { ExerciseType, Gym, RPE, SetType, ShareToPlatform, TODO, WorkoutBiometrics, WorkoutMedia, WorkoutVisibility } from '.';
2
2
  /**
3
3
  * Events are used to determine the start time, end time and duration of a
4
4
  * `NormalizedWorkout`, in a way that can be persisted to disk.
@@ -36,7 +36,8 @@ export interface NormalizedWorkout {
36
36
  clientId: string;
37
37
  biometrics?: WorkoutBiometrics;
38
38
  trainerProgramId: string | undefined;
39
- gym: Gym | undefined;
39
+ gym: TODO<Gym> | undefined;
40
+ trainerWorkoutTemplateId: string | undefined;
40
41
  }
41
42
  export interface NormalizedSet {
42
43
  index: number;
@@ -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,
@@ -68,3 +68,9 @@ export declare const parseJSON: (...args: Parameters<typeof JSON.parse>) => unkn
68
68
  */
69
69
  export declare const typeSafeIndex: <T>(array: T[], index: number) => T | undefined;
70
70
  export declare const TODO: (message?: string) => never;
71
+ /**
72
+ * @deprecated Placeholder for avoiding cross-repo type issues due to merges.
73
+ * Meant to be replaced with the enclosed type T as the earliest possible dev
74
+ * convenience. DO NOT USE THIS IN PRODUCTION!
75
+ */
76
+ export type TODO<T> = T & never;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-shared",
3
- "version": "1.0.1016",
3
+ "version": "1.0.1018",
4
4
  "description": "",
5
5
  "main": "built/index.js",
6
6
  "types": "built/index.d.ts",