@dgpholdings/greatoak-shared 1.2.54 → 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.
- package/dist/__mocks__/exercises.mock.d.ts +35 -0
- package/dist/__mocks__/exercises.mock.js +144 -0
- package/dist/__mocks__/templateExercises.mock.d.ts +90 -0
- package/dist/__mocks__/templateExercises.mock.js +258 -0
- package/dist/__mocks__/user.mock.d.ts +2 -0
- package/dist/__mocks__/user.mock.js +36 -0
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +30 -0
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +138 -0
- package/dist/utils/exerciseRecord/recordValidator.d.ts +12 -0
- package/dist/utils/exerciseRecord/recordValidator.integration.test.d.ts +1 -0
- package/dist/utils/exerciseRecord/recordValidator.integration.test.js +51 -0
- package/dist/utils/exerciseRecord/recordValidator.js +85 -0
- package/dist/utils/exerciseRecord/recordValidator.test.d.ts +1 -0
- package/dist/utils/exerciseRecord/recordValidator.test.js +165 -0
- package/dist/utils/exerciseRecord/workoutMath.d.ts +28 -0
- package/dist/utils/exerciseRecord/workoutMath.js +116 -0
- package/dist/utils/exerciseRecord/workoutMath.test.d.ts +1 -0
- package/dist/utils/exerciseRecord/workoutMath.test.js +238 -0
- package/dist/utils/index.d.ts +3 -1
- package/dist/utils/index.js +5 -3
- package/dist/utils/scoringWorkout/calculateCalories.d.ts +67 -0
- package/dist/utils/scoringWorkout/calculateCalories.js +351 -0
- package/dist/utils/scoringWorkout/calculateMuscleFatiue.d.ts +67 -0
- package/dist/utils/scoringWorkout/calculateMuscleFatiue.js +330 -0
- package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +73 -0
- package/dist/utils/scoringWorkout/calculateQualityScore.js +357 -0
- package/dist/utils/scoringWorkout/calculateTotalVolume.d.ts +15 -0
- package/dist/utils/scoringWorkout/calculateTotalVolume.js +73 -0
- package/dist/utils/scoringWorkout/constants.d.ts +211 -0
- package/dist/utils/scoringWorkout/constants.js +247 -0
- package/dist/utils/scoringWorkout/helpers.d.ts +127 -0
- package/dist/utils/scoringWorkout/helpers.js +245 -0
- package/dist/utils/scoringWorkout/index.d.ts +27 -0
- package/dist/utils/scoringWorkout/index.js +56 -0
- package/dist/utils/scoringWorkout/parseRecords.d.ts +68 -0
- package/dist/utils/scoringWorkout/parseRecords.js +281 -0
- package/dist/utils/scoringWorkout/scoringWorkout.integration.test.d.ts +1 -0
- package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +439 -0
- package/dist/utils/scoringWorkout/types.d.ts +104 -0
- package/dist/utils/scoringWorkout/types.js +11 -0
- 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
|
+
});
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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 "./
|
|
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";
|
package/dist/utils/index.js
CHANGED
|
@@ -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
|
|
42
|
-
Object.defineProperty(exports, "calculateExerciseScoreV2", { enumerable: true, get: function () { return
|
|
43
|
-
Object.defineProperty(exports, "calculateTotalVolume", { enumerable: true, get: function () { return
|
|
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 {};
|