@dgpholdings/greatoak-shared 1.2.86 → 1.2.87
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/README.md +148 -148
- package/dist/__mocks__/exercises.mock.js +1 -0
- package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
- package/dist/types/TApiClientConstellation.d.ts +33 -0
- package/dist/types/TApiClientConstellation.js +13 -0
- package/dist/types/TApiExercise.d.ts +5 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
- package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
- package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
- package/dist/utils/constellation/evaluateConstellation.js +135 -0
- package/dist/utils/constellation/index.d.ts +17 -0
- package/dist/utils/constellation/index.js +26 -0
- package/dist/utils/constellation/levelThresholds.d.ts +99 -0
- package/dist/utils/constellation/levelThresholds.js +123 -0
- package/dist/utils/constellation/starFoundation.d.ts +25 -0
- package/dist/utils/constellation/starFoundation.js +54 -0
- package/dist/utils/constellation/stars/consistency.d.ts +29 -0
- package/dist/utils/constellation/stars/consistency.js +142 -0
- package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
- package/dist/utils/constellation/stars/lowerBody.js +30 -0
- package/dist/utils/constellation/stars/pull.d.ts +11 -0
- package/dist/utils/constellation/stars/pull.js +24 -0
- package/dist/utils/constellation/stars/push.d.ts +11 -0
- package/dist/utils/constellation/stars/push.js +24 -0
- package/dist/utils/constellation/stars/quality.d.ts +19 -0
- package/dist/utils/constellation/stars/quality.js +98 -0
- package/dist/utils/constellation/stars/recovery.d.ts +29 -0
- package/dist/utils/constellation/stars/recovery.js +169 -0
- package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
- package/dist/utils/constellation/strengthStarHelpers.js +104 -0
- package/dist/utils/constellation/types.d.ts +124 -0
- package/dist/utils/constellation/types.js +18 -0
- package/dist/utils/index.d.ts +5 -3
- package/dist/utils/index.js +1 -0
- package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
- package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
- package/dist/utils/scoringWorkout/constants.d.ts +20 -6
- package/dist/utils/scoringWorkout/constants.js +23 -9
- package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
- package/dist/utils/scoringWorkout/helpers.js +24 -18
- package/dist/utils/scoringWorkout/index.d.ts +12 -8
- package/dist/utils/scoringWorkout/index.js +23 -15
- package/dist/utils/scoringWorkout/parseRecords.js +4 -3
- package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +210 -172
- package/dist/utils/scoringWorkout/types.d.ts +34 -14
- package/package.json +31 -31
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
- package/dist/utils/scaleProPlan.util.d.ts +0 -9
- package/dist/utils/scaleProPlan.util.js +0 -139
- package/dist/utils/scoring/calculateCalories.d.ts +0 -67
- package/dist/utils/scoring/calculateCalories.js +0 -345
- package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
- package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
- package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
- package/dist/utils/scoring/calculateQualityScore.js +0 -334
- package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
- package/dist/utils/scoring/calculateTotalVolume.js +0 -73
- package/dist/utils/scoring/constants.d.ts +0 -211
- package/dist/utils/scoring/constants.js +0 -247
- package/dist/utils/scoring/helpers.d.ts +0 -119
- package/dist/utils/scoring/helpers.js +0 -229
- package/dist/utils/scoring/index.d.ts +0 -28
- package/dist/utils/scoring/index.js +0 -47
- package/dist/utils/scoring/parseRecords.d.ts +0 -98
- package/dist/utils/scoring/parseRecords.js +0 -284
- package/dist/utils/scoring/types.d.ts +0 -86
- package/dist/utils/scoring/types.js +0 -11
- package/dist/utils/scoring.utils.d.ts +0 -14
- package/dist/utils/scoring.utils.js +0 -243
- /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
- /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.js → calculateMuscleFatigue.js} +0 -0
|
@@ -11,17 +11,22 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Recovery model accounts for:
|
|
13
13
|
* P2-7 Age — recovery slows with age (< 35 → 1.0×, > 55 → 0.85×)
|
|
14
|
-
* P2-8 Gender — females recover slightly faster (1.05×)
|
|
15
14
|
* P2-9 fitnessLevel — sedentary 0.8× → very-active 1.15×
|
|
16
15
|
* P3-7 Weekly volume — muscles trained multiple times this week recover slower
|
|
17
16
|
* P3-8 Muscle group — small muscles (biceps) recover 1.4× faster than legs (0.80×)
|
|
18
17
|
*
|
|
19
18
|
* Accumulation model:
|
|
20
|
-
* P3-3 Real muscleScores → additive with diminishing returns (P3-5)
|
|
21
|
-
*
|
|
19
|
+
* P3-3 Real per-session muscleScores → additive with diminishing returns (P3-5)
|
|
20
|
+
*
|
|
21
|
+
* Note: the legacy quality-score proxy path (pre-P3-1 sessions with empty
|
|
22
|
+
* muscleScores) has been removed — every session now saves real muscleScores.
|
|
23
|
+
* The P2-8 gender recovery factor has also been removed: the 1.05× female
|
|
24
|
+
* bump was within the noise of the model and not well-evidenced enough to
|
|
25
|
+
* justify a silent default-male assumption for unmentioned-gender users.
|
|
22
26
|
*/
|
|
23
27
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
28
|
exports.computeMuscleFatigueMap = computeMuscleFatigueMap;
|
|
29
|
+
const helpers_1 = require("./helpers");
|
|
25
30
|
// ---------------------------------------------------------------------------
|
|
26
31
|
// P2-7: Age-adjusted recovery multiplier
|
|
27
32
|
// ---------------------------------------------------------------------------
|
|
@@ -29,26 +34,20 @@ function getAgeRecoveryMultiplier(age) {
|
|
|
29
34
|
if (age < 25)
|
|
30
35
|
return 1.05;
|
|
31
36
|
if (age < 35)
|
|
32
|
-
return 1.
|
|
37
|
+
return 1.0;
|
|
33
38
|
if (age < 45)
|
|
34
39
|
return 0.95;
|
|
35
40
|
if (age < 55)
|
|
36
|
-
return 0.
|
|
41
|
+
return 0.9;
|
|
37
42
|
return 0.85;
|
|
38
43
|
}
|
|
39
44
|
// ---------------------------------------------------------------------------
|
|
40
|
-
// P2-8: Gender-adjusted recovery multiplier
|
|
41
|
-
// ---------------------------------------------------------------------------
|
|
42
|
-
function getGenderRecoveryMultiplier(gender) {
|
|
43
|
-
return gender === "female" ? 1.05 : 1.00;
|
|
44
|
-
}
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
45
|
// P2-9: Fitness-level recovery multiplier
|
|
47
46
|
// ---------------------------------------------------------------------------
|
|
48
47
|
const FITNESS_RECOVERY_SCALE = {
|
|
49
|
-
sedentary: 0.
|
|
50
|
-
"lightly-active": 0.
|
|
51
|
-
"moderately-active": 1.
|
|
48
|
+
sedentary: 0.8,
|
|
49
|
+
"lightly-active": 0.9,
|
|
50
|
+
"moderately-active": 1.0,
|
|
52
51
|
"very-active": 1.15,
|
|
53
52
|
};
|
|
54
53
|
// ---------------------------------------------------------------------------
|
|
@@ -57,22 +56,34 @@ const FITNESS_RECOVERY_SCALE = {
|
|
|
57
56
|
// ---------------------------------------------------------------------------
|
|
58
57
|
const MUSCLE_RECOVERY_RATE = {
|
|
59
58
|
// Core — very fast
|
|
60
|
-
"abs-upper": 1.8,
|
|
59
|
+
"abs-upper": 1.8,
|
|
60
|
+
"abs-lower": 1.8,
|
|
61
|
+
obliques: 1.8,
|
|
61
62
|
// Small arm muscles — fast
|
|
62
|
-
"bicep-short-inner": 1.4,
|
|
63
|
-
"
|
|
64
|
-
"
|
|
63
|
+
"bicep-short-inner": 1.4,
|
|
64
|
+
"bicep-long-outer": 1.4,
|
|
65
|
+
"tricep-brachii-long": 1.4,
|
|
66
|
+
"tricep-brachii-lateral": 1.4,
|
|
67
|
+
"fore-arm-inner": 1.5,
|
|
68
|
+
"fore-arm-outer": 1.5,
|
|
65
69
|
// Calves — fast
|
|
66
|
-
"calf-inner": 1.3,
|
|
70
|
+
"calf-inner": 1.3,
|
|
71
|
+
"calf-outer": 1.3,
|
|
67
72
|
// Shoulders / upper back — moderate
|
|
68
|
-
"deltoids-anterior": 1.1,
|
|
69
|
-
"
|
|
73
|
+
"deltoids-anterior": 1.1,
|
|
74
|
+
"deltoids-middle": 1.1,
|
|
75
|
+
trapezius: 1.0,
|
|
76
|
+
rhomboids: 1.0,
|
|
70
77
|
// Chest — moderate
|
|
71
|
-
"pectoralis-major": 1.0,
|
|
78
|
+
"pectoralis-major": 1.0,
|
|
79
|
+
"pectoralis-minor": 1.0,
|
|
72
80
|
// Large compound — slow
|
|
73
81
|
"latissimus-dorsi": 0.85,
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
quadriceps: 0.8,
|
|
83
|
+
hamstrings: 0.8,
|
|
84
|
+
adductors: 0.8,
|
|
85
|
+
"glutes-maximus": 0.8,
|
|
86
|
+
"glutes-medius": 0.85,
|
|
76
87
|
"lower-back": 0.75,
|
|
77
88
|
};
|
|
78
89
|
function getMuscleRecoveryRate(muscle) {
|
|
@@ -98,7 +109,7 @@ function getMuscleRecoveryRate(muscle) {
|
|
|
98
109
|
const WEEKLY_VOLUME_BASELINE = 80; // ~1 normal session score
|
|
99
110
|
const WEEKLY_VOLUME_STEP = 80; // score units per step
|
|
100
111
|
const WEEKLY_VOLUME_REDUCTION_PER_STEP = 0.05; // 5% per additional session
|
|
101
|
-
const WEEKLY_VOLUME_MAX_REDUCTION = 0.
|
|
112
|
+
const WEEKLY_VOLUME_MAX_REDUCTION = 0.4;
|
|
102
113
|
function getWeeklyVolumeMultiplier(muscle, weeklyVolume) {
|
|
103
114
|
var _a;
|
|
104
115
|
const total = (_a = weeklyVolume[muscle]) !== null && _a !== void 0 ? _a : 0;
|
|
@@ -127,14 +138,11 @@ function calculateRecoveryFatigue(originalFatigue, hoursSinceWorkout, recoveryMu
|
|
|
127
138
|
const recovered = originalFatigue * ((recoveryRatePerHour * hoursSinceWorkout) / 100);
|
|
128
139
|
return Math.max(0, originalFatigue - recovered);
|
|
129
140
|
}
|
|
130
|
-
function updateEntry(map, muscle, remainingFatigue, workoutDate
|
|
141
|
+
function updateEntry(map, muscle, remainingFatigue, workoutDate) {
|
|
131
142
|
var _a;
|
|
132
143
|
const existing = (_a = map.get(muscle)) !== null && _a !== void 0 ? _a : { fatigue: 0, lastWorked: workoutDate };
|
|
133
|
-
// P3-5: additive with diminishing returns
|
|
134
|
-
|
|
135
|
-
const newFatigue = useAdditive
|
|
136
|
-
? existing.fatigue + remainingFatigue * (1 - existing.fatigue / 100)
|
|
137
|
-
: Math.max(existing.fatigue, remainingFatigue);
|
|
144
|
+
// P3-5: additive with diminishing returns.
|
|
145
|
+
const newFatigue = existing.fatigue + remainingFatigue * (1 - existing.fatigue / 100);
|
|
138
146
|
map.set(muscle, {
|
|
139
147
|
fatigue: Math.min(100, newFatigue),
|
|
140
148
|
lastWorked: existing.lastWorked > workoutDate ? existing.lastWorked : workoutDate,
|
|
@@ -154,17 +162,15 @@ const RECOVERY_WINDOW_HOURS = 168; // 7 days
|
|
|
154
162
|
* @returns Record<muscleKey, { fatigue 0–100, lastWorked }>
|
|
155
163
|
*/
|
|
156
164
|
function computeMuscleFatigueMap(exercises, scoreHistory, user, currentDate = new Date()) {
|
|
157
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j
|
|
158
|
-
// Build combined user recovery multiplier (P2-7, P2-
|
|
165
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
166
|
+
// Build combined user recovery multiplier (P2-7, P2-9).
|
|
167
|
+
// Age uses the shared deriveAge helper so it is never computed two ways.
|
|
159
168
|
let userRecoveryMultiplier = 1.0;
|
|
160
169
|
if (user) {
|
|
161
|
-
const age = user.dob
|
|
162
|
-
? currentDate.getFullYear() - new Date(user.dob).getFullYear()
|
|
163
|
-
: 30;
|
|
170
|
+
const age = (0, helpers_1.deriveAge)(user.dob, currentDate);
|
|
164
171
|
userRecoveryMultiplier =
|
|
165
172
|
getAgeRecoveryMultiplier(age) *
|
|
166
|
-
|
|
167
|
-
((_c = FITNESS_RECOVERY_SCALE[(_b = user.fitnessLevel) !== null && _b !== void 0 ? _b : "moderately-active"]) !== null && _c !== void 0 ? _c : 1.0);
|
|
173
|
+
((_b = FITNESS_RECOVERY_SCALE[(_a = user.fitnessLevel) !== null && _a !== void 0 ? _a : "moderately-active"]) !== null && _b !== void 0 ? _b : 1.0);
|
|
168
174
|
}
|
|
169
175
|
const cutoffTime = currentDate.getTime() - RECOVERY_WINDOW_HOURS * 3600000;
|
|
170
176
|
const weekCutoff = currentDate.getTime() - 7 * 24 * 3600000; // same as RECOVERY_WINDOW_HOURS
|
|
@@ -180,12 +186,12 @@ function computeMuscleFatigueMap(exercises, scoreHistory, user, currentDate = ne
|
|
|
180
186
|
.reduce((sum, r) => sum + r.score, 0);
|
|
181
187
|
if (weeklyScore === 0)
|
|
182
188
|
continue;
|
|
183
|
-
for (const muscle of (
|
|
184
|
-
weeklyVolume[muscle] = ((
|
|
189
|
+
for (const muscle of (_c = exercise.primaryMuscles) !== null && _c !== void 0 ? _c : []) {
|
|
190
|
+
weeklyVolume[muscle] = ((_d = weeklyVolume[muscle]) !== null && _d !== void 0 ? _d : 0) + weeklyScore;
|
|
185
191
|
}
|
|
186
|
-
for (const muscle of (
|
|
192
|
+
for (const muscle of (_e = exercise.secondaryMuscles) !== null && _e !== void 0 ? _e : []) {
|
|
187
193
|
// Secondary muscles receive 35% of the stimulus
|
|
188
|
-
weeklyVolume[muscle] = ((
|
|
194
|
+
weeklyVolume[muscle] = ((_f = weeklyVolume[muscle]) !== null && _f !== void 0 ? _f : 0) + weeklyScore * 0.35;
|
|
189
195
|
}
|
|
190
196
|
}
|
|
191
197
|
const internalMap = new Map();
|
|
@@ -197,58 +203,36 @@ function computeMuscleFatigueMap(exercises, scoreHistory, user, currentDate = ne
|
|
|
197
203
|
for (const record of relevantRecords) {
|
|
198
204
|
const workoutDate = new Date(record.recordDate);
|
|
199
205
|
const hoursSince = (currentDate.getTime() - workoutDate.getTime()) / 3600000;
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
])
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
else {
|
|
221
|
-
// Legacy path: quality score × 0.7/0.3 proxy (P1-7 band-aid)
|
|
222
|
-
const MIN_MEANINGFUL_SCORE = 88;
|
|
223
|
-
const MAX_SCORE = 100;
|
|
224
|
-
const intensityFraction = record.score <= MIN_MEANINGFUL_SCORE
|
|
225
|
-
? (record.score / MIN_MEANINGFUL_SCORE) * 0.3
|
|
226
|
-
: ((record.score - MIN_MEANINGFUL_SCORE) / (MAX_SCORE - MIN_MEANINGFUL_SCORE)) * 0.7 + 0.3;
|
|
227
|
-
for (const muscle of (_l = exercise.primaryMuscles) !== null && _l !== void 0 ? _l : []) {
|
|
228
|
-
const baseFatigue = record.score * 0.7 * intensityFraction;
|
|
229
|
-
const multiplier = userRecoveryMultiplier
|
|
230
|
-
* getMuscleRecoveryRate(muscle)
|
|
231
|
-
* getWeeklyVolumeMultiplier(muscle, weeklyVolume);
|
|
232
|
-
const remaining = calculateRecoveryFatigue(baseFatigue, hoursSince, multiplier);
|
|
233
|
-
if (remaining > 0)
|
|
234
|
-
updateEntry(internalMap, muscle, remaining, workoutDate, false);
|
|
235
|
-
}
|
|
236
|
-
for (const muscle of (_m = exercise.secondaryMuscles) !== null && _m !== void 0 ? _m : []) {
|
|
237
|
-
const baseFatigue = record.score * 0.3 * intensityFraction;
|
|
238
|
-
const multiplier = userRecoveryMultiplier
|
|
239
|
-
* getMuscleRecoveryRate(muscle)
|
|
240
|
-
* getWeeklyVolumeMultiplier(muscle, weeklyVolume);
|
|
241
|
-
const remaining = calculateRecoveryFatigue(baseFatigue, hoursSince, multiplier);
|
|
242
|
-
if (remaining > 0)
|
|
243
|
-
updateEntry(internalMap, muscle, remaining, workoutDate, false);
|
|
244
|
-
}
|
|
206
|
+
// Real per-session muscleScores is the only path. Skip records that
|
|
207
|
+
// somehow carry none (defensive — should not occur post-P3-1).
|
|
208
|
+
const muscleScores = record.muscleScores;
|
|
209
|
+
if (!muscleScores || Object.keys(muscleScores).length === 0)
|
|
210
|
+
continue;
|
|
211
|
+
const allMuscles = new Set([
|
|
212
|
+
...((_g = exercise.primaryMuscles) !== null && _g !== void 0 ? _g : []),
|
|
213
|
+
...((_h = exercise.secondaryMuscles) !== null && _h !== void 0 ? _h : []),
|
|
214
|
+
]);
|
|
215
|
+
for (const muscle of allMuscles) {
|
|
216
|
+
const baseFatigue = (_j = muscleScores[muscle]) !== null && _j !== void 0 ? _j : 0;
|
|
217
|
+
if (baseFatigue <= 0)
|
|
218
|
+
continue;
|
|
219
|
+
// P3-7: weekly volume slows recovery for heavily-trained muscles
|
|
220
|
+
const multiplier = userRecoveryMultiplier *
|
|
221
|
+
getMuscleRecoveryRate(muscle) *
|
|
222
|
+
getWeeklyVolumeMultiplier(muscle, weeklyVolume);
|
|
223
|
+
const remaining = calculateRecoveryFatigue(baseFatigue, hoursSince, multiplier);
|
|
224
|
+
if (remaining > 0)
|
|
225
|
+
updateEntry(internalMap, muscle, remaining, workoutDate);
|
|
245
226
|
}
|
|
246
227
|
}
|
|
247
228
|
}
|
|
248
229
|
// Convert internal Map → plain object for the public return type
|
|
249
230
|
const result = {};
|
|
250
231
|
internalMap.forEach((entry, muscle) => {
|
|
251
|
-
result[muscle] = {
|
|
232
|
+
result[muscle] = {
|
|
233
|
+
fatigue: Math.round(entry.fatigue),
|
|
234
|
+
lastWorked: entry.lastWorked,
|
|
235
|
+
};
|
|
252
236
|
});
|
|
253
237
|
return result;
|
|
254
238
|
}
|
|
@@ -169,8 +169,12 @@ export declare const REFERENCE_MAX_REPS = 12;
|
|
|
169
169
|
/** Reference effort multiplier for "fully fatigued" benchmark */
|
|
170
170
|
export declare const REFERENCE_MAX_EFFORT = 1.3;
|
|
171
171
|
/**
|
|
172
|
-
* Weights for the four quality sub-components.
|
|
173
|
-
*
|
|
172
|
+
* Weights for the four quality sub-components. Must sum to 1.0.
|
|
173
|
+
*
|
|
174
|
+
* Effort leads (0.40): now that RPE/RIR is captured on every set, effort is
|
|
175
|
+
* the strongest available proxy for whether a session drove adaptation.
|
|
176
|
+
* Consistency demoted (0.20): it is the weakest quality signal and mildly
|
|
177
|
+
* penalises genuine fatigue-driven rep drop-off, so it should not dominate.
|
|
174
178
|
*/
|
|
175
179
|
export declare const QUALITY_WEIGHTS: {
|
|
176
180
|
completion: number;
|
|
@@ -197,10 +201,20 @@ export declare const OPTIMAL_RIR_RANGE: [number, number];
|
|
|
197
201
|
export declare const OPTIMAL_RPE_RANGE: [number, number];
|
|
198
202
|
/** Penalty per unit of distance from the optimal effort range */
|
|
199
203
|
export declare const EFFORT_DISTANCE_PENALTY = 20;
|
|
200
|
-
/**
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Minimum effort adequacy score.
|
|
206
|
+
* Set equal to EFFORT_NO_DATA_SCORE so that logging any RPE/RIR value
|
|
207
|
+
* is never worse than logging nothing. Honest data is always safe to log.
|
|
208
|
+
*/
|
|
209
|
+
export declare const EFFORT_MIN_SCORE = 50;
|
|
210
|
+
/**
|
|
211
|
+
* Default effort adequacy score when no RPE/RIR data is available.
|
|
212
|
+
* RPE and RIR are optional — users may choose not to log effort intensity.
|
|
213
|
+
* Set equal to EFFORT_MIN_SCORE (50) so that omitting effort data never
|
|
214
|
+
* produces a higher score than honestly logging any RPE or RIR value.
|
|
215
|
+
* There is no incentive to withhold effort data.
|
|
216
|
+
*/
|
|
217
|
+
export declare const EFFORT_NO_DATA_SCORE = 50;
|
|
204
218
|
/** Score when rest is within the optimal range */
|
|
205
219
|
export declare const REST_OPTIMAL_SCORE = 100;
|
|
206
220
|
/** Score when rest is within acceptable (min–max) but not optimal */
|
|
@@ -205,13 +205,17 @@ exports.REFERENCE_MAX_EFFORT = 1.3;
|
|
|
205
205
|
// Quality Score
|
|
206
206
|
// ---------------------------------------------------------------------------
|
|
207
207
|
/**
|
|
208
|
-
* Weights for the four quality sub-components.
|
|
209
|
-
*
|
|
208
|
+
* Weights for the four quality sub-components. Must sum to 1.0.
|
|
209
|
+
*
|
|
210
|
+
* Effort leads (0.40): now that RPE/RIR is captured on every set, effort is
|
|
211
|
+
* the strongest available proxy for whether a session drove adaptation.
|
|
212
|
+
* Consistency demoted (0.20): it is the weakest quality signal and mildly
|
|
213
|
+
* penalises genuine fatigue-driven rep drop-off, so it should not dominate.
|
|
210
214
|
*/
|
|
211
215
|
exports.QUALITY_WEIGHTS = {
|
|
212
|
-
completion: 0.
|
|
213
|
-
consistency: 0.
|
|
214
|
-
effortAdequacy: 0.
|
|
216
|
+
completion: 0.25,
|
|
217
|
+
consistency: 0.2,
|
|
218
|
+
effortAdequacy: 0.4,
|
|
215
219
|
restDiscipline: 0.15,
|
|
216
220
|
};
|
|
217
221
|
/**
|
|
@@ -233,10 +237,20 @@ exports.OPTIMAL_RIR_RANGE = [1, 4];
|
|
|
233
237
|
exports.OPTIMAL_RPE_RANGE = [6, 9];
|
|
234
238
|
/** Penalty per unit of distance from the optimal effort range */
|
|
235
239
|
exports.EFFORT_DISTANCE_PENALTY = 20;
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Minimum effort adequacy score.
|
|
242
|
+
* Set equal to EFFORT_NO_DATA_SCORE so that logging any RPE/RIR value
|
|
243
|
+
* is never worse than logging nothing. Honest data is always safe to log.
|
|
244
|
+
*/
|
|
245
|
+
exports.EFFORT_MIN_SCORE = 50;
|
|
246
|
+
/**
|
|
247
|
+
* Default effort adequacy score when no RPE/RIR data is available.
|
|
248
|
+
* RPE and RIR are optional — users may choose not to log effort intensity.
|
|
249
|
+
* Set equal to EFFORT_MIN_SCORE (50) so that omitting effort data never
|
|
250
|
+
* produces a higher score than honestly logging any RPE or RIR value.
|
|
251
|
+
* There is no incentive to withhold effort data.
|
|
252
|
+
*/
|
|
253
|
+
exports.EFFORT_NO_DATA_SCORE = 50;
|
|
240
254
|
/** Score when rest is within the optimal range */
|
|
241
255
|
exports.REST_OPTIMAL_SCORE = 100;
|
|
242
256
|
/** Score when rest is within acceptable (min–max) but not optimal */
|
|
@@ -125,3 +125,10 @@ export declare function extractUserContext(user: {
|
|
|
125
125
|
* Used for RIR attenuation and reference max scaling (P2-10).
|
|
126
126
|
*/
|
|
127
127
|
export declare function deriveTrainingAgeBracket(accountAgeDays: number, totalSessionCount: number): import("./types").TTrainingAgeBracket;
|
|
128
|
+
/**
|
|
129
|
+
* Derive integer age from a date of birth, accounting for whether this year's
|
|
130
|
+
* birthday has occurred yet. Sanity-clamped to 10–100; returns DEFAULT_USER_AGE
|
|
131
|
+
* on missing or invalid input. Shared by extractUserContext and the fatigue
|
|
132
|
+
* recovery model so age is never computed two different ways.
|
|
133
|
+
*/
|
|
134
|
+
export declare function deriveAge(dob: Date | string | undefined | null, now?: Date): number;
|
|
@@ -12,6 +12,7 @@ exports.rpeToEffortFraction = rpeToEffortFraction;
|
|
|
12
12
|
exports.getEffortFraction = getEffortFraction;
|
|
13
13
|
exports.extractUserContext = extractUserContext;
|
|
14
14
|
exports.deriveTrainingAgeBracket = deriveTrainingAgeBracket;
|
|
15
|
+
exports.deriveAge = deriveAge;
|
|
15
16
|
const constants_1 = require("./constants");
|
|
16
17
|
// We reference these types but don't import them to keep this module decoupled.
|
|
17
18
|
// The caller passes the raw values; we just parse them.
|
|
@@ -103,7 +104,7 @@ function standardDeviation(values) {
|
|
|
103
104
|
if (values.length < 2)
|
|
104
105
|
return 0;
|
|
105
106
|
const avg = mean(values);
|
|
106
|
-
const squaredDiffs = values.map((v) =>
|
|
107
|
+
const squaredDiffs = values.map((v) => (v - avg) ** 2);
|
|
107
108
|
return Math.sqrt(mean(squaredDiffs));
|
|
108
109
|
}
|
|
109
110
|
/**
|
|
@@ -205,23 +206,7 @@ function extractUserContext(user) {
|
|
|
205
206
|
const heightCm = user.heightCm && user.heightCm > 100 && user.heightCm < 250
|
|
206
207
|
? user.heightCm
|
|
207
208
|
: constants_1.DEFAULT_USER_HEIGHT_CM;
|
|
208
|
-
|
|
209
|
-
if (user.dob) {
|
|
210
|
-
const dob = typeof user.dob === "string" ? new Date(user.dob) : user.dob;
|
|
211
|
-
if (!isNaN(dob.getTime())) {
|
|
212
|
-
const today = new Date();
|
|
213
|
-
age = today.getFullYear() - dob.getFullYear();
|
|
214
|
-
// Adjust if birthday hasn't happened yet this year
|
|
215
|
-
const monthDiff = today.getMonth() - dob.getMonth();
|
|
216
|
-
if (monthDiff < 0 ||
|
|
217
|
-
(monthDiff === 0 && today.getDate() < dob.getDate())) {
|
|
218
|
-
age--;
|
|
219
|
-
}
|
|
220
|
-
// Sanity check
|
|
221
|
-
if (age < 10 || age > 100)
|
|
222
|
-
age = constants_1.DEFAULT_USER_AGE;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
209
|
+
const age = deriveAge(user.dob);
|
|
225
210
|
return {
|
|
226
211
|
weightKg,
|
|
227
212
|
heightCm,
|
|
@@ -243,3 +228,24 @@ function deriveTrainingAgeBracket(accountAgeDays, totalSessionCount) {
|
|
|
243
228
|
return "intermediate";
|
|
244
229
|
return "advanced";
|
|
245
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Derive integer age from a date of birth, accounting for whether this year's
|
|
233
|
+
* birthday has occurred yet. Sanity-clamped to 10–100; returns DEFAULT_USER_AGE
|
|
234
|
+
* on missing or invalid input. Shared by extractUserContext and the fatigue
|
|
235
|
+
* recovery model so age is never computed two different ways.
|
|
236
|
+
*/
|
|
237
|
+
function deriveAge(dob, now = new Date()) {
|
|
238
|
+
if (!dob)
|
|
239
|
+
return constants_1.DEFAULT_USER_AGE;
|
|
240
|
+
const birth = typeof dob === "string" ? new Date(dob) : dob;
|
|
241
|
+
if (isNaN(birth.getTime()))
|
|
242
|
+
return constants_1.DEFAULT_USER_AGE;
|
|
243
|
+
let age = now.getFullYear() - birth.getFullYear();
|
|
244
|
+
const monthDiff = now.getMonth() - birth.getMonth();
|
|
245
|
+
if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birth.getDate())) {
|
|
246
|
+
age--;
|
|
247
|
+
}
|
|
248
|
+
if (age < 10 || age > 100)
|
|
249
|
+
return constants_1.DEFAULT_USER_AGE;
|
|
250
|
+
return age;
|
|
251
|
+
}
|
|
@@ -3,16 +3,20 @@
|
|
|
3
3
|
* FITFRIX EXERCISE SCORING SYSTEM
|
|
4
4
|
* ============================================================================
|
|
5
5
|
*
|
|
6
|
-
* calculateExerciseScoreV2({ exercise, record, user, historicalContext? })
|
|
6
|
+
* calculateExerciseScoreV2({ exercise, record, user, historicalContext? })
|
|
7
|
+
* => IScoreResult
|
|
7
8
|
*
|
|
8
9
|
* IScoreResult = {
|
|
9
|
-
* score:
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* score: number, // 0–100 quality of execution
|
|
11
|
+
* qualityBreakdown: IQualityBreakdown, // completion/consistency/effort/rest
|
|
12
|
+
* muscleScores: Record<string, number>, // 0–100 per muscle, keyed by EBodyParts
|
|
13
|
+
* calorieBurn: number, // kcal — placeholder 0 until Phase 3
|
|
14
|
+
* restDisciplineActive: boolean, // false when no rest data was logged
|
|
13
15
|
* }
|
|
14
16
|
*
|
|
15
|
-
* Internally computes
|
|
17
|
+
* Internally computes:
|
|
18
|
+
* Pillar 2: Muscle Fatigue → muscleScores
|
|
19
|
+
* Pillar 3: Quality Score → single score + breakdown (not per-goal)
|
|
16
20
|
* Calorie burn (Pillar 1) is implemented in calculateCalories.ts but not yet
|
|
17
21
|
* wired into the save flow — scheduled for Phase 3.
|
|
18
22
|
*/
|
|
@@ -23,10 +27,10 @@ export { calculateTotalVolume } from "./calculateTotalVolume";
|
|
|
23
27
|
export { computeMuscleFatigueMap } from "./computeMuscleFatigueMap";
|
|
24
28
|
export { deriveTrainingAgeBracket };
|
|
25
29
|
export type { IHistoricalContext, TTrainingAgeBracket };
|
|
26
|
-
export type { TEnrichedSessionScore, TEnrichedExerciseRecord, TMuscleFatigueEntry, TMuscleFatigueResult } from "./types";
|
|
30
|
+
export type { TEnrichedSessionScore, TEnrichedExerciseRecord, TMuscleFatigueEntry, TMuscleFatigueResult, } from "./types";
|
|
27
31
|
export declare const calculateExerciseScoreV2: (param: {
|
|
28
32
|
exercise: TExercise;
|
|
29
33
|
record: TRecord[];
|
|
30
34
|
user: TUserMetric;
|
|
31
|
-
historicalContext?:
|
|
35
|
+
historicalContext?: IHistoricalContext;
|
|
32
36
|
}) => IScoreResult;
|
|
@@ -4,22 +4,26 @@
|
|
|
4
4
|
* FITFRIX EXERCISE SCORING SYSTEM
|
|
5
5
|
* ============================================================================
|
|
6
6
|
*
|
|
7
|
-
* calculateExerciseScoreV2({ exercise, record, user, historicalContext? })
|
|
7
|
+
* calculateExerciseScoreV2({ exercise, record, user, historicalContext? })
|
|
8
|
+
* => IScoreResult
|
|
8
9
|
*
|
|
9
10
|
* IScoreResult = {
|
|
10
|
-
* score:
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* score: number, // 0–100 quality of execution
|
|
12
|
+
* qualityBreakdown: IQualityBreakdown, // completion/consistency/effort/rest
|
|
13
|
+
* muscleScores: Record<string, number>, // 0–100 per muscle, keyed by EBodyParts
|
|
14
|
+
* calorieBurn: number, // kcal — placeholder 0 until Phase 3
|
|
15
|
+
* restDisciplineActive: boolean, // false when no rest data was logged
|
|
14
16
|
* }
|
|
15
17
|
*
|
|
16
|
-
* Internally computes
|
|
18
|
+
* Internally computes:
|
|
19
|
+
* Pillar 2: Muscle Fatigue → muscleScores
|
|
20
|
+
* Pillar 3: Quality Score → single score + breakdown (not per-goal)
|
|
17
21
|
* Calorie burn (Pillar 1) is implemented in calculateCalories.ts but not yet
|
|
18
22
|
* wired into the save flow — scheduled for Phase 3.
|
|
19
23
|
*/
|
|
20
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
25
|
exports.calculateExerciseScoreV2 = exports.deriveTrainingAgeBracket = exports.computeMuscleFatigueMap = exports.calculateTotalVolume = void 0;
|
|
22
|
-
const
|
|
26
|
+
const calculateMuscleFatigue_1 = require("./calculateMuscleFatigue");
|
|
23
27
|
const calculateQualityScore_1 = require("./calculateQualityScore");
|
|
24
28
|
const helpers_1 = require("./helpers");
|
|
25
29
|
Object.defineProperty(exports, "deriveTrainingAgeBracket", { enumerable: true, get: function () { return helpers_1.deriveTrainingAgeBracket; } });
|
|
@@ -29,14 +33,14 @@ Object.defineProperty(exports, "calculateTotalVolume", { enumerable: true, get:
|
|
|
29
33
|
var computeMuscleFatigueMap_1 = require("./computeMuscleFatigueMap");
|
|
30
34
|
Object.defineProperty(exports, "computeMuscleFatigueMap", { enumerable: true, get: function () { return computeMuscleFatigueMap_1.computeMuscleFatigueMap; } });
|
|
31
35
|
// ---------------------------------------------------------------------------
|
|
32
|
-
// Main Function
|
|
36
|
+
// Main Function
|
|
33
37
|
// ---------------------------------------------------------------------------
|
|
34
38
|
const calculateExerciseScoreV2 = (param) => {
|
|
35
39
|
const { exercise, record, user, historicalContext } = param;
|
|
36
40
|
const userContext = (0, helpers_1.extractUserContext)(user);
|
|
37
41
|
const parsedSets = (0, parseRecords_1.parseRecords)(record, exercise.timingGuardrails, historicalContext);
|
|
38
|
-
// Pillar 2: Muscle Fatigue
|
|
39
|
-
const muscleScores = (0,
|
|
42
|
+
// ── Pillar 2: Muscle Fatigue ─────────────────────────────────────────────
|
|
43
|
+
const muscleScores = (0, calculateMuscleFatigue_1.calculateMuscleFatigue)(parsedSets, {
|
|
40
44
|
primaryMuscles: exercise.primaryMuscles,
|
|
41
45
|
secondaryMuscles: exercise.secondaryMuscles,
|
|
42
46
|
difficultyLevel: exercise.difficultyLevel,
|
|
@@ -47,13 +51,17 @@ const calculateExerciseScoreV2 = (param) => {
|
|
|
47
51
|
scoringSpecialHandling: exercise.scoringSpecialHandling,
|
|
48
52
|
isUnilateral: exercise.isUnilateral,
|
|
49
53
|
}, userContext, exercise.timingGuardrails, historicalContext);
|
|
50
|
-
// Pillar 3: Quality Score
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
// ── Pillar 3: Quality Score ──────────────────────────────────────────────
|
|
55
|
+
// userContext is intentionally not passed — quality scoring is goal-agnostic.
|
|
56
|
+
// Goal-specific logic lives in the gate system and quick plan generator.
|
|
57
|
+
const { score, qualityBreakdown, restDisciplineActive } = (0, calculateQualityScore_1.calculateQualityScore)(parsedSets, record, exercise.timingGuardrails, historicalContext);
|
|
58
|
+
// ── Result (Option A flat shape) ─────────────────────────────────────────
|
|
53
59
|
return {
|
|
60
|
+
score,
|
|
61
|
+
qualityBreakdown,
|
|
54
62
|
muscleScores,
|
|
55
|
-
calorieBurn: 0, //
|
|
56
|
-
|
|
63
|
+
calorieBurn: 0, // placeholder — Pillar 1 wired in Phase 3
|
|
64
|
+
restDisciplineActive,
|
|
57
65
|
};
|
|
58
66
|
};
|
|
59
67
|
exports.calculateExerciseScoreV2 = calculateExerciseScoreV2;
|
|
@@ -250,9 +250,10 @@ function resolveWorkDuration(measured, estimated, guardrails) {
|
|
|
250
250
|
*/
|
|
251
251
|
function resolveRestDuration(measured, setIndex, totalSets, guardrails) {
|
|
252
252
|
var _a, _b, _c;
|
|
253
|
-
// 1. Last set
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
// 1. Last set → null. Rest after the final set is not training rest —
|
|
254
|
+
// there is no subsequent set it prepares for. Holds whether or not a
|
|
255
|
+
// value was logged: a measured rest on the final set is still not scorable.
|
|
256
|
+
if (setIndex === totalSets - 1) {
|
|
256
257
|
return null;
|
|
257
258
|
}
|
|
258
259
|
// Extract exercise-specific rest config
|