@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.
Files changed (79) hide show
  1. package/README.md +148 -148
  2. package/dist/__mocks__/exercises.mock.js +1 -0
  3. package/dist/constants/index.d.ts +1 -0
  4. package/dist/constants/index.js +1 -0
  5. package/dist/constants/quickStartIntents.d.ts +19 -0
  6. package/dist/constants/quickStartIntents.js +39 -0
  7. package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
  8. package/dist/types/TApiClientConstellation.d.ts +33 -0
  9. package/dist/types/TApiClientConstellation.js +13 -0
  10. package/dist/types/TApiExercise.d.ts +5 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
  13. package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
  14. package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
  15. package/dist/utils/constellation/evaluateConstellation.js +135 -0
  16. package/dist/utils/constellation/index.d.ts +17 -0
  17. package/dist/utils/constellation/index.js +26 -0
  18. package/dist/utils/constellation/levelThresholds.d.ts +99 -0
  19. package/dist/utils/constellation/levelThresholds.js +123 -0
  20. package/dist/utils/constellation/starFoundation.d.ts +25 -0
  21. package/dist/utils/constellation/starFoundation.js +54 -0
  22. package/dist/utils/constellation/stars/consistency.d.ts +29 -0
  23. package/dist/utils/constellation/stars/consistency.js +142 -0
  24. package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
  25. package/dist/utils/constellation/stars/lowerBody.js +30 -0
  26. package/dist/utils/constellation/stars/pull.d.ts +11 -0
  27. package/dist/utils/constellation/stars/pull.js +24 -0
  28. package/dist/utils/constellation/stars/push.d.ts +11 -0
  29. package/dist/utils/constellation/stars/push.js +24 -0
  30. package/dist/utils/constellation/stars/quality.d.ts +19 -0
  31. package/dist/utils/constellation/stars/quality.js +98 -0
  32. package/dist/utils/constellation/stars/recovery.d.ts +29 -0
  33. package/dist/utils/constellation/stars/recovery.js +169 -0
  34. package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
  35. package/dist/utils/constellation/strengthStarHelpers.js +104 -0
  36. package/dist/utils/constellation/types.d.ts +124 -0
  37. package/dist/utils/constellation/types.js +18 -0
  38. package/dist/utils/index.d.ts +5 -3
  39. package/dist/utils/index.js +1 -0
  40. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
  41. package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
  42. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
  43. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
  44. package/dist/utils/scoringWorkout/constants.d.ts +20 -6
  45. package/dist/utils/scoringWorkout/constants.js +23 -9
  46. package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
  47. package/dist/utils/scoringWorkout/helpers.js +24 -18
  48. package/dist/utils/scoringWorkout/index.d.ts +12 -8
  49. package/dist/utils/scoringWorkout/index.js +23 -15
  50. package/dist/utils/scoringWorkout/parseRecords.js +4 -3
  51. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +210 -172
  52. package/dist/utils/scoringWorkout/types.d.ts +34 -14
  53. package/package.json +31 -31
  54. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
  55. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
  56. package/dist/utils/scaleProPlan.util.d.ts +0 -9
  57. package/dist/utils/scaleProPlan.util.js +0 -139
  58. package/dist/utils/scoring/calculateCalories.d.ts +0 -67
  59. package/dist/utils/scoring/calculateCalories.js +0 -345
  60. package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
  61. package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
  62. package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
  63. package/dist/utils/scoring/calculateQualityScore.js +0 -334
  64. package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
  65. package/dist/utils/scoring/calculateTotalVolume.js +0 -73
  66. package/dist/utils/scoring/constants.d.ts +0 -211
  67. package/dist/utils/scoring/constants.js +0 -247
  68. package/dist/utils/scoring/helpers.d.ts +0 -119
  69. package/dist/utils/scoring/helpers.js +0 -229
  70. package/dist/utils/scoring/index.d.ts +0 -28
  71. package/dist/utils/scoring/index.js +0 -47
  72. package/dist/utils/scoring/parseRecords.d.ts +0 -98
  73. package/dist/utils/scoring/parseRecords.js +0 -284
  74. package/dist/utils/scoring/types.d.ts +0 -86
  75. package/dist/utils/scoring/types.js +0 -11
  76. package/dist/utils/scoring.utils.d.ts +0 -14
  77. package/dist/utils/scoring.utils.js +0 -243
  78. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
  79. /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 with undefined rest on last done set + strict mode", () => {
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
- // Modify to prove P1-1: the LAST done set (index 1) must have NO rest data.
25
- // Set 0 has good rest, Set 1 has no rest. If P1-1 is fixed, Set 1 gets null rest
26
- // and is excluded from the rest average, meaning only Set 0's perfect rest is scored.
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"]); // remove rest data
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 }); // 120 is optimal
31
+ return Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: 120 });
33
32
  });
34
- // Direct parseRecords assertion the real proof of P1-1.
35
- // With fix (totalSets=doneRecords.length=2): Set1 index=1 === 2-1 → null rest.
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
- const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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: strictRecords,
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
- // Only Set0 (120s, optimal) contributes to rest — Set1 is correctly excluded
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: mockTemplateWeightRepsFailedSet - zero rep set still counts towards completion if isDone is true", () => {
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 exercise = exercises_mock_1.mockExerciseWeightReps;
52
- const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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: mockTemplateLazyLogger - handles missing data gracefully using fallbacks", () => {
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 exercise = exercises_mock_1.mockExerciseWeightReps;
63
- const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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: clamping path (Original 600s rest clamped to typical → good rest score)", () => {
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
- // Use 600s rest. 600 > 300 * 1.5, so it gets clamped to typical (120s)
76
- // 120s is in the optimal range (90-180), so rest score should be 100.
77
- const strictRecords = template.initialRecords.map((r, i) => (Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: i === 0 ? 600 : 120 })));
78
- const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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: strictRecords,
74
+ record: records,
81
75
  user: user_mock_1.mockUser,
82
76
  });
83
- const { qualityBreakdown } = scoresByGoal.hypertrophy;
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: penalty path (350s rest outside acceptableMax → score 50)", () => {
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
- // 350s is within grace bounds (450s) but outside acceptableMax (300s).
91
- const strictRecords = template.initialRecords.map((r, i) => (Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: i === 0 ? 350 : 120 })));
92
- const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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: strictRecords,
89
+ record: records,
95
90
  user: user_mock_1.mockUser,
96
91
  });
97
- const { qualityBreakdown } = scoresByGoal.hypertrophy;
98
- // Set 1 (350s) -> 50. Set 2 (120s) -> 100. Avg = 75. Plus 5 point bonus = 80.
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)("mockTemplateAllUndone - Score = 0, no errors", () => {
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 exercise = exercises_mock_1.mockExerciseWeightReps;
104
- const { scoresByGoal, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
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: low muscleScores (3s duration primary muscle value < 5)", () => {
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", // 3 seconds
117
+ durationMmSs: "00:03",
125
118
  auxWeightKg: "0",
126
- }
119
+ },
127
120
  ];
128
- const { scoresByGoal, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
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
- (0, vitest_1.expect)(score).toBe(87);
136
- // Check fatigue pillar correctness
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" ? ((_b = (_a = guardrails.singleRep) === null || _a === void 0 ? void 0 : _a.typical) !== null && _b !== void 0 ? _b : 0) : 0;
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 * 3.5 + 10 = 45
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)("no guardrails doesn't crash: mockExerciseNoGuardrails scores > 0", () => {
169
+ (0, vitest_1.it)("No guardrails does not crash, score > 0", () => {
175
170
  const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard;
176
- const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "50", reps: "10", rpe: "8" }, // 100
187
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "50", reps: "10", rpe: "4" }, // 60 (Distance = 2 -> 100 - 2*20 = 60)
188
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "50", reps: "10", rir: "0" }, // 80 (Distance = 1 -> 100 - 1*20 = 80)
189
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "50", reps: "10" } // 70 (No data)
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 { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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
- const { qualityBreakdown } = scoresByGoal.hypertrophy;
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: mockTemplateWeightRepsMicroLoaded -> consistency >= 75", () => {
224
+ (0, vitest_1.it)("Progressive overload detection: micro-loaded template consistency >= 75", () => {
201
225
  const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsMicroLoaded;
202
- const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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 rest is neutral: mockTemplateSingleSet -> restDiscipline = 75", () => {
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 strictRecords = template.initialRecords.map(r => {
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: strictRecords,
242
+ record: template.initialRecords,
219
243
  user: user_mock_1.mockUser,
220
244
  });
221
- const { qualityBreakdown } = scoresByGoal.hypertrophy;
222
- // Single set → last set gets null rest → setsWithRest=[] → REST_NO_DATA_SCORE (75)
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: mockTemplateNonSequential -> completion = 67%", () => {
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 { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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 has 4 fields: calorieBurn and qualityBreakdown are present", () => {
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("scoresByGoal");
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: mockTemplateCardioMachine/Free score > 0 and use correct muscles", () => {
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.scoresByGoal.hypertrophy.score).toBeGreaterThan(0);
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.scoresByGoal.hypertrophy.score).toBeGreaterThan(0);
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: mockTemplateRepsOnly -> score > 0, pectoralis-major mapped", () => {
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 { scoresByGoal, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
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: mockTemplateDuration -> abs-lower/abs-upper in muscleScores", () => {
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 { scoresByGoal, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
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)("Default restDiscipline (non-strict): Any non-strict call -> restDiscipline === 75", () => {
286
- const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard;
287
- // Standard mock is `isStrictMode: false`
288
- const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
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
- const { qualityBreakdown } = scoresByGoal.hypertrophy;
294
- (0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(75); // REST_NO_DATA_SCORE from constants.ts
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: sedentaryUser,
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: veryActiveUser,
340
+ user: Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessLevel: "very-active" }),
330
341
  });
331
- // sedentary: activityScale 0.70 → lower referenceMax denominator → higher normalised score
332
- // very-active: activityScale 1.20 → higher referenceMax denominator → lower normalised score
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 the same weight", () => {
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 — the session's top set (85kg) exceeds their 1RM,
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 only ~43% of max → low relative effort
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: [100kg×10, 50kg×5, 90kg×8]
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
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "100", reps: "10" },
393
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "50", reps: "5" },
394
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "90", reps: "8" },
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
- // currentSessionVolume = 1970, previousSessionVolume = 1000 → 1970 >= 1000 → progressive
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.scoresByGoal.hypertrophy.qualityBreakdown.consistency).toBe(20); // CONSISTENCY_MIN_SCORE
409
- (0, vitest_1.expect)(withContext.scoresByGoal.hypertrophy.qualityBreakdown.consistency).toBe(75); // PROGRESSIVE_OVERLOAD_FLOOR
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
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "100", reps: "10" },
414
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "50", reps: "5" },
415
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "90", reps: "8" },
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
- // currentSessionVolume = 1970, previousSessionVolume = 3000 → 1970 < 3000 → no floor
418
- const result = (0, index_1.calculateExerciseScoreV2)({
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)(result.scoresByGoal.hypertrophy.qualityBreakdown.consistency).toBe(20); // no floor applied
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
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "80", reps: "5", rir: "0" },
434
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "80", reps: "5", rir: "0" },
435
- { type: "weight-reps", isDone: true, isStrictMode: false, kg: "80", reps: "5", rir: "0" },
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", () => {