@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.
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +3 -1
- package/dist/utils/scoring/calculateCalories.d.ts +67 -0
- package/dist/utils/scoring/calculateCalories.js +329 -0
- package/dist/utils/scoring/calculateMuscleFatiue.d.ts +66 -0
- package/dist/utils/scoring/calculateMuscleFatiue.js +290 -0
- package/dist/utils/scoring/calculateQualityScore.d.ts +69 -0
- package/dist/utils/scoring/calculateQualityScore.js +333 -0
- package/dist/utils/scoring/constants.d.ts +191 -0
- package/dist/utils/scoring/constants.js +227 -0
- package/dist/utils/scoring/helpers.d.ts +119 -0
- package/dist/utils/scoring/helpers.js +229 -0
- package/dist/utils/scoring/index.d.ts +28 -0
- package/dist/utils/scoring/index.js +44 -0
- package/dist/utils/scoring/parseRecords.d.ts +97 -0
- package/dist/utils/scoring/parseRecords.js +280 -0
- package/dist/utils/scoring/types.d.ts +84 -0
- package/dist/utils/scoring/types.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// calculateMuscleFatiue.ts
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Pillar 2: Muscle Fatigue
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* Calculates per-muscle fatigue scores (0–100) for a single exercise.
|
|
9
|
+
*
|
|
10
|
+
* HOW IT WORKS:
|
|
11
|
+
*
|
|
12
|
+
* 1. For each set, compute a "fatigue stimulus" — how much mechanical work
|
|
13
|
+
* the muscles experienced. This is type-specific:
|
|
14
|
+
* weight-reps: kg × reps
|
|
15
|
+
* reps-only: (bodyweightLoad + auxWeight) × reps
|
|
16
|
+
* duration: durationSecs × (difficultyLevel + 1) × auxBoost
|
|
17
|
+
* cardio: durationSecs × speedFactor
|
|
18
|
+
*
|
|
19
|
+
* 2. Multiply by effort (RPE/RIR → 0.5–1.3) and the exercise's
|
|
20
|
+
* fatigueMultiplier from timingGuardrails.
|
|
21
|
+
*
|
|
22
|
+
* 3. Accumulate across sets with DIMINISHING RETURNS — later sets contribute
|
|
23
|
+
* less because the muscle is pre-fatigued and can't generate as much force.
|
|
24
|
+
* Decay: setDecay = 1 / (1 + 0.15 × setIndex)
|
|
25
|
+
*
|
|
26
|
+
* 4. Distribute the accumulated fatigue to muscles:
|
|
27
|
+
* Primary muscles → 100% of stimulus
|
|
28
|
+
* Secondary muscles → 35% of stimulus
|
|
29
|
+
*
|
|
30
|
+
* 5. Normalize to 0–100 using an EXERCISE-AWARE reference maximum.
|
|
31
|
+
* The reference max is what "5 hard sets of THIS exercise" would produce,
|
|
32
|
+
* scaled by the exercise's difficultyLevel so bicep curls and squats
|
|
33
|
+
* get fair scores.
|
|
34
|
+
*
|
|
35
|
+
* IMPORTANT: This function scores ONE exercise. The caller aggregates
|
|
36
|
+
* across exercises for full-workout muscle fatigue maps.
|
|
37
|
+
*/
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.calculateMuscleFatigue = calculateMuscleFatigue;
|
|
40
|
+
const constants_1 = require("./constants");
|
|
41
|
+
const helpers_1 = require("./helpers");
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Main Fatigue Calculation
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
/**
|
|
46
|
+
* Calculate per-muscle fatigue scores for an exercise.
|
|
47
|
+
*
|
|
48
|
+
* @param sets Parsed & validated sets (from parseRecords)
|
|
49
|
+
* @param exercise Exercise metadata (muscles, difficulty, metabolic)
|
|
50
|
+
* @param user Validated user context
|
|
51
|
+
* @param timingGuardrails For fatigueMultiplier
|
|
52
|
+
* @returns Map of muscle key → fatigue score (0–100)
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* const fatigue = calculateMuscleFatigue(parsedSets, exercise, userCtx, exercise.timingGuardrails);
|
|
56
|
+
* // → { "pectoralis-major": 24, "pectoralis-minor": 24, "deltoids-anterior": 8, ... }
|
|
57
|
+
*/
|
|
58
|
+
function calculateMuscleFatigue(sets, exercise, user, timingGuardrails) {
|
|
59
|
+
var _a;
|
|
60
|
+
if (sets.length === 0)
|
|
61
|
+
return {};
|
|
62
|
+
const fatigueMultiplier = (_a = timingGuardrails === null || timingGuardrails === void 0 ? void 0 : timingGuardrails.fatigueMultiplier) !== null && _a !== void 0 ? _a : constants_1.FALLBACK_FATIGUE_MULTIPLIER;
|
|
63
|
+
// --- Step 1–3: Compute cumulative fatigue stimulus ---
|
|
64
|
+
const cumulativeFatigue = computeCumulativeFatigue(sets, exercise.difficultyLevel, user, fatigueMultiplier);
|
|
65
|
+
// --- Step 4: Distribute to muscles ---
|
|
66
|
+
const rawMuscleFatigue = distributeFatigueToMuscles(cumulativeFatigue, exercise.primaryMuscles, exercise.secondaryMuscles);
|
|
67
|
+
// --- Step 5: Normalize to 0–100 ---
|
|
68
|
+
const referenceMax = computeReferenceMax(sets[0].type, exercise.difficultyLevel, user, exercise.metabolicData.muscleGroupFactor);
|
|
69
|
+
return normalizeScores(rawMuscleFatigue, referenceMax);
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Step 1: Volume Load per Set
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
/**
|
|
75
|
+
* Calculate raw volume load for a single set.
|
|
76
|
+
*
|
|
77
|
+
* Volume load represents the mechanical work experienced by the muscles.
|
|
78
|
+
* Different exercise types produce volume in fundamentally different ways:
|
|
79
|
+
*
|
|
80
|
+
* weight-reps: Force (kg) × repetitions
|
|
81
|
+
* reps-only: Effective load (BW fraction + aux) × repetitions
|
|
82
|
+
* duration: Time under tension × difficulty scaling × aux boost
|
|
83
|
+
* cardio: Duration × speed factor (represents sustained effort)
|
|
84
|
+
*/
|
|
85
|
+
function computeVolumeLoad(set, difficultyLevel, user) {
|
|
86
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
|
|
87
|
+
switch (set.type) {
|
|
88
|
+
case "weight-reps": {
|
|
89
|
+
const kg = (_a = set.kg) !== null && _a !== void 0 ? _a : 0;
|
|
90
|
+
const reps = (_b = set.reps) !== null && _b !== void 0 ? _b : 0;
|
|
91
|
+
return kg * reps;
|
|
92
|
+
}
|
|
93
|
+
case "reps-only": {
|
|
94
|
+
const reps = (_c = set.reps) !== null && _c !== void 0 ? _c : 0;
|
|
95
|
+
const auxWeightKg = (_d = set.auxWeightKg) !== null && _d !== void 0 ? _d : 0;
|
|
96
|
+
// Estimate how much bodyweight the exercise loads
|
|
97
|
+
// difficulty 0 → 0%, difficulty 2 → ~33%, difficulty 4 → 65%
|
|
98
|
+
const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * user.weightKg;
|
|
99
|
+
const totalLoad = bodyweightLoad + auxWeightKg;
|
|
100
|
+
return totalLoad * reps;
|
|
101
|
+
}
|
|
102
|
+
case "duration": {
|
|
103
|
+
const durationSecs = (_e = set.durationSecs) !== null && _e !== void 0 ? _e : set.activeDurationSecs;
|
|
104
|
+
const auxWeightKg = (_f = set.auxWeightKg) !== null && _f !== void 0 ? _f : 0;
|
|
105
|
+
// Duration exercises: time under tension is the primary driver
|
|
106
|
+
// Scale by (difficultyLevel + 1) so even difficulty-0 produces some fatigue
|
|
107
|
+
// Aux weight adds load (holding a plate during plank, etc.)
|
|
108
|
+
const auxBoost = 1 + (auxWeightKg / user.weightKg) * 0.3;
|
|
109
|
+
return durationSecs * (difficultyLevel + 1) * auxBoost;
|
|
110
|
+
}
|
|
111
|
+
case "cardio-machine": {
|
|
112
|
+
const durationSecs = (_g = set.cardioDurationSecs) !== null && _g !== void 0 ? _g : set.activeDurationSecs;
|
|
113
|
+
const speedMin = (_h = set.speedMin) !== null && _h !== void 0 ? _h : 0;
|
|
114
|
+
const speedMax = (_j = set.speedMax) !== null && _j !== void 0 ? _j : 0;
|
|
115
|
+
const avgSpeed = (speedMin + speedMax) / 2;
|
|
116
|
+
// Speed factor: faster pace = more muscle engagement per second
|
|
117
|
+
// Normalize against expected speed range (3–20 km/h)
|
|
118
|
+
const speedFactor = avgSpeed > 0 ? (0, helpers_1.clamp)(avgSpeed / constants_1.CARDIO_SPEED_RANGE.max, 0.1, 1.5) : 0.3; // fallback: light effort
|
|
119
|
+
// Cardio dampener: running fatigues muscles less than lifting
|
|
120
|
+
// A 30-min jog ≠ 30 min of squats for muscle fatigue
|
|
121
|
+
return durationSecs * speedFactor * constants_1.CARDIO_MUSCLE_FATIGUE_DAMPENER;
|
|
122
|
+
}
|
|
123
|
+
case "cardio-free": {
|
|
124
|
+
const durationSecs = (_k = set.cardioDurationSecs) !== null && _k !== void 0 ? _k : set.activeDurationSecs;
|
|
125
|
+
const distance = (_l = set.distance) !== null && _l !== void 0 ? _l : 0;
|
|
126
|
+
let baseVolume;
|
|
127
|
+
if (distance > 0 && durationSecs > 0) {
|
|
128
|
+
const speedKmh = distance / (durationSecs / 3600);
|
|
129
|
+
const speedFactor = (0, helpers_1.clamp)(speedKmh / constants_1.CARDIO_SPEED_RANGE.max, 0.1, 1.5);
|
|
130
|
+
baseVolume = durationSecs * speedFactor;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
baseVolume = durationSecs * 0.3;
|
|
134
|
+
}
|
|
135
|
+
return baseVolume * constants_1.CARDIO_MUSCLE_FATIGUE_DAMPENER;
|
|
136
|
+
}
|
|
137
|
+
default:
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Steps 2–3: Effort × Decay Accumulation
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
/**
|
|
145
|
+
* Compute cumulative fatigue stimulus across all sets.
|
|
146
|
+
*
|
|
147
|
+
* For each set:
|
|
148
|
+
* stimulus = volumeLoad × effortFraction × fatigueMultiplier
|
|
149
|
+
*
|
|
150
|
+
* Then accumulated with diminishing returns:
|
|
151
|
+
* total += stimulus × (1 / (1 + 0.15 × setIndex))
|
|
152
|
+
*
|
|
153
|
+
* The decay models real physiology: set 1 creates the most novel stimulus,
|
|
154
|
+
* later sets work a pre-fatigued muscle that can't generate as much force.
|
|
155
|
+
*
|
|
156
|
+
* @returns Single number representing total fatigue stimulus
|
|
157
|
+
*/
|
|
158
|
+
function computeCumulativeFatigue(sets, difficultyLevel, user, fatigueMultiplier) {
|
|
159
|
+
let cumulative = 0;
|
|
160
|
+
for (let i = 0; i < sets.length; i++) {
|
|
161
|
+
const set = sets[i];
|
|
162
|
+
// Step 1: Raw volume for this set
|
|
163
|
+
const volumeLoad = computeVolumeLoad(set, difficultyLevel, user);
|
|
164
|
+
// Step 2: Scale by effort and exercise fatigue multiplier
|
|
165
|
+
const stimulus = volumeLoad * set.effortFraction * fatigueMultiplier;
|
|
166
|
+
// Step 3: Apply diminishing returns decay
|
|
167
|
+
// set 0: 1.000 (full stimulus)
|
|
168
|
+
// set 1: 0.870
|
|
169
|
+
// set 2: 0.769
|
|
170
|
+
// set 3: 0.690
|
|
171
|
+
// set 4: 0.625
|
|
172
|
+
const setDecay = 1 / (1 + constants_1.SET_FATIGUE_DECAY_RATE * i);
|
|
173
|
+
cumulative += stimulus * setDecay;
|
|
174
|
+
}
|
|
175
|
+
return cumulative;
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Step 4: Distribute to Muscles
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* Distribute cumulative fatigue to individual muscles.
|
|
182
|
+
*
|
|
183
|
+
* Primary muscles receive 100% of the stimulus (they're doing the work).
|
|
184
|
+
* Secondary muscles receive 35% (they assist but aren't the prime movers).
|
|
185
|
+
*
|
|
186
|
+
* If a muscle appears in BOTH primary and secondary (unlikely but possible
|
|
187
|
+
* with data inconsistencies), the values are summed — not overwritten.
|
|
188
|
+
*/
|
|
189
|
+
function distributeFatigueToMuscles(cumulativeFatigue, primaryMuscles, secondaryMuscles) {
|
|
190
|
+
var _a, _b;
|
|
191
|
+
const muscleFatigue = {};
|
|
192
|
+
for (const muscle of primaryMuscles) {
|
|
193
|
+
muscleFatigue[muscle] =
|
|
194
|
+
((_a = muscleFatigue[muscle]) !== null && _a !== void 0 ? _a : 0) +
|
|
195
|
+
cumulativeFatigue * constants_1.PRIMARY_MUSCLE_ALLOCATION;
|
|
196
|
+
}
|
|
197
|
+
for (const muscle of secondaryMuscles) {
|
|
198
|
+
muscleFatigue[muscle] =
|
|
199
|
+
((_b = muscleFatigue[muscle]) !== null && _b !== void 0 ? _b : 0) +
|
|
200
|
+
cumulativeFatigue * constants_1.SECONDARY_MUSCLE_ALLOCATION;
|
|
201
|
+
}
|
|
202
|
+
return muscleFatigue;
|
|
203
|
+
}
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// Step 5: Normalize to 0–100
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
/**
|
|
208
|
+
* Compute the reference maximum for normalization.
|
|
209
|
+
*
|
|
210
|
+
* This represents "what would 5 hard sets of THIS exercise produce?"
|
|
211
|
+
* The answer depends on:
|
|
212
|
+
* - Exercise type (weight vs duration vs cardio)
|
|
213
|
+
* - Difficulty level (scales expected weight/load)
|
|
214
|
+
* - User weight (heavier users move more mass)
|
|
215
|
+
*
|
|
216
|
+
* NOTE: fatigueMultiplier is intentionally NOT included here.
|
|
217
|
+
* It only applies to the stimulus (numerator), not the reference (denominator).
|
|
218
|
+
* This way, exercises with high fatigueMultiplier (e.g., bench press 1.3)
|
|
219
|
+
* actually produce HIGHER fatigue scores — the multiplier amplifies the
|
|
220
|
+
* stimulus without raising the bar to score against.
|
|
221
|
+
*
|
|
222
|
+
* muscleGroupFactor IS included because it's a property of the exercise's
|
|
223
|
+
* muscle engagement pattern, not the user's effort. It scales both sides
|
|
224
|
+
* proportionally so exercises with more muscle groups don't get inflated scores.
|
|
225
|
+
*
|
|
226
|
+
* WHY exercise-aware: A bicep curl (difficulty 1) and a squat (difficulty 3)
|
|
227
|
+
* should both be able to score 80–100 if performed well. Without scaling,
|
|
228
|
+
* a squat would always dominate because it uses more weight absolutely.
|
|
229
|
+
*/
|
|
230
|
+
function computeReferenceMax(exerciseType, difficultyLevel, user, muscleGroupFactor) {
|
|
231
|
+
let singleSetMax;
|
|
232
|
+
switch (exerciseType) {
|
|
233
|
+
case "weight-reps": {
|
|
234
|
+
// Expected max weight for this difficulty: 0.3×BW (easy) → 1.5×BW (hard)
|
|
235
|
+
const typicalMaxWeight = (0, constants_1.DIFFICULTY_TO_WEIGHT_FRACTION)(difficultyLevel) * user.weightKg;
|
|
236
|
+
singleSetMax = typicalMaxWeight * constants_1.REFERENCE_MAX_REPS;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case "reps-only": {
|
|
240
|
+
const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * user.weightKg;
|
|
241
|
+
const typicalAux = 5; // assume up to 5kg aux for reference
|
|
242
|
+
singleSetMax = (bodyweightLoad + typicalAux) * 15;
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case "duration": {
|
|
246
|
+
// Max hold time scales with difficulty:
|
|
247
|
+
// difficulty 0 → 60s (easy stretch)
|
|
248
|
+
// difficulty 2 → 120s (plank, side plank)
|
|
249
|
+
// difficulty 4 → 180s (advanced isometric hold)
|
|
250
|
+
const maxHoldSecs = 60 + difficultyLevel * 30;
|
|
251
|
+
singleSetMax = maxHoldSecs * (difficultyLevel + 1);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case "cardio-machine":
|
|
255
|
+
case "cardio-free": {
|
|
256
|
+
// 30 minutes at 75% max speed is a strong cardio effort
|
|
257
|
+
// Apply same dampener as volume calculation for consistent scaling
|
|
258
|
+
singleSetMax = 1800 * 0.75 * constants_1.CARDIO_MUSCLE_FATIGUE_DAMPENER;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
default:
|
|
262
|
+
singleSetMax = 100;
|
|
263
|
+
}
|
|
264
|
+
// Simulate 5 sets with decay (same decay as actual calculation)
|
|
265
|
+
let totalWithDecay = 0;
|
|
266
|
+
for (let i = 0; i < constants_1.REFERENCE_MAX_SETS; i++) {
|
|
267
|
+
const setDecay = 1 / (1 + constants_1.SET_FATIGUE_DECAY_RATE * i);
|
|
268
|
+
totalWithDecay += setDecay;
|
|
269
|
+
}
|
|
270
|
+
return (singleSetMax * constants_1.REFERENCE_MAX_EFFORT * totalWithDecay * muscleGroupFactor);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Normalize raw fatigue values to 0–100 scale using the reference maximum.
|
|
274
|
+
*
|
|
275
|
+
* muscleGroupFactor is already baked into the reference max, so it
|
|
276
|
+
* naturally adjusts: exercises that engage more/larger muscles (higher factor)
|
|
277
|
+
* have a proportionally higher reference max, keeping scores fair.
|
|
278
|
+
*/
|
|
279
|
+
function normalizeScores(rawFatigue, referenceMax) {
|
|
280
|
+
if (referenceMax <= 0)
|
|
281
|
+
return rawFatigue;
|
|
282
|
+
const normalized = {};
|
|
283
|
+
const keys = Object.keys(rawFatigue);
|
|
284
|
+
for (let i = 0; i < keys.length; i++) {
|
|
285
|
+
const muscle = keys[i];
|
|
286
|
+
const raw = rawFatigue[muscle];
|
|
287
|
+
normalized[muscle] = Math.round((0, helpers_1.clamp)((raw / referenceMax) * 100, 0, 100));
|
|
288
|
+
}
|
|
289
|
+
return normalized;
|
|
290
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Pillar 3: Quality Score
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Measures HOW WELL the user performed the exercise (0–100).
|
|
7
|
+
* This is NOT about volume or calories — those are separate pillars.
|
|
8
|
+
* Quality is about execution: did you finish, stay consistent, push hard
|
|
9
|
+
* enough, and rest appropriately?
|
|
10
|
+
*
|
|
11
|
+
* FOUR SUB-COMPONENTS:
|
|
12
|
+
*
|
|
13
|
+
* ┌─────────────────────┬────────┬──────────────────────────────────────────┐
|
|
14
|
+
* │ Component │ Weight │ What it measures │
|
|
15
|
+
* ├─────────────────────┼────────┼──────────────────────────────────────────┤
|
|
16
|
+
* │ Completion │ 20% │ Did you finish all planned sets? │
|
|
17
|
+
* │ Consistency │ 35% │ Were sets stable or intentionally │
|
|
18
|
+
* │ │ │ progressive? (not random drops) │
|
|
19
|
+
* │ Effort Adequacy │ 30% │ Were you in the productive effort zone? │
|
|
20
|
+
* │ │ │ (RPE 6–9 or RIR 1–4) │
|
|
21
|
+
* │ Rest Discipline │ 15% │ Did you respect optimal rest windows? │
|
|
22
|
+
* └─────────────────────┴────────┴──────────────────────────────────────────┘
|
|
23
|
+
*
|
|
24
|
+
* Each sub-component produces 0–100. The final score is a weighted average.
|
|
25
|
+
*
|
|
26
|
+
* DESIGN PRINCIPLES:
|
|
27
|
+
* - Motivational: even a mediocre workout should score 40–60, not 10
|
|
28
|
+
* - Honest: perfect scores require real effort and discipline
|
|
29
|
+
* - Fair across types: cardio and strength use the same framework
|
|
30
|
+
* - Transparent: the breakdown is returned so the UI can explain the score
|
|
31
|
+
*/
|
|
32
|
+
import type { IParsedSet, IQualityBreakdown } from "./types";
|
|
33
|
+
import type { ITimingGuardrails } from "./parseRecords";
|
|
34
|
+
/**
|
|
35
|
+
* Raw record shape — we need the original RPE/RIR strings and isDone flag
|
|
36
|
+
* that aren't in IParsedSet (which only contains completed sets).
|
|
37
|
+
*/
|
|
38
|
+
interface IRawRecord {
|
|
39
|
+
type: string;
|
|
40
|
+
isDone: boolean;
|
|
41
|
+
rpe?: string;
|
|
42
|
+
rir?: string;
|
|
43
|
+
kg?: string;
|
|
44
|
+
reps?: string;
|
|
45
|
+
durationMmSs?: string;
|
|
46
|
+
auxWeightKg?: string;
|
|
47
|
+
speedMin?: string;
|
|
48
|
+
speedMax?: string;
|
|
49
|
+
distance?: string;
|
|
50
|
+
restDurationSecs?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Calculate the overall quality score and its breakdown.
|
|
54
|
+
*
|
|
55
|
+
* @param parsedSets Cleaned sets (from parseRecords) — only completed sets
|
|
56
|
+
* @param rawRecords Original TRecord[] — needed for completion count (includes skipped)
|
|
57
|
+
* @param timingGuardrails Exercise's guardrails — for rest period validation
|
|
58
|
+
* @returns { score: 0–100, breakdown: { completion, consistency, effortAdequacy, restDiscipline } }
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* const { score, breakdown } = calculateQualityScore(parsed, raw, guardrails);
|
|
62
|
+
* // score: 81
|
|
63
|
+
* // breakdown: { completion: 100, consistency: 75, effortAdequacy: 80, restDiscipline: 73 }
|
|
64
|
+
*/
|
|
65
|
+
export declare function calculateQualityScore(parsedSets: IParsedSet[], rawRecords: IRawRecord[], timingGuardrails?: ITimingGuardrails): {
|
|
66
|
+
score: number;
|
|
67
|
+
breakdown: IQualityBreakdown;
|
|
68
|
+
};
|
|
69
|
+
export {};
|