@dgpholdings/greatoak-shared 1.2.16 → 1.2.17

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.
@@ -8,3 +8,4 @@ export { toError } from "./toError.util";
8
8
  export { generatePlanCode } from "./planCode.util";
9
9
  export { maskEmail, isAnonymousEmail, isEmail } from "./email.utils";
10
10
  export { NOOP } from "./noop.utils";
11
+ export { calculateExerciseScoreV2 } from "./scoring";
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NOOP = exports.isEmail = exports.isAnonymousEmail = exports.maskEmail = exports.generatePlanCode = exports.toError = exports.slugifyText = exports.isDefinedNumber = exports.isDefined = exports.calculateExerciseScore = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
3
+ exports.calculateExerciseScoreV2 = exports.NOOP = exports.isEmail = exports.isAnonymousEmail = exports.maskEmail = exports.generatePlanCode = exports.toError = exports.slugifyText = exports.isDefinedNumber = exports.isDefined = exports.calculateExerciseScore = exports.countryToCurrencyCode = exports.getDaysAndHoursDifference = exports.isUserAllowedToUpdate = exports.mmssToSecs = exports.toNumber = void 0;
4
4
  var number_util_1 = require("./number.util");
5
5
  Object.defineProperty(exports, "toNumber", { enumerable: true, get: function () { return number_util_1.toNumber; } });
6
6
  var time_util_1 = require("./time.util");
@@ -26,3 +26,5 @@ Object.defineProperty(exports, "isAnonymousEmail", { enumerable: true, get: func
26
26
  Object.defineProperty(exports, "isEmail", { enumerable: true, get: function () { return email_utils_1.isEmail; } });
27
27
  var noop_utils_1 = require("./noop.utils");
28
28
  Object.defineProperty(exports, "NOOP", { enumerable: true, get: function () { return noop_utils_1.NOOP; } });
29
+ var scoring_1 = require("./scoring");
30
+ Object.defineProperty(exports, "calculateExerciseScoreV2", { enumerable: true, get: function () { return scoring_1.calculateExerciseScoreV2; } });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX EXERCISE SCORING SYSTEM — Pillar 1: Calorie Burn
4
+ * ============================================================================
5
+ *
6
+ * Estimates energy expenditure (kcal) for an exercise using the MET system.
7
+ *
8
+ * Formula (per set):
9
+ * calories = effectiveMET × userWeightKg × (activeDurationSecs / 3600)
10
+ *
11
+ * Where effectiveMET is the exercise's baseMET adjusted for:
12
+ * - Effort level (RPE/RIR → scales within metRange)
13
+ * - Weight intensity (for weight-reps: how heavy relative to bodyweight)
14
+ * - Duration category (for duration: short/medium/long multiplier)
15
+ * - Speed (for cardio: interpolated from pace or speed range)
16
+ * - Compound multiplier (multi-joint exercises burn more)
17
+ *
18
+ * After summing all sets:
19
+ * - Rest calories are added (elevated MET during rest periods)
20
+ * - EPOC (afterburn) is added as a percentage of work calories
21
+ *
22
+ * References:
23
+ * - Ainsworth BE et al. "Compendium of Physical Activities" (2011)
24
+ * - Katch, McArdle & Katch, "Exercise Physiology" (8th ed.)
25
+ */
26
+ import type { IParsedSet, IUserContext } from "./types";
27
+ interface IMetabolicData {
28
+ baseMET: number;
29
+ metRange: [number, number];
30
+ compoundMultiplier: number;
31
+ muscleGroupFactor: number;
32
+ intensityScaling: "linear" | "exponential" | "plateau";
33
+ epocFactor: number;
34
+ weightFactors?: {
35
+ lightWeight: number;
36
+ moderateWeight: number;
37
+ heavyWeight: number;
38
+ };
39
+ durationFactors?: {
40
+ shortDuration: number;
41
+ mediumDuration: number;
42
+ longDuration: number;
43
+ };
44
+ paceFactors?: Record<string, number>;
45
+ lightWeight?: number;
46
+ moderateWeight?: number;
47
+ heavyWeight?: number;
48
+ shortDuration?: number;
49
+ mediumDuration?: number;
50
+ longDuration?: number;
51
+ }
52
+ /**
53
+ * Calculate total calorie burn for an exercise.
54
+ *
55
+ * @param sets Parsed & validated sets (from parseRecords)
56
+ * @param metabolicData Exercise's metabolic configuration
57
+ * @param user Validated user context
58
+ * @param difficultyLevel Exercise difficulty (0–4), used for bodyweight estimation
59
+ * @returns Total calories burned (kcal), rounded to 1 decimal
60
+ *
61
+ * @example
62
+ * const calories = calculateCalories(parsedSets, exercise.metabolicData, userCtx, exercise.difficultyLevel);
63
+ * // → 6.4 (for 3 light bench press sets)
64
+ * // → 142.3 (for a 20-min treadmill run)
65
+ */
66
+ export declare function calculateCalories(sets: IParsedSet[], metabolicData: IMetabolicData, user: IUserContext, difficultyLevel: number): number;
67
+ export {};
@@ -0,0 +1,329 @@
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) {
50
+ if (sets.length === 0)
51
+ return 0;
52
+ // Normalize metRange so [0] = min, [1] = max (sample data has them reversed)
53
+ const [metMin, metMax] = normalizeMetRange(metabolicData.metRange, metabolicData.baseMET);
54
+ let totalWorkCalories = 0;
55
+ let totalRestCalories = 0;
56
+ for (const set of sets) {
57
+ // 1. Calculate effective MET for this set
58
+ const effectiveMET = calculateEffectiveMET(set, metabolicData, metMin, metMax, user, difficultyLevel);
59
+ // 2. Work calories: MET × weight × time
60
+ const workHours = set.activeDurationSecs / 3600;
61
+ totalWorkCalories += effectiveMET * user.weightKg * workHours;
62
+ // 3. Rest calories: elevated resting MET during recovery
63
+ if (set.restDurationSecs !== null && set.restDurationSecs > 0) {
64
+ const restHours = set.restDurationSecs / 3600;
65
+ totalRestCalories += constants_1.REST_MET * user.weightKg * restHours;
66
+ }
67
+ }
68
+ // 4. EPOC (Excess Post-exercise Oxygen Consumption) — afterburn effect
69
+ const epocCalories = totalWorkCalories * metabolicData.epocFactor;
70
+ // 5. Total = work + rest + afterburn
71
+ const total = totalWorkCalories + totalRestCalories + epocCalories;
72
+ return Math.round(total * 10) / 10; // Round to 1 decimal place
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Effective MET Calculation (per set)
76
+ // ---------------------------------------------------------------------------
77
+ /**
78
+ * Calculate the effective MET for a single set based on its type and intensity.
79
+ *
80
+ * The general approach:
81
+ * 1. Interpolate base MET within [metMin, metMax] using effort fraction
82
+ * 2. Apply type-specific intensity multiplier
83
+ * 3. Apply compound multiplier
84
+ *
85
+ * The effort fraction (from RPE/RIR) determines WHERE in the MET range
86
+ * this set falls. Low effort → closer to metMin, high effort → closer to metMax.
87
+ */
88
+ function calculateEffectiveMET(set, metabolicData, metMin, metMax, user, difficultyLevel) {
89
+ var _a, _b, _c, _d;
90
+ // Start with effort-scaled MET
91
+ // effortFraction is 0.5–1.3 (from helpers), we normalize to 0–1 for interpolation
92
+ const effortNormalized = (0, helpers_1.clamp)((set.effortFraction - 0.5) / 0.8, 0, 1);
93
+ let met = metMin + (metMax - metMin) * effortNormalized;
94
+ // Apply type-specific intensity scaling
95
+ switch (set.type) {
96
+ case "weight-reps":
97
+ met *= getWeightMultiplier((_a = set.kg) !== null && _a !== void 0 ? _a : 0, user.weightKg, metabolicData);
98
+ break;
99
+ case "reps-only":
100
+ met *= getRepsOnlyMultiplier((_b = set.auxWeightKg) !== null && _b !== void 0 ? _b : 0, user.weightKg, difficultyLevel);
101
+ break;
102
+ case "duration":
103
+ met *= getDurationMultiplier((_c = set.durationSecs) !== null && _c !== void 0 ? _c : 0, (_d = set.auxWeightKg) !== null && _d !== void 0 ? _d : 0, user.weightKg, metabolicData);
104
+ break;
105
+ case "cardio-machine":
106
+ // For cardio, we override the effort-based MET with speed-based MET
107
+ met = getCardioMachineMET(set, metabolicData, metMin, metMax, effortNormalized);
108
+ break;
109
+ case "cardio-free":
110
+ met = getCardioFreeMET(set, metabolicData, metMin, metMax, effortNormalized);
111
+ break;
112
+ }
113
+ // Apply compound multiplier (multi-joint exercises have higher energy cost)
114
+ met *= metabolicData.compoundMultiplier;
115
+ // Safety clamp: MET should never go below 1 or above 25
116
+ return (0, helpers_1.clamp)(met, 1, 25);
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // Type-Specific Multipliers
120
+ // ---------------------------------------------------------------------------
121
+ /**
122
+ * Weight multiplier for weight-reps exercises.
123
+ *
124
+ * Classifies the weight as light/moderate/heavy relative to bodyweight
125
+ * and returns the corresponding multiplier.
126
+ *
127
+ * Example: 10kg at 75kg bodyweight → ratio 0.13 → "light" → 0.8 multiplier
128
+ */
129
+ function getWeightMultiplier(kg, userWeightKg, metabolicData) {
130
+ const ratio = kg / userWeightKg;
131
+ // Resolve weight factors (handle both nested and flat shapes)
132
+ const factors = resolveWeightFactors(metabolicData);
133
+ if (ratio < constants_1.WEIGHT_CATEGORY_THRESHOLDS.lightMax) {
134
+ return factors.light;
135
+ }
136
+ else if (ratio < constants_1.WEIGHT_CATEGORY_THRESHOLDS.moderateMax) {
137
+ return factors.moderate;
138
+ }
139
+ else {
140
+ return factors.heavy;
141
+ }
142
+ }
143
+ /**
144
+ * Multiplier for reps-only (bodyweight) exercises.
145
+ *
146
+ * The "load" is the user's bodyweight (scaled by difficulty) + any aux weight.
147
+ * We return a gentle multiplier that boosts MET slightly for added weight.
148
+ *
149
+ * Example: Push-up (difficulty 1) with 2.5kg vest at 75kg bodyweight
150
+ * bodyweightLoad = (1/4) × 0.65 × 75 = 12.19 kg equivalent
151
+ * totalLoad = 12.19 + 2.5 = 14.69
152
+ * ratio = 14.69 / 75 = 0.196
153
+ * multiplier = 1.0 + 0.196 × 0.5 = 1.098
154
+ */
155
+ function getRepsOnlyMultiplier(auxWeightKg, userWeightKg, difficultyLevel) {
156
+ const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * userWeightKg;
157
+ const totalLoad = bodyweightLoad + auxWeightKg;
158
+ const ratio = totalLoad / userWeightKg;
159
+ // Gentle scaling: aux weight adds intensity but not dramatically
160
+ return 1.0 + ratio * 0.5;
161
+ }
162
+ /**
163
+ * Multiplier for duration (isometric/hold) exercises.
164
+ *
165
+ * Two factors:
166
+ * 1. Duration category (short/medium/long) — longer holds are metabolically harder
167
+ * 2. Aux weight boost — holding weight during a plank increases energy cost
168
+ */
169
+ function getDurationMultiplier(durationSecs, auxWeightKg, userWeightKg, metabolicData) {
170
+ // Resolve duration factors (handle both nested and flat shapes)
171
+ const factors = resolveDurationFactors(metabolicData);
172
+ let durationMultiplier;
173
+ if (durationSecs < constants_1.DURATION_CATEGORY_THRESHOLDS.shortMax) {
174
+ durationMultiplier = factors.short;
175
+ }
176
+ else if (durationSecs <= constants_1.DURATION_CATEGORY_THRESHOLDS.mediumMax) {
177
+ durationMultiplier = factors.medium;
178
+ }
179
+ else {
180
+ durationMultiplier = factors.long;
181
+ }
182
+ // Aux weight boosts MET (holding a plate during plank, weighted vest, etc.)
183
+ const auxBoost = 1.0 + (auxWeightKg / userWeightKg) * 0.3;
184
+ return durationMultiplier * auxBoost;
185
+ }
186
+ /**
187
+ * MET calculation for cardio-machine exercises (treadmill, stationary bike, etc.)
188
+ *
189
+ * Uses average speed to interpolate within the MET range.
190
+ * If paceFactors are available, we interpolate from the pace table instead.
191
+ */
192
+ function getCardioMachineMET(set, metabolicData, metMin, metMax, effortNormalized) {
193
+ var _a, _b;
194
+ const speedMin = (_a = set.speedMin) !== null && _a !== void 0 ? _a : 0;
195
+ const speedMax = (_b = set.speedMax) !== null && _b !== void 0 ? _b : 0;
196
+ // Average speed from the set data
197
+ const avgSpeed = (speedMin + speedMax) / 2;
198
+ if (avgSpeed > 0) {
199
+ // If we have pace factors, use them for more accurate MET lookup
200
+ if (metabolicData.paceFactors) {
201
+ return interpolatePaceFactors(avgSpeed, metabolicData.paceFactors);
202
+ }
203
+ // Otherwise, interpolate linearly within metRange based on speed
204
+ const speedFraction = (0, helpers_1.clamp)((avgSpeed - constants_1.CARDIO_SPEED_RANGE.min) /
205
+ (constants_1.CARDIO_SPEED_RANGE.max - constants_1.CARDIO_SPEED_RANGE.min), 0, 1);
206
+ return metMin + (metMax - metMin) * speedFraction;
207
+ }
208
+ // No speed data: fall back to effort-based interpolation
209
+ return metMin + (metMax - metMin) * effortNormalized;
210
+ }
211
+ /**
212
+ * MET calculation for cardio-free exercises (outdoor running, cycling, etc.)
213
+ *
214
+ * Derives speed from distance ÷ time, then interpolates MET.
215
+ */
216
+ function getCardioFreeMET(set, metabolicData, metMin, metMax, effortNormalized) {
217
+ var _a, _b;
218
+ const distance = (_a = set.distance) !== null && _a !== void 0 ? _a : 0;
219
+ const durationSecs = (_b = set.cardioDurationSecs) !== null && _b !== void 0 ? _b : 0;
220
+ if (distance > 0 && durationSecs > 0) {
221
+ // Speed in km/h
222
+ const speed = distance / (durationSecs / 3600);
223
+ if (metabolicData.paceFactors) {
224
+ return interpolatePaceFactors(speed, metabolicData.paceFactors);
225
+ }
226
+ const speedFraction = (0, helpers_1.clamp)((speed - 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 distance/time data: fall back to effort-based
231
+ return metMin + (metMax - metMin) * effortNormalized;
232
+ }
233
+ // ---------------------------------------------------------------------------
234
+ // Resolution Helpers (handle flat vs nested metabolicData shapes)
235
+ // ---------------------------------------------------------------------------
236
+ /**
237
+ * Resolve weight factors from metabolicData.
238
+ * Sample data sometimes has them nested under `weightFactors`, sometimes flat.
239
+ */
240
+ function resolveWeightFactors(metabolicData) {
241
+ var _a, _b;
242
+ if (metabolicData.weightFactors) {
243
+ return {
244
+ light: metabolicData.weightFactors.lightWeight,
245
+ moderate: metabolicData.weightFactors.moderateWeight,
246
+ heavy: metabolicData.weightFactors.heavyWeight,
247
+ };
248
+ }
249
+ // Check flat shape
250
+ if (metabolicData.lightWeight !== undefined) {
251
+ return {
252
+ light: metabolicData.lightWeight,
253
+ moderate: (_a = metabolicData.moderateWeight) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_WEIGHT_FACTORS.moderate,
254
+ heavy: (_b = metabolicData.heavyWeight) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_WEIGHT_FACTORS.heavy,
255
+ };
256
+ }
257
+ return constants_1.DEFAULT_WEIGHT_FACTORS;
258
+ }
259
+ /**
260
+ * Resolve duration factors from metabolicData.
261
+ * Same flat-vs-nested handling as weight factors.
262
+ */
263
+ function resolveDurationFactors(metabolicData) {
264
+ var _a, _b;
265
+ if (metabolicData.durationFactors) {
266
+ return {
267
+ short: metabolicData.durationFactors.shortDuration,
268
+ medium: metabolicData.durationFactors.mediumDuration,
269
+ long: metabolicData.durationFactors.longDuration,
270
+ };
271
+ }
272
+ // Check flat shape
273
+ if (metabolicData.shortDuration !== undefined) {
274
+ return {
275
+ short: metabolicData.shortDuration,
276
+ medium: (_a = metabolicData.mediumDuration) !== null && _a !== void 0 ? _a : constants_1.DEFAULT_DURATION_FACTORS.medium,
277
+ long: (_b = metabolicData.longDuration) !== null && _b !== void 0 ? _b : constants_1.DEFAULT_DURATION_FACTORS.long,
278
+ };
279
+ }
280
+ return constants_1.DEFAULT_DURATION_FACTORS;
281
+ }
282
+ /**
283
+ * Interpolate MET from a pace factors table.
284
+ *
285
+ * paceFactors is a map of speed (km/h) → MET value.
286
+ * We find the two closest speeds and linearly interpolate between them.
287
+ *
288
+ * Example:
289
+ * paceFactors = { "5.0": 4.3, "8.0": 8.3, "12.0": 11.0, "16.0": 15.0 }
290
+ * speed = 10 km/h → interpolate between 8.0 (8.3 MET) and 12.0 (11.0 MET)
291
+ * result ≈ 9.65 MET
292
+ */
293
+ function interpolatePaceFactors(speed, paceFactors) {
294
+ const entries = Object.keys(paceFactors)
295
+ .map((k) => [parseFloat(k), paceFactors[k]])
296
+ .filter(([k]) => !isNaN(k))
297
+ .sort((a, b) => a[0] - b[0]);
298
+ if (entries.length === 0)
299
+ return 3; // fallback moderate MET
300
+ // Below minimum pace
301
+ if (speed <= entries[0][0])
302
+ return entries[0][1];
303
+ // Above maximum pace
304
+ if (speed >= entries[entries.length - 1][0])
305
+ return entries[entries.length - 1][1];
306
+ // Find surrounding entries and interpolate
307
+ for (let i = 0; i < entries.length - 1; i++) {
308
+ const [speedLow, metLow] = entries[i];
309
+ const [speedHigh, metHigh] = entries[i + 1];
310
+ if (speed >= speedLow && speed <= speedHigh) {
311
+ const fraction = (speed - speedLow) / (speedHigh - speedLow);
312
+ return metLow + (metHigh - metLow) * fraction;
313
+ }
314
+ }
315
+ return entries[entries.length - 1][1]; // shouldn't reach here
316
+ }
317
+ /**
318
+ * Normalize metRange so [0] is always min and [1] is always max.
319
+ * Sample data has metRange as [5, 2] (max first) for some exercises.
320
+ */
321
+ function normalizeMetRange(metRange, baseMET) {
322
+ const min = Math.min(metRange[0], metRange[1]);
323
+ const max = Math.max(metRange[0], metRange[1]);
324
+ // If range seems invalid, create one around baseMET
325
+ if (min === max || max === 0) {
326
+ return [baseMET * 0.7, baseMET * 1.5];
327
+ }
328
+ return [min, max];
329
+ }
@@ -0,0 +1,66 @@
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
+ }
52
+ /**
53
+ * Calculate per-muscle fatigue scores for an exercise.
54
+ *
55
+ * @param sets Parsed & validated sets (from parseRecords)
56
+ * @param exercise Exercise metadata (muscles, difficulty, metabolic)
57
+ * @param user Validated user context
58
+ * @param timingGuardrails For fatigueMultiplier
59
+ * @returns Map of muscle key → fatigue score (0–100)
60
+ *
61
+ * @example
62
+ * const fatigue = calculateMuscleFatigue(parsedSets, exercise, userCtx, exercise.timingGuardrails);
63
+ * // → { "pectoralis-major": 24, "pectoralis-minor": 24, "deltoids-anterior": 8, ... }
64
+ */
65
+ export declare function calculateMuscleFatigue(sets: IParsedSet[], exercise: IFatigueExerciseData, user: IUserContext, timingGuardrails?: ITimingGuardrails): Record<string, number>;
66
+ export {};