@dgpholdings/greatoak-shared 1.1.41 → 1.1.42

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.
@@ -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
- avgSingleRepDurationInSecs?: number;
81
+ timingGuardrails?: TTimingGuardrails;
42
82
  difficultyLevel: number;
43
83
  hypertrophyLevel: number;
44
84
  strengthGainLevel: number;
@@ -92,3 +132,4 @@ export type TApiListExercisesReq = null;
92
132
  export type TApiListExercisesRes = {
93
133
  data: TExercise[];
94
134
  };
135
+ 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
- enableRestTime: boolean;
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: string;
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
- durationSecs: string;
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: (_c = (0, number_util_1.toNumber)(entry.reps)) !== null && _c !== void 0 ? _c : 0 }), ((0, isDefined_utils_1.isDefinedNumber)((0, number_util_1.toNumber)(entry.auxWeightKg)) && {
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 = (_d = (0, number_util_1.toNumber)(entry.distanceKm)) !== null && _d !== void 0 ? _d : 0;
32
- const durationSecs = (_e = (0, number_util_1.toNumber)(entry.durationSecs)) !== null && _e !== void 0 ? _e : 0;
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, durationSecs: entry.durationSecs }), ((0, isDefined_utils_1.isDefinedNumber)(avgPaceSecsPerKm) && { avgPaceSecsPerKm }));
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, _d, _e;
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 = (0, time_util_1.mmssToSecs)((_c = record.durationSecs) !== null && _c !== void 0 ? _c : "00:00");
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 = (_d = record.distanceKm) !== null && _d !== void 0 ? _d : 0;
91
- const durationSecs = (0, time_util_1.mmssToSecs)((_e = record.durationSecs) !== null && _e !== void 0 ? _e : "00:00");
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
- switch (record.type) {
155
- case "weight-reps":
156
- case "reps-only": {
157
- if (exercise.avgSingleRepDurationInSecs) {
158
- durationSecs = record.reps * exercise.avgSingleRepDurationInSecs;
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
- else {
161
- // Estimate based on reps and exercise complexity
162
- const reps = record.type === "weight-reps" ? record.reps : record.reps;
163
- const baseTimePerRep = exercise.difficultyLevel > 7 ? 3.5 : 2.5;
164
- durationSecs = reps * baseTimePerRep;
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
- * Enhanced training stress score computation using metabolic data
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, _b;
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 baseGrace = 8 + exercise.difficultyLevel * 1.5; // More grace for complex exercises
323
- const effortGrace = effortFactor * 2.5;
324
- const totalGrace = baseGrace + effortGrace;
325
- const maxPenaltyThreshold = totalGrace + 45;
326
- const excessRest = restUsed - avgRestDurationSecs;
327
- if (excessRest > totalGrace) {
328
- const penaltyExcess = Math.min(excessRest - totalGrace, maxPenaltyThreshold - totalGrace);
329
- const penaltyRatio = penaltyExcess / (maxPenaltyThreshold - totalGrace);
330
- const restPenalty = penaltyRatio * 0.12 * workingScore; // Reduced penalty
331
- workingScore -= restPenalty;
332
- minus.push({ excessRestPenalty: parseFloat(restPenalty.toFixed(3)) });
333
- }
334
- }
335
- // Work duration adherence for interval training
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 intensity = averageScore;
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.1.41",
3
+ "version": "1.1.42",
4
4
  "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",