@aneuhold/core-ts-db-lib 4.1.12 → 4.1.14

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 (58) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/lib/browser.d.ts +11 -8
  3. package/lib/browser.d.ts.map +1 -1
  4. package/lib/browser.js +6 -4
  5. package/lib/browser.js.map +1 -1
  6. package/lib/browser.ts +23 -9
  7. package/lib/ctos/workout/WorkoutMuscleGroupVolumeCTO.d.ts +14 -0
  8. package/lib/ctos/workout/WorkoutMuscleGroupVolumeCTO.d.ts.map +1 -1
  9. package/lib/ctos/workout/WorkoutMuscleGroupVolumeCTO.ts +18 -0
  10. package/lib/documents/workout/README.md +43 -9
  11. package/lib/documents/workout/WorkoutSet.d.ts +8 -0
  12. package/lib/documents/workout/WorkoutSet.d.ts.map +1 -1
  13. package/lib/documents/workout/WorkoutSet.ts +9 -0
  14. package/lib/services/workout/Exercise/WorkoutExerciseService.d.ts +40 -0
  15. package/lib/services/workout/Exercise/WorkoutExerciseService.d.ts.map +1 -1
  16. package/lib/services/workout/Exercise/WorkoutExerciseService.js +134 -1
  17. package/lib/services/workout/Exercise/WorkoutExerciseService.js.map +1 -1
  18. package/lib/services/workout/Exercise/WorkoutExerciseService.ts +204 -1
  19. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.d.ts +15 -0
  20. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.d.ts.map +1 -1
  21. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.js +18 -1
  22. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.js.map +1 -1
  23. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.ts +19 -1
  24. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.d.ts +30 -4
  25. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.d.ts.map +1 -1
  26. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.js +58 -4
  27. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.js.map +1 -1
  28. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.ts +69 -3
  29. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.d.ts +45 -1
  30. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.d.ts.map +1 -1
  31. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.js +201 -11
  32. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.js.map +1 -1
  33. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.ts +285 -9
  34. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.d.ts +33 -0
  35. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.d.ts.map +1 -0
  36. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.js +24 -0
  37. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.js.map +1 -0
  38. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.ts +36 -0
  39. package/lib/services/workout/Session/WorkoutSessionService.d.ts.map +1 -1
  40. package/lib/services/workout/Session/WorkoutSessionService.js +1 -11
  41. package/lib/services/workout/Session/WorkoutSessionService.js.map +1 -1
  42. package/lib/services/workout/Session/WorkoutSessionService.ts +1 -17
  43. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.d.ts +14 -2
  44. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.d.ts.map +1 -1
  45. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.js +17 -3
  46. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.js.map +1 -1
  47. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.ts +28 -3
  48. package/lib/services/workout/Set/WorkoutSetService.d.ts +17 -5
  49. package/lib/services/workout/Set/WorkoutSetService.d.ts.map +1 -1
  50. package/lib/services/workout/Set/WorkoutSetService.js +83 -16
  51. package/lib/services/workout/Set/WorkoutSetService.js.map +1 -1
  52. package/lib/services/workout/Set/WorkoutSetService.ts +107 -24
  53. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.d.ts +161 -11
  54. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.d.ts.map +1 -1
  55. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.js +364 -127
  56. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.js.map +1 -1
  57. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.ts +551 -160
  58. package/package.json +1 -1
@@ -1,3 +1,5 @@
1
+ import { CycleType } from '../../../documents/workout/WorkoutMesocycle.js';
2
+ import WorkoutVolumePlanningService from '../util/VolumePlanning/WorkoutVolumePlanningService.js';
1
3
  /**
2
4
  * Central shared context for generating or updating a {@link WorkoutMesocycle}.
3
5
  *
@@ -12,14 +14,30 @@ export default class WorkoutMesocyclePlanContext {
12
14
  existingSessionExercises;
13
15
  existingSets;
14
16
  /**
15
- * A constant for now that defines what the target RIR is for the first microcycle of every mesocycle,
16
- * regardless of anything else.
17
+ * The target RIR for the first microcycle of the mesocycle. Varies by cycle
18
+ * type: MuscleGain starts at 4, Cut starts at 3, Resensitization stays at 3.
17
19
  */
18
- FIRST_MICROCYCLE_RIR = 4;
20
+ firstMicrocycleRir;
21
+ /**
22
+ * Number of microcycles between each baseline set addition.
23
+ * MuscleGain = 1 (every microcycle), Cut = 2 (every other), Resensitization = 0 (never).
24
+ */
25
+ progressionInterval;
26
+ /**
27
+ * Whether this mesocycle type skips the deload microcycle. True for
28
+ * Resensitization cycles.
29
+ */
30
+ skipDeload;
31
+ /**
32
+ * Number of accumulation (non-deload) microcycles in this mesocycle.
33
+ * Equal to the total planned count when deload is skipped, or total minus one otherwise.
34
+ */
35
+ accumulationMicrocycleCount;
19
36
  exerciseMap;
20
37
  equipmentMap;
21
38
  sessionMap;
22
39
  sessionExerciseMap;
40
+ setMap;
23
41
  microcyclesToCreate = [];
24
42
  /**
25
43
  * All microcycles for this mesocycle in chronological order.
@@ -44,22 +62,49 @@ export default class WorkoutMesocyclePlanContext {
44
62
  * ended up in.
45
63
  */
46
64
  muscleGroupToExerciseCTOsMap;
65
+ /**
66
+ * Maps muscle group ID to its estimated volume landmarks (MEV, MRV, MAV).
67
+ * Populated from WorkoutMuscleGroupVolumeCTOs when provided.
68
+ */
69
+ muscleGroupToVolumeLandmarkMap;
47
70
  exerciseIdToSessionIndex;
48
71
  /**
49
72
  * Creates a new workout mesocycle planning context.
50
73
  */
51
- constructor(mesocycle, exerciseCTOs, existingMicrocycles = [], existingSessions = [], existingSessionExercises = [], existingSets = []) {
74
+ constructor(mesocycle, exerciseCTOs, volumeCTOs = [], existingMicrocycles = [], existingSessions = [], existingSessionExercises = [], existingSets = []) {
52
75
  this.mesocycle = mesocycle;
53
76
  this.existingMicrocycles = existingMicrocycles;
54
77
  this.existingSessions = existingSessions;
55
78
  this.existingSessionExercises = existingSessionExercises;
56
79
  this.existingSets = existingSets;
80
+ // Set cycle-type-specific planning parameters
81
+ const { cycleType } = mesocycle;
82
+ if (cycleType === CycleType.Cut) {
83
+ this.firstMicrocycleRir = 3;
84
+ this.progressionInterval = 2;
85
+ this.skipDeload = false;
86
+ }
87
+ else if (cycleType === CycleType.Resensitization) {
88
+ this.firstMicrocycleRir = 3;
89
+ this.progressionInterval = 0;
90
+ this.skipDeload = true;
91
+ }
92
+ else {
93
+ this.firstMicrocycleRir = 4;
94
+ this.progressionInterval = 1;
95
+ this.skipDeload = false;
96
+ }
97
+ const totalMicrocycles = mesocycle.plannedMicrocycleCount ?? 6;
98
+ this.accumulationMicrocycleCount = this.skipDeload ? totalMicrocycles : totalMicrocycles - 1;
99
+ // Build volume landmark estimates from historical CTOs
100
+ this.muscleGroupToVolumeLandmarkMap = new Map(volumeCTOs.map((cto) => [cto._id, WorkoutVolumePlanningService.estimateVolumeLandmarks(cto)]));
57
101
  // Derive exercise map from CTOs
58
102
  this.exerciseMap = new Map(exerciseCTOs.map((cto) => [cto._id, cto]));
59
103
  // Derive equipment map from CTOs
60
104
  this.equipmentMap = new Map(exerciseCTOs.map((cto) => [cto.equipmentType._id, cto.equipmentType]));
61
105
  this.sessionMap = new Map(existingSessions.map((s) => [s._id, s]));
62
106
  this.sessionExerciseMap = new Map(existingSessionExercises.map((s) => [s._id, s]));
107
+ this.setMap = new Map(existingSets.map((s) => [s._id, s]));
63
108
  const existingMicrocyclesForMesocycle = existingMicrocycles
64
109
  .filter((m) => m.workoutMesocycleId === mesocycle._id)
65
110
  .sort((a, b) => a.startDate.getTime() - b.startDate.getTime());
@@ -86,6 +131,15 @@ export default class WorkoutMesocyclePlanContext {
86
131
  this.sessionExercisesToCreate.push(sessionExercise);
87
132
  this.sessionExerciseMap.set(sessionExercise._id, sessionExercise);
88
133
  }
134
+ /**
135
+ * Adds sets to the context and updates the set map for O(1) lookup.
136
+ */
137
+ addSets(sets) {
138
+ this.setsToCreate.push(...sets);
139
+ for (const set of sets) {
140
+ this.setMap.set(set._id, set);
141
+ }
142
+ }
89
143
  /**
90
144
  * Stores the planned session -> exercises structure for the mesocycle and derives
91
145
  * the muscle-group-wide ordering used for set progression.
@@ -1 +1 @@
1
- {"version":3,"file":"WorkoutMesocyclePlanContext.js","sourceRoot":"","sources":["../../../../src/services/workout/Mesocycle/WorkoutMesocyclePlanContext.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"WorkoutMesocyclePlanContext.js","sourceRoot":"","sources":["../../../../src/services/workout/Mesocycle/WorkoutMesocyclePlanContext.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,18 @@
1
1
  import type { UUID } from 'crypto';
2
2
  import type { WorkoutExerciseCTO } from '../../../ctos/workout/WorkoutExerciseCTO.js';
3
+ import type {
4
+ WorkoutMuscleGroupVolumeCTO,
5
+ WorkoutVolumeLandmarkEstimate
6
+ } from '../../../ctos/workout/WorkoutMuscleGroupVolumeCTO.js';
3
7
  import type { WorkoutEquipmentType } from '../../../documents/workout/WorkoutEquipmentType.js';
4
8
  import type { WorkoutExercise } from '../../../documents/workout/WorkoutExercise.js';
5
9
  import type { WorkoutMesocycle } from '../../../documents/workout/WorkoutMesocycle.js';
10
+ import { CycleType } from '../../../documents/workout/WorkoutMesocycle.js';
6
11
  import type { WorkoutMicrocycle } from '../../../documents/workout/WorkoutMicrocycle.js';
7
12
  import type { WorkoutSession } from '../../../documents/workout/WorkoutSession.js';
8
13
  import type { WorkoutSessionExercise } from '../../../documents/workout/WorkoutSessionExercise.js';
9
14
  import type { WorkoutSet } from '../../../documents/workout/WorkoutSet.js';
15
+ import WorkoutVolumePlanningService from '../util/VolumePlanning/WorkoutVolumePlanningService.js';
10
16
 
11
17
  /**
12
18
  * Central shared context for generating or updating a {@link WorkoutMesocycle}.
@@ -17,15 +23,34 @@ import type { WorkoutSet } from '../../../documents/workout/WorkoutSet.js';
17
23
  */
18
24
  export default class WorkoutMesocyclePlanContext {
19
25
  /**
20
- * A constant for now that defines what the target RIR is for the first microcycle of every mesocycle,
21
- * regardless of anything else.
26
+ * The target RIR for the first microcycle of the mesocycle. Varies by cycle
27
+ * type: MuscleGain starts at 4, Cut starts at 3, Resensitization stays at 3.
22
28
  */
23
- public readonly FIRST_MICROCYCLE_RIR = 4;
29
+ public readonly firstMicrocycleRir: number;
30
+
31
+ /**
32
+ * Number of microcycles between each baseline set addition.
33
+ * MuscleGain = 1 (every microcycle), Cut = 2 (every other), Resensitization = 0 (never).
34
+ */
35
+ public readonly progressionInterval: number;
36
+
37
+ /**
38
+ * Whether this mesocycle type skips the deload microcycle. True for
39
+ * Resensitization cycles.
40
+ */
41
+ public readonly skipDeload: boolean;
42
+
43
+ /**
44
+ * Number of accumulation (non-deload) microcycles in this mesocycle.
45
+ * Equal to the total planned count when deload is skipped, or total minus one otherwise.
46
+ */
47
+ public readonly accumulationMicrocycleCount: number;
24
48
 
25
49
  public readonly exerciseMap: Map<UUID, WorkoutExercise>;
26
50
  public readonly equipmentMap: Map<UUID, WorkoutEquipmentType>;
27
51
  public readonly sessionMap: Map<UUID, WorkoutSession>;
28
52
  public readonly sessionExerciseMap: Map<UUID, WorkoutSessionExercise>;
53
+ public readonly setMap: Map<UUID, WorkoutSet>;
29
54
 
30
55
  public readonly microcyclesToCreate: WorkoutMicrocycle[] = [];
31
56
  /**
@@ -52,6 +77,11 @@ export default class WorkoutMesocyclePlanContext {
52
77
  * ended up in.
53
78
  */
54
79
  public muscleGroupToExerciseCTOsMap: Map<UUID, WorkoutExerciseCTO[]> | undefined;
80
+ /**
81
+ * Maps muscle group ID to its estimated volume landmarks (MEV, MRV, MAV).
82
+ * Populated from WorkoutMuscleGroupVolumeCTOs when provided.
83
+ */
84
+ public muscleGroupToVolumeLandmarkMap: Map<UUID, WorkoutVolumeLandmarkEstimate>;
55
85
  public exerciseIdToSessionIndex: Map<UUID, number> | undefined;
56
86
 
57
87
  /**
@@ -60,11 +90,36 @@ export default class WorkoutMesocyclePlanContext {
60
90
  constructor(
61
91
  public mesocycle: WorkoutMesocycle,
62
92
  exerciseCTOs: WorkoutExerciseCTO[],
93
+ volumeCTOs: WorkoutMuscleGroupVolumeCTO[] = [],
63
94
  public existingMicrocycles: WorkoutMicrocycle[] = [],
64
95
  public existingSessions: WorkoutSession[] = [],
65
96
  public existingSessionExercises: WorkoutSessionExercise[] = [],
66
97
  public existingSets: WorkoutSet[] = []
67
98
  ) {
99
+ // Set cycle-type-specific planning parameters
100
+ const { cycleType } = mesocycle;
101
+ if (cycleType === CycleType.Cut) {
102
+ this.firstMicrocycleRir = 3;
103
+ this.progressionInterval = 2;
104
+ this.skipDeload = false;
105
+ } else if (cycleType === CycleType.Resensitization) {
106
+ this.firstMicrocycleRir = 3;
107
+ this.progressionInterval = 0;
108
+ this.skipDeload = true;
109
+ } else {
110
+ this.firstMicrocycleRir = 4;
111
+ this.progressionInterval = 1;
112
+ this.skipDeload = false;
113
+ }
114
+
115
+ const totalMicrocycles = mesocycle.plannedMicrocycleCount ?? 6;
116
+ this.accumulationMicrocycleCount = this.skipDeload ? totalMicrocycles : totalMicrocycles - 1;
117
+
118
+ // Build volume landmark estimates from historical CTOs
119
+ this.muscleGroupToVolumeLandmarkMap = new Map(
120
+ volumeCTOs.map((cto) => [cto._id, WorkoutVolumePlanningService.estimateVolumeLandmarks(cto)])
121
+ );
122
+
68
123
  // Derive exercise map from CTOs
69
124
  this.exerciseMap = new Map(exerciseCTOs.map((cto) => [cto._id, cto]));
70
125
 
@@ -75,6 +130,7 @@ export default class WorkoutMesocyclePlanContext {
75
130
 
76
131
  this.sessionMap = new Map(existingSessions.map((s) => [s._id, s]));
77
132
  this.sessionExerciseMap = new Map(existingSessionExercises.map((s) => [s._id, s]));
133
+ this.setMap = new Map(existingSets.map((s) => [s._id, s]));
78
134
 
79
135
  const existingMicrocyclesForMesocycle = existingMicrocycles
80
136
  .filter((m) => m.workoutMesocycleId === mesocycle._id)
@@ -106,6 +162,16 @@ export default class WorkoutMesocyclePlanContext {
106
162
  this.sessionExerciseMap.set(sessionExercise._id, sessionExercise);
107
163
  }
108
164
 
165
+ /**
166
+ * Adds sets to the context and updates the set map for O(1) lookup.
167
+ */
168
+ public addSets(sets: WorkoutSet[]): void {
169
+ this.setsToCreate.push(...sets);
170
+ for (const set of sets) {
171
+ this.setMap.set(set._id, set);
172
+ }
173
+ }
174
+
109
175
  /**
110
176
  * Stores the planned session -> exercises structure for the mesocycle and derives
111
177
  * the muscle-group-wide ordering used for set progression.
@@ -1,15 +1,29 @@
1
1
  import type { UUID } from 'crypto';
2
2
  import type { WorkoutExerciseCTO } from '../../../ctos/workout/WorkoutExerciseCTO.js';
3
+ import type { WorkoutMuscleGroupVolumeCTO } from '../../../ctos/workout/WorkoutMuscleGroupVolumeCTO.js';
3
4
  import { type WorkoutMesocycle } from '../../../documents/workout/WorkoutMesocycle.js';
4
5
  import type { WorkoutMicrocycle } from '../../../documents/workout/WorkoutMicrocycle.js';
5
6
  import type { WorkoutSession } from '../../../documents/workout/WorkoutSession.js';
6
7
  import type { WorkoutSessionExercise } from '../../../documents/workout/WorkoutSessionExercise.js';
7
8
  import type { WorkoutSet } from '../../../documents/workout/WorkoutSet.js';
8
9
  import type { DocumentOperations } from '../../DocumentService.js';
10
+ import type { WorkoutDeloadRecommendation } from './WorkoutMesocycleService.types.js';
9
11
  /**
10
12
  * A service for handling operations related to {@link WorkoutMesocycle}s.
11
13
  */
12
14
  export default class WorkoutMesocycleService {
15
+ /** Minimum microcycle index (0-based) before deload detection is active. */
16
+ private static readonly MIN_MICROCYCLE_INDEX_FOR_DELOAD;
17
+ /** Recovery ratio above which a deload is Recommended. */
18
+ private static readonly RECOVERY_RATIO_RECOMMENDED;
19
+ /** Recovery ratio at or above which a deload is Suggested. */
20
+ private static readonly RECOVERY_RATIO_SUGGESTED;
21
+ /** Set surplus at or below which a performance drop is counted. */
22
+ private static readonly PERFORMANCE_DROP_SURPLUS_THRESHOLD;
23
+ /** Number of consecutive performance drops needed to trigger the rule. */
24
+ private static readonly CONSECUTIVE_DROPS_REQUIRED;
25
+ /** Number of exercises with consecutive drops needed for Recommended severity. */
26
+ private static readonly EXERCISES_WITH_DROPS_FOR_RECOMMENDED;
13
27
  /**
14
28
  * Generates or updates the workout plan for a mesocycle.
15
29
  *
@@ -25,13 +39,14 @@ export default class WorkoutMesocycleService {
25
39
  *
26
40
  * @param mesocycle The mesocycle configuration.
27
41
  * @param exerciseCTOs The exercise CTOs containing exercise, calibration, equipment, and historical data.
42
+ * @param volumeCTOs Optional muscle group volume CTOs for historical volume landmark estimation.
28
43
  * @param existingMicrocycles Existing microcycle documents for this mesocycle.
29
44
  * @param existingSessions Existing session documents.
30
45
  * @param existingSessionExercises Existing session exercise documents.
31
46
  * @param existingSets Existing set documents.
32
47
  * @param startDate Optional start date for the first microcycle. Defaults to the current date when not provided.
33
48
  */
34
- static generateOrUpdateMesocycle(mesocycle: WorkoutMesocycle, exerciseCTOs: WorkoutExerciseCTO[], existingMicrocycles?: WorkoutMicrocycle[], existingSessions?: WorkoutSession[], existingSessionExercises?: WorkoutSessionExercise[], existingSets?: WorkoutSet[], startDate?: Date): {
49
+ static generateOrUpdateMesocycle(mesocycle: WorkoutMesocycle, exerciseCTOs: WorkoutExerciseCTO[], volumeCTOs?: WorkoutMuscleGroupVolumeCTO[], existingMicrocycles?: WorkoutMicrocycle[], existingSessions?: WorkoutSession[], existingSessionExercises?: WorkoutSessionExercise[], existingSets?: WorkoutSet[], startDate?: Date): {
35
50
  mesocycleUpdate?: Partial<WorkoutMesocycle>;
36
51
  microcycles?: DocumentOperations<WorkoutMicrocycle>;
37
52
  sessions?: DocumentOperations<WorkoutSession>;
@@ -85,6 +100,35 @@ export default class WorkoutMesocycleService {
85
100
  * @param mesocycleToMicrocyclesMap A map from mesocycle ID to its microcycles.
86
101
  */
87
102
  static getEarliestAllowedStartDate(existingMesocycles: WorkoutMesocycle[], mesocycleToMicrocyclesMap: Map<UUID, WorkoutMicrocycle[]>): Date;
103
+ /**
104
+ * Evaluates whether the mesocycle should trigger an early deload based on
105
+ * fatigue indicators from recent session data.
106
+ *
107
+ * Should be called after each session completion. Accepts the same document
108
+ * inputs as {@link generateOrUpdateMesocycle} plus a current microcycle ID,
109
+ * and uses {@link WorkoutMesocyclePlanContext} internally to derive all
110
+ * needed data.
111
+ */
112
+ static shouldTriggerEarlyDeload(mesocycle: WorkoutMesocycle, exerciseCTOs: WorkoutExerciseCTO[], currentMicrocycleId: UUID, existingMicrocycles: WorkoutMicrocycle[], existingSessions: WorkoutSession[], existingSessionExercises: WorkoutSessionExercise[], existingSets: WorkoutSet[]): WorkoutDeloadRecommendation;
113
+ /**
114
+ * Checks whether the ratio of muscle groups in recovery mode exceeds the
115
+ * deload thresholds. Derives the trained muscle group list and per-exercise
116
+ * muscle group mapping from the exercise map built by
117
+ * {@link WorkoutMesocyclePlanContext}.
118
+ *
119
+ * @param recentSessionExercises Session exercises from the last 2 microcycles.
120
+ * @param exerciseMap Map of exercise ID to exercise (from context).
121
+ */
122
+ private static evaluateRecoverySessionThreshold;
123
+ /**
124
+ * Checks whether exercises show consecutive performance drops (negative set
125
+ * surplus) across recent session exercises. Uses the context's set map for
126
+ * O(1) set lookups instead of building a local map.
127
+ *
128
+ * @param recentSessionExercises Session exercises from the last 2 microcycles.
129
+ * @param setMap Map of set ID to set (from context).
130
+ */
131
+ private static evaluateConsecutivePerformanceDrops;
88
132
  /**
89
133
  * Cleans up incomplete microcycles and their associated documents.
90
134
  *
@@ -1 +1 @@
1
- {"version":3,"file":"WorkoutMesocycleService.d.ts","sourceRoot":"","sources":["../../../../src/services/workout/Mesocycle/WorkoutMesocycleService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACnC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6CAA6C,CAAC;AACtF,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,gDAAgD,CAAC;AAClG,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iDAAiD,CAAC;AAEzF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8CAA8C,CAAC;AACnF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sDAAsD,CAAC;AACnG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,0CAA0C,CAAC;AAC3E,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAInE;;GAEG;AACH,MAAM,CAAC,OAAO,OAAO,uBAAuB;IAC1C;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,yBAAyB,CAC9B,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EAAE,kBAAkB,EAAE,EAClC,mBAAmB,GAAE,iBAAiB,EAAO,EAC7C,gBAAgB,GAAE,cAAc,EAAO,EACvC,wBAAwB,GAAE,sBAAsB,EAAO,EACvD,YAAY,GAAE,UAAU,EAAO,EAC/B,SAAS,CAAC,EAAE,IAAI,GACf;QACD,eAAe,CAAC,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAC5C,WAAW,CAAC,EAAE,kBAAkB,CAAC,iBAAiB,CAAC,CAAC;QACpD,QAAQ,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,CAAC;QAC9C,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,sBAAsB,CAAC,CAAC;QAC9D,IAAI,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAC;KACvC;IA+HD;;;;;;;OAOG;IACH,MAAM,CAAC,qBAAqB,CAC1B,SAAS,EAAE,gBAAgB,EAC3B,WAAW,EAAE,iBAAiB,EAAE,GAC/B,IAAI,GAAG,IAAI;IAQd;;;;;;;OAOG;IACH,MAAM,CAAC,mBAAmB,CACxB,SAAS,EAAE,gBAAgB,EAC3B,WAAW,EAAE,iBAAiB,EAAE,GAC/B,IAAI,GAAG,IAAI;IAiBd;;;;;;;;OAQG;IACH,MAAM,CAAC,mBAAmB,CACxB,SAAS,EAAE,gBAAgB,EAC3B,WAAW,EAAE,iBAAiB,EAAE,EAChC,QAAQ,EAAE,cAAc,EAAE,EAC1B,SAAS,EAAE,MAAM,GAChB,IAAI;IAeP;;;;;;OAMG;IACH,MAAM,CAAC,sBAAsB,CAC3B,UAAU,EAAE,gBAAgB,EAAE,EAC9B,yBAAyB,EAAE,GAAG,CAAC,IAAI,EAAE,iBAAiB,EAAE,CAAC,GACxD;QAAE,UAAU,EAAE,OAAO,CAAC;QAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAA;KAAE;IA8BjE;;;;;;OAMG;IACH,MAAM,CAAC,2BAA2B,CAChC,kBAAkB,EAAE,gBAAgB,EAAE,EACtC,yBAAyB,EAAE,GAAG,CAAC,IAAI,EAAE,iBAAiB,EAAE,CAAC,GACxD,IAAI;IAiBP;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,4BAA4B;CA4F5C"}
1
+ {"version":3,"file":"WorkoutMesocycleService.d.ts","sourceRoot":"","sources":["../../../../src/services/workout/Mesocycle/WorkoutMesocycleService.ts"],"names":[],"mappings":"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"}
@@ -2,11 +2,25 @@ import { DateService } from '@aneuhold/core-ts-lib';
2
2
  import { CycleType } from '../../../documents/workout/WorkoutMesocycle.js';
3
3
  import { WorkoutMicrocycleSchema } from '../../../documents/workout/WorkoutMicrocycle.js';
4
4
  import WorkoutMicrocycleService from '../Microcycle/WorkoutMicrocycleService.js';
5
+ import WorkoutSessionExerciseService from '../SessionExercise/WorkoutSessionExerciseService.js';
5
6
  import WorkoutMesocyclePlanContext from './WorkoutMesocyclePlanContext.js';
7
+ import { WorkoutDeloadSeverity, WorkoutDeloadTriggerRule } from './WorkoutMesocycleService.types.js';
6
8
  /**
7
9
  * A service for handling operations related to {@link WorkoutMesocycle}s.
8
10
  */
9
11
  export default class WorkoutMesocycleService {
12
+ /** Minimum microcycle index (0-based) before deload detection is active. */
13
+ static MIN_MICROCYCLE_INDEX_FOR_DELOAD = 2;
14
+ /** Recovery ratio above which a deload is Recommended. */
15
+ static RECOVERY_RATIO_RECOMMENDED = 0.5;
16
+ /** Recovery ratio at or above which a deload is Suggested. */
17
+ static RECOVERY_RATIO_SUGGESTED = 0.4;
18
+ /** Set surplus at or below which a performance drop is counted. */
19
+ static PERFORMANCE_DROP_SURPLUS_THRESHOLD = -3;
20
+ /** Number of consecutive performance drops needed to trigger the rule. */
21
+ static CONSECUTIVE_DROPS_REQUIRED = 2;
22
+ /** Number of exercises with consecutive drops needed for Recommended severity. */
23
+ static EXERCISES_WITH_DROPS_FOR_RECOMMENDED = 2;
10
24
  /**
11
25
  * Generates or updates the workout plan for a mesocycle.
12
26
  *
@@ -22,13 +36,14 @@ export default class WorkoutMesocycleService {
22
36
  *
23
37
  * @param mesocycle The mesocycle configuration.
24
38
  * @param exerciseCTOs The exercise CTOs containing exercise, calibration, equipment, and historical data.
39
+ * @param volumeCTOs Optional muscle group volume CTOs for historical volume landmark estimation.
25
40
  * @param existingMicrocycles Existing microcycle documents for this mesocycle.
26
41
  * @param existingSessions Existing session documents.
27
42
  * @param existingSessionExercises Existing session exercise documents.
28
43
  * @param existingSets Existing set documents.
29
44
  * @param startDate Optional start date for the first microcycle. Defaults to the current date when not provided.
30
45
  */
31
- static generateOrUpdateMesocycle(mesocycle, exerciseCTOs, existingMicrocycles = [], existingSessions = [], existingSessionExercises = [], existingSets = [], startDate) {
46
+ static generateOrUpdateMesocycle(mesocycle, exerciseCTOs, volumeCTOs = [], existingMicrocycles = [], existingSessions = [], existingSessionExercises = [], existingSets = [], startDate) {
32
47
  // Free-form mesocycles are intentionally not auto-planned. The user can still log workouts,
33
48
  // but we avoid generating microcycles/sessions/sets because recommendations wouldn't be able
34
49
  // to be done / make any sense.
@@ -38,8 +53,7 @@ export default class WorkoutMesocycleService {
38
53
  // Validate all exercises have calibrations before doing any work
39
54
  for (const cto of exerciseCTOs) {
40
55
  if (!cto.bestCalibration) {
41
- throw new Error(`Exercise "${cto.exerciseName}" (${cto._id}) has no bestCalibration. ` +
42
- `All exercises must be calibrated before planning a mesocycle.`);
56
+ throw new Error(`Exercise "${cto.exerciseName}" (${cto._id}) has no bestCalibration. All exercises must be calibrated before planning a mesocycle.`);
43
57
  }
44
58
  }
45
59
  // Clean up incomplete microcycles before creating context
@@ -50,13 +64,13 @@ export default class WorkoutMesocycleService {
50
64
  const cleanSessionExercises = existingSessionExercises.filter((se) => !cleanupResult.sessionExercisesToDelete.includes(se._id));
51
65
  const cleanSets = existingSets.filter((s) => !cleanupResult.setsToDelete.includes(s._id));
52
66
  // Create planning context with clean data
53
- const context = new WorkoutMesocyclePlanContext(mesocycle, exerciseCTOs, cleanMicrocycles, cleanSessions, cleanSessionExercises, cleanSets);
67
+ const context = new WorkoutMesocyclePlanContext(mesocycle, exerciseCTOs, volumeCTOs, cleanMicrocycles, cleanSessions, cleanSessionExercises, cleanSets);
54
68
  // Distribute exercises across sessions once for the entire mesocycle plan.
55
69
  // This session layout is expected to be stable across microcycles.
56
70
  context.setPlannedSessionExerciseCTOs(WorkoutMicrocycleService.distributeExercisesAcrossSessions(mesocycle.plannedSessionCountPerMicrocycle, exerciseCTOs));
57
71
  // Determine number of microcycles (default to 6 if not specified: 5 accumulation + 1 deload)
58
72
  const totalMicrocycles = mesocycle.plannedMicrocycleCount ?? 6;
59
- const deloadMicrocycleIndex = totalMicrocycles - 1;
73
+ const deloadMicrocycleIndex = context.skipDeload ? -1 : totalMicrocycles - 1;
60
74
  // Determine starting point for generation
61
75
  const startMicrocycleIndex = context.microcyclesInOrder.length;
62
76
  let currentDate;
@@ -68,12 +82,17 @@ export default class WorkoutMesocycleService {
68
82
  const lastExistingMicrocycle = context.microcyclesInOrder[startMicrocycleIndex - 1];
69
83
  currentDate = lastExistingMicrocycle.endDate;
70
84
  }
85
+ const { firstMicrocycleRir } = context;
71
86
  // Generate remaining microcycles
72
87
  for (let microcycleIndex = startMicrocycleIndex; microcycleIndex < totalMicrocycles; microcycleIndex++) {
73
88
  const isDeloadMicrocycle = microcycleIndex === deloadMicrocycleIndex;
74
- // Calculate RIR for this microcycle (4 -> 3 -> 2 -> 1 -> 0, capped at microcycle 5)
75
- const rirForMicrocycle = Math.min(microcycleIndex, 4);
76
- const targetRir = isDeloadMicrocycle ? null : 4 - rirForMicrocycle;
89
+ // Calculate RIR for this microcycle. Uses the cycle-type-specific starting RIR
90
+ // and tapers down by 1 per microcycle: e.g. MuscleGain 4->3->2->1->0, Cut 3->2->1->0->0.
91
+ // If progression is still, keep RIR flat
92
+ const rirForMicrocycle = context.progressionInterval === 0
93
+ ? firstMicrocycleRir
94
+ : Math.max(firstMicrocycleRir - microcycleIndex, 0);
95
+ const targetRir = isDeloadMicrocycle ? null : rirForMicrocycle;
77
96
  // Create microcycle
78
97
  const microcycle = WorkoutMicrocycleSchema.parse({
79
98
  userId: mesocycle.userId,
@@ -221,6 +240,179 @@ export default class WorkoutMesocycleService {
221
240
  }
222
241
  return latestEnd;
223
242
  }
243
+ /**
244
+ * Evaluates whether the mesocycle should trigger an early deload based on
245
+ * fatigue indicators from recent session data.
246
+ *
247
+ * Should be called after each session completion. Accepts the same document
248
+ * inputs as {@link generateOrUpdateMesocycle} plus a current microcycle ID,
249
+ * and uses {@link WorkoutMesocyclePlanContext} internally to derive all
250
+ * needed data.
251
+ */
252
+ static shouldTriggerEarlyDeload(mesocycle, exerciseCTOs, currentMicrocycleId, existingMicrocycles, existingSessions, existingSessionExercises, existingSets) {
253
+ const noDeload = {
254
+ shouldDeload: false,
255
+ severity: WorkoutDeloadSeverity.None,
256
+ triggeredRules: []
257
+ };
258
+ const context = new WorkoutMesocyclePlanContext(mesocycle, exerciseCTOs, [], existingMicrocycles, existingSessions, existingSessionExercises, existingSets);
259
+ // Find current microcycle index
260
+ const currentMicrocycleIndex = context.microcyclesInOrder.findIndex((mc) => mc._id === currentMicrocycleId);
261
+ // Guard: don't trigger before enough microcycles have been completed
262
+ if (currentMicrocycleIndex < this.MIN_MICROCYCLE_INDEX_FOR_DELOAD) {
263
+ return noDeload;
264
+ }
265
+ // Guard: don't suggest a deload if already on the deload microcycle
266
+ const isLastMicrocycle = currentMicrocycleIndex === context.microcyclesInOrder.length - 1;
267
+ if (isLastMicrocycle && !context.skipDeload) {
268
+ return noDeload;
269
+ }
270
+ // Gather recent session exercises from the last 2 microcycles
271
+ const startIndex = Math.max(0, currentMicrocycleIndex - 1);
272
+ const recentMicrocycles = context.microcyclesInOrder.slice(startIndex, currentMicrocycleIndex + 1);
273
+ const recentMicrocycleIds = new Set(recentMicrocycles.map((mc) => mc._id));
274
+ const recentSessionExercises = [...context.sessionExerciseMap.values()].filter((se) => {
275
+ const session = context.sessionMap.get(se.workoutSessionId);
276
+ return session?.workoutMicrocycleId && recentMicrocycleIds.has(session.workoutMicrocycleId);
277
+ });
278
+ const triggeredRules = [];
279
+ let recoverySeverity = WorkoutDeloadSeverity.None;
280
+ let performanceSeverity = WorkoutDeloadSeverity.None;
281
+ // --- Rule 1: Recovery Session Threshold ---
282
+ const recoveryResult = this.evaluateRecoverySessionThreshold(recentSessionExercises, context.exerciseMap);
283
+ if (recoveryResult !== WorkoutDeloadSeverity.None) {
284
+ triggeredRules.push(WorkoutDeloadTriggerRule.RecoverySessionThreshold);
285
+ recoverySeverity = recoveryResult;
286
+ }
287
+ // --- Rule 2: Consecutive Performance Drops ---
288
+ const performanceResult = this.evaluateConsecutivePerformanceDrops(recentSessionExercises, context.setMap);
289
+ if (performanceResult !== WorkoutDeloadSeverity.None) {
290
+ triggeredRules.push(WorkoutDeloadTriggerRule.ConsecutivePerformanceDrop);
291
+ performanceSeverity = performanceResult;
292
+ }
293
+ if (triggeredRules.length === 0) {
294
+ return noDeload;
295
+ }
296
+ // Determine combined severity
297
+ let severity;
298
+ if (triggeredRules.length >= 2) {
299
+ severity = WorkoutDeloadSeverity.Urgent;
300
+ }
301
+ else {
302
+ severity =
303
+ recoverySeverity !== WorkoutDeloadSeverity.None ? recoverySeverity : performanceSeverity;
304
+ }
305
+ return {
306
+ shouldDeload: true,
307
+ severity,
308
+ triggeredRules
309
+ };
310
+ }
311
+ /**
312
+ * Checks whether the ratio of muscle groups in recovery mode exceeds the
313
+ * deload thresholds. Derives the trained muscle group list and per-exercise
314
+ * muscle group mapping from the exercise map built by
315
+ * {@link WorkoutMesocyclePlanContext}.
316
+ *
317
+ * @param recentSessionExercises Session exercises from the last 2 microcycles.
318
+ * @param exerciseMap Map of exercise ID to exercise (from context).
319
+ */
320
+ static evaluateRecoverySessionThreshold(recentSessionExercises, exerciseMap) {
321
+ // Derive unique trained muscle groups from all exercises in the plan
322
+ const trainedMuscleGroupIds = new Set();
323
+ for (const exercise of exerciseMap.values()) {
324
+ for (const mgId of exercise.primaryMuscleGroups) {
325
+ trainedMuscleGroupIds.add(mgId);
326
+ }
327
+ }
328
+ if (trainedMuscleGroupIds.size === 0) {
329
+ return WorkoutDeloadSeverity.None;
330
+ }
331
+ const recoveryMuscleGroups = new Set();
332
+ for (const sessionExercise of recentSessionExercises) {
333
+ if (sessionExercise.isRecoveryExercise) {
334
+ const exercise = exerciseMap.get(sessionExercise.workoutExerciseId);
335
+ if (exercise) {
336
+ for (const mgId of exercise.primaryMuscleGroups) {
337
+ recoveryMuscleGroups.add(mgId);
338
+ }
339
+ }
340
+ }
341
+ }
342
+ const ratio = recoveryMuscleGroups.size / trainedMuscleGroupIds.size;
343
+ if (ratio > this.RECOVERY_RATIO_RECOMMENDED) {
344
+ return WorkoutDeloadSeverity.Recommended;
345
+ }
346
+ if (ratio >= this.RECOVERY_RATIO_SUGGESTED) {
347
+ return WorkoutDeloadSeverity.Suggested;
348
+ }
349
+ return WorkoutDeloadSeverity.None;
350
+ }
351
+ /**
352
+ * Checks whether exercises show consecutive performance drops (negative set
353
+ * surplus) across recent session exercises. Uses the context's set map for
354
+ * O(1) set lookups instead of building a local map.
355
+ *
356
+ * @param recentSessionExercises Session exercises from the last 2 microcycles.
357
+ * @param setMap Map of set ID to set (from context).
358
+ */
359
+ static evaluateConsecutivePerformanceDrops(recentSessionExercises, setMap) {
360
+ // Group session exercises by exercise ID
361
+ const exerciseGroups = new Map();
362
+ for (const se of recentSessionExercises) {
363
+ const existing = exerciseGroups.get(se.workoutExerciseId);
364
+ if (existing) {
365
+ existing.push(se);
366
+ }
367
+ else {
368
+ exerciseGroups.set(se.workoutExerciseId, [se]);
369
+ }
370
+ }
371
+ let exercisesWithDropsCount = 0;
372
+ for (const [, sessionExercises] of exerciseGroups) {
373
+ // Sort by session exercise creation date (ascending)
374
+ const sorted = [...sessionExercises].sort((a, b) => a.createdDate.getTime() - b.createdDate.getTime());
375
+ let consecutiveDrops = 0;
376
+ let hasConsecutiveDrops = false;
377
+ for (const se of sorted) {
378
+ if (se.setOrder.length === 0) {
379
+ consecutiveDrops = 0;
380
+ continue;
381
+ }
382
+ const firstSetId = se.setOrder[0];
383
+ const firstSet = setMap.get(firstSetId);
384
+ if (!firstSet ||
385
+ firstSet.actualReps == null ||
386
+ firstSet.plannedReps == null ||
387
+ firstSet.rir == null ||
388
+ firstSet.plannedRir == null) {
389
+ consecutiveDrops = 0;
390
+ continue;
391
+ }
392
+ const surplus = WorkoutSessionExerciseService.calculateSetSurplus(firstSet.actualReps, firstSet.plannedReps, firstSet.rir, firstSet.plannedRir);
393
+ if (surplus <= this.PERFORMANCE_DROP_SURPLUS_THRESHOLD) {
394
+ consecutiveDrops++;
395
+ }
396
+ else {
397
+ consecutiveDrops = 0;
398
+ }
399
+ if (consecutiveDrops >= this.CONSECUTIVE_DROPS_REQUIRED) {
400
+ hasConsecutiveDrops = true;
401
+ break;
402
+ }
403
+ }
404
+ if (hasConsecutiveDrops) {
405
+ exercisesWithDropsCount++;
406
+ }
407
+ }
408
+ if (exercisesWithDropsCount === 0) {
409
+ return WorkoutDeloadSeverity.None;
410
+ }
411
+ if (exercisesWithDropsCount >= this.EXERCISES_WITH_DROPS_FOR_RECOMMENDED) {
412
+ return WorkoutDeloadSeverity.Recommended;
413
+ }
414
+ return WorkoutDeloadSeverity.Suggested;
415
+ }
224
416
  /**
225
417
  * Cleans up incomplete microcycles and their associated documents.
226
418
  *
@@ -265,9 +457,7 @@ export default class WorkoutMesocycleService {
265
457
  const firstSessionId = firstIncompleteMicrocycle.sessionOrder[0];
266
458
  const firstSession = existingSessions.find((s) => s._id === firstSessionId);
267
459
  if (firstSession?.complete) {
268
- throw new Error(`Cannot generate new microcycles for mesocycle ${mesocycle._id}: ` +
269
- `Microcycle at index ${firstIncompleteMicrocycleIndex} has started but is not complete. ` +
270
- `All sessions in the current microcycle must be completed before generating new microcycles.`);
460
+ throw new Error(`Cannot generate new microcycles for mesocycle ${mesocycle._id}: Microcycle at index ${firstIncompleteMicrocycleIndex} has started but is not complete. All sessions in the current microcycle must be completed before generating new microcycles.`);
271
461
  }
272
462
  }
273
463
  // Collect IDs of incomplete microcycles (from firstIncompleteMicrocycleIndex onward)