@dgpholdings/greatoak-shared 1.1.41 → 1.1.43
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/types/TApiExercise.d.ts +45 -3
- package/dist/types/TApiExerciseRecord.d.ts +2 -1
- package/dist/types/TApiTemplateData.d.ts +11 -2
- package/dist/utils/record.utils.d.ts +4 -2
- package/dist/utils/record.utils.js +10 -7
- package/dist/utils/scoring.utils.d.ts +1 -1
- package/dist/utils/scoring.utils.js +145 -64
- package/dist/utils/workoutSummary.util.js +6 -3
- package/package.json +1 -1
|
@@ -29,6 +29,46 @@ export declare enum EBodyParts {
|
|
|
29
29
|
}
|
|
30
30
|
export type TBodyPartKeys = Array<keyof typeof EBodyParts>;
|
|
31
31
|
export type TTrainingType = "band" | "body-weight" | "weight" | "yoga" | "cardio" | "stretching" | "plyometric" | "sports-specific" | "rehabilitation" | "balance" | "isometric";
|
|
32
|
+
type TBaseTimingGuardrails = {
|
|
33
|
+
stressRestBonus: number;
|
|
34
|
+
fatigueMultiplier: number;
|
|
35
|
+
setupTypicalSecs: number;
|
|
36
|
+
restPeriods: {
|
|
37
|
+
minimum: number;
|
|
38
|
+
typical: number;
|
|
39
|
+
maximum: number;
|
|
40
|
+
optimalRange: [number, number];
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export type TTimingGuardrails = TBaseTimingGuardrails & ({
|
|
44
|
+
type: "weight-reps";
|
|
45
|
+
singleRep: {
|
|
46
|
+
min: number;
|
|
47
|
+
max: number;
|
|
48
|
+
typical: number;
|
|
49
|
+
};
|
|
50
|
+
} | {
|
|
51
|
+
type: "reps-only";
|
|
52
|
+
singleRep: {
|
|
53
|
+
min: number;
|
|
54
|
+
max: number;
|
|
55
|
+
typical: number;
|
|
56
|
+
};
|
|
57
|
+
} | {
|
|
58
|
+
type: "duration";
|
|
59
|
+
setDuration: {
|
|
60
|
+
min: number;
|
|
61
|
+
max: number;
|
|
62
|
+
typical: number;
|
|
63
|
+
};
|
|
64
|
+
} | {
|
|
65
|
+
type: "distance";
|
|
66
|
+
pacing: {
|
|
67
|
+
minPacePerUnit: number;
|
|
68
|
+
maxPacePerUnit: number;
|
|
69
|
+
typicalPacePerUnit: number;
|
|
70
|
+
};
|
|
71
|
+
});
|
|
32
72
|
export type TExercise = {
|
|
33
73
|
exerciseId: string;
|
|
34
74
|
name: string;
|
|
@@ -38,7 +78,7 @@ export type TExercise = {
|
|
|
38
78
|
primaryMuscles: TBodyPartKeys;
|
|
39
79
|
secondaryMuscles: TBodyPartKeys;
|
|
40
80
|
trainingTypes: TTrainingType[];
|
|
41
|
-
|
|
81
|
+
timingGuardrails?: TTimingGuardrails;
|
|
42
82
|
difficultyLevel: number;
|
|
43
83
|
hypertrophyLevel: number;
|
|
44
84
|
strengthGainLevel: number;
|
|
@@ -81,10 +121,11 @@ export type TExercise = {
|
|
|
81
121
|
importantTipsHtml: string;
|
|
82
122
|
};
|
|
83
123
|
export type TBodyPartExercises = Record<TBodyPart, TExercise[]>;
|
|
84
|
-
export type
|
|
124
|
+
export type TApiCreateOrUpdateExerciseReq = {
|
|
85
125
|
exercise: Omit<TExercise, "exerciseId">;
|
|
126
|
+
updateId?: string;
|
|
86
127
|
};
|
|
87
|
-
export type
|
|
128
|
+
export type TApiCreateOrUpdateExerciseRes = {
|
|
88
129
|
success: boolean;
|
|
89
130
|
message: string;
|
|
90
131
|
};
|
|
@@ -92,3 +133,4 @@ export type TApiListExercisesReq = null;
|
|
|
92
133
|
export type TApiListExercisesRes = {
|
|
93
134
|
data: TExercise[];
|
|
94
135
|
};
|
|
136
|
+
export {};
|
|
@@ -4,6 +4,7 @@ export type TRecord = {
|
|
|
4
4
|
setNote?: string;
|
|
5
5
|
workDurationSecs?: number;
|
|
6
6
|
restDurationSecs?: number;
|
|
7
|
+
isStrictMode: boolean;
|
|
7
8
|
} & ({
|
|
8
9
|
type: "weight-reps";
|
|
9
10
|
kg: string;
|
|
@@ -21,8 +22,8 @@ export type TRecord = {
|
|
|
21
22
|
} | {
|
|
22
23
|
type: "distance";
|
|
23
24
|
distanceKm: string;
|
|
25
|
+
auxWeightKg: string;
|
|
24
26
|
durationSecs: string;
|
|
25
|
-
avgPaceSecsPerKm?: string;
|
|
26
27
|
});
|
|
27
28
|
export type TRecordDuration = Extract<TRecord, {
|
|
28
29
|
type: "duration";
|
|
@@ -6,6 +6,10 @@ export type TTemplate = {
|
|
|
6
6
|
exerciseIds: string[];
|
|
7
7
|
lastUsed?: Date[];
|
|
8
8
|
createdAt?: string;
|
|
9
|
+
colorHex?: string;
|
|
10
|
+
config: {
|
|
11
|
+
isEnabledStrictDurationTrackingMode: boolean;
|
|
12
|
+
};
|
|
9
13
|
};
|
|
10
14
|
export type TTemplateExercise = {
|
|
11
15
|
template: TTemplate;
|
|
@@ -40,7 +44,6 @@ export type TApiTemplateUpdateRes = {
|
|
|
40
44
|
export type TExerciseLatestRecord = {
|
|
41
45
|
recordDate: Date;
|
|
42
46
|
exerciseNote?: string;
|
|
43
|
-
restTimeSecs?: number;
|
|
44
47
|
score: number;
|
|
45
48
|
records: {
|
|
46
49
|
kg?: Extract<TRecord, {
|
|
@@ -63,10 +66,16 @@ export type TExerciseLatestRecord = {
|
|
|
63
66
|
type: "weight-reps";
|
|
64
67
|
}>["rir"];
|
|
65
68
|
setNote?: TRecord["setNote"];
|
|
69
|
+
distanceKm?: Extract<TRecord, {
|
|
70
|
+
type: "distance";
|
|
71
|
+
}>["distanceKm"];
|
|
72
|
+
isStrictMode: TRecord["isStrictMode"];
|
|
73
|
+
workDurationSecs?: number;
|
|
74
|
+
restDurationSecs?: number;
|
|
66
75
|
}[];
|
|
67
76
|
config: {
|
|
68
77
|
enableAuxWeight: boolean;
|
|
69
|
-
|
|
78
|
+
enableTimeIntervalMode: boolean;
|
|
70
79
|
enableSetNote: boolean;
|
|
71
80
|
enableExerciseNote: boolean;
|
|
72
81
|
enableEffort: "rir" | "rpe" | "none";
|
|
@@ -6,6 +6,7 @@ type RefinedBase = {
|
|
|
6
6
|
workDurationSecs?: number;
|
|
7
7
|
restDurationSecs?: number;
|
|
8
8
|
setNote?: string;
|
|
9
|
+
isStrictMode: boolean;
|
|
9
10
|
};
|
|
10
11
|
export type TRefinedWeightRecord = RefinedBase & {
|
|
11
12
|
type: "weight-reps";
|
|
@@ -14,7 +15,7 @@ export type TRefinedWeightRecord = RefinedBase & {
|
|
|
14
15
|
};
|
|
15
16
|
export type TRefinedDurationRecord = RefinedBase & {
|
|
16
17
|
type: "duration";
|
|
17
|
-
durationSecs:
|
|
18
|
+
durationSecs: number;
|
|
18
19
|
auxWeightKg?: number;
|
|
19
20
|
};
|
|
20
21
|
export type TRefinedBodyWeightRecord = RefinedBase & {
|
|
@@ -25,7 +26,8 @@ export type TRefinedBodyWeightRecord = RefinedBase & {
|
|
|
25
26
|
export type TRefinedDistanceRecord = RefinedBase & {
|
|
26
27
|
type: "distance";
|
|
27
28
|
distanceKm: number;
|
|
28
|
-
|
|
29
|
+
auxWeightKg?: number;
|
|
30
|
+
durationSecs: number;
|
|
29
31
|
avgPaceSecsPerKm?: number;
|
|
30
32
|
};
|
|
31
33
|
export type TRefinedRecord = TRefinedWeightRecord | TRefinedDurationRecord | TRefinedBodyWeightRecord | TRefinedDistanceRecord;
|
|
@@ -4,8 +4,8 @@ exports.refineRecordEntry = void 0;
|
|
|
4
4
|
const isDefined_utils_1 = require("./isDefined.utils");
|
|
5
5
|
const number_util_1 = require("./number.util");
|
|
6
6
|
const refineRecordEntry = (entry) => {
|
|
7
|
-
var _a, _b, _c, _d, _e;
|
|
8
|
-
const base = Object.assign(Object.assign(Object.assign(Object.assign({ isDone: entry.isDone }, ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rpe)) && { rpe: (0, number_util_1.toNumber)(entry.rpe) })), ((0, isDefined_utils_1.isDefinedNumber)(entry.workDurationSecs) && {
|
|
7
|
+
var _a, _b, _c, _d, _e, _f;
|
|
8
|
+
const base = Object.assign(Object.assign(Object.assign(Object.assign({ isDone: entry.isDone, isStrictMode: entry.isStrictMode }, ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rpe)) && { rpe: (0, number_util_1.toNumber)(entry.rpe) })), ((0, isDefined_utils_1.isDefinedNumber)(entry.workDurationSecs) && {
|
|
9
9
|
workDurationSecs: entry.workDurationSecs,
|
|
10
10
|
})), ((0, isDefined_utils_1.isDefinedNumber)(entry.restDurationSecs) && {
|
|
11
11
|
restDurationSecs: entry.restDurationSecs,
|
|
@@ -16,25 +16,28 @@ const refineRecordEntry = (entry) => {
|
|
|
16
16
|
: undefined, reps: (_b = (0, number_util_1.toNumber)(entry.reps)) !== null && _b !== void 0 ? _b : 0 });
|
|
17
17
|
}
|
|
18
18
|
if (entry.type === "duration") {
|
|
19
|
-
return Object.assign(Object.assign(Object.assign({}, base), { type: "duration", durationSecs: entry.durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.auxWeightKg)) && {
|
|
19
|
+
return Object.assign(Object.assign(Object.assign({}, base), { type: "duration", durationSecs: (_c = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _c !== void 0 ? _c : 0 }), ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.auxWeightKg)) && {
|
|
20
20
|
auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
|
|
21
21
|
}));
|
|
22
22
|
}
|
|
23
23
|
if (entry.type === "reps-only") {
|
|
24
24
|
return Object.assign(Object.assign(Object.assign({}, base), { type: "reps-only", rir: (0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.rir))
|
|
25
25
|
? (0, number_util_1.toNumber)(entry.rir)
|
|
26
|
-
: undefined, reps: (
|
|
26
|
+
: undefined, reps: (_d = (0, number_util_1.toNumber)(entry.reps)) !== null && _d !== void 0 ? _d : 0 }), ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.auxWeightKg)) && {
|
|
27
27
|
auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
|
|
28
28
|
}));
|
|
29
29
|
}
|
|
30
30
|
if (entry.type === "distance") {
|
|
31
|
-
const distanceKm = (
|
|
32
|
-
const durationSecs = (
|
|
31
|
+
const distanceKm = (_e = (0, number_util_1.toNumber)(entry.distanceKm)) !== null && _e !== void 0 ? _e : 0;
|
|
32
|
+
const durationSecs = (_f = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _f !== void 0 ? _f : 0;
|
|
33
33
|
// Calculate pace if both distance and duration are provided
|
|
34
34
|
const avgPaceSecsPerKm = distanceKm > 0 && durationSecs > 0
|
|
35
35
|
? durationSecs / distanceKm
|
|
36
36
|
: undefined;
|
|
37
|
-
return Object.assign(Object.assign(Object.assign({}, base), { type: "distance", distanceKm,
|
|
37
|
+
return Object.assign(Object.assign(Object.assign(Object.assign({}, base), { type: "distance", distanceKm,
|
|
38
|
+
durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.auxWeightKg)) && {
|
|
39
|
+
auxWeightKg: (0, number_util_1.toNumber)(entry.auxWeightKg),
|
|
40
|
+
})), ((0, isDefined_utils_1.isDefinedNumber)(avgPaceSecsPerKm) && { avgPaceSecsPerKm }));
|
|
38
41
|
}
|
|
39
42
|
throw new Error(`Unknown record type: ${entry.type}`);
|
|
40
43
|
};
|
|
@@ -30,7 +30,7 @@ type TParams = {
|
|
|
30
30
|
isTimeIntervalModeEnabled?: boolean;
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
|
-
* Enhanced training stress score computation using metabolic data
|
|
33
|
+
* Enhanced training stress score computation using metabolic data and timing guardrails
|
|
34
34
|
* This is for scoring a SINGLE record of an exercise. Ref: TRecord, TRefinedRecord.
|
|
35
35
|
* To calculate complete scoring/summary of workout use: calculateWorkoutSummary(...) function
|
|
36
36
|
*/
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.computeScoreFromRecord = void 0;
|
|
4
|
-
const time_util_1 = require("./time.util");
|
|
5
4
|
/**
|
|
6
5
|
* Calculate BMR using Mifflin-St Jeor Equation with body composition adjustment
|
|
7
6
|
*/
|
|
@@ -29,7 +28,7 @@ const calculateBMR = (userProfile) => {
|
|
|
29
28
|
* Calculate precise MET value using exercise metabolic data
|
|
30
29
|
*/
|
|
31
30
|
const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
|
|
32
|
-
var _a, _b, _c
|
|
31
|
+
var _a, _b, _c;
|
|
33
32
|
const { metabolicData } = exercise;
|
|
34
33
|
const userWeight = (_a = userProfile.weightKg) !== null && _a !== void 0 ? _a : 70;
|
|
35
34
|
const fitnessLevel = (_b = userProfile.fitnessLevel) !== null && _b !== void 0 ? _b : 3;
|
|
@@ -73,7 +72,7 @@ const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
|
|
|
73
72
|
break;
|
|
74
73
|
}
|
|
75
74
|
case "duration": {
|
|
76
|
-
const durationSecs =
|
|
75
|
+
const durationSecs = record.durationSecs;
|
|
77
76
|
if (metabolicData.durationFactors) {
|
|
78
77
|
let durationMultiplier = metabolicData.durationFactors.mediumDuration;
|
|
79
78
|
if (durationSecs < 30) {
|
|
@@ -87,8 +86,8 @@ const calculateMETValue = (record, exercise, effortFactor, userProfile) => {
|
|
|
87
86
|
break;
|
|
88
87
|
}
|
|
89
88
|
case "distance": {
|
|
90
|
-
const distance = (
|
|
91
|
-
const durationSecs =
|
|
89
|
+
const distance = (_c = record.distanceKm) !== null && _c !== void 0 ? _c : 0;
|
|
90
|
+
const durationSecs = record.durationSecs;
|
|
92
91
|
if (distance > 0 && durationSecs > 0 && metabolicData.paceFactors) {
|
|
93
92
|
const speedKmh = (distance * 3600) / durationSecs;
|
|
94
93
|
// Find closest pace factor
|
|
@@ -146,32 +145,53 @@ const getRepIntensityAdjustment = (reps, scalingType) => {
|
|
|
146
145
|
}
|
|
147
146
|
};
|
|
148
147
|
/**
|
|
149
|
-
* Calculate exercise duration in minutes
|
|
148
|
+
* Calculate exercise duration in minutes using timing guardrails
|
|
150
149
|
*/
|
|
151
150
|
const getExerciseDurationMinutes = (record, exercise) => {
|
|
152
|
-
var _a, _b;
|
|
153
151
|
let durationSecs = 0;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
152
|
+
const timingGuardrails = exercise.timingGuardrails;
|
|
153
|
+
// Use actual recorded work duration if available and in strict mode
|
|
154
|
+
if (record.workDurationSecs && record.isStrictMode) {
|
|
155
|
+
durationSecs = record.workDurationSecs;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
// Calculate expected duration using timing guardrails
|
|
159
|
+
switch (record.type) {
|
|
160
|
+
case "weight-reps":
|
|
161
|
+
case "reps-only": {
|
|
162
|
+
if (timingGuardrails &&
|
|
163
|
+
(timingGuardrails.type === "weight-reps" ||
|
|
164
|
+
timingGuardrails.type === "reps-only") &&
|
|
165
|
+
timingGuardrails.singleRep) {
|
|
166
|
+
const repTime = timingGuardrails.singleRep.typical;
|
|
167
|
+
const setupTime = timingGuardrails.setupTypicalSecs || 0;
|
|
168
|
+
durationSecs = record.reps * repTime + setupTime;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// Fallback to difficulty-based estimation
|
|
172
|
+
const baseTimePerRep = exercise.difficultyLevel > 7 ? 3.5 : 2.5;
|
|
173
|
+
durationSecs = record.reps * baseTimePerRep;
|
|
174
|
+
}
|
|
175
|
+
break;
|
|
159
176
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
177
|
+
case "duration": {
|
|
178
|
+
if (record.workDurationSecs && record.isStrictMode) {
|
|
179
|
+
durationSecs = record.workDurationSecs;
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
durationSecs = record.durationSecs;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
case "distance": {
|
|
187
|
+
if (record.workDurationSecs && record.isStrictMode) {
|
|
188
|
+
durationSecs = record.workDurationSecs;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
durationSecs = record.durationSecs;
|
|
192
|
+
}
|
|
193
|
+
break;
|
|
165
194
|
}
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
case "duration": {
|
|
169
|
-
durationSecs = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
case "distance": {
|
|
173
|
-
durationSecs = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
|
|
174
|
-
break;
|
|
175
195
|
}
|
|
176
196
|
}
|
|
177
197
|
// Add rest time if available (but not for distance exercises)
|
|
@@ -203,12 +223,77 @@ const calculateCalorieBurn = (record, exercise, metValue, durationMinutes, userP
|
|
|
203
223
|
};
|
|
204
224
|
};
|
|
205
225
|
/**
|
|
206
|
-
*
|
|
226
|
+
* Calculate timing adherence bonus/penalty using timing guardrails
|
|
227
|
+
*/
|
|
228
|
+
const calculateTimingAdherence = (record, exercise, workingScore) => {
|
|
229
|
+
const timingGuardrails = exercise.timingGuardrails;
|
|
230
|
+
if (!timingGuardrails || !record.workDurationSecs) {
|
|
231
|
+
return { bonus: 0, penalty: 0, reason: "no_timing_data" };
|
|
232
|
+
}
|
|
233
|
+
let expectedDuration = 0;
|
|
234
|
+
let tolerance = 0;
|
|
235
|
+
switch (record.type) {
|
|
236
|
+
case "weight-reps":
|
|
237
|
+
case "reps-only": {
|
|
238
|
+
if ((timingGuardrails.type === "weight-reps" ||
|
|
239
|
+
timingGuardrails.type === "reps-only") &&
|
|
240
|
+
timingGuardrails.singleRep) {
|
|
241
|
+
const repTime = timingGuardrails.singleRep.typical;
|
|
242
|
+
const setupTime = timingGuardrails.setupTypicalSecs || 0;
|
|
243
|
+
expectedDuration = record.reps * repTime + setupTime;
|
|
244
|
+
tolerance = expectedDuration * 0.25; // 25% tolerance
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case "duration": {
|
|
249
|
+
if (timingGuardrails.type === "duration" &&
|
|
250
|
+
timingGuardrails.setDuration) {
|
|
251
|
+
expectedDuration = record.durationSecs;
|
|
252
|
+
tolerance = timingGuardrails.setDuration.typical * 0.15; // 15% tolerance for duration exercises
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case "distance": {
|
|
257
|
+
if (timingGuardrails.type === "distance" &&
|
|
258
|
+
timingGuardrails.pacing &&
|
|
259
|
+
record.distanceKm) {
|
|
260
|
+
expectedDuration =
|
|
261
|
+
record.distanceKm * timingGuardrails.pacing.typicalPacePerUnit;
|
|
262
|
+
tolerance = expectedDuration * 0.2; // 20% tolerance for distance
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (expectedDuration === 0) {
|
|
268
|
+
return { bonus: 0, penalty: 0, reason: "no_expected_duration" };
|
|
269
|
+
}
|
|
270
|
+
const actualDuration = record.workDurationSecs;
|
|
271
|
+
const deviation = Math.abs(actualDuration - expectedDuration);
|
|
272
|
+
// Strict mode gets bonus for precision
|
|
273
|
+
if (record.isStrictMode && deviation <= tolerance) {
|
|
274
|
+
const precisionBonus = workingScore * 0.05; // 5% bonus for precise timing
|
|
275
|
+
return {
|
|
276
|
+
bonus: precisionBonus,
|
|
277
|
+
penalty: 0,
|
|
278
|
+
reason: "strict_mode_precision",
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
// Penalty for significant deviation (only in strict mode)
|
|
282
|
+
if (record.isStrictMode && deviation > tolerance) {
|
|
283
|
+
const deviationPercent = (deviation - tolerance) / expectedDuration;
|
|
284
|
+
const timingPenalty = Math.min(workingScore * 0.08, // Max 8% penalty
|
|
285
|
+
deviationPercent * workingScore * 0.03);
|
|
286
|
+
return { bonus: 0, penalty: timingPenalty, reason: "timing_deviation" };
|
|
287
|
+
}
|
|
288
|
+
return { bonus: 0, penalty: 0, reason: "relaxed_mode" };
|
|
289
|
+
};
|
|
290
|
+
/**
|
|
291
|
+
* Enhanced training stress score computation using metabolic data and timing guardrails
|
|
207
292
|
* This is for scoring a SINGLE record of an exercise. Ref: TRecord, TRefinedRecord.
|
|
208
293
|
* To calculate complete scoring/summary of workout use: calculateWorkoutSummary(...) function
|
|
209
294
|
*/
|
|
210
295
|
const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userProfile, isTimeIntervalModeEnabled = false, }) => {
|
|
211
|
-
var _a
|
|
296
|
+
var _a;
|
|
212
297
|
if (!record.isDone) {
|
|
213
298
|
return {
|
|
214
299
|
baseScore: 0,
|
|
@@ -232,7 +317,7 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
|
|
|
232
317
|
}
|
|
233
318
|
// Calculate precise MET value using exercise metabolic data
|
|
234
319
|
const metValue = calculateMETValue(record, exercise, effortFactor, userProfile || {});
|
|
235
|
-
// Calculate exercise duration
|
|
320
|
+
// Calculate exercise duration using timing guardrails
|
|
236
321
|
const durationMinutes = getExerciseDurationMinutes(record, exercise);
|
|
237
322
|
// Calculate comprehensive calorie burn
|
|
238
323
|
const calorieData = calculateCalorieBurn(record, exercise, metValue, durationMinutes, userProfile || {});
|
|
@@ -294,6 +379,20 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
|
|
|
294
379
|
intervalTraining: parseFloat((intervalScoreBonus - 1).toFixed(3)),
|
|
295
380
|
});
|
|
296
381
|
}
|
|
382
|
+
// Timing adherence bonus/penalty using guardrails
|
|
383
|
+
const timingAdherence = calculateTimingAdherence(record, exercise, workingScore);
|
|
384
|
+
if (timingAdherence.bonus > 0) {
|
|
385
|
+
workingScore += timingAdherence.bonus;
|
|
386
|
+
plus.push({
|
|
387
|
+
timingPrecision: parseFloat(timingAdherence.bonus.toFixed(3)),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (timingAdherence.penalty > 0) {
|
|
391
|
+
workingScore -= timingAdherence.penalty;
|
|
392
|
+
minus.push({
|
|
393
|
+
timingDeviation: parseFloat(timingAdherence.penalty.toFixed(3)),
|
|
394
|
+
});
|
|
395
|
+
}
|
|
297
396
|
// User profile adjustments
|
|
298
397
|
let profileMultiplier = 1;
|
|
299
398
|
if ((userProfile === null || userProfile === void 0 ? void 0 : userProfile.gender) === "female") {
|
|
@@ -315,43 +414,25 @@ const computeScoreFromRecord = ({ avgRestDurationSecs, record, exercise, userPro
|
|
|
315
414
|
profileAdjustment: parseFloat((profileMultiplier - 1).toFixed(3)),
|
|
316
415
|
});
|
|
317
416
|
}
|
|
318
|
-
// Enhanced rest efficiency penalty
|
|
417
|
+
// Enhanced rest efficiency penalty using timing guardrails
|
|
319
418
|
if (typeof record.restDurationSecs === "number" &&
|
|
320
|
-
typeof avgRestDurationSecs === "number"
|
|
419
|
+
typeof avgRestDurationSecs === "number" &&
|
|
420
|
+
((_a = exercise.timingGuardrails) === null || _a === void 0 ? void 0 : _a.restPeriods)) {
|
|
321
421
|
const restUsed = record.restDurationSecs;
|
|
322
|
-
const
|
|
323
|
-
const
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
if (isTimeIntervalModeEnabled && record.workDurationSecs) {
|
|
337
|
-
let actualDuration = 0;
|
|
338
|
-
if (record.type === "duration") {
|
|
339
|
-
actualDuration = (0, time_util_1.mmssToSecs)((_a = record.durationSecs) !== null && _a !== void 0 ? _a : "00:00");
|
|
340
|
-
}
|
|
341
|
-
else if (record.type === "distance") {
|
|
342
|
-
actualDuration = (0, time_util_1.mmssToSecs)((_b = record.durationSecs) !== null && _b !== void 0 ? _b : "00:00");
|
|
343
|
-
}
|
|
344
|
-
if (actualDuration > 0) {
|
|
345
|
-
const expectedDuration = exercise.avgSingleRepDurationInSecs || record.workDurationSecs;
|
|
346
|
-
const durationDiff = Math.abs(actualDuration - expectedDuration);
|
|
347
|
-
const tolerance = expectedDuration * 0.25; // 25% tolerance
|
|
348
|
-
if (durationDiff > tolerance) {
|
|
349
|
-
const excessPercent = (durationDiff - tolerance) / expectedDuration;
|
|
350
|
-
const timingPenalty = Math.min(0.08 * workingScore, excessPercent * 0.05 * workingScore);
|
|
351
|
-
workingScore -= timingPenalty;
|
|
352
|
-
minus.push({
|
|
353
|
-
timingDeviationPenalty: parseFloat(timingPenalty.toFixed(3)),
|
|
354
|
-
});
|
|
422
|
+
const restGuardrails = exercise.timingGuardrails.restPeriods;
|
|
423
|
+
const optimalRest = restGuardrails.typical;
|
|
424
|
+
const maxAcceptableRest = restGuardrails.maximum;
|
|
425
|
+
// Calculate stress-based rest bonus from guardrails
|
|
426
|
+
const stressBonus = exercise.timingGuardrails.stressRestBonus || 0;
|
|
427
|
+
const adjustedOptimalRest = optimalRest + (effortFactor > 7 ? stressBonus : 0);
|
|
428
|
+
if (restUsed > adjustedOptimalRest) {
|
|
429
|
+
const excessRest = restUsed - adjustedOptimalRest;
|
|
430
|
+
const maxExcess = maxAcceptableRest - adjustedOptimalRest;
|
|
431
|
+
if (excessRest > 0) {
|
|
432
|
+
const penaltyRatio = Math.min(1, excessRest / maxExcess);
|
|
433
|
+
const restPenalty = penaltyRatio * 0.1 * workingScore; // Up to 10% penalty
|
|
434
|
+
workingScore -= restPenalty;
|
|
435
|
+
minus.push({ excessRestPenalty: parseFloat(restPenalty.toFixed(3)) });
|
|
355
436
|
}
|
|
356
437
|
}
|
|
357
438
|
}
|
|
@@ -88,7 +88,7 @@ const calculateWorkoutSummary = (exercises, userProfile) => {
|
|
|
88
88
|
const muscleRecovery = calculateMuscleRecovery(muscleStressMap, exercises);
|
|
89
89
|
// Determine overall fatigue level
|
|
90
90
|
const averageScore = totalSets > 0 ? totalScore / totalSets : 0;
|
|
91
|
-
const fatigueLevel = getFatigueLevel(averageScore, totalScore, workoutDuration);
|
|
91
|
+
const fatigueLevel = getFatigueLevel(averageScore, totalScore, workoutDuration, userProfile);
|
|
92
92
|
const recommendedRestDays = getRecommendedRestDays(fatigueLevel, muscleRecovery);
|
|
93
93
|
return {
|
|
94
94
|
totalScore: Math.round(totalScore),
|
|
@@ -240,8 +240,11 @@ const getMuscleBaseRecovery = (muscle) => {
|
|
|
240
240
|
/**
|
|
241
241
|
* Determine overall fatigue level
|
|
242
242
|
*/
|
|
243
|
-
const getFatigueLevel = (averageScore, totalScore, workoutDuration) => {
|
|
244
|
-
const
|
|
243
|
+
const getFatigueLevel = (averageScore, totalScore, workoutDuration, userProfile) => {
|
|
244
|
+
const fitnessAdjustment = (userProfile === null || userProfile === void 0 ? void 0 : userProfile.fitnessLevel)
|
|
245
|
+
? (userProfile.fitnessLevel - 3) * 0.1 // ±20% based on fitness level
|
|
246
|
+
: 0;
|
|
247
|
+
const intensity = averageScore * (1 - fitnessAdjustment);
|
|
245
248
|
const volume = totalScore / 100; // Normalize volume
|
|
246
249
|
const density = totalScore / workoutDuration; // Score per minute
|
|
247
250
|
if (intensity > 80 || volume > 15 || density > 8)
|