hevy-shared 1.0.985 → 1.0.987

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.
@@ -32,6 +32,7 @@ export interface HTTPResponse<T = unknown> {
32
32
  export interface HTTPError<T = unknown> extends Error {
33
33
  response: HTTPResponse<T>;
34
34
  }
35
+ export declare const HTTP_METHODS: readonly ["get", "delete", "head", "options", "post", "put", "patch"];
35
36
  export declare const isHTTPError: <T = unknown>(e: unknown) => e is HTTPError<T>;
36
37
  export declare const isHTTPErrorResponse: (response: HTTPResponse<unknown>) => response is HTTPResponse<{
37
38
  error: string;
@@ -1,6 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isHTTPErrorResponse = exports.isHTTPError = void 0;
3
+ exports.isHTTPErrorResponse = exports.isHTTPError = exports.HTTP_METHODS = void 0;
4
+ exports.HTTP_METHODS = [
5
+ 'get',
6
+ 'delete',
7
+ 'head',
8
+ 'options',
9
+ 'post',
10
+ 'put',
11
+ 'patch',
12
+ ];
4
13
  const isHTTPError = (e) => {
5
14
  return (e instanceof Error &&
6
15
  'response' in e &&
@@ -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 ProgramExerciseSelectionTraceRecord {
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
- export interface TrainerProgramExercise {
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 pickExerciseForPrescription<T extends HevyTrainerLibraryExercise>(params: ExerciseSelectionParams<T> & {
229
+ export declare function pickTrainerExercise<T extends HevyTrainerLibraryExercise>(params: ExerciseSelectionParams<T> & {
207
230
  withTrace: true;
208
231
  }): PickExerciseResult<T>;
209
- export declare function pickExerciseForPrescription<T extends HevyTrainerLibraryExercise>(params: ExerciseSelectionParams<T>): T | undefined;
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;
@@ -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.pickExerciseForPrescription = pickExerciseForPrescription;
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, criteria, context) => {
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 pickExerciseForPrescriptionWithTrace = (params) => {
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 pickExerciseForPrescription(params) {
537
+ function pickTrainerExercise(params) {
533
538
  if (params.withTrace) {
534
- return pickExerciseForPrescriptionWithTrace(params);
539
+ return pickTrainerExerciseWithTrace(params);
535
540
  }
536
- return pickExerciseForPrescriptionWithTrace(params).exercise;
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 = pickExerciseForPrescription({
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 = pickExerciseForPrescription({
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?: WorkoutDurationMinutes;
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?: number;
1293
- workout_duration_minutes?: WorkoutDurationMinutes;
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?: number;
1307
- workout_duration_minutes?: WorkoutDurationMinutes;
1308
- rest_timer_length?: RestTimerLength;
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.isHeartRateSamples = exports.isPublicWorkout = exports.isSetType = exports.isRPE = exports.validRpeValues = exports.isSetPersonalRecordType = exports.weeklyTrainingFrequencies = exports.isGranularEquipment = exports.granularEquipments = exports.hevyTrainerProgramEquipments = 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 = 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('pickExerciseForPrescription', () => {
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.pickExerciseForPrescription)({
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.map((e) => e.warmupSetCount)).toEqual([1, 2]);
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.map((t) => t.resolvedMuscleGroup)).toEqual([
999
- 'biceps',
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.slice(0, 3).map((t) => t.selectedExerciseId)).toEqual([
1007
- 'biceps-ex',
1008
- 'triceps-ex',
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.map((t) => t.resolvedMuscleGroup)).toEqual(['quadriceps', 'hamstrings']);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-shared",
3
- "version": "1.0.985",
3
+ "version": "1.0.987",
4
4
  "description": "",
5
5
  "main": "built/index.js",
6
6
  "types": "built/index.d.ts",