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.
@@ -1,4 +1,4 @@
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 } 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 TemplateExerciseSelectionTraceRecord {
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
- /** Resistance prescriptions from templates (sets, reps, rest, notes). */
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;
@@ -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.routineNames = exports.defaultDurationPerFrequency = exports.hevyTrainerExerciseCategories = exports.granularEquipmentsToTrainerEquipments = exports.trainerEquipmentToGranularEquipments = exports.granularEquipmentDefaults = exports.trainerGymTypes = exports.workoutDurationOptions = void 0;
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, //TODO: muscle group 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, cardioPreference, } = params;
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: WorkoutDurationMinutes;
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: number;
1282
- workout_duration_minutes: WorkoutDurationMinutes;
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: number;
1298
- workout_duration_minutes: WorkoutDurationMinutes;
1299
- rest_timer_length: RestTimerLength;
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.every((e) => e.kind === 'resistance')).toBe(true);
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
- .filter((t) => t.traceKind === 'template')
1224
- .map((t) => t.resolvedMuscleGroup)).toEqual(['biceps', 'triceps', 'forearms', 'biceps']);
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
- .filter((t) => t.traceKind === 'template')
1229
- .slice(0, 3)
1230
- .map((t) => t.selectedExerciseId)).toEqual(['biceps-ex', 'triceps-ex', 'forearms-ex']);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-shared",
3
- "version": "1.0.980",
3
+ "version": "1.0.982",
4
4
  "description": "",
5
5
  "main": "built/index.js",
6
6
  "types": "built/index.d.ts",