@dgpholdings/greatoak-shared 1.2.53 → 1.2.55

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 (42) hide show
  1. package/dist/__mocks__/exercises.mock.d.ts +35 -0
  2. package/dist/__mocks__/exercises.mock.js +144 -0
  3. package/dist/__mocks__/templateExercises.mock.d.ts +90 -0
  4. package/dist/__mocks__/templateExercises.mock.js +258 -0
  5. package/dist/__mocks__/user.mock.d.ts +2 -0
  6. package/dist/__mocks__/user.mock.js +36 -0
  7. package/dist/types/TApiUser.d.ts +3 -1
  8. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +30 -0
  9. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +138 -0
  10. package/dist/utils/exerciseRecord/recordValidator.d.ts +12 -0
  11. package/dist/utils/exerciseRecord/recordValidator.integration.test.d.ts +1 -0
  12. package/dist/utils/exerciseRecord/recordValidator.integration.test.js +51 -0
  13. package/dist/utils/exerciseRecord/recordValidator.js +85 -0
  14. package/dist/utils/exerciseRecord/recordValidator.test.d.ts +1 -0
  15. package/dist/utils/exerciseRecord/recordValidator.test.js +165 -0
  16. package/dist/utils/exerciseRecord/workoutMath.d.ts +28 -0
  17. package/dist/utils/exerciseRecord/workoutMath.js +116 -0
  18. package/dist/utils/exerciseRecord/workoutMath.test.d.ts +1 -0
  19. package/dist/utils/exerciseRecord/workoutMath.test.js +238 -0
  20. package/dist/utils/index.d.ts +3 -1
  21. package/dist/utils/index.js +5 -3
  22. package/dist/utils/scoringWorkout/calculateCalories.d.ts +67 -0
  23. package/dist/utils/scoringWorkout/calculateCalories.js +351 -0
  24. package/dist/utils/scoringWorkout/calculateMuscleFatiue.d.ts +67 -0
  25. package/dist/utils/scoringWorkout/calculateMuscleFatiue.js +330 -0
  26. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +73 -0
  27. package/dist/utils/scoringWorkout/calculateQualityScore.js +357 -0
  28. package/dist/utils/scoringWorkout/calculateTotalVolume.d.ts +15 -0
  29. package/dist/utils/scoringWorkout/calculateTotalVolume.js +73 -0
  30. package/dist/utils/scoringWorkout/constants.d.ts +211 -0
  31. package/dist/utils/scoringWorkout/constants.js +247 -0
  32. package/dist/utils/scoringWorkout/helpers.d.ts +127 -0
  33. package/dist/utils/scoringWorkout/helpers.js +245 -0
  34. package/dist/utils/scoringWorkout/index.d.ts +27 -0
  35. package/dist/utils/scoringWorkout/index.js +56 -0
  36. package/dist/utils/scoringWorkout/parseRecords.d.ts +68 -0
  37. package/dist/utils/scoringWorkout/parseRecords.js +281 -0
  38. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.d.ts +1 -0
  39. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +439 -0
  40. package/dist/utils/scoringWorkout/types.d.ts +104 -0
  41. package/dist/utils/scoringWorkout/types.js +11 -0
  42. package/package.json +6 -3
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const workoutMath_1 = require("./workoutMath");
5
+ const templateExercises_mock_1 = require("../../__mocks__/templateExercises.mock");
6
+ (0, vitest_1.describe)('workoutMath - Core Utilities', () => {
7
+ (0, vitest_1.describe)('parseMmSsToSecs', () => {
8
+ (0, vitest_1.it)('handles empty or invalid strings safely', () => {
9
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)(undefined)).toBe(0);
10
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)('')).toBe(0);
11
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)('invalid')).toBe(0);
12
+ });
13
+ (0, vitest_1.it)('parses MM:SS formats correctly', () => {
14
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)('00:00')).toBe(0);
15
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)('05:30')).toBe(330);
16
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)('15:00')).toBe(900);
17
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)('99:59')).toBe(5999);
18
+ });
19
+ (0, vitest_1.it)('parses HH:MM:SS formats correctly', () => {
20
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)('01:05:30')).toBe(3930);
21
+ });
22
+ (0, vitest_1.it)('resists NaN propagation from empty segments', () => {
23
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)(':30')).toBe(30); // Uses || 0 fallback
24
+ (0, vitest_1.expect)((0, workoutMath_1.parseMmSsToSecs)('05:')).toBe(300);
25
+ });
26
+ });
27
+ (0, vitest_1.describe)('estimateWorkDuration', () => {
28
+ const benchGuardrails = {
29
+ type: 'weight-reps',
30
+ stressRestBonus: 3,
31
+ fatigueMultiplier: 1.0,
32
+ setupTypicalSecs: 5,
33
+ restPeriods: { minimum: 30, typical: 90, maximum: 180, optimalRange: [60, 120] },
34
+ singleRep: { min: 1.5, max: 4.0, typical: 2.5 },
35
+ };
36
+ (0, vitest_1.it)('returns 0 if set is not done', () => {
37
+ const record = { type: 'weight-reps', isDone: false, isStrictMode: false, kg: '100', reps: '10' };
38
+ (0, vitest_1.expect)((0, workoutMath_1.estimateWorkDuration)(record, benchGuardrails)).toBe(0);
39
+ });
40
+ (0, vitest_1.it)('calculates duration for standard weight-reps set based on guardrails', () => {
41
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '10' };
42
+ // 10 reps * 2.5s typical + 5s setup = 30s
43
+ (0, vitest_1.expect)((0, workoutMath_1.estimateWorkDuration)(record, benchGuardrails)).toBe(30);
44
+ });
45
+ (0, vitest_1.it)('handles reps-only type exactly like weight-reps', () => {
46
+ const record = { type: 'reps-only', isDone: true, isStrictMode: false, reps: '10', auxWeightKg: '' };
47
+ // 10 reps * 2.5s typical + 5s setup = 30s
48
+ (0, vitest_1.expect)((0, workoutMath_1.estimateWorkDuration)(record, benchGuardrails)).toBe(30);
49
+ });
50
+ (0, vitest_1.it)('handles Zero Reps / Failed Set edge case safely', () => {
51
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '150', reps: '0' };
52
+ // 0 reps * 2.5s + 5s setup = 5s (still took time to setup and fail)
53
+ (0, vitest_1.expect)((0, workoutMath_1.estimateWorkDuration)(record, benchGuardrails)).toBe(5);
54
+ });
55
+ (0, vitest_1.it)('guards against negative reps input returning negative duration', () => {
56
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '150', reps: '-5' };
57
+ // Math.max(0, -5) -> 0 reps * 2.5s + 5s setup = 5s
58
+ (0, vitest_1.expect)((0, workoutMath_1.estimateWorkDuration)(record, benchGuardrails)).toBe(5);
59
+ });
60
+ (0, vitest_1.it)('falls back to hardcoded defaults (2.5 rep, 3 setup) if guardrails are completely missing', () => {
61
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '10' };
62
+ // 10 reps * 2.5s fallback + 3s setup fallback = 28s
63
+ (0, vitest_1.expect)((0, workoutMath_1.estimateWorkDuration)(record, undefined)).toBe(28);
64
+ });
65
+ (0, vitest_1.it)('parses duration explicitly for cardio/duration types', () => {
66
+ const record = { type: 'duration', isDone: true, isStrictMode: false, durationMmSs: '02:30', auxWeightKg: '0' };
67
+ (0, vitest_1.expect)((0, workoutMath_1.estimateWorkDuration)(record)).toBe(150);
68
+ });
69
+ (0, vitest_1.it)('parses duration explicitly for cardio-machine type', () => {
70
+ const record = { type: 'cardio-machine', isDone: true, isStrictMode: false, durationMmSs: '05:00' };
71
+ (0, vitest_1.expect)((0, workoutMath_1.estimateWorkDuration)(record)).toBe(300);
72
+ });
73
+ });
74
+ (0, vitest_1.describe)('estimateRestDuration', () => {
75
+ const guardrails = {
76
+ type: 'weight-reps',
77
+ stressRestBonus: 0,
78
+ fatigueMultiplier: 1.0,
79
+ setupTypicalSecs: 5,
80
+ restPeriods: { minimum: 30, typical: 60, maximum: 180, optimalRange: [45, 90] },
81
+ singleRep: { min: 2, max: 3, typical: 2.5 },
82
+ };
83
+ (0, vitest_1.it)('returns 0 if set is not done', () => {
84
+ const record = { type: 'weight-reps', isDone: false, isStrictMode: false, kg: '10', reps: '10' };
85
+ (0, vitest_1.expect)((0, workoutMath_1.estimateRestDuration)(record, guardrails)).toBe(0);
86
+ });
87
+ (0, vitest_1.it)('prioritizes explicit rest duration if valid', () => {
88
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '10', reps: '10', restDurationSecs: 45 };
89
+ (0, vitest_1.expect)((0, workoutMath_1.estimateRestDuration)(record, guardrails)).toBe(45);
90
+ });
91
+ (0, vitest_1.it)('clamps distracted logger rest to maximum guardrail', () => {
92
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '10', reps: '10', restDurationSecs: 500 }; // 500s is > 180s
93
+ (0, vitest_1.expect)((0, workoutMath_1.estimateRestDuration)(record, guardrails)).toBe(180);
94
+ });
95
+ (0, vitest_1.it)('falls back to typical rest if missing', () => {
96
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '10', reps: '10' };
97
+ (0, vitest_1.expect)((0, workoutMath_1.estimateRestDuration)(record, guardrails)).toBe(60);
98
+ });
99
+ (0, vitest_1.it)('falls back to typical rest if explicitly 0 (physically impossible edge case)', () => {
100
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '10', reps: '10', restDurationSecs: 0 };
101
+ (0, vitest_1.expect)((0, workoutMath_1.estimateRestDuration)(record, guardrails)).toBe(60);
102
+ });
103
+ (0, vitest_1.it)('falls back to hardcoded default (60) if guardrails are completely missing', () => {
104
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '10', reps: '10' };
105
+ (0, vitest_1.expect)((0, workoutMath_1.estimateRestDuration)(record, undefined)).toBe(60);
106
+ });
107
+ });
108
+ (0, vitest_1.describe)('calculateTotalExerciseDuration (Unit Tests)', () => {
109
+ const guardrails = {
110
+ type: 'weight-reps',
111
+ stressRestBonus: 0,
112
+ fatigueMultiplier: 1.0,
113
+ setupTypicalSecs: 5,
114
+ restPeriods: { minimum: 30, typical: 90, maximum: 180, optimalRange: [60, 120] },
115
+ singleRep: { min: 2, max: 3, typical: 2.5 },
116
+ };
117
+ (0, vitest_1.it)('returns 0 for empty array', () => {
118
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)([], guardrails)).toBe(0);
119
+ });
120
+ (0, vitest_1.it)('calculates total correctly excluding the last rest for multiple sets', () => {
121
+ const records = [
122
+ { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '10', restDurationSecs: 60 }, // Work: 30s + Rest: 60s
123
+ { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '10', restDurationSecs: 60 }, // Work: 30s + Rest: 60s
124
+ { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '10', restDurationSecs: 60 }, // Work: 30s + Rest: ignored
125
+ ];
126
+ // Work = 30 + 30 + 30 = 90
127
+ // Rest = 60 + 60 = 120
128
+ // Total = 210
129
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(records, guardrails)).toBe(210);
130
+ });
131
+ (0, vitest_1.it)('calculates just work duration if only a single set is completed', () => {
132
+ const records = [
133
+ { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '10', restDurationSecs: 60 },
134
+ ];
135
+ // Work = 30
136
+ // Rest = 0 (ignored b/c it is the final set)
137
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(records, guardrails)).toBe(30);
138
+ });
139
+ (0, vitest_1.it)('ignores undone sets and handles non-sequential completion correctly', () => {
140
+ const records = [
141
+ { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '10', restDurationSecs: 60 }, // Work: 30, Rest: 60
142
+ { type: 'weight-reps', isDone: false, isStrictMode: false, kg: '100', reps: '10' }, // Ignored completely
143
+ { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '10', restDurationSecs: 60 }, // Work: 30, Rest: Ignored (final done set)
144
+ ];
145
+ // Done records length = 2. Index 0 gets work+rest. Index 1 gets work only.
146
+ // 30 + 60 + 30 = 120
147
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(records, guardrails)).toBe(120);
148
+ });
149
+ });
150
+ (0, vitest_1.describe)('calculateSetVolume', () => {
151
+ (0, vitest_1.it)('calculates weight-reps volume', () => {
152
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '100', reps: '5' };
153
+ (0, vitest_1.expect)((0, workoutMath_1.calculateSetVolume)(record)).toBe(500);
154
+ });
155
+ (0, vitest_1.it)('calculates weight-reps decimal (micro-load) volume safely', () => {
156
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '42.5', reps: '5' };
157
+ (0, vitest_1.expect)((0, workoutMath_1.calculateSetVolume)(record)).toBe(212.5);
158
+ });
159
+ (0, vitest_1.it)('returns 0 for undone records', () => {
160
+ const record = { type: 'weight-reps', isDone: false, isStrictMode: false, kg: '100', reps: '5' };
161
+ (0, vitest_1.expect)((0, workoutMath_1.calculateSetVolume)(record)).toBe(0);
162
+ });
163
+ (0, vitest_1.it)('returns 0 for cardio/duration types by design', () => {
164
+ const record = { type: 'duration', isDone: true, isStrictMode: false, durationMmSs: '15:00', auxWeightKg: '0' };
165
+ (0, vitest_1.expect)((0, workoutMath_1.calculateSetVolume)(record)).toBe(0);
166
+ });
167
+ (0, vitest_1.it)('safely handles Imperial Footgun (treats string verbatim)', () => {
168
+ // If frontend sends LBS but it is expected to be KG, the pure math doesn't crash,
169
+ // it just processes it. Data cleansing for LBS must happen before this or at the UI layer.
170
+ const record = { type: 'weight-reps', isDone: true, isStrictMode: false, kg: '220', reps: '10' };
171
+ (0, vitest_1.expect)((0, workoutMath_1.calculateSetVolume)(record)).toBe(2200);
172
+ });
173
+ (0, vitest_1.it)('calculates reps-only volume with empty auxWeight string (defaults to 1 per rep)', () => {
174
+ const record = { type: 'reps-only', isDone: true, isStrictMode: false, reps: '10', auxWeightKg: '' };
175
+ (0, vitest_1.expect)((0, workoutMath_1.calculateSetVolume)(record)).toBe(10); // 1 * 10
176
+ });
177
+ (0, vitest_1.it)('calculates reps-only volume with explicit zero auxWeight (defaults to 1 per rep)', () => {
178
+ const record = { type: 'reps-only', isDone: true, isStrictMode: false, reps: '10', auxWeightKg: '0' };
179
+ (0, vitest_1.expect)((0, workoutMath_1.calculateSetVolume)(record)).toBe(10); // 1 * 10
180
+ });
181
+ (0, vitest_1.it)('calculates reps-only volume with positive Aux Weight (Dip belt)', () => {
182
+ const record = { type: 'reps-only', isDone: true, isStrictMode: false, reps: '10', auxWeightKg: '20' };
183
+ (0, vitest_1.expect)((0, workoutMath_1.calculateSetVolume)(record)).toBe(210); // (1 + 20) * 10
184
+ });
185
+ });
186
+ (0, vitest_1.describe)('Scenario Integration (End-to-End)', () => {
187
+ const { allUndone: mockTemplateAllUndone, singleSet: mockTemplateSingleSet, nonSequential: mockTemplateNonSequential, lazyLogger: mockTemplateLazyLogger, distractedLogger: mockTemplateDistractedLogger } = templateExercises_mock_1.mockTemplateExercisesDictionary;
188
+ const guardrails = {
189
+ type: 'weight-reps',
190
+ stressRestBonus: 0,
191
+ fatigueMultiplier: 1.0,
192
+ setupTypicalSecs: 5,
193
+ restPeriods: { minimum: 30, typical: 90, maximum: 180, optimalRange: [60, 120] },
194
+ singleRep: { min: 2, max: 3, typical: 2.5 },
195
+ };
196
+ (0, vitest_1.it)('Scenario: "Distracted Logger" -> clamping fires, total is correct', () => {
197
+ // mockTemplateDistractedLogger has:
198
+ // Set 1: 10 reps, 600s rest -> Work: 30s, Rest clamped to 180s
199
+ // Set 2: 10 reps, 90s rest -> Work: 30s, Rest ignored (final set)
200
+ // Total = 30 + 180 + 30 = 240
201
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(mockTemplateDistractedLogger.initialRecords, guardrails)).toBe(240);
202
+ });
203
+ (0, vitest_1.it)('Scenario: "Lazy Logger" -> fallback to typical, total is correct', () => {
204
+ // mockTemplateLazyLogger has 3 sets, all rest durations are undefined.
205
+ // Guardrail typical rest is 90s.
206
+ // Set 1: 30s work + 90s rest
207
+ // Set 2: 30s work + 90s rest
208
+ // Set 3: 30s work + ignored rest
209
+ // Total = 30*3 + 90*2 = 270
210
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(mockTemplateLazyLogger.initialRecords, guardrails)).toBe(270);
211
+ });
212
+ (0, vitest_1.it)('Scenario: Single set -> rest excluded, work only', () => {
213
+ // Work: (5 reps * 2.5s) + 5s = 17.5 -> rounded to 18s
214
+ // Rest: 180s (should be ignored because it's the final set)
215
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(mockTemplateSingleSet.initialRecords, guardrails)).toBe(18);
216
+ });
217
+ (0, vitest_1.it)('Scenario: Non-sequential -> filter ordering, last done set has no rest', () => {
218
+ // mockTemplateNonSequential has:
219
+ // Set 1: Done, 10 reps, 90s rest -> Work: 30s, Rest: 90s
220
+ // Set 2: Undone -> Ignored
221
+ // Set 3: Done, 10 reps, 90s rest -> Work: 30s, Rest: ignored (because it is the last DONE set)
222
+ // Total = 30 + 90 + 30 = 150
223
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(mockTemplateNonSequential.initialRecords, guardrails)).toBe(150);
224
+ });
225
+ (0, vitest_1.it)('Scenario: All undone -> returns 0', () => {
226
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(mockTemplateAllUndone.initialRecords, guardrails)).toBe(0);
227
+ });
228
+ (0, vitest_1.it)('Scenario: No guardrails exercise -> hardcoded fallbacks kick in', () => {
229
+ // Re-use lazy logger (3 sets, no rest tracked).
230
+ // Fallbacks: 2.5s rep, 3s setup, 60s rest.
231
+ // Set 1: 28s work + 60s rest
232
+ // Set 2: 28s work + 60s rest
233
+ // Set 3: 28s work
234
+ // Total = 28*3 + 60*2 = 204
235
+ (0, vitest_1.expect)((0, workoutMath_1.calculateTotalExerciseDuration)(mockTemplateLazyLogger.initialRecords, undefined)).toBe(204);
236
+ });
237
+ });
238
+ });
@@ -7,6 +7,8 @@ export { toError } from "./toError.util";
7
7
  export { generatePlanCode } from "./planCode.util";
8
8
  export { maskEmail, isAnonymousEmail, isEmail } from "./email.utils";
9
9
  export { NOOP } from "./noop.utils";
10
- export { calculateExerciseScoreV2, calculateTotalVolume } from "./scoring";
10
+ export { calculateExerciseScoreV2, calculateTotalVolume } from "./scoringWorkout";
11
11
  export { scaleProPlan, calculateBMI, calculateDayPlanDuration, calculateExerciseDurationSecs } from "./adoptionEngine/scaleProPlan.util";
12
+ export * from "./exerciseRecord/workoutMath";
13
+ export * from "./exerciseRecord/recordValidator";
12
14
  export * from "./metricConversions";
@@ -38,12 +38,14 @@ Object.defineProperty(exports, "isAnonymousEmail", { enumerable: true, get: func
38
38
  Object.defineProperty(exports, "isEmail", { enumerable: true, get: function () { return email_utils_1.isEmail; } });
39
39
  var noop_utils_1 = require("./noop.utils");
40
40
  Object.defineProperty(exports, "NOOP", { enumerable: true, get: function () { return noop_utils_1.NOOP; } });
41
- var scoring_1 = require("./scoring");
42
- Object.defineProperty(exports, "calculateExerciseScoreV2", { enumerable: true, get: function () { return scoring_1.calculateExerciseScoreV2; } });
43
- Object.defineProperty(exports, "calculateTotalVolume", { enumerable: true, get: function () { return scoring_1.calculateTotalVolume; } });
41
+ var scoringWorkout_1 = require("./scoringWorkout");
42
+ Object.defineProperty(exports, "calculateExerciseScoreV2", { enumerable: true, get: function () { return scoringWorkout_1.calculateExerciseScoreV2; } });
43
+ Object.defineProperty(exports, "calculateTotalVolume", { enumerable: true, get: function () { return scoringWorkout_1.calculateTotalVolume; } });
44
44
  var scaleProPlan_util_1 = require("./adoptionEngine/scaleProPlan.util");
45
45
  Object.defineProperty(exports, "scaleProPlan", { enumerable: true, get: function () { return scaleProPlan_util_1.scaleProPlan; } });
46
46
  Object.defineProperty(exports, "calculateBMI", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateBMI; } });
47
47
  Object.defineProperty(exports, "calculateDayPlanDuration", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateDayPlanDuration; } });
48
48
  Object.defineProperty(exports, "calculateExerciseDurationSecs", { enumerable: true, get: function () { return scaleProPlan_util_1.calculateExerciseDurationSecs; } });
49
+ __exportStar(require("./exerciseRecord/workoutMath"), exports);
50
+ __exportStar(require("./exerciseRecord/recordValidator"), exports);
49
51
  __exportStar(require("./metricConversions"), exports);
@@ -0,0 +1,67 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX EXERCISE SCORING SYSTEM — Pillar 1: Calorie Burn
4
+ * ============================================================================
5
+ *
6
+ * Estimates energy expenditure (kcal) for an exercise using the MET system.
7
+ *
8
+ * Formula (per set):
9
+ * calories = effectiveMET × userWeightKg × (activeDurationSecs / 3600)
10
+ *
11
+ * Where effectiveMET is the exercise's baseMET adjusted for:
12
+ * - Effort level (RPE/RIR → scales within metRange)
13
+ * - Weight intensity (for weight-reps: how heavy relative to bodyweight)
14
+ * - Duration category (for duration: short/medium/long multiplier)
15
+ * - Speed (for cardio: interpolated from pace or speed range)
16
+ * - Compound multiplier (multi-joint exercises burn more)
17
+ *
18
+ * After summing all sets:
19
+ * - Rest calories are added (elevated MET during rest periods)
20
+ * - EPOC (afterburn) is added as a percentage of work calories
21
+ *
22
+ * References:
23
+ * - Ainsworth BE et al. "Compendium of Physical Activities" (2011)
24
+ * - Katch, McArdle & Katch, "Exercise Physiology" (8th ed.)
25
+ */
26
+ import type { IParsedSet, IUserContext } from "./types";
27
+ interface IMetabolicData {
28
+ baseMET: number;
29
+ metRange: [number, number];
30
+ compoundMultiplier: number;
31
+ muscleGroupFactor: number;
32
+ intensityScaling: "linear" | "exponential" | "plateau";
33
+ epocFactor: number;
34
+ weightFactors?: {
35
+ lightWeight: number;
36
+ moderateWeight: number;
37
+ heavyWeight: number;
38
+ };
39
+ durationFactors?: {
40
+ shortDuration: number;
41
+ mediumDuration: number;
42
+ longDuration: number;
43
+ };
44
+ paceFactors?: Record<string, number>;
45
+ lightWeight?: number;
46
+ moderateWeight?: number;
47
+ heavyWeight?: number;
48
+ shortDuration?: number;
49
+ mediumDuration?: number;
50
+ longDuration?: number;
51
+ }
52
+ /**
53
+ * Calculate total calorie burn for an exercise.
54
+ *
55
+ * @param sets Parsed & validated sets (from parseRecords)
56
+ * @param metabolicData Exercise's metabolic configuration
57
+ * @param user Validated user context
58
+ * @param difficultyLevel Exercise difficulty (0–4), used for bodyweight estimation
59
+ * @returns Total calories burned (kcal), rounded to 1 decimal
60
+ *
61
+ * @example
62
+ * const calories = calculateCalories(parsedSets, exercise.metabolicData, userCtx, exercise.difficultyLevel);
63
+ * // → 6.4 (for 3 light bench press sets)
64
+ * // → 142.3 (for a 20-min treadmill run)
65
+ */
66
+ export declare function calculateCalories(sets: IParsedSet[], metabolicData: IMetabolicData, user: IUserContext, difficultyLevel: number, scoringSpecialHandling?: "plyometric" | "stretch-mobility" | "continuous-duration" | "loaded-carry"): number;
67
+ export {};