@dgpholdings/greatoak-shared 1.2.16 → 1.2.17
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/utils/index.d.ts +1 -0
- package/dist/utils/index.js +3 -1
- package/dist/utils/scoring/calculateCalories.d.ts +67 -0
- package/dist/utils/scoring/calculateCalories.js +329 -0
- package/dist/utils/scoring/calculateMuscleFatiue.d.ts +66 -0
- package/dist/utils/scoring/calculateMuscleFatiue.js +290 -0
- package/dist/utils/scoring/calculateQualityScore.d.ts +69 -0
- package/dist/utils/scoring/calculateQualityScore.js +333 -0
- package/dist/utils/scoring/constants.d.ts +191 -0
- package/dist/utils/scoring/constants.js +227 -0
- package/dist/utils/scoring/helpers.d.ts +119 -0
- package/dist/utils/scoring/helpers.js +229 -0
- package/dist/utils/scoring/index.d.ts +28 -0
- package/dist/utils/scoring/index.js +44 -0
- package/dist/utils/scoring/parseRecords.d.ts +97 -0
- package/dist/utils/scoring/parseRecords.js +280 -0
- package/dist/utils/scoring/types.d.ts +84 -0
- package/dist/utils/scoring/types.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Record Parser
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Transforms raw TRecord[] into clean IParsedSet[].
|
|
7
|
+
*
|
|
8
|
+
* This is the SINGLE POINT of data cleaning. After this step, all downstream
|
|
9
|
+
* code (calories, fatigue, quality) works with guaranteed-valid numbers.
|
|
10
|
+
*
|
|
11
|
+
* VALIDATION STRATEGY:
|
|
12
|
+
* The exercise's `timingGuardrails` is the PRIMARY source of truth for
|
|
13
|
+
* what constitutes valid timing data. Each exercise defines its own:
|
|
14
|
+
* - restPeriods.minimum / maximum → acceptable rest range
|
|
15
|
+
* - setDuration.min / max → acceptable work duration (duration type)
|
|
16
|
+
* - singleRep.min / max → acceptable rep timing (rep-based types)
|
|
17
|
+
*
|
|
18
|
+
* Global constants (ABSOLUTE_* and FALLBACK_*) are ONLY used when
|
|
19
|
+
* timingGuardrails is completely missing from the exercise.
|
|
20
|
+
*
|
|
21
|
+
* Responsibilities:
|
|
22
|
+
* 1. Filter out incomplete sets (isDone === false)
|
|
23
|
+
* 2. Parse all string fields to numbers
|
|
24
|
+
* 3. Estimate active work duration when not measured
|
|
25
|
+
* 4. Validate timing against exercise-specific guardrails
|
|
26
|
+
* 5. Compute effort fraction per set
|
|
27
|
+
*/
|
|
28
|
+
import type { IParsedSet } from "./types";
|
|
29
|
+
/**
|
|
30
|
+
* Minimal shape of TRecord that we need. Avoids importing the full
|
|
31
|
+
* app type, making the scoring module independently testable.
|
|
32
|
+
*/
|
|
33
|
+
interface IRawRecord {
|
|
34
|
+
type: string;
|
|
35
|
+
isDone: boolean;
|
|
36
|
+
isStrictMode: boolean;
|
|
37
|
+
rpe?: string;
|
|
38
|
+
rir?: string;
|
|
39
|
+
workDurationSecs?: number;
|
|
40
|
+
restDurationSecs?: number;
|
|
41
|
+
kg?: string;
|
|
42
|
+
reps?: string;
|
|
43
|
+
durationMmSs?: string;
|
|
44
|
+
auxWeightKg?: string;
|
|
45
|
+
speedMin?: string;
|
|
46
|
+
speedMax?: string;
|
|
47
|
+
distance?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Full timing guardrails shape from TExercise.
|
|
51
|
+
*
|
|
52
|
+
* Every field is optional because:
|
|
53
|
+
* - Different exercise types have different sub-shapes (singleRep vs setDuration)
|
|
54
|
+
* - Some exercises might have incomplete guardrails
|
|
55
|
+
* - We need graceful fallback for each missing piece
|
|
56
|
+
*/
|
|
57
|
+
export interface ITimingGuardrails {
|
|
58
|
+
type?: string;
|
|
59
|
+
stressRestBonus?: number;
|
|
60
|
+
fatigueMultiplier?: number;
|
|
61
|
+
setupTypicalSecs?: number;
|
|
62
|
+
restPeriods?: {
|
|
63
|
+
minimum?: number;
|
|
64
|
+
typical?: number;
|
|
65
|
+
maximum?: number;
|
|
66
|
+
optimalRange?: [number, number];
|
|
67
|
+
};
|
|
68
|
+
singleRep?: {
|
|
69
|
+
min?: number;
|
|
70
|
+
max?: number;
|
|
71
|
+
typical?: number;
|
|
72
|
+
};
|
|
73
|
+
setDuration?: {
|
|
74
|
+
min?: number;
|
|
75
|
+
max?: number;
|
|
76
|
+
typical?: number;
|
|
77
|
+
};
|
|
78
|
+
pacing?: {
|
|
79
|
+
minPacePerUnit?: number;
|
|
80
|
+
maxPacePerUnit?: number;
|
|
81
|
+
typicalPacePerUnit?: number;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Parse and clean raw records into a validated set array.
|
|
86
|
+
*
|
|
87
|
+
* @param records Raw TRecord[] from the workout
|
|
88
|
+
* @param timingGuardrails Exercise's timing guardrails (for validation & fallbacks)
|
|
89
|
+
* @returns Cleaned IParsedSet[] with only completed, valid sets
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const parsed = parseRecords(workout.records, exercise.timingGuardrails);
|
|
93
|
+
* // parsed[0].activeDurationSecs → guaranteed valid number
|
|
94
|
+
* // parsed[0].effortFraction → guaranteed 0.5–1.3
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseRecords(records: IRawRecord[], timingGuardrails?: ITimingGuardrails): IParsedSet[];
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// parseRecords.ts
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Record Parser
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* Transforms raw TRecord[] into clean IParsedSet[].
|
|
9
|
+
*
|
|
10
|
+
* This is the SINGLE POINT of data cleaning. After this step, all downstream
|
|
11
|
+
* code (calories, fatigue, quality) works with guaranteed-valid numbers.
|
|
12
|
+
*
|
|
13
|
+
* VALIDATION STRATEGY:
|
|
14
|
+
* The exercise's `timingGuardrails` is the PRIMARY source of truth for
|
|
15
|
+
* what constitutes valid timing data. Each exercise defines its own:
|
|
16
|
+
* - restPeriods.minimum / maximum → acceptable rest range
|
|
17
|
+
* - setDuration.min / max → acceptable work duration (duration type)
|
|
18
|
+
* - singleRep.min / max → acceptable rep timing (rep-based types)
|
|
19
|
+
*
|
|
20
|
+
* Global constants (ABSOLUTE_* and FALLBACK_*) are ONLY used when
|
|
21
|
+
* timingGuardrails is completely missing from the exercise.
|
|
22
|
+
*
|
|
23
|
+
* Responsibilities:
|
|
24
|
+
* 1. Filter out incomplete sets (isDone === false)
|
|
25
|
+
* 2. Parse all string fields to numbers
|
|
26
|
+
* 3. Estimate active work duration when not measured
|
|
27
|
+
* 4. Validate timing against exercise-specific guardrails
|
|
28
|
+
* 5. Compute effort fraction per set
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.parseRecords = parseRecords;
|
|
32
|
+
const constants_1 = require("./constants");
|
|
33
|
+
const helpers_1 = require("./helpers");
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Main Parser
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/**
|
|
38
|
+
* Parse and clean raw records into a validated set array.
|
|
39
|
+
*
|
|
40
|
+
* @param records Raw TRecord[] from the workout
|
|
41
|
+
* @param timingGuardrails Exercise's timing guardrails (for validation & fallbacks)
|
|
42
|
+
* @returns Cleaned IParsedSet[] with only completed, valid sets
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const parsed = parseRecords(workout.records, exercise.timingGuardrails);
|
|
46
|
+
* // parsed[0].activeDurationSecs → guaranteed valid number
|
|
47
|
+
* // parsed[0].effortFraction → guaranteed 0.5–1.3
|
|
48
|
+
*/
|
|
49
|
+
function parseRecords(records, timingGuardrails) {
|
|
50
|
+
return records
|
|
51
|
+
.filter((r) => r.isDone) // Only completed sets count
|
|
52
|
+
.map((record, index) => parseOneRecord(record, index, records.length, timingGuardrails))
|
|
53
|
+
.filter((set) => set !== null);
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Single Record Parser
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
function parseOneRecord(record, index, totalSets, guardrails) {
|
|
59
|
+
const type = record.type;
|
|
60
|
+
const effortFraction = (0, helpers_1.getEffortFraction)(record);
|
|
61
|
+
// Parse rest duration using exercise-specific guardrails for validation
|
|
62
|
+
const restDurationSecs = resolveRestDuration(record.restDurationSecs, index, totalSets, guardrails);
|
|
63
|
+
switch (type) {
|
|
64
|
+
case "weight-reps":
|
|
65
|
+
return parseWeightReps(record, effortFraction, restDurationSecs, guardrails);
|
|
66
|
+
case "reps-only":
|
|
67
|
+
return parseRepsOnly(record, effortFraction, restDurationSecs, guardrails);
|
|
68
|
+
case "duration":
|
|
69
|
+
return parseDuration(record, effortFraction, restDurationSecs, guardrails);
|
|
70
|
+
case "cardio-machine":
|
|
71
|
+
return parseCardioMachine(record, effortFraction, restDurationSecs);
|
|
72
|
+
case "cardio-free":
|
|
73
|
+
return parseCardioFree(record, effortFraction, restDurationSecs);
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Type-Specific Parsers
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
function parseWeightReps(record, effortFraction, restDurationSecs, guardrails) {
|
|
82
|
+
var _a, _b;
|
|
83
|
+
const kg = (0, helpers_1.safeParseFloat)(record.kg);
|
|
84
|
+
const reps = (0, helpers_1.safeParseFloat)(record.reps);
|
|
85
|
+
// Skip sets with zero reps (likely user error / empty row)
|
|
86
|
+
if (reps === 0)
|
|
87
|
+
return null;
|
|
88
|
+
// Estimate active duration: reps × seconds-per-rep
|
|
89
|
+
// PRIMARY: exercise's own singleRep.typical
|
|
90
|
+
// FALLBACK: global default
|
|
91
|
+
const secsPerRep = (_b = (_a = guardrails === null || guardrails === void 0 ? void 0 : guardrails.singleRep) === null || _a === void 0 ? void 0 : _a.typical) !== null && _b !== void 0 ? _b : constants_1.FALLBACK_SECS_PER_REP;
|
|
92
|
+
const estimatedDuration = reps * secsPerRep;
|
|
93
|
+
const activeDurationSecs = resolveWorkDuration(record.workDurationSecs, estimatedDuration, guardrails);
|
|
94
|
+
return {
|
|
95
|
+
type: "weight-reps",
|
|
96
|
+
activeDurationSecs,
|
|
97
|
+
restDurationSecs,
|
|
98
|
+
effortFraction,
|
|
99
|
+
kg,
|
|
100
|
+
reps,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function parseRepsOnly(record, effortFraction, restDurationSecs, guardrails) {
|
|
104
|
+
var _a, _b;
|
|
105
|
+
const reps = (0, helpers_1.safeParseFloat)(record.reps);
|
|
106
|
+
const auxWeightKg = (0, helpers_1.safeParseFloat)(record.auxWeightKg);
|
|
107
|
+
if (reps === 0)
|
|
108
|
+
return null;
|
|
109
|
+
const secsPerRep = (_b = (_a = guardrails === null || guardrails === void 0 ? void 0 : guardrails.singleRep) === null || _a === void 0 ? void 0 : _a.typical) !== null && _b !== void 0 ? _b : constants_1.FALLBACK_SECS_PER_REP;
|
|
110
|
+
const estimatedDuration = reps * secsPerRep;
|
|
111
|
+
const activeDurationSecs = resolveWorkDuration(record.workDurationSecs, estimatedDuration, guardrails);
|
|
112
|
+
return {
|
|
113
|
+
type: "reps-only",
|
|
114
|
+
activeDurationSecs,
|
|
115
|
+
restDurationSecs,
|
|
116
|
+
effortFraction,
|
|
117
|
+
reps,
|
|
118
|
+
auxWeightKg,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function parseDuration(record, effortFraction, restDurationSecs, guardrails) {
|
|
122
|
+
var _a, _b;
|
|
123
|
+
// Primary: parse the user's recorded duration
|
|
124
|
+
// Fallback: exercise's typical set duration → global fallback
|
|
125
|
+
const fallbackDuration = (_b = (_a = guardrails === null || guardrails === void 0 ? void 0 : guardrails.setDuration) === null || _a === void 0 ? void 0 : _a.typical) !== null && _b !== void 0 ? _b : constants_1.FALLBACK_SET_DURATION_SECS;
|
|
126
|
+
const durationSecs = (0, helpers_1.parseDurationMmSs)(record.durationMmSs, fallbackDuration);
|
|
127
|
+
const auxWeightKg = (0, helpers_1.safeParseFloat)(record.auxWeightKg);
|
|
128
|
+
if (durationSecs === 0)
|
|
129
|
+
return null;
|
|
130
|
+
// For duration exercises, the parsed durationMmSs IS the active duration
|
|
131
|
+
// We validate it against guardrails to get the final value
|
|
132
|
+
const activeDurationSecs = resolveWorkDuration(durationSecs, fallbackDuration, guardrails);
|
|
133
|
+
return {
|
|
134
|
+
type: "duration",
|
|
135
|
+
activeDurationSecs,
|
|
136
|
+
restDurationSecs,
|
|
137
|
+
effortFraction,
|
|
138
|
+
// Use the SAME validated value for both — activeDurationSecs is used
|
|
139
|
+
// for calorie time calculation, durationSecs is used for the
|
|
140
|
+
// short/medium/long multiplier classification. They must agree.
|
|
141
|
+
durationSecs: activeDurationSecs,
|
|
142
|
+
auxWeightKg,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function parseCardioMachine(record, effortFraction, restDurationSecs) {
|
|
146
|
+
const speedMin = (0, helpers_1.safeParseFloat)(record.speedMin);
|
|
147
|
+
const speedMax = (0, helpers_1.safeParseFloat)(record.speedMax);
|
|
148
|
+
const distance = (0, helpers_1.safeParseFloat)(record.distance);
|
|
149
|
+
const cardioDurationSecs = (0, helpers_1.parseDurationMmSs)(record.durationMmSs);
|
|
150
|
+
// Need at least duration or distance to be meaningful
|
|
151
|
+
if (cardioDurationSecs === 0 && distance === 0)
|
|
152
|
+
return null;
|
|
153
|
+
const activeDurationSecs = cardioDurationSecs > 0 ? cardioDurationSecs : 0;
|
|
154
|
+
return {
|
|
155
|
+
type: "cardio-machine",
|
|
156
|
+
activeDurationSecs,
|
|
157
|
+
restDurationSecs,
|
|
158
|
+
effortFraction,
|
|
159
|
+
speedMin,
|
|
160
|
+
speedMax,
|
|
161
|
+
distance,
|
|
162
|
+
cardioDurationSecs,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function parseCardioFree(record, effortFraction, restDurationSecs) {
|
|
166
|
+
const distance = (0, helpers_1.safeParseFloat)(record.distance);
|
|
167
|
+
const cardioDurationSecs = (0, helpers_1.parseDurationMmSs)(record.durationMmSs);
|
|
168
|
+
if (cardioDurationSecs === 0 && distance === 0)
|
|
169
|
+
return null;
|
|
170
|
+
const activeDurationSecs = cardioDurationSecs > 0 ? cardioDurationSecs : 0;
|
|
171
|
+
return {
|
|
172
|
+
type: "cardio-free",
|
|
173
|
+
activeDurationSecs,
|
|
174
|
+
restDurationSecs,
|
|
175
|
+
effortFraction,
|
|
176
|
+
distance,
|
|
177
|
+
cardioDurationSecs,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Duration Resolution Helpers
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
/**
|
|
184
|
+
* Resolve and validate work duration for a set.
|
|
185
|
+
*
|
|
186
|
+
* Validation cascade:
|
|
187
|
+
* ┌──────────────────────────────────────────────────────────────┐
|
|
188
|
+
* │ 1. Absolute sanity check (catches truly broken data) │
|
|
189
|
+
* │ measured < 1s or > 14400s → reject │
|
|
190
|
+
* │ │
|
|
191
|
+
* │ 2. Exercise-specific validation (PRIMARY) │
|
|
192
|
+
* │ duration type: check against setDuration.min / max │
|
|
193
|
+
* │ rep type: check against estimate ± tolerance │
|
|
194
|
+
* │ Grace factor: 2× on max, 0.5× on min │
|
|
195
|
+
* │ │
|
|
196
|
+
* │ 3. Passed all checks → use measured value │
|
|
197
|
+
* │ │
|
|
198
|
+
* │ 4. No measured value → use estimated │
|
|
199
|
+
* └──────────────────────────────────────────────────────────────┘
|
|
200
|
+
*/
|
|
201
|
+
function resolveWorkDuration(measured, estimated, guardrails) {
|
|
202
|
+
var _a, _b;
|
|
203
|
+
if (measured !== undefined && measured > 0) {
|
|
204
|
+
// 1. Absolute sanity check
|
|
205
|
+
if (measured < constants_1.ABSOLUTE_WORK_MIN || measured > constants_1.ABSOLUTE_WORK_MAX) {
|
|
206
|
+
return Math.max(estimated, constants_1.ABSOLUTE_WORK_MIN);
|
|
207
|
+
}
|
|
208
|
+
// 2. Exercise-specific validation for duration-type exercises
|
|
209
|
+
if (guardrails === null || guardrails === void 0 ? void 0 : guardrails.setDuration) {
|
|
210
|
+
const minReasonable = ((_a = guardrails.setDuration.min) !== null && _a !== void 0 ? _a : 1) * 0.5;
|
|
211
|
+
const maxReasonable = ((_b = guardrails.setDuration.max) !== null && _b !== void 0 ? _b : constants_1.ABSOLUTE_WORK_MAX) * 2;
|
|
212
|
+
if (measured < minReasonable || measured > maxReasonable) {
|
|
213
|
+
return estimated; // Measured seems wrong for this exercise
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// 2b. Exercise-specific validation for rep-based exercises
|
|
217
|
+
if ((guardrails === null || guardrails === void 0 ? void 0 : guardrails.singleRep) && estimated > 0) {
|
|
218
|
+
// Measured should be within 3× of our estimate
|
|
219
|
+
// (generous because user might pause mid-set)
|
|
220
|
+
if (measured > estimated * 3 || measured < estimated * 0.2) {
|
|
221
|
+
return estimated;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// 3. Passed validation
|
|
225
|
+
return measured;
|
|
226
|
+
}
|
|
227
|
+
// 4. No measured value
|
|
228
|
+
return Math.max(estimated, constants_1.ABSOLUTE_WORK_MIN);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Resolve and validate rest duration for a set.
|
|
232
|
+
*
|
|
233
|
+
* Validation cascade:
|
|
234
|
+
* ┌──────────────────────────────────────────────────────────────┐
|
|
235
|
+
* │ 1. Last set with no data → null (no rest after final set) │
|
|
236
|
+
* │ │
|
|
237
|
+
* │ 2. Absolute sanity check (< 0 or > 3600s) │
|
|
238
|
+
* │ → use exercise typical rest │
|
|
239
|
+
* │ │
|
|
240
|
+
* │ 3. Exercise-specific validation (PRIMARY) │
|
|
241
|
+
* │ Check against restPeriods.minimum / maximum │
|
|
242
|
+
* │ Grace factor: 50% on each side │
|
|
243
|
+
* │ Outside → use exercise typical rest │
|
|
244
|
+
* │ │
|
|
245
|
+
* │ 4. Passed checks → use measured value │
|
|
246
|
+
* │ │
|
|
247
|
+
* │ 5. No measured data → exercise typical → global fallback │
|
|
248
|
+
* └──────────────────────────────────────────────────────────────┘
|
|
249
|
+
*/
|
|
250
|
+
function resolveRestDuration(measured, setIndex, totalSets, guardrails) {
|
|
251
|
+
var _a, _b, _c;
|
|
252
|
+
// 1. Last set with no rest data → null
|
|
253
|
+
if (setIndex === totalSets - 1 &&
|
|
254
|
+
(measured === undefined || measured === 0)) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
// Extract exercise-specific rest config
|
|
258
|
+
const exerciseRestMin = (_a = guardrails === null || guardrails === void 0 ? void 0 : guardrails.restPeriods) === null || _a === void 0 ? void 0 : _a.minimum;
|
|
259
|
+
const exerciseRestMax = (_b = guardrails === null || guardrails === void 0 ? void 0 : guardrails.restPeriods) === null || _b === void 0 ? void 0 : _b.maximum;
|
|
260
|
+
const exerciseRestTypical = (_c = guardrails === null || guardrails === void 0 ? void 0 : guardrails.restPeriods) === null || _c === void 0 ? void 0 : _c.typical;
|
|
261
|
+
if (measured !== undefined && measured > 0) {
|
|
262
|
+
// 2. Absolute sanity check
|
|
263
|
+
if (measured < constants_1.ABSOLUTE_REST_MIN || measured > constants_1.ABSOLUTE_REST_MAX) {
|
|
264
|
+
return exerciseRestTypical !== null && exerciseRestTypical !== void 0 ? exerciseRestTypical : constants_1.FALLBACK_REST_SECS;
|
|
265
|
+
}
|
|
266
|
+
// 3. Exercise-specific validation
|
|
267
|
+
if (exerciseRestMin !== undefined && exerciseRestMax !== undefined) {
|
|
268
|
+
const graceFactor = 0.5;
|
|
269
|
+
const lowerBound = exerciseRestMin * (1 - graceFactor);
|
|
270
|
+
const upperBound = exerciseRestMax * (1 + graceFactor);
|
|
271
|
+
if (measured < lowerBound || measured > upperBound) {
|
|
272
|
+
return exerciseRestTypical !== null && exerciseRestTypical !== void 0 ? exerciseRestTypical : constants_1.FALLBACK_REST_SECS;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// 4. Passed validation
|
|
276
|
+
return measured;
|
|
277
|
+
}
|
|
278
|
+
// 5. No measured data → exercise typical → global fallback
|
|
279
|
+
return exerciseRestTypical !== null && exerciseRestTypical !== void 0 ? exerciseRestTypical : constants_1.FALLBACK_REST_SECS;
|
|
280
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Types & Interfaces
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Central type definitions for the scoring algorithm.
|
|
7
|
+
* This file defines the output shape and internal types used across all three
|
|
8
|
+
* scoring pillars (Calories, Muscle Fatigue, Quality).
|
|
9
|
+
*/
|
|
10
|
+
import { TGender, TRecord } from "../../types";
|
|
11
|
+
/**
|
|
12
|
+
* Quality score breakdown — lets the UI show users WHY they got their score.
|
|
13
|
+
* Each sub-score is 0–100.
|
|
14
|
+
*/
|
|
15
|
+
export interface IQualityBreakdown {
|
|
16
|
+
/** Did the user complete all sets? */
|
|
17
|
+
completion: number;
|
|
18
|
+
/** Were sets consistent in output (or intentionally progressive)? */
|
|
19
|
+
consistency: number;
|
|
20
|
+
/** Was effort in the productive RPE/RIR zone? */
|
|
21
|
+
effortAdequacy: number;
|
|
22
|
+
/** Were rest periods within the exercise's optimal range? */
|
|
23
|
+
restDiscipline: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The final result returned by calculateExerciseScore.
|
|
27
|
+
*
|
|
28
|
+
* - score: 0–100 overall quality of execution
|
|
29
|
+
* - muscleScores: per-muscle fatigue map (keyed by EBodyParts key, value 0–100)
|
|
30
|
+
* - calorieBurn: estimated kilocalories burned (gross, including EPOC)
|
|
31
|
+
* - qualityBreakdown: transparent sub-scores for the UI
|
|
32
|
+
*/
|
|
33
|
+
export interface IScoreResult {
|
|
34
|
+
score: number;
|
|
35
|
+
muscleScores: Record<string, number>;
|
|
36
|
+
calorieBurn: number;
|
|
37
|
+
qualityBreakdown: IQualityBreakdown;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A cleaned, parsed version of a single TRecord.
|
|
41
|
+
* All string fields are parsed to numbers, unreliable timings are replaced
|
|
42
|
+
* with fallbacks, and skipped sets are filtered out before this stage.
|
|
43
|
+
*
|
|
44
|
+
* This is the ONLY representation the scoring pillars work with — they never
|
|
45
|
+
* touch raw TRecord directly.
|
|
46
|
+
*/
|
|
47
|
+
export interface IParsedSet {
|
|
48
|
+
type: TRecord["type"];
|
|
49
|
+
/** Active work duration for this set in seconds (estimated or measured) */
|
|
50
|
+
activeDurationSecs: number;
|
|
51
|
+
/** Rest duration after this set in seconds (validated or fallback) */
|
|
52
|
+
restDurationSecs: number | null;
|
|
53
|
+
/**
|
|
54
|
+
* Effort fraction: 0.0 (no effort) to 1.0 (max effort).
|
|
55
|
+
* Derived from RPE, RIR, or fallback (0.6).
|
|
56
|
+
*/
|
|
57
|
+
effortFraction: number;
|
|
58
|
+
/** weight-reps: weight in kg */
|
|
59
|
+
kg?: number;
|
|
60
|
+
/** weight-reps / reps-only: rep count */
|
|
61
|
+
reps?: number;
|
|
62
|
+
/** reps-only / duration: auxiliary weight in kg */
|
|
63
|
+
auxWeightKg?: number;
|
|
64
|
+
/** duration: hold time in seconds */
|
|
65
|
+
durationSecs?: number;
|
|
66
|
+
/** cardio-machine: min speed */
|
|
67
|
+
speedMin?: number;
|
|
68
|
+
/** cardio-machine: max speed */
|
|
69
|
+
speedMax?: number;
|
|
70
|
+
/** cardio-machine / cardio-free: distance (km) */
|
|
71
|
+
distance?: number;
|
|
72
|
+
/** cardio-free / cardio-machine: session duration in seconds */
|
|
73
|
+
cardioDurationSecs?: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validated user context with guaranteed fallbacks.
|
|
77
|
+
* No optional fields — everything has a sensible default.
|
|
78
|
+
*/
|
|
79
|
+
export interface IUserContext {
|
|
80
|
+
weightKg: number;
|
|
81
|
+
heightCm: number;
|
|
82
|
+
gender: TGender;
|
|
83
|
+
age: number;
|
|
84
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ============================================================================
|
|
4
|
+
* FITFRIX EXERCISE SCORING SYSTEM — Types & Interfaces
|
|
5
|
+
* ============================================================================
|
|
6
|
+
*
|
|
7
|
+
* Central type definitions for the scoring algorithm.
|
|
8
|
+
* This file defines the output shape and internal types used across all three
|
|
9
|
+
* scoring pillars (Calories, Muscle Fatigue, Quality).
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|