@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.
- 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/types/TApiUser.d.ts +3 -1
- 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,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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|