hevy-shared 1.0.1047 → 1.0.1049

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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { InstructionsLanguage } from './exerciseLocaleUtils';
2
- import { WorkoutDurationMinutes } from './hevyTrainer';
2
+ import { TrainerWorkoutTemplateName, WorkoutDurationMinutes } from './hevyTrainer';
3
3
  import { BugReportQuestionSchema, CreateBugReportRequestBodySchema, postEmailBackofficeSchema } from './schemas';
4
4
  import { Language } from './translations';
5
5
  import { Lookup } from './typeUtils';
@@ -29,6 +29,7 @@ export * from './hevyTrainer';
29
29
  export * from './translations';
30
30
  export * from './exerciseLocaleUtils';
31
31
  export * from './getVolumeComparison';
32
+ export * from './geography';
32
33
  export type WeightUnit = 'kg' | 'lbs';
33
34
  export declare const isWeightUnit: (x: string) => x is WeightUnit;
34
35
  export type DistanceUnit = 'kilometers' | 'miles';
@@ -302,8 +303,7 @@ export interface BackofficeExistingUserResponse {
302
303
  public_api_key: string | null;
303
304
  limited_discovery: boolean;
304
305
  coach_trial_expire_date?: string;
305
- hevy_trainer_program: HevyTrainerProgram | null;
306
- email_consent: boolean;
306
+ hevy_trainer_program: TrainerProgramV3 | null;
307
307
  }
308
308
  export interface BackofficeDeletedUserResponse {
309
309
  state: 'deleted-account';
@@ -748,6 +748,10 @@ export interface RepRange {
748
748
  start: number | null;
749
749
  end: number | null;
750
750
  }
751
+ export interface StrictRepRange extends RepRange {
752
+ start: number;
753
+ end: number;
754
+ }
751
755
  export interface WorkoutComment {
752
756
  id: number;
753
757
  username: string;
@@ -876,12 +880,14 @@ export interface Routine extends BaseRoutine {
876
880
  username: string;
877
881
  coach_id: null;
878
882
  }
883
+ /** @deprecated Use TrainerWorkoutTemplate instead */
879
884
  export interface HevyTrainerRoutine extends Routine {
880
885
  hevy_trainer_program_id: string;
881
886
  program_id: null;
882
887
  coach_force_rpe_enabled: false;
883
888
  folder_id: null;
884
889
  }
890
+ /** @deprecated Use TrainerWorkoutTemplate instead */
885
891
  export declare const isHevyTrainerRoutine: (routine: BaseRoutine) => routine is HevyTrainerRoutine;
886
892
  export type CoachsShallowLibraryRoutine = Omit<BaseRoutine, 'exercises' | 'profile_pic' | 'hevy_trainer_program_id' | 'username'>;
887
893
  export interface CoachesRoutine extends BaseRoutine {
@@ -1050,6 +1056,105 @@ export interface GetTeamInviteResponse {
1050
1056
  export interface OutstandingInvitesForCoachTeamResponse {
1051
1057
  invites: CoachTeamInvite[];
1052
1058
  }
1059
+ export interface TrainerWorkoutTemplateResistanceExercise {
1060
+ kind: 'resistance';
1061
+ id: string;
1062
+ exercise_template_id: string;
1063
+ index: number;
1064
+ set_count: number;
1065
+ warmup_set_count: number;
1066
+ rep_range: StrictRepRange;
1067
+ rest_seconds: number;
1068
+ }
1069
+ export interface TrainerWorkoutTemplateCardioExercise {
1070
+ kind: 'cardio';
1071
+ id: string;
1072
+ exercise_template_id: string;
1073
+ index: number;
1074
+ set_count: 1;
1075
+ duration_seconds: number;
1076
+ }
1077
+ export interface TrainerWorkoutTemplateOtherExercise {
1078
+ kind: 'other';
1079
+ id: string;
1080
+ exercise_template_id: string;
1081
+ index: number;
1082
+ set_count: number;
1083
+ }
1084
+ export type TrainerWorkoutTemplateExercise = TrainerWorkoutTemplateResistanceExercise | TrainerWorkoutTemplateCardioExercise | TrainerWorkoutTemplateOtherExercise;
1085
+ export interface TrainerWorkoutTemplate {
1086
+ id: string;
1087
+ hevy_trainer_program_id: string;
1088
+ index: number;
1089
+ name: TrainerWorkoutTemplateName;
1090
+ exercises: TrainerWorkoutTemplateExercise[];
1091
+ }
1092
+ export interface TrainerProgramV3 {
1093
+ id: string;
1094
+ schema_version: 'v3';
1095
+ created_at: string;
1096
+ updated_at: string;
1097
+ level: TrainingLevel;
1098
+ goal: TrainingGoal;
1099
+ equipments: GranularEquipment[];
1100
+ weekly_frequency: WeeklyTrainingFrequency;
1101
+ templates: TrainerWorkoutTemplate[];
1102
+ focus_muscle?: SimplifiedMuscleGroup;
1103
+ next_workout_index: number;
1104
+ workout_duration_minutes: WorkoutDurationMinutes;
1105
+ rest_timer_length: RestTimerLength;
1106
+ cardio_preference: CardioPreference;
1107
+ }
1108
+ export type TrainerWorkoutTemplateExerciseInput = Omit<TrainerWorkoutTemplateResistanceExercise, 'id'> | Omit<TrainerWorkoutTemplateCardioExercise, 'id'> | Omit<TrainerWorkoutTemplateOtherExercise, 'id'>;
1109
+ export interface PostTrainerWorkoutTemplate {
1110
+ name: TrainerWorkoutTemplateName;
1111
+ index: number;
1112
+ exercises: TrainerWorkoutTemplateExerciseInput[];
1113
+ }
1114
+ export interface UpdateTrainerWorkoutTemplate extends PostTrainerWorkoutTemplate {
1115
+ id: string;
1116
+ }
1117
+ export interface PostTrainerProgramV3RequestBody {
1118
+ program: {
1119
+ version: 3;
1120
+ title: string;
1121
+ level: TrainingLevel;
1122
+ goal: TrainingGoal;
1123
+ equipments: GranularEquipment[];
1124
+ weekly_frequency: WeeklyTrainingFrequency;
1125
+ focus_muscle?: SimplifiedMuscleGroup;
1126
+ next_workout_index?: number;
1127
+ workout_duration_minutes: WorkoutDurationMinutes;
1128
+ rest_timer_length: RestTimerLength;
1129
+ cardio_preference: CardioPreference;
1130
+ templates: PostTrainerWorkoutTemplate[];
1131
+ };
1132
+ /**
1133
+ * Whether to delete (archive) the caller's currently active Hevy Trainer
1134
+ * program before creating the new one. Creating a program implicitly
1135
+ * replaces any active program, so this must be set explicitly:
1136
+ * - `false` — fail with `ActiveProgramExists` (409) if one already exists.
1137
+ * - `true` — archive the existing active program, then create the new one.
1138
+ */
1139
+ deleteActiveProgram: boolean;
1140
+ }
1141
+ export interface UpdateTrainerProgramV3RequestBody {
1142
+ program: {
1143
+ version: 3;
1144
+ programId: string;
1145
+ level: TrainingLevel;
1146
+ goal: TrainingGoal;
1147
+ equipments: GranularEquipment[];
1148
+ weekly_frequency: WeeklyTrainingFrequency;
1149
+ focus_muscle?: SimplifiedMuscleGroup;
1150
+ next_workout_index?: number;
1151
+ workout_duration_minutes: WorkoutDurationMinutes;
1152
+ rest_timer_length: RestTimerLength;
1153
+ cardio_preference: CardioPreference;
1154
+ templates: UpdateTrainerWorkoutTemplate[];
1155
+ };
1156
+ }
1157
+ /** @deprecated Use TrainerProgramV3 instead */
1053
1158
  export interface HevyTrainerProgram {
1054
1159
  id: string;
1055
1160
  created_at: string;
@@ -1066,6 +1171,7 @@ export interface HevyTrainerProgram {
1066
1171
  rest_timer_length: RestTimerLength;
1067
1172
  cardio_preference: CardioPreference;
1068
1173
  }
1174
+ /** @deprecated Use PostTrainerProgramV3RequestBody instead */
1069
1175
  export interface PostHevyTrainerProgramRequestBody {
1070
1176
  program: {
1071
1177
  version: 1;
@@ -1082,6 +1188,7 @@ export interface PostHevyTrainerProgramRequestBody {
1082
1188
  cardio_preference: CardioPreference;
1083
1189
  };
1084
1190
  }
1191
+ /** @deprecated Use UpdateTrainerProgramV3RequestBody instead */
1085
1192
  export interface UpdateHevyTrainerProgramRequestBody {
1086
1193
  program: {
1087
1194
  version: 1;
@@ -1104,7 +1211,7 @@ export interface UpdateHevyTrainerProgramRequestBody {
1104
1211
  }[];
1105
1212
  };
1106
1213
  }
1107
- /** @deprecated Use HevyTrainerProgram instead */
1214
+ /** @deprecated Use TrainerProgramV3 instead */
1108
1215
  export interface HevyTrainerProgramOld {
1109
1216
  id: string;
1110
1217
  created_at: string;
@@ -1119,7 +1226,7 @@ export interface HevyTrainerProgramOld {
1119
1226
  next_workout_index: number;
1120
1227
  workout_duration_minutes?: WorkoutDurationMinutes;
1121
1228
  }
1122
- /** @deprecated Use PostHevyTrainerProgramRequestBody instead */
1229
+ /** @deprecated Use PostTrainerProgramV3RequestBody instead */
1123
1230
  export interface PostHevyTrainerProgramOldRequestBody {
1124
1231
  program: {
1125
1232
  title: string;
@@ -1133,7 +1240,7 @@ export interface PostHevyTrainerProgramOldRequestBody {
1133
1240
  workout_duration_minutes?: WorkoutDurationMinutes;
1134
1241
  };
1135
1242
  }
1136
- /** @deprecated Use UpdateHevyTrainerProgramRequestBody instead */
1243
+ /** @deprecated Use UpdateTrainerProgramV3RequestBody instead */
1137
1244
  export interface UpdateHevyTrainerProgramOldRequestBody {
1138
1245
  program: {
1139
1246
  programId: string;
@@ -1407,7 +1514,8 @@ type CommercialGym = {
1407
1514
  name: string;
1408
1515
  fullAddress: string;
1409
1516
  city: string;
1410
- distanceM: number;
1517
+ latitude: number;
1518
+ longitude: number;
1411
1519
  };
1412
1520
  export type Gym = CommercialGym;
1413
1521
  export interface StripePrice {
package/built/index.js CHANGED
@@ -41,6 +41,7 @@ __exportStar(require("./hevyTrainer"), exports);
41
41
  __exportStar(require("./translations"), exports);
42
42
  __exportStar(require("./exerciseLocaleUtils"), exports);
43
43
  __exportStar(require("./getVolumeComparison"), exports);
44
+ __exportStar(require("./geography"), exports);
44
45
  const isWeightUnit = (x) => {
45
46
  return x === 'kg' || x === 'lbs';
46
47
  };
@@ -321,6 +322,7 @@ const isWorkoutBiometrics = (x) => {
321
322
  return caloriesAreValid && heartSamplesAreValid;
322
323
  };
323
324
  exports.isWorkoutBiometrics = isWorkoutBiometrics;
325
+ /** @deprecated Use TrainerWorkoutTemplate instead */
324
326
  const isHevyTrainerRoutine = (routine) => routine.hevy_trainer_program_id !== null;
325
327
  exports.isHevyTrainerRoutine = isHevyTrainerRoutine;
326
328
  exports.measurementsList = [
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const geography_1 = require("../geography");
13
+ describe('Geography utils', () => {
14
+ describe('distanceMBetween', () => {
15
+ it('Calculates the distance between two sets of coordinates', () => __awaiter(void 0, void 0, void 0, function* () {
16
+ expect((0, geography_1.distanceMBetween)(51.001, 52, 51, 52)).toBe(111);
17
+ expect((0, geography_1.distanceMBetween)(51.002, 52, 51, 52)).toBe(222);
18
+ expect((0, geography_1.distanceMBetween)(50.999, 52, 51, 52)).toBe(111);
19
+ expect((0, geography_1.distanceMBetween)(51.01, 52, 51, 52)).toBe(1112);
20
+ expect((0, geography_1.distanceMBetween)(51.04, 52, 51, 52)).toBe(4448);
21
+ expect((0, geography_1.distanceMBetween)(51.035, 52.035, 51, 52)).toBe(4598);
22
+ expect((0, geography_1.distanceMBetween)(50.965, 51.965, 51, 52)).toBe(4599);
23
+ }));
24
+ });
25
+ });
@@ -236,7 +236,7 @@ const makeSettings = (overrides = {}) => {
236
236
  };
237
237
  return acc;
238
238
  }, {});
239
- const templates = hevyTrainer_1.routineNames.reduce((acc, name) => {
239
+ const templates = hevyTrainer_1.workoutTemplateNames.reduce((acc, name) => {
240
240
  acc[name] = { exercises: [] };
241
241
  return acc;
242
242
  }, {});
@@ -372,7 +372,7 @@ describe('getPrioritySortedExercises', () => {
372
372
  expect(result.quadriceps.map((e) => e.id)).toEqual(['leg-ex']);
373
373
  });
374
374
  });
375
- const baseCriteria = (overrides = {}) => (Object.assign({ exerciseCategory: 'compound', equipments: [], routineBarbellExerciseCount: 0, level: 'beginner', goal: 'strength', muscleGroup: 'chest', frequency: 3 }, overrides));
375
+ const baseCriteria = (overrides = {}) => (Object.assign({ exerciseCategory: 'compound', equipments: [], workoutBarbellExerciseCount: 0, level: 'beginner', goal: 'strength', muscleGroup: 'chest', frequency: 3 }, overrides));
376
376
  /** Produces a `sortedExercises` record where only `chest` is populated. */
377
377
  const chestOnlySorted = (exercises) => {
378
378
  const empty = __1.muscleGroups.reduce((acc, mg) => {
@@ -394,7 +394,7 @@ describe('pickTrainerExercise', () => {
394
394
  });
395
395
  expect(result === null || result === void 0 ? void 0 : result.id).toBe('fresh');
396
396
  });
397
- it('pass 2: falls back to exercises not used in the current routine when all are used in the program', () => {
397
+ it('pass 2: falls back to exercises not used in the current workout when all are used in the program', () => {
398
398
  const onlyExercise = makeExercise({ id: 'only' });
399
399
  const result = (0, hevyTrainer_1.pickTrainerExercise)({
400
400
  sortedExercises: chestOnlySorted([onlyExercise]),
@@ -402,8 +402,8 @@ describe('pickTrainerExercise', () => {
402
402
  context: {
403
403
  // Marked as used in the whole program already.
404
404
  programUsedExerciseIds: new Set(['only']),
405
- // But not used in this routine, so pass 2 should succeed.
406
- routineUsedExerciseIds: new Set(),
405
+ // But not used in this workout, so pass 2 should succeed.
406
+ workoutUsedExerciseIds: new Set(),
407
407
  },
408
408
  });
409
409
  expect(result === null || result === void 0 ? void 0 : result.id).toBe('only');
@@ -506,7 +506,7 @@ describe('pickTrainerExercise', () => {
506
506
  sortedExercises: chestOnlySorted([ex]),
507
507
  criteria: baseCriteria({
508
508
  frequency: 3,
509
- routineBarbellExerciseCount: 10,
509
+ workoutBarbellExerciseCount: 10,
510
510
  equipments: ['dumbbell'],
511
511
  }),
512
512
  context: {},
@@ -519,7 +519,7 @@ describe('pickTrainerExercise', () => {
519
519
  sortedExercises: chestOnlySorted([ex]),
520
520
  criteria: baseCriteria({
521
521
  frequency: 1,
522
- routineBarbellExerciseCount: 2,
522
+ workoutBarbellExerciseCount: 2,
523
523
  equipments: ['dumbbell'],
524
524
  }),
525
525
  context: {},
@@ -532,7 +532,7 @@ describe('pickTrainerExercise', () => {
532
532
  sortedExercises: chestOnlySorted([ex]),
533
533
  criteria: baseCriteria({
534
534
  frequency: 1,
535
- routineBarbellExerciseCount: 3,
535
+ workoutBarbellExerciseCount: 3,
536
536
  // Dumbbell is listed as a barbell substitute, so the cap applies.
537
537
  equipments: ['dumbbell'],
538
538
  }),
@@ -546,7 +546,7 @@ describe('pickTrainerExercise', () => {
546
546
  sortedExercises: chestOnlySorted([ex]),
547
547
  criteria: baseCriteria({
548
548
  frequency: 1,
549
- routineBarbellExerciseCount: 5,
549
+ workoutBarbellExerciseCount: 5,
550
550
  // No substitutes → avoid dead-ends by allowing the barbell.
551
551
  equipments: [],
552
552
  }),
@@ -587,7 +587,7 @@ describe('pickTrainerExercise', () => {
587
587
  describe('generateProgram', () => {
588
588
  const buildSettingsForChestProgram = () => {
589
589
  const settings = makeSettings();
590
- // Put a single chest compound prescription into the 1-day full body routine.
590
+ // Put a single chest compound prescription into the 1-day full body workout.
591
591
  settings.templates.full_body_1 = {
592
592
  exercises: [
593
593
  { muscle_group: 'chest', category: 'compound', warmup_set_count: 2 },
@@ -596,7 +596,7 @@ describe('generateProgram', () => {
596
596
  };
597
597
  return settings;
598
598
  };
599
- it('produces one routine per programSplit and populates exercise fields from the settings', () => {
599
+ it('produces one workout per programSplit and populates exercise fields from the settings', () => {
600
600
  const settings = buildSettingsForChestProgram();
601
601
  const chestExercise = makeExercise({ id: 'bench', muscle_group: 'chest' });
602
602
  const result = (0, hevyTrainer_1.generateProgram)({
@@ -613,27 +613,19 @@ describe('generateProgram', () => {
613
613
  expect(result.success).toBe(true);
614
614
  if (!result.success)
615
615
  return;
616
- expect(result.program.name).toBe(1);
617
- expect(result.program.routines).toHaveLength(hevyTrainer_1.programSplits[1].length);
618
- const [routine] = result.program.routines;
619
- expect(routine.name).toBe('full_body_1');
620
- expect(routine.notes).toBe('Hit the chest hard.');
621
- expect(routine.exercises).toHaveLength(1);
622
- const [exercise] = routine.exercises;
623
- expect(exercise.kind).toBe('resistance');
624
- if (exercise.kind !== 'resistance')
616
+ expect(result.program.workoutTemplates).toHaveLength(hevyTrainer_1.programSplits[1].length);
617
+ const [workoutTemplate] = result.program.workoutTemplates;
618
+ expect(workoutTemplate.name).toBe('full_body_1');
619
+ expect(workoutTemplate.exercises).toHaveLength(1);
620
+ const [exercise] = workoutTemplate.exercises;
621
+ if (!(0, hevyTrainer_1.isTrainerProgramResistanceExercise)(exercise))
625
622
  return;
626
- expect(exercise.exerciseTemplate.id).toBe('bench');
627
- expect(exercise.category).toBe('compound');
628
- expect(exercise.muscleGroup).toBe('chest');
629
- expect(exercise.warmupSetCount).toBe(2);
630
- // strength + one_day -> 1 (see `makeSettings`)
631
- expect(exercise.sets).toBe(1);
632
- // strength + compound -> 1..5
633
- expect(exercise.repRangeStart).toBe(1);
634
- expect(exercise.repRangeEnd).toBe(5);
635
- // strength + medium + compound -> 60
636
- expect(exercise.restTimerSeconds).toBe(60);
623
+ expect(exercise.kind).toBe('resistance');
624
+ expect(exercise.exercise_template.id).toBe('bench');
625
+ expect(exercise.warmup_set_count).toBe(2);
626
+ expect(exercise.set_count).toBe(1);
627
+ expect(exercise.rep_range).toEqual({ start: 1, end: 5 });
628
+ expect(exercise.rest_seconds).toBe(60);
637
629
  });
638
630
  it('returns a partial program and errors when no matching exercise can be found', () => {
639
631
  const settings = buildSettingsForChestProgram();
@@ -656,14 +648,14 @@ describe('generateProgram', () => {
656
648
  type: 'exercise_not_found',
657
649
  muscleGroup: 'chest',
658
650
  });
659
- // The routine structure still exists in the partial program, just without
651
+ // The workout template structure still exists in the partial program, just without
660
652
  // any resolved exercises.
661
- expect(result.partialProgram.routines).toHaveLength(1);
662
- expect(result.partialProgram.routines[0].exercises).toEqual([]);
653
+ expect(result.partialProgram.workoutTemplates).toHaveLength(1);
654
+ expect(result.partialProgram.workoutTemplates[0].exercises).toEqual([]);
663
655
  });
664
656
  describe('min_workout_duration_limit filtering', () => {
665
657
  /**
666
- * Runs a 1-day program containing a single chest compound prescription
658
+ * Runs a 1-day workout program containing a single chest compound prescription
667
659
  * with the provided `min_workout_duration_limit` against the provided
668
660
  * `workoutDurationMinutes`, and returns whether the prescription was
669
661
  * placed.
@@ -690,9 +682,8 @@ describe('generateProgram', () => {
690
682
  exerciseStore: [makeExercise({ id: 'bench' })],
691
683
  cardioPreference: 'no-cardio',
692
684
  });
693
- const routine = (result.success ? result.program : result.partialProgram)
694
- .routines[0];
695
- return routine.exercises.length === 1;
685
+ const workoutTemplate = (result.success ? result.program : result.partialProgram).workoutTemplates[0];
686
+ return workoutTemplate.exercises.length === 1;
696
687
  };
697
688
  it('skips prescriptions whose limit is strictly greater than the selected duration', () => {
698
689
  expect(runDurationProbe({ limit: 80, workoutDurationMinutes: 40 })).toBe(false);
@@ -764,11 +755,11 @@ describe('generateProgram', () => {
764
755
  return;
765
756
  // Only the 40- and 60-minute prescriptions survive; the 80-minute one
766
757
  // is filtered out. Order is preserved.
767
- const placed = result.program.routines[0].exercises;
758
+ const placed = result.program.workoutTemplates[0].exercises;
768
759
  expect(placed.every((e) => e.kind === 'resistance')).toBe(true);
769
760
  expect(placed
770
761
  .filter(hevyTrainer_1.isTrainerProgramResistanceExercise)
771
- .map((e) => e.warmupSetCount)).toEqual([1, 2]);
762
+ .map((e) => e.warmup_set_count)).toEqual([1, 2]);
772
763
  });
773
764
  });
774
765
  it('skips focus_muscle prescriptions when no focusMuscle is provided', () => {
@@ -792,7 +783,7 @@ describe('generateProgram', () => {
792
783
  expect(result.success).toBe(true);
793
784
  if (!result.success)
794
785
  return;
795
- expect(result.program.routines[0].exercises).toEqual([]);
786
+ expect(result.program.workoutTemplates[0].exercises).toEqual([]);
796
787
  });
797
788
  it('routes focus_muscle prescriptions to the requested SimplifiedMuscleGroup', () => {
798
789
  const settings = makeSettings();
@@ -819,12 +810,11 @@ describe('generateProgram', () => {
819
810
  expect(result.success).toBe(true);
820
811
  if (!result.success)
821
812
  return;
822
- const [exercise] = result.program.routines[0].exercises;
813
+ const [exercise] = result.program.workoutTemplates[0].exercises;
823
814
  expect(exercise.kind).toBe('resistance');
824
815
  if (exercise.kind !== 'resistance')
825
816
  return;
826
- expect(exercise.exerciseTemplate.id).toBe('biceps-curl');
827
- expect(exercise.muscleGroup).toBe('focus_muscle');
817
+ expect(exercise.exercise_template.id).toBe('biceps-curl');
828
818
  });
829
819
  it('excludes exercises listed in `excludedExerciseIds`', () => {
830
820
  const settings = buildSettingsForChestProgram();
@@ -845,7 +835,7 @@ describe('generateProgram', () => {
845
835
  expect(result.success).toBe(true);
846
836
  if (!result.success)
847
837
  return;
848
- expect(result.program.routines[0].exercises[0].exerciseTemplate.id).toBe('alt');
838
+ expect(result.program.workoutTemplates[0].exercises[0].exercise_template.id).toBe('alt');
849
839
  });
850
840
  it('attaches exercise selection traces when debugExerciseSelectionTrace is true', () => {
851
841
  const settings = buildSettingsForChestProgram();
@@ -866,7 +856,7 @@ describe('generateProgram', () => {
866
856
  expect(result.exerciseSelectionTraces).toHaveLength(1);
867
857
  const [trace] = result.exerciseSelectionTraces;
868
858
  expect(trace.traceKind).toBe('template');
869
- expect(trace.routine).toBe('full_body_1');
859
+ expect(trace.workoutTemplate).toBe('full_body_1');
870
860
  expect(trace.selectedExerciseId).toBe('bench');
871
861
  expect(trace.trace.selectedPass).toBe(1);
872
862
  });
@@ -938,7 +928,7 @@ describe('generateProgram', () => {
938
928
  expect(cardioTrace.equipments).toEqual([]);
939
929
  expect(cardioTrace.level).toBe('beginner');
940
930
  });
941
- it('uses the same prioritized cardio exercise on every routine (not blocked by program reuse)', () => {
931
+ it('uses the same prioritized cardio exercise on every workout (not blocked by program reuse)', () => {
942
932
  const settings = makeSettings();
943
933
  settings.templates.full_body_2_a = {
944
934
  exercises: [{ muscle_group: 'chest', category: 'compound' }],
@@ -971,13 +961,13 @@ describe('generateProgram', () => {
971
961
  expect(result.success).toBe(true);
972
962
  if (!result.success)
973
963
  return;
974
- expect(result.program.routines[0].exercises[0]).toMatchObject({
964
+ expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
975
965
  kind: 'cardio',
976
- exerciseTemplate: { id: 'run' },
966
+ exercise_template: { id: 'run' },
977
967
  });
978
- expect(result.program.routines[1].exercises[0]).toMatchObject({
968
+ expect(result.program.workoutTemplates[1].exercises[0]).toMatchObject({
979
969
  kind: 'cardio',
980
- exerciseTemplate: { id: 'run' },
970
+ exercise_template: { id: 'run' },
981
971
  });
982
972
  });
983
973
  it('skips cardio exercises that need equipment the user does not have', () => {
@@ -1016,9 +1006,9 @@ describe('generateProgram', () => {
1016
1006
  expect(result.success).toBe(true);
1017
1007
  if (!result.success)
1018
1008
  return;
1019
- expect(result.program.routines[0].exercises[0]).toMatchObject({
1009
+ expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
1020
1010
  kind: 'cardio',
1021
- exerciseTemplate: { id: 'body-run' },
1011
+ exercise_template: { id: 'body-run' },
1022
1012
  });
1023
1013
  });
1024
1014
  it('skips globally excluded cardio and uses the next priority exercise', () => {
@@ -1057,12 +1047,12 @@ describe('generateProgram', () => {
1057
1047
  expect(result.success).toBe(true);
1058
1048
  if (!result.success)
1059
1049
  return;
1060
- expect(result.program.routines[0].exercises[0]).toMatchObject({
1050
+ expect(result.program.workoutTemplates[0].exercises[0]).toMatchObject({
1061
1051
  kind: 'cardio',
1062
- exerciseTemplate: { id: 'run-b' },
1052
+ exercise_template: { id: 'run-b' },
1063
1053
  });
1064
1054
  });
1065
- it('keeps global trace order aligned with routine generation (no cross-routine unshift)', () => {
1055
+ it('keeps global trace order aligned with workout generation (no cross-workout unshift)', () => {
1066
1056
  const settings = makeSettings();
1067
1057
  settings.templates.full_body_2_a = {
1068
1058
  exercises: [{ muscle_group: 'chest', category: 'compound' }],
@@ -1096,15 +1086,15 @@ describe('generateProgram', () => {
1096
1086
  expect(result.success).toBe(true);
1097
1087
  if (!result.success)
1098
1088
  return;
1099
- const idxA = result.exerciseSelectionTraces.findIndex((t) => t.routine === 'full_body_2_a');
1100
- const idxB = result.exerciseSelectionTraces.findIndex((t) => t.routine === 'full_body_2_b');
1089
+ const idxA = result.exerciseSelectionTraces.findIndex((t) => t.workoutTemplate === 'full_body_2_a');
1090
+ const idxB = result.exerciseSelectionTraces.findIndex((t) => t.workoutTemplate === 'full_body_2_b');
1101
1091
  expect(idxA).toBeGreaterThanOrEqual(0);
1102
1092
  expect(idxB).toBeGreaterThan(idxA);
1103
1093
  });
1104
1094
  describe('barbell cap at frequency === 1 (end-to-end)', () => {
1105
1095
  it('drops the 4th+ barbell prescription when the user has a substitute', () => {
1106
1096
  const settings = makeSettings();
1107
- // 4 identical barbell compound prescriptions in the 1-day routine.
1097
+ // 4 identical barbell compound prescriptions in the 1-day workout.
1108
1098
  settings.templates.full_body_1 = {
1109
1099
  exercises: Array.from({ length: 4 }, () => ({
1110
1100
  muscle_group: 'chest',
@@ -1133,7 +1123,7 @@ describe('generateProgram', () => {
1133
1123
  expect(result.success).toBe(false);
1134
1124
  if (result.success)
1135
1125
  return;
1136
- expect(result.partialProgram.routines[0].exercises).toHaveLength(3);
1126
+ expect(result.partialProgram.workoutTemplates[0].exercises).toHaveLength(3);
1137
1127
  expect(result.errors).toHaveLength(1);
1138
1128
  });
1139
1129
  it('allows 4+ barbell prescriptions at frequency 1 when no substitute is present', () => {
@@ -1164,19 +1154,19 @@ describe('generateProgram', () => {
1164
1154
  expect(result.success).toBe(true);
1165
1155
  if (!result.success)
1166
1156
  return;
1167
- expect(result.program.routines[0].exercises).toHaveLength(4);
1157
+ expect(result.program.workoutTemplates[0].exercises).toHaveLength(4);
1168
1158
  });
1169
1159
  });
1170
1160
  describe('focus_muscle cycling', () => {
1171
1161
  it('cycles through simplifiedMuscleGroupToMuscleGroups as focus_muscle prescriptions are placed', () => {
1172
1162
  // Use a 2-day split so we can place 4 focus_muscle exercises across two
1173
- // routines and confirm the counter survives between routines.
1163
+ // workouts and confirm the counter survives between workouts.
1174
1164
  const settings = makeSettings();
1175
1165
  const focusPrescription = {
1176
1166
  muscle_group: 'focus_muscle',
1177
1167
  category: 'isolation',
1178
1168
  };
1179
- // full_body routines allow any focus muscle. Place 2 per routine so the
1169
+ // full_body workouts allow any focus muscle. Place 2 per workout so the
1180
1170
  // combined count across the program is 4.
1181
1171
  settings.templates.full_body_2_a = {
1182
1172
  exercises: [focusPrescription, focusPrescription],
@@ -1246,7 +1236,7 @@ describe('generateProgram', () => {
1246
1236
  settings.templates.upper_2 = { exercises: [focusPrescription] };
1247
1237
  settings.templates.lower_2 = { exercises: [focusPrescription] };
1248
1238
  // `legs` cycles through ['quadriceps', 'hamstrings', 'calves', 'glutes',
1249
- // 'abductors', 'adductors']. The first two allowed routines (legs_1 and
1239
+ // 'abductors', 'adductors']. The first two allowed workouts (legs_1 and
1250
1240
  // lower_2) should land on quadriceps and hamstrings respectively.
1251
1241
  const exercises = [
1252
1242
  makeExercise({
@@ -1279,10 +1269,7 @@ describe('generateProgram', () => {
1279
1269
  // Only legs_1 and lower_2 run the focus_muscle prescription. Both
1280
1270
  // succeed and should have selected quadriceps then hamstrings in order.
1281
1271
  expect(result.exerciseSelectionTraces).toHaveLength(2);
1282
- expect(result.exerciseSelectionTraces.map((t) => t.routine)).toEqual([
1283
- 'legs_1',
1284
- 'lower_2',
1285
- ]);
1272
+ expect(result.exerciseSelectionTraces.map((t) => t.workoutTemplate)).toEqual(['legs_1', 'lower_2']);
1286
1273
  expect(result.exerciseSelectionTraces
1287
1274
  .filter((t) => t.traceKind === 'template')
1288
1275
  .map((t) => t.resolvedMuscleGroup)).toEqual(['quadriceps', 'hamstrings']);
@@ -1290,14 +1277,14 @@ describe('generateProgram', () => {
1290
1277
  });
1291
1278
  describe('isFocusMuscleExtraExerciseAllowed template compatibility', () => {
1292
1279
  /**
1293
- * Runs a single-routine program that places one focus_muscle
1280
+ * Runs a single-workout template program that places one focus_muscle
1294
1281
  * prescription into the requested template and returns whether the
1295
1282
  * prescription produced an exercise or was skipped.
1296
1283
  */
1297
- const runFocusMuscleProbe = (routineName, focusMuscle, frequency, candidateExercise) => {
1284
+ const runFocusMuscleProbe = (workoutTemplateName, focusMuscle, frequency, candidateExercise) => {
1298
1285
  var _a;
1299
1286
  const settings = makeSettings();
1300
- settings.templates[routineName] = {
1287
+ settings.templates[workoutTemplateName] = {
1301
1288
  exercises: [{ muscle_group: 'focus_muscle', category: 'isolation' }],
1302
1289
  };
1303
1290
  const result = (0, hevyTrainer_1.generateProgram)({
@@ -1313,13 +1300,13 @@ describe('generateProgram', () => {
1313
1300
  debugExerciseSelectionTrace: true,
1314
1301
  cardioPreference: 'no-cardio',
1315
1302
  });
1316
- // Find the routine we targeted (it may not be first in the split).
1317
- const routine = (result.success ? result.program : result.partialProgram).routines.find((r) => r.name === routineName);
1303
+ // Find the workout template we targeted (it may not be first in the split).
1304
+ const workoutTemplate = (result.success ? result.program : result.partialProgram).workoutTemplates.find((w) => w.name === workoutTemplateName);
1318
1305
  return {
1319
- placed: ((_a = routine === null || routine === void 0 ? void 0 : routine.exercises.length) !== null && _a !== void 0 ? _a : 0) > 0,
1306
+ placed: ((_a = workoutTemplate === null || workoutTemplate === void 0 ? void 0 : workoutTemplate.exercises.length) !== null && _a !== void 0 ? _a : 0) > 0,
1320
1307
  };
1321
1308
  };
1322
- it('allows every focus muscle on full_body routines', () => {
1309
+ it('allows every focus muscle on full_body workouts', () => {
1323
1310
  const muscles = ['chest', 'shoulders', 'arms', 'legs', 'back'];
1324
1311
  muscles.forEach((focusMuscle) => {
1325
1312
  // Use a muscle that's in the simplified group: biceps for arms, chest
@@ -155,6 +155,7 @@ class WorkoutBuilder {
155
155
  workoutVisibility: 'public',
156
156
  isWorkoutBiometricsPublic: true,
157
157
  trainerProgramId: undefined,
158
+ trainerWorkoutTemplateId: undefined,
158
159
  shareTo: {
159
160
  strava: false,
160
161
  appleHealth: false,
@@ -11,7 +11,7 @@ export interface WorkoutSync {
11
11
  isMore: boolean;
12
12
  updated_at?: string;
13
13
  }
14
- export type Workout = OwnedWorkout & Partial<UserWorkout>;
15
- export type WorkoutExercise = OwnedWorkoutExercise & Partial<UserWorkoutExercise>;
16
- export type WorkoutExerciseSet = OwnedWorkoutExerciseSet & Partial<UserWorkoutExerciseSet>;
14
+ export type Workout = OwnedWorkout | UserWorkout;
15
+ export type WorkoutExercise = OwnedWorkoutExercise | UserWorkoutExercise;
16
+ export type WorkoutExerciseSet = OwnedWorkoutExerciseSet | UserWorkoutExerciseSet;
17
17
  export declare function isOwnedWorkout(workout: OwnedWorkout | UserWorkout): workout is OwnedWorkout;
@@ -20,8 +20,7 @@ __exportStar(require("./userWorkout"), exports);
20
20
  __exportStar(require("./publicWorkout"), exports);
21
21
  __exportStar(require("./normalizedWorkout"), exports);
22
22
  __exportStar(require("./postWorkoutRequest"), exports);
23
- // Stub
23
+ // Placeholder, will be done better but this works for now
24
24
  function isOwnedWorkout(workout) {
25
- void workout;
26
- return true;
25
+ return !('gym' in workout);
27
26
  }