@dgpholdings/greatoak-shared 1.2.57 → 1.2.59
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/types/TApiExercise.d.ts +3 -0
- package/dist/utils/scoringWorkout/calculateMuscleFatiue.d.ts +2 -0
- package/dist/utils/scoringWorkout/calculateMuscleFatiue.js +6 -5
- package/dist/utils/scoringWorkout/calculateTotalVolume.d.ts +1 -1
- package/dist/utils/scoringWorkout/calculateTotalVolume.js +3 -2
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +1 -0
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +67 -9
- package/dist/utils/scoringWorkout/index.js +1 -0
- package/package.json +1 -1
|
@@ -135,6 +135,9 @@ export type TExercise = {
|
|
|
135
135
|
female: number;
|
|
136
136
|
default: number;
|
|
137
137
|
};
|
|
138
|
+
/** True for single-arm / single-leg exercises. The user enters weight per side,
|
|
139
|
+
* so the scoring engine doubles it to get total mechanical load. */
|
|
140
|
+
isUnilateral?: boolean;
|
|
138
141
|
};
|
|
139
142
|
export type TBodyPartExercises = Record<TBodyPart, TExercise[]>;
|
|
140
143
|
export type TApiCreateOrUpdateExerciseReq = {
|
|
@@ -49,6 +49,8 @@ interface IFatigueExerciseData {
|
|
|
49
49
|
muscleGroupFactor: number;
|
|
50
50
|
};
|
|
51
51
|
scoringSpecialHandling?: "plyometric" | "stretch-mobility" | "continuous-duration" | "loaded-carry";
|
|
52
|
+
/** P3-9: Single-arm/leg exercises. User enters weight per side; double it for total load. */
|
|
53
|
+
isUnilateral?: boolean;
|
|
52
54
|
}
|
|
53
55
|
/**
|
|
54
56
|
* Calculate per-muscle fatigue scores for an exercise.
|
|
@@ -64,7 +64,7 @@ function calculateMuscleFatigue(sets, exercise, user, timingGuardrails, historic
|
|
|
64
64
|
return {};
|
|
65
65
|
const fatigueMultiplier = (_a = timingGuardrails === null || timingGuardrails === void 0 ? void 0 : timingGuardrails.fatigueMultiplier) !== null && _a !== void 0 ? _a : constants_1.FALLBACK_FATIGUE_MULTIPLIER;
|
|
66
66
|
// --- Step 1–3: Compute cumulative fatigue stimulus ---
|
|
67
|
-
const cumulativeFatigue = computeCumulativeFatigue(sets, exercise.difficultyLevel, user, fatigueMultiplier, exercise.scoringSpecialHandling);
|
|
67
|
+
const cumulativeFatigue = computeCumulativeFatigue(sets, exercise.difficultyLevel, user, fatigueMultiplier, exercise.scoringSpecialHandling, exercise.isUnilateral);
|
|
68
68
|
// --- Step 4: Distribute to muscles ---
|
|
69
69
|
const rawMuscleFatigue = distributeFatigueToMuscles(cumulativeFatigue, exercise.primaryMuscles, exercise.secondaryMuscles);
|
|
70
70
|
// --- Step 5: Normalize to 0–100 ---
|
|
@@ -88,11 +88,12 @@ function calculateMuscleFatigue(sets, exercise, user, timingGuardrails, historic
|
|
|
88
88
|
* cardio: Duration × speed factor (represents sustained effort)
|
|
89
89
|
* loaded-carry: Speed factor boosted by carried weight relative to bodyweight
|
|
90
90
|
*/
|
|
91
|
-
function computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling) {
|
|
91
|
+
function computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling, isUnilateral) {
|
|
92
92
|
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
93
93
|
switch (set.type) {
|
|
94
94
|
case "weight-reps": {
|
|
95
|
-
|
|
95
|
+
// P3-9: User enters weight per side for unilateral exercises — double for total load
|
|
96
|
+
const kg = ((_a = set.kg) !== null && _a !== void 0 ? _a : 0) * (isUnilateral ? 2 : 1);
|
|
96
97
|
const reps = (_b = set.reps) !== null && _b !== void 0 ? _b : 0;
|
|
97
98
|
return kg * reps;
|
|
98
99
|
}
|
|
@@ -175,12 +176,12 @@ function computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling) {
|
|
|
175
176
|
*
|
|
176
177
|
* @returns Single number representing total fatigue stimulus
|
|
177
178
|
*/
|
|
178
|
-
function computeCumulativeFatigue(sets, difficultyLevel, user, fatigueMultiplier, scoringSpecialHandling) {
|
|
179
|
+
function computeCumulativeFatigue(sets, difficultyLevel, user, fatigueMultiplier, scoringSpecialHandling, isUnilateral) {
|
|
179
180
|
let cumulative = 0;
|
|
180
181
|
for (let i = 0; i < sets.length; i++) {
|
|
181
182
|
const set = sets[i];
|
|
182
183
|
// Step 1: Raw volume for this set
|
|
183
|
-
const volumeLoad = computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling);
|
|
184
|
+
const volumeLoad = computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling, isUnilateral);
|
|
184
185
|
// Step 2: Scale by effort and exercise fatigue multiplier
|
|
185
186
|
const stimulus = volumeLoad * set.effortFraction * fatigueMultiplier;
|
|
186
187
|
// Step 3: Apply diminishing returns decay
|
|
@@ -15,4 +15,4 @@ import { TRecord, TUserMetric } from "../../types";
|
|
|
15
15
|
* @param difficultyLevel Exercise difficulty (0–4). Used for bodyweight load
|
|
16
16
|
* scaling on reps-only exercises (matches fatigue pillar).
|
|
17
17
|
*/
|
|
18
|
-
export declare const calculateTotalVolume: (record: TRecord[], user: TUserMetric, scoringSpecialHandling?: "plyometric" | "stretch-mobility" | "continuous-duration" | "loaded-carry", difficultyLevel?: number) => number;
|
|
18
|
+
export declare const calculateTotalVolume: (record: TRecord[], user: TUserMetric, scoringSpecialHandling?: "plyometric" | "stretch-mobility" | "continuous-duration" | "loaded-carry", difficultyLevel?: number, isUnilateral?: boolean) => number;
|
|
@@ -19,7 +19,7 @@ const constants_1 = require("./constants");
|
|
|
19
19
|
* @param difficultyLevel Exercise difficulty (0–4). Used for bodyweight load
|
|
20
20
|
* scaling on reps-only exercises (matches fatigue pillar).
|
|
21
21
|
*/
|
|
22
|
-
const calculateTotalVolume = (record, user, scoringSpecialHandling, difficultyLevel = 2) => {
|
|
22
|
+
const calculateTotalVolume = (record, user, scoringSpecialHandling, difficultyLevel = 2, isUnilateral = false) => {
|
|
23
23
|
if (scoringSpecialHandling === "stretch-mobility")
|
|
24
24
|
return 0;
|
|
25
25
|
// Bug A fix: only completed sets contribute to volume.
|
|
@@ -28,7 +28,8 @@ const calculateTotalVolume = (record, user, scoringSpecialHandling, difficultyLe
|
|
|
28
28
|
switch (set.type) {
|
|
29
29
|
case "weight-reps": {
|
|
30
30
|
const reps = parseFloat(set.reps) || 0;
|
|
31
|
-
|
|
31
|
+
// P3-9: User enters weight per side for unilateral exercises — double for total load
|
|
32
|
+
const weight = (parseFloat(set.kg) || 0) * (isUnilateral ? 2 : 1);
|
|
32
33
|
return total + reps * weight;
|
|
33
34
|
}
|
|
34
35
|
case "reps-only": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* P2-7 Age — recovery slows with age (< 35 → 1.0×, > 55 → 0.85×)
|
|
13
13
|
* P2-8 Gender — females recover slightly faster (1.05×)
|
|
14
14
|
* P2-9 fitnessLevel — sedentary 0.8× → very-active 1.15×
|
|
15
|
+
* P3-7 Weekly volume — muscles trained multiple times this week recover slower
|
|
15
16
|
* P3-8 Muscle group — small muscles (biceps) recover 1.4× faster than legs (0.80×)
|
|
16
17
|
*
|
|
17
18
|
* Accumulation model:
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* P2-7 Age — recovery slows with age (< 35 → 1.0×, > 55 → 0.85×)
|
|
14
14
|
* P2-8 Gender — females recover slightly faster (1.05×)
|
|
15
15
|
* P2-9 fitnessLevel — sedentary 0.8× → very-active 1.15×
|
|
16
|
+
* P3-7 Weekly volume — muscles trained multiple times this week recover slower
|
|
16
17
|
* P3-8 Muscle group — small muscles (biceps) recover 1.4× faster than legs (0.80×)
|
|
17
18
|
*
|
|
18
19
|
* Accumulation model:
|
|
@@ -79,6 +80,35 @@ function getMuscleRecoveryRate(muscle) {
|
|
|
79
80
|
return (_a = MUSCLE_RECOVERY_RATE[muscle]) !== null && _a !== void 0 ? _a : 1.0;
|
|
80
81
|
}
|
|
81
82
|
// ---------------------------------------------------------------------------
|
|
83
|
+
// P3-7: Weekly volume recovery modifier
|
|
84
|
+
//
|
|
85
|
+
// A muscle trained multiple times this week is carrying accumulated fatigue
|
|
86
|
+
// beyond what any single session decay captures. High weekly volume slows
|
|
87
|
+
// the effective recovery rate for that muscle.
|
|
88
|
+
//
|
|
89
|
+
// Formula: each session beyond the first (~80 score units) reduces recovery
|
|
90
|
+
// rate by 5%, up to a 40% maximum reduction.
|
|
91
|
+
//
|
|
92
|
+
// 1 session (~80): 0% reduction → 1.00× multiplier
|
|
93
|
+
// 2 sessions (~160): 5% reduction → 0.95×
|
|
94
|
+
// 3 sessions (~240): 10% reduction → 0.90×
|
|
95
|
+
// 5 sessions (~400): 20% reduction → 0.80×
|
|
96
|
+
// 9+ sessions (cap): 40% reduction → 0.60×
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
const WEEKLY_VOLUME_BASELINE = 80; // ~1 normal session score
|
|
99
|
+
const WEEKLY_VOLUME_STEP = 80; // score units per step
|
|
100
|
+
const WEEKLY_VOLUME_REDUCTION_PER_STEP = 0.05; // 5% per additional session
|
|
101
|
+
const WEEKLY_VOLUME_MAX_REDUCTION = 0.40;
|
|
102
|
+
function getWeeklyVolumeMultiplier(muscle, weeklyVolume) {
|
|
103
|
+
var _a;
|
|
104
|
+
const total = (_a = weeklyVolume[muscle]) !== null && _a !== void 0 ? _a : 0;
|
|
105
|
+
if (total <= WEEKLY_VOLUME_BASELINE)
|
|
106
|
+
return 1.0;
|
|
107
|
+
const steps = (total - WEEKLY_VOLUME_BASELINE) / WEEKLY_VOLUME_STEP;
|
|
108
|
+
const reduction = Math.min(WEEKLY_VOLUME_MAX_REDUCTION, steps * WEEKLY_VOLUME_REDUCTION_PER_STEP);
|
|
109
|
+
return 1.0 - reduction;
|
|
110
|
+
}
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
82
112
|
// Core recovery formula
|
|
83
113
|
// ---------------------------------------------------------------------------
|
|
84
114
|
function calculateRecoveryFatigue(originalFatigue, hoursSinceWorkout, recoveryMultiplier = 1.0) {
|
|
@@ -124,7 +154,7 @@ const RECOVERY_WINDOW_HOURS = 168; // 7 days
|
|
|
124
154
|
* @returns Record<muscleKey, { fatigue 0–100, lastWorked }>
|
|
125
155
|
*/
|
|
126
156
|
function computeMuscleFatigueMap(exercises, scoreHistory, user, currentDate = new Date()) {
|
|
127
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
157
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
|
|
128
158
|
// Build combined user recovery multiplier (P2-7, P2-8, P2-9)
|
|
129
159
|
let userRecoveryMultiplier = 1.0;
|
|
130
160
|
if (user) {
|
|
@@ -137,6 +167,27 @@ function computeMuscleFatigueMap(exercises, scoreHistory, user, currentDate = ne
|
|
|
137
167
|
((_c = FITNESS_RECOVERY_SCALE[(_b = user.fitnessLevel) !== null && _b !== void 0 ? _b : "moderately-active"]) !== null && _c !== void 0 ? _c : 1.0);
|
|
138
168
|
}
|
|
139
169
|
const cutoffTime = currentDate.getTime() - RECOVERY_WINDOW_HOURS * 3600000;
|
|
170
|
+
const weekCutoff = currentDate.getTime() - 7 * 24 * 3600000; // same as RECOVERY_WINDOW_HOURS
|
|
171
|
+
// P3-7: First pass — compute weekly accumulated score per muscle across ALL exercises.
|
|
172
|
+
// This captures "chest trained 3× this week" even when each session appears independent.
|
|
173
|
+
const weeklyVolume = {};
|
|
174
|
+
for (const { exerciseId, recordScore } of scoreHistory) {
|
|
175
|
+
const exercise = exercises[exerciseId];
|
|
176
|
+
if (!exercise)
|
|
177
|
+
continue;
|
|
178
|
+
const weeklyScore = recordScore
|
|
179
|
+
.filter((r) => r.recordDate >= weekCutoff)
|
|
180
|
+
.reduce((sum, r) => sum + r.score, 0);
|
|
181
|
+
if (weeklyScore === 0)
|
|
182
|
+
continue;
|
|
183
|
+
for (const muscle of (_d = exercise.primaryMuscles) !== null && _d !== void 0 ? _d : []) {
|
|
184
|
+
weeklyVolume[muscle] = ((_e = weeklyVolume[muscle]) !== null && _e !== void 0 ? _e : 0) + weeklyScore;
|
|
185
|
+
}
|
|
186
|
+
for (const muscle of (_f = exercise.secondaryMuscles) !== null && _f !== void 0 ? _f : []) {
|
|
187
|
+
// Secondary muscles receive 35% of the stimulus
|
|
188
|
+
weeklyVolume[muscle] = ((_g = weeklyVolume[muscle]) !== null && _g !== void 0 ? _g : 0) + weeklyScore * 0.35;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
140
191
|
const internalMap = new Map();
|
|
141
192
|
for (const { exerciseId, recordScore } of scoreHistory) {
|
|
142
193
|
const exercise = exercises[exerciseId];
|
|
@@ -150,14 +201,17 @@ function computeMuscleFatigueMap(exercises, scoreHistory, user, currentDate = ne
|
|
|
150
201
|
const hasRealMuscleScores = record.muscleScores && Object.keys(record.muscleScores).length > 0;
|
|
151
202
|
if (hasRealMuscleScores) {
|
|
152
203
|
const allMuscles = new Set([
|
|
153
|
-
...((
|
|
154
|
-
...((
|
|
204
|
+
...((_h = exercise.primaryMuscles) !== null && _h !== void 0 ? _h : []),
|
|
205
|
+
...((_j = exercise.secondaryMuscles) !== null && _j !== void 0 ? _j : []),
|
|
155
206
|
]);
|
|
156
207
|
for (const muscle of allMuscles) {
|
|
157
|
-
const baseFatigue = (
|
|
208
|
+
const baseFatigue = (_k = record.muscleScores[muscle]) !== null && _k !== void 0 ? _k : 0;
|
|
158
209
|
if (baseFatigue <= 0)
|
|
159
210
|
continue;
|
|
160
|
-
|
|
211
|
+
// P3-7: weekly volume slows recovery for heavily-trained muscles
|
|
212
|
+
const multiplier = userRecoveryMultiplier
|
|
213
|
+
* getMuscleRecoveryRate(muscle)
|
|
214
|
+
* getWeeklyVolumeMultiplier(muscle, weeklyVolume);
|
|
161
215
|
const remaining = calculateRecoveryFatigue(baseFatigue, hoursSince, multiplier);
|
|
162
216
|
if (remaining > 0)
|
|
163
217
|
updateEntry(internalMap, muscle, remaining, workoutDate, true);
|
|
@@ -170,16 +224,20 @@ function computeMuscleFatigueMap(exercises, scoreHistory, user, currentDate = ne
|
|
|
170
224
|
const intensityFraction = record.score <= MIN_MEANINGFUL_SCORE
|
|
171
225
|
? (record.score / MIN_MEANINGFUL_SCORE) * 0.3
|
|
172
226
|
: ((record.score - MIN_MEANINGFUL_SCORE) / (MAX_SCORE - MIN_MEANINGFUL_SCORE)) * 0.7 + 0.3;
|
|
173
|
-
for (const muscle of (
|
|
227
|
+
for (const muscle of (_l = exercise.primaryMuscles) !== null && _l !== void 0 ? _l : []) {
|
|
174
228
|
const baseFatigue = record.score * 0.7 * intensityFraction;
|
|
175
|
-
const multiplier = userRecoveryMultiplier
|
|
229
|
+
const multiplier = userRecoveryMultiplier
|
|
230
|
+
* getMuscleRecoveryRate(muscle)
|
|
231
|
+
* getWeeklyVolumeMultiplier(muscle, weeklyVolume);
|
|
176
232
|
const remaining = calculateRecoveryFatigue(baseFatigue, hoursSince, multiplier);
|
|
177
233
|
if (remaining > 0)
|
|
178
234
|
updateEntry(internalMap, muscle, remaining, workoutDate, false);
|
|
179
235
|
}
|
|
180
|
-
for (const muscle of (
|
|
236
|
+
for (const muscle of (_m = exercise.secondaryMuscles) !== null && _m !== void 0 ? _m : []) {
|
|
181
237
|
const baseFatigue = record.score * 0.3 * intensityFraction;
|
|
182
|
-
const multiplier = userRecoveryMultiplier
|
|
238
|
+
const multiplier = userRecoveryMultiplier
|
|
239
|
+
* getMuscleRecoveryRate(muscle)
|
|
240
|
+
* getWeeklyVolumeMultiplier(muscle, weeklyVolume);
|
|
183
241
|
const remaining = calculateRecoveryFatigue(baseFatigue, hoursSince, multiplier);
|
|
184
242
|
if (remaining > 0)
|
|
185
243
|
updateEntry(internalMap, muscle, remaining, workoutDate, false);
|
|
@@ -45,6 +45,7 @@ const calculateExerciseScoreV2 = (param) => {
|
|
|
45
45
|
muscleGroupFactor: exercise.metabolicData.muscleGroupFactor,
|
|
46
46
|
},
|
|
47
47
|
scoringSpecialHandling: exercise.scoringSpecialHandling,
|
|
48
|
+
isUnilateral: exercise.isUnilateral,
|
|
48
49
|
}, userContext, exercise.timingGuardrails, historicalContext);
|
|
49
50
|
// Pillar 3: Quality Score → score
|
|
50
51
|
const isStrictTimingModeScoring = record.some((r) => r.isStrictMode);
|