@dgpholdings/greatoak-shared 1.2.85 → 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/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/quickStartIntents.d.ts +19 -0
- package/dist/constants/quickStartIntents.js +39 -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
|
@@ -18,122 +18,115 @@ const templateExercises_mock_1 = require("../../__mocks__/templateExercises.mock
|
|
|
18
18
|
const exercises_mock_1 = require("../../__mocks__/exercises.mock");
|
|
19
19
|
const user_mock_1 = require("../../__mocks__/user.mock");
|
|
20
20
|
(0, vitest_1.describe)("Scoring Engine Phase 1 Integration Tests", () => {
|
|
21
|
-
(0, vitest_1.it)("P1-1 proper proof: Partial workout
|
|
21
|
+
(0, vitest_1.it)("P1-1 proper proof: Partial workout — last done set gets null rest, only set 0 contributes to rest score", () => {
|
|
22
22
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsPartial;
|
|
23
23
|
const exercise = exercises_mock_1.mockExerciseWeightReps;
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
const strictRecords = template.initialRecords.map((r, i) => {
|
|
24
|
+
// Set 0 has restDurationSecs: 120 (optimal), Set 1 has rest removed.
|
|
25
|
+
// parseRecords should null-out the last done set's rest regardless of isStrictMode.
|
|
26
|
+
const records = template.initialRecords.map((r, i) => {
|
|
28
27
|
if (i === 1) {
|
|
29
|
-
const { restDurationSecs } = r, rest = __rest(r, ["restDurationSecs"]);
|
|
28
|
+
const { restDurationSecs } = r, rest = __rest(r, ["restDurationSecs"]);
|
|
30
29
|
return Object.assign(Object.assign({}, rest), { isStrictMode: true });
|
|
31
30
|
}
|
|
32
|
-
return Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: 120 });
|
|
31
|
+
return Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: 120 });
|
|
33
32
|
});
|
|
34
|
-
// Direct parseRecords
|
|
35
|
-
|
|
36
|
-
// Without fix (totalSets=records.length=4): Set1 index=1 !== 3 → gets typical (120s).
|
|
37
|
-
const parsed = (0, parseRecords_1.parseRecords)(strictRecords, exercise.timingGuardrails);
|
|
33
|
+
// Direct parseRecords proof: last done set (index 1) must have null rest.
|
|
34
|
+
const parsed = (0, parseRecords_1.parseRecords)(records, exercise.timingGuardrails);
|
|
38
35
|
(0, vitest_1.expect)(parsed[1].restDurationSecs).toBeNull();
|
|
39
|
-
|
|
36
|
+
// Rest data IS present (set 0 has 120s) → restDisciplineActive = true
|
|
37
|
+
const { score, qualityBreakdown, restDisciplineActive } = (0, index_1.calculateExerciseScoreV2)({
|
|
40
38
|
exercise,
|
|
41
|
-
record:
|
|
39
|
+
record: records,
|
|
42
40
|
user: user_mock_1.mockUser,
|
|
43
41
|
});
|
|
44
|
-
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
45
42
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(50); // 2/4 done
|
|
46
|
-
|
|
43
|
+
(0, vitest_1.expect)(restDisciplineActive).toBe(true);
|
|
44
|
+
// Only set 0 (120s, optimal) contributes to rest — set 1 is correctly excluded
|
|
47
45
|
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBeGreaterThan(95);
|
|
48
46
|
});
|
|
49
|
-
(0, vitest_1.it)("P1-2:
|
|
47
|
+
(0, vitest_1.it)("P1-2: Failed set — zero rep set still counts towards completion if isDone is true", () => {
|
|
50
48
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsFailedSet;
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
exercise,
|
|
49
|
+
const { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
|
|
50
|
+
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
54
51
|
record: template.initialRecords,
|
|
55
52
|
user: user_mock_1.mockUser,
|
|
56
53
|
});
|
|
57
|
-
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
58
54
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(67); // 2/3 done
|
|
59
55
|
});
|
|
60
|
-
(0, vitest_1.it)("P1-1 + P1-3:
|
|
56
|
+
(0, vitest_1.it)("P1-1 + P1-3: Lazy logger — handles missing data gracefully using fallbacks", () => {
|
|
61
57
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.lazyLogger;
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
exercise,
|
|
58
|
+
const { score, qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
|
|
59
|
+
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
65
60
|
record: template.initialRecords,
|
|
66
61
|
user: user_mock_1.mockUser,
|
|
67
62
|
});
|
|
68
|
-
const { score, qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
69
63
|
(0, vitest_1.expect)(score).toBeGreaterThan(50);
|
|
70
64
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(100);
|
|
71
65
|
});
|
|
72
|
-
(0, vitest_1.it)("Distracted logger
|
|
66
|
+
(0, vitest_1.it)("Distracted logger — clamping path: 600s rest clamped to typical (120s) → restDiscipline 100", () => {
|
|
73
67
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.distractedLogger;
|
|
74
68
|
const exercise = exercises_mock_1.mockExerciseWeightReps;
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
const
|
|
78
|
-
const {
|
|
69
|
+
// restDurationSecs present → hasRestData = true → rest IS scored
|
|
70
|
+
// 600s > acceptableMax so parseRecords clamps to typical (120s) → optimal → 100
|
|
71
|
+
const records = template.initialRecords.map((r, i) => (Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: i === 0 ? 600 : 120 })));
|
|
72
|
+
const { qualityBreakdown, restDisciplineActive } = (0, index_1.calculateExerciseScoreV2)({
|
|
79
73
|
exercise,
|
|
80
|
-
record:
|
|
74
|
+
record: records,
|
|
81
75
|
user: user_mock_1.mockUser,
|
|
82
76
|
});
|
|
83
|
-
|
|
84
|
-
// Both sets end up evaluating as 120s (optimal) due to clamping + explicit
|
|
77
|
+
(0, vitest_1.expect)(restDisciplineActive).toBe(true);
|
|
85
78
|
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(100);
|
|
86
79
|
});
|
|
87
|
-
(0, vitest_1.it)("Distracted logger
|
|
80
|
+
(0, vitest_1.it)("Distracted logger — penalty path: 350s rest outside acceptableMax → restDiscipline 80", () => {
|
|
88
81
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.distractedLogger;
|
|
89
82
|
const exercise = exercises_mock_1.mockExerciseWeightReps;
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
// Set 0: 350s → outside acceptableMax (300s) → 50
|
|
84
|
+
// Set 1: 120s → optimal → 100
|
|
85
|
+
// avg = 75, plus stressRestBonus (5) = 80
|
|
86
|
+
const records = template.initialRecords.map((r, i) => (Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: i === 0 ? 350 : 120 })));
|
|
87
|
+
const { qualityBreakdown, restDisciplineActive } = (0, index_1.calculateExerciseScoreV2)({
|
|
93
88
|
exercise,
|
|
94
|
-
record:
|
|
89
|
+
record: records,
|
|
95
90
|
user: user_mock_1.mockUser,
|
|
96
91
|
});
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(80);
|
|
92
|
+
(0, vitest_1.expect)(restDisciplineActive).toBe(true);
|
|
93
|
+
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(50);
|
|
100
94
|
});
|
|
101
|
-
(0, vitest_1.it)("
|
|
95
|
+
(0, vitest_1.it)("All undone — score = 0 with zeroed breakdown, no errors", () => {
|
|
102
96
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.allUndone;
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
exercise,
|
|
97
|
+
const { score, qualityBreakdown, muscleScores, restDisciplineActive } = (0, index_1.calculateExerciseScoreV2)({
|
|
98
|
+
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
106
99
|
record: template.initialRecords,
|
|
107
100
|
user: user_mock_1.mockUser,
|
|
108
101
|
});
|
|
109
|
-
const { score, qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
110
102
|
(0, vitest_1.expect)(score).toBe(0);
|
|
111
103
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(0);
|
|
112
104
|
(0, vitest_1.expect)(qualityBreakdown.consistency).toBe(0);
|
|
113
105
|
(0, vitest_1.expect)(qualityBreakdown.effortAdequacy).toBe(0);
|
|
114
106
|
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(0);
|
|
107
|
+
(0, vitest_1.expect)(restDisciplineActive).toBe(false);
|
|
115
108
|
(0, vitest_1.expect)(Object.keys(muscleScores).length).toBe(0);
|
|
116
109
|
});
|
|
117
|
-
(0, vitest_1.it)("Trivial workout:
|
|
110
|
+
(0, vitest_1.it)("Trivial workout: 3s duration — completion 100, no rest data, no effort data", () => {
|
|
118
111
|
const exercise = exercises_mock_1.mockExerciseDuration;
|
|
119
112
|
const trivialRecord = [
|
|
120
113
|
{
|
|
121
114
|
type: "duration",
|
|
122
115
|
isDone: true,
|
|
123
116
|
isStrictMode: false,
|
|
124
|
-
durationMmSs: "00:03",
|
|
117
|
+
durationMmSs: "00:03",
|
|
125
118
|
auxWeightKg: "0",
|
|
126
|
-
}
|
|
119
|
+
},
|
|
127
120
|
];
|
|
128
|
-
const {
|
|
121
|
+
const { score, qualityBreakdown, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
|
|
129
122
|
exercise,
|
|
130
123
|
record: trivialRecord,
|
|
131
124
|
user: user_mock_1.mockUser,
|
|
132
125
|
});
|
|
133
|
-
const { score, qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
134
126
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(100);
|
|
135
|
-
|
|
136
|
-
//
|
|
127
|
+
// completion=100, consistency=100 (single set), effortAdequacy=50 (no RPE/RIR),
|
|
128
|
+
// no rest data → (100×0.25 + 100×0.20 + 50×0.40) / 0.85 = 65 / 0.85 = 76
|
|
129
|
+
(0, vitest_1.expect)(score).toBe(76);
|
|
137
130
|
const primaryMuscle = exercise.primaryMuscles[0];
|
|
138
131
|
(0, vitest_1.expect)(muscleScores[primaryMuscle]).toBeLessThan(5);
|
|
139
132
|
});
|
|
@@ -144,9 +137,11 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
144
137
|
const record = template.initialRecords[0];
|
|
145
138
|
const reps = parseFloat(record.type === "weight-reps" ? record.reps : "0");
|
|
146
139
|
const guardrails = exercises_mock_1.mockExerciseWeightReps.timingGuardrails;
|
|
147
|
-
const typicalSecs = (guardrails === null || guardrails === void 0 ? void 0 : guardrails.type) === "weight-reps"
|
|
140
|
+
const typicalSecs = (guardrails === null || guardrails === void 0 ? void 0 : guardrails.type) === "weight-reps"
|
|
141
|
+
? ((_b = (_a = guardrails.singleRep) === null || _a === void 0 ? void 0 : _a.typical) !== null && _b !== void 0 ? _b : 0)
|
|
142
|
+
: 0;
|
|
148
143
|
const setupSecs = (_c = guardrails === null || guardrails === void 0 ? void 0 : guardrails.setupTypicalSecs) !== null && _c !== void 0 ? _c : 0;
|
|
149
|
-
const expectedDuration = reps * typicalSecs + setupSecs; // 10
|
|
144
|
+
const expectedDuration = reps * typicalSecs + setupSecs; // 10 × 3.5 + 10 = 45
|
|
150
145
|
(0, vitest_1.expect)(parsed[0].activeDurationSecs).toBe(expectedDuration);
|
|
151
146
|
});
|
|
152
147
|
(0, vitest_1.it)("Muscle fatigue shape: quadriceps and glutes-maximus present in [0, 100]", () => {
|
|
@@ -171,143 +166,161 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
171
166
|
});
|
|
172
167
|
(0, vitest_1.expect)(Object.keys(muscleScores).length).toBe(0);
|
|
173
168
|
});
|
|
174
|
-
(0, vitest_1.it)("
|
|
169
|
+
(0, vitest_1.it)("No guardrails — does not crash, score > 0", () => {
|
|
175
170
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard;
|
|
176
|
-
const {
|
|
171
|
+
const { score } = (0, index_1.calculateExerciseScoreV2)({
|
|
177
172
|
exercise: exercises_mock_1.mockExerciseNoGuardrails,
|
|
178
173
|
record: template.initialRecords,
|
|
179
174
|
user: user_mock_1.mockUser,
|
|
180
175
|
});
|
|
181
|
-
const { score } = scoresByGoal.hypertrophy;
|
|
182
176
|
(0, vitest_1.expect)(score).toBeGreaterThan(0);
|
|
183
177
|
});
|
|
184
178
|
(0, vitest_1.it)("RPE/RIR scoring: effortAdequacy calculates correctly for various inputs", () => {
|
|
179
|
+
// RPE 8 → in zone (6–9) → 100
|
|
180
|
+
// RPE 4 → 2 units below zone → 100 - 2×20 = 60
|
|
181
|
+
// RIR 0 → 1 unit below zone (1–4) → 100 - 1×20 = 80
|
|
182
|
+
// no data → EFFORT_NO_DATA_SCORE = 50
|
|
183
|
+
// average: (100 + 60 + 80 + 50) / 4 = 72.5 → 73
|
|
185
184
|
const records = [
|
|
186
|
-
{
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
{
|
|
186
|
+
type: "weight-reps",
|
|
187
|
+
isDone: true,
|
|
188
|
+
isStrictMode: false,
|
|
189
|
+
kg: "50",
|
|
190
|
+
reps: "10",
|
|
191
|
+
rpe: "8",
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
type: "weight-reps",
|
|
195
|
+
isDone: true,
|
|
196
|
+
isStrictMode: false,
|
|
197
|
+
kg: "50",
|
|
198
|
+
reps: "10",
|
|
199
|
+
rpe: "4",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
type: "weight-reps",
|
|
203
|
+
isDone: true,
|
|
204
|
+
isStrictMode: false,
|
|
205
|
+
kg: "50",
|
|
206
|
+
reps: "10",
|
|
207
|
+
rir: "0",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
type: "weight-reps",
|
|
211
|
+
isDone: true,
|
|
212
|
+
isStrictMode: false,
|
|
213
|
+
kg: "50",
|
|
214
|
+
reps: "10",
|
|
215
|
+
},
|
|
190
216
|
];
|
|
191
|
-
const {
|
|
217
|
+
const { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
|
|
192
218
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
193
219
|
record: records,
|
|
194
220
|
user: user_mock_1.mockUser,
|
|
195
221
|
});
|
|
196
|
-
|
|
197
|
-
// Average: (100 + 60 + 80 + 70) / 4 = 77.5 -> 78
|
|
198
|
-
(0, vitest_1.expect)(qualityBreakdown.effortAdequacy).toBe(78);
|
|
222
|
+
(0, vitest_1.expect)(qualityBreakdown.effortAdequacy).toBe(69);
|
|
199
223
|
});
|
|
200
|
-
(0, vitest_1.it)("Progressive overload detection:
|
|
224
|
+
(0, vitest_1.it)("Progressive overload detection: micro-loaded template → consistency >= 75", () => {
|
|
201
225
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsMicroLoaded;
|
|
202
|
-
const {
|
|
226
|
+
const { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
|
|
203
227
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
204
228
|
record: template.initialRecords,
|
|
205
229
|
user: user_mock_1.mockUser,
|
|
206
230
|
});
|
|
207
|
-
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
208
231
|
(0, vitest_1.expect)(qualityBreakdown.consistency).toBeGreaterThanOrEqual(75);
|
|
209
232
|
});
|
|
210
|
-
(0, vitest_1.it)("Single-set
|
|
233
|
+
(0, vitest_1.it)("Single-set — only set is the last set → no evaluable rest → restDisciplineActive false", () => {
|
|
234
|
+
// mockTemplateSingleSet has restDurationSecs: 180 in the raw record.
|
|
235
|
+
// hasRawRestData = true (raw data exists). BUT parseRecords marks the only
|
|
236
|
+
// set as last-set → parsedSet.restDurationSecs = null.
|
|
237
|
+
// calculateRestDiscipline: setsWithRest = [] → returns null.
|
|
238
|
+
// restDisciplineActive = false — no evaluable data despite raw value existing.
|
|
211
239
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.singleSet;
|
|
212
|
-
const
|
|
213
|
-
const { restDurationSecs } = r, rest = __rest(r, ["restDurationSecs"]); // remove rest data so it becomes the null case
|
|
214
|
-
return Object.assign(Object.assign({}, rest), { isStrictMode: true });
|
|
215
|
-
});
|
|
216
|
-
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
240
|
+
const { qualityBreakdown, restDisciplineActive } = (0, index_1.calculateExerciseScoreV2)({
|
|
217
241
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
218
|
-
record:
|
|
242
|
+
record: template.initialRecords,
|
|
219
243
|
user: user_mock_1.mockUser,
|
|
220
244
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(75);
|
|
245
|
+
(0, vitest_1.expect)(restDisciplineActive).toBe(false);
|
|
246
|
+
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(0);
|
|
224
247
|
});
|
|
225
|
-
(0, vitest_1.it)("Non-sequential completion:
|
|
248
|
+
(0, vitest_1.it)("Non-sequential completion: 2 of 3 records done → completion 67%", () => {
|
|
226
249
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.nonSequential;
|
|
227
|
-
const {
|
|
250
|
+
const { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
|
|
228
251
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
229
252
|
record: template.initialRecords,
|
|
230
253
|
user: user_mock_1.mockUser,
|
|
231
254
|
});
|
|
232
|
-
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
233
255
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(67);
|
|
234
256
|
});
|
|
235
|
-
(0, vitest_1.it)("IScoreResult
|
|
257
|
+
(0, vitest_1.it)("IScoreResult shape: flat Option A — score, qualityBreakdown, muscleScores, calorieBurn, restDisciplineActive", () => {
|
|
236
258
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard;
|
|
237
259
|
const result = (0, index_1.calculateExerciseScoreV2)({
|
|
238
260
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
239
261
|
record: template.initialRecords,
|
|
240
262
|
user: user_mock_1.mockUser,
|
|
241
263
|
});
|
|
242
|
-
(0, vitest_1.expect)(result).toHaveProperty("
|
|
264
|
+
(0, vitest_1.expect)(result).toHaveProperty("score");
|
|
265
|
+
(0, vitest_1.expect)(result).toHaveProperty("qualityBreakdown");
|
|
243
266
|
(0, vitest_1.expect)(result).toHaveProperty("muscleScores");
|
|
244
267
|
(0, vitest_1.expect)(result).toHaveProperty("calorieBurn");
|
|
268
|
+
(0, vitest_1.expect)(result).toHaveProperty("restDisciplineActive");
|
|
269
|
+
// Confirm scoresByGoal is gone
|
|
270
|
+
(0, vitest_1.expect)(result).not.toHaveProperty("scoresByGoal");
|
|
271
|
+
(0, vitest_1.expect)(typeof result.score).toBe("number");
|
|
272
|
+
(0, vitest_1.expect)(result.calorieBurn).toBe(0); // Phase 3 placeholder
|
|
273
|
+
(0, vitest_1.expect)(typeof result.restDisciplineActive).toBe("boolean");
|
|
245
274
|
});
|
|
246
|
-
(0, vitest_1.it)("Cardio exercises:
|
|
275
|
+
(0, vitest_1.it)("Cardio exercises: machine and free both score > 0 and map correct muscles", () => {
|
|
247
276
|
const machineResult = (0, index_1.calculateExerciseScoreV2)({
|
|
248
277
|
exercise: exercises_mock_1.mockExerciseCardioMachine,
|
|
249
278
|
record: templateExercises_mock_1.mockTemplateExercisesDictionary.cardioMachine.initialRecords,
|
|
250
279
|
user: user_mock_1.mockUser,
|
|
251
280
|
});
|
|
252
|
-
(0, vitest_1.expect)(machineResult.
|
|
281
|
+
(0, vitest_1.expect)(machineResult.score).toBeGreaterThan(0);
|
|
253
282
|
(0, vitest_1.expect)(machineResult.muscleScores).toHaveProperty("quadriceps");
|
|
254
283
|
const freeResult = (0, index_1.calculateExerciseScoreV2)({
|
|
255
284
|
exercise: exercises_mock_1.mockExerciseCardioFree,
|
|
256
285
|
record: templateExercises_mock_1.mockTemplateExercisesDictionary.cardioFree.initialRecords,
|
|
257
286
|
user: user_mock_1.mockUser,
|
|
258
287
|
});
|
|
259
|
-
(0, vitest_1.expect)(freeResult.
|
|
288
|
+
(0, vitest_1.expect)(freeResult.score).toBeGreaterThan(0);
|
|
260
289
|
(0, vitest_1.expect)(freeResult.muscleScores).toHaveProperty("quadriceps");
|
|
261
290
|
});
|
|
262
|
-
(0, vitest_1.it)("reps-only with aux weight:
|
|
291
|
+
(0, vitest_1.it)("reps-only with aux weight: score > 0, pectoralis-major in muscleScores", () => {
|
|
263
292
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.repsOnly;
|
|
264
|
-
const {
|
|
293
|
+
const { score, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
|
|
265
294
|
exercise: exercises_mock_1.mockExerciseRepsOnly,
|
|
266
295
|
record: template.initialRecords,
|
|
267
296
|
user: user_mock_1.mockUser,
|
|
268
297
|
});
|
|
269
|
-
const { score } = scoresByGoal.hypertrophy;
|
|
270
298
|
(0, vitest_1.expect)(score).toBeGreaterThan(0);
|
|
271
299
|
(0, vitest_1.expect)(muscleScores).toHaveProperty("pectoralis-major");
|
|
272
300
|
});
|
|
273
|
-
(0, vitest_1.it)("Duration exercise:
|
|
301
|
+
(0, vitest_1.it)("Duration exercise: score > 0, abs-lower and abs-upper in muscleScores", () => {
|
|
274
302
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.duration;
|
|
275
|
-
const {
|
|
303
|
+
const { score, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
|
|
276
304
|
exercise: exercises_mock_1.mockExerciseDuration,
|
|
277
305
|
record: template.initialRecords,
|
|
278
306
|
user: user_mock_1.mockUser,
|
|
279
307
|
});
|
|
280
|
-
const { score } = scoresByGoal.hypertrophy;
|
|
281
308
|
(0, vitest_1.expect)(score).toBeGreaterThan(0);
|
|
282
309
|
(0, vitest_1.expect)(muscleScores).toHaveProperty("abs-lower");
|
|
283
310
|
(0, vitest_1.expect)(muscleScores).toHaveProperty("abs-upper");
|
|
284
311
|
});
|
|
285
|
-
(0, vitest_1.it)("
|
|
286
|
-
|
|
287
|
-
//
|
|
288
|
-
|
|
312
|
+
(0, vitest_1.it)("No rest data logged (standard app flow) → restDiscipline 0, restDisciplineActive false", () => {
|
|
313
|
+
// lazyLogger has no restDurationSecs on any raw record — the user batched
|
|
314
|
+
// their input after the workout and didn't track rest time at all.
|
|
315
|
+
// hasRawRestData = false → rest excluded → weight redistributed.
|
|
316
|
+
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.lazyLogger;
|
|
317
|
+
const { qualityBreakdown, restDisciplineActive } = (0, index_1.calculateExerciseScoreV2)({
|
|
289
318
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
290
319
|
record: template.initialRecords,
|
|
291
320
|
user: user_mock_1.mockUser,
|
|
292
321
|
});
|
|
293
|
-
|
|
294
|
-
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(
|
|
295
|
-
});
|
|
296
|
-
(0, vitest_1.it)("Parallel scoring: strength score differs from endurance score for identical heavy workout", () => {
|
|
297
|
-
const records = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard.initialRecords;
|
|
298
|
-
// Give user two vastly different goals
|
|
299
|
-
const multiGoalUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessGoals: ["strength", "endurance"] });
|
|
300
|
-
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
301
|
-
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
302
|
-
record: records,
|
|
303
|
-
user: multiGoalUser,
|
|
304
|
-
});
|
|
305
|
-
(0, vitest_1.expect)(scoresByGoal).toHaveProperty("strength");
|
|
306
|
-
(0, vitest_1.expect)(scoresByGoal).toHaveProperty("endurance");
|
|
307
|
-
const strengthScore = scoresByGoal.strength.score;
|
|
308
|
-
const enduranceScore = scoresByGoal.endurance.score;
|
|
309
|
-
// The weights and effort math naturally produce different results
|
|
310
|
-
(0, vitest_1.expect)(strengthScore).not.toEqual(enduranceScore);
|
|
322
|
+
(0, vitest_1.expect)(restDisciplineActive).toBe(false);
|
|
323
|
+
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(0);
|
|
311
324
|
});
|
|
312
325
|
});
|
|
313
326
|
(0, vitest_1.describe)("Scoring Engine Phase 2 Integration Tests", () => {
|
|
@@ -316,63 +329,33 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
316
329
|
// ---------------------------------------------------------------------------
|
|
317
330
|
(0, vitest_1.it)("P2-1: sedentary user scores higher muscle fatigue than very-active for identical work", () => {
|
|
318
331
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard;
|
|
319
|
-
const sedentaryUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessLevel: "sedentary" });
|
|
320
|
-
const veryActiveUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessLevel: "very-active" });
|
|
321
332
|
const sedentaryResult = (0, index_1.calculateExerciseScoreV2)({
|
|
322
333
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
323
334
|
record: template.initialRecords,
|
|
324
|
-
user:
|
|
335
|
+
user: Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessLevel: "sedentary" }),
|
|
325
336
|
});
|
|
326
337
|
const veryActiveResult = (0, index_1.calculateExerciseScoreV2)({
|
|
327
338
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
328
339
|
record: template.initialRecords,
|
|
329
|
-
user:
|
|
340
|
+
user: Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessLevel: "very-active" }),
|
|
330
341
|
});
|
|
331
|
-
// sedentary: activityScale 0.70 → lower referenceMax
|
|
332
|
-
// very-active: activityScale 1.20 → higher referenceMax
|
|
342
|
+
// sedentary: activityScale 0.70 → lower referenceMax → higher normalised fatigue
|
|
343
|
+
// very-active: activityScale 1.20 → higher referenceMax → lower normalised fatigue
|
|
333
344
|
(0, vitest_1.expect)(sedentaryResult.muscleScores["quadriceps"]).toBeGreaterThan(veryActiveResult.muscleScores["quadriceps"]);
|
|
334
345
|
});
|
|
335
346
|
// ---------------------------------------------------------------------------
|
|
336
|
-
// P2-3: fitnessGoal-aware quality weights
|
|
337
|
-
// ---------------------------------------------------------------------------
|
|
338
|
-
(0, vitest_1.it)("P2-3: strength goal scores higher than hypertrophy when effort is good but completion is 50%", () => {
|
|
339
|
-
// 2 of 4 sets done (50% completion) with RPE 8 (effort in productive zone)
|
|
340
|
-
// strength weights effortAdequacy 0.45 vs hypertrophy 0.30 — effort matters more
|
|
341
|
-
const partialWithEffort = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsPartial.initialRecords.map((r) => (Object.assign(Object.assign({}, r), { rpe: "8" })));
|
|
342
|
-
const strengthUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessGoals: ["strength"] });
|
|
343
|
-
const hypertrophyUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessGoals: ["hypertrophy"] });
|
|
344
|
-
const strengthResult = (0, index_1.calculateExerciseScoreV2)({
|
|
345
|
-
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
346
|
-
record: partialWithEffort,
|
|
347
|
-
user: strengthUser,
|
|
348
|
-
});
|
|
349
|
-
const hypertrophyResult = (0, index_1.calculateExerciseScoreV2)({
|
|
350
|
-
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
351
|
-
record: partialWithEffort,
|
|
352
|
-
user: hypertrophyUser,
|
|
353
|
-
});
|
|
354
|
-
// completion=50, consistency=100, effortAdequacy=100, restDiscipline=75
|
|
355
|
-
// strength: 50×0.15 + 100×0.25 + 100×0.45 + 75×0.15 = 88.75 → 89
|
|
356
|
-
// hypertrophy: 50×0.20 + 100×0.35 + 100×0.30 + 75×0.15 = 86.25 → 86
|
|
357
|
-
(0, vitest_1.expect)(strengthResult.scoresByGoal.strength.score).toBe(89);
|
|
358
|
-
(0, vitest_1.expect)(hypertrophyResult.scoresByGoal.hypertrophy.score).toBe(86);
|
|
359
|
-
(0, vitest_1.expect)(strengthResult.scoresByGoal.strength.score).toBeGreaterThan(hypertrophyResult.scoresByGoal.hypertrophy.score);
|
|
360
|
-
});
|
|
361
|
-
// ---------------------------------------------------------------------------
|
|
362
347
|
// P2-5: estimatedOneRepMax in referenceMax
|
|
363
348
|
// ---------------------------------------------------------------------------
|
|
364
|
-
(0, vitest_1.it)("P2-5: user near their 1RM shows higher muscle fatigue than a strong user doing
|
|
349
|
+
(0, vitest_1.it)("P2-5: user near their 1RM shows higher muscle fatigue than a strong user doing same weight", () => {
|
|
365
350
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard;
|
|
366
|
-
// Weak user: 1RM=60kg —
|
|
367
|
-
// so referenceMax is low → same absolute volume normalises to a higher score
|
|
351
|
+
// Weak user: 1RM=60kg — session top set (85kg) exceeds their 1RM → low referenceMax → high fatigue score
|
|
368
352
|
const weakUserResult = (0, index_1.calculateExerciseScoreV2)({
|
|
369
353
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
370
354
|
record: template.initialRecords,
|
|
371
355
|
user: user_mock_1.mockUser,
|
|
372
356
|
historicalContext: { estimatedOneRepMax: 60 },
|
|
373
357
|
});
|
|
374
|
-
// Strong user: 1RM=200kg — 85kg is
|
|
375
|
-
// referenceMax is high → same absolute volume normalises to a lower score
|
|
358
|
+
// Strong user: 1RM=200kg — 85kg is ~43% of max → high referenceMax → low fatigue score
|
|
376
359
|
const strongUserResult = (0, index_1.calculateExerciseScoreV2)({
|
|
377
360
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
378
361
|
record: template.initialRecords,
|
|
@@ -385,43 +368,78 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
385
368
|
// P2-6: previousSessionVolume cross-session progressive overload
|
|
386
369
|
// ---------------------------------------------------------------------------
|
|
387
370
|
(0, vitest_1.it)("P2-6: beating previous session volume floors consistency at 75 even with high CV", () => {
|
|
388
|
-
// Highly inconsistent sets: [
|
|
389
|
-
// volume metric: [1000, 250, 720]
|
|
390
|
-
// CV ≈ 0.47 → base consistency score = clamp(100 - 0.47×200, 20, 100) = 20
|
|
371
|
+
// Highly inconsistent sets: [100×10, 50×5, 90×8]
|
|
372
|
+
// volume metric: [1000, 250, 720], CV ≈ 0.47 → base consistency = 20 (CONSISTENCY_MIN_SCORE)
|
|
391
373
|
const inconsistentRecords = [
|
|
392
|
-
{
|
|
393
|
-
|
|
394
|
-
|
|
374
|
+
{
|
|
375
|
+
type: "weight-reps",
|
|
376
|
+
isDone: true,
|
|
377
|
+
isStrictMode: false,
|
|
378
|
+
kg: "100",
|
|
379
|
+
reps: "10",
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
type: "weight-reps",
|
|
383
|
+
isDone: true,
|
|
384
|
+
isStrictMode: false,
|
|
385
|
+
kg: "50",
|
|
386
|
+
reps: "5",
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
type: "weight-reps",
|
|
390
|
+
isDone: true,
|
|
391
|
+
isStrictMode: false,
|
|
392
|
+
kg: "90",
|
|
393
|
+
reps: "8",
|
|
394
|
+
},
|
|
395
395
|
];
|
|
396
396
|
const withoutContext = (0, index_1.calculateExerciseScoreV2)({
|
|
397
397
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
398
398
|
record: inconsistentRecords,
|
|
399
399
|
user: user_mock_1.mockUser,
|
|
400
400
|
});
|
|
401
|
-
//
|
|
401
|
+
// currentVolume = 1970, previousSessionVolume = 1000 → 1970 >= 1000 → progressive overload floor
|
|
402
402
|
const withContext = (0, index_1.calculateExerciseScoreV2)({
|
|
403
403
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
404
404
|
record: inconsistentRecords,
|
|
405
405
|
user: user_mock_1.mockUser,
|
|
406
406
|
historicalContext: { previousSessionVolume: 1000 },
|
|
407
407
|
});
|
|
408
|
-
(0, vitest_1.expect)(withoutContext.
|
|
409
|
-
(0, vitest_1.expect)(withContext.
|
|
408
|
+
(0, vitest_1.expect)(withoutContext.qualityBreakdown.consistency).toBe(20); // CONSISTENCY_MIN_SCORE
|
|
409
|
+
(0, vitest_1.expect)(withContext.qualityBreakdown.consistency).toBe(75); // PROGRESSIVE_OVERLOAD_FLOOR
|
|
410
410
|
});
|
|
411
411
|
(0, vitest_1.it)("P2-6: not beating previous session volume does NOT trigger the floor", () => {
|
|
412
412
|
const inconsistentRecords = [
|
|
413
|
-
{
|
|
414
|
-
|
|
415
|
-
|
|
413
|
+
{
|
|
414
|
+
type: "weight-reps",
|
|
415
|
+
isDone: true,
|
|
416
|
+
isStrictMode: false,
|
|
417
|
+
kg: "100",
|
|
418
|
+
reps: "10",
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
type: "weight-reps",
|
|
422
|
+
isDone: true,
|
|
423
|
+
isStrictMode: false,
|
|
424
|
+
kg: "50",
|
|
425
|
+
reps: "5",
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
type: "weight-reps",
|
|
429
|
+
isDone: true,
|
|
430
|
+
isStrictMode: false,
|
|
431
|
+
kg: "90",
|
|
432
|
+
reps: "8",
|
|
433
|
+
},
|
|
416
434
|
];
|
|
417
|
-
//
|
|
418
|
-
const
|
|
435
|
+
// currentVolume = 1970, previousSessionVolume = 3000 → 1970 < 3000 → no floor
|
|
436
|
+
const { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
|
|
419
437
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
420
438
|
record: inconsistentRecords,
|
|
421
439
|
user: user_mock_1.mockUser,
|
|
422
440
|
historicalContext: { previousSessionVolume: 3000 },
|
|
423
441
|
});
|
|
424
|
-
(0, vitest_1.expect)(
|
|
442
|
+
(0, vitest_1.expect)(qualityBreakdown.consistency).toBe(20);
|
|
425
443
|
});
|
|
426
444
|
// ---------------------------------------------------------------------------
|
|
427
445
|
// P2-10: trainingAgeBracket RIR attenuation + referenceMax scaling
|
|
@@ -430,9 +448,30 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
430
448
|
// RIR 0 = near-failure: effortFraction ≈ 1.3 without attenuation
|
|
431
449
|
// With beginner: effortFraction = 0.6 + (1.3 - 0.6) × 0.5 = 0.95
|
|
432
450
|
const nearFailureRecords = [
|
|
433
|
-
{
|
|
434
|
-
|
|
435
|
-
|
|
451
|
+
{
|
|
452
|
+
type: "weight-reps",
|
|
453
|
+
isDone: true,
|
|
454
|
+
isStrictMode: false,
|
|
455
|
+
kg: "80",
|
|
456
|
+
reps: "5",
|
|
457
|
+
rir: "0",
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
type: "weight-reps",
|
|
461
|
+
isDone: true,
|
|
462
|
+
isStrictMode: false,
|
|
463
|
+
kg: "80",
|
|
464
|
+
reps: "5",
|
|
465
|
+
rir: "0",
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
type: "weight-reps",
|
|
469
|
+
isDone: true,
|
|
470
|
+
isStrictMode: false,
|
|
471
|
+
kg: "80",
|
|
472
|
+
reps: "5",
|
|
473
|
+
rir: "0",
|
|
474
|
+
},
|
|
436
475
|
];
|
|
437
476
|
const noContext = (0, index_1.calculateExerciseScoreV2)({
|
|
438
477
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
@@ -445,8 +484,7 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
445
484
|
user: user_mock_1.mockUser,
|
|
446
485
|
historicalContext: { trainingAgeBracket: "beginner" },
|
|
447
486
|
});
|
|
448
|
-
// Beginner: lower effort (attenuated) + lower referenceMax (0.80 scale)
|
|
449
|
-
// Net effect: ~9% lower score. Verified: noContext ≈ 20, beginnerContext ≈ 18
|
|
487
|
+
// Beginner: lower effort (attenuated) + lower referenceMax (0.80 scale) → lower fatigue score
|
|
450
488
|
(0, vitest_1.expect)(beginnerContext.muscleScores["quadriceps"]).toBeLessThan(noContext.muscleScores["quadriceps"]);
|
|
451
489
|
});
|
|
452
490
|
(0, vitest_1.it)("P2-10: advanced bracket raises referenceMax → lower fatigue score for same work than intermediate", () => {
|