hevy-shared 1.0.985 → 1.0.986
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 +29 -5
- package/built/hevyTrainer.js +76 -10
- package/built/index.d.ts +13 -6
- package/built/index.js +7 -2
- package/built/tests/hevyTrainer.test.js +253 -29
- package/package.json +1 -1
package/built/hevyTrainer.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WeeklyTrainingFrequency, TrainingGoal, TrainingLevel, SimplifiedMuscleGroup, MuscleGroup, LibraryExercise, ExerciseCategory, GranularEquipment, HevyTrainerProgramEquipment, RestTimerLength } from '.';
|
|
1
|
+
import { WeeklyTrainingFrequency, TrainingGoal, TrainingLevel, SimplifiedMuscleGroup, MuscleGroup, LibraryExercise, ExerciseCategory, GranularEquipment, HevyTrainerProgramEquipment, RestTimerLength, CardioPreference } from '.';
|
|
2
2
|
export type HevyTrainerExerciseCategory = typeof hevyTrainerExerciseCategories[number];
|
|
3
3
|
export type HevyTrainerRoutineName = typeof routineNames[number];
|
|
4
4
|
export declare const workoutDurationOptions: readonly [40, 60, 80];
|
|
@@ -59,6 +59,7 @@ export interface ProgramGenerationParams<T extends HevyTrainerLibraryExercise> {
|
|
|
59
59
|
exerciseStore: T[];
|
|
60
60
|
focusMuscle?: SimplifiedMuscleGroup;
|
|
61
61
|
excludedExerciseIds?: Set<string>;
|
|
62
|
+
cardioPreference: CardioPreference;
|
|
62
63
|
/**
|
|
63
64
|
* When enabled, includes exercise selection trace breadcrumbs in the result
|
|
64
65
|
* for debugging. Intended for troubleshooting on backoffice; keep off in
|
|
@@ -66,7 +67,8 @@ export interface ProgramGenerationParams<T extends HevyTrainerLibraryExercise> {
|
|
|
66
67
|
*/
|
|
67
68
|
debugExerciseSelectionTrace?: boolean;
|
|
68
69
|
}
|
|
69
|
-
export interface
|
|
70
|
+
export interface TemplateExerciseSelectionTraceRecord {
|
|
71
|
+
traceKind: 'template';
|
|
70
72
|
routine: HevyTrainerRoutineName;
|
|
71
73
|
prescriptionIndex: number;
|
|
72
74
|
prescription: ExercisePrescription;
|
|
@@ -75,6 +77,16 @@ export interface ProgramExerciseSelectionTraceRecord {
|
|
|
75
77
|
selectedExerciseId?: string;
|
|
76
78
|
trace: ExerciseSelectionTrace;
|
|
77
79
|
}
|
|
80
|
+
export interface CardioExerciseSelectionTraceRecord {
|
|
81
|
+
traceKind: 'cardio';
|
|
82
|
+
routine: HevyTrainerRoutineName;
|
|
83
|
+
prescriptionIndex: number;
|
|
84
|
+
selectedExerciseId?: string;
|
|
85
|
+
equipments: GranularEquipment[];
|
|
86
|
+
level: TrainingLevel;
|
|
87
|
+
trace: ExerciseSelectionTrace;
|
|
88
|
+
}
|
|
89
|
+
export type ProgramExerciseSelectionTraceRecord = TemplateExerciseSelectionTraceRecord | CardioExerciseSelectionTraceRecord;
|
|
78
90
|
export type TrainerProgramAttemptWithTraces = TrainerProgramAttempt & {
|
|
79
91
|
exerciseSelectionTraces: ProgramExerciseSelectionTraceRecord[];
|
|
80
92
|
};
|
|
@@ -145,7 +157,9 @@ export interface TrainerAlgorithmSettings {
|
|
|
145
157
|
exercise_notes: ExerciseNotes;
|
|
146
158
|
exercise_replacements: ExerciseReplacements;
|
|
147
159
|
}
|
|
148
|
-
|
|
160
|
+
/** Resistance prescriptions from templates (sets, reps, rest, notes). */
|
|
161
|
+
export interface TrainerProgramResistanceExercise {
|
|
162
|
+
kind: 'resistance';
|
|
149
163
|
exerciseTemplate: HevyTrainerLibraryExercise;
|
|
150
164
|
muscleGroup: MuscleGroup | 'focus_muscle';
|
|
151
165
|
category: HevyTrainerExerciseCategory;
|
|
@@ -156,6 +170,15 @@ export interface TrainerProgramExercise {
|
|
|
156
170
|
restTimerSeconds: number;
|
|
157
171
|
notes?: string;
|
|
158
172
|
}
|
|
173
|
+
/** Optional cardio attachment — no prescription semantics beyond exercise choice. */
|
|
174
|
+
export interface TrainerProgramCardioExercise {
|
|
175
|
+
kind: 'cardio';
|
|
176
|
+
exerciseTemplate: HevyTrainerLibraryExercise;
|
|
177
|
+
durationSeconds: number;
|
|
178
|
+
}
|
|
179
|
+
export type TrainerProgramExercise = TrainerProgramResistanceExercise | TrainerProgramCardioExercise;
|
|
180
|
+
export declare const isTrainerProgramResistanceExercise: (exercise: TrainerProgramExercise) => exercise is TrainerProgramResistanceExercise;
|
|
181
|
+
export declare const isTrainerProgramCardioExercise: (exercise: TrainerProgramExercise) => exercise is TrainerProgramCardioExercise;
|
|
159
182
|
export interface TrainerProgramRoutine {
|
|
160
183
|
name: HevyTrainerRoutineName;
|
|
161
184
|
exercises: TrainerProgramExercise[];
|
|
@@ -203,11 +226,12 @@ type PickExerciseResult<T extends HevyTrainerLibraryExercise> = {
|
|
|
203
226
|
exercise?: T;
|
|
204
227
|
trace: ExerciseSelectionTrace;
|
|
205
228
|
};
|
|
206
|
-
export declare function
|
|
229
|
+
export declare function pickTrainerExercise<T extends HevyTrainerLibraryExercise>(params: ExerciseSelectionParams<T> & {
|
|
207
230
|
withTrace: true;
|
|
208
231
|
}): PickExerciseResult<T>;
|
|
209
|
-
export declare function
|
|
232
|
+
export declare function pickTrainerExercise<T extends HevyTrainerLibraryExercise>(params: ExerciseSelectionParams<T>): T | undefined;
|
|
210
233
|
export type HevyTrainerLibraryExercise = Pick<LibraryExercise, 'id' | 'title' | 'priority' | 'muscle_group' | 'other_muscles' | 'exercise_type' | 'equipment_category' | 'category' | 'level' | 'goal' | 'granular_equipments'>;
|
|
234
|
+
export declare const DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS = 600;
|
|
211
235
|
export interface ExercisePrescriptionError {
|
|
212
236
|
type: 'exercise_not_found';
|
|
213
237
|
prescription: ExercisePrescription;
|
package/built/hevyTrainer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
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.
|
|
3
|
+
exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS = exports.getPrioritySortedExercises = exports.isEquipmentCompatible = exports.normalizeExerciseCategory = exports.getTrainerRestTimerSeconds = exports.getTrainerRepRange = exports.getTrainerSetCount = exports.isTrainerProgramCardioExercise = exports.isTrainerProgramResistanceExercise = exports.programSplits = exports.frequencyMap = exports.routineNames = exports.defaultDurationPerFrequency = exports.hevyTrainerExerciseCategories = exports.granularEquipmentsToTrainerEquipments = exports.trainerEquipmentToGranularEquipments = exports.granularEquipmentDefaults = exports.trainerGymTypes = exports.workoutDurationOptions = void 0;
|
|
4
|
+
exports.pickTrainerExercise = pickTrainerExercise;
|
|
5
5
|
exports.generateProgram = generateProgram;
|
|
6
6
|
const _1 = require(".");
|
|
7
7
|
exports.workoutDurationOptions = [40, 60, 80];
|
|
@@ -197,6 +197,10 @@ exports.programSplits = {
|
|
|
197
197
|
5: ['push_1', 'pull_1', 'legs_1', 'upper_2', 'lower_2'],
|
|
198
198
|
6: ['push_2_a', 'pull_2_a', 'legs_2_a', 'push_2_b', 'pull_2_b', 'legs_2_b'],
|
|
199
199
|
};
|
|
200
|
+
const isTrainerProgramResistanceExercise = (exercise) => exercise.kind === 'resistance';
|
|
201
|
+
exports.isTrainerProgramResistanceExercise = isTrainerProgramResistanceExercise;
|
|
202
|
+
const isTrainerProgramCardioExercise = (exercise) => exercise.kind === 'cardio';
|
|
203
|
+
exports.isTrainerProgramCardioExercise = isTrainerProgramCardioExercise;
|
|
200
204
|
const getTrainerSetCount = (trainerAlgorithmSettings, goal, frequency) => {
|
|
201
205
|
return trainerAlgorithmSettings.sets[exports.frequencyMap[frequency]][goal];
|
|
202
206
|
};
|
|
@@ -433,7 +437,8 @@ const isAlternativeIsolationExerciseMatch = (exercise, criteria) => {
|
|
|
433
437
|
/**
|
|
434
438
|
* Finds an exercise that matches the criteria and is not already used
|
|
435
439
|
*/
|
|
436
|
-
const findMatchingExercise = (exercises,
|
|
440
|
+
const findMatchingExercise = (exercises, //TODO: muscle group exercises
|
|
441
|
+
criteria, context) => {
|
|
437
442
|
return exercises.find((exercise) => {
|
|
438
443
|
const matchesCriteria = isExerciseMatch(exercise, criteria);
|
|
439
444
|
const isNotUsed = !isExerciseUsed(exercise, context);
|
|
@@ -450,7 +455,7 @@ const findAlternativeIsolationExercise = (exercises, criteria, context) => {
|
|
|
450
455
|
return matchesCriteria && isNotUsed;
|
|
451
456
|
});
|
|
452
457
|
};
|
|
453
|
-
const
|
|
458
|
+
const pickTrainerExerciseWithTrace = (params) => {
|
|
454
459
|
const { sortedExercises, handPickedExercises, criteria, context } = params;
|
|
455
460
|
const trace = { entries: [] };
|
|
456
461
|
const exercises = handPickedExercises
|
|
@@ -529,21 +534,70 @@ const pickExerciseForPrescriptionWithTrace = (params) => {
|
|
|
529
534
|
? { exercise, trace: Object.assign(Object.assign({}, trace), { selectedPass: 6 }) }
|
|
530
535
|
: { exercise: undefined, trace };
|
|
531
536
|
};
|
|
532
|
-
function
|
|
537
|
+
function pickTrainerExercise(params) {
|
|
533
538
|
if (params.withTrace) {
|
|
534
|
-
return
|
|
539
|
+
return pickTrainerExerciseWithTrace(params);
|
|
535
540
|
}
|
|
536
|
-
return
|
|
541
|
+
return pickTrainerExerciseWithTrace(params).exercise;
|
|
537
542
|
}
|
|
543
|
+
const CARDIO_TRACE_INDEX_BEFORE_TEMPLATE = -1;
|
|
544
|
+
exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS = 600;
|
|
545
|
+
/** Inserts optional cardio before or after template exercises for one routine. */
|
|
546
|
+
const attachOptionalCardioToRoutine = ({ cardioPreference, sortedExercises, criteria, debugExerciseSelectionTrace, exerciseSelectionTraces, routine, templatePrescriptionCount, routineExercises, }) => {
|
|
547
|
+
if (cardioPreference === 'no-cardio')
|
|
548
|
+
return;
|
|
549
|
+
// Cardio is reusable across routines, so program/routine used-id contexts are
|
|
550
|
+
// intentionally omitted — only the global excluded set applies.
|
|
551
|
+
const selectionParams = {
|
|
552
|
+
sortedExercises,
|
|
553
|
+
criteria: {
|
|
554
|
+
frequency: criteria.frequency,
|
|
555
|
+
goal: criteria.goal,
|
|
556
|
+
level: criteria.level,
|
|
557
|
+
equipments: criteria.equipments,
|
|
558
|
+
muscleGroup: 'cardio',
|
|
559
|
+
exerciseCategory: 'all',
|
|
560
|
+
routineBarbellExerciseCount: 0,
|
|
561
|
+
},
|
|
562
|
+
context: { excludedExerciseIds: criteria.excludedExerciseIds },
|
|
563
|
+
};
|
|
564
|
+
const selection = pickTrainerExercise(Object.assign(Object.assign({}, selectionParams), { withTrace: true }));
|
|
565
|
+
const cardioExercise = selection.exercise;
|
|
566
|
+
if (!cardioExercise)
|
|
567
|
+
return;
|
|
568
|
+
const slot = {
|
|
569
|
+
kind: 'cardio',
|
|
570
|
+
exerciseTemplate: cardioExercise,
|
|
571
|
+
durationSeconds: exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS,
|
|
572
|
+
};
|
|
573
|
+
if (cardioPreference === 'workout-start')
|
|
574
|
+
routineExercises.unshift(slot);
|
|
575
|
+
else
|
|
576
|
+
routineExercises.push(slot);
|
|
577
|
+
if (debugExerciseSelectionTrace) {
|
|
578
|
+
exerciseSelectionTraces.push({
|
|
579
|
+
traceKind: 'cardio',
|
|
580
|
+
routine,
|
|
581
|
+
prescriptionIndex: cardioPreference === 'workout-start'
|
|
582
|
+
? CARDIO_TRACE_INDEX_BEFORE_TEMPLATE
|
|
583
|
+
: templatePrescriptionCount,
|
|
584
|
+
selectedExerciseId: cardioExercise.id,
|
|
585
|
+
equipments: criteria.equipments,
|
|
586
|
+
level: criteria.level,
|
|
587
|
+
trace: selection.trace,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
};
|
|
538
591
|
function generateProgram(params) {
|
|
539
592
|
var _a, _b;
|
|
540
|
-
const { trainerAlgorithmSettings, frequency, goal, level, equipments, workoutDurationMinutes, restTimerLength, exerciseStore, focusMuscle, excludedExerciseIds, debugExerciseSelectionTrace, } = params;
|
|
593
|
+
const { trainerAlgorithmSettings, frequency, goal, level, equipments, workoutDurationMinutes, restTimerLength, exerciseStore, focusMuscle, excludedExerciseIds, debugExerciseSelectionTrace, cardioPreference, } = params;
|
|
541
594
|
const exerciseSelectionTraces = [];
|
|
542
595
|
const routines = exports.programSplits[frequency];
|
|
543
596
|
const program = {
|
|
544
597
|
name: frequency,
|
|
545
598
|
routines: [],
|
|
546
599
|
};
|
|
600
|
+
// TODO: Rename sortedExercises to sortedExercisesByMuscleGroup or something similar
|
|
547
601
|
const sortedExercises = (0, exports.getPrioritySortedExercises)(trainerAlgorithmSettings.exercise_priorities, exerciseStore);
|
|
548
602
|
const programUsedExerciseIds = new Set();
|
|
549
603
|
let programFocusMuscleExerciseCount = 0;
|
|
@@ -584,7 +638,7 @@ function generateProgram(params) {
|
|
|
584
638
|
};
|
|
585
639
|
let exercise;
|
|
586
640
|
if (debugExerciseSelectionTrace) {
|
|
587
|
-
const selection =
|
|
641
|
+
const selection = pickTrainerExercise({
|
|
588
642
|
sortedExercises,
|
|
589
643
|
criteria,
|
|
590
644
|
context: {
|
|
@@ -596,6 +650,7 @@ function generateProgram(params) {
|
|
|
596
650
|
});
|
|
597
651
|
exercise = selection.exercise;
|
|
598
652
|
exerciseSelectionTraces.push({
|
|
653
|
+
traceKind: 'template',
|
|
599
654
|
routine,
|
|
600
655
|
prescriptionIndex,
|
|
601
656
|
prescription: exercisePrescription,
|
|
@@ -606,7 +661,7 @@ function generateProgram(params) {
|
|
|
606
661
|
});
|
|
607
662
|
}
|
|
608
663
|
else {
|
|
609
|
-
exercise =
|
|
664
|
+
exercise = pickTrainerExercise({
|
|
610
665
|
sortedExercises,
|
|
611
666
|
criteria,
|
|
612
667
|
context: {
|
|
@@ -625,6 +680,7 @@ function generateProgram(params) {
|
|
|
625
680
|
programFocusMuscleExerciseCount++;
|
|
626
681
|
const repRange = (0, exports.getTrainerRepRange)(trainerAlgorithmSettings, goal, exercisePrescription.category);
|
|
627
682
|
routineExercises.push({
|
|
683
|
+
kind: 'resistance',
|
|
628
684
|
exerciseTemplate: exercise,
|
|
629
685
|
muscleGroup: exercisePrescription.muscle_group,
|
|
630
686
|
category: exercisePrescription.category,
|
|
@@ -651,6 +707,16 @@ function generateProgram(params) {
|
|
|
651
707
|
});
|
|
652
708
|
}
|
|
653
709
|
}
|
|
710
|
+
attachOptionalCardioToRoutine({
|
|
711
|
+
cardioPreference,
|
|
712
|
+
sortedExercises,
|
|
713
|
+
criteria: { equipments, level, goal, frequency, excludedExerciseIds },
|
|
714
|
+
debugExerciseSelectionTrace,
|
|
715
|
+
exerciseSelectionTraces,
|
|
716
|
+
routine,
|
|
717
|
+
templatePrescriptionCount: routineTemplate.exercises.length,
|
|
718
|
+
routineExercises,
|
|
719
|
+
});
|
|
654
720
|
program.routines.push({
|
|
655
721
|
name: routine,
|
|
656
722
|
notes: routineTemplate.notes,
|
package/built/index.d.ts
CHANGED
|
@@ -647,10 +647,12 @@ export declare const trainingGoals: readonly ["strength", "build_muscle", "fat_l
|
|
|
647
647
|
export declare const trainingLevels: readonly ["beginner", "intermediate", "advanced"];
|
|
648
648
|
export declare const exerciseCategories: readonly ["isolation", "compound", "assistance-compound"];
|
|
649
649
|
export declare const restTimerLengths: readonly ["short", "medium", "long"];
|
|
650
|
+
export declare const cardioPreferences: readonly ["no-cardio", "workout-start", "workout-end"];
|
|
650
651
|
export type TrainingGoal = Lookup<typeof trainingGoals>;
|
|
651
652
|
export type TrainingLevel = Lookup<typeof trainingLevels>;
|
|
652
653
|
export type ExerciseCategory = Lookup<typeof exerciseCategories>;
|
|
653
654
|
export type RestTimerLength = typeof restTimerLengths[number];
|
|
655
|
+
export type CardioPreference = typeof cardioPreferences[number];
|
|
654
656
|
export type HevyTrainerProgramEquipment = Extract<Equipment, 'barbell' | 'dumbbell' | 'machine'>;
|
|
655
657
|
export declare const hevyTrainerProgramEquipments: readonly ["barbell", "dumbbell", "machine"];
|
|
656
658
|
export declare const granularEquipments: readonly ["barbell", "dumbbell", "kettlebell", "plate", "medicine_ball", "ez_bar", "landmine", "trap_bar", "pullup_bar", "dip_bar", "squat_rack", "flat_bench", "adjustable_bench", "dual_cable_machine", "single_cable_machine", "lat_pulldown_cable", "leg_press_machine", "smith_machine", "t_bar", "plate_machines", "stack_machines", "treadmill", "elliptical_trainer", "rowing_machine", "spinning", "stair_machine", "air_bike", "suspension_band", "resistance_band", "battle_rope", "rings", "jump_rope"];
|
|
@@ -1277,11 +1279,13 @@ export interface HevyTrainerProgram {
|
|
|
1277
1279
|
routines: HevyTrainerRoutine[];
|
|
1278
1280
|
focus_muscle?: SimplifiedMuscleGroup;
|
|
1279
1281
|
next_workout_index: number;
|
|
1280
|
-
workout_duration_minutes
|
|
1282
|
+
workout_duration_minutes: WorkoutDurationMinutes;
|
|
1281
1283
|
rest_timer_length: RestTimerLength;
|
|
1284
|
+
cardio_preference: CardioPreference;
|
|
1282
1285
|
}
|
|
1283
1286
|
export interface PostHevyTrainerProgramRequestBody {
|
|
1284
1287
|
program: {
|
|
1288
|
+
version: 1;
|
|
1285
1289
|
title: string;
|
|
1286
1290
|
level: TrainingLevel;
|
|
1287
1291
|
goal: TrainingGoal;
|
|
@@ -1289,13 +1293,15 @@ export interface PostHevyTrainerProgramRequestBody {
|
|
|
1289
1293
|
weekly_frequency: WeeklyTrainingFrequency;
|
|
1290
1294
|
focus_muscle?: SimplifiedMuscleGroup;
|
|
1291
1295
|
routines: RoutineUpdate[];
|
|
1292
|
-
next_workout_index
|
|
1293
|
-
workout_duration_minutes
|
|
1296
|
+
next_workout_index: number;
|
|
1297
|
+
workout_duration_minutes: WorkoutDurationMinutes;
|
|
1294
1298
|
rest_timer_length: RestTimerLength;
|
|
1299
|
+
cardio_preference: CardioPreference;
|
|
1295
1300
|
};
|
|
1296
1301
|
}
|
|
1297
1302
|
export interface UpdateHevyTrainerProgramRequestBody {
|
|
1298
1303
|
program: {
|
|
1304
|
+
version: 1;
|
|
1299
1305
|
programId: string;
|
|
1300
1306
|
title: string;
|
|
1301
1307
|
level: TrainingLevel;
|
|
@@ -1303,9 +1309,10 @@ export interface UpdateHevyTrainerProgramRequestBody {
|
|
|
1303
1309
|
equipments: GranularEquipment[];
|
|
1304
1310
|
weekly_frequency: WeeklyTrainingFrequency;
|
|
1305
1311
|
focus_muscle?: SimplifiedMuscleGroup;
|
|
1306
|
-
next_workout_index
|
|
1307
|
-
workout_duration_minutes
|
|
1308
|
-
rest_timer_length
|
|
1312
|
+
next_workout_index: number;
|
|
1313
|
+
workout_duration_minutes: WorkoutDurationMinutes;
|
|
1314
|
+
rest_timer_length: RestTimerLength;
|
|
1315
|
+
cardio_preference: CardioPreference;
|
|
1309
1316
|
routines: {
|
|
1310
1317
|
id: string;
|
|
1311
1318
|
title: string;
|
package/built/index.js
CHANGED
|
@@ -14,8 +14,8 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.
|
|
18
|
-
exports.isOAuthScope = exports.supportedScopes = exports.isSuggestedUserSource = exports.isValidUserWorkoutMetricsType = exports.isBodyMeasurementKey = exports.measurementsList = exports.isHevyTrainerRoutine = exports.isWorkoutBiometrics = void 0;
|
|
17
|
+
exports.isPublicWorkout = exports.isSetType = exports.isRPE = exports.validRpeValues = exports.isSetPersonalRecordType = exports.weeklyTrainingFrequencies = exports.isGranularEquipment = exports.granularEquipments = exports.hevyTrainerProgramEquipments = exports.cardioPreferences = exports.restTimerLengths = exports.exerciseCategories = exports.trainingLevels = exports.trainingGoals = exports.isCustomExerciseType = exports.customExericseTypes = exports.isExerciseType = exports.exerciseTypes = exports.isExerciseRepType = exports.exerciseRepTypes = exports.isEquipmentFilter = exports.equipmentFilters = exports.isEquipment = exports.equipments = exports.simplifiedMuscleGroupToMuscleGroups = exports.isMuscleGroupFilter = exports.muscleGroupFilters = exports.isMuscleGroup = exports.muscleGroups = exports.isSimplifiedMuscleGroup = exports.simplifiedMuscleGroups = exports.miscellaneousMuscles = exports.chestMuscles = exports.backMuscles = exports.legMuscles = exports.armMuscles = exports.shoulderMuscles = exports.coreMuscles = exports.DefaultClientConfiguration = exports.parseClientAuthTokenResponse = exports.isCoachRole = exports.isErrorResponse = exports.isLivePRVolumeOption = exports.isTimerVolumeOption = exports.isWeekday = exports.orderedWeekdays = exports.isBodyMeasurementUnit = exports.isDistanceUnitShort = exports.isDistanceUnit = exports.isWeightUnit = void 0;
|
|
18
|
+
exports.isOAuthScope = exports.supportedScopes = exports.isSuggestedUserSource = exports.isValidUserWorkoutMetricsType = exports.isBodyMeasurementKey = exports.measurementsList = exports.isHevyTrainerRoutine = exports.isWorkoutBiometrics = exports.isHeartRateSamples = void 0;
|
|
19
19
|
const typeUtils_1 = require("./typeUtils");
|
|
20
20
|
__exportStar(require("./schemas"), exports);
|
|
21
21
|
__exportStar(require("./constants"), exports);
|
|
@@ -230,6 +230,11 @@ exports.exerciseCategories = [
|
|
|
230
230
|
'assistance-compound',
|
|
231
231
|
];
|
|
232
232
|
exports.restTimerLengths = ['short', 'medium', 'long'];
|
|
233
|
+
exports.cardioPreferences = [
|
|
234
|
+
'no-cardio',
|
|
235
|
+
'workout-start',
|
|
236
|
+
'workout-end',
|
|
237
|
+
];
|
|
233
238
|
exports.hevyTrainerProgramEquipments = [
|
|
234
239
|
'barbell',
|
|
235
240
|
'dumbbell',
|
|
@@ -381,11 +381,11 @@ const chestOnlySorted = (exercises) => {
|
|
|
381
381
|
}, {});
|
|
382
382
|
return Object.assign(Object.assign({}, empty), { chest: exercises });
|
|
383
383
|
};
|
|
384
|
-
describe('
|
|
384
|
+
describe('pickTrainerExercise', () => {
|
|
385
385
|
it('pass 1: returns the first matching exercise that is not used in the program', () => {
|
|
386
386
|
const used = makeExercise({ id: 'used' });
|
|
387
387
|
const fresh = makeExercise({ id: 'fresh' });
|
|
388
|
-
const result = (0, hevyTrainer_1.
|
|
388
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
389
389
|
sortedExercises: chestOnlySorted([used, fresh]),
|
|
390
390
|
criteria: baseCriteria(),
|
|
391
391
|
context: {
|
|
@@ -396,7 +396,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
396
396
|
});
|
|
397
397
|
it('pass 2: falls back to exercises not used in the current routine when all are used in the program', () => {
|
|
398
398
|
const onlyExercise = makeExercise({ id: 'only' });
|
|
399
|
-
const result = (0, hevyTrainer_1.
|
|
399
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
400
400
|
sortedExercises: chestOnlySorted([onlyExercise]),
|
|
401
401
|
criteria: baseCriteria(),
|
|
402
402
|
context: {
|
|
@@ -411,7 +411,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
411
411
|
it('excludes exercises in `excludedExerciseIds` across all passes', () => {
|
|
412
412
|
const excluded = makeExercise({ id: 'excluded' });
|
|
413
413
|
const alternative = makeExercise({ id: 'alt' });
|
|
414
|
-
const result = (0, hevyTrainer_1.
|
|
414
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
415
415
|
sortedExercises: chestOnlySorted([excluded, alternative]),
|
|
416
416
|
criteria: baseCriteria(),
|
|
417
417
|
context: {
|
|
@@ -421,7 +421,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
421
421
|
expect(result === null || result === void 0 ? void 0 : result.id).toBe('alt');
|
|
422
422
|
});
|
|
423
423
|
it('returns undefined when no exercise satisfies any pass', () => {
|
|
424
|
-
const result = (0, hevyTrainer_1.
|
|
424
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
425
425
|
sortedExercises: chestOnlySorted([]),
|
|
426
426
|
criteria: baseCriteria(),
|
|
427
427
|
context: {},
|
|
@@ -437,7 +437,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
437
437
|
id: 'bw',
|
|
438
438
|
granular_equipments: [],
|
|
439
439
|
});
|
|
440
|
-
const result = (0, hevyTrainer_1.
|
|
440
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
441
441
|
sortedExercises: chestOnlySorted([needsBarbell, bodyweight]),
|
|
442
442
|
criteria: baseCriteria({ equipments: [] }),
|
|
443
443
|
context: {},
|
|
@@ -448,7 +448,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
448
448
|
const wrongLevel = makeExercise({ id: 'wrong', level: ['advanced'] });
|
|
449
449
|
const wrongGoal = makeExercise({ id: 'wrongGoal', goal: ['fat_loss'] });
|
|
450
450
|
const rightOne = makeExercise({ id: 'right' });
|
|
451
|
-
const result = (0, hevyTrainer_1.
|
|
451
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
452
452
|
sortedExercises: chestOnlySorted([wrongLevel, wrongGoal, rightOne]),
|
|
453
453
|
criteria: baseCriteria({ level: 'beginner', goal: 'strength' }),
|
|
454
454
|
context: {},
|
|
@@ -458,7 +458,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
458
458
|
it('considers handpicked exercises before sortedExercises', () => {
|
|
459
459
|
const hand = makeExercise({ id: 'hand' });
|
|
460
460
|
const sorted = makeExercise({ id: 'sorted' });
|
|
461
|
-
const result = (0, hevyTrainer_1.
|
|
461
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
462
462
|
sortedExercises: chestOnlySorted([sorted]),
|
|
463
463
|
handPickedExercises: [hand],
|
|
464
464
|
criteria: baseCriteria(),
|
|
@@ -473,7 +473,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
473
473
|
id: 'compound',
|
|
474
474
|
category: 'compound',
|
|
475
475
|
});
|
|
476
|
-
const result = (0, hevyTrainer_1.
|
|
476
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
477
477
|
sortedExercises: chestOnlySorted([compoundOnly]),
|
|
478
478
|
criteria: baseCriteria({ exerciseCategory: 'isolation' }),
|
|
479
479
|
context: {},
|
|
@@ -491,7 +491,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
491
491
|
return acc;
|
|
492
492
|
}, {});
|
|
493
493
|
sorted.shoulders = [secondaryChest];
|
|
494
|
-
const result = (0, hevyTrainer_1.
|
|
494
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
495
495
|
sortedExercises: sorted,
|
|
496
496
|
criteria: baseCriteria({ muscleGroup: 'chest' }),
|
|
497
497
|
context: {},
|
|
@@ -502,7 +502,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
502
502
|
const barbellExercise = (id) => makeExercise({ id, equipment_category: 'barbell' });
|
|
503
503
|
it('allows barbell exercises regardless of count when frequency > 1', () => {
|
|
504
504
|
const ex = barbellExercise('bench');
|
|
505
|
-
const result = (0, hevyTrainer_1.
|
|
505
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
506
506
|
sortedExercises: chestOnlySorted([ex]),
|
|
507
507
|
criteria: baseCriteria({
|
|
508
508
|
frequency: 3,
|
|
@@ -515,7 +515,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
515
515
|
});
|
|
516
516
|
it('allows barbell exercises under the cap when frequency === 1', () => {
|
|
517
517
|
const ex = barbellExercise('bench');
|
|
518
|
-
const result = (0, hevyTrainer_1.
|
|
518
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
519
519
|
sortedExercises: chestOnlySorted([ex]),
|
|
520
520
|
criteria: baseCriteria({
|
|
521
521
|
frequency: 1,
|
|
@@ -528,7 +528,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
528
528
|
});
|
|
529
529
|
it('rejects barbell exercises at or over the cap when a substitute is available', () => {
|
|
530
530
|
const ex = barbellExercise('bench');
|
|
531
|
-
const result = (0, hevyTrainer_1.
|
|
531
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
532
532
|
sortedExercises: chestOnlySorted([ex]),
|
|
533
533
|
criteria: baseCriteria({
|
|
534
534
|
frequency: 1,
|
|
@@ -542,7 +542,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
542
542
|
});
|
|
543
543
|
it('still allows barbell exercises over the cap when the user has no substitutes', () => {
|
|
544
544
|
const ex = barbellExercise('bench');
|
|
545
|
-
const result = (0, hevyTrainer_1.
|
|
545
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
546
546
|
sortedExercises: chestOnlySorted([ex]),
|
|
547
547
|
criteria: baseCriteria({
|
|
548
548
|
frequency: 1,
|
|
@@ -558,7 +558,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
558
558
|
it('returns a trace when `withTrace: true` and records the winning pass', () => {
|
|
559
559
|
var _a;
|
|
560
560
|
const fresh = makeExercise({ id: 'fresh' });
|
|
561
|
-
const result = (0, hevyTrainer_1.
|
|
561
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
562
562
|
sortedExercises: chestOnlySorted([fresh]),
|
|
563
563
|
criteria: baseCriteria(),
|
|
564
564
|
context: {},
|
|
@@ -572,7 +572,7 @@ describe('pickExerciseForPrescription', () => {
|
|
|
572
572
|
});
|
|
573
573
|
});
|
|
574
574
|
it('returns a trace with no selected pass when nothing matches', () => {
|
|
575
|
-
const result = (0, hevyTrainer_1.
|
|
575
|
+
const result = (0, hevyTrainer_1.pickTrainerExercise)({
|
|
576
576
|
sortedExercises: chestOnlySorted([]),
|
|
577
577
|
criteria: baseCriteria(),
|
|
578
578
|
context: {},
|
|
@@ -608,6 +608,7 @@ describe('generateProgram', () => {
|
|
|
608
608
|
workoutDurationMinutes: 60,
|
|
609
609
|
restTimerLength: 'medium',
|
|
610
610
|
exerciseStore: [chestExercise],
|
|
611
|
+
cardioPreference: 'no-cardio',
|
|
611
612
|
});
|
|
612
613
|
expect(result.success).toBe(true);
|
|
613
614
|
if (!result.success)
|
|
@@ -619,6 +620,9 @@ describe('generateProgram', () => {
|
|
|
619
620
|
expect(routine.notes).toBe('Hit the chest hard.');
|
|
620
621
|
expect(routine.exercises).toHaveLength(1);
|
|
621
622
|
const [exercise] = routine.exercises;
|
|
623
|
+
expect(exercise.kind).toBe('resistance');
|
|
624
|
+
if (exercise.kind !== 'resistance')
|
|
625
|
+
return;
|
|
622
626
|
expect(exercise.exerciseTemplate.id).toBe('bench');
|
|
623
627
|
expect(exercise.category).toBe('compound');
|
|
624
628
|
expect(exercise.muscleGroup).toBe('chest');
|
|
@@ -642,6 +646,7 @@ describe('generateProgram', () => {
|
|
|
642
646
|
workoutDurationMinutes: 60,
|
|
643
647
|
restTimerLength: 'medium',
|
|
644
648
|
exerciseStore: [],
|
|
649
|
+
cardioPreference: 'no-cardio',
|
|
645
650
|
});
|
|
646
651
|
expect(result.success).toBe(false);
|
|
647
652
|
if (result.success)
|
|
@@ -683,6 +688,7 @@ describe('generateProgram', () => {
|
|
|
683
688
|
workoutDurationMinutes,
|
|
684
689
|
restTimerLength: 'medium',
|
|
685
690
|
exerciseStore: [makeExercise({ id: 'bench' })],
|
|
691
|
+
cardioPreference: 'no-cardio',
|
|
686
692
|
});
|
|
687
693
|
const routine = (result.success ? result.program : result.partialProgram)
|
|
688
694
|
.routines[0];
|
|
@@ -751,6 +757,7 @@ describe('generateProgram', () => {
|
|
|
751
757
|
makeExercise({ id: 'bench-2', priority: 2 }),
|
|
752
758
|
makeExercise({ id: 'bench-3', priority: 1 }),
|
|
753
759
|
],
|
|
760
|
+
cardioPreference: 'no-cardio',
|
|
754
761
|
});
|
|
755
762
|
expect(result.success).toBe(true);
|
|
756
763
|
if (!result.success)
|
|
@@ -758,7 +765,10 @@ describe('generateProgram', () => {
|
|
|
758
765
|
// Only the 40- and 60-minute prescriptions survive; the 80-minute one
|
|
759
766
|
// is filtered out. Order is preserved.
|
|
760
767
|
const placed = result.program.routines[0].exercises;
|
|
761
|
-
expect(placed.
|
|
768
|
+
expect(placed.every((e) => e.kind === 'resistance')).toBe(true);
|
|
769
|
+
expect(placed
|
|
770
|
+
.filter(hevyTrainer_1.isTrainerProgramResistanceExercise)
|
|
771
|
+
.map((e) => e.warmupSetCount)).toEqual([1, 2]);
|
|
762
772
|
});
|
|
763
773
|
});
|
|
764
774
|
it('skips focus_muscle prescriptions when no focusMuscle is provided', () => {
|
|
@@ -777,6 +787,7 @@ describe('generateProgram', () => {
|
|
|
777
787
|
exerciseStore: [
|
|
778
788
|
makeExercise({ id: 'biceps-curl', muscle_group: 'biceps' }),
|
|
779
789
|
],
|
|
790
|
+
cardioPreference: 'no-cardio',
|
|
780
791
|
});
|
|
781
792
|
expect(result.success).toBe(true);
|
|
782
793
|
if (!result.success)
|
|
@@ -803,11 +814,15 @@ describe('generateProgram', () => {
|
|
|
803
814
|
restTimerLength: 'medium',
|
|
804
815
|
exerciseStore: [bicepsCurl],
|
|
805
816
|
focusMuscle: 'arms',
|
|
817
|
+
cardioPreference: 'no-cardio',
|
|
806
818
|
});
|
|
807
819
|
expect(result.success).toBe(true);
|
|
808
820
|
if (!result.success)
|
|
809
821
|
return;
|
|
810
822
|
const [exercise] = result.program.routines[0].exercises;
|
|
823
|
+
expect(exercise.kind).toBe('resistance');
|
|
824
|
+
if (exercise.kind !== 'resistance')
|
|
825
|
+
return;
|
|
811
826
|
expect(exercise.exerciseTemplate.id).toBe('biceps-curl');
|
|
812
827
|
expect(exercise.muscleGroup).toBe('focus_muscle');
|
|
813
828
|
});
|
|
@@ -825,6 +840,7 @@ describe('generateProgram', () => {
|
|
|
825
840
|
restTimerLength: 'medium',
|
|
826
841
|
exerciseStore: [excluded, alt],
|
|
827
842
|
excludedExerciseIds: new Set(['excluded']),
|
|
843
|
+
cardioPreference: 'no-cardio',
|
|
828
844
|
});
|
|
829
845
|
expect(result.success).toBe(true);
|
|
830
846
|
if (!result.success)
|
|
@@ -844,10 +860,12 @@ describe('generateProgram', () => {
|
|
|
844
860
|
restTimerLength: 'medium',
|
|
845
861
|
exerciseStore: [chest],
|
|
846
862
|
debugExerciseSelectionTrace: true,
|
|
863
|
+
cardioPreference: 'no-cardio',
|
|
847
864
|
});
|
|
848
865
|
expect(result.success).toBe(true);
|
|
849
866
|
expect(result.exerciseSelectionTraces).toHaveLength(1);
|
|
850
867
|
const [trace] = result.exerciseSelectionTraces;
|
|
868
|
+
expect(trace.traceKind).toBe('template');
|
|
851
869
|
expect(trace.routine).toBe('full_body_1');
|
|
852
870
|
expect(trace.selectedExerciseId).toBe('bench');
|
|
853
871
|
expect(trace.trace.selectedPass).toBe(1);
|
|
@@ -876,10 +894,213 @@ describe('generateProgram', () => {
|
|
|
876
894
|
makeExercise({ id: 'bench-2' }),
|
|
877
895
|
],
|
|
878
896
|
debugExerciseSelectionTrace: true,
|
|
897
|
+
cardioPreference: 'no-cardio',
|
|
879
898
|
});
|
|
880
899
|
expect(result.success).toBe(true);
|
|
881
900
|
expect(result.exerciseSelectionTraces.map((t) => t.prescriptionIndex)).toEqual([0, 1]);
|
|
882
901
|
});
|
|
902
|
+
it('records a single cardio trace entry on pass 1', () => {
|
|
903
|
+
const settings = makeSettings();
|
|
904
|
+
settings.templates.full_body_1 = {
|
|
905
|
+
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
906
|
+
};
|
|
907
|
+
settings.exercise_priorities.chest = ['bench'];
|
|
908
|
+
settings.exercise_priorities.cardio = ['run'];
|
|
909
|
+
const result = (0, hevyTrainer_1.generateProgram)({
|
|
910
|
+
trainerAlgorithmSettings: settings,
|
|
911
|
+
frequency: 1,
|
|
912
|
+
goal: 'strength',
|
|
913
|
+
level: 'beginner',
|
|
914
|
+
equipments: [],
|
|
915
|
+
workoutDurationMinutes: 60,
|
|
916
|
+
restTimerLength: 'medium',
|
|
917
|
+
exerciseStore: [
|
|
918
|
+
makeExercise({ id: 'bench', muscle_group: 'chest' }),
|
|
919
|
+
makeExercise({
|
|
920
|
+
id: 'run',
|
|
921
|
+
muscle_group: 'cardio',
|
|
922
|
+
category: 'compound',
|
|
923
|
+
equipment_category: 'other',
|
|
924
|
+
}),
|
|
925
|
+
],
|
|
926
|
+
debugExerciseSelectionTrace: true,
|
|
927
|
+
cardioPreference: 'workout-start',
|
|
928
|
+
});
|
|
929
|
+
expect(result.success).toBe(true);
|
|
930
|
+
if (!result.success)
|
|
931
|
+
return;
|
|
932
|
+
const cardioTrace = result.exerciseSelectionTraces.find((t) => t.traceKind === 'cardio');
|
|
933
|
+
expect(cardioTrace).toBeDefined();
|
|
934
|
+
expect(cardioTrace.prescriptionIndex).toBe(-1);
|
|
935
|
+
expect(cardioTrace.trace.entries).toHaveLength(1);
|
|
936
|
+
expect(cardioTrace.trace.selectedPass).toBe(1);
|
|
937
|
+
expect(cardioTrace.trace.entries[0].selectedExerciseId).toBe('run');
|
|
938
|
+
expect(cardioTrace.equipments).toEqual([]);
|
|
939
|
+
expect(cardioTrace.level).toBe('beginner');
|
|
940
|
+
});
|
|
941
|
+
it('uses the same prioritized cardio exercise on every routine (not blocked by program reuse)', () => {
|
|
942
|
+
const settings = makeSettings();
|
|
943
|
+
settings.templates.full_body_2_a = {
|
|
944
|
+
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
945
|
+
};
|
|
946
|
+
settings.templates.full_body_2_b = {
|
|
947
|
+
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
948
|
+
};
|
|
949
|
+
settings.exercise_priorities.chest = ['bench-a', 'bench-b'];
|
|
950
|
+
settings.exercise_priorities.cardio = ['run'];
|
|
951
|
+
const result = (0, hevyTrainer_1.generateProgram)({
|
|
952
|
+
trainerAlgorithmSettings: settings,
|
|
953
|
+
frequency: 2,
|
|
954
|
+
goal: 'strength',
|
|
955
|
+
level: 'beginner',
|
|
956
|
+
equipments: [],
|
|
957
|
+
workoutDurationMinutes: 60,
|
|
958
|
+
restTimerLength: 'medium',
|
|
959
|
+
exerciseStore: [
|
|
960
|
+
makeExercise({ id: 'bench-a', muscle_group: 'chest' }),
|
|
961
|
+
makeExercise({ id: 'bench-b', muscle_group: 'chest' }),
|
|
962
|
+
makeExercise({
|
|
963
|
+
id: 'run',
|
|
964
|
+
muscle_group: 'cardio',
|
|
965
|
+
category: 'compound',
|
|
966
|
+
equipment_category: 'other',
|
|
967
|
+
}),
|
|
968
|
+
],
|
|
969
|
+
cardioPreference: 'workout-start',
|
|
970
|
+
});
|
|
971
|
+
expect(result.success).toBe(true);
|
|
972
|
+
if (!result.success)
|
|
973
|
+
return;
|
|
974
|
+
expect(result.program.routines[0].exercises[0]).toMatchObject({
|
|
975
|
+
kind: 'cardio',
|
|
976
|
+
exerciseTemplate: { id: 'run' },
|
|
977
|
+
});
|
|
978
|
+
expect(result.program.routines[1].exercises[0]).toMatchObject({
|
|
979
|
+
kind: 'cardio',
|
|
980
|
+
exerciseTemplate: { id: 'run' },
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
it('skips cardio exercises that need equipment the user does not have', () => {
|
|
984
|
+
const settings = makeSettings();
|
|
985
|
+
settings.templates.full_body_1 = {
|
|
986
|
+
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
987
|
+
};
|
|
988
|
+
settings.exercise_priorities.chest = ['bench'];
|
|
989
|
+
settings.exercise_priorities.cardio = ['treadmill-run', 'body-run'];
|
|
990
|
+
const result = (0, hevyTrainer_1.generateProgram)({
|
|
991
|
+
trainerAlgorithmSettings: settings,
|
|
992
|
+
frequency: 1,
|
|
993
|
+
goal: 'strength',
|
|
994
|
+
level: 'beginner',
|
|
995
|
+
equipments: [],
|
|
996
|
+
workoutDurationMinutes: 60,
|
|
997
|
+
restTimerLength: 'medium',
|
|
998
|
+
exerciseStore: [
|
|
999
|
+
makeExercise({ id: 'bench', muscle_group: 'chest' }),
|
|
1000
|
+
makeExercise({
|
|
1001
|
+
id: 'treadmill-run',
|
|
1002
|
+
muscle_group: 'cardio',
|
|
1003
|
+
category: 'compound',
|
|
1004
|
+
equipment_category: 'other',
|
|
1005
|
+
granular_equipments: ['treadmill'],
|
|
1006
|
+
}),
|
|
1007
|
+
makeExercise({
|
|
1008
|
+
id: 'body-run',
|
|
1009
|
+
muscle_group: 'cardio',
|
|
1010
|
+
category: 'compound',
|
|
1011
|
+
equipment_category: 'other',
|
|
1012
|
+
}),
|
|
1013
|
+
],
|
|
1014
|
+
cardioPreference: 'workout-start',
|
|
1015
|
+
});
|
|
1016
|
+
expect(result.success).toBe(true);
|
|
1017
|
+
if (!result.success)
|
|
1018
|
+
return;
|
|
1019
|
+
expect(result.program.routines[0].exercises[0]).toMatchObject({
|
|
1020
|
+
kind: 'cardio',
|
|
1021
|
+
exerciseTemplate: { id: 'body-run' },
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
it('skips globally excluded cardio and uses the next priority exercise', () => {
|
|
1025
|
+
const settings = makeSettings();
|
|
1026
|
+
settings.templates.full_body_1 = {
|
|
1027
|
+
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
1028
|
+
};
|
|
1029
|
+
settings.exercise_priorities.chest = ['bench'];
|
|
1030
|
+
settings.exercise_priorities.cardio = ['run-a', 'run-b'];
|
|
1031
|
+
const result = (0, hevyTrainer_1.generateProgram)({
|
|
1032
|
+
trainerAlgorithmSettings: settings,
|
|
1033
|
+
frequency: 1,
|
|
1034
|
+
goal: 'strength',
|
|
1035
|
+
level: 'beginner',
|
|
1036
|
+
equipments: [],
|
|
1037
|
+
workoutDurationMinutes: 60,
|
|
1038
|
+
restTimerLength: 'medium',
|
|
1039
|
+
exerciseStore: [
|
|
1040
|
+
makeExercise({ id: 'bench', muscle_group: 'chest' }),
|
|
1041
|
+
makeExercise({
|
|
1042
|
+
id: 'run-a',
|
|
1043
|
+
muscle_group: 'cardio',
|
|
1044
|
+
category: 'compound',
|
|
1045
|
+
equipment_category: 'other',
|
|
1046
|
+
}),
|
|
1047
|
+
makeExercise({
|
|
1048
|
+
id: 'run-b',
|
|
1049
|
+
muscle_group: 'cardio',
|
|
1050
|
+
category: 'compound',
|
|
1051
|
+
equipment_category: 'other',
|
|
1052
|
+
}),
|
|
1053
|
+
],
|
|
1054
|
+
excludedExerciseIds: new Set(['run-a']),
|
|
1055
|
+
cardioPreference: 'workout-start',
|
|
1056
|
+
});
|
|
1057
|
+
expect(result.success).toBe(true);
|
|
1058
|
+
if (!result.success)
|
|
1059
|
+
return;
|
|
1060
|
+
expect(result.program.routines[0].exercises[0]).toMatchObject({
|
|
1061
|
+
kind: 'cardio',
|
|
1062
|
+
exerciseTemplate: { id: 'run-b' },
|
|
1063
|
+
});
|
|
1064
|
+
});
|
|
1065
|
+
it('keeps global trace order aligned with routine generation (no cross-routine unshift)', () => {
|
|
1066
|
+
const settings = makeSettings();
|
|
1067
|
+
settings.templates.full_body_2_a = {
|
|
1068
|
+
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
1069
|
+
};
|
|
1070
|
+
settings.templates.full_body_2_b = {
|
|
1071
|
+
exercises: [{ muscle_group: 'chest', category: 'compound' }],
|
|
1072
|
+
};
|
|
1073
|
+
settings.exercise_priorities.chest = ['bench-a', 'bench-b'];
|
|
1074
|
+
settings.exercise_priorities.cardio = ['run'];
|
|
1075
|
+
const result = (0, hevyTrainer_1.generateProgram)({
|
|
1076
|
+
trainerAlgorithmSettings: settings,
|
|
1077
|
+
frequency: 2,
|
|
1078
|
+
goal: 'strength',
|
|
1079
|
+
level: 'beginner',
|
|
1080
|
+
equipments: [],
|
|
1081
|
+
workoutDurationMinutes: 60,
|
|
1082
|
+
restTimerLength: 'medium',
|
|
1083
|
+
exerciseStore: [
|
|
1084
|
+
makeExercise({ id: 'bench-a', muscle_group: 'chest' }),
|
|
1085
|
+
makeExercise({ id: 'bench-b', muscle_group: 'chest' }),
|
|
1086
|
+
makeExercise({
|
|
1087
|
+
id: 'run',
|
|
1088
|
+
muscle_group: 'cardio',
|
|
1089
|
+
category: 'compound',
|
|
1090
|
+
equipment_category: 'other',
|
|
1091
|
+
}),
|
|
1092
|
+
],
|
|
1093
|
+
debugExerciseSelectionTrace: true,
|
|
1094
|
+
cardioPreference: 'workout-start',
|
|
1095
|
+
});
|
|
1096
|
+
expect(result.success).toBe(true);
|
|
1097
|
+
if (!result.success)
|
|
1098
|
+
return;
|
|
1099
|
+
const idxA = result.exerciseSelectionTraces.findIndex((t) => t.routine === 'full_body_2_a');
|
|
1100
|
+
const idxB = result.exerciseSelectionTraces.findIndex((t) => t.routine === 'full_body_2_b');
|
|
1101
|
+
expect(idxA).toBeGreaterThanOrEqual(0);
|
|
1102
|
+
expect(idxB).toBeGreaterThan(idxA);
|
|
1103
|
+
});
|
|
883
1104
|
describe('barbell cap at frequency === 1 (end-to-end)', () => {
|
|
884
1105
|
it('drops the 4th+ barbell prescription when the user has a substitute', () => {
|
|
885
1106
|
const settings = makeSettings();
|
|
@@ -907,6 +1128,7 @@ describe('generateProgram', () => {
|
|
|
907
1128
|
workoutDurationMinutes: 60,
|
|
908
1129
|
restTimerLength: 'medium',
|
|
909
1130
|
exerciseStore: exercises,
|
|
1131
|
+
cardioPreference: 'no-cardio',
|
|
910
1132
|
});
|
|
911
1133
|
expect(result.success).toBe(false);
|
|
912
1134
|
if (result.success)
|
|
@@ -937,6 +1159,7 @@ describe('generateProgram', () => {
|
|
|
937
1159
|
workoutDurationMinutes: 60,
|
|
938
1160
|
restTimerLength: 'medium',
|
|
939
1161
|
exerciseStore: exercises,
|
|
1162
|
+
cardioPreference: 'no-cardio',
|
|
940
1163
|
});
|
|
941
1164
|
expect(result.success).toBe(true);
|
|
942
1165
|
if (!result.success)
|
|
@@ -990,24 +1213,21 @@ describe('generateProgram', () => {
|
|
|
990
1213
|
exerciseStore: exercises,
|
|
991
1214
|
focusMuscle: 'arms',
|
|
992
1215
|
debugExerciseSelectionTrace: true,
|
|
1216
|
+
cardioPreference: 'no-cardio',
|
|
993
1217
|
});
|
|
994
1218
|
const traces = result.exerciseSelectionTraces;
|
|
995
1219
|
// Counter increments once per placed focus_muscle prescription, so the
|
|
996
1220
|
// 4th prescription wraps back to the first entry of
|
|
997
1221
|
// simplifiedMuscleGroupToMuscleGroups.arms ('biceps').
|
|
998
|
-
expect(traces
|
|
999
|
-
'
|
|
1000
|
-
'triceps',
|
|
1001
|
-
'forearms',
|
|
1002
|
-
'biceps',
|
|
1003
|
-
]);
|
|
1222
|
+
expect(traces
|
|
1223
|
+
.filter((t) => t.traceKind === 'template')
|
|
1224
|
+
.map((t) => t.resolvedMuscleGroup)).toEqual(['biceps', 'triceps', 'forearms', 'biceps']);
|
|
1004
1225
|
// The first three passes should land on the priority-matching exercise
|
|
1005
1226
|
// (each muscle's only exercise is picked by pass 1).
|
|
1006
|
-
expect(traces
|
|
1007
|
-
'
|
|
1008
|
-
|
|
1009
|
-
'forearms-ex'
|
|
1010
|
-
]);
|
|
1227
|
+
expect(traces
|
|
1228
|
+
.filter((t) => t.traceKind === 'template')
|
|
1229
|
+
.slice(0, 3)
|
|
1230
|
+
.map((t) => t.selectedExerciseId)).toEqual(['biceps-ex', 'triceps-ex', 'forearms-ex']);
|
|
1011
1231
|
});
|
|
1012
1232
|
it('does not advance the focus_muscle counter when a prescription is skipped for template mismatch', () => {
|
|
1013
1233
|
// 5-day split: push_1, pull_1, legs_1, upper_2, lower_2.
|
|
@@ -1051,6 +1271,7 @@ describe('generateProgram', () => {
|
|
|
1051
1271
|
exerciseStore: exercises,
|
|
1052
1272
|
focusMuscle: 'legs',
|
|
1053
1273
|
debugExerciseSelectionTrace: true,
|
|
1274
|
+
cardioPreference: 'no-cardio',
|
|
1054
1275
|
});
|
|
1055
1276
|
expect(result.success).toBe(true);
|
|
1056
1277
|
if (!result.success)
|
|
@@ -1062,7 +1283,9 @@ describe('generateProgram', () => {
|
|
|
1062
1283
|
'legs_1',
|
|
1063
1284
|
'lower_2',
|
|
1064
1285
|
]);
|
|
1065
|
-
expect(result.exerciseSelectionTraces
|
|
1286
|
+
expect(result.exerciseSelectionTraces
|
|
1287
|
+
.filter((t) => t.traceKind === 'template')
|
|
1288
|
+
.map((t) => t.resolvedMuscleGroup)).toEqual(['quadriceps', 'hamstrings']);
|
|
1066
1289
|
});
|
|
1067
1290
|
});
|
|
1068
1291
|
describe('isFocusMuscleExtraExerciseAllowed template compatibility', () => {
|
|
@@ -1088,6 +1311,7 @@ describe('generateProgram', () => {
|
|
|
1088
1311
|
exerciseStore: [candidateExercise],
|
|
1089
1312
|
focusMuscle,
|
|
1090
1313
|
debugExerciseSelectionTrace: true,
|
|
1314
|
+
cardioPreference: 'no-cardio',
|
|
1091
1315
|
});
|
|
1092
1316
|
// Find the routine we targeted (it may not be first in the split).
|
|
1093
1317
|
const routine = (result.success ? result.program : result.partialProgram).routines.find((r) => r.name === routineName);
|