@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,56 @@
1
+ "use strict";
2
+ /**
3
+ * ============================================================================
4
+ * FITFRIX EXERCISE SCORING SYSTEM
5
+ * ============================================================================
6
+ *
7
+ * calculateExerciseScoreV2({ exercise, record, user, historicalContext? }) => IScoreResult
8
+ *
9
+ * IScoreResult = {
10
+ * score: number, // 0–100 quality of execution
11
+ * muscleScores: Record<string, number>, // 0–100 per muscle fatigue
12
+ * calorieBurn: number, // kcal (placeholder: 0 until Phase 3)
13
+ * qualityBreakdown: IQualityBreakdown // completion/consistency/effort/rest sub-scores
14
+ * }
15
+ *
16
+ * Internally computes muscle fatigue (Pillar 2) and quality score (Pillar 3).
17
+ * Calorie burn (Pillar 1) is implemented in calculateCalories.ts but not yet
18
+ * wired into the save flow — scheduled for Phase 3.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.calculateExerciseScoreV2 = exports.calculateTotalVolume = void 0;
22
+ const calculateMuscleFatiue_1 = require("./calculateMuscleFatiue");
23
+ const calculateQualityScore_1 = require("./calculateQualityScore");
24
+ const helpers_1 = require("./helpers");
25
+ const parseRecords_1 = require("./parseRecords");
26
+ var calculateTotalVolume_1 = require("./calculateTotalVolume");
27
+ Object.defineProperty(exports, "calculateTotalVolume", { enumerable: true, get: function () { return calculateTotalVolume_1.calculateTotalVolume; } });
28
+ // ---------------------------------------------------------------------------
29
+ // Main Function — existing signature
30
+ // ---------------------------------------------------------------------------
31
+ const calculateExerciseScoreV2 = (param) => {
32
+ const { exercise, record, user, historicalContext } = param;
33
+ const userContext = (0, helpers_1.extractUserContext)(user);
34
+ const parsedSets = (0, parseRecords_1.parseRecords)(record, exercise.timingGuardrails, historicalContext);
35
+ // Pillar 2: Muscle Fatigue → muscleScores
36
+ const muscleScores = (0, calculateMuscleFatiue_1.calculateMuscleFatigue)(parsedSets, {
37
+ primaryMuscles: exercise.primaryMuscles,
38
+ secondaryMuscles: exercise.secondaryMuscles,
39
+ difficultyLevel: exercise.difficultyLevel,
40
+ metabolicData: {
41
+ compoundMultiplier: exercise.metabolicData.compoundMultiplier,
42
+ muscleGroupFactor: exercise.metabolicData.muscleGroupFactor,
43
+ },
44
+ scoringSpecialHandling: exercise.scoringSpecialHandling,
45
+ }, userContext, exercise.timingGuardrails, historicalContext);
46
+ // Pillar 3: Quality Score → score
47
+ const isStrictTimingModeScoring = record.some((r) => r.isStrictMode);
48
+ const { score, breakdown } = (0, calculateQualityScore_1.calculateQualityScore)(parsedSets, record, exercise.timingGuardrails, isStrictTimingModeScoring, userContext, historicalContext);
49
+ return {
50
+ score,
51
+ muscleScores,
52
+ calorieBurn: 0, // Placeholder until Pillar 1 is fully wired in Phase 3
53
+ qualityBreakdown: breakdown,
54
+ };
55
+ };
56
+ exports.calculateExerciseScoreV2 = calculateExerciseScoreV2;
@@ -0,0 +1,68 @@
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 { TRecord } from "../../types";
29
+ import type { IParsedSet, IHistoricalContext } from "./types";
30
+ /**
31
+ * Full timing guardrails shape from TExercise.
32
+ */
33
+ export interface ITimingGuardrails {
34
+ type?: string;
35
+ stressRestBonus?: number;
36
+ fatigueMultiplier?: number;
37
+ setupTypicalSecs?: number;
38
+ restPeriods?: {
39
+ minimum?: number;
40
+ typical?: number;
41
+ maximum?: number;
42
+ optimalRange?: [number, number];
43
+ };
44
+ singleRep?: {
45
+ min?: number;
46
+ max?: number;
47
+ typical?: number;
48
+ };
49
+ setDuration?: {
50
+ min?: number;
51
+ max?: number;
52
+ typical?: number;
53
+ };
54
+ pacing?: {
55
+ minPacePerUnit?: number;
56
+ maxPacePerUnit?: number;
57
+ typicalPacePerUnit?: number;
58
+ };
59
+ }
60
+ /**
61
+ * Parse and clean raw records into a validated set array.
62
+ *
63
+ * @param records Raw TRecord[] from the workout
64
+ * @param timingGuardrails Exercise's timing guardrails (for validation & fallbacks)
65
+ * @param historicalContext Optional historical data (used for RIR attenuation)
66
+ * @returns Cleaned IParsedSet[] with only completed, valid sets
67
+ */
68
+ export declare function parseRecords(records: TRecord[], timingGuardrails?: ITimingGuardrails, historicalContext?: IHistoricalContext): IParsedSet[];
@@ -0,0 +1,281 @@
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
+ * @param historicalContext Optional historical data (used for RIR attenuation)
43
+ * @returns Cleaned IParsedSet[] with only completed, valid sets
44
+ */
45
+ function parseRecords(records, timingGuardrails, historicalContext) {
46
+ const doneRecords = records.filter((r) => r.isDone);
47
+ return doneRecords
48
+ .map((record, index) => parseOneRecord(record, index, doneRecords.length, timingGuardrails, historicalContext))
49
+ .filter((set) => set !== null);
50
+ }
51
+ // ---------------------------------------------------------------------------
52
+ // Single Record Parser
53
+ // ---------------------------------------------------------------------------
54
+ function parseOneRecord(record, index, totalSets, guardrails, historicalContext) {
55
+ const type = record.type;
56
+ let effortFraction = (0, helpers_1.getEffortFraction)(record);
57
+ // P2-10: RIR/RPE attenuation for beginners
58
+ if ((historicalContext === null || historicalContext === void 0 ? void 0 : historicalContext.trainingAgeBracket) === "beginner") {
59
+ effortFraction =
60
+ constants_1.DEFAULT_EFFORT_FRACTION +
61
+ (effortFraction - constants_1.DEFAULT_EFFORT_FRACTION) * 0.5;
62
+ }
63
+ // Parse rest duration using exercise-specific guardrails for validation
64
+ const restDurationSecs = resolveRestDuration(record.restDurationSecs, index, totalSets, guardrails);
65
+ switch (type) {
66
+ case "weight-reps":
67
+ return parseWeightReps(record, effortFraction, restDurationSecs, guardrails);
68
+ case "reps-only":
69
+ return parseRepsOnly(record, effortFraction, restDurationSecs, guardrails);
70
+ case "duration":
71
+ return parseDuration(record, effortFraction, restDurationSecs, guardrails);
72
+ case "cardio-machine":
73
+ return parseCardioMachine(record, effortFraction, restDurationSecs);
74
+ case "cardio-free":
75
+ return parseCardioFree(record, effortFraction, restDurationSecs);
76
+ default:
77
+ return null;
78
+ }
79
+ }
80
+ // ---------------------------------------------------------------------------
81
+ // Type-Specific Parsers
82
+ // ---------------------------------------------------------------------------
83
+ function parseWeightReps(record, effortFraction, restDurationSecs, guardrails) {
84
+ var _a, _b, _c;
85
+ const kg = (0, helpers_1.safeParseFloat)(record.kg);
86
+ const reps = (0, helpers_1.safeParseFloat)(record.reps);
87
+ // Estimate active duration: (reps × seconds-per-rep) + setup
88
+ 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;
89
+ const setup = (_c = guardrails === null || guardrails === void 0 ? void 0 : guardrails.setupTypicalSecs) !== null && _c !== void 0 ? _c : 0;
90
+ const estimatedDuration = reps * secsPerRep + setup;
91
+ const activeDurationSecs = resolveWorkDuration(record.workDurationSecs, estimatedDuration, guardrails);
92
+ return {
93
+ type: "weight-reps",
94
+ activeDurationSecs,
95
+ restDurationSecs,
96
+ effortFraction,
97
+ kg,
98
+ reps,
99
+ };
100
+ }
101
+ function parseRepsOnly(record, effortFraction, restDurationSecs, guardrails) {
102
+ var _a, _b, _c;
103
+ const reps = (0, helpers_1.safeParseFloat)(record.reps);
104
+ const auxWeightKg = (0, helpers_1.safeParseFloat)(record.auxWeightKg);
105
+ 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;
106
+ const setup = (_c = guardrails === null || guardrails === void 0 ? void 0 : guardrails.setupTypicalSecs) !== null && _c !== void 0 ? _c : 0;
107
+ const estimatedDuration = reps * secsPerRep + setup;
108
+ const activeDurationSecs = resolveWorkDuration(record.workDurationSecs, estimatedDuration, guardrails);
109
+ return {
110
+ type: "reps-only",
111
+ activeDurationSecs,
112
+ restDurationSecs,
113
+ effortFraction,
114
+ reps,
115
+ auxWeightKg,
116
+ };
117
+ }
118
+ function parseDuration(record, effortFraction, restDurationSecs, guardrails) {
119
+ var _a, _b;
120
+ // Primary: parse the user's recorded duration
121
+ // Fallback: exercise's typical set duration → global fallback
122
+ 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;
123
+ const durationSecs = (0, helpers_1.parseDurationMmSs)(record.durationMmSs, fallbackDuration);
124
+ const auxWeightKg = (0, helpers_1.safeParseFloat)(record.auxWeightKg);
125
+ if (durationSecs === 0)
126
+ return null;
127
+ // For duration exercises, the parsed durationMmSs IS the active duration
128
+ // We validate it against guardrails to get the final value
129
+ const activeDurationSecs = resolveWorkDuration(durationSecs, fallbackDuration, guardrails);
130
+ return {
131
+ type: "duration",
132
+ activeDurationSecs,
133
+ restDurationSecs,
134
+ effortFraction,
135
+ // Use the SAME validated value for both — activeDurationSecs is used
136
+ // for calorie time calculation, durationSecs is used for the
137
+ // short/medium/long multiplier classification. They must agree.
138
+ durationSecs: activeDurationSecs,
139
+ auxWeightKg,
140
+ };
141
+ }
142
+ function parseCardioMachine(record, effortFraction, restDurationSecs) {
143
+ const speed = (0, helpers_1.safeParseFloat)(record.speed);
144
+ const inclinePercentage = (0, helpers_1.safeParseFloat)(record.inclinePercentage);
145
+ const resistanceLevel = (0, helpers_1.safeParseFloat)(record.resistanceLevel);
146
+ const distance = (0, helpers_1.safeParseFloat)(record.distance);
147
+ const cardioDurationSecs = (0, helpers_1.parseDurationMmSs)(record.durationMmSs);
148
+ // Need at least duration or distance to be meaningful
149
+ if (cardioDurationSecs === 0 && distance === 0)
150
+ return null;
151
+ const activeDurationSecs = cardioDurationSecs > 0 ? cardioDurationSecs : 0;
152
+ return {
153
+ type: "cardio-machine",
154
+ activeDurationSecs,
155
+ restDurationSecs,
156
+ effortFraction,
157
+ speed,
158
+ inclinePercentage,
159
+ resistanceLevel,
160
+ distance,
161
+ cardioDurationSecs,
162
+ };
163
+ }
164
+ function parseCardioFree(record, effortFraction, restDurationSecs) {
165
+ const distance = (0, helpers_1.safeParseFloat)(record.distance);
166
+ const auxWeightKg = (0, helpers_1.safeParseFloat)(record.auxWeightKg);
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
+ auxWeightKg,
178
+ cardioDurationSecs,
179
+ };
180
+ }
181
+ // ---------------------------------------------------------------------------
182
+ // Duration Resolution Helpers
183
+ // ---------------------------------------------------------------------------
184
+ /**
185
+ * Resolve and validate work duration for a set.
186
+ *
187
+ * Validation cascade:
188
+ * ┌──────────────────────────────────────────────────────────────┐
189
+ * │ 1. Absolute sanity check (catches truly broken data) │
190
+ * │ measured < 1s or > 14400s → reject │
191
+ * │ │
192
+ * │ 2. Exercise-specific validation (PRIMARY) │
193
+ * │ duration type: check against setDuration.min / max │
194
+ * │ rep type: check against estimate ± tolerance │
195
+ * │ Grace factor: 2× on max, 0.5× on min │
196
+ * │ │
197
+ * │ 3. Passed all checks → use measured value │
198
+ * │ │
199
+ * │ 4. No measured value → use estimated │
200
+ * └──────────────────────────────────────────────────────────────┘
201
+ */
202
+ function resolveWorkDuration(measured, estimated, guardrails) {
203
+ var _a, _b;
204
+ if (measured !== undefined && measured > 0) {
205
+ // 1. Absolute sanity check
206
+ if (measured < constants_1.ABSOLUTE_WORK_MIN || measured > constants_1.ABSOLUTE_WORK_MAX) {
207
+ return Math.max(estimated, constants_1.ABSOLUTE_WORK_MIN);
208
+ }
209
+ // 2. Exercise-specific validation for duration-type exercises
210
+ if (guardrails === null || guardrails === void 0 ? void 0 : guardrails.setDuration) {
211
+ const minReasonable = ((_a = guardrails.setDuration.min) !== null && _a !== void 0 ? _a : 1) * 0.5;
212
+ const maxReasonable = ((_b = guardrails.setDuration.max) !== null && _b !== void 0 ? _b : constants_1.ABSOLUTE_WORK_MAX) * 2;
213
+ if (measured < minReasonable || measured > maxReasonable) {
214
+ return estimated; // Measured seems wrong for this exercise
215
+ }
216
+ }
217
+ // 2b. Exercise-specific validation for rep-based exercises
218
+ if ((guardrails === null || guardrails === void 0 ? void 0 : guardrails.singleRep) && estimated > 0) {
219
+ // Measured should be within 3× of our estimate
220
+ // (generous because user might pause mid-set)
221
+ if (measured > estimated * 3 || measured < estimated * 0.2) {
222
+ return estimated;
223
+ }
224
+ }
225
+ // 3. Passed validation
226
+ return measured;
227
+ }
228
+ // 4. No measured value
229
+ return Math.max(estimated, constants_1.ABSOLUTE_WORK_MIN);
230
+ }
231
+ /**
232
+ * Resolve and validate rest duration for a set.
233
+ *
234
+ * Validation cascade:
235
+ * ┌──────────────────────────────────────────────────────────────┐
236
+ * │ 1. Last set with no data → null (no rest after final set) │
237
+ * │ │
238
+ * │ 2. Absolute sanity check (< 0 or > 3600s) │
239
+ * │ → use exercise typical rest │
240
+ * │ │
241
+ * │ 3. Exercise-specific validation (PRIMARY) │
242
+ * │ Check against restPeriods.minimum / maximum │
243
+ * │ Grace factor: 50% on each side │
244
+ * │ Outside → use exercise typical rest │
245
+ * │ │
246
+ * │ 4. Passed checks → use measured value │
247
+ * │ │
248
+ * │ 5. No measured data → exercise typical → global fallback │
249
+ * └──────────────────────────────────────────────────────────────┘
250
+ */
251
+ function resolveRestDuration(measured, setIndex, totalSets, guardrails) {
252
+ var _a, _b, _c;
253
+ // 1. Last set with no rest data → null
254
+ if (setIndex === totalSets - 1 &&
255
+ (measured === undefined || measured === 0)) {
256
+ return null;
257
+ }
258
+ // Extract exercise-specific rest config
259
+ const exerciseRestMin = (_a = guardrails === null || guardrails === void 0 ? void 0 : guardrails.restPeriods) === null || _a === void 0 ? void 0 : _a.minimum;
260
+ const exerciseRestMax = (_b = guardrails === null || guardrails === void 0 ? void 0 : guardrails.restPeriods) === null || _b === void 0 ? void 0 : _b.maximum;
261
+ const exerciseRestTypical = (_c = guardrails === null || guardrails === void 0 ? void 0 : guardrails.restPeriods) === null || _c === void 0 ? void 0 : _c.typical;
262
+ if (measured !== undefined && measured > 0) {
263
+ // 2. Absolute sanity check
264
+ if (measured < constants_1.ABSOLUTE_REST_MIN || measured > constants_1.ABSOLUTE_REST_MAX) {
265
+ return exerciseRestTypical !== null && exerciseRestTypical !== void 0 ? exerciseRestTypical : constants_1.FALLBACK_REST_SECS;
266
+ }
267
+ // 3. Exercise-specific validation
268
+ if (exerciseRestMin !== undefined && exerciseRestMax !== undefined) {
269
+ const graceFactor = 0.5;
270
+ const lowerBound = exerciseRestMin * (1 - graceFactor);
271
+ const upperBound = exerciseRestMax * (1 + graceFactor);
272
+ if (measured < lowerBound || measured > upperBound) {
273
+ return exerciseRestTypical !== null && exerciseRestTypical !== void 0 ? exerciseRestTypical : constants_1.FALLBACK_REST_SECS;
274
+ }
275
+ }
276
+ // 4. Passed validation
277
+ return measured;
278
+ }
279
+ // 5. No measured data → exercise typical → global fallback
280
+ return exerciseRestTypical !== null && exerciseRestTypical !== void 0 ? exerciseRestTypical : constants_1.FALLBACK_REST_SECS;
281
+ }