hevy-shared 1.0.714 → 1.0.715

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.
@@ -5,6 +5,34 @@ export type HevyTrainerProgramEquipment = Extract<Equipment, 'barbell' | 'dumbbe
5
5
  export declare const hevyTrainerExerciseCategories: readonly ["compound", "isolation"];
6
6
  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"];
7
7
  export type exerciseId = string;
8
+ export interface ExerciseSelectionCriteria {
9
+ exerciseCategory: HevyTrainerExerciseCategory;
10
+ equipments: HevyTrainerProgramEquipment[];
11
+ routineBarbellExerciseCount: number;
12
+ level: TrainingLevel;
13
+ goal: TrainingGoal;
14
+ muscleGroup: MuscleGroup;
15
+ }
16
+ export interface ExerciseSelectionContext {
17
+ programUsedExerciseIds?: Set<string>;
18
+ routineUsedExerciseIds?: Set<string>;
19
+ excludedExerciseIds?: Set<string>;
20
+ }
21
+ export interface ExerciseSelectionParams {
22
+ sortedExercises: Record<MuscleGroup, HevyTrainerLibraryExercise[]>;
23
+ criteria: ExerciseSelectionCriteria;
24
+ context: ExerciseSelectionContext;
25
+ }
26
+ export interface ProgramGenerationParams<T extends HevyTrainerLibraryExercise> {
27
+ trainerPreset: TrainerPreset;
28
+ selectedDays: WeeklyTrainingFrequency;
29
+ selectedGoal: TrainingGoal;
30
+ selectedLevel: TrainingLevel;
31
+ selectedEquipments: HevyTrainerProgramEquipment[];
32
+ exerciseStore: T[];
33
+ focusMuscle?: SimplifiedMuscleGroup;
34
+ excludedExerciseIds?: Set<string>;
35
+ }
8
36
  export declare const frequencyMap: Record<WeeklyTrainingFrequency, string>;
9
37
  export declare const programSplits: Record<WeeklyTrainingFrequency, HevyTrainerRoutineName[]>;
10
38
  export type SetsPerGoal = {
@@ -84,6 +112,21 @@ export interface TrainerProgram {
84
112
  name: WeeklyTrainingFrequency;
85
113
  routines: TrainerProgramRoutine[];
86
114
  }
115
+ /**
116
+ * Sorts exercises by priority for each muscle group based on the provided priorities
117
+ * and adds any remaining exercises from the store that weren't in the priorities.
118
+ *
119
+ * @param exercisePriorities - Object mapping muscle groups to arrays of exercise IDs in priority order
120
+ * @param exerciseStore - Array of all available exercises
121
+ * @returns Object mapping muscle groups to arrays of exercises sorted by priority
122
+ */
123
+ export declare const getPrioritySortedExercises: <T extends HevyTrainerLibraryExercise>(exercisePriorities: ExercisePriorities, exerciseStore: T[]) => Record<MuscleGroup, T[]>;
124
+ /**
125
+ * Selects the best exercise for a given prescription using a multi-pass strategy
126
+ */
127
+ export declare const pickExerciseForPrescription: <T extends HevyTrainerLibraryExercise>(params: ExerciseSelectionParams & {
128
+ sortedExercises: Record<MuscleGroup, T[]>;
129
+ }) => T | undefined;
87
130
  export type HevyTrainerLibraryExercise = Pick<LibraryExercise, 'id' | 'title' | 'priority' | 'muscle_group' | 'other_muscles' | 'exercise_type' | 'equipment_category' | 'category' | 'level' | 'goal'>;
88
131
  export interface ExercisePrescriptionError {
89
132
  type: 'exercise_not_found';
@@ -115,12 +158,7 @@ export interface TrainerProgramError {
115
158
  partialProgram?: TrainerProgram;
116
159
  }
117
160
  export type TrainerProgramAttempt = TrainerProgramResult | TrainerProgramError;
118
- export declare const generateProgram: ({ trainerPreset, selectedDays, selectedGoal, selectedLevel, selectedEquipments, exerciseStore, focusMuscle, }: {
119
- trainerPreset: TrainerPreset;
120
- selectedDays: WeeklyTrainingFrequency;
121
- selectedGoal: TrainingGoal;
122
- selectedLevel: TrainingLevel;
123
- selectedEquipments: HevyTrainerProgramEquipment[];
124
- exerciseStore: HevyTrainerLibraryExercise[];
125
- focusMuscle?: SimplifiedMuscleGroup;
126
- }) => TrainerProgramAttempt;
161
+ /**
162
+ * Generates a complete training program based on the provided parameters
163
+ */
164
+ export declare const generateProgram: <T extends HevyTrainerLibraryExercise>(params: ProgramGenerationParams<T>) => TrainerProgramAttempt;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateProgram = exports.programSplits = exports.frequencyMap = exports.routineNames = exports.hevyTrainerExerciseCategories = void 0;
3
+ exports.generateProgram = exports.pickExerciseForPrescription = exports.getPrioritySortedExercises = exports.programSplits = exports.frequencyMap = exports.routineNames = exports.hevyTrainerExerciseCategories = void 0;
4
4
  const _1 = require(".");
5
+ const MAX_BARBELL_EXERCISES_PER_ROUTINE = 2;
5
6
  exports.hevyTrainerExerciseCategories = ['compound', 'isolation'];
6
7
  exports.routineNames = [
7
8
  // Full body 1x
@@ -48,6 +49,24 @@ exports.programSplits = {
48
49
  5: ['push_1', 'pull_1', 'legs_1', 'upper_2', 'lower_2'],
49
50
  6: ['push_2_a', 'pull_2_a', 'legs_2_a', 'push_2_b', 'pull_2_b', 'legs_2_b'],
50
51
  };
52
+ // Utility functions for better code organization
53
+ const isEquipmentCompatible = (exercise, allowedEquipments) => {
54
+ return (allowedEquipments.includes(exercise.equipment_category) ||
55
+ exercise.equipment_category === 'none' ||
56
+ exercise.equipment_category === 'other');
57
+ };
58
+ const isBarbellLimitExceeded = (exercise, routineBarbellExerciseCount, allowedEquipments) => {
59
+ return (exercise.equipment_category === 'barbell' &&
60
+ routineBarbellExerciseCount >= MAX_BARBELL_EXERCISES_PER_ROUTINE &&
61
+ allowedEquipments.length > 1);
62
+ };
63
+ const isExerciseUsed = (exercise, context) => {
64
+ var _a, _b, _c;
65
+ return (((_a = context.programUsedExerciseIds) === null || _a === void 0 ? void 0 : _a.has(exercise.id)) ||
66
+ ((_b = context.routineUsedExerciseIds) === null || _b === void 0 ? void 0 : _b.has(exercise.id)) ||
67
+ ((_c = context.excludedExerciseIds) === null || _c === void 0 ? void 0 : _c.has(exercise.id)) ||
68
+ false);
69
+ };
51
70
  const isolationExerciseAlternatives = {
52
71
  chest: ['B74A95BB', '392887AA', '39C99849'],
53
72
  triceps: ['6575F52D', 'CD6DC8E5'],
@@ -70,68 +89,145 @@ const isolationExerciseAlternatives = {
70
89
  neck: [],
71
90
  full_body: [],
72
91
  };
92
+ /**
93
+ * Sorts exercises by priority for each muscle group based on the provided priorities
94
+ * and adds any remaining exercises from the store that weren't in the priorities.
95
+ *
96
+ * @param exercisePriorities - Object mapping muscle groups to arrays of exercise IDs in priority order
97
+ * @param exerciseStore - Array of all available exercises
98
+ * @returns Object mapping muscle groups to arrays of exercises sorted by priority
99
+ */
100
+ const getPrioritySortedExercises = (exercisePriorities, exerciseStore) => {
101
+ // Have a map of muscle group to exercises sorted by exercise priority
102
+ const sortedExercises = Object.entries(exercisePriorities).reduce((acc, [muscleGroup, exercises]) => {
103
+ const foundExercises = exercises
104
+ .map((exercise) => exerciseStore.find((e) => e.id === exercise))
105
+ .filter((exercise) => exercise !== undefined);
106
+ // Debug: Log missing exercise IDs
107
+ const missingIds = exercises.filter((id) => !exerciseStore.some((e) => e.id === id));
108
+ if (missingIds.length > 0) {
109
+ console.log(`Missing the following exercises with IDs in the store for muscle group ${muscleGroup}:`, missingIds);
110
+ }
111
+ acc[muscleGroup] = foundExercises;
112
+ return acc;
113
+ }, {});
114
+ // Then add remaining exercises to the map
115
+ Object.keys(sortedExercises).forEach((muscleGroup) => {
116
+ const remainingExercises = exerciseStore
117
+ .filter((e) => e.muscle_group === muscleGroup &&
118
+ !sortedExercises[muscleGroup].includes(e))
119
+ .sort((a, b) => b.priority - a.priority);
120
+ sortedExercises[muscleGroup].push(...remainingExercises);
121
+ });
122
+ return sortedExercises;
123
+ };
124
+ exports.getPrioritySortedExercises = getPrioritySortedExercises;
73
125
  const getMuscleGroup = ({ muscleGroupPrescription, programFocusMuscleExerciseCount, focusMuscle, }) => {
74
126
  if (muscleGroupPrescription === 'focus_muscle') {
75
127
  if (!!focusMuscle) {
76
128
  const n = _1.simplifiedMuscleGroupToMuscleGroups[focusMuscle].length;
77
129
  return _1.simplifiedMuscleGroupToMuscleGroups[focusMuscle][programFocusMuscleExerciseCount % n];
78
130
  }
79
- // User has not selected a focus muscle, so we skip this extra exercise for the focus muscle
131
+ // User has not selected a focus muscle, so we skip this extra exercise for the focus muscle in the program
80
132
  return undefined;
81
133
  }
82
134
  return muscleGroupPrescription;
83
135
  };
84
- const isExerciseMatch = ({ exercisePrescription, exercise, equipments, routineBarbellExerciseCount, level, goal, }) => {
136
+ /**
137
+ * Checks if an exercise matches the given criteria for selection
138
+ */
139
+ const isExerciseMatch = (exercise, criteria) => {
140
+ var _a, _b, _c, _d;
141
+ const categoryMatch = exercise.category === criteria.exerciseCategory ||
142
+ (exercise.category === 'assistance-compound' &&
143
+ criteria.exerciseCategory === 'compound');
144
+ const levelMatch = (_b = (_a = exercise.level) === null || _a === void 0 ? void 0 : _a.includes(criteria.level)) !== null && _b !== void 0 ? _b : false;
145
+ const goalMatch = (_d = (_c = exercise.goal) === null || _c === void 0 ? void 0 : _c.includes(criteria.goal)) !== null && _d !== void 0 ? _d : false;
146
+ const equipmentMatch = isEquipmentCompatible(exercise, criteria.equipments);
147
+ const barbellLimitOk = !isBarbellLimitExceeded(exercise, criteria.routineBarbellExerciseCount, criteria.equipments);
148
+ return (categoryMatch && levelMatch && goalMatch && equipmentMatch && barbellLimitOk);
149
+ };
150
+ /**
151
+ * Checks if an exercise is a valid alternative isolation exercise
152
+ */
153
+ const isAlternativeIsolationExerciseMatch = (exercise, criteria) => {
85
154
  var _a, _b;
86
- return ((exercise.category === exercisePrescription.category ||
87
- (exercise.category === 'assistance-compound' && // Airtable also has `assistance-compound` as another category
88
- exercisePrescription.category === 'compound')) && // but we are not differentiating between them and using `compound` instead
89
- ((_a = exercise.level) === null || _a === void 0 ? void 0 : _a.includes(level)) &&
90
- ((_b = exercise.goal) === null || _b === void 0 ? void 0 : _b.includes(goal)) &&
91
- (equipments.includes(exercise.equipment_category) ||
92
- exercise.equipment_category === 'none' ||
93
- exercise.equipment_category === 'other') &&
94
- // Up to 2 barbell exercises per routine, unless there is only one equipment category selected
95
- (exercise.equipment_category !== 'barbell' ||
96
- routineBarbellExerciseCount < 2 ||
97
- equipments.length === 1));
155
+ const isIsolationCategory = criteria.exerciseCategory === 'isolation';
156
+ const isAlternativeExercise = isolationExerciseAlternatives[criteria.muscleGroup].includes(exercise.id);
157
+ const equipmentMatch = isEquipmentCompatible(exercise, criteria.equipments);
158
+ const levelMatch = (_b = (_a = exercise.level) === null || _a === void 0 ? void 0 : _a.includes(criteria.level)) !== null && _b !== void 0 ? _b : false;
159
+ return (isIsolationCategory && isAlternativeExercise && equipmentMatch && levelMatch);
98
160
  };
99
- const isAlternativeIsolationExerciseMatch = ({ exercise, exercisePrescription, equipments, level, muscleGroup, }) => {
100
- var _a;
101
- return (exercisePrescription.category === 'isolation' &&
102
- isolationExerciseAlternatives[muscleGroup].includes(exercise.id) &&
103
- (equipments.includes(exercise.equipment_category) ||
104
- exercise.equipment_category === 'none' ||
105
- exercise.equipment_category === 'other') &&
106
- ((_a = exercise.level) === null || _a === void 0 ? void 0 : _a.includes(level)));
161
+ /**
162
+ * Finds an exercise that matches the criteria and is not already used
163
+ */
164
+ const findMatchingExercise = (exercises, criteria, context) => {
165
+ return exercises.find((exercise) => {
166
+ const matchesCriteria = isExerciseMatch(exercise, criteria);
167
+ const isNotUsed = !isExerciseUsed(exercise, context);
168
+ return matchesCriteria && isNotUsed;
169
+ });
170
+ };
171
+ /**
172
+ * Finds an alternative isolation exercise that matches the criteria
173
+ */
174
+ const findAlternativeIsolationExercise = (exercises, criteria, context) => {
175
+ return exercises.find((exercise) => {
176
+ const matchesCriteria = isAlternativeIsolationExerciseMatch(exercise, criteria);
177
+ const isNotUsed = !isExerciseUsed(exercise, context);
178
+ return matchesCriteria && isNotUsed;
179
+ });
180
+ };
181
+ /**
182
+ * Selects the best exercise for a given prescription using a multi-pass strategy
183
+ */
184
+ const pickExerciseForPrescription = (params) => {
185
+ const { sortedExercises, criteria, context } = params;
186
+ const exercises = sortedExercises[criteria.muscleGroup];
187
+ // Pass 1: Find exact match not used in program
188
+ const programContext = {
189
+ programUsedExerciseIds: context.programUsedExerciseIds,
190
+ excludedExerciseIds: context.excludedExerciseIds,
191
+ };
192
+ let exercise = findMatchingExercise(exercises, criteria, programContext);
193
+ if (exercise)
194
+ return exercise;
195
+ // Pass 2: Find exact match not used in routine (allow reuse in program)
196
+ const routineContext = {
197
+ routineUsedExerciseIds: context.routineUsedExerciseIds,
198
+ excludedExerciseIds: context.excludedExerciseIds,
199
+ };
200
+ exercise = findMatchingExercise(exercises, criteria, routineContext);
201
+ if (exercise)
202
+ return exercise;
203
+ // Pass 3: Find alternative isolation exercise not used in program
204
+ exercise = findAlternativeIsolationExercise(exercises, criteria, programContext);
205
+ if (exercise)
206
+ return exercise;
207
+ // Pass 4: Find alternative isolation exercise not used in routine
208
+ exercise = findAlternativeIsolationExercise(exercises, criteria, routineContext);
209
+ return exercise;
107
210
  };
108
- const generateProgram = ({ trainerPreset, selectedDays, selectedGoal, selectedLevel, selectedEquipments, exerciseStore, focusMuscle, }) => {
211
+ exports.pickExerciseForPrescription = pickExerciseForPrescription;
212
+ /**
213
+ * Generates a complete training program based on the provided parameters
214
+ */
215
+ const generateProgram = (params) => {
109
216
  var _a;
217
+ const { trainerPreset, selectedDays, selectedGoal, selectedLevel, selectedEquipments, exerciseStore, focusMuscle, excludedExerciseIds, } = params;
110
218
  const routines = exports.programSplits[selectedDays];
111
219
  const program = {
112
220
  name: selectedDays,
113
221
  routines: [],
114
222
  };
115
- // Have a map of muscle group to exercises sorted by exercise priority
116
- const sortedExercises = Object.entries(trainerPreset.settings.exercise_priorities).reduce((acc, [muscleGroup, exercises]) => {
117
- acc[muscleGroup] = exercises.map((exercise) => exerciseStore.find((e) => e.id === exercise));
118
- return acc;
119
- }, {});
120
- // Then add remaining exercises to the map
121
- Object.keys(sortedExercises).forEach((muscleGroup) => {
122
- const remainingExercises = exerciseStore
123
- .filter((e) => e.muscle_group === muscleGroup &&
124
- !sortedExercises[muscleGroup].includes(e))
125
- .sort((a, b) => b.priority - a.priority);
126
- sortedExercises[muscleGroup].push(...remainingExercises);
127
- });
128
- const programUsedExercises = new Set();
223
+ const sortedExercises = (0, exports.getPrioritySortedExercises)(trainerPreset.settings.exercise_priorities, exerciseStore);
224
+ const programUsedExerciseIds = new Set();
129
225
  let programFocusMuscleExerciseCount = 0;
130
226
  const allErrors = [];
131
227
  for (const routine of routines) {
132
228
  const routineTemplate = trainerPreset.settings.templates[routine];
133
229
  let routineBarbellExerciseCount = 0;
134
- const routineUsedExercises = new Set();
230
+ const routineUsedExerciseIds = new Set();
135
231
  const routineExercises = [];
136
232
  for (const exercisePrescription of routineTemplate.exercises) {
137
233
  const muscleGroup = getMuscleGroup({
@@ -143,55 +239,25 @@ const generateProgram = ({ trainerPreset, selectedDays, selectedGoal, selectedLe
143
239
  if (!muscleGroup) {
144
240
  continue;
145
241
  }
146
- // First pass: Try to find an exact match for the exercise that is not used in the program
147
- let exercise = sortedExercises[muscleGroup].find((e) => isExerciseMatch({
148
- exercisePrescription,
149
- exercise: e,
150
- equipments: selectedEquipments,
151
- routineBarbellExerciseCount,
152
- level: selectedLevel,
153
- goal: selectedGoal,
154
- }) && !programUsedExercises.has(e.title));
155
- if (exercise === undefined) {
156
- // Second pass
157
- // By allowing the same exercise to be assigned in the same program more than once
158
- const extendedMatchingExercise = sortedExercises[muscleGroup].find((e) => isExerciseMatch({
159
- exercisePrescription,
160
- exercise: e,
242
+ const exercise = (0, exports.pickExerciseForPrescription)({
243
+ sortedExercises,
244
+ criteria: {
245
+ exerciseCategory: exercisePrescription.category,
161
246
  equipments: selectedEquipments,
247
+ muscleGroup,
162
248
  routineBarbellExerciseCount,
163
249
  level: selectedLevel,
164
250
  goal: selectedGoal,
165
- }) && !routineUsedExercises.has(e.title));
166
- exercise = extendedMatchingExercise;
167
- }
168
- if (exercise === undefined) {
169
- // Third pass
170
- // Check if isolation exercise alternatives are available and not used in the program
171
- const alternativeIsolationExercise = sortedExercises[muscleGroup].find((e) => isAlternativeIsolationExerciseMatch({
172
- exercise: e,
173
- exercisePrescription,
174
- equipments: selectedEquipments,
175
- level: selectedLevel,
176
- muscleGroup,
177
- }) && !programUsedExercises.has(e.title));
178
- exercise = alternativeIsolationExercise;
179
- }
180
- if (exercise === undefined) {
181
- // Fourth pass
182
- // Check if isolation exercise alternatives are available and not used in the workout
183
- const extendedIsolationExercise = sortedExercises[muscleGroup].find((e) => isAlternativeIsolationExerciseMatch({
184
- exercise: e,
185
- exercisePrescription,
186
- equipments: selectedEquipments,
187
- level: selectedLevel,
188
- muscleGroup,
189
- }) && !routineUsedExercises.has(e.title));
190
- exercise = extendedIsolationExercise;
191
- }
251
+ },
252
+ context: {
253
+ programUsedExerciseIds,
254
+ routineUsedExerciseIds,
255
+ excludedExerciseIds,
256
+ },
257
+ });
192
258
  if (!!exercise) {
193
- programUsedExercises.add(exercise.title);
194
- routineUsedExercises.add(exercise.title);
259
+ programUsedExerciseIds.add(exercise.id);
260
+ routineUsedExerciseIds.add(exercise.id);
195
261
  if (exercise.equipment_category === 'barbell')
196
262
  routineBarbellExerciseCount++;
197
263
  if (exercisePrescription.muscle_group === 'focus_muscle')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-shared",
3
- "version": "1.0.714",
3
+ "version": "1.0.715",
4
4
  "description": "",
5
5
  "main": "built/index.js",
6
6
  "types": "built/index.d.ts",