hevy-shared 1.0.980 → 1.0.982
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 +3 -27
- package/built/hevyTrainer.js +3 -79
- package/built/index.d.ts +7 -12
- package/built/tests/hevyTrainer.test.js +13 -237
- 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
|
|
1
|
+
import { WeeklyTrainingFrequency, TrainingGoal, TrainingLevel, SimplifiedMuscleGroup, MuscleGroup, LibraryExercise, ExerciseCategory, GranularEquipment, HevyTrainerProgramEquipment, RestTimerLength } 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,7 +59,6 @@ export interface ProgramGenerationParams<T extends HevyTrainerLibraryExercise> {
|
|
|
59
59
|
exerciseStore: T[];
|
|
60
60
|
focusMuscle?: SimplifiedMuscleGroup;
|
|
61
61
|
excludedExerciseIds?: Set<string>;
|
|
62
|
-
cardioPreference: CardioPreference;
|
|
63
62
|
/**
|
|
64
63
|
* When enabled, includes exercise selection trace breadcrumbs in the result
|
|
65
64
|
* for debugging. Intended for troubleshooting on backoffice; keep off in
|
|
@@ -67,8 +66,7 @@ export interface ProgramGenerationParams<T extends HevyTrainerLibraryExercise> {
|
|
|
67
66
|
*/
|
|
68
67
|
debugExerciseSelectionTrace?: boolean;
|
|
69
68
|
}
|
|
70
|
-
export interface
|
|
71
|
-
traceKind: 'template';
|
|
69
|
+
export interface ProgramExerciseSelectionTraceRecord {
|
|
72
70
|
routine: HevyTrainerRoutineName;
|
|
73
71
|
prescriptionIndex: number;
|
|
74
72
|
prescription: ExercisePrescription;
|
|
@@ -77,16 +75,6 @@ export interface TemplateExerciseSelectionTraceRecord {
|
|
|
77
75
|
selectedExerciseId?: string;
|
|
78
76
|
trace: ExerciseSelectionTrace;
|
|
79
77
|
}
|
|
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;
|
|
90
78
|
export type TrainerProgramAttemptWithTraces = TrainerProgramAttempt & {
|
|
91
79
|
exerciseSelectionTraces: ProgramExerciseSelectionTraceRecord[];
|
|
92
80
|
};
|
|
@@ -157,9 +145,7 @@ export interface TrainerAlgorithmSettings {
|
|
|
157
145
|
exercise_notes: ExerciseNotes;
|
|
158
146
|
exercise_replacements: ExerciseReplacements;
|
|
159
147
|
}
|
|
160
|
-
|
|
161
|
-
export interface TrainerProgramResistanceExercise {
|
|
162
|
-
kind: 'resistance';
|
|
148
|
+
export interface TrainerProgramExercise {
|
|
163
149
|
exerciseTemplate: HevyTrainerLibraryExercise;
|
|
164
150
|
muscleGroup: MuscleGroup | 'focus_muscle';
|
|
165
151
|
category: HevyTrainerExerciseCategory;
|
|
@@ -170,15 +156,6 @@ export interface TrainerProgramResistanceExercise {
|
|
|
170
156
|
restTimerSeconds: number;
|
|
171
157
|
notes?: string;
|
|
172
158
|
}
|
|
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;
|
|
182
159
|
export interface TrainerProgramRoutine {
|
|
183
160
|
name: HevyTrainerRoutineName;
|
|
184
161
|
exercises: TrainerProgramExercise[];
|
|
@@ -231,7 +208,6 @@ export declare function pickExerciseForPrescription<T extends HevyTrainerLibrary
|
|
|
231
208
|
}): PickExerciseResult<T>;
|
|
232
209
|
export declare function pickExerciseForPrescription<T extends HevyTrainerLibraryExercise>(params: ExerciseSelectionParams<T>): T | undefined;
|
|
233
210
|
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;
|
|
235
211
|
export interface ExercisePrescriptionError {
|
|
236
212
|
type: 'exercise_not_found';
|
|
237
213
|
prescription: ExercisePrescription;
|
package/built/hevyTrainer.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
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
4
|
exports.pickExerciseForPrescription = pickExerciseForPrescription;
|
|
5
5
|
exports.generateProgram = generateProgram;
|
|
6
6
|
const _1 = require(".");
|
|
@@ -197,10 +197,6 @@ 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;
|
|
204
200
|
const getTrainerSetCount = (trainerAlgorithmSettings, goal, frequency) => {
|
|
205
201
|
return trainerAlgorithmSettings.sets[exports.frequencyMap[frequency]][goal];
|
|
206
202
|
};
|
|
@@ -437,8 +433,7 @@ const isAlternativeIsolationExerciseMatch = (exercise, criteria) => {
|
|
|
437
433
|
/**
|
|
438
434
|
* Finds an exercise that matches the criteria and is not already used
|
|
439
435
|
*/
|
|
440
|
-
const findMatchingExercise = (exercises,
|
|
441
|
-
criteria, context) => {
|
|
436
|
+
const findMatchingExercise = (exercises, criteria, context) => {
|
|
442
437
|
return exercises.find((exercise) => {
|
|
443
438
|
const matchesCriteria = isExerciseMatch(exercise, criteria);
|
|
444
439
|
const isNotUsed = !isExerciseUsed(exercise, context);
|
|
@@ -540,74 +535,15 @@ function pickExerciseForPrescription(params) {
|
|
|
540
535
|
}
|
|
541
536
|
return pickExerciseForPrescriptionWithTrace(params).exercise;
|
|
542
537
|
}
|
|
543
|
-
const CARDIO_TRACE_INDEX_BEFORE_TEMPLATE = -1;
|
|
544
|
-
exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS = 600;
|
|
545
|
-
const appendCardioExerciseSelectionTrace = (traces, params) => {
|
|
546
|
-
traces.push({
|
|
547
|
-
traceKind: 'cardio',
|
|
548
|
-
routine: params.routine,
|
|
549
|
-
prescriptionIndex: params.prescriptionIndex,
|
|
550
|
-
selectedExerciseId: params.selectedExerciseId,
|
|
551
|
-
equipments: params.equipments,
|
|
552
|
-
level: params.level,
|
|
553
|
-
trace: {
|
|
554
|
-
entries: [
|
|
555
|
-
{
|
|
556
|
-
pass: 1,
|
|
557
|
-
label: 'cardio: first priority exercise matching equipment + level; globally excluded IDs skipped',
|
|
558
|
-
candidatePoolSize: params.candidatePoolSize,
|
|
559
|
-
selectedExerciseId: params.selectedExerciseId,
|
|
560
|
-
},
|
|
561
|
-
],
|
|
562
|
-
selectedPass: 1,
|
|
563
|
-
},
|
|
564
|
-
});
|
|
565
|
-
};
|
|
566
|
-
/** Inserts optional cardio before or after template exercises for one routine. */
|
|
567
|
-
const attachOptionalCardioToRoutine = ({ cardioPreference, cardioExercises, criteria, debugExerciseSelectionTrace, exerciseSelectionTraces, routine, templatePrescriptionCount, routineExercises, }) => {
|
|
568
|
-
if (cardioPreference === 'no-cardio')
|
|
569
|
-
return;
|
|
570
|
-
const cardioExercise = cardioExercises.find((ex) => {
|
|
571
|
-
var _a, _b, _c;
|
|
572
|
-
const equipmentCompatible = (0, exports.isEquipmentCompatible)(ex, criteria.equipments);
|
|
573
|
-
const levelCompatible = (_a = ex.level) === null || _a === void 0 ? void 0 : _a.includes(criteria.level);
|
|
574
|
-
const excluded = (_c = (_b = criteria.excludedExerciseIds) === null || _b === void 0 ? void 0 : _b.has(ex.id)) !== null && _c !== void 0 ? _c : false;
|
|
575
|
-
return equipmentCompatible && levelCompatible && !excluded;
|
|
576
|
-
});
|
|
577
|
-
if (!cardioExercise)
|
|
578
|
-
return;
|
|
579
|
-
const slot = {
|
|
580
|
-
kind: 'cardio',
|
|
581
|
-
exerciseTemplate: cardioExercise,
|
|
582
|
-
durationSeconds: exports.DEFAULT_TRAINER_CARDIO_ATTACHMENT_DURATION_SECONDS,
|
|
583
|
-
};
|
|
584
|
-
if (cardioPreference === 'beginning')
|
|
585
|
-
routineExercises.unshift(slot);
|
|
586
|
-
else
|
|
587
|
-
routineExercises.push(slot);
|
|
588
|
-
if (debugExerciseSelectionTrace) {
|
|
589
|
-
appendCardioExerciseSelectionTrace(exerciseSelectionTraces, {
|
|
590
|
-
routine,
|
|
591
|
-
prescriptionIndex: cardioPreference === 'beginning'
|
|
592
|
-
? CARDIO_TRACE_INDEX_BEFORE_TEMPLATE
|
|
593
|
-
: templatePrescriptionCount,
|
|
594
|
-
selectedExerciseId: cardioExercise.id,
|
|
595
|
-
equipments: criteria.equipments,
|
|
596
|
-
level: criteria.level,
|
|
597
|
-
candidatePoolSize: cardioExercises.length,
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
};
|
|
601
538
|
function generateProgram(params) {
|
|
602
539
|
var _a, _b;
|
|
603
|
-
const { trainerAlgorithmSettings, frequency, goal, level, equipments, workoutDurationMinutes, restTimerLength, exerciseStore, focusMuscle, excludedExerciseIds, debugExerciseSelectionTrace,
|
|
540
|
+
const { trainerAlgorithmSettings, frequency, goal, level, equipments, workoutDurationMinutes, restTimerLength, exerciseStore, focusMuscle, excludedExerciseIds, debugExerciseSelectionTrace, } = params;
|
|
604
541
|
const exerciseSelectionTraces = [];
|
|
605
542
|
const routines = exports.programSplits[frequency];
|
|
606
543
|
const program = {
|
|
607
544
|
name: frequency,
|
|
608
545
|
routines: [],
|
|
609
546
|
};
|
|
610
|
-
// TODO: Rename sortedExercises to sortedExercisesByMuscleGroup or something similar
|
|
611
547
|
const sortedExercises = (0, exports.getPrioritySortedExercises)(trainerAlgorithmSettings.exercise_priorities, exerciseStore);
|
|
612
548
|
const programUsedExerciseIds = new Set();
|
|
613
549
|
let programFocusMuscleExerciseCount = 0;
|
|
@@ -660,7 +596,6 @@ function generateProgram(params) {
|
|
|
660
596
|
});
|
|
661
597
|
exercise = selection.exercise;
|
|
662
598
|
exerciseSelectionTraces.push({
|
|
663
|
-
traceKind: 'template',
|
|
664
599
|
routine,
|
|
665
600
|
prescriptionIndex,
|
|
666
601
|
prescription: exercisePrescription,
|
|
@@ -690,7 +625,6 @@ function generateProgram(params) {
|
|
|
690
625
|
programFocusMuscleExerciseCount++;
|
|
691
626
|
const repRange = (0, exports.getTrainerRepRange)(trainerAlgorithmSettings, goal, exercisePrescription.category);
|
|
692
627
|
routineExercises.push({
|
|
693
|
-
kind: 'resistance',
|
|
694
628
|
exerciseTemplate: exercise,
|
|
695
629
|
muscleGroup: exercisePrescription.muscle_group,
|
|
696
630
|
category: exercisePrescription.category,
|
|
@@ -717,16 +651,6 @@ function generateProgram(params) {
|
|
|
717
651
|
});
|
|
718
652
|
}
|
|
719
653
|
}
|
|
720
|
-
attachOptionalCardioToRoutine({
|
|
721
|
-
cardioPreference,
|
|
722
|
-
cardioExercises: sortedExercises.cardio,
|
|
723
|
-
criteria: { equipments, level, excludedExerciseIds },
|
|
724
|
-
debugExerciseSelectionTrace,
|
|
725
|
-
exerciseSelectionTraces,
|
|
726
|
-
routine,
|
|
727
|
-
templatePrescriptionCount: routineTemplate.exercises.length,
|
|
728
|
-
routineExercises,
|
|
729
|
-
});
|
|
730
654
|
program.routines.push({
|
|
731
655
|
name: routine,
|
|
732
656
|
notes: routineTemplate.notes,
|
package/built/index.d.ts
CHANGED
|
@@ -326,6 +326,7 @@ export type BackofficeShadowBannedUser = Pick<BackofficeExistingUserResponse, 'i
|
|
|
326
326
|
backoffice_notes?: string;
|
|
327
327
|
};
|
|
328
328
|
export interface BackofficeUserComment {
|
|
329
|
+
comment_id: number;
|
|
329
330
|
workout_username: string;
|
|
330
331
|
workout_short_id: string;
|
|
331
332
|
comment_date: string;
|
|
@@ -650,7 +651,6 @@ export type TrainingGoal = Lookup<typeof trainingGoals>;
|
|
|
650
651
|
export type TrainingLevel = Lookup<typeof trainingLevels>;
|
|
651
652
|
export type ExerciseCategory = Lookup<typeof exerciseCategories>;
|
|
652
653
|
export type RestTimerLength = typeof restTimerLengths[number];
|
|
653
|
-
export type CardioPreference = 'no-cardio' | 'beginning' | 'end';
|
|
654
654
|
export type HevyTrainerProgramEquipment = Extract<Equipment, 'barbell' | 'dumbbell' | 'machine'>;
|
|
655
655
|
export declare const hevyTrainerProgramEquipments: readonly ["barbell", "dumbbell", "machine"];
|
|
656
656
|
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"];
|
|
@@ -1264,13 +1264,11 @@ export interface HevyTrainerProgram {
|
|
|
1264
1264
|
routines: HevyTrainerRoutine[];
|
|
1265
1265
|
focus_muscle?: SimplifiedMuscleGroup;
|
|
1266
1266
|
next_workout_index: number;
|
|
1267
|
-
workout_duration_minutes
|
|
1267
|
+
workout_duration_minutes?: WorkoutDurationMinutes;
|
|
1268
1268
|
rest_timer_length: RestTimerLength;
|
|
1269
|
-
cardio_preference: CardioPreference;
|
|
1270
1269
|
}
|
|
1271
1270
|
export interface PostHevyTrainerProgramRequestBody {
|
|
1272
1271
|
program: {
|
|
1273
|
-
version: 1;
|
|
1274
1272
|
title: string;
|
|
1275
1273
|
level: TrainingLevel;
|
|
1276
1274
|
goal: TrainingGoal;
|
|
@@ -1278,15 +1276,13 @@ export interface PostHevyTrainerProgramRequestBody {
|
|
|
1278
1276
|
weekly_frequency: WeeklyTrainingFrequency;
|
|
1279
1277
|
focus_muscle?: SimplifiedMuscleGroup;
|
|
1280
1278
|
routines: RoutineUpdate[];
|
|
1281
|
-
next_workout_index
|
|
1282
|
-
workout_duration_minutes
|
|
1279
|
+
next_workout_index?: number;
|
|
1280
|
+
workout_duration_minutes?: WorkoutDurationMinutes;
|
|
1283
1281
|
rest_timer_length: RestTimerLength;
|
|
1284
|
-
cardio_preference: CardioPreference;
|
|
1285
1282
|
};
|
|
1286
1283
|
}
|
|
1287
1284
|
export interface UpdateHevyTrainerProgramRequestBody {
|
|
1288
1285
|
program: {
|
|
1289
|
-
version: 1;
|
|
1290
1286
|
programId: string;
|
|
1291
1287
|
title: string;
|
|
1292
1288
|
level: TrainingLevel;
|
|
@@ -1294,10 +1290,9 @@ export interface UpdateHevyTrainerProgramRequestBody {
|
|
|
1294
1290
|
equipments: GranularEquipment[];
|
|
1295
1291
|
weekly_frequency: WeeklyTrainingFrequency;
|
|
1296
1292
|
focus_muscle?: SimplifiedMuscleGroup;
|
|
1297
|
-
next_workout_index
|
|
1298
|
-
workout_duration_minutes
|
|
1299
|
-
rest_timer_length
|
|
1300
|
-
cardio_preference: CardioPreference;
|
|
1293
|
+
next_workout_index?: number;
|
|
1294
|
+
workout_duration_minutes?: WorkoutDurationMinutes;
|
|
1295
|
+
rest_timer_length?: RestTimerLength;
|
|
1301
1296
|
routines: {
|
|
1302
1297
|
id: string;
|
|
1303
1298
|
title: string;
|
|
@@ -608,7 +608,6 @@ describe('generateProgram', () => {
|
|
|
608
608
|
workoutDurationMinutes: 60,
|
|
609
609
|
restTimerLength: 'medium',
|
|
610
610
|
exerciseStore: [chestExercise],
|
|
611
|
-
cardioPreference: 'no-cardio',
|
|
612
611
|
});
|
|
613
612
|
expect(result.success).toBe(true);
|
|
614
613
|
if (!result.success)
|
|
@@ -620,9 +619,6 @@ describe('generateProgram', () => {
|
|
|
620
619
|
expect(routine.notes).toBe('Hit the chest hard.');
|
|
621
620
|
expect(routine.exercises).toHaveLength(1);
|
|
622
621
|
const [exercise] = routine.exercises;
|
|
623
|
-
expect(exercise.kind).toBe('resistance');
|
|
624
|
-
if (exercise.kind !== 'resistance')
|
|
625
|
-
return;
|
|
626
622
|
expect(exercise.exerciseTemplate.id).toBe('bench');
|
|
627
623
|
expect(exercise.category).toBe('compound');
|
|
628
624
|
expect(exercise.muscleGroup).toBe('chest');
|
|
@@ -646,7 +642,6 @@ describe('generateProgram', () => {
|
|
|
646
642
|
workoutDurationMinutes: 60,
|
|
647
643
|
restTimerLength: 'medium',
|
|
648
644
|
exerciseStore: [],
|
|
649
|
-
cardioPreference: 'no-cardio',
|
|
650
645
|
});
|
|
651
646
|
expect(result.success).toBe(false);
|
|
652
647
|
if (result.success)
|
|
@@ -688,7 +683,6 @@ describe('generateProgram', () => {
|
|
|
688
683
|
workoutDurationMinutes,
|
|
689
684
|
restTimerLength: 'medium',
|
|
690
685
|
exerciseStore: [makeExercise({ id: 'bench' })],
|
|
691
|
-
cardioPreference: 'no-cardio',
|
|
692
686
|
});
|
|
693
687
|
const routine = (result.success ? result.program : result.partialProgram)
|
|
694
688
|
.routines[0];
|
|
@@ -757,7 +751,6 @@ describe('generateProgram', () => {
|
|
|
757
751
|
makeExercise({ id: 'bench-2', priority: 2 }),
|
|
758
752
|
makeExercise({ id: 'bench-3', priority: 1 }),
|
|
759
753
|
],
|
|
760
|
-
cardioPreference: 'no-cardio',
|
|
761
754
|
});
|
|
762
755
|
expect(result.success).toBe(true);
|
|
763
756
|
if (!result.success)
|
|
@@ -765,10 +758,7 @@ describe('generateProgram', () => {
|
|
|
765
758
|
// Only the 40- and 60-minute prescriptions survive; the 80-minute one
|
|
766
759
|
// is filtered out. Order is preserved.
|
|
767
760
|
const placed = result.program.routines[0].exercises;
|
|
768
|
-
expect(placed.
|
|
769
|
-
expect(placed
|
|
770
|
-
.filter(hevyTrainer_1.isTrainerProgramResistanceExercise)
|
|
771
|
-
.map((e) => e.warmupSetCount)).toEqual([1, 2]);
|
|
761
|
+
expect(placed.map((e) => e.warmupSetCount)).toEqual([1, 2]);
|
|
772
762
|
});
|
|
773
763
|
});
|
|
774
764
|
it('skips focus_muscle prescriptions when no focusMuscle is provided', () => {
|
|
@@ -787,7 +777,6 @@ describe('generateProgram', () => {
|
|
|
787
777
|
exerciseStore: [
|
|
788
778
|
makeExercise({ id: 'biceps-curl', muscle_group: 'biceps' }),
|
|
789
779
|
],
|
|
790
|
-
cardioPreference: 'no-cardio',
|
|
791
780
|
});
|
|
792
781
|
expect(result.success).toBe(true);
|
|
793
782
|
if (!result.success)
|
|
@@ -814,15 +803,11 @@ describe('generateProgram', () => {
|
|
|
814
803
|
restTimerLength: 'medium',
|
|
815
804
|
exerciseStore: [bicepsCurl],
|
|
816
805
|
focusMuscle: 'arms',
|
|
817
|
-
cardioPreference: 'no-cardio',
|
|
818
806
|
});
|
|
819
807
|
expect(result.success).toBe(true);
|
|
820
808
|
if (!result.success)
|
|
821
809
|
return;
|
|
822
810
|
const [exercise] = result.program.routines[0].exercises;
|
|
823
|
-
expect(exercise.kind).toBe('resistance');
|
|
824
|
-
if (exercise.kind !== 'resistance')
|
|
825
|
-
return;
|
|
826
811
|
expect(exercise.exerciseTemplate.id).toBe('biceps-curl');
|
|
827
812
|
expect(exercise.muscleGroup).toBe('focus_muscle');
|
|
828
813
|
});
|
|
@@ -840,7 +825,6 @@ describe('generateProgram', () => {
|
|
|
840
825
|
restTimerLength: 'medium',
|
|
841
826
|
exerciseStore: [excluded, alt],
|
|
842
827
|
excludedExerciseIds: new Set(['excluded']),
|
|
843
|
-
cardioPreference: 'no-cardio',
|
|
844
828
|
});
|
|
845
829
|
expect(result.success).toBe(true);
|
|
846
830
|
if (!result.success)
|
|
@@ -860,12 +844,10 @@ describe('generateProgram', () => {
|
|
|
860
844
|
restTimerLength: 'medium',
|
|
861
845
|
exerciseStore: [chest],
|
|
862
846
|
debugExerciseSelectionTrace: true,
|
|
863
|
-
cardioPreference: 'no-cardio',
|
|
864
847
|
});
|
|
865
848
|
expect(result.success).toBe(true);
|
|
866
849
|
expect(result.exerciseSelectionTraces).toHaveLength(1);
|
|
867
850
|
const [trace] = result.exerciseSelectionTraces;
|
|
868
|
-
expect(trace.traceKind).toBe('template');
|
|
869
851
|
expect(trace.routine).toBe('full_body_1');
|
|
870
852
|
expect(trace.selectedExerciseId).toBe('bench');
|
|
871
853
|
expect(trace.trace.selectedPass).toBe(1);
|
|
@@ -894,213 +876,10 @@ describe('generateProgram', () => {
|
|
|
894
876
|
makeExercise({ id: 'bench-2' }),
|
|
895
877
|
],
|
|
896
878
|
debugExerciseSelectionTrace: true,
|
|
897
|
-
cardioPreference: 'no-cardio',
|
|
898
879
|
});
|
|
899
880
|
expect(result.success).toBe(true);
|
|
900
881
|
expect(result.exerciseSelectionTraces.map((t) => t.prescriptionIndex)).toEqual([0, 1]);
|
|
901
882
|
});
|
|
902
|
-
it('records a single cardio trace entry (no multi-pass picker)', () => {
|
|
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: 'beginning',
|
|
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: 'beginning',
|
|
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: 'beginning',
|
|
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: 'beginning',
|
|
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: 'beginning',
|
|
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
|
-
});
|
|
1104
883
|
describe('barbell cap at frequency === 1 (end-to-end)', () => {
|
|
1105
884
|
it('drops the 4th+ barbell prescription when the user has a substitute', () => {
|
|
1106
885
|
const settings = makeSettings();
|
|
@@ -1128,7 +907,6 @@ describe('generateProgram', () => {
|
|
|
1128
907
|
workoutDurationMinutes: 60,
|
|
1129
908
|
restTimerLength: 'medium',
|
|
1130
909
|
exerciseStore: exercises,
|
|
1131
|
-
cardioPreference: 'no-cardio',
|
|
1132
910
|
});
|
|
1133
911
|
expect(result.success).toBe(false);
|
|
1134
912
|
if (result.success)
|
|
@@ -1159,7 +937,6 @@ describe('generateProgram', () => {
|
|
|
1159
937
|
workoutDurationMinutes: 60,
|
|
1160
938
|
restTimerLength: 'medium',
|
|
1161
939
|
exerciseStore: exercises,
|
|
1162
|
-
cardioPreference: 'no-cardio',
|
|
1163
940
|
});
|
|
1164
941
|
expect(result.success).toBe(true);
|
|
1165
942
|
if (!result.success)
|
|
@@ -1213,21 +990,24 @@ describe('generateProgram', () => {
|
|
|
1213
990
|
exerciseStore: exercises,
|
|
1214
991
|
focusMuscle: 'arms',
|
|
1215
992
|
debugExerciseSelectionTrace: true,
|
|
1216
|
-
cardioPreference: 'no-cardio',
|
|
1217
993
|
});
|
|
1218
994
|
const traces = result.exerciseSelectionTraces;
|
|
1219
995
|
// Counter increments once per placed focus_muscle prescription, so the
|
|
1220
996
|
// 4th prescription wraps back to the first entry of
|
|
1221
997
|
// simplifiedMuscleGroupToMuscleGroups.arms ('biceps').
|
|
1222
|
-
expect(traces
|
|
1223
|
-
|
|
1224
|
-
|
|
998
|
+
expect(traces.map((t) => t.resolvedMuscleGroup)).toEqual([
|
|
999
|
+
'biceps',
|
|
1000
|
+
'triceps',
|
|
1001
|
+
'forearms',
|
|
1002
|
+
'biceps',
|
|
1003
|
+
]);
|
|
1225
1004
|
// The first three passes should land on the priority-matching exercise
|
|
1226
1005
|
// (each muscle's only exercise is picked by pass 1).
|
|
1227
|
-
expect(traces
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1006
|
+
expect(traces.slice(0, 3).map((t) => t.selectedExerciseId)).toEqual([
|
|
1007
|
+
'biceps-ex',
|
|
1008
|
+
'triceps-ex',
|
|
1009
|
+
'forearms-ex',
|
|
1010
|
+
]);
|
|
1231
1011
|
});
|
|
1232
1012
|
it('does not advance the focus_muscle counter when a prescription is skipped for template mismatch', () => {
|
|
1233
1013
|
// 5-day split: push_1, pull_1, legs_1, upper_2, lower_2.
|
|
@@ -1271,7 +1051,6 @@ describe('generateProgram', () => {
|
|
|
1271
1051
|
exerciseStore: exercises,
|
|
1272
1052
|
focusMuscle: 'legs',
|
|
1273
1053
|
debugExerciseSelectionTrace: true,
|
|
1274
|
-
cardioPreference: 'no-cardio',
|
|
1275
1054
|
});
|
|
1276
1055
|
expect(result.success).toBe(true);
|
|
1277
1056
|
if (!result.success)
|
|
@@ -1283,9 +1062,7 @@ describe('generateProgram', () => {
|
|
|
1283
1062
|
'legs_1',
|
|
1284
1063
|
'lower_2',
|
|
1285
1064
|
]);
|
|
1286
|
-
expect(result.exerciseSelectionTraces
|
|
1287
|
-
.filter((t) => t.traceKind === 'template')
|
|
1288
|
-
.map((t) => t.resolvedMuscleGroup)).toEqual(['quadriceps', 'hamstrings']);
|
|
1065
|
+
expect(result.exerciseSelectionTraces.map((t) => t.resolvedMuscleGroup)).toEqual(['quadriceps', 'hamstrings']);
|
|
1289
1066
|
});
|
|
1290
1067
|
});
|
|
1291
1068
|
describe('isFocusMuscleExtraExerciseAllowed template compatibility', () => {
|
|
@@ -1311,7 +1088,6 @@ describe('generateProgram', () => {
|
|
|
1311
1088
|
exerciseStore: [candidateExercise],
|
|
1312
1089
|
focusMuscle,
|
|
1313
1090
|
debugExerciseSelectionTrace: true,
|
|
1314
|
-
cardioPreference: 'no-cardio',
|
|
1315
1091
|
});
|
|
1316
1092
|
// Find the routine we targeted (it may not be first in the split).
|
|
1317
1093
|
const routine = (result.success ? result.program : result.partialProgram).routines.find((r) => r.name === routineName);
|