@dgpholdings/greatoak-shared 1.2.54 → 1.2.55

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.
Files changed (41) hide show
  1. package/dist/__mocks__/exercises.mock.d.ts +35 -0
  2. package/dist/__mocks__/exercises.mock.js +144 -0
  3. package/dist/__mocks__/templateExercises.mock.d.ts +90 -0
  4. package/dist/__mocks__/templateExercises.mock.js +258 -0
  5. package/dist/__mocks__/user.mock.d.ts +2 -0
  6. package/dist/__mocks__/user.mock.js +36 -0
  7. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +30 -0
  8. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +138 -0
  9. package/dist/utils/exerciseRecord/recordValidator.d.ts +12 -0
  10. package/dist/utils/exerciseRecord/recordValidator.integration.test.d.ts +1 -0
  11. package/dist/utils/exerciseRecord/recordValidator.integration.test.js +51 -0
  12. package/dist/utils/exerciseRecord/recordValidator.js +85 -0
  13. package/dist/utils/exerciseRecord/recordValidator.test.d.ts +1 -0
  14. package/dist/utils/exerciseRecord/recordValidator.test.js +165 -0
  15. package/dist/utils/exerciseRecord/workoutMath.d.ts +28 -0
  16. package/dist/utils/exerciseRecord/workoutMath.js +116 -0
  17. package/dist/utils/exerciseRecord/workoutMath.test.d.ts +1 -0
  18. package/dist/utils/exerciseRecord/workoutMath.test.js +238 -0
  19. package/dist/utils/index.d.ts +3 -1
  20. package/dist/utils/index.js +5 -3
  21. package/dist/utils/scoringWorkout/calculateCalories.d.ts +67 -0
  22. package/dist/utils/scoringWorkout/calculateCalories.js +351 -0
  23. package/dist/utils/scoringWorkout/calculateMuscleFatiue.d.ts +67 -0
  24. package/dist/utils/scoringWorkout/calculateMuscleFatiue.js +330 -0
  25. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +73 -0
  26. package/dist/utils/scoringWorkout/calculateQualityScore.js +357 -0
  27. package/dist/utils/scoringWorkout/calculateTotalVolume.d.ts +15 -0
  28. package/dist/utils/scoringWorkout/calculateTotalVolume.js +73 -0
  29. package/dist/utils/scoringWorkout/constants.d.ts +211 -0
  30. package/dist/utils/scoringWorkout/constants.js +247 -0
  31. package/dist/utils/scoringWorkout/helpers.d.ts +127 -0
  32. package/dist/utils/scoringWorkout/helpers.js +245 -0
  33. package/dist/utils/scoringWorkout/index.d.ts +27 -0
  34. package/dist/utils/scoringWorkout/index.js +56 -0
  35. package/dist/utils/scoringWorkout/parseRecords.d.ts +68 -0
  36. package/dist/utils/scoringWorkout/parseRecords.js +281 -0
  37. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.d.ts +1 -0
  38. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +439 -0
  39. package/dist/utils/scoringWorkout/types.d.ts +104 -0
  40. package/dist/utils/scoringWorkout/types.js +11 -0
  41. package/package.json +6 -3
@@ -0,0 +1,351 @@
1
+ "use strict";
2
+ // calculateCalories.ts
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX EXERCISE SCORING SYSTEM — Pillar 1: Calorie Burn
6
+ * ============================================================================
7
+ *
8
+ * Estimates energy expenditure (kcal) for an exercise using the MET system.
9
+ *
10
+ * Formula (per set):
11
+ * calories = effectiveMET × userWeightKg × (activeDurationSecs / 3600)
12
+ *
13
+ * Where effectiveMET is the exercise's baseMET adjusted for:
14
+ * - Effort level (RPE/RIR → scales within metRange)
15
+ * - Weight intensity (for weight-reps: how heavy relative to bodyweight)
16
+ * - Duration category (for duration: short/medium/long multiplier)
17
+ * - Speed (for cardio: interpolated from pace or speed range)
18
+ * - Compound multiplier (multi-joint exercises burn more)
19
+ *
20
+ * After summing all sets:
21
+ * - Rest calories are added (elevated MET during rest periods)
22
+ * - EPOC (afterburn) is added as a percentage of work calories
23
+ *
24
+ * References:
25
+ * - Ainsworth BE et al. "Compendium of Physical Activities" (2011)
26
+ * - Katch, McArdle & Katch, "Exercise Physiology" (8th ed.)
27
+ */
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.calculateCalories = calculateCalories;
30
+ const constants_1 = require("./constants");
31
+ const helpers_1 = require("./helpers");
32
+ // ---------------------------------------------------------------------------
33
+ // Main Calorie Calculation
34
+ // ---------------------------------------------------------------------------
35
+ /**
36
+ * Calculate total calorie burn for an exercise.
37
+ *
38
+ * @param sets Parsed & validated sets (from parseRecords)
39
+ * @param metabolicData Exercise's metabolic configuration
40
+ * @param user Validated user context
41
+ * @param difficultyLevel Exercise difficulty (0–4), used for bodyweight estimation
42
+ * @returns Total calories burned (kcal), rounded to 1 decimal
43
+ *
44
+ * @example
45
+ * const calories = calculateCalories(parsedSets, exercise.metabolicData, userCtx, exercise.difficultyLevel);
46
+ * // → 6.4 (for 3 light bench press sets)
47
+ * // → 142.3 (for a 20-min treadmill run)
48
+ */
49
+ function calculateCalories(sets, metabolicData, user, difficultyLevel, scoringSpecialHandling) {
50
+ if (sets.length === 0)
51
+ return 0;
52
+ // P2-2: Metabolic Weight Calculation
53
+ // Adipose tissue is metabolically inert during exercise compared to muscle.
54
+ // Using lean mass provides a much more accurate scale for energy expenditure.
55
+ // Computed before any early return so all paths (including stretch-mobility) use lean mass.
56
+ const leanMassFraction = 1 - (user.bodyFatPercentage / 100);
57
+ const metabolicWeight = user.weightKg * leanMassFraction;
58
+ // Stretch/mobility: no meaningful work calories — only passive rest MET applies
59
+ if (scoringSpecialHandling === "stretch-mobility") {
60
+ const totalRestSecs = sets.reduce((sum, s) => { var _a; return sum + ((_a = s.restDurationSecs) !== null && _a !== void 0 ? _a : 0); }, 0);
61
+ return Math.round(constants_1.REST_MET * metabolicWeight * (totalRestSecs / 3600) * 10) / 10;
62
+ }
63
+ // Normalize metRange so [0] = min, [1] = max (sample data has them reversed)
64
+ const [metMin, metMax] = normalizeMetRange(metabolicData.metRange, metabolicData.baseMET);
65
+ let totalWorkCalories = 0;
66
+ let totalRestCalories = 0;
67
+ for (const set of sets) {
68
+ // 1. Calculate effective MET for this set
69
+ const effectiveMET = calculateEffectiveMET(set, metabolicData, metMin, metMax, user, difficultyLevel, scoringSpecialHandling);
70
+ // 2. Work calories: MET × weight × time
71
+ const workHours = set.activeDurationSecs / 3600;
72
+ totalWorkCalories += effectiveMET * metabolicWeight * workHours;
73
+ // 3. Rest calories: elevated resting MET during recovery
74
+ if (set.restDurationSecs !== null && set.restDurationSecs > 0) {
75
+ const restHours = set.restDurationSecs / 3600;
76
+ totalRestCalories += constants_1.REST_MET * metabolicWeight * restHours;
77
+ }
78
+ }
79
+ // 4. EPOC (Excess Post-exercise Oxygen Consumption) — afterburn effect
80
+ const epocCalories = totalWorkCalories * metabolicData.epocFactor;
81
+ // 5. Total = work + rest + afterburn
82
+ const total = totalWorkCalories + totalRestCalories + epocCalories;
83
+ return Math.round(total * 10) / 10; // Round to 1 decimal place
84
+ }
85
+ // ---------------------------------------------------------------------------
86
+ // Effective MET Calculation (per set)
87
+ // ---------------------------------------------------------------------------
88
+ /**
89
+ * Calculate the effective MET for a single set based on its type and intensity.
90
+ *
91
+ * The general approach:
92
+ * 1. Interpolate base MET within [metMin, metMax] using effort fraction
93
+ * 2. Apply type-specific intensity multiplier
94
+ * 3. Apply compound multiplier
95
+ *
96
+ * The effort fraction (from RPE/RIR) determines WHERE in the MET range
97
+ * this set falls. Low effort → closer to metMin, high effort → closer to metMax.
98
+ */
99
+ function calculateEffectiveMET(set, metabolicData, metMin, metMax, user, difficultyLevel, scoringSpecialHandling) {
100
+ var _a, _b, _c, _d, _e;
101
+ // Start with effort-scaled MET
102
+ // effortFraction is 0.5–1.3 (from helpers), we normalize to 0–1 for interpolation
103
+ const effortNormalized = (0, helpers_1.clamp)((set.effortFraction - 0.5) / 0.8, 0, 1);
104
+ let met = metMin + (metMax - metMin) * effortNormalized;
105
+ // Continuous-duration (battle ropes, high knees, jump rope): these behave like
106
+ // cardio metabolically — bypass the static-hold duration formula entirely
107
+ if (scoringSpecialHandling === "continuous-duration") {
108
+ met = metMin + (metMax - metMin) * effortNormalized;
109
+ met *= metabolicData.compoundMultiplier;
110
+ return (0, helpers_1.clamp)(met, 1, 25);
111
+ }
112
+ // Apply type-specific intensity scaling
113
+ switch (set.type) {
114
+ case "weight-reps":
115
+ met *= getWeightMultiplier((_a = set.kg) !== null && _a !== void 0 ? _a : 0, user.weightKg, metabolicData);
116
+ break;
117
+ case "reps-only":
118
+ met *= getRepsOnlyMultiplier((_b = set.auxWeightKg) !== null && _b !== void 0 ? _b : 0, user.weightKg, difficultyLevel);
119
+ break;
120
+ case "duration":
121
+ met *= getDurationMultiplier((_c = set.durationSecs) !== null && _c !== void 0 ? _c : 0, (_d = set.auxWeightKg) !== null && _d !== void 0 ? _d : 0, user.weightKg, metabolicData);
122
+ break;
123
+ case "cardio-machine":
124
+ met = getCardioMachineMET(set, metabolicData, metMin, metMax, effortNormalized);
125
+ break;
126
+ case "cardio-free":
127
+ met = getCardioFreeMET(set, metabolicData, metMin, metMax, effortNormalized);
128
+ // Loaded carry: scale MET up by the weight being carried relative to bodyweight
129
+ if (scoringSpecialHandling === "loaded-carry") {
130
+ const auxWeightKg = (_e = set.auxWeightKg) !== null && _e !== void 0 ? _e : 0;
131
+ if (auxWeightKg > 0) {
132
+ met *= 1 + (auxWeightKg / user.weightKg) * constants_1.LOADED_CARRY_WEIGHT_MET_SCALE;
133
+ }
134
+ }
135
+ break;
136
+ }
137
+ // Apply compound multiplier (multi-joint exercises have higher energy cost)
138
+ met *= metabolicData.compoundMultiplier;
139
+ // Safety clamp: MET should never go below 1 or above 25
140
+ return (0, helpers_1.clamp)(met, 1, 25);
141
+ }
142
+ // ---------------------------------------------------------------------------
143
+ // Type-Specific Multipliers
144
+ // ---------------------------------------------------------------------------
145
+ /**
146
+ * Weight multiplier for weight-reps exercises.
147
+ *
148
+ * Classifies the weight as light/moderate/heavy relative to bodyweight
149
+ * and returns the corresponding multiplier.
150
+ *
151
+ * Example: 10kg at 75kg bodyweight → ratio 0.13 → "light" → 0.8 multiplier
152
+ */
153
+ function getWeightMultiplier(kg, userWeightKg, metabolicData) {
154
+ const ratio = kg / userWeightKg;
155
+ // Resolve weight factors (handle both nested and flat shapes)
156
+ const factors = resolveWeightFactors(metabolicData);
157
+ if (ratio < constants_1.WEIGHT_CATEGORY_THRESHOLDS.lightMax) {
158
+ return factors.light;
159
+ }
160
+ else if (ratio < constants_1.WEIGHT_CATEGORY_THRESHOLDS.moderateMax) {
161
+ return factors.moderate;
162
+ }
163
+ else {
164
+ return factors.heavy;
165
+ }
166
+ }
167
+ /**
168
+ * Multiplier for reps-only (bodyweight) exercises.
169
+ *
170
+ * The "load" is the user's bodyweight (scaled by difficulty) + any aux weight.
171
+ * We return a gentle multiplier that boosts MET slightly for added weight.
172
+ *
173
+ * Example: Push-up (difficulty 1) with 2.5kg vest at 75kg bodyweight
174
+ * bodyweightLoad = (1/4) × 0.65 × 75 = 12.19 kg equivalent
175
+ * totalLoad = 12.19 + 2.5 = 14.69
176
+ * ratio = 14.69 / 75 = 0.196
177
+ * multiplier = 1.0 + 0.196 × 0.5 = 1.098
178
+ */
179
+ function getRepsOnlyMultiplier(auxWeightKg, userWeightKg, difficultyLevel) {
180
+ const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * userWeightKg;
181
+ const totalLoad = bodyweightLoad + auxWeightKg;
182
+ const ratio = totalLoad / userWeightKg;
183
+ // Gentle scaling: aux weight adds intensity but not dramatically
184
+ return 1.0 + ratio * 0.5;
185
+ }
186
+ /**
187
+ * Multiplier for duration (isometric/hold) exercises.
188
+ *
189
+ * Two factors:
190
+ * 1. Duration category (short/medium/long) — longer holds are metabolically harder
191
+ * 2. Aux weight boost — holding weight during a plank increases energy cost
192
+ */
193
+ function getDurationMultiplier(durationSecs, auxWeightKg, userWeightKg, metabolicData) {
194
+ // Resolve duration factors (handle both nested and flat shapes)
195
+ const factors = resolveDurationFactors(metabolicData);
196
+ let durationMultiplier;
197
+ if (durationSecs < constants_1.DURATION_CATEGORY_THRESHOLDS.shortMax) {
198
+ durationMultiplier = factors.short;
199
+ }
200
+ else if (durationSecs <= constants_1.DURATION_CATEGORY_THRESHOLDS.mediumMax) {
201
+ durationMultiplier = factors.medium;
202
+ }
203
+ else {
204
+ durationMultiplier = factors.long;
205
+ }
206
+ // Aux weight boosts MET (holding a plate during plank, weighted vest, etc.)
207
+ const auxBoost = 1.0 + (auxWeightKg / userWeightKg) * 0.3;
208
+ return durationMultiplier * auxBoost;
209
+ }
210
+ /**
211
+ * MET calculation for cardio-machine exercises (treadmill, stationary bike, etc.)
212
+ *
213
+ * Uses average speed to interpolate within the MET range.
214
+ * If paceFactors are available, we interpolate from the pace table instead.
215
+ */
216
+ function getCardioMachineMET(set, metabolicData, metMin, metMax, effortNormalized) {
217
+ var _a;
218
+ // Average speed from the set data
219
+ const avgSpeed = (_a = set.speed) !== null && _a !== void 0 ? _a : 0;
220
+ if (avgSpeed > 0) {
221
+ // If we have pace factors, use them for more accurate MET lookup
222
+ if (metabolicData.paceFactors) {
223
+ return interpolatePaceFactors(avgSpeed, metabolicData.paceFactors);
224
+ }
225
+ // Otherwise, interpolate linearly within metRange based on speed
226
+ const speedFraction = (0, helpers_1.clamp)((avgSpeed - constants_1.CARDIO_SPEED_RANGE.min) /
227
+ (constants_1.CARDIO_SPEED_RANGE.max - constants_1.CARDIO_SPEED_RANGE.min), 0, 1);
228
+ return metMin + (metMax - metMin) * speedFraction;
229
+ }
230
+ // No speed data: fall back to effort-based interpolation
231
+ return metMin + (metMax - metMin) * effortNormalized;
232
+ }
233
+ /**
234
+ * MET calculation for cardio-free exercises (outdoor running, cycling, etc.)
235
+ *
236
+ * Derives speed from distance ÷ time, then interpolates MET.
237
+ */
238
+ function getCardioFreeMET(set, metabolicData, metMin, metMax, effortNormalized) {
239
+ var _a, _b;
240
+ const distance = (_a = set.distance) !== null && _a !== void 0 ? _a : 0;
241
+ const durationSecs = (_b = set.cardioDurationSecs) !== null && _b !== void 0 ? _b : 0;
242
+ if (distance > 0 && durationSecs > 0) {
243
+ // Speed in km/h
244
+ const speed = distance / (durationSecs / 3600);
245
+ if (metabolicData.paceFactors) {
246
+ return interpolatePaceFactors(speed, metabolicData.paceFactors);
247
+ }
248
+ const speedFraction = (0, helpers_1.clamp)((speed - constants_1.CARDIO_SPEED_RANGE.min) /
249
+ (constants_1.CARDIO_SPEED_RANGE.max - constants_1.CARDIO_SPEED_RANGE.min), 0, 1);
250
+ return metMin + (metMax - metMin) * speedFraction;
251
+ }
252
+ // No distance/time data: fall back to effort-based
253
+ return metMin + (metMax - metMin) * effortNormalized;
254
+ }
255
+ // ---------------------------------------------------------------------------
256
+ // Resolution Helpers (handle flat vs nested metabolicData shapes)
257
+ // ---------------------------------------------------------------------------
258
+ /**
259
+ * Resolve weight factors from metabolicData.
260
+ * Sample data sometimes has them nested under `weightFactors`, sometimes flat.
261
+ */
262
+ function resolveWeightFactors(metabolicData) {
263
+ var _a, _b;
264
+ if (metabolicData.weightFactors) {
265
+ return {
266
+ light: metabolicData.weightFactors.lightWeight,
267
+ moderate: metabolicData.weightFactors.moderateWeight,
268
+ heavy: metabolicData.weightFactors.heavyWeight,
269
+ };
270
+ }
271
+ // Check flat shape
272
+ if (metabolicData.lightWeight !== undefined) {
273
+ return {
274
+ light: metabolicData.lightWeight,
275
+ moderate: (_a = metabolicData.moderateWeight) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_WEIGHT_FACTORS.moderate,
276
+ heavy: (_b = metabolicData.heavyWeight) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_WEIGHT_FACTORS.heavy,
277
+ };
278
+ }
279
+ return constants_1.DEFAULT_WEIGHT_FACTORS;
280
+ }
281
+ /**
282
+ * Resolve duration factors from metabolicData.
283
+ * Same flat-vs-nested handling as weight factors.
284
+ */
285
+ function resolveDurationFactors(metabolicData) {
286
+ var _a, _b;
287
+ if (metabolicData.durationFactors) {
288
+ return {
289
+ short: metabolicData.durationFactors.shortDuration,
290
+ medium: metabolicData.durationFactors.mediumDuration,
291
+ long: metabolicData.durationFactors.longDuration,
292
+ };
293
+ }
294
+ // Check flat shape
295
+ if (metabolicData.shortDuration !== undefined) {
296
+ return {
297
+ short: metabolicData.shortDuration,
298
+ medium: (_a = metabolicData.mediumDuration) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_DURATION_FACTORS.medium,
299
+ long: (_b = metabolicData.longDuration) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_DURATION_FACTORS.long,
300
+ };
301
+ }
302
+ return constants_1.DEFAULT_DURATION_FACTORS;
303
+ }
304
+ /**
305
+ * Interpolate MET from a pace factors table.
306
+ *
307
+ * paceFactors is a map of speed (km/h) → MET value.
308
+ * We find the two closest speeds and linearly interpolate between them.
309
+ *
310
+ * Example:
311
+ * paceFactors = { "5.0": 4.3, "8.0": 8.3, "12.0": 11.0, "16.0": 15.0 }
312
+ * speed = 10 km/h → interpolate between 8.0 (8.3 MET) and 12.0 (11.0 MET)
313
+ * result ≈ 9.65 MET
314
+ */
315
+ function interpolatePaceFactors(speed, paceFactors) {
316
+ const entries = Object.keys(paceFactors)
317
+ .map((k) => [parseFloat(k), paceFactors[k]])
318
+ .filter(([k]) => !isNaN(k))
319
+ .sort((a, b) => a[0] - b[0]);
320
+ if (entries.length === 0)
321
+ return 3; // fallback moderate MET
322
+ // Below minimum pace
323
+ if (speed <= entries[0][0])
324
+ return entries[0][1];
325
+ // Above maximum pace
326
+ if (speed >= entries[entries.length - 1][0])
327
+ return entries[entries.length - 1][1];
328
+ // Find surrounding entries and interpolate
329
+ for (let i = 0; i < entries.length - 1; i++) {
330
+ const [speedLow, metLow] = entries[i];
331
+ const [speedHigh, metHigh] = entries[i + 1];
332
+ if (speed >= speedLow && speed <= speedHigh) {
333
+ const fraction = (speed - speedLow) / (speedHigh - speedLow);
334
+ return metLow + (metHigh - metLow) * fraction;
335
+ }
336
+ }
337
+ return entries[entries.length - 1][1]; // shouldn't reach here
338
+ }
339
+ /**
340
+ * Normalize metRange so [0] is always min and [1] is always max.
341
+ * Sample data has metRange as [5, 2] (max first) for some exercises.
342
+ */
343
+ function normalizeMetRange(metRange, baseMET) {
344
+ const min = Math.min(metRange[0], metRange[1]);
345
+ const max = Math.max(metRange[0], metRange[1]);
346
+ // If range seems invalid, create one around baseMET
347
+ if (min === max || max === 0) {
348
+ return [baseMET * 0.7, baseMET * 1.5];
349
+ }
350
+ return [min, max];
351
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX EXERCISE SCORING SYSTEM — Pillar 2: Muscle Fatigue
4
+ * ============================================================================
5
+ *
6
+ * Calculates per-muscle fatigue scores (0–100) for a single exercise.
7
+ *
8
+ * HOW IT WORKS:
9
+ *
10
+ * 1. For each set, compute a "fatigue stimulus" — how much mechanical work
11
+ * the muscles experienced. This is type-specific:
12
+ * weight-reps: kg × reps
13
+ * reps-only: (bodyweightLoad + auxWeight) × reps
14
+ * duration: durationSecs × (difficultyLevel + 1) × auxBoost
15
+ * cardio: durationSecs × speedFactor
16
+ *
17
+ * 2. Multiply by effort (RPE/RIR → 0.5–1.3) and the exercise's
18
+ * fatigueMultiplier from timingGuardrails.
19
+ *
20
+ * 3. Accumulate across sets with DIMINISHING RETURNS — later sets contribute
21
+ * less because the muscle is pre-fatigued and can't generate as much force.
22
+ * Decay: setDecay = 1 / (1 + 0.15 × setIndex)
23
+ *
24
+ * 4. Distribute the accumulated fatigue to muscles:
25
+ * Primary muscles → 100% of stimulus
26
+ * Secondary muscles → 35% of stimulus
27
+ *
28
+ * 5. Normalize to 0–100 using an EXERCISE-AWARE reference maximum.
29
+ * The reference max is what "5 hard sets of THIS exercise" would produce,
30
+ * scaled by the exercise's difficultyLevel so bicep curls and squats
31
+ * get fair scores.
32
+ *
33
+ * IMPORTANT: This function scores ONE exercise. The caller aggregates
34
+ * across exercises for full-workout muscle fatigue maps.
35
+ */
36
+ import type { IParsedSet, IUserContext } from "./types";
37
+ import type { ITimingGuardrails } from "./parseRecords";
38
+ /** Muscle key from EBodyParts (e.g., "pectoralis-major", "quadriceps") */
39
+ type MuscleKey = string;
40
+ /**
41
+ * Minimal exercise metadata needed for fatigue calculation.
42
+ */
43
+ interface IFatigueExerciseData {
44
+ primaryMuscles: MuscleKey[];
45
+ secondaryMuscles: MuscleKey[];
46
+ difficultyLevel: number;
47
+ metabolicData: {
48
+ compoundMultiplier: number;
49
+ muscleGroupFactor: number;
50
+ };
51
+ scoringSpecialHandling?: "plyometric" | "stretch-mobility" | "continuous-duration" | "loaded-carry";
52
+ }
53
+ /**
54
+ * Calculate per-muscle fatigue scores for an exercise.
55
+ *
56
+ * @param sets Parsed & validated sets (from parseRecords)
57
+ * @param exercise Exercise metadata (muscles, difficulty, metabolic)
58
+ * @param user Validated user context
59
+ * @param timingGuardrails For fatigueMultiplier
60
+ * @returns Map of muscle key → fatigue score (0–100)
61
+ *
62
+ * @example
63
+ * const fatigue = calculateMuscleFatigue(parsedSets, exercise, userCtx, exercise.timingGuardrails);
64
+ * // → { "pectoralis-major": 24, "pectoralis-minor": 24, "deltoids-anterior": 8, ... }
65
+ */
66
+ export declare function calculateMuscleFatigue(sets: IParsedSet[], exercise: IFatigueExerciseData, user: IUserContext, timingGuardrails?: ITimingGuardrails, historicalContext?: import("./types").IHistoricalContext): Record<string, number>;
67
+ export {};