@dgpholdings/greatoak-shared 1.1.49 → 1.1.51

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,5 +1,6 @@
1
- import { TRecord, TRecordWeight, TRecordDuration, TRecordBodyWeight, TRecordDistance } from "../types";
1
+ import { TRecord, TRecordWeight, TRecordDuration, TRecordBodyWeight, TRecordCardioFree, TRecordCardioMachine } from "../types";
2
2
  export declare const isRecordWeightTypeGuard: (param: TRecord[]) => param is TRecordWeight[];
3
3
  export declare const isRecordDurationTypeGuard: (param: TRecord[]) => param is TRecordDuration[];
4
4
  export declare const isRecordBodyWeightTypeGuard: (param: TRecord[]) => param is TRecordBodyWeight[];
5
- export declare const isRecordDistanceTypeGuard: (param: TRecord[]) => param is TRecordDistance[];
5
+ export declare const isRecordCardioFreeTypeGuard: (param: TRecord[]) => param is TRecordCardioFree[];
6
+ export declare const isRecordCardioMachineTypeGuard: (param: TRecord[]) => param is TRecordCardioMachine[];
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isRecordDistanceTypeGuard = exports.isRecordBodyWeightTypeGuard = exports.isRecordDurationTypeGuard = exports.isRecordWeightTypeGuard = void 0;
3
+ exports.isRecordCardioMachineTypeGuard = exports.isRecordCardioFreeTypeGuard = exports.isRecordBodyWeightTypeGuard = exports.isRecordDurationTypeGuard = exports.isRecordWeightTypeGuard = void 0;
4
4
  const isRecordWeightTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "weight-reps"; };
5
5
  exports.isRecordWeightTypeGuard = isRecordWeightTypeGuard;
6
6
  const isRecordDurationTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "duration"; };
7
7
  exports.isRecordDurationTypeGuard = isRecordDurationTypeGuard;
8
8
  const isRecordBodyWeightTypeGuard = (param) => { var _a; return ((_a = param === null || param === void 0 ? void 0 : param[0]) === null || _a === void 0 ? void 0 : _a.type) === "reps-only"; };
9
9
  exports.isRecordBodyWeightTypeGuard = isRecordBodyWeightTypeGuard;
10
- const isRecordDistanceTypeGuard = (param) => param[0].type === "distance";
11
- exports.isRecordDistanceTypeGuard = isRecordDistanceTypeGuard;
10
+ const isRecordCardioFreeTypeGuard = (param) => param[0].type === "cardio-free";
11
+ exports.isRecordCardioFreeTypeGuard = isRecordCardioFreeTypeGuard;
12
+ const isRecordCardioMachineTypeGuard = (param) => param[0].type === "cardio-machine";
13
+ exports.isRecordCardioMachineTypeGuard = isRecordCardioMachineTypeGuard;
@@ -61,13 +61,6 @@ export type TTimingGuardrails = TBaseTimingGuardrails & ({
61
61
  max: number;
62
62
  typical: number;
63
63
  };
64
- } | {
65
- type: "distance";
66
- pacing: {
67
- minPacePerUnit: number;
68
- maxPacePerUnit: number;
69
- typicalPacePerUnit: number;
70
- };
71
64
  });
72
65
  export type TExercise = {
73
66
  exerciseId: string;
@@ -1,7 +1,6 @@
1
1
  export type TAuthType = "email" | "apple" | "anonymous";
2
2
  export type TGender = "male" | "female" | "unmentioned";
3
3
  export type TFitnessGoal = "strength" | "hypertrophy" | "endurance" | "general" | "fat_burn" | "flexibility";
4
- export type TUserFitnessGoal = "strength" | "hypertrophy" | "endurance" | "general";
5
4
  export type TUserMetric = {
6
5
  dob: Date;
7
6
  weightKg?: number;
@@ -39,7 +38,6 @@ export type TUser = {
39
38
  billingPlanType: "monthly" | "annually" | "free";
40
39
  billingPlanVersion?: string;
41
40
  billingPlanGeoCountryCode: string;
42
- fitnessLevel?: number;
43
41
  paymentMethod?: "credit_card" | "paypal" | "apple_pay" | "upi";
44
42
  billingPlanNextDueDate?: string;
45
43
  devices: {
@@ -54,5 +52,6 @@ export type TUser = {
54
52
  weightKg?: number;
55
53
  heightCm?: number;
56
54
  bodyFatPercentage?: number;
57
- fitnessGoal: TUserFitnessGoal;
55
+ fitnessGoal: TFitnessGoal;
56
+ fitnessLevel: number;
58
57
  };
@@ -20,9 +20,13 @@ export type TRecord = {
20
20
  auxWeightKg: string;
21
21
  rir?: string;
22
22
  } | {
23
- type: "distance";
23
+ type: "cardio-machine";
24
+ speed: string;
25
+ resistance: string;
26
+ durationSecs: string;
27
+ } | {
28
+ type: "cardio-free";
24
29
  distanceKm: string;
25
- auxWeightKg: string;
26
30
  durationSecs: string;
27
31
  });
28
32
  export type TRecordDuration = Extract<TRecord, {
@@ -34,8 +38,11 @@ export type TRecordWeight = Extract<TRecord, {
34
38
  export type TRecordBodyWeight = Extract<TRecord, {
35
39
  type: "reps-only";
36
40
  }>;
37
- export type TRecordDistance = Extract<TRecord, {
38
- type: "distance";
41
+ export type TRecordCardioFree = Extract<TRecord, {
42
+ type: "cardio-free";
43
+ }>;
44
+ export type TRecordCardioMachine = Extract<TRecord, {
45
+ type: "cardio-machine";
39
46
  }>;
40
47
  export type TTemplateConfig = {
41
48
  enableAuxWeight?: boolean;
@@ -2,7 +2,6 @@ import { TRecord } from "../types";
2
2
  type RefinedBase = {
3
3
  isDone: boolean;
4
4
  rpe?: number;
5
- rir?: number;
6
5
  workDurationSecs?: number;
7
6
  restDurationSecs?: number;
8
7
  setNote?: string;
@@ -12,24 +11,33 @@ export type TRefinedWeightRecord = RefinedBase & {
12
11
  type: "weight-reps";
13
12
  kg: number;
14
13
  reps: number;
14
+ rir?: number;
15
15
  };
16
16
  export type TRefinedDurationRecord = RefinedBase & {
17
17
  type: "duration";
18
18
  durationSecs: number;
19
- auxWeightKg?: number;
19
+ auxWeightKg: number;
20
20
  };
21
21
  export type TRefinedBodyWeightRecord = RefinedBase & {
22
22
  type: "reps-only";
23
23
  reps: number;
24
- auxWeightKg?: number;
24
+ auxWeightKg: number;
25
+ rir?: number;
26
+ };
27
+ export type TRefinedCardioMachineRecord = RefinedBase & {
28
+ type: "cardio-machine";
29
+ speed: number;
30
+ resistance: number;
31
+ durationSecs: number;
32
+ intensityScore?: number;
25
33
  };
26
- export type TRefinedDistanceRecord = RefinedBase & {
27
- type: "distance";
34
+ export type TRefinedCardioFreeRecord = RefinedBase & {
35
+ type: "cardio-free";
28
36
  distanceKm: number;
29
- auxWeightKg?: number;
30
37
  durationSecs: number;
31
38
  avgPaceSecsPerKm?: number;
39
+ avgSpeedKmh?: number;
32
40
  };
33
- export type TRefinedRecord = TRefinedWeightRecord | TRefinedDurationRecord | TRefinedBodyWeightRecord | TRefinedDistanceRecord;
41
+ export type TRefinedRecord = TRefinedWeightRecord | TRefinedDurationRecord | TRefinedBodyWeightRecord | TRefinedCardioMachineRecord | TRefinedCardioFreeRecord;
34
42
  export declare const refineRecordEntry: (entry: TRecord) => TRefinedRecord;
35
43
  export {};
@@ -4,40 +4,48 @@ exports.refineRecordEntry = void 0;
4
4
  const isDefined_utils_1 = require("./isDefined.utils");
5
5
  const number_util_1 = require("./number.util");
6
6
  const refineRecordEntry = (entry) => {
7
- var _a, _b, _c, _d, _e, _f;
7
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
8
8
  const base = Object.assign(Object.assign(Object.assign(Object.assign({ isDone: entry.isDone, isStrictMode: entry.isStrictMode }, ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rpe)) && { rpe: (0, number_util_1.toNumber)(entry.rpe) })), ((0, isDefined_utils_1.isDefinedNumber)(entry.workDurationSecs) && {
9
9
  workDurationSecs: entry.workDurationSecs,
10
10
  })), ((0, isDefined_utils_1.isDefinedNumber)(entry.restDurationSecs) && {
11
11
  restDurationSecs: entry.restDurationSecs,
12
12
  })), ((0, isDefined_utils_1.isDefined)(entry.setNote) && { setNote: entry.setNote }));
13
13
  if (entry.type === "weight-reps") {
14
- return Object.assign(Object.assign({}, base), { type: "weight-reps", kg: (_a = (0, number_util_1.toNumber)(entry.kg)) !== null && _a !== void 0 ? _a : 0, rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
14
+ return Object.assign(Object.assign({}, base), { type: "weight-reps", kg: (_a = (0, number_util_1.toNumber)(entry.kg)) !== null && _a !== void 0 ? _a : 0, reps: (_b = (0, number_util_1.toNumber)(entry.reps)) !== null && _b !== void 0 ? _b : 0, rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
15
15
  ? (0, number_util_1.toNumber)(entry.rir)
16
- : undefined, reps: (_b = (0, number_util_1.toNumber)(entry.reps)) !== null && _b !== void 0 ? _b : 0 });
16
+ : undefined });
17
17
  }
18
18
  if (entry.type === "duration") {
19
- return Object.assign(Object.assign(Object.assign({}, base), { type: "duration", durationSecs: (_c = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _c !== void 0 ? _c : 0 }), ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.auxWeightKg)) && {
20
- auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
21
- }));
19
+ return Object.assign(Object.assign({}, base), { type: "duration", durationSecs: (_c = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _c !== void 0 ? _c : 0, auxWeightKg: (_d = (0, number_util_1.toNumber)(entry.auxWeightKg)) !== null && _d !== void 0 ? _d : 0 });
22
20
  }
23
21
  if (entry.type === "reps-only") {
24
- return Object.assign(Object.assign(Object.assign({}, base), { type: "reps-only", rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
22
+ return Object.assign(Object.assign({}, base), { type: "reps-only", reps: (_e = (0, number_util_1.toNumber)(entry.reps)) !== null && _e !== void 0 ? _e : 0, auxWeightKg: (_f = (0, number_util_1.toNumber)(entry.auxWeightKg)) !== null && _f !== void 0 ? _f : 0, rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
25
23
  ? (0, number_util_1.toNumber)(entry.rir)
26
- : undefined, reps: (_d = (0, number_util_1.toNumber)(entry.reps)) !== null && _d !== void 0 ? _d : 0 }), ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.auxWeightKg)) && {
27
- auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
28
- }));
24
+ : undefined });
29
25
  }
30
- if (entry.type === "distance") {
31
- const distanceKm = (_e = (0, number_util_1.toNumber)(entry.distanceKm)) !== null && _e !== void 0 ? _e : 0;
32
- const durationSecs = (_f = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _f !== void 0 ? _f : 0;
33
- // Calculate pace if both distance and duration are provided
26
+ if (entry.type === "cardio-machine") {
27
+ const speed = (_g = (0, number_util_1.toNumber)(entry.speed)) !== null && _g !== void 0 ? _g : 0;
28
+ const resistance = (_h = (0, number_util_1.toNumber)(entry.resistance)) !== null && _h !== void 0 ? _h : 0;
29
+ const durationSecs = (_j = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _j !== void 0 ? _j : 0;
30
+ // Calculate intensity score for progress tracking
31
+ const intensityScore = speed > 0 && resistance > 0 ? speed * resistance : undefined;
32
+ return Object.assign(Object.assign(Object.assign({}, base), { type: "cardio-machine", speed,
33
+ resistance,
34
+ durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)(intensityScore) && { intensityScore }));
35
+ }
36
+ if (entry.type === "cardio-free") {
37
+ const distanceKm = (_k = (0, number_util_1.toNumber)(entry.distanceKm)) !== null && _k !== void 0 ? _k : 0;
38
+ const durationSecs = (_l = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _l !== void 0 ? _l : 0;
39
+ // Calculate pace (seconds per km)
34
40
  const avgPaceSecsPerKm = distanceKm > 0 && durationSecs > 0
35
41
  ? durationSecs / distanceKm
36
42
  : undefined;
37
- return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { type: "distance", distanceKm,
38
- durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.auxWeightKg)) && {
39
- auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
40
- })), ((0, isDefined_utils_1.isDefinedNumber)(avgPaceSecsPerKm) && { avgPaceSecsPerKm }));
43
+ // Calculate average speed (km/h)
44
+ const avgSpeedKmh = distanceKm > 0 && durationSecs > 0
45
+ ? distanceKm / (durationSecs / 3600)
46
+ : undefined;
47
+ return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { type: "cardio-free", distanceKm,
48
+ durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)(avgPaceSecsPerKm) && { avgPaceSecsPerKm })), ((0, isDefined_utils_1.isDefinedNumber)(avgSpeedKmh) && { avgSpeedKmh }));
41
49
  }
42
50
  throw new Error(`Unknown record type: ${entry.type}`);
43
51
  };
@@ -1,5 +1,5 @@
1
1
  import { TRefinedRecord } from "./record.utils";
2
- import { TExercise, TGender, TUserFitnessGoal, TUserMetric } from "../types";
2
+ import { TExercise, TGender, TFitnessGoal, TUserMetric } from "../types";
3
3
  export type TUserProfile = {
4
4
  age?: number;
5
5
  gender?: TGender;
@@ -7,7 +7,7 @@ export type TUserProfile = {
7
7
  heightCm?: number;
8
8
  bodyFatPercentage?: number;
9
9
  fitnessLevel?: TUserMetric["fitnessLevel"];
10
- fitnessGoal?: TUserFitnessGoal;
10
+ fitnessGoal?: TFitnessGoal;
11
11
  };
12
12
  export type TScoreBreakdown = {
13
13
  baseScore: number;
@@ -1,6 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.computeScoreFromRecord = void 0;
4
+ /**
5
+ * GIST: Advanced Exercise Scoring Algorithm
6
+ *
7
+ * This function calculates a comprehensive score for a single exercise set using metabolic science,
8
+ * user personalization, and exercise characteristics. The algorithm:
9
+ *
10
+ * 🔬 SCIENTIFIC FOUNDATION:
11
+ * - Uses MET (Metabolic Equivalent) values for accurate calorie calculations
12
+ * - Incorporates BMR, exercise calories, and EPOC (post-exercise oxygen consumption)
13
+ * - Validates performance using timing guardrails and realistic human limits
14
+ *
15
+ * 👤 PERSONALIZATION:
16
+ * - Adjusts for user age, gender, weight, fitness level, and body composition
17
+ * - Provides goal-specific bonuses (strength, hypertrophy, endurance, fat burn, etc.)
18
+ * - Accounts for fitness level efficiency (trained athletes are more metabolically efficient)
19
+ *
20
+ * 🏋️ EXERCISE INTELLIGENCE:
21
+ * - Handles 5 record types: weight-reps, reps-only, duration, cardio-machine, cardio-free
22
+ * - Scales difficulty based on exercise complexity, compound movements, and muscle engagement
23
+ * - Rewards progressive overload, proper timing, and interval training
24
+ *
25
+ * 📊 SCORING SYSTEM:
26
+ * - Base score derived from calorie burn (more accurate than arbitrary load calculations)
27
+ * - Bonus system for goal alignment, high intensity, precision timing, stability demands
28
+ * - Penalty system for excessive rest, timing deviations (strict mode only)
29
+ * - Final score normalized to 0-200 range for intuitive user feedback
30
+ *
31
+ * The result motivates users through science-based, personalized feedback that encourages
32
+ * proper form, progressive overload, and goal-aligned training.
33
+ */
4
34
  /**
5
35
  * Calculate BMR using Mifflin-St Jeor Equation with body composition adjustment
6
36
  */
@@ -28,7 +58,7 @@ const calculateBMR = (userProfile) => {
28
58
  * Calculate precise MET value using exercise metabolic data
29
59
  */
30
60
  const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
31
- var _a, _b, _c;
61
+ var _a, _b;
32
62
  const { metabolicData } = exercise;
33
63
  const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
34
64
  const fitnessLevel = (_b = userProfile.fitnessLevel) !== null && _b !== void 0 ? _b : 3;
@@ -85,11 +115,27 @@ const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
85
115
  }
86
116
  break;
87
117
  }
88
- case "distance": {
89
- const distance = (_c = record.distanceKm) !== null && _c !== void 0 ? _c : 0;
118
+ case "cardio-machine": {
119
+ const speed = record.speed;
120
+ const resistance = record.resistance;
90
121
  const durationSecs = record.durationSecs;
91
- if (distance > 0 && durationSecs > 0 && metabolicData.paceFactors) {
92
- const speedKmh = (distance * 3600) / durationSecs;
122
+ // Calculate intensity from speed and resistance combination
123
+ const intensityFactor = (speed * resistance) / 100; // Normalize intensity
124
+ // Apply intensity multiplier to base MET
125
+ const intensityMultiplier = 0.8 + intensityFactor * 0.4; // 0.8 to 1.2x range
126
+ calculatedMET *= Math.min(2.0, intensityMultiplier); // Cap at 2x
127
+ // Duration bonus for sustained cardio
128
+ if (durationSecs > 600) {
129
+ // 10+ minutes
130
+ calculatedMET *= 1.1; // 10% bonus for sustained effort
131
+ }
132
+ break;
133
+ }
134
+ case "cardio-free": {
135
+ const distanceKm = record.distanceKm;
136
+ const durationSecs = record.durationSecs;
137
+ if (distanceKm > 0 && durationSecs > 0 && metabolicData.paceFactors) {
138
+ const speedKmh = (distanceKm * 3600) / durationSecs;
93
139
  // Find closest pace factor
94
140
  let closestMET = metabolicData.baseMET;
95
141
  let closestSpeed = Infinity;
@@ -183,7 +229,8 @@ const getExerciseDurationMinutes = (record, exercise) => {
183
229
  }
184
230
  break;
185
231
  }
186
- case "distance": {
232
+ case "cardio-machine":
233
+ case "cardio-free": {
187
234
  if (record.workDurationSecs && record.isStrictMode) {
188
235
  durationSecs = record.workDurationSecs;
189
236
  }
@@ -194,8 +241,10 @@ const getExerciseDurationMinutes = (record, exercise) => {
194
241
  }
195
242
  }
196
243
  }
197
- // Add rest time if available (but not for distance exercises)
198
- if (record.type !== "distance" && record.restDurationSecs) {
244
+ // Add rest time if available (but not for cardio exercises)
245
+ if (record.type !== "cardio-machine" &&
246
+ record.type !== "cardio-free" &&
247
+ record.restDurationSecs) {
199
248
  durationSecs += record.restDurationSecs;
200
249
  }
201
250
  return Math.max(0.5, durationSecs / 60);
@@ -253,15 +302,11 @@ const calculateTimingAdherence = (record, exercise, workingScore) => {
253
302
  }
254
303
  break;
255
304
  }
256
- case "distance": {
257
- if (timingGuardrails.type === "distance" &&
258
- timingGuardrails.pacing &&
259
- record.distanceKm) {
260
- expectedDuration =
261
- record.distanceKm * timingGuardrails.pacing.typicalPacePerUnit;
262
- tolerance = expectedDuration * 0.2; // 20% tolerance for distance
263
- }
264
- break;
305
+ case "cardio-machine":
306
+ case "cardio-free": {
307
+ // For cardio exercises, timing adherence is less critical
308
+ // Users can go longer or shorter based on their fitness level
309
+ return { bonus: 0, penalty: 0, reason: "cardio_flexible_timing" };
265
310
  }
266
311
  }
267
312
  if (expectedDuration === 0) {
@@ -312,7 +357,7 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
312
357
  if (typeof record.rpe === "number") {
313
358
  effortFactor = Math.max(1, Math.min(10, record.rpe));
314
359
  }
315
- else if (typeof record.rir === "number") {
360
+ else if ("rir" in record && typeof record.rir === "number") {
316
361
  effortFactor = Math.max(1, Math.min(10, 10 - record.rir));
317
362
  }
318
363
  // Calculate precise MET value using exercise metabolic data
@@ -331,18 +376,28 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
331
376
  plus.push({
332
377
  exerciseDifficulty: parseFloat((difficultyMultiplier - 1).toFixed(3)),
333
378
  });
334
- // Goal alignment bonus
379
+ // ENHANCED Goal alignment bonus
335
380
  if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.fitnessGoal) {
336
381
  let goalMultiplier = 1;
337
- switch (userProfile.fitnessGoal) {
382
+ const goal = userProfile.fitnessGoal;
383
+ switch (goal) {
338
384
  case "strength":
339
- goalMultiplier = 1 + (exercise.strengthGainLevel / 10) * 0.12;
385
+ goalMultiplier = 1 + (exercise.strengthGainLevel / 10) * 0.15; // Increased from 0.12
340
386
  break;
341
387
  case "hypertrophy":
342
- goalMultiplier = 1 + (exercise.hypertrophyLevel / 10) * 0.12;
388
+ goalMultiplier = 1 + (exercise.hypertrophyLevel / 10) * 0.15;
343
389
  break;
344
390
  case "endurance":
345
- goalMultiplier = 1 + (exercise.enduranceLevel / 10) * 0.12;
391
+ goalMultiplier = 1 + (exercise.enduranceLevel / 10) * 0.15;
392
+ break;
393
+ case "fat_burn":
394
+ // For fat burn, prioritize calorie burn level and high MET exercises
395
+ const calorieBurnBonus = (exercise.calorieBurnLevel / 10) * 0.18; // Higher bonus for calorie burn
396
+ const metBonus = exercise.metabolicData.baseMET > 6 ? 0.08 : 0; // 8% bonus for high MET exercises
397
+ goalMultiplier = 1 + calorieBurnBonus + metBonus;
398
+ break;
399
+ case "flexibility":
400
+ goalMultiplier = 1 + (exercise.flexibilityLevel / 10) * 0.15;
346
401
  break;
347
402
  case "general":
348
403
  const avgLevel = (exercise.strengthGainLevel +
@@ -350,7 +405,7 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
350
405
  exercise.enduranceLevel +
351
406
  exercise.flexibilityLevel) /
352
407
  4;
353
- goalMultiplier = 1 + (avgLevel / 10) * 0.08;
408
+ goalMultiplier = 1 + (avgLevel / 10) * 0.1;
354
409
  break;
355
410
  }
356
411
  if (goalMultiplier > 1) {
@@ -109,7 +109,7 @@ exports.calculateWorkoutSummary = calculateWorkoutSummary;
109
109
  const generateSetDescription = (record, exercise, scoreBreakdown) => {
110
110
  const effort = record.rpe
111
111
  ? `RPE ${record.rpe}`
112
- : record.rir
112
+ : "rir" in record && record.rir
113
113
  ? `${record.rir} RIR`
114
114
  : "";
115
115
  const calories = `${Math.round(scoreBreakdown.caloriesBurned)} cal`;
@@ -123,16 +123,34 @@ const generateSetDescription = (record, exercise, scoreBreakdown) => {
123
123
  const auxWeightDur = record.auxWeightKg
124
124
  ? ` (+${record.auxWeightKg}kg)`
125
125
  : "";
126
- return `${record.durationSecs}${auxWeightDur} ${effort} • ${calories}`;
127
- case "distance":
126
+ const minutes = Math.floor(record.durationSecs / 60);
127
+ const seconds = record.durationSecs % 60;
128
+ const timeStr = minutes > 0
129
+ ? `${minutes}:${seconds < 10 ? `0${seconds}` : `${seconds}`}`
130
+ : `${seconds}s`;
131
+ return `${timeStr}${auxWeightDur} ${effort} • ${calories}`;
132
+ case "cardio-machine":
133
+ const resistance = record.resistance > 0 ? ` R${record.resistance}` : "";
134
+ const minutes_cm = Math.floor(record.durationSecs / 60);
135
+ const seconds_cm = record.durationSecs % 60;
136
+ const timeStr_cm = minutes_cm > 0
137
+ ? `${minutes_cm}:${seconds_cm < 10 ? `0${seconds_cm}` : `${seconds_cm}`}`
138
+ : `${seconds_cm}s`;
139
+ return `${record.speed} speed${resistance} × ${timeStr_cm} ${effort} • ${calories}`;
140
+ case "cardio-free":
141
+ const minutes_cf = Math.floor(record.durationSecs / 60);
142
+ const seconds_cf = record.durationSecs % 60;
143
+ const timeStr_cf = minutes_cf > 0
144
+ ? `${minutes_cf}:${seconds_cf < 10 ? `0${seconds_cf}` : `${seconds_cf}`}`
145
+ : `${seconds_cf}s`;
128
146
  let pace = "";
129
- if (record.avgPaceSecsPerKm) {
130
- const minutes = Math.floor(record.avgPaceSecsPerKm / 60);
131
- const seconds = (record.avgPaceSecsPerKm % 60).toFixed(0);
132
- const secondsFormatted = seconds.length === 1 ? "0" + seconds : seconds;
133
- pace = ` (${minutes}:${secondsFormatted}/km)`;
147
+ if ("avgPaceSecsPerKm" in record && record.avgPaceSecsPerKm) {
148
+ const paceMinutes = Math.floor(record.avgPaceSecsPerKm / 60);
149
+ const paceSeconds = Math.round(record.avgPaceSecsPerKm % 60);
150
+ const paceSecondsFormatted = paceSeconds < 10 ? `0${paceSeconds}` : `${paceSeconds}`;
151
+ pace = ` (${paceMinutes}:${paceSecondsFormatted}/km)`;
134
152
  }
135
- return `${record.distanceKm}km in ${record.durationSecs}${pace} ${effort} • ${calories}`;
153
+ return `${record.distanceKm}km in ${timeStr_cf}${pace} ${effort} • ${calories}`;
136
154
  default:
137
155
  return `${effort} • ${calories}`;
138
156
  }
@@ -148,7 +166,9 @@ const getBestSetReason = (record, exercise) => {
148
166
  return "most reps";
149
167
  case "duration":
150
168
  return "longest hold";
151
- case "distance":
169
+ case "cardio-machine":
170
+ return "highest intensity";
171
+ case "cardio-free":
152
172
  return "fastest pace";
153
173
  default:
154
174
  return "highest effort";
@@ -189,13 +209,30 @@ const calculateMuscleRecovery = (muscleStressMap, exercises) => {
189
209
  recoveryHours *= 1.3; // 30% longer for moderate-high fatigue
190
210
  else if (fatigueLevel > 40)
191
211
  recoveryHours *= 1.1; // 10% longer for moderate fatigue
192
- // Find exercises that worked this muscle - using indexOf instead of includes
193
- const involvedExercises = exercises
194
- .filter(({ exercise }) => exercise.primaryMuscles.indexOf(muscle) !==
195
- -1 ||
196
- exercise.secondaryMuscles.indexOf(muscle) !==
197
- -1)
198
- .map(({ exercise }) => exercise.name);
212
+ // Find exercises that worked this muscle - using manual search instead of includes
213
+ const involvedExercises = [];
214
+ exercises.forEach(({ exercise }) => {
215
+ let found = false;
216
+ // Check primary muscles
217
+ for (let i = 0; i < exercise.primaryMuscles.length; i++) {
218
+ if (exercise.primaryMuscles[i] === muscle) {
219
+ found = true;
220
+ break;
221
+ }
222
+ }
223
+ // Check secondary muscles if not found in primary
224
+ if (!found) {
225
+ for (let i = 0; i < exercise.secondaryMuscles.length; i++) {
226
+ if (exercise.secondaryMuscles[i] === muscle) {
227
+ found = true;
228
+ break;
229
+ }
230
+ }
231
+ }
232
+ if (found) {
233
+ involvedExercises.push(exercise.name);
234
+ }
235
+ });
199
236
  recovery[muscle] = {
200
237
  fatigueLevel: Math.round(fatigueLevel),
201
238
  recoveryHours: Math.round(recoveryHours),
@@ -270,14 +307,19 @@ const getRecommendedRestDays = (fatigueLevel, muscleRecovery) => {
270
307
  extreme: 3,
271
308
  };
272
309
  const restDays = baseDays[fatigueLevel];
273
- // Consider muscle recovery times - using manual iteration instead of Object.values
310
+ // Consider muscle recovery times - using manual iteration
274
311
  const recoveryHours = [];
275
312
  for (const muscle in muscleRecovery) {
276
313
  if (muscleRecovery.hasOwnProperty(muscle)) {
277
314
  recoveryHours.push(muscleRecovery[muscle].recoveryHours);
278
315
  }
279
316
  }
280
- const maxRecoveryHours = recoveryHours.length > 0 ? Math.max.apply(Math, recoveryHours) : 0;
317
+ let maxRecoveryHours = 0;
318
+ for (let i = 0; i < recoveryHours.length; i++) {
319
+ if (recoveryHours[i] > maxRecoveryHours) {
320
+ maxRecoveryHours = recoveryHours[i];
321
+ }
322
+ }
281
323
  const muscleBasedDays = Math.ceil(maxRecoveryHours / 24);
282
324
  return Math.max(restDays, muscleBasedDays);
283
325
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.1.49",
3
+ "version": "1.1.51",
4
4
  "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",