@dgpholdings/greatoak-shared 1.2.15 → 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.
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ // calculateQualityScore.ts
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.calculateQualityScore = calculateQualityScore;
5
+ const constants_1 = require("./constants");
6
+ const helpers_1 = require("./helpers");
7
+ // ---------------------------------------------------------------------------
8
+ // Main Quality Calculation
9
+ // ---------------------------------------------------------------------------
10
+ /**
11
+ * Calculate the overall quality score and its breakdown.
12
+ *
13
+ * @param parsedSets Cleaned sets (from parseRecords) — only completed sets
14
+ * @param rawRecords Original TRecord[] — needed for completion count (includes skipped)
15
+ * @param timingGuardrails Exercise's guardrails — for rest period validation
16
+ * @returns { score: 0–100, breakdown: { completion, consistency, effortAdequacy, restDiscipline } }
17
+ *
18
+ * @example
19
+ * const { score, breakdown } = calculateQualityScore(parsed, raw, guardrails);
20
+ * // score: 81
21
+ * // breakdown: { completion: 100, consistency: 75, effortAdequacy: 80, restDiscipline: 73 }
22
+ */
23
+ function calculateQualityScore(parsedSets, rawRecords, timingGuardrails) {
24
+ // Edge case: no records at all
25
+ if (rawRecords.length === 0) {
26
+ return {
27
+ score: 0,
28
+ breakdown: {
29
+ completion: 0,
30
+ consistency: 0,
31
+ effortAdequacy: 0,
32
+ restDiscipline: 0,
33
+ },
34
+ };
35
+ }
36
+ // Edge case: records exist but NONE were completed
37
+ // If you didn't do a single set, the score is 0 — no partial credit
38
+ // from consistency/rest defaults
39
+ if (parsedSets.length === 0) {
40
+ return {
41
+ score: 0,
42
+ breakdown: {
43
+ completion: 0,
44
+ consistency: 0,
45
+ effortAdequacy: 0,
46
+ restDiscipline: 0,
47
+ },
48
+ };
49
+ }
50
+ const completion = calculateCompletion(parsedSets, rawRecords);
51
+ const consistency = calculateConsistency(parsedSets);
52
+ const effortAdequacy = calculateEffortAdequacy(rawRecords);
53
+ const restDiscipline = calculateRestDiscipline(parsedSets, timingGuardrails);
54
+ const score = Math.round(completion * constants_1.QUALITY_WEIGHTS.completion +
55
+ consistency * constants_1.QUALITY_WEIGHTS.consistency +
56
+ effortAdequacy * constants_1.QUALITY_WEIGHTS.effortAdequacy +
57
+ restDiscipline * constants_1.QUALITY_WEIGHTS.restDiscipline);
58
+ return {
59
+ score: (0, helpers_1.clamp)(score, 0, 100),
60
+ breakdown: {
61
+ completion: Math.round(completion),
62
+ consistency: Math.round(consistency),
63
+ effortAdequacy: Math.round(effortAdequacy),
64
+ restDiscipline: Math.round(restDiscipline),
65
+ },
66
+ };
67
+ }
68
+ // ---------------------------------------------------------------------------
69
+ // Component A: Completion (0–100)
70
+ // ---------------------------------------------------------------------------
71
+ /**
72
+ * Did the user complete all their sets?
73
+ *
74
+ * Simple ratio: completedSets / totalSets × 100
75
+ *
76
+ * This rewards finishing what you started. Skipping sets tanks the score.
77
+ *
78
+ * Uses rawRecords.length as total (includes isDone:false) and
79
+ * parsedSets.length as completed (only isDone:true after filtering).
80
+ */
81
+ function calculateCompletion(parsedSets, rawRecords) {
82
+ const total = rawRecords.length;
83
+ if (total === 0)
84
+ return 0;
85
+ const completed = parsedSets.length;
86
+ return (completed / total) * 100;
87
+ }
88
+ // ---------------------------------------------------------------------------
89
+ // Component B: Consistency (0–100)
90
+ // ---------------------------------------------------------------------------
91
+ /**
92
+ * Were sets consistent in performance output?
93
+ *
94
+ * Measures the coefficient of variation (CV) of the per-set "output metric".
95
+ * CV = 0 → perfectly consistent → score 100
96
+ * CV = 0.50 → very variable → score ~0 (clamped to minimum)
97
+ *
98
+ * The "output metric" depends on exercise type:
99
+ * weight-reps: kg × reps (volume per set)
100
+ * reps-only: reps (aux weight change is intentional, so just reps)
101
+ * duration: durationSecs
102
+ * cardio-machine: avg speed (speedMin + speedMax) / 2
103
+ * cardio-free: effective speed (distance / time)
104
+ *
105
+ * SPECIAL CASE — Progressive overload detection:
106
+ * If values are monotonically increasing (warming up / pyramiding UP)
107
+ * or monotonically decreasing (drop sets / fatigue), this is INTENTIONAL.
108
+ * We don't penalize it — instead, we floor the score at 75.
109
+ * Requires ≥ 3 sets to detect (2 sets are always monotonic).
110
+ *
111
+ * SINGLE SET: Returns 100 (nothing to compare against).
112
+ */
113
+ function calculateConsistency(parsedSets) {
114
+ if (parsedSets.length <= 1)
115
+ return 100;
116
+ const values = parsedSets.map(extractConsistencyMetric);
117
+ // Filter out zero/invalid values
118
+ const validValues = values.filter((v) => v > 0);
119
+ if (validValues.length <= 1)
120
+ return 100;
121
+ // Calculate coefficient of variation
122
+ const cv = (0, helpers_1.coefficientOfVariation)(validValues);
123
+ // Base score: penalize variance
124
+ let score = (0, helpers_1.clamp)(100 - cv * constants_1.CONSISTENCY_CV_PENALTY, constants_1.CONSISTENCY_MIN_SCORE, 100);
125
+ // Progressive overload / intentional progression detection
126
+ if (validValues.length >= 3) {
127
+ const isMonotonicallyIncreasing = isMonotonic(validValues, "asc");
128
+ const isMonotonicallyDecreasing = isMonotonic(validValues, "desc");
129
+ if (isMonotonicallyIncreasing || isMonotonicallyDecreasing) {
130
+ score = Math.max(score, constants_1.PROGRESSIVE_OVERLOAD_FLOOR);
131
+ }
132
+ }
133
+ return score;
134
+ }
135
+ /**
136
+ * Extract the consistency metric for a set based on its type.
137
+ */
138
+ function extractConsistencyMetric(set) {
139
+ var _a, _b, _c, _d, _e, _f, _g, _h;
140
+ switch (set.type) {
141
+ case "weight-reps": {
142
+ // Volume per set: weight × reps
143
+ const kg = (_a = set.kg) !== null && _a !== void 0 ? _a : 0;
144
+ const reps = (_b = set.reps) !== null && _b !== void 0 ? _b : 0;
145
+ return kg * reps;
146
+ }
147
+ case "reps-only": {
148
+ // Just reps (aux weight changes are intentional, not inconsistency)
149
+ return (_c = set.reps) !== null && _c !== void 0 ? _c : 0;
150
+ }
151
+ case "duration": {
152
+ // Hold duration
153
+ return (_d = set.durationSecs) !== null && _d !== void 0 ? _d : set.activeDurationSecs;
154
+ }
155
+ case "cardio-machine": {
156
+ // Average speed
157
+ const speedMin = (_e = set.speedMin) !== null && _e !== void 0 ? _e : 0;
158
+ const speedMax = (_f = set.speedMax) !== null && _f !== void 0 ? _f : 0;
159
+ return (speedMin + speedMax) / 2;
160
+ }
161
+ case "cardio-free": {
162
+ // Effective speed (km/h)
163
+ const distance = (_g = set.distance) !== null && _g !== void 0 ? _g : 0;
164
+ const durationSecs = (_h = set.cardioDurationSecs) !== null && _h !== void 0 ? _h : set.activeDurationSecs;
165
+ if (durationSecs > 0 && distance > 0) {
166
+ return distance / (durationSecs / 3600);
167
+ }
168
+ return 0;
169
+ }
170
+ default:
171
+ return 0;
172
+ }
173
+ }
174
+ /**
175
+ * Check if an array of numbers is monotonically increasing or decreasing.
176
+ * "Allowing equal" — [10, 10, 12] counts as ascending.
177
+ */
178
+ function isMonotonic(values, direction) {
179
+ for (let i = 1; i < values.length; i++) {
180
+ if (direction === "asc" && values[i] < values[i - 1])
181
+ return false;
182
+ if (direction === "desc" && values[i] > values[i - 1])
183
+ return false;
184
+ }
185
+ return true;
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // Component C: Effort Adequacy (0–100)
189
+ // ---------------------------------------------------------------------------
190
+ /**
191
+ * Was the user working in a productive effort zone?
192
+ *
193
+ * The "productive zone" for most training goals:
194
+ * RIR 1–4: close enough to failure to stimulate adaptation, not so close
195
+ * that recovery is impaired
196
+ * RPE 6–9: same concept from the subjective side
197
+ *
198
+ * Scoring per set:
199
+ * - Within optimal range → 100
200
+ * - Outside by 1 unit → 80
201
+ * - Outside by 2 units → 60
202
+ * - Outside by 3+ units → 40/20
203
+ *
204
+ * If neither RPE nor RIR is provided → 70 (neutral, can't confirm or deny).
205
+ *
206
+ * Uses RAW records (not parsed sets) because:
207
+ * 1. We need the original RPE/RIR strings
208
+ * 2. We only score completed sets (isDone === true)
209
+ */
210
+ function calculateEffortAdequacy(rawRecords) {
211
+ const completedRecords = rawRecords.filter((r) => r.isDone);
212
+ if (completedRecords.length === 0)
213
+ return 0;
214
+ const setScores = [];
215
+ for (const record of completedRecords) {
216
+ setScores.push(scoreSetEffort(record));
217
+ }
218
+ // Average all set effort scores
219
+ return setScores.reduce((sum, s) => sum + s, 0) / setScores.length;
220
+ }
221
+ /**
222
+ * Score a single set's effort level.
223
+ *
224
+ * Priority: RIR > RPE > no data.
225
+ * For RIR: optimal range [1, 4]
226
+ * For RPE: optimal range [6, 9]
227
+ */
228
+ function scoreSetEffort(record) {
229
+ // Try RIR first (more objective)
230
+ if (record.rir !== undefined && record.rir !== "") {
231
+ const rir = (0, helpers_1.safeParseFloat)(record.rir, -1);
232
+ if (rir >= 0) {
233
+ return scoreAgainstRange(rir, constants_1.OPTIMAL_RIR_RANGE);
234
+ }
235
+ }
236
+ // Try RPE
237
+ if (record.rpe !== undefined && record.rpe !== "") {
238
+ const rpe = (0, helpers_1.safeParseFloat)(record.rpe, -1);
239
+ if (rpe >= 1) {
240
+ return scoreAgainstRange(rpe, constants_1.OPTIMAL_RPE_RANGE);
241
+ }
242
+ }
243
+ // No effort data — neutral score
244
+ return constants_1.EFFORT_NO_DATA_SCORE;
245
+ }
246
+ /**
247
+ * Score a value against an optimal range.
248
+ *
249
+ * Within [min, max] → 100
250
+ * Distance 1 outside → 100 - penalty
251
+ * Distance 2 outside → 100 - 2×penalty
252
+ * etc., floored at EFFORT_MIN_SCORE
253
+ */
254
+ function scoreAgainstRange(value, range) {
255
+ const [min, max] = range;
256
+ if (value >= min && value <= max)
257
+ return 100;
258
+ // Distance from nearest edge of the range
259
+ const distance = value < min ? min - value : value - max;
260
+ return Math.max(100 - distance * constants_1.EFFORT_DISTANCE_PENALTY, constants_1.EFFORT_MIN_SCORE);
261
+ }
262
+ // ---------------------------------------------------------------------------
263
+ // Component D: Rest Discipline (0–100)
264
+ // ---------------------------------------------------------------------------
265
+ /**
266
+ * Were rest periods within the exercise's optimal windows?
267
+ *
268
+ * Uses the exercise's timingGuardrails.restPeriods:
269
+ * optimalRange [min, max] → 100 points
270
+ * within [minimum, maximum] → 60–100 (proportional to how close to optimal)
271
+ * outside [minimum, maximum] → 30 points
272
+ *
273
+ * Only scores sets that HAVE rest data (last set often doesn't).
274
+ * If no valid rest data exists at all → neutral 65.
275
+ *
276
+ * The stressRestBonus from timingGuardrails is applied as a final multiplier:
277
+ * exercises where proper rest is especially important (heavy compounds)
278
+ * get a boost when rest is done right.
279
+ */
280
+ function calculateRestDiscipline(parsedSets, timingGuardrails) {
281
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
282
+ // Gather sets that have rest data (non-null)
283
+ const setsWithRest = parsedSets.filter((s) => s.restDurationSecs !== null);
284
+ if (setsWithRest.length === 0)
285
+ return constants_1.REST_NO_DATA_SCORE;
286
+ // Extract rest period bounds from guardrails
287
+ const restConfig = timingGuardrails === null || timingGuardrails === void 0 ? void 0 : timingGuardrails.restPeriods;
288
+ const optimalMin = (_b = (_a = restConfig === null || restConfig === void 0 ? void 0 : restConfig.optimalRange) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : ((_c = restConfig === null || restConfig === void 0 ? void 0 : restConfig.typical) !== null && _c !== void 0 ? _c : constants_1.FALLBACK_REST_SECS) * 0.75;
289
+ const optimalMax = (_e = (_d = restConfig === null || restConfig === void 0 ? void 0 : restConfig.optimalRange) === null || _d === void 0 ? void 0 : _d[1]) !== null && _e !== void 0 ? _e : ((_f = restConfig === null || restConfig === void 0 ? void 0 : restConfig.typical) !== null && _f !== void 0 ? _f : constants_1.FALLBACK_REST_SECS) * 1.25;
290
+ const acceptableMin = (_g = restConfig === null || restConfig === void 0 ? void 0 : restConfig.minimum) !== null && _g !== void 0 ? _g : optimalMin * 0.5;
291
+ const acceptableMax = (_h = restConfig === null || restConfig === void 0 ? void 0 : restConfig.maximum) !== null && _h !== void 0 ? _h : optimalMax * 2;
292
+ const setScores = [];
293
+ for (const set of setsWithRest) {
294
+ const rest = set.restDurationSecs; // Guaranteed non-null by filter
295
+ if (rest >= optimalMin && rest <= optimalMax) {
296
+ // Within optimal range → perfect score
297
+ setScores.push(constants_1.REST_OPTIMAL_SCORE);
298
+ }
299
+ else if (rest >= acceptableMin && rest <= acceptableMax) {
300
+ // Within acceptable range but not optimal
301
+ // Score proportionally based on how close to optimal
302
+ let ratio;
303
+ if (rest < optimalMin) {
304
+ // Between acceptable min and optimal min
305
+ ratio = (rest - acceptableMin) / (optimalMin - acceptableMin);
306
+ }
307
+ else {
308
+ // Between optimal max and acceptable max
309
+ ratio = (acceptableMax - rest) / (acceptableMax - optimalMax);
310
+ }
311
+ setScores.push(constants_1.REST_ACCEPTABLE_BASE +
312
+ ratio * (constants_1.REST_OPTIMAL_SCORE - constants_1.REST_ACCEPTABLE_BASE));
313
+ }
314
+ else {
315
+ // Outside even the acceptable range
316
+ setScores.push(constants_1.REST_OUTSIDE_SCORE);
317
+ }
318
+ }
319
+ // Average rest scores
320
+ let avgScore = setScores.reduce((sum, s) => sum + s, 0) / setScores.length;
321
+ // Apply stressRestBonus: exercises where proper rest matters more
322
+ // get a boost (capped at 100). E.g., bench press stressRestBonus=1.3
323
+ // means good rest is 30% more valuable for this exercise.
324
+ // We only apply this as a REWARD (when score > 60), not to amplify bad rest.
325
+ const stressRestBonus = (_j = timingGuardrails === null || timingGuardrails === void 0 ? void 0 : timingGuardrails.stressRestBonus) !== null && _j !== void 0 ? _j : 1.0;
326
+ if (avgScore > 60 && stressRestBonus > 1.0) {
327
+ // Scale the "above baseline" portion by the bonus
328
+ const baseline = 60;
329
+ const aboveBaseline = avgScore - baseline;
330
+ avgScore = baseline + aboveBaseline * stressRestBonus;
331
+ }
332
+ return (0, helpers_1.clamp)(avgScore, 0, 100);
333
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX EXERCISE SCORING SYSTEM — Constants
4
+ * ============================================================================
5
+ *
6
+ * All magic numbers, default values, and tuning parameters live here.
7
+ * When we need to adjust scoring behavior, this is the ONLY file to change.
8
+ *
9
+ * Naming convention:
10
+ * DEFAULT_* = user-related fallbacks (weight, height, age)
11
+ * FALLBACK_* = exercise timing fallbacks (only when timingGuardrails is missing)
12
+ * ABSOLUTE_* = hard safety bounds (catches truly broken data)
13
+ * QUALITY_* = quality score weights and thresholds
14
+ * FACTOR_* = multipliers used in formulas
15
+ */
16
+ /** Average adult weight when user hasn't provided theirs */
17
+ export declare const DEFAULT_USER_WEIGHT_KG = 70;
18
+ /** Average adult height */
19
+ export declare const DEFAULT_USER_HEIGHT_CM = 170;
20
+ /** Assumed age when DOB is missing */
21
+ export declare const DEFAULT_USER_AGE = 30;
22
+ /** Seconds per rep when timingGuardrails.singleRep is missing entirely */
23
+ export declare const FALLBACK_SECS_PER_REP = 3;
24
+ /** Set duration when timingGuardrails.setDuration is missing entirely */
25
+ export declare const FALLBACK_SET_DURATION_SECS = 30;
26
+ /** Rest period when timingGuardrails.restPeriods is missing entirely */
27
+ export declare const FALLBACK_REST_SECS = 90;
28
+ /** Fatigue multiplier when timingGuardrails.fatigueMultiplier is missing */
29
+ export declare const FALLBACK_FATIGUE_MULTIPLIER = 1;
30
+ /** Stress rest bonus when timingGuardrails.stressRestBonus is missing */
31
+ export declare const FALLBACK_STRESS_REST_BONUS = 1;
32
+ /**
33
+ * Absolute sanity bounds — these catch truly absurd sensor/input errors
34
+ * that even the exercise's own guardrails wouldn't expect.
35
+ * These are NOT the exercise's min/max; they're a safety net.
36
+ *
37
+ * Example: a restDurationSecs of 86400 (24 hours) is clearly a bug,
38
+ * regardless of what the exercise's guardrails say.
39
+ */
40
+ export declare const ABSOLUTE_REST_MIN = 0;
41
+ export declare const ABSOLUTE_REST_MAX = 3600;
42
+ export declare const ABSOLUTE_WORK_MIN = 1;
43
+ export declare const ABSOLUTE_WORK_MAX = 14400;
44
+ /**
45
+ * Effort fraction when neither RPE nor RIR is provided.
46
+ * 0.6 = moderate effort assumption.
47
+ */
48
+ export declare const DEFAULT_EFFORT_FRACTION = 0.6;
49
+ /**
50
+ * Exponent for non-linear RPE/RIR → effort conversion.
51
+ * Values > 1.0 make the last reps near failure disproportionately harder,
52
+ * which matches real physiology (RPE 9→10 is a bigger jump than 5→6).
53
+ */
54
+ export declare const EFFORT_CURVE_EXPONENT = 1.3;
55
+ /**
56
+ * Effort fraction output range.
57
+ * effort = BASE + RANGE × (normalized_input ^ EXPONENT)
58
+ */
59
+ export declare const EFFORT_FRACTION_BASE = 0.5;
60
+ export declare const EFFORT_FRACTION_RANGE = 0.8;
61
+ /**
62
+ * MET value during rest periods.
63
+ * Not true resting (1.0) because the body is still elevated after a set.
64
+ * 1.5 accounts for elevated heart rate and recovery metabolism.
65
+ */
66
+ export declare const REST_MET = 1.5;
67
+ /**
68
+ * Default weight intensity multipliers when metabolicData.weightFactors
69
+ * is missing. Keyed by intensity category.
70
+ */
71
+ export declare const DEFAULT_WEIGHT_FACTORS: {
72
+ light: number;
73
+ moderate: number;
74
+ heavy: number;
75
+ };
76
+ /** Thresholds for weight category classification (as ratio of bodyweight) */
77
+ export declare const WEIGHT_CATEGORY_THRESHOLDS: {
78
+ lightMax: number;
79
+ moderateMax: number;
80
+ };
81
+ /**
82
+ * Default duration multipliers when metabolicData.durationFactors is missing.
83
+ */
84
+ export declare const DEFAULT_DURATION_FACTORS: {
85
+ short: number;
86
+ medium: number;
87
+ long: number;
88
+ };
89
+ /** Thresholds for duration category classification (in seconds) */
90
+ export declare const DURATION_CATEGORY_THRESHOLDS: {
91
+ shortMax: number;
92
+ mediumMax: number;
93
+ };
94
+ /**
95
+ * For reps-only / bodyweight exercises: how much of bodyweight is "the load".
96
+ * Scaled by exercise difficultyLevel (0–4):
97
+ * load = (difficultyLevel / 4) × BW_FRACTION_SCALE × userWeightKg
98
+ *
99
+ * difficulty 0 → 0.0 × 0.65 = 0% (basically no BW load, e.g. finger exercise)
100
+ * difficulty 1 → 0.25 × 0.65 = 16% (light BW, e.g. easy crunches)
101
+ * difficulty 2 → 0.50 × 0.65 = 33% (moderate, e.g. push-ups)
102
+ * difficulty 4 → 1.00 × 0.65 = 65% (heavy, e.g. pistol squats)
103
+ */
104
+ export declare const BW_FRACTION_SCALE = 0.65;
105
+ /**
106
+ * Expected min/max speed range for cardio (km/h) when paceFactors are missing.
107
+ * Used to normalize speed into a 0–1 fraction for MET interpolation.
108
+ */
109
+ export declare const CARDIO_SPEED_RANGE: {
110
+ min: number;
111
+ max: number;
112
+ };
113
+ /** Fraction of total fatigue stimulus allocated to primary muscles */
114
+ export declare const PRIMARY_MUSCLE_ALLOCATION = 1;
115
+ /** Fraction of total fatigue stimulus allocated to secondary muscles */
116
+ export declare const SECONDARY_MUSCLE_ALLOCATION = 0.35;
117
+ /**
118
+ * Diminishing returns decay rate for consecutive sets.
119
+ * setDecay = 1 / (1 + DECAY_RATE × setIndex)
120
+ *
121
+ * With 0.15: set0=1.00, set1=0.87, set2=0.77, set3=0.69, set4=0.63
122
+ */
123
+ export declare const SET_FATIGUE_DECAY_RATE = 0.15;
124
+ /**
125
+ * Cardio exercises create cardiovascular fatigue, not the same mechanical
126
+ * muscle fatigue as strength training. A 30-minute jog doesn't fatigue
127
+ * your quads the way 5 sets of heavy squats does.
128
+ *
129
+ * This dampener reduces the volume-load contribution of cardio exercises
130
+ * so their muscle fatigue scores are proportional to actual muscle damage/stress.
131
+ *
132
+ * 0.3 means cardio produces ~30% of the muscle fatigue that an equivalent
133
+ * "volume" of strength work would.
134
+ */
135
+ export declare const CARDIO_MUSCLE_FATIGUE_DAMPENER = 0.3;
136
+ /**
137
+ * Reference max scaling by difficulty level.
138
+ * Maps difficultyLevel (0–4) → expected max weight as a fraction of bodyweight.
139
+ *
140
+ * This makes fatigue normalization exercise-aware:
141
+ * - Bicep curl (difficulty 1) → ref weight ~0.6× BW → smaller denominator → fair score
142
+ * - Squat (difficulty 3) → ref weight ~1.2× BW → larger denominator → fair score
143
+ */
144
+ export declare const DIFFICULTY_TO_WEIGHT_FRACTION: (difficulty: number) => number;
145
+ /** Reference set count for "fully fatigued" benchmark */
146
+ export declare const REFERENCE_MAX_SETS = 5;
147
+ /** Reference rep count for "fully fatigued" benchmark */
148
+ export declare const REFERENCE_MAX_REPS = 12;
149
+ /** Reference effort multiplier for "fully fatigued" benchmark */
150
+ export declare const REFERENCE_MAX_EFFORT = 1.3;
151
+ /**
152
+ * Weights for the four quality sub-components.
153
+ * Must sum to 1.0.
154
+ */
155
+ export declare const QUALITY_WEIGHTS: {
156
+ completion: number;
157
+ consistency: number;
158
+ effortAdequacy: number;
159
+ restDiscipline: number;
160
+ };
161
+ /**
162
+ * Consistency scoring: how aggressively CV (coefficient of variation) is penalized.
163
+ * consistencyScore = 100 - CV × PENALTY
164
+ * CV 0.0 → 100, CV 0.15 → 70, CV 0.50 → 0
165
+ */
166
+ export declare const CONSISTENCY_CV_PENALTY = 200;
167
+ /** Minimum consistency score (even very inconsistent sets get some credit) */
168
+ export declare const CONSISTENCY_MIN_SCORE = 20;
169
+ /** Floor score for intentional progressive/descending sets (≥3 sets) */
170
+ export declare const PROGRESSIVE_OVERLOAD_FLOOR = 75;
171
+ /**
172
+ * Optimal RIR range for effort adequacy scoring.
173
+ * Within this range → 100 points. Outside → penalized by distance.
174
+ */
175
+ export declare const OPTIMAL_RIR_RANGE: [number, number];
176
+ /** Optimal RPE range for effort adequacy scoring. */
177
+ export declare const OPTIMAL_RPE_RANGE: [number, number];
178
+ /** Penalty per unit of distance from the optimal effort range */
179
+ export declare const EFFORT_DISTANCE_PENALTY = 20;
180
+ /** Minimum effort adequacy score */
181
+ export declare const EFFORT_MIN_SCORE = 20;
182
+ /** Default effort adequacy score when no RPE/RIR data is available */
183
+ export declare const EFFORT_NO_DATA_SCORE = 70;
184
+ /** Score when rest is within the optimal range */
185
+ export declare const REST_OPTIMAL_SCORE = 100;
186
+ /** Score when rest is within acceptable (min–max) but not optimal */
187
+ export declare const REST_ACCEPTABLE_BASE = 60;
188
+ /** Score when rest is completely outside the acceptable range */
189
+ export declare const REST_OUTSIDE_SCORE = 30;
190
+ /** Default rest discipline score when no valid rest data exists */
191
+ export declare const REST_NO_DATA_SCORE = 65;