@dgpholdings/greatoak-shared 1.1.60 → 1.1.62

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,508 +1,276 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.computeScoreFromRecord = void 0;
4
- /**
5
- * GIST: Advanced Exercise Scoring Algorithm
2
+ /*
3
+ * Exercise Scoring System Documentation
6
4
  *
7
- * This function calculates a comprehensive score for a single exercise set using metabolic science,
8
- * user personalization, and exercise characteristics. The algorithm:
5
+ * OVERVIEW:
6
+ * Evaluates workout performance (0-100) with overall score and muscle fatigue ratings.
7
+ * Designed to motivate users by quantifying effort and tracking muscle engagement.
9
8
  *
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
9
+ * SCORE COMPOSITION:
10
+ * - Volume (35%): Total work (weight×reps, duration×difficulty, distance×speed)
11
+ * - Intensity (35%): Effort level (relative weight, rep ranges, speed, RPE when provided)
12
+ * - Quality (30%): Set consistency and performance maintenance
14
13
  *
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)
14
+ * SCORE INTERPRETATION:
15
+ * 90-100: Exceptional - peak performance with high volume/intensity/consistency
16
+ * 75-89: Excellent - strong effort with good form and progression
17
+ * 60-74: Good - solid baseline performance, room for improvement
18
+ * 45-59: Moderate - acceptable but consider increasing intensity/volume
19
+ * 0-44: Light - warm-up sets or recovery day
19
20
  *
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
21
+ * MUSCLE FATIGUE (0-100):
22
+ * Primary muscles get 70% fatigue impact, secondary muscles get 30%
23
+ * 80-100: Fully fatigued - heavily worked, needs recovery
24
+ * 60-79: High fatigue - significant work, approaching limit
25
+ * 40-59: Moderate fatigue - good activation, can handle more
26
+ * 20-39: Light fatigue - warmed up, plenty capacity remaining
27
+ * 0-19: Minimal fatigue - barely engaged
24
28
  *
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
29
+ * QUALITY SCORE EXPLAINED:
30
+ * Measures how well performance is maintained across sets (starts at 85 points)
31
+ * - Performance drop <10% between sets: +5 points
32
+ * - Performance drop 10-20%: +2 points
33
+ * - Performance drop >30%: -10 points
34
+ * - Consistent RIR/RPE between sets: +3 points
35
+ * - Completing 3+ sets: +5 points, 4+ sets: +8 total points
30
36
  *
31
- * The result motivates users through science-based, personalized feedback that encourages
32
- * proper form, progressive overload, and goal-aligned training.
33
- */
34
- /**
35
- * Calculate BMR using Mifflin-St Jeor Equation with body composition adjustment
37
+ * KEY FACTORS:
38
+ * - User fitness level (higher level = higher expectations)
39
+ * - Exercise difficulty (harder exercises boost score)
40
+ * - RPE (1-10): When provided, adjusts intensity (higher RPE = higher intensity)
41
+ * - RIR (0+): When provided, checks consistency between sets
42
+ * - Fatigue multiplier (later sets get 8% bonus per set for accumulated fatigue)
36
43
  */
37
- const calculateBMR = (userProfile) => {
38
- var _a, _b, _c;
39
- const weight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
40
- const height = (_b = userProfile.heightCm) !== null && _b !== void 0 ? _b : 170;
41
- const age = (_c = userProfile.age) !== null && _c !== void 0 ? _c : 30;
42
- let bmr;
43
- if (userProfile.gender === "female") {
44
- bmr = 10 * weight + 6.25 * height - 5 * age - 161;
45
- }
46
- else {
47
- bmr = 10 * weight + 6.25 * height - 5 * age + 5;
48
- }
49
- // Adjust for body composition if available
50
- if (userProfile.bodyFatPercentage) {
51
- const leanBodyMass = weight * (1 - userProfile.bodyFatPercentage / 100);
52
- const lbmAdjustment = leanBodyMass * 0.56;
53
- bmr += lbmAdjustment;
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.calculateExerciseScore = void 0;
46
+ const time_util_1 = require("./time.util");
47
+ const calculateExerciseScore = (param) => {
48
+ const { exercise, record, user } = param;
49
+ // Filter only completed sets
50
+ const completedSets = record.filter((r) => r.isDone);
51
+ if (completedSets.length === 0) {
52
+ return { score: 0, muscleScores: {} };
54
53
  }
55
- return Math.max(1200, bmr);
54
+ // Calculate base performance metrics
55
+ const totalVolume = calculateTotalVolume(completedSets, user);
56
+ const avgIntensity = calculateAverageIntensity(completedSets, exercise, user);
57
+ const setQuality = calculateSetQuality(completedSets);
58
+ // Calculate muscle fatigue scores
59
+ const muscleScores = calculateMuscleScores(exercise, completedSets, totalVolume);
60
+ // Final score composition
61
+ const volumeComponent = normalizeVolume(totalVolume, exercise, user) * 0.35;
62
+ const intensityComponent = avgIntensity * 0.35;
63
+ const qualityComponent = setQuality * 0.3;
64
+ const score = Math.round(volumeComponent + intensityComponent + qualityComponent);
65
+ return {
66
+ score: Math.min(100, Math.max(0, score)),
67
+ muscleScores,
68
+ };
56
69
  };
57
- /**
58
- * Calculate precise MET value using exercise metabolic data
59
- */
60
- const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
61
- var _a, _b, _c;
62
- const { metabolicData } = exercise;
63
- const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
64
- const fitnessLevel = (_b = userProfile.fitnessLevel) !== null && _b !== void 0 ? _b : 3;
65
- // Fitness level efficiency: trained individuals are more efficient
66
- const efficiencyFactor = 1 - (fitnessLevel - 1) * 0.04; // 4% more efficient per level
67
- let calculatedMET = metabolicData.baseMET;
68
- // Apply compound movement multiplier
69
- calculatedMET *= metabolicData.compoundMultiplier;
70
- // Apply muscle group factor
71
- calculatedMET *= metabolicData.muscleGroupFactor / 2; // Normalize to reasonable range
72
- // Record type specific adjustments
73
- switch (record.type) {
74
- case "weight-reps": {
75
- if (metabolicData.weightFactors) {
76
- const weightRatio = record.kg / userWeight;
77
- let weightMultiplier = metabolicData.weightFactors.moderateWeight;
78
- if (weightRatio < 0.5) {
79
- weightMultiplier = metabolicData.weightFactors.lightWeight;
80
- }
81
- else if (weightRatio > 1.0) {
82
- weightMultiplier = metabolicData.weightFactors.heavyWeight;
83
- }
84
- calculatedMET *= weightMultiplier;
85
- }
86
- // Apply rep-based intensity scaling
87
- const repAdjustment = getRepIntensityAdjustment(record.reps, metabolicData.intensityScaling);
88
- calculatedMET *= repAdjustment;
89
- break;
70
+ exports.calculateExerciseScore = calculateExerciseScore;
71
+ const calculateTotalVolume = (record, user) => {
72
+ return record.reduce((total, set) => {
73
+ const weight = parseFloat(set.type === "weight-reps"
74
+ ? set.kg
75
+ : set.type === "duration" || set.type === "reps-only"
76
+ ? set.auxWeightKg
77
+ : "0") || 0;
78
+ const reps = parseFloat(set.type === "weight-reps" || set.type === "reps-only" ? set.reps : "0") || 0;
79
+ const duration = set.type === "duration" ||
80
+ set.type === "cardio-machine" ||
81
+ set.type === "cardio-free"
82
+ ? (0, time_util_1.mmssToSecs)(set.durationMmSs)
83
+ : 0;
84
+ if (set.type === "weight-reps") {
85
+ return total + reps * weight;
90
86
  }
91
- case "reps-only": {
92
- // High rep bodyweight exercises get metabolic bonus
93
- const repAdjustment = getRepIntensityAdjustment(record.reps, metabolicData.intensityScaling);
94
- calculatedMET *= repAdjustment;
95
- // Bodyweight exercises scale with user weight efficiency
96
- if (userWeight > 80) {
97
- calculatedMET *= 1.1; // Heavier people work harder
98
- }
99
- else if (userWeight < 60) {
100
- calculatedMET *= 0.95; // Lighter people have slight advantage
101
- }
102
- break;
87
+ else if (set.type === "reps-only") {
88
+ const bodyweight = user.weightKg || 70;
89
+ const effectiveWeight = weight > 0 ? weight : bodyweight * 0.3;
90
+ return total + reps * effectiveWeight;
103
91
  }
104
- case "duration": {
105
- const durationSecs = record.durationSecs;
106
- if (metabolicData.durationFactors) {
107
- let durationMultiplier = metabolicData.durationFactors.mediumDuration;
108
- if (durationSecs < 30) {
109
- durationMultiplier = metabolicData.durationFactors.shortDuration;
110
- }
111
- else if (durationSecs > 120) {
112
- durationMultiplier = metabolicData.durationFactors.longDuration;
113
- }
114
- calculatedMET *= durationMultiplier;
115
- }
116
- break;
92
+ else if (set.type === "duration") {
93
+ const weightFactor = weight > 0 ? 1 + weight / 100 : 1;
94
+ return total + duration * 10 * weightFactor;
117
95
  }
118
- case "cardio-machine": {
119
- const speed = record.speed;
120
- const resistance = (_c = record.rpe) !== null && _c !== void 0 ? _c : 1;
121
- const durationSecs = record.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;
96
+ else if (set.type === "cardio-machine") {
97
+ const avgSpeed = (parseFloat(set.speedMin) + parseFloat(set.speedMax)) / 2 || 10;
98
+ const distance = parseFloat(set.distance || "0") || (avgSpeed * duration) / 3600;
99
+ return total + distance * 1000 * (1 + avgSpeed / 20);
133
100
  }
134
- case "cardio-free": {
135
- const distance = record.distance;
136
- const durationSecs = record.durationSecs;
137
- if (distance > 0 && durationSecs > 0 && metabolicData.paceFactors) {
138
- const speedKmh = (distance * 3600) / durationSecs;
139
- // Find closest pace factor
140
- let closestMET = metabolicData.baseMET;
141
- let closestSpeed = Infinity;
142
- const paceKeys = Object.keys(metabolicData.paceFactors);
143
- for (let i = 0; i < paceKeys.length; i++) {
144
- const paceStr = paceKeys[i];
145
- const metVal = metabolicData.paceFactors[paceStr];
146
- const pace = parseFloat(paceStr);
147
- if (Math.abs(speedKmh - pace) < Math.abs(speedKmh - closestSpeed)) {
148
- closestSpeed = pace;
149
- closestMET = metVal;
150
- }
151
- }
152
- calculatedMET = closestMET;
153
- }
154
- break;
101
+ else if (set.type === "cardio-free") {
102
+ const distance = parseFloat(set.distance) || 0;
103
+ const speed = distance / (duration / 3600);
104
+ return total + distance * 1000 * (1 + speed / 20);
155
105
  }
156
- }
157
- // Apply effort factor (RPE/RIR influence on MET)
158
- const effortMultiplier = 0.7 + (effortFactor / 10) * 0.5; // 0.7 to 1.2x
159
- calculatedMET *= effortMultiplier;
160
- // Apply interval training bonus
161
- if (record.workDurationSecs && record.restDurationSecs) {
162
- const workToRestRatio = record.workDurationSecs / record.restDurationSecs;
163
- const intervalBonus = 1 + Math.min(0.3, workToRestRatio * 0.08); // Up to 30% bonus
164
- calculatedMET *= intervalBonus;
165
- }
166
- // Apply fitness level efficiency
167
- calculatedMET *= efficiencyFactor;
168
- // Ensure MET stays within exercise's defined range
169
- const [minMET, maxMET] = metabolicData.metRange;
170
- calculatedMET = Math.max(minMET, Math.min(maxMET, calculatedMET));
171
- return parseFloat(calculatedMET.toFixed(1));
106
+ return total;
107
+ }, 0);
172
108
  };
173
- /**
174
- * Get rep-based intensity adjustment
175
- */
176
- const getRepIntensityAdjustment = (reps, scalingType) => {
177
- const baseReps = 8; // Reference point
178
- const repDiff = reps - baseReps;
179
- switch (scalingType) {
180
- case "linear":
181
- return Math.max(0.7, 1 + repDiff * 0.02); // 2% per rep difference
182
- case "exponential":
183
- return Math.max(0.7, Math.pow(1.04, repDiff)); // Exponential growth
184
- case "plateau":
185
- // Plateaus at high reps but grows quickly initially
186
- if (reps > 20)
187
- return 1.3;
188
- return Math.max(0.7, 1 + repDiff * 0.04);
189
- default:
190
- return 1.0;
191
- }
192
- };
193
- /**
194
- * Calculate exercise duration in minutes using timing guardrails
195
- */
196
- const getExerciseDurationMinutes = (record, exercise) => {
197
- let durationSecs = 0;
198
- const timingGuardrails = exercise.timingGuardrails;
199
- // Use actual recorded work duration if available and in strict mode
200
- if (record.workDurationSecs && record.isStrictMode) {
201
- durationSecs = record.workDurationSecs;
202
- }
203
- else {
204
- // Calculate expected duration using timing guardrails
205
- switch (record.type) {
206
- case "weight-reps":
207
- case "reps-only": {
208
- if (timingGuardrails &&
209
- (timingGuardrails.type === "weight-reps" ||
210
- timingGuardrails.type === "reps-only") &&
211
- timingGuardrails.singleRep) {
212
- const repTime = timingGuardrails.singleRep.typical;
213
- const setupTime = timingGuardrails.setupTypicalSecs || 0;
214
- durationSecs = record.reps * repTime + setupTime;
215
- }
216
- else {
217
- // Fallback to difficulty-based estimation
218
- const baseTimePerRep = exercise.difficultyLevel > 7 ? 3.5 : 2.5;
219
- durationSecs = record.reps * baseTimePerRep;
109
+ const calculateAverageIntensity = (record, exercise, user) => {
110
+ const intensities = record.map((set, index) => {
111
+ let baseIntensity = 0;
112
+ if (set.type === "weight-reps") {
113
+ const weight = parseFloat(set.kg) || 0;
114
+ const reps = parseFloat(set.reps) || 0;
115
+ const userWeight = user.weightKg || 70;
116
+ const relativeLoad = weight / userWeight;
117
+ const repIntensity = getRepIntensity(reps);
118
+ baseIntensity = (relativeLoad * 40 + repIntensity * 60) / 100;
119
+ // RPE adjustment (only if provided and valid)
120
+ if (set.rpe && set.rpe !== "0") {
121
+ const rpe = parseFloat(set.rpe);
122
+ if (rpe > 0 && rpe <= 10) {
123
+ baseIntensity *= 0.5 + rpe / 20; // Scale from 0.55x to 1.0x
220
124
  }
221
- break;
222
125
  }
223
- case "duration": {
224
- if (record.workDurationSecs && record.isStrictMode) {
225
- durationSecs = record.workDurationSecs;
226
- }
227
- else {
228
- durationSecs = record.durationSecs;
229
- }
230
- break;
126
+ }
127
+ else if (set.type === "reps-only") {
128
+ const reps = parseFloat(set.reps) || 0;
129
+ baseIntensity = getRepIntensity(reps) / 100;
130
+ if (set.workDurationSecs) {
131
+ const tempo = (reps * 3) / set.workDurationSecs;
132
+ baseIntensity *= Math.min(1.2, Math.max(0.8, tempo));
231
133
  }
232
- case "cardio-machine":
233
- case "cardio-free": {
234
- if (record.workDurationSecs && record.isStrictMode) {
235
- durationSecs = record.workDurationSecs;
236
- }
237
- else {
238
- durationSecs = record.durationSecs;
134
+ // RPE adjustment for bodyweight exercises
135
+ if (set.rpe && set.rpe !== "0") {
136
+ const rpe = parseFloat(set.rpe);
137
+ if (rpe > 0 && rpe <= 10) {
138
+ baseIntensity *= 0.5 + rpe / 20;
239
139
  }
240
- break;
241
140
  }
242
141
  }
243
- }
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) {
248
- durationSecs += record.restDurationSecs;
249
- }
250
- return Math.max(0.5, durationSecs / 60);
251
- };
252
- /**
253
- * Calculate comprehensive calorie burn including EPOC
254
- */
255
- const calculateCalorieBurn = (record, exercise, metValue, durationMinutes, userProfile) => {
256
- var _a;
257
- const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
258
- const bmr = calculateBMR(userProfile);
259
- const bmrPerMinute = bmr / (24 * 60);
260
- // Exercise calories: MET × weight(kg) × duration(hours)
261
- const exerciseCalories = metValue * userWeight * (durationMinutes / 60);
262
- // EPOC (Excess Post-Exercise Oxygen Consumption) calories
263
- const epocCalories = exerciseCalories * exercise.metabolicData.epocFactor;
264
- // BMR calories during exercise time
265
- const bmrCaloriesDuringExercise = bmrPerMinute * durationMinutes;
266
- // Total calories = exercise + EPOC + BMR during exercise
267
- const totalCalories = exerciseCalories + epocCalories + bmrCaloriesDuringExercise;
268
- return {
269
- exerciseCalories: parseFloat(exerciseCalories.toFixed(1)),
270
- epocCalories: parseFloat(epocCalories.toFixed(1)),
271
- totalCalories: parseFloat(totalCalories.toFixed(1)),
272
- };
273
- };
274
- /**
275
- * Calculate timing adherence bonus/penalty using timing guardrails
276
- */
277
- const calculateTimingAdherence = (record, exercise, workingScore) => {
278
- const timingGuardrails = exercise.timingGuardrails;
279
- if (!timingGuardrails || !record.workDurationSecs) {
280
- return { bonus: 0, penalty: 0, reason: "no_timing_data" };
281
- }
282
- let expectedDuration = 0;
283
- let tolerance = 0;
284
- switch (record.type) {
285
- case "weight-reps":
286
- case "reps-only": {
287
- if ((timingGuardrails.type === "weight-reps" ||
288
- timingGuardrails.type === "reps-only") &&
289
- timingGuardrails.singleRep) {
290
- const repTime = timingGuardrails.singleRep.typical;
291
- const setupTime = timingGuardrails.setupTypicalSecs || 0;
292
- expectedDuration = record.reps * repTime + setupTime;
293
- tolerance = expectedDuration * 0.25; // 25% tolerance
142
+ else if (set.type === "duration") {
143
+ const duration = (0, time_util_1.mmssToSecs)(set.durationMmSs);
144
+ const weight = parseFloat(set.auxWeightKg) || 0;
145
+ baseIntensity = Math.min(100, duration * 1.5) / 100;
146
+ if (weight > 0) {
147
+ baseIntensity *= 1 + weight / 100;
294
148
  }
295
- break;
296
149
  }
297
- case "duration": {
298
- if (timingGuardrails.type === "duration" &&
299
- timingGuardrails.setDuration) {
300
- expectedDuration = record.durationSecs;
301
- tolerance = timingGuardrails.setDuration.typical * 0.15; // 15% tolerance for duration exercises
150
+ else if (set.type === "cardio-machine" || set.type === "cardio-free") {
151
+ const duration = (0, time_util_1.mmssToSecs)(set.durationMmSs);
152
+ if (set.type === "cardio-machine") {
153
+ const avgSpeed = (parseFloat(set.speedMin) + parseFloat(set.speedMax)) / 2 || 0;
154
+ baseIntensity = Math.min(100, avgSpeed * 5.5) / 100;
155
+ }
156
+ else {
157
+ const distance = parseFloat(set.distance) || 0;
158
+ const speedKmh = duration > 0 ? distance / (duration / 3600) : 0;
159
+ baseIntensity = Math.min(100, speedKmh * 5.5) / 100;
302
160
  }
303
- break;
304
- }
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" };
310
161
  }
311
- }
312
- if (expectedDuration === 0) {
313
- return { bonus: 0, penalty: 0, reason: "no_expected_duration" };
314
- }
315
- const actualDuration = record.workDurationSecs;
316
- const deviation = Math.abs(actualDuration - expectedDuration);
317
- // Strict mode gets bonus for precision
318
- if (record.isStrictMode && deviation <= tolerance) {
319
- const precisionBonus = workingScore * 0.05; // 5% bonus for precise timing
320
- return {
321
- bonus: precisionBonus,
322
- penalty: 0,
323
- reason: "strict_mode_precision",
324
- };
325
- }
326
- // Penalty for significant deviation (only in strict mode)
327
- if (record.isStrictMode && deviation > tolerance) {
328
- const deviationPercent = (deviation - tolerance) / expectedDuration;
329
- const timingPenalty = Math.min(workingScore * 0.08, // Max 8% penalty
330
- deviationPercent * workingScore * 0.03);
331
- return { bonus: 0, penalty: timingPenalty, reason: "timing_deviation" };
332
- }
333
- return { bonus: 0, penalty: 0, reason: "relaxed_mode" };
334
- };
335
- /**
336
- * Enhanced training stress score computation using metabolic data and timing guardrails
337
- * This is for scoring a SINGLE record of an exercise. Ref: TRecord, TRefinedRecord.
338
- * To calculate complete scoring/summary of workout use: calculateWorkoutSummary(...) function
339
- */
340
- const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userProfile, isTimeIntervalModeEnabled = false, }) => {
341
- var _a;
342
- if (!record.isDone) {
343
- return {
344
- baseScore: 0,
345
- plus: [],
346
- minus: [],
347
- finalScore: 0,
348
- caloriesBurned: 0,
349
- metValue: 0,
350
- epocCalories: 0,
351
- };
352
- }
353
- const plus = [];
354
- const minus = [];
355
- // Calculate effort factor from RPE/RIR
356
- let effortFactor = 5; // Default moderate effort
357
- if (typeof record.rpe === "number") {
358
- effortFactor = Math.max(1, Math.min(10, record.rpe));
359
- }
360
- else if ("rir" in record && typeof record.rir === "number") {
361
- effortFactor = Math.max(1, Math.min(10, 10 - record.rir));
362
- }
363
- // Calculate precise MET value using exercise metabolic data
364
- const metValue = calculateMETValue(record, exercise, effortFactor, userProfile || {});
365
- // Calculate exercise duration using timing guardrails
366
- const durationMinutes = getExerciseDurationMinutes(record, exercise);
367
- // Calculate comprehensive calorie burn
368
- const calorieData = calculateCalorieBurn(record, exercise, metValue, durationMinutes, userProfile || {});
369
- // Base score from calorie burn (more accurate than arbitrary load calculations)
370
- const calorieBasedLoad = calorieData.exerciseCalories + calorieData.epocCalories * 2; // EPOC counts double
371
- const base = Math.log10(1 + Math.max(5, calorieBasedLoad));
372
- let workingScore = base;
373
- // Exercise difficulty bonus (from database)
374
- const difficultyMultiplier = 0.8 + (exercise.difficultyLevel / 10) * 0.4; // 0.8 to 1.2x
375
- workingScore *= difficultyMultiplier;
376
- plus.push({
377
- exerciseDifficulty: parseFloat((difficultyMultiplier - 1).toFixed(3)),
162
+ // Apply fatigue factor for later sets (8% bonus per set)
163
+ const fatigueMultiplier = 1 + index * 0.08;
164
+ return Math.min(100, baseIntensity * fatigueMultiplier * 100);
378
165
  });
379
- // ENHANCED Goal alignment bonus
380
- if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.fitnessGoal) {
381
- let goalMultiplier = 1;
382
- const goal = userProfile.fitnessGoal;
383
- switch (goal) {
384
- case "strength":
385
- goalMultiplier = 1 + (exercise.strengthGainLevel / 10) * 0.15; // Increased from 0.12
386
- break;
387
- case "hypertrophy":
388
- goalMultiplier = 1 + (exercise.hypertrophyLevel / 10) * 0.15;
389
- break;
390
- case "endurance":
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;
401
- break;
402
- case "general":
403
- const avgLevel = (exercise.strengthGainLevel +
404
- exercise.hypertrophyLevel +
405
- exercise.enduranceLevel +
406
- exercise.flexibilityLevel) /
407
- 4;
408
- goalMultiplier = 1 + (avgLevel / 10) * 0.1;
409
- break;
166
+ const avgIntensity = intensities.reduce((a, b) => a + b, 0) / intensities.length;
167
+ const difficultyBonus = exercise.difficultyLevel * 5;
168
+ return Math.min(100, avgIntensity + difficultyBonus);
169
+ };
170
+ const calculateSetQuality = (record) => {
171
+ if (record.length === 0)
172
+ return 0;
173
+ if (record.length === 1)
174
+ return 75;
175
+ let qualityScore = 85; // Base score
176
+ // Check performance drop-off between sets
177
+ for (let i = 1; i < record.length; i++) {
178
+ const curr = record[i];
179
+ const prev = record[i - 1];
180
+ if (curr.type === "weight-reps" && prev.type === "weight-reps") {
181
+ const currWork = parseFloat(curr.reps) * parseFloat(curr.kg);
182
+ const prevWork = parseFloat(prev.reps) * parseFloat(prev.kg);
183
+ const dropOff = prevWork > 0 ? 1 - currWork / prevWork : 0;
184
+ if (dropOff < 0.1)
185
+ qualityScore += 5; // Excellent maintenance
186
+ else if (dropOff < 0.2)
187
+ qualityScore += 2; // Good maintenance
188
+ else if (dropOff > 0.3)
189
+ qualityScore -= 10; // Poor pacing
410
190
  }
411
- if (goalMultiplier > 1) {
412
- workingScore *= goalMultiplier;
413
- plus.push({ goalAlignment: parseFloat((goalMultiplier - 1).toFixed(3)) });
191
+ else if (curr.type === "reps-only" && prev.type === "reps-only") {
192
+ const currReps = parseFloat(curr.reps);
193
+ const prevReps = parseFloat(prev.reps);
194
+ const dropOff = prevReps > 0 ? 1 - currReps / prevReps : 0;
195
+ if (dropOff < 0.15)
196
+ qualityScore += 5;
197
+ else if (dropOff > 0.3)
198
+ qualityScore -= 8;
414
199
  }
415
- }
416
- // Stability and complexity bonus
417
- const stabilityBonus = 1 + (exercise.stabilityLevel / 10) * 0.06;
418
- workingScore *= stabilityBonus;
419
- plus.push({ stabilityDemand: parseFloat((stabilityBonus - 1).toFixed(3)) });
420
- // High calorie burn efficiency bonus
421
- if (calorieData.exerciseCalories > 50) {
422
- // High calorie burn exercises
423
- const efficiencyBonus = 1.05;
424
- workingScore *= efficiencyBonus;
425
- plus.push({
426
- highCalorieBurn: parseFloat((efficiencyBonus - 1).toFixed(3)),
427
- });
428
- }
429
- // Interval training bonus (already calculated in MET, but add scoring bonus)
430
- if (isTimeIntervalModeEnabled) {
431
- const intervalScoreBonus = 1.08;
432
- workingScore *= intervalScoreBonus;
433
- plus.push({
434
- intervalTraining: parseFloat((intervalScoreBonus - 1).toFixed(3)),
435
- });
436
- }
437
- // Timing adherence bonus/penalty using guardrails
438
- const timingAdherence = calculateTimingAdherence(record, exercise, workingScore);
439
- if (timingAdherence.bonus > 0) {
440
- workingScore += timingAdherence.bonus;
441
- plus.push({
442
- timingPrecision: parseFloat(timingAdherence.bonus.toFixed(3)),
443
- });
444
- }
445
- if (timingAdherence.penalty > 0) {
446
- workingScore -= timingAdherence.penalty;
447
- minus.push({
448
- timingDeviation: parseFloat(timingAdherence.penalty.toFixed(3)),
449
- });
450
- }
451
- // User profile adjustments
452
- let profileMultiplier = 1;
453
- if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female") {
454
- profileMultiplier *= 1.08; // Reduced from 1.12 since metabolic calculations are more accurate
455
- }
456
- if (userProfile === null || userProfile === void 0 ? void 0 : userProfile.age) {
457
- if (userProfile.age > 50)
458
- profileMultiplier *= 1.06;
459
- if (userProfile.age > 65)
460
- profileMultiplier *= 1.1;
461
- }
462
- if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.weightKg) && userProfile.weightKg > 0) {
463
- const weightAdjustment = 70 / userProfile.weightKg;
464
- profileMultiplier *= Math.max(0.85, Math.min(1.25, weightAdjustment));
465
- }
466
- if (profileMultiplier !== 1) {
467
- workingScore *= profileMultiplier;
468
- plus.push({
469
- profileAdjustment: parseFloat((profileMultiplier - 1).toFixed(3)),
470
- });
471
- }
472
- // Enhanced rest efficiency penalty using timing guardrails
473
- if (typeof record.restDurationSecs === "number" &&
474
- typeof avgRestDurationSecs === "number" &&
475
- ((_a = exercise.timingGuardrails) === null || _a === void 0 ? void 0 : _a.restPeriods)) {
476
- const restUsed = record.restDurationSecs;
477
- const restGuardrails = exercise.timingGuardrails.restPeriods;
478
- const optimalRest = restGuardrails.typical;
479
- const maxAcceptableRest = restGuardrails.maximum;
480
- // Calculate stress-based rest bonus from guardrails
481
- const stressBonus = exercise.timingGuardrails.stressRestBonus || 0;
482
- const adjustedOptimalRest = optimalRest + (effortFactor > 7 ? stressBonus : 0);
483
- if (restUsed > adjustedOptimalRest) {
484
- const excessRest = restUsed - adjustedOptimalRest;
485
- const maxExcess = maxAcceptableRest - adjustedOptimalRest;
486
- if (excessRest > 0) {
487
- const penaltyRatio = Math.min(1, excessRest / maxExcess);
488
- const restPenalty = penaltyRatio * 0.1 * workingScore; // Up to 10% penalty
489
- workingScore -= restPenalty;
490
- minus.push({ excessRestPenalty: parseFloat(restPenalty.toFixed(3)) });
200
+ // Check effort consistency using either RIR or RPE (whichever is provided)
201
+ // RIR consistency check
202
+ if ("rir" in curr &&
203
+ "rir" in prev &&
204
+ curr.rir &&
205
+ prev.rir &&
206
+ curr.rir !== "0" &&
207
+ prev.rir !== "0") {
208
+ const currRir = parseFloat(curr.rir);
209
+ const prevRir = parseFloat(prev.rir);
210
+ if (!isNaN(currRir) && !isNaN(prevRir)) {
211
+ const rirDiff = Math.abs(currRir - prevRir);
212
+ if (rirDiff <= 1)
213
+ qualityScore += 3; // Consistent effort
214
+ }
215
+ }
216
+ // RPE consistency check (alternative to RIR)
217
+ else if (curr.rpe && prev.rpe && curr.rpe !== "0" && prev.rpe !== "0") {
218
+ const currRpe = parseFloat(curr.rpe);
219
+ const prevRpe = parseFloat(prev.rpe);
220
+ if (!isNaN(currRpe) && !isNaN(prevRpe)) {
221
+ const rpeDiff = Math.abs(currRpe - prevRpe);
222
+ if (rpeDiff <= 1)
223
+ qualityScore += 3; // Consistent effort
491
224
  }
492
225
  }
493
226
  }
494
- // Final score normalization
495
- let finalScore = Math.max(0, workingScore);
496
- finalScore *= 12; // Scale to intuitive range (0-120+ for typical exercises)
497
- finalScore = Math.min(200, finalScore); // Cap extreme scores
498
- return {
499
- baseScore: parseFloat(base.toFixed(2)),
500
- plus,
501
- minus,
502
- finalScore: parseFloat(finalScore.toFixed(1)),
503
- caloriesBurned: calorieData.totalCalories,
504
- metValue,
505
- epocCalories: calorieData.epocCalories,
506
- };
227
+ // Bonus for multiple quality sets
228
+ if (record.length >= 3)
229
+ qualityScore += 5;
230
+ if (record.length >= 4)
231
+ qualityScore += 3;
232
+ return Math.min(100, Math.max(0, qualityScore));
233
+ };
234
+ const calculateMuscleScores = (exercise, record, totalVolume) => {
235
+ const muscleScores = {};
236
+ const volumePerSet = totalVolume / record.length;
237
+ // Primary muscles take 70% of the fatigue
238
+ exercise.primaryMuscles.forEach((muscle) => {
239
+ const baseFatigue = Math.min(100, (volumePerSet / 500) * 70);
240
+ const setMultiplier = Math.min(1.5, 1 + (record.length - 1) * 0.15);
241
+ muscleScores[muscle] = Math.min(100, baseFatigue * setMultiplier);
242
+ });
243
+ // Secondary muscles take 30% of the fatigue
244
+ exercise.secondaryMuscles.forEach((muscle) => {
245
+ const baseFatigue = Math.min(100, (volumePerSet / 500) * 30);
246
+ const setMultiplier = Math.min(1.3, 1 + (record.length - 1) * 0.1);
247
+ muscleScores[muscle] = Math.min(100, baseFatigue * setMultiplier);
248
+ });
249
+ return muscleScores;
250
+ };
251
+ const normalizeVolume = (totalVolume, exercise, user) => {
252
+ const userWeight = user.weightKg || 70;
253
+ const fitnessLevel = user.fitnessLevel || 2;
254
+ // Expected volume baseline
255
+ const baseExpected = userWeight * 15 * fitnessLevel;
256
+ const difficultyAdjustment = 1 + exercise.difficultyLevel / 8;
257
+ const expectedVolume = baseExpected * difficultyAdjustment;
258
+ // Sigmoid normalization for smooth scoring
259
+ const ratio = totalVolume / expectedVolume;
260
+ return 100 / (1 + Math.exp(-3 * (ratio - 0.8)));
261
+ };
262
+ const getRepIntensity = (reps) => {
263
+ if (reps <= 3)
264
+ return 95;
265
+ if (reps <= 5)
266
+ return 87;
267
+ if (reps <= 8)
268
+ return 80;
269
+ if (reps <= 12)
270
+ return 70;
271
+ if (reps <= 15)
272
+ return 65;
273
+ if (reps <= 20)
274
+ return 60;
275
+ return 50;
507
276
  };
508
- exports.computeScoreFromRecord = computeScoreFromRecord;