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

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 (57) hide show
  1. package/CHANGELOG.md +21 -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/WorkoutSet.d.ts +8 -0
  11. package/lib/documents/workout/WorkoutSet.d.ts.map +1 -1
  12. package/lib/documents/workout/WorkoutSet.ts +9 -0
  13. package/lib/services/workout/Exercise/WorkoutExerciseService.d.ts +40 -0
  14. package/lib/services/workout/Exercise/WorkoutExerciseService.d.ts.map +1 -1
  15. package/lib/services/workout/Exercise/WorkoutExerciseService.js +134 -1
  16. package/lib/services/workout/Exercise/WorkoutExerciseService.js.map +1 -1
  17. package/lib/services/workout/Exercise/WorkoutExerciseService.ts +204 -1
  18. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.d.ts +15 -0
  19. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.d.ts.map +1 -1
  20. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.js +18 -1
  21. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.js.map +1 -1
  22. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.ts +19 -1
  23. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.d.ts +26 -4
  24. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.d.ts.map +1 -1
  25. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.js +51 -4
  26. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.js.map +1 -1
  27. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.ts +58 -3
  28. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.d.ts +45 -1
  29. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.d.ts.map +1 -1
  30. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.js +201 -11
  31. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.js.map +1 -1
  32. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.ts +285 -9
  33. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.d.ts +33 -0
  34. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.d.ts.map +1 -0
  35. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.js +24 -0
  36. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.js.map +1 -0
  37. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.types.ts +36 -0
  38. package/lib/services/workout/Session/WorkoutSessionService.d.ts.map +1 -1
  39. package/lib/services/workout/Session/WorkoutSessionService.js +1 -11
  40. package/lib/services/workout/Session/WorkoutSessionService.js.map +1 -1
  41. package/lib/services/workout/Session/WorkoutSessionService.ts +1 -17
  42. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.d.ts +14 -2
  43. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.d.ts.map +1 -1
  44. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.js +17 -3
  45. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.js.map +1 -1
  46. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.ts +28 -3
  47. package/lib/services/workout/Set/WorkoutSetService.d.ts +17 -5
  48. package/lib/services/workout/Set/WorkoutSetService.d.ts.map +1 -1
  49. package/lib/services/workout/Set/WorkoutSetService.js +83 -16
  50. package/lib/services/workout/Set/WorkoutSetService.js.map +1 -1
  51. package/lib/services/workout/Set/WorkoutSetService.ts +107 -24
  52. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.d.ts +72 -2
  53. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.d.ts.map +1 -1
  54. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.js +202 -15
  55. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.js.map +1 -1
  56. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.ts +268 -18
  57. package/package.json +1 -1
@@ -1,8 +1,13 @@
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 { WorkoutSessionExercise } from '../../../../documents/workout/WorkoutSessionExercise.js';
4
8
  import type WorkoutMesocyclePlanContext from '../../Mesocycle/WorkoutMesocyclePlanContext.js';
5
9
  import WorkoutSessionExerciseService from '../../SessionExercise/WorkoutSessionExerciseService.js';
10
+ import WorkoutSFRService from '../SFR/WorkoutSFRService.js';
6
11
 
7
12
  /**
8
13
  * A service for handling volume planning operations across microcycles.
@@ -24,6 +29,39 @@ export default class WorkoutVolumePlanningService {
24
29
  private static readonly MAX_SETS_PER_EXERCISE = 8;
25
30
  private static readonly MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION = 10;
26
31
 
32
+ /** Minimum average RSM required for a mesocycle to count toward MEV estimation. */
33
+ private static readonly MEV_RSM_THRESHOLD = 4;
34
+
35
+ /** Default estimated MEV when no qualifying mesocycle history exists. */
36
+ private static readonly DEFAULT_MEV = 2;
37
+
38
+ /** Minimum average performance score (or recovery presence) to count a mesocycle toward MRV estimation. */
39
+ private static readonly MRV_PERFORMANCE_THRESHOLD = 2.5;
40
+
41
+ /** Extra sets added above the historical peak when no stressed mesocycles exist, to estimate MRV. */
42
+ private static readonly MRV_HEADROOM = 2;
43
+
44
+ /** Default estimated MRV when no mesocycle history exists at all. */
45
+ private static readonly DEFAULT_MRV = 8;
46
+
47
+ /** RSM bracket upper bound for "below MEV" proximity (0 to this value inclusive). */
48
+ private static readonly MEV_PROXIMITY_BELOW_THRESHOLD = 3;
49
+
50
+ /** RSM bracket upper bound for "at MEV" proximity (above BELOW threshold up to this value inclusive). */
51
+ private static readonly MEV_PROXIMITY_AT_THRESHOLD = 6;
52
+
53
+ /** Recommended set adjustment when volume is below MEV. */
54
+ private static readonly MEV_BELOW_SET_ADJUSTMENT = 3;
55
+
56
+ /** Recommended set adjustment when volume is above MEV. */
57
+ private static readonly MEV_ABOVE_SET_ADJUSTMENT = -2;
58
+
59
+ /** Maximum sets that can be added to a single exercise in one progression step. */
60
+ private static readonly MAX_SET_ADDITION_PER_EXERCISE = 2;
61
+
62
+ /** Maximum total sets to distribute across a muscle group in one progression step. */
63
+ private static readonly MAX_TOTAL_SET_ADDITIONS = 3;
64
+
27
65
  /**
28
66
  * Calculates the set plan for an entire microcycle.
29
67
  */
@@ -56,9 +94,185 @@ export default class WorkoutVolumePlanningService {
56
94
  }
57
95
  });
58
96
 
97
+ // Apply MEV proximity adjustment when generating the second microcycle (index 1)
98
+ // after the first microcycle is complete with RSM data. Only applies when volume
99
+ // data (volumeCTOs) was provided to the context.
100
+ if (
101
+ microcycleIndex === 1 &&
102
+ !isDeloadMicrocycle &&
103
+ context.muscleGroupToVolumeLandmarkMap.size > 0
104
+ ) {
105
+ this.applyMevProximityAdjustments(context, exerciseIdToSetCount);
106
+ }
107
+
59
108
  return { exerciseIdToSetCount, recoveryExerciseIds };
60
109
  }
61
110
 
111
+ /**
112
+ * Estimates MEV, MRV, and MAV for a muscle group based on historical data
113
+ * across completed mesocycles.
114
+ *
115
+ * @param volumeCTO The WorkoutMuscleGroupVolumeCTO containing mesocycle
116
+ * history for this muscle group.
117
+ */
118
+ static estimateVolumeLandmarks(
119
+ volumeCTO: WorkoutMuscleGroupVolumeCTO
120
+ ): WorkoutVolumeLandmarkEstimate {
121
+ const { mesocycleHistory } = volumeCTO;
122
+
123
+ // Estimated MEV
124
+ let estimatedMev: number;
125
+ const effectiveMesocycles = mesocycleHistory.filter(
126
+ (m) => m.avgRsm !== null && m.avgRsm >= this.MEV_RSM_THRESHOLD
127
+ );
128
+ if (effectiveMesocycles.length > 0) {
129
+ estimatedMev =
130
+ effectiveMesocycles.reduce((sum, m) => sum + m.startingSetCount, 0) /
131
+ effectiveMesocycles.length;
132
+ estimatedMev = Math.round(estimatedMev);
133
+ } else if (mesocycleHistory.length > 0) {
134
+ estimatedMev = Math.min(...mesocycleHistory.map((m) => m.startingSetCount));
135
+ } else {
136
+ estimatedMev = this.DEFAULT_MEV;
137
+ }
138
+
139
+ // Estimated MRV
140
+ let estimatedMrv: number;
141
+ const stressedMesocycles = mesocycleHistory.filter(
142
+ (m) =>
143
+ (m.avgPerformanceScore !== null &&
144
+ m.avgPerformanceScore >= this.MRV_PERFORMANCE_THRESHOLD) ||
145
+ m.recoverySessionCount > 0
146
+ );
147
+ if (stressedMesocycles.length > 0) {
148
+ estimatedMrv =
149
+ stressedMesocycles.reduce((sum, m) => sum + m.peakSetCount, 0) / stressedMesocycles.length;
150
+ estimatedMrv = Math.round(estimatedMrv);
151
+ } else if (mesocycleHistory.length > 0) {
152
+ estimatedMrv = Math.max(...mesocycleHistory.map((m) => m.peakSetCount)) + this.MRV_HEADROOM;
153
+ } else {
154
+ estimatedMrv = this.DEFAULT_MRV;
155
+ }
156
+
157
+ // Hard cap MRV at 10 (MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION)
158
+ estimatedMrv = Math.min(estimatedMrv, this.MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION);
159
+
160
+ // Ensure MRV > MEV
161
+ if (estimatedMrv <= estimatedMev) {
162
+ estimatedMrv = estimatedMev + 1;
163
+ }
164
+
165
+ const estimatedMav = Math.ceil((estimatedMev + estimatedMrv) / 2);
166
+
167
+ return { estimatedMev, estimatedMrv, estimatedMav, mesocycleCount: mesocycleHistory.length };
168
+ }
169
+
170
+ /**
171
+ * Evaluates MEV (Minimum Effective Volume) proximity for a muscle group based on
172
+ * RSM scores from the first microcycle. Called when generating the second microcycle to adjust
173
+ * the volume baseline.
174
+ *
175
+ * Returns `null` when the first microcycle is incomplete or has no RSM data for the muscle group.
176
+ *
177
+ * @param context The mesocycle planning context containing microcycle/session/exercise data.
178
+ * @param muscleGroupId The muscle group to evaluate.
179
+ */
180
+ static evaluateMevProximity(
181
+ context: WorkoutMesocyclePlanContext,
182
+ muscleGroupId: UUID
183
+ ): {
184
+ /** 'below' = RSM 0-3, 'at' = RSM 4-6, 'above' = RSM 7-9 */
185
+ proximity: 'below' | 'at' | 'above';
186
+
187
+ /**
188
+ * Recommended total set adjustment for this muscle group.
189
+ * Positive = add sets, negative = remove sets, 0 = no change.
190
+ * Range: -2 to +3
191
+ */
192
+ recommendedSetAdjustment: number;
193
+
194
+ /** The average RSM across session exercises targeting this muscle group. */
195
+ averageRsm: number;
196
+ } | null {
197
+ const firstMicrocycle = context.microcyclesInOrder[0];
198
+ if (!firstMicrocycle) return null;
199
+
200
+ // Check the first microcycle is complete
201
+ const lastSessionId = firstMicrocycle.sessionOrder[firstMicrocycle.sessionOrder.length - 1];
202
+ if (!context.sessionMap.get(lastSessionId)?.complete) return null;
203
+
204
+ // Collect RSM totals from session exercises that target this muscle group
205
+ const rsmTotals: number[] = [];
206
+ for (const sessionId of firstMicrocycle.sessionOrder) {
207
+ const session = context.sessionMap.get(sessionId);
208
+ if (!session) continue;
209
+ for (const seId of session.sessionExerciseOrder) {
210
+ const se = context.sessionExerciseMap.get(seId);
211
+ if (!se) continue;
212
+ const exercise = context.exerciseMap.get(se.workoutExerciseId);
213
+ if (!exercise?.primaryMuscleGroups.includes(muscleGroupId)) continue;
214
+
215
+ const rsmTotal = WorkoutSFRService.getRsmTotal(se.rsm);
216
+ if (rsmTotal !== null) {
217
+ rsmTotals.push(rsmTotal);
218
+ }
219
+ }
220
+ }
221
+
222
+ if (rsmTotals.length === 0) return null;
223
+
224
+ const averageRsm = rsmTotals.reduce((sum, val) => sum + val, 0) / rsmTotals.length;
225
+ const bracket = Math.floor(averageRsm);
226
+
227
+ if (bracket <= this.MEV_PROXIMITY_BELOW_THRESHOLD) {
228
+ return {
229
+ proximity: 'below',
230
+ recommendedSetAdjustment: this.MEV_BELOW_SET_ADJUSTMENT,
231
+ averageRsm
232
+ };
233
+ } else if (bracket <= this.MEV_PROXIMITY_AT_THRESHOLD) {
234
+ return { proximity: 'at', recommendedSetAdjustment: 0, averageRsm };
235
+ }
236
+ return {
237
+ proximity: 'above',
238
+ recommendedSetAdjustment: this.MEV_ABOVE_SET_ADJUSTMENT,
239
+ averageRsm
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Applies MEV proximity adjustments based on RSM data from the first microcycle.
245
+ * Adjusts set counts per muscle group when the first microcycle indicates volume
246
+ * was below or above MEV.
247
+ */
248
+ private static applyMevProximityAdjustments(
249
+ context: WorkoutMesocyclePlanContext,
250
+ exerciseIdToSetCount: Map<UUID, number>
251
+ ): void {
252
+ if (!context.muscleGroupToExerciseCTOsMap) return;
253
+
254
+ for (const [muscleGroupId, muscleGroupExerciseCTOs] of context.muscleGroupToExerciseCTOsMap) {
255
+ const mevResult = this.evaluateMevProximity(context, muscleGroupId);
256
+ if (!mevResult || mevResult.recommendedSetAdjustment === 0) continue;
257
+
258
+ // Distribute the adjustment evenly across exercises in this muscle group
259
+ const exerciseCount = muscleGroupExerciseCTOs.length;
260
+ const adjustmentPerExercise = Math.floor(mevResult.recommendedSetAdjustment / exerciseCount);
261
+ const adjustmentRemainder = mevResult.recommendedSetAdjustment % exerciseCount;
262
+
263
+ muscleGroupExerciseCTOs.forEach((cto, index) => {
264
+ const currentSets = exerciseIdToSetCount.get(cto._id) ?? 2;
265
+ const extra =
266
+ index < Math.abs(adjustmentRemainder) ? Math.sign(mevResult.recommendedSetAdjustment) : 0;
267
+ const newSets = Math.max(
268
+ 1,
269
+ Math.min(currentSets + adjustmentPerExercise + extra, this.MAX_SETS_PER_EXERCISE)
270
+ );
271
+ exerciseIdToSetCount.set(cto._id, newSets);
272
+ });
273
+ }
274
+ }
275
+
62
276
  /**
63
277
  * Calculates the set count for each exercise in a particular muscle group for this microcycle.
64
278
  *
@@ -75,13 +289,16 @@ export default class WorkoutVolumePlanningService {
75
289
  const recoveryExerciseIds = new Set<UUID>();
76
290
  const sessionIndexToExerciseIds = new Map<number, UUID[]>();
77
291
 
292
+ const { progressionInterval } = context;
293
+
78
294
  // 1. Calculate baselines for all exercises in muscle group
79
295
  muscleGroupExerciseCTOs.forEach((cto, index) => {
80
296
  const baseline = this.calculateBaselineSetCount(
81
297
  microcycleIndex,
82
298
  muscleGroupExerciseCTOs.length,
83
299
  index,
84
- isDeloadMicrocycle
300
+ isDeloadMicrocycle,
301
+ progressionInterval
85
302
  );
86
303
  exerciseIdToSetCount.set(cto._id, Math.min(baseline, this.MAX_SETS_PER_EXERCISE));
87
304
 
@@ -150,13 +367,26 @@ export default class WorkoutVolumePlanningService {
150
367
  // For deload microcycles, halve the historical count (minimum 1 set). We don't use baseline
151
368
  // because the user may have adjusted over the mesocycle in a way that the baseline is actually
152
369
  // a higher set count than what would be calculated by halving the previous microcycle's sets.
370
+ //
371
+ // For exercises returning from recovery, use the estimated MAV from volume landmarks
372
+ // when available, instead of the pre-recovery historical count.
373
+ const primaryMuscleGroupId = muscleGroupExerciseCTOs[0]?.primaryMuscleGroups[0];
374
+ const volumeLandmark = primaryMuscleGroupId
375
+ ? context.muscleGroupToVolumeLandmarkMap.get(primaryMuscleGroupId)
376
+ : undefined;
377
+
153
378
  muscleGroupExerciseCTOs.forEach((cto) => {
154
379
  const previousSessionExercise = exerciseIdToPrevSessionExercise.get(cto._id);
155
380
  if (previousSessionExercise) {
156
- let setCount = Math.min(
157
- previousSessionExercise.setOrder.length,
158
- this.MAX_SETS_PER_EXERCISE
159
- );
381
+ let setCount: number;
382
+
383
+ if (exercisesThatWerePreviouslyInRecovery.has(cto._id) && volumeLandmark) {
384
+ // Exercise is returning from recovery — resume at the estimated MAV
385
+ setCount = Math.min(volumeLandmark.estimatedMav, this.MAX_SETS_PER_EXERCISE);
386
+ } else {
387
+ setCount = Math.min(previousSessionExercise.setOrder.length, this.MAX_SETS_PER_EXERCISE);
388
+ }
389
+
160
390
  if (isDeloadMicrocycle) {
161
391
  setCount = Math.max(1, Math.floor(setCount / 2));
162
392
  }
@@ -164,8 +394,8 @@ export default class WorkoutVolumePlanningService {
164
394
  }
165
395
  });
166
396
 
167
- // Deload microcycles use reduced volume skip SFR-based set additions
168
- if (isDeloadMicrocycle) {
397
+ // Deload microcycles and zero-progression cycles (Resensitization) skip SFR-based set additions
398
+ if (isDeloadMicrocycle || progressionInterval === 0) {
169
399
  return { exerciseIdToSetCount, recoveryExerciseIds };
170
400
  }
171
401
 
@@ -225,7 +455,7 @@ export default class WorkoutVolumePlanningService {
225
455
  const previousSetCount = previousSessionExercise.setOrder.length;
226
456
  const recoverySets = Math.max(1, Math.floor(previousSetCount / 2));
227
457
  exerciseIdToSetCount.set(cto._id, recoverySets);
228
- } else if (recommendation != null && recommendation >= 0) {
458
+ } else if (recommendation !== null && recommendation >= 0) {
229
459
  totalSetsToAdd += recommendation;
230
460
 
231
461
  // Consider as candidate if session is not already capped
@@ -289,8 +519,12 @@ export default class WorkoutVolumePlanningService {
289
519
  WorkoutVolumePlanningService.MAX_SETS_PER_EXERCISE - currentSets;
290
520
  const maxDueToSessionLimit =
291
521
  WorkoutVolumePlanningService.MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION - sessionTotal;
292
- // Hard limit of 2 to add to a particular exercise at once
293
- const maxAddable = Math.min(setsToAdd, maxDueToExerciseLimit, maxDueToSessionLimit, 2);
522
+ const maxAddable = Math.min(
523
+ setsToAdd,
524
+ maxDueToExerciseLimit,
525
+ maxDueToSessionLimit,
526
+ WorkoutVolumePlanningService.MAX_SET_ADDITION_PER_EXERCISE
527
+ );
294
528
 
295
529
  if (maxAddable > 0) {
296
530
  exerciseIdToSetCount.set(exerciseId, currentSets + maxAddable);
@@ -298,8 +532,10 @@ export default class WorkoutVolumePlanningService {
298
532
  return maxAddable;
299
533
  }
300
534
 
301
- // Cap the actual sets to add to 3 total
302
- let setsRemaining = totalSetsToAdd >= 3 ? 3 : totalSetsToAdd;
535
+ let setsRemaining =
536
+ totalSetsToAdd >= WorkoutVolumePlanningService.MAX_TOTAL_SET_ADDITIONS
537
+ ? WorkoutVolumePlanningService.MAX_TOTAL_SET_ADDITIONS
538
+ : totalSetsToAdd;
303
539
  for (const candidate of candidates) {
304
540
  const added = addSetsToExercise(candidate.exerciseId, setsRemaining);
305
541
  setsRemaining -= added;
@@ -316,14 +552,26 @@ export default class WorkoutVolumePlanningService {
316
552
  * for the entire microcycle, regardless of which session those exercises are in.
317
553
  *
318
554
  * Baseline: 2 sets per exercise in the muscle group.
319
- * Progression: add 1 total set per microcycle per muscle group (distributed to earlier exercises
320
- * in the muscle-group-wide ordering).
555
+ * Progression: add 1 set per muscle group every `progressionInterval` microcycles.
556
+ * - MuscleGain (interval 1): every microcycle
557
+ * - Cut (interval 2): every other microcycle
558
+ * - Resensitization (interval 0): no progression (flat 2 sets per exercise)
559
+ *
560
+ * @param microcycleIndex The index of the current microcycle.
561
+ * @param totalExercisesInMuscleGroupForMicrocycle Total exercises in the muscle group for
562
+ * this microcycle.
563
+ * @param exerciseIndexInMuscleGroupForMicrocycle Index of the current exercise within the
564
+ * muscle group.
565
+ * @param isDeloadMicrocycle Whether this is a deload microcycle.
566
+ * @param progressionInterval Number of microcycles between each set addition. 0 means no
567
+ * progression.
321
568
  */
322
569
  private static calculateBaselineSetCount(
323
570
  microcycleIndex: number,
324
571
  totalExercisesInMuscleGroupForMicrocycle: number,
325
572
  exerciseIndexInMuscleGroupForMicrocycle: number,
326
- isDeloadMicrocycle: boolean
573
+ isDeloadMicrocycle: boolean,
574
+ progressionInterval: number = 1
327
575
  ): number {
328
576
  // Deload microcycle: half the sets from the previous microcycle, minimum 1 set.
329
577
  if (isDeloadMicrocycle) {
@@ -331,14 +579,16 @@ export default class WorkoutVolumePlanningService {
331
579
  microcycleIndex - 1,
332
580
  totalExercisesInMuscleGroupForMicrocycle,
333
581
  exerciseIndexInMuscleGroupForMicrocycle,
334
- false
582
+ false,
583
+ progressionInterval
335
584
  );
336
585
  return Math.max(1, Math.floor(baselineSets / 2));
337
586
  }
338
587
 
339
588
  // Total sets to distribute for this muscle group in this microcycle.
340
- // For now, add exactly +1 total set per microcycle per muscle group.
341
- const totalSets = 2 * totalExercisesInMuscleGroupForMicrocycle + microcycleIndex;
589
+ const progressionSets =
590
+ progressionInterval === 0 ? 0 : Math.ceil(microcycleIndex / progressionInterval);
591
+ const totalSets = 2 * totalExercisesInMuscleGroupForMicrocycle + progressionSets;
342
592
 
343
593
  // Distribute sets evenly, with earlier exercises getting extra sets from the remainder.
344
594
  const baseSetsPerExercise = Math.floor(totalSets / totalExercisesInMuscleGroupForMicrocycle);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@aneuhold/core-ts-db-lib",
3
3
  "author": "Anton G. Neuhold Jr.",
4
4
  "license": "MIT",
5
- "version": "4.1.12",
5
+ "version": "4.1.13",
6
6
  "description": "A core database library used for personal projects",
7
7
  "packageManager": "pnpm@10.25.0",
8
8
  "type": "module",