hevy-shared 1.0.962 → 1.0.964

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.
Files changed (86) hide show
  1. package/README.md +17 -2
  2. package/built/API/APIClient.d.ts +157 -0
  3. package/built/API/APIClient.js +381 -0
  4. package/built/API/index.d.ts +2 -0
  5. package/built/API/index.js +18 -0
  6. package/built/API/types.d.ts +38 -0
  7. package/built/API/types.js +18 -0
  8. package/built/adjustEventTokens.d.ts +16 -0
  9. package/built/adjustEventTokens.js +18 -0
  10. package/built/adminPermissions.d.ts +4 -0
  11. package/built/adminPermissions.js +22 -0
  12. package/built/async.d.ts +50 -0
  13. package/built/async.js +170 -0
  14. package/built/chat.d.ts +25 -23
  15. package/built/coachPlans.d.ts +2 -1
  16. package/built/coachPlans.js +2 -2
  17. package/built/cue.d.ts +12 -0
  18. package/built/cue.js +22 -0
  19. package/built/exerciseLocaleUtils.d.ts +17 -0
  20. package/built/exerciseLocaleUtils.js +62 -0
  21. package/built/filterExercises.d.ts +19 -3
  22. package/built/filterExercises.js +72 -60
  23. package/built/hevyTrainer.d.ts +250 -0
  24. package/built/hevyTrainer.js +676 -0
  25. package/built/index.d.ts +1217 -304
  26. package/built/index.js +268 -75
  27. package/built/muscleHeatmaps.d.ts +31 -0
  28. package/built/muscleHeatmaps.js +68 -0
  29. package/built/muscleSplits.d.ts +36 -0
  30. package/built/muscleSplits.js +100 -0
  31. package/built/normalizedWorkoutUtils.d.ts +88 -0
  32. package/built/normalizedWorkoutUtils.js +112 -0
  33. package/built/notifications.d.ts +215 -0
  34. package/built/notifications.js +9 -0
  35. package/built/routineUtils.d.ts +14 -0
  36. package/built/routineUtils.js +186 -0
  37. package/built/setIndicatorUtils.d.ts +4 -3
  38. package/built/setIndicatorUtils.js +15 -1
  39. package/built/tests/async.test.d.ts +1 -0
  40. package/built/tests/async.test.js +49 -0
  41. package/built/tests/hevyTrainer.test.d.ts +1 -0
  42. package/built/tests/hevyTrainer.test.js +1199 -0
  43. package/built/tests/muscleSplit.test.d.ts +1 -0
  44. package/built/tests/muscleSplit.test.js +153 -0
  45. package/built/tests/routineUtils.test.d.ts +1 -0
  46. package/built/tests/routineUtils.test.js +745 -0
  47. package/built/tests/testUtils.d.ts +85 -0
  48. package/built/tests/testUtils.js +319 -0
  49. package/built/tests/utils.test.js +748 -0
  50. package/built/tests/workoutVolume.test.js +165 -49
  51. package/built/translations/index.d.ts +2 -0
  52. package/built/translations/index.js +18 -0
  53. package/built/translations/translationUtils.d.ts +2 -0
  54. package/built/translations/translationUtils.js +61 -0
  55. package/built/translations/types.d.ts +8 -0
  56. package/built/translations/types.js +20 -0
  57. package/built/typeUtils.d.ts +70 -0
  58. package/built/typeUtils.js +55 -0
  59. package/built/units.d.ts +14 -7
  60. package/built/units.js +24 -14
  61. package/built/utils.d.ts +192 -5
  62. package/built/utils.js +598 -85
  63. package/built/websocket.d.ts +14 -2
  64. package/built/workoutVolume.d.ts +24 -5
  65. package/built/workoutVolume.js +25 -34
  66. package/package.json +30 -9
  67. package/.eslintignore +0 -2
  68. package/.eslintrc +0 -21
  69. package/.github/workflows/ci.yml +0 -15
  70. package/.github/workflows/npm-publish.yml +0 -59
  71. package/.github/workflows/pr-auto-assign.yml +0 -15
  72. package/.prettierrc.js +0 -5
  73. package/jest.config.js +0 -4
  74. package/src/chat.ts +0 -130
  75. package/src/coachPlans.ts +0 -57
  76. package/src/constants.ts +0 -14
  77. package/src/filterExercises.ts +0 -222
  78. package/src/index.ts +0 -1576
  79. package/src/setIndicatorUtils.ts +0 -137
  80. package/src/tests/utils.test.ts +0 -156
  81. package/src/tests/workoutVolume.test.ts +0 -93
  82. package/src/units.ts +0 -41
  83. package/src/utils.ts +0 -516
  84. package/src/websocket.ts +0 -36
  85. package/src/workoutVolume.ts +0 -175
  86. package/tsconfig.json +0 -70
@@ -0,0 +1,676 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPrioritySortedExercises = exports.isEquipmentCompatible = exports.normalizeExerciseCategory = exports.getTrainerRestTimerSeconds = exports.getTrainerRepRange = exports.getTrainerSetCount = exports.programSplits = exports.frequencyMap = exports.routineNames = exports.defaultDurationPerFrequency = exports.hevyTrainerExerciseCategories = exports.granularEquipmentsToTrainerEquipments = exports.trainerEquipmentToGranularEquipments = exports.granularEquipmentDefaults = exports.trainerGymTypes = exports.workoutDurationOptions = void 0;
4
+ exports.pickExerciseForPrescription = pickExerciseForPrescription;
5
+ exports.generateProgram = generateProgram;
6
+ const _1 = require(".");
7
+ exports.workoutDurationOptions = [40, 60, 80];
8
+ exports.trainerGymTypes = [
9
+ 'home_gym',
10
+ 'garage_gym',
11
+ 'commercial_gym',
12
+ 'full_gym',
13
+ ];
14
+ exports.granularEquipmentDefaults = {
15
+ home_gym: ['dumbbell', 'pullup_bar', 'adjustable_bench'],
16
+ garage_gym: [
17
+ 'dumbbell',
18
+ 'kettlebell',
19
+ 'barbell',
20
+ 'plate',
21
+ 'pullup_bar',
22
+ 'dip_bar',
23
+ 'squat_rack',
24
+ 'adjustable_bench',
25
+ 'spinning',
26
+ ],
27
+ commercial_gym: [
28
+ 'dumbbell',
29
+ 'barbell',
30
+ 'plate',
31
+ 'ez_bar',
32
+ 'pullup_bar',
33
+ 'dip_bar',
34
+ 'squat_rack',
35
+ 'adjustable_bench',
36
+ 'single_cable_machine',
37
+ 'lat_pulldown_cable',
38
+ 'leg_press_machine',
39
+ 'smith_machine',
40
+ 'stack_machines',
41
+ 'treadmill',
42
+ 'elliptical_trainer',
43
+ 'spinning',
44
+ ],
45
+ // All equipment is available in a full gym
46
+ full_gym: [
47
+ 'barbell',
48
+ 'dumbbell',
49
+ 'kettlebell',
50
+ 'plate',
51
+ 'medicine_ball',
52
+ 'ez_bar',
53
+ 'landmine',
54
+ 'trap_bar',
55
+ 'pullup_bar',
56
+ 'dip_bar',
57
+ 'squat_rack',
58
+ 'flat_bench',
59
+ 'adjustable_bench',
60
+ 'dual_cable_machine',
61
+ 'single_cable_machine',
62
+ 'lat_pulldown_cable',
63
+ 'leg_press_machine',
64
+ 'smith_machine',
65
+ 't_bar',
66
+ 'plate_machines',
67
+ 'stack_machines',
68
+ 'treadmill',
69
+ 'elliptical_trainer',
70
+ 'rowing_machine',
71
+ 'spinning',
72
+ 'stair_machine',
73
+ 'air_bike',
74
+ 'suspension_band',
75
+ 'resistance_band',
76
+ 'battle_rope',
77
+ 'rings',
78
+ 'jump_rope',
79
+ ],
80
+ };
81
+ const trainerEquipmentToGranularEquipmentsMap = {
82
+ barbell: [
83
+ 'adjustable_bench',
84
+ 'barbell',
85
+ 'ez_bar',
86
+ 'flat_bench',
87
+ 'landmine',
88
+ 'plate',
89
+ 'squat_rack',
90
+ 't_bar',
91
+ ],
92
+ dumbbell: ['adjustable_bench', 'dumbbell', 'flat_bench'],
93
+ machine: [
94
+ 'adjustable_bench',
95
+ 'dip_bar',
96
+ 'dual_cable_machine',
97
+ 'flat_bench',
98
+ 'lat_pulldown_cable',
99
+ 'leg_press_machine',
100
+ 'plate',
101
+ 'plate_machines',
102
+ 'pullup_bar',
103
+ 'single_cable_machine',
104
+ 'smith_machine',
105
+ 'stack_machines',
106
+ ],
107
+ };
108
+ const trainerEquipmentToGranularEquipments = (equipments) => {
109
+ const granularEquipments = [];
110
+ equipments.forEach((equipment) => {
111
+ granularEquipments.push(...trainerEquipmentToGranularEquipmentsMap[equipment]);
112
+ });
113
+ return Array.from(new Set(granularEquipments));
114
+ };
115
+ exports.trainerEquipmentToGranularEquipments = trainerEquipmentToGranularEquipments;
116
+ const MACHINE_SPECIFIC_GRANULAR_EQUIPMENTS = [
117
+ 'smith_machine',
118
+ 'leg_press_machine',
119
+ 'dual_cable_machine',
120
+ 'single_cable_machine',
121
+ 'lat_pulldown_cable',
122
+ 'plate_machines',
123
+ 'stack_machines',
124
+ 'dip_bar',
125
+ 'pullup_bar',
126
+ ];
127
+ /**
128
+ * Converts granular equipment values back to the old HevyTrainerProgramEquipment
129
+ * types for backwards compatibility with old mobile clients.
130
+ */
131
+ const granularEquipmentsToTrainerEquipments = (granularEquipments) => {
132
+ const result = [];
133
+ if (granularEquipments.includes('barbell')) {
134
+ result.push('barbell');
135
+ }
136
+ if (granularEquipments.includes('dumbbell')) {
137
+ result.push('dumbbell');
138
+ }
139
+ if (granularEquipments.some((eq) => MACHINE_SPECIFIC_GRANULAR_EQUIPMENTS.includes(eq))) {
140
+ result.push('machine');
141
+ }
142
+ return result;
143
+ };
144
+ exports.granularEquipmentsToTrainerEquipments = granularEquipmentsToTrainerEquipments;
145
+ exports.hevyTrainerExerciseCategories = ['compound', 'isolation'];
146
+ // These are the values that Philip decided for the default duration per frequency
147
+ exports.defaultDurationPerFrequency = {
148
+ 1: 80,
149
+ 2: 60,
150
+ 3: 60,
151
+ 4: 60,
152
+ 5: 40,
153
+ 6: 40,
154
+ };
155
+ exports.routineNames = [
156
+ // Full body 1x
157
+ 'full_body_1',
158
+ // Full body 2x
159
+ 'full_body_2_a',
160
+ 'full_body_2_b',
161
+ // Full body 3x
162
+ 'full_body_3_a',
163
+ 'full_body_3_b',
164
+ 'full_body_3_c',
165
+ // Upper body x4 per week
166
+ 'upper_1_a',
167
+ 'lower_1_a',
168
+ 'upper_1_b',
169
+ 'lower_1_b',
170
+ // Push Pull Legs Upper Lower 5x per week
171
+ 'push_1',
172
+ 'pull_1',
173
+ 'legs_1',
174
+ 'upper_2',
175
+ 'lower_2',
176
+ // Push Pull Legs x6 per week
177
+ 'push_2_a',
178
+ 'pull_2_a',
179
+ 'legs_2_a',
180
+ 'push_2_b',
181
+ 'pull_2_b',
182
+ 'legs_2_b',
183
+ ];
184
+ exports.frequencyMap = {
185
+ 1: 'one_day',
186
+ 2: 'two_days',
187
+ 3: 'three_days',
188
+ 4: 'four_days',
189
+ 5: 'five_days',
190
+ 6: 'six_days',
191
+ };
192
+ exports.programSplits = {
193
+ 1: ['full_body_1'],
194
+ 2: ['full_body_2_a', 'full_body_2_b'],
195
+ 3: ['full_body_3_a', 'full_body_3_b', 'full_body_3_c'],
196
+ 4: ['upper_1_a', 'lower_1_a', 'upper_1_b', 'lower_1_b'],
197
+ 5: ['push_1', 'pull_1', 'legs_1', 'upper_2', 'lower_2'],
198
+ 6: ['push_2_a', 'pull_2_a', 'legs_2_a', 'push_2_b', 'pull_2_b', 'legs_2_b'],
199
+ };
200
+ const getTrainerSetCount = (trainerAlgorithmSettings, goal, frequency) => {
201
+ return trainerAlgorithmSettings.sets[exports.frequencyMap[frequency]][goal];
202
+ };
203
+ exports.getTrainerSetCount = getTrainerSetCount;
204
+ const getTrainerRepRange = (trainerAlgorithmSettings, goal, exerciseCategory) => {
205
+ return trainerAlgorithmSettings.rep_ranges[goal][exerciseCategory];
206
+ };
207
+ exports.getTrainerRepRange = getTrainerRepRange;
208
+ const getTrainerRestTimerSeconds = (trainerAlgorithmSettings, goal, length, exerciseCategory) => {
209
+ return trainerAlgorithmSettings.rest_timers[goal][length][exerciseCategory];
210
+ };
211
+ exports.getTrainerRestTimerSeconds = getTrainerRestTimerSeconds;
212
+ /**
213
+ * Normalizes the exercise category to a HevyTrainerExerciseCategory
214
+ * - Treat custom exercises and exercises with no category as `compound`
215
+ * - Treat `assistance-compound` as `compound`
216
+ * - Treat `isolation` as `isolation`
217
+ * @param exercise - The exercise to normalize the category for
218
+ * @returns The normalized exercise category
219
+ */
220
+ const normalizeExerciseCategory = (exercise) => {
221
+ return exercise.is_custom
222
+ ? 'compound'
223
+ : exercise.category === 'isolation'
224
+ ? 'isolation'
225
+ : 'compound';
226
+ };
227
+ exports.normalizeExerciseCategory = normalizeExerciseCategory;
228
+ /**
229
+ * Returns true if the focus muscle group extra exercise is allowed for a given template
230
+ */
231
+ const isFocusMuscleExtraExerciseAllowed = (templateName, focusMuscle) => {
232
+ const isFullBodyTemplate = templateName.startsWith('full_body');
233
+ // Full body templates allow all muscle groups
234
+ if (isFullBodyTemplate) {
235
+ return true;
236
+ }
237
+ const isLegsTemplate = templateName.startsWith('legs');
238
+ const isUpperTemplate = templateName.startsWith('upper');
239
+ const isLowerTemplate = templateName.startsWith('lower');
240
+ const isPushTemplate = templateName.startsWith('push');
241
+ const isPullTemplate = templateName.startsWith('pull');
242
+ switch (focusMuscle) {
243
+ case 'core':
244
+ // Core exercises are allowed for all templates
245
+ return true;
246
+ case 'chest':
247
+ case 'shoulders':
248
+ return isUpperTemplate || isPushTemplate;
249
+ case 'arms':
250
+ return isUpperTemplate || isPullTemplate || isPushTemplate;
251
+ case 'legs':
252
+ return isLowerTemplate || isLegsTemplate;
253
+ case 'back':
254
+ return isUpperTemplate || isPullTemplate;
255
+ default:
256
+ (0, _1.exhaustiveTypeCheck)(focusMuscle);
257
+ return false;
258
+ }
259
+ };
260
+ /**
261
+ * Checks if an exercise is compatible with the user's selected granular equipments
262
+ *
263
+ * - If the exercise has no granular equipments, it is compatible no matter what the user has selected
264
+ * - Otherwise, the exercise is only allowed if the user has all the granular equipments
265
+ */
266
+ const isEquipmentCompatible = (exercise, userSelectedGranularEquipments) => {
267
+ if (!exercise.granular_equipments ||
268
+ exercise.granular_equipments.length === 0) {
269
+ return true;
270
+ }
271
+ const allowedEquipments = [...userSelectedGranularEquipments];
272
+ // Special case: adjustable bench can substitute for flat bench
273
+ if (userSelectedGranularEquipments.includes('adjustable_bench')) {
274
+ allowedEquipments.push('flat_bench');
275
+ }
276
+ // Special case: dual cable machine can substitute for single cable machine
277
+ if (userSelectedGranularEquipments.includes('dual_cable_machine')) {
278
+ allowedEquipments.push('single_cable_machine');
279
+ }
280
+ return exercise.granular_equipments.every((granularEquipment) => allowedEquipments.includes(granularEquipment));
281
+ };
282
+ exports.isEquipmentCompatible = isEquipmentCompatible;
283
+ const MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK = 3;
284
+ /**
285
+ * - If frequency is 1 and the candidate exercise is a barbell exercise
286
+ * - Then allow at most `MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK` barbell exercises
287
+ * - Only enforce the cap when the user has at least one "barbell substitute" equipment available
288
+ * (otherwise we allow barbell exercises to avoid running out of viable options)
289
+ */
290
+ const isBarbellExerciseAllowed = (exercise, routineBarbellExerciseCount, userEquipments, frequency) => {
291
+ const isCandidateBarbell = exercise.equipment_category === 'barbell';
292
+ const isOncePerWeek = frequency === 1;
293
+ if (!isCandidateBarbell || !isOncePerWeek)
294
+ return true;
295
+ const isAtOrOverLimit = routineBarbellExerciseCount >= MAX_BARBELL_EXERCISES_FOR_ONCE_PER_WEEK;
296
+ if (!isAtOrOverLimit)
297
+ return true;
298
+ const barbellSubstitutes = [
299
+ 'dumbbell',
300
+ 'dual_cable_machine',
301
+ 'single_cable_machine',
302
+ 'smith_machine',
303
+ 'plate_machines',
304
+ 'stack_machines',
305
+ ];
306
+ const hasAnySubstitute = barbellSubstitutes.some((substitute) => userEquipments.includes(substitute));
307
+ // If the user has substitutes, enforce the cap; otherwise allow barbell to avoid dead-ends.
308
+ return !hasAnySubstitute;
309
+ };
310
+ const isExerciseUsed = (exercise, context) => {
311
+ var _a, _b, _c;
312
+ return (((_a = context.programUsedExerciseIds) === null || _a === void 0 ? void 0 : _a.has(exercise.id)) ||
313
+ ((_b = context.routineUsedExerciseIds) === null || _b === void 0 ? void 0 : _b.has(exercise.id)) ||
314
+ ((_c = context.excludedExerciseIds) === null || _c === void 0 ? void 0 : _c.has(exercise.id)) ||
315
+ false);
316
+ };
317
+ const isolationExerciseAlternatives = {
318
+ /**
319
+ * - B74A95BB (Kneeling Push Up)
320
+ * - 392887AA (Push Up)
321
+ * - 39C99849 (Incline Push Ups)
322
+ * - 982734D4 (Plate Press)
323
+ */
324
+ chest: ['B74A95BB', '392887AA', '39C99849', '982734D4'],
325
+ /**
326
+ * - 6575F52D (Diamond Push Up)
327
+ * - CD6DC8E5 (Bench Dip)
328
+ */
329
+ triceps: ['6575F52D', 'CD6DC8E5'],
330
+ /**
331
+ * - 32HKJ34K (Walking Lunge)
332
+ * - A733CC5B (Walking Lunge (Dumbbell))
333
+ * - 9694DA61 (Squat (Bodyweight))
334
+ */
335
+ quadriceps: ['32HKJ34K', 'A733CC5B', '9694DA61'],
336
+ /**
337
+ * - F6948F17 (Frog Pumps (Dumbbell))
338
+ * - 487B3755 (Single Leg Hip Thrust)
339
+ * - CDA23948 (Glute Bridge)
340
+ */
341
+ hamstrings: ['F6948F17', '487B3755', 'CDA23948'],
342
+ glutes: [],
343
+ lats: [],
344
+ lower_back: [],
345
+ upper_back: [],
346
+ shoulders: [],
347
+ biceps: [],
348
+ adductors: [],
349
+ abductors: [],
350
+ calves: [],
351
+ forearms: [],
352
+ abdominals: [],
353
+ other: [],
354
+ cardio: [],
355
+ traps: [],
356
+ neck: [],
357
+ full_body: [],
358
+ };
359
+ /**
360
+ * Sorts exercises by priority for each muscle group based on the provided priorities
361
+ * and adds any remaining exercises from the store that weren't in the priorities.
362
+ *
363
+ * @param exercisePriorities - Object mapping muscle groups to arrays of exercise IDs in priority order
364
+ * @param exerciseStore - Array of all available exercises
365
+ * @returns Object mapping muscle groups to arrays of exercises sorted by priority
366
+ */
367
+ const getPrioritySortedExercises = (exercisePriorities, exerciseStore) => {
368
+ // Have a map of muscle group to exercises sorted by exercise priority
369
+ const sortedExercises = Object.entries(exercisePriorities).reduce((acc, [muscleGroup, exercises]) => {
370
+ const foundExercises = exercises
371
+ .map((exercise) => exerciseStore.find((e) => e.id === exercise))
372
+ .filter((exercise) => exercise !== undefined);
373
+ // Debug: Log missing exercise IDs
374
+ const missingIds = exercises.filter((id) => !exerciseStore.some((e) => e.id === id));
375
+ if (missingIds.length > 0) {
376
+ console.log(`Missing the following exercises with IDs in the store for muscle group ${muscleGroup}:`, missingIds);
377
+ }
378
+ acc[muscleGroup] = foundExercises;
379
+ return acc;
380
+ }, {});
381
+ // Then add remaining exercises to the map
382
+ Object.keys(sortedExercises).forEach((muscleGroup) => {
383
+ const remainingExercises = exerciseStore
384
+ .filter((e) => e.muscle_group === muscleGroup &&
385
+ !sortedExercises[muscleGroup].includes(e))
386
+ .sort((a, b) => b.priority - a.priority);
387
+ sortedExercises[muscleGroup].push(...remainingExercises);
388
+ });
389
+ return sortedExercises;
390
+ };
391
+ exports.getPrioritySortedExercises = getPrioritySortedExercises;
392
+ const getMuscleGroup = ({ muscleGroupPrescription, programFocusMuscleExerciseCount, focusMuscle, routineName, }) => {
393
+ if (muscleGroupPrescription === 'focus_muscle') {
394
+ // If the user has selected a focus muscle and it is allowed for the routine,
395
+ // we return the focus muscle extra exercise
396
+ if (!!focusMuscle &&
397
+ isFocusMuscleExtraExerciseAllowed(routineName, focusMuscle)) {
398
+ const n = _1.simplifiedMuscleGroupToMuscleGroups[focusMuscle].length;
399
+ return _1.simplifiedMuscleGroupToMuscleGroups[focusMuscle][programFocusMuscleExerciseCount % n];
400
+ }
401
+ // If the user has not selected a focus muscle or it is not allowed for the routine
402
+ // we skip this extra exercise for the focus muscle in the program
403
+ return undefined;
404
+ }
405
+ return muscleGroupPrescription;
406
+ };
407
+ /**
408
+ * Checks if an exercise matches the given criteria for selection
409
+ */
410
+ const isExerciseMatch = (exercise, criteria) => {
411
+ var _a, _b, _c, _d;
412
+ const categoryMatch = criteria.exerciseCategory === 'all' ||
413
+ exercise.category === criteria.exerciseCategory ||
414
+ (exercise.category === 'assistance-compound' &&
415
+ criteria.exerciseCategory === 'compound');
416
+ const levelMatch = (_b = (_a = exercise.level) === null || _a === void 0 ? void 0 : _a.includes(criteria.level)) !== null && _b !== void 0 ? _b : false;
417
+ const goalMatch = (_d = (_c = exercise.goal) === null || _c === void 0 ? void 0 : _c.includes(criteria.goal)) !== null && _d !== void 0 ? _d : false;
418
+ const equipmentMatch = (0, exports.isEquipmentCompatible)(exercise, criteria.equipments);
419
+ const barbellLimitOk = isBarbellExerciseAllowed(exercise, criteria.routineBarbellExerciseCount, criteria.equipments, criteria.frequency);
420
+ return (categoryMatch && levelMatch && goalMatch && equipmentMatch && barbellLimitOk);
421
+ };
422
+ /**
423
+ * Checks if an exercise is a valid alternative isolation exercise
424
+ */
425
+ const isAlternativeIsolationExerciseMatch = (exercise, criteria) => {
426
+ var _a, _b;
427
+ const isIsolationCategory = criteria.exerciseCategory === 'isolation';
428
+ const isAlternativeExercise = isolationExerciseAlternatives[criteria.muscleGroup].includes(exercise.id);
429
+ const equipmentMatch = (0, exports.isEquipmentCompatible)(exercise, criteria.equipments);
430
+ const levelMatch = (_b = (_a = exercise.level) === null || _a === void 0 ? void 0 : _a.includes(criteria.level)) !== null && _b !== void 0 ? _b : false;
431
+ return (isIsolationCategory && isAlternativeExercise && equipmentMatch && levelMatch);
432
+ };
433
+ /**
434
+ * Finds an exercise that matches the criteria and is not already used
435
+ */
436
+ const findMatchingExercise = (exercises, criteria, context) => {
437
+ return exercises.find((exercise) => {
438
+ const matchesCriteria = isExerciseMatch(exercise, criteria);
439
+ const isNotUsed = !isExerciseUsed(exercise, context);
440
+ return matchesCriteria && isNotUsed;
441
+ });
442
+ };
443
+ /**
444
+ * Finds an alternative isolation exercise that matches the criteria
445
+ */
446
+ const findAlternativeIsolationExercise = (exercises, criteria, context) => {
447
+ return exercises.find((exercise) => {
448
+ const matchesCriteria = isAlternativeIsolationExerciseMatch(exercise, criteria);
449
+ const isNotUsed = !isExerciseUsed(exercise, context);
450
+ return matchesCriteria && isNotUsed;
451
+ });
452
+ };
453
+ const pickExerciseForPrescriptionWithTrace = (params) => {
454
+ const { sortedExercises, handPickedExercises, criteria, context } = params;
455
+ const trace = { entries: [] };
456
+ const exercises = handPickedExercises
457
+ ? [...handPickedExercises, ...sortedExercises[criteria.muscleGroup]]
458
+ : sortedExercises[criteria.muscleGroup];
459
+ const programContext = {
460
+ programUsedExerciseIds: context.programUsedExerciseIds,
461
+ excludedExerciseIds: context.excludedExerciseIds,
462
+ };
463
+ const routineContext = {
464
+ routineUsedExerciseIds: context.routineUsedExerciseIds,
465
+ excludedExerciseIds: context.excludedExerciseIds,
466
+ };
467
+ // Pass 1
468
+ let exercise = findMatchingExercise(exercises, criteria, programContext);
469
+ trace.entries.push({
470
+ pass: 1,
471
+ label: 'exact match; not used in program',
472
+ candidatePoolSize: exercises.length,
473
+ selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
474
+ });
475
+ if (exercise)
476
+ return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 1 }) };
477
+ // Pass 2
478
+ exercise = findMatchingExercise(exercises, criteria, routineContext);
479
+ trace.entries.push({
480
+ pass: 2,
481
+ label: 'exact match; not used in routine (reuse allowed in program)',
482
+ candidatePoolSize: exercises.length,
483
+ selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
484
+ });
485
+ if (exercise)
486
+ return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 2 }) };
487
+ // Pass 3
488
+ exercise = findAlternativeIsolationExercise(exercises, criteria, programContext);
489
+ trace.entries.push({
490
+ pass: 3,
491
+ label: 'handpicked alternative isolation; not used in program',
492
+ candidatePoolSize: exercises.length,
493
+ selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
494
+ });
495
+ if (exercise)
496
+ return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 3 }) };
497
+ // Pass 4
498
+ exercise = findAlternativeIsolationExercise(exercises, criteria, routineContext);
499
+ trace.entries.push({
500
+ pass: 4,
501
+ label: 'handpicked alternative isolation; not used in routine (reuse allowed in program)',
502
+ candidatePoolSize: exercises.length,
503
+ selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
504
+ });
505
+ if (exercise)
506
+ return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 4 }) };
507
+ // Pass 5
508
+ exercise = findMatchingExercise(exercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), routineContext);
509
+ trace.entries.push({
510
+ pass: 5,
511
+ label: 'match regardless of isolation or compound; not used in routine (reuse allowed in program)',
512
+ candidatePoolSize: exercises.length,
513
+ selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
514
+ });
515
+ if (exercise)
516
+ return { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 5 }) };
517
+ // Pass 6
518
+ const secondaryMuscleExercises = Object.values(sortedExercises)
519
+ .flat()
520
+ .filter((e) => e.other_muscles.includes(criteria.muscleGroup));
521
+ exercise = findMatchingExercise(secondaryMuscleExercises, Object.assign(Object.assign({}, criteria), { exerciseCategory: 'all' }), routineContext);
522
+ trace.entries.push({
523
+ pass: 6,
524
+ label: 'secondary muscle match; regardless of isolation or compound; not used in routine (reuse allowed in program)',
525
+ candidatePoolSize: secondaryMuscleExercises.length,
526
+ selectedExerciseId: exercise === null || exercise === void 0 ? void 0 : exercise.id,
527
+ });
528
+ return exercise
529
+ ? { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 6 }) }
530
+ : { exercise: undefined, trace };
531
+ };
532
+ function pickExerciseForPrescription(params) {
533
+ if (params.withTrace) {
534
+ return pickExerciseForPrescriptionWithTrace(params);
535
+ }
536
+ return pickExerciseForPrescriptionWithTrace(params).exercise;
537
+ }
538
+ function generateProgram(params) {
539
+ var _a, _b;
540
+ const { trainerAlgorithmSettings, frequency, goal, level, equipments, workoutDurationMinutes, restTimerLength, exerciseStore, focusMuscle, excludedExerciseIds, debugExerciseSelectionTrace, } = params;
541
+ const exerciseSelectionTraces = [];
542
+ const routines = exports.programSplits[frequency];
543
+ const program = {
544
+ name: frequency,
545
+ routines: [],
546
+ };
547
+ const sortedExercises = (0, exports.getPrioritySortedExercises)(trainerAlgorithmSettings.exercise_priorities, exerciseStore);
548
+ const programUsedExerciseIds = new Set();
549
+ let programFocusMuscleExerciseCount = 0;
550
+ const allErrors = [];
551
+ for (const routine of routines) {
552
+ const routineTemplate = trainerAlgorithmSettings.templates[routine];
553
+ let routineBarbellExerciseCount = 0;
554
+ const routineUsedExerciseIds = new Set();
555
+ const routineExercises = [];
556
+ for (let prescriptionIndex = 0; prescriptionIndex < routineTemplate.exercises.length; prescriptionIndex++) {
557
+ const exercisePrescription = routineTemplate.exercises[prescriptionIndex];
558
+ const muscleGroup = getMuscleGroup({
559
+ muscleGroupPrescription: exercisePrescription.muscle_group,
560
+ programFocusMuscleExerciseCount,
561
+ focusMuscle,
562
+ routineName: routine,
563
+ });
564
+ // If the muscle group is not found, skip the exercise
565
+ if (!muscleGroup) {
566
+ continue;
567
+ }
568
+ // If the exercise does not meet the min workout duration limit, skip the exercise
569
+ const execiseDoesNotMeetMinWorkoutDurationLimit = exercisePrescription.min_workout_duration_limit
570
+ ? exercisePrescription.min_workout_duration_limit >
571
+ workoutDurationMinutes
572
+ : false;
573
+ if (execiseDoesNotMeetMinWorkoutDurationLimit) {
574
+ continue;
575
+ }
576
+ const criteria = {
577
+ frequency,
578
+ exerciseCategory: exercisePrescription.category,
579
+ equipments,
580
+ muscleGroup,
581
+ routineBarbellExerciseCount,
582
+ level,
583
+ goal,
584
+ };
585
+ let exercise;
586
+ if (debugExerciseSelectionTrace) {
587
+ const selection = pickExerciseForPrescription({
588
+ sortedExercises,
589
+ criteria,
590
+ context: {
591
+ programUsedExerciseIds,
592
+ routineUsedExerciseIds,
593
+ excludedExerciseIds,
594
+ },
595
+ withTrace: true,
596
+ });
597
+ exercise = selection.exercise;
598
+ exerciseSelectionTraces.push({
599
+ routine,
600
+ prescriptionIndex,
601
+ prescription: exercisePrescription,
602
+ resolvedMuscleGroup: muscleGroup,
603
+ criteria,
604
+ selectedExerciseId: (_a = selection.exercise) === null || _a === void 0 ? void 0 : _a.id,
605
+ trace: selection.trace,
606
+ });
607
+ }
608
+ else {
609
+ exercise = pickExerciseForPrescription({
610
+ sortedExercises,
611
+ criteria,
612
+ context: {
613
+ programUsedExerciseIds,
614
+ routineUsedExerciseIds,
615
+ excludedExerciseIds,
616
+ },
617
+ });
618
+ }
619
+ if (!!exercise) {
620
+ programUsedExerciseIds.add(exercise.id);
621
+ routineUsedExerciseIds.add(exercise.id);
622
+ if (exercise.equipment_category === 'barbell')
623
+ routineBarbellExerciseCount++;
624
+ if (exercisePrescription.muscle_group === 'focus_muscle')
625
+ programFocusMuscleExerciseCount++;
626
+ const repRange = (0, exports.getTrainerRepRange)(trainerAlgorithmSettings, goal, exercisePrescription.category);
627
+ routineExercises.push({
628
+ exerciseTemplate: exercise,
629
+ muscleGroup: exercisePrescription.muscle_group,
630
+ category: exercisePrescription.category,
631
+ sets: (0, exports.getTrainerSetCount)(trainerAlgorithmSettings, goal, frequency),
632
+ repRangeStart: repRange.rep_range_start,
633
+ repRangeEnd: repRange.rep_range_end,
634
+ restTimerSeconds: (0, exports.getTrainerRestTimerSeconds)(trainerAlgorithmSettings, goal, restTimerLength, exercisePrescription.category),
635
+ warmupSetCount: exercisePrescription.warmup_set_count,
636
+ notes: (_b = trainerAlgorithmSettings.exercise_notes[exercise.id]) !== null && _b !== void 0 ? _b : '',
637
+ });
638
+ }
639
+ else {
640
+ // Collect error instead of throwing
641
+ allErrors.push({
642
+ type: 'exercise_not_found',
643
+ prescription: exercisePrescription,
644
+ muscleGroup,
645
+ context: {
646
+ goal,
647
+ level,
648
+ equipments,
649
+ focusMuscle,
650
+ },
651
+ });
652
+ }
653
+ }
654
+ program.routines.push({
655
+ name: routine,
656
+ notes: routineTemplate.notes,
657
+ exercises: routineExercises,
658
+ });
659
+ }
660
+ // Return result based on whether there were errors
661
+ if (allErrors.length > 0) {
662
+ const result = {
663
+ success: false,
664
+ errors: allErrors,
665
+ partialProgram: program,
666
+ };
667
+ return debugExerciseSelectionTrace
668
+ ? Object.assign(Object.assign({}, result), { exerciseSelectionTraces }) : result;
669
+ }
670
+ const result = {
671
+ success: true,
672
+ program,
673
+ };
674
+ return debugExerciseSelectionTrace
675
+ ? Object.assign(Object.assign({}, result), { exerciseSelectionTraces }) : result;
676
+ }