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.
- package/built/hevyTrainer.d.ts +45 -54
- package/built/hevyTrainer.js +64 -65
- package/built/index.d.ts +109 -9
- package/built/index.js +1 -0
- package/built/normalizedWorkoutUtils.d.ts +3 -2
- package/built/tests/hevyTrainer.test.js +62 -75
- package/built/tests/testUtils.js +1 -0
- package/built/typeUtils.d.ts +6 -0
- package/package.json +1 -1
package/built/hevyTrainer.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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]:
|
|
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
|
|
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
|
-
|
|
161
|
-
export interface TrainerProgramResistanceExercise {
|
|
161
|
+
export interface TrainerProgramResistanceExercise<T> {
|
|
162
162
|
kind: 'resistance';
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
174
|
-
export interface TrainerProgramCardioExercise {
|
|
169
|
+
export interface TrainerProgramCardioExercise<T> {
|
|
175
170
|
kind: 'cardio';
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
exercise_template: T;
|
|
172
|
+
set_count: 1;
|
|
173
|
+
duration_seconds: number;
|
|
178
174
|
}
|
|
179
|
-
export
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
188
|
-
|
|
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) =>
|
|
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
|
|
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:
|
|
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:
|
|
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 {};
|
package/built/hevyTrainer.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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,
|
|
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 =
|
|
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.
|
|
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,
|
|
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
|
|
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(
|
|
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
|
|
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.
|
|
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
|
|
469
|
-
|
|
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,
|
|
485
|
+
exercise = findMatchingExercise(exercises, criteria, workoutContext);
|
|
484
486
|
trace.entries.push({
|
|
485
487
|
pass: 2,
|
|
486
|
-
label: 'exact match; not used in
|
|
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,
|
|
505
|
+
exercise = findAlternativeIsolationExercise(exercises, criteria, workoutContext);
|
|
504
506
|
trace.entries.push({
|
|
505
507
|
pass: 4,
|
|
506
|
-
label: 'handpicked alternative isolation; not used in
|
|
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' }),
|
|
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
|
|
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' }),
|
|
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
|
|
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
|
|
546
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
571
|
-
|
|
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
|
-
|
|
577
|
+
workoutExercises.unshift(slot);
|
|
575
578
|
else
|
|
576
|
-
|
|
579
|
+
workoutExercises.push(slot);
|
|
577
580
|
if (debugExerciseSelectionTrace) {
|
|
578
581
|
exerciseSelectionTraces.push({
|
|
579
582
|
traceKind: 'cardio',
|
|
580
|
-
|
|
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
|
|
598
|
+
const workoutTemplates = exports.programSplits[frequency];
|
|
596
599
|
const program = {
|
|
597
|
-
|
|
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
|
|
606
|
-
const
|
|
607
|
-
let
|
|
608
|
-
const
|
|
609
|
-
const
|
|
610
|
-
for (let prescriptionIndex = 0; prescriptionIndex <
|
|
611
|
-
const exercisePrescription =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
671
|
+
workoutUsedExerciseIds,
|
|
670
672
|
excludedExerciseIds,
|
|
671
673
|
},
|
|
672
674
|
});
|
|
673
675
|
}
|
|
674
676
|
if (!!exercise) {
|
|
675
677
|
programUsedExerciseIds.add(exercise.id);
|
|
676
|
-
|
|
678
|
+
workoutUsedExerciseIds.add(exercise.id);
|
|
677
679
|
if (exercise.equipment_category === 'barbell')
|
|
678
|
-
|
|
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
|
-
|
|
684
|
+
workoutExercises.push({
|
|
683
685
|
kind: 'resistance',
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
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
|
-
|
|
711
|
+
attachOptionalCardioToWorkoutTemplate({
|
|
711
712
|
cardioPreference,
|
|
712
713
|
sortedExercises,
|
|
713
714
|
criteria: { equipments, level, goal, frequency, excludedExerciseIds },
|
|
714
715
|
debugExerciseSelectionTrace,
|
|
715
716
|
exerciseSelectionTraces,
|
|
716
|
-
|
|
717
|
-
templatePrescriptionCount:
|
|
718
|
-
|
|
717
|
+
workoutTemplate: workout,
|
|
718
|
+
templatePrescriptionCount: workoutTemplate.exercises.length,
|
|
719
|
+
workoutExercises,
|
|
719
720
|
});
|
|
720
|
-
program.
|
|
721
|
-
name:
|
|
722
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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: [],
|
|
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
|
|
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
|
|
406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
expect(
|
|
620
|
-
|
|
621
|
-
|
|
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.
|
|
627
|
-
expect(exercise.
|
|
628
|
-
expect(exercise.
|
|
629
|
-
expect(exercise.
|
|
630
|
-
|
|
631
|
-
expect(exercise.
|
|
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
|
|
651
|
+
// The workout template structure still exists in the partial program, just without
|
|
660
652
|
// any resolved exercises.
|
|
661
|
-
expect(result.partialProgram.
|
|
662
|
-
expect(result.partialProgram.
|
|
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
|
|
694
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
964
|
+
expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
|
|
975
965
|
kind: 'cardio',
|
|
976
|
-
|
|
966
|
+
exercise_template: { id: 'run' },
|
|
977
967
|
});
|
|
978
|
-
expect(result.program.
|
|
968
|
+
expect(result.program.workoutTemplates[1].exercises[0]).toMatchObject({
|
|
979
969
|
kind: 'cardio',
|
|
980
|
-
|
|
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.
|
|
1009
|
+
expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
|
|
1020
1010
|
kind: 'cardio',
|
|
1021
|
-
|
|
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.
|
|
1050
|
+
expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
|
|
1061
1051
|
kind: 'cardio',
|
|
1062
|
-
|
|
1052
|
+
exercise_template: { id: 'run-b' },
|
|
1063
1053
|
});
|
|
1064
1054
|
});
|
|
1065
|
-
it('keeps global trace order aligned with
|
|
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.
|
|
1100
|
-
const idxB = result.exerciseSelectionTraces.findIndex((t) => t.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
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 = (
|
|
1284
|
+
const runFocusMuscleProbe = (workoutTemplateName, focusMuscle, frequency, candidateExercise) => {
|
|
1298
1285
|
var _a;
|
|
1299
1286
|
const settings = makeSettings();
|
|
1300
|
-
settings.templates[
|
|
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
|
|
1317
|
-
const
|
|
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 =
|
|
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
|
|
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
|
package/built/tests/testUtils.js
CHANGED
package/built/typeUtils.d.ts
CHANGED
|
@@ -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;
|