@aneuhold/core-ts-db-lib 4.0.4 → 4.1.1

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 (140) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/lib/browser.d.ts +36 -4
  3. package/lib/browser.d.ts.map +1 -1
  4. package/lib/browser.js +23 -2
  5. package/lib/browser.js.map +1 -1
  6. package/lib/browser.ts +126 -4
  7. package/lib/documents/BaseDocument.d.ts +8 -0
  8. package/lib/documents/BaseDocument.d.ts.map +1 -1
  9. package/lib/documents/BaseDocument.js +10 -0
  10. package/lib/documents/BaseDocument.js.map +1 -1
  11. package/lib/documents/BaseDocument.ts +18 -0
  12. package/lib/documents/common/User.d.ts +1 -0
  13. package/lib/documents/common/User.d.ts.map +1 -1
  14. package/lib/documents/common/User.js +4 -2
  15. package/lib/documents/common/User.js.map +1 -1
  16. package/lib/documents/common/User.ts +4 -2
  17. package/lib/documents/dashboard/NonogramKatanaItem.d.ts +1 -1
  18. package/lib/documents/dashboard/NonogramKatanaItem.js +1 -1
  19. package/lib/documents/dashboard/NonogramKatanaItem.js.map +1 -1
  20. package/lib/documents/dashboard/NonogramKatanaItem.ts +1 -1
  21. package/lib/documents/dashboard/NonogramKatanaUpgrade.d.ts +1 -1
  22. package/lib/documents/dashboard/NonogramKatanaUpgrade.js +1 -1
  23. package/lib/documents/dashboard/NonogramKatanaUpgrade.js.map +1 -1
  24. package/lib/documents/dashboard/NonogramKatanaUpgrade.ts +1 -1
  25. package/lib/documents/workout/README.md +557 -0
  26. package/lib/documents/workout/WorkoutEquipmentType.d.ts +22 -0
  27. package/lib/documents/workout/WorkoutEquipmentType.d.ts.map +1 -0
  28. package/lib/documents/workout/WorkoutEquipmentType.js +31 -0
  29. package/lib/documents/workout/WorkoutEquipmentType.js.map +1 -0
  30. package/lib/documents/workout/WorkoutEquipmentType.ts +40 -0
  31. package/lib/documents/workout/WorkoutExercise.d.ts +82 -0
  32. package/lib/documents/workout/WorkoutExercise.d.ts.map +1 -0
  33. package/lib/documents/workout/WorkoutExercise.js +124 -0
  34. package/lib/documents/workout/WorkoutExercise.js.map +1 -0
  35. package/lib/documents/workout/WorkoutExercise.ts +143 -0
  36. package/lib/documents/workout/WorkoutExerciseCalibration.d.ts +43 -0
  37. package/lib/documents/workout/WorkoutExerciseCalibration.d.ts.map +1 -0
  38. package/lib/documents/workout/WorkoutExerciseCalibration.js +45 -0
  39. package/lib/documents/workout/WorkoutExerciseCalibration.js.map +1 -0
  40. package/lib/documents/workout/WorkoutExerciseCalibration.ts +74 -0
  41. package/lib/documents/workout/WorkoutMesocycle.d.ts +49 -0
  42. package/lib/documents/workout/WorkoutMesocycle.d.ts.map +1 -0
  43. package/lib/documents/workout/WorkoutMesocycle.js +78 -0
  44. package/lib/documents/workout/WorkoutMesocycle.js.map +1 -0
  45. package/lib/documents/workout/WorkoutMesocycle.ts +95 -0
  46. package/lib/documents/workout/WorkoutMicrocycle.d.ts +27 -0
  47. package/lib/documents/workout/WorkoutMicrocycle.d.ts.map +1 -0
  48. package/lib/documents/workout/WorkoutMicrocycle.js +42 -0
  49. package/lib/documents/workout/WorkoutMicrocycle.js.map +1 -0
  50. package/lib/documents/workout/WorkoutMicrocycle.ts +55 -0
  51. package/lib/documents/workout/WorkoutMuscleGroup.d.ts +22 -0
  52. package/lib/documents/workout/WorkoutMuscleGroup.d.ts.map +1 -0
  53. package/lib/documents/workout/WorkoutMuscleGroup.js +25 -0
  54. package/lib/documents/workout/WorkoutMuscleGroup.js.map +1 -0
  55. package/lib/documents/workout/WorkoutMuscleGroup.ts +34 -0
  56. package/lib/documents/workout/WorkoutSession.d.ts +39 -0
  57. package/lib/documents/workout/WorkoutSession.d.ts.map +1 -0
  58. package/lib/documents/workout/WorkoutSession.js +66 -0
  59. package/lib/documents/workout/WorkoutSession.js.map +1 -0
  60. package/lib/documents/workout/WorkoutSession.ts +79 -0
  61. package/lib/documents/workout/WorkoutSessionExercise.d.ts +39 -0
  62. package/lib/documents/workout/WorkoutSessionExercise.d.ts.map +1 -0
  63. package/lib/documents/workout/WorkoutSessionExercise.js +66 -0
  64. package/lib/documents/workout/WorkoutSessionExercise.js.map +1 -0
  65. package/lib/documents/workout/WorkoutSessionExercise.ts +79 -0
  66. package/lib/documents/workout/WorkoutSet.d.ts +41 -0
  67. package/lib/documents/workout/WorkoutSet.d.ts.map +1 -0
  68. package/lib/documents/workout/WorkoutSet.js +69 -0
  69. package/lib/documents/workout/WorkoutSet.js.map +1 -0
  70. package/lib/documents/workout/WorkoutSet.ts +90 -0
  71. package/lib/embedded-types/workout/Fatigue.d.ts +16 -0
  72. package/lib/embedded-types/workout/Fatigue.d.ts.map +1 -0
  73. package/lib/embedded-types/workout/Fatigue.js +34 -0
  74. package/lib/embedded-types/workout/Fatigue.js.map +1 -0
  75. package/lib/embedded-types/workout/Fatigue.ts +41 -0
  76. package/lib/embedded-types/workout/Rsm.d.ts +17 -0
  77. package/lib/embedded-types/workout/Rsm.d.ts.map +1 -0
  78. package/lib/embedded-types/workout/Rsm.js +34 -0
  79. package/lib/embedded-types/workout/Rsm.js.map +1 -0
  80. package/lib/embedded-types/workout/Rsm.ts +42 -0
  81. package/lib/services/DocumentService.d.ts +19 -0
  82. package/lib/services/DocumentService.d.ts.map +1 -1
  83. package/lib/services/DocumentService.js.map +1 -1
  84. package/lib/services/DocumentService.ts +20 -0
  85. package/lib/services/workout/EquipmentType/WorkoutEquipmentTypeService.d.ts +35 -0
  86. package/lib/services/workout/EquipmentType/WorkoutEquipmentTypeService.d.ts.map +1 -0
  87. package/lib/services/workout/EquipmentType/WorkoutEquipmentTypeService.js +74 -0
  88. package/lib/services/workout/EquipmentType/WorkoutEquipmentTypeService.js.map +1 -0
  89. package/lib/services/workout/EquipmentType/WorkoutEquipmentTypeService.ts +90 -0
  90. package/lib/services/workout/Exercise/WorkoutExerciseService.d.ts +55 -0
  91. package/lib/services/workout/Exercise/WorkoutExerciseService.d.ts.map +1 -0
  92. package/lib/services/workout/Exercise/WorkoutExerciseService.js +133 -0
  93. package/lib/services/workout/Exercise/WorkoutExerciseService.js.map +1 -0
  94. package/lib/services/workout/Exercise/WorkoutExerciseService.ts +173 -0
  95. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.d.ts +35 -0
  96. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.d.ts.map +1 -0
  97. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.js +42 -0
  98. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.js.map +1 -0
  99. package/lib/services/workout/ExerciseCalibration/WorkoutExerciseCalibrationService.ts +45 -0
  100. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.d.ts +90 -0
  101. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.d.ts.map +1 -0
  102. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.js +131 -0
  103. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.js.map +1 -0
  104. package/lib/services/workout/Mesocycle/WorkoutMesocyclePlanContext.ts +159 -0
  105. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.d.ts +52 -0
  106. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.d.ts.map +1 -0
  107. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.js +180 -0
  108. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.js.map +1 -0
  109. package/lib/services/workout/Mesocycle/WorkoutMesocycleService.ts +274 -0
  110. package/lib/services/workout/Microcycle/WorkoutMicrocycleService.d.ts +28 -0
  111. package/lib/services/workout/Microcycle/WorkoutMicrocycleService.d.ts.map +1 -0
  112. package/lib/services/workout/Microcycle/WorkoutMicrocycleService.js +172 -0
  113. package/lib/services/workout/Microcycle/WorkoutMicrocycleService.js.map +1 -0
  114. package/lib/services/workout/Microcycle/WorkoutMicrocycleService.ts +236 -0
  115. package/lib/services/workout/Session/WorkoutSessionService.d.ts +49 -0
  116. package/lib/services/workout/Session/WorkoutSessionService.d.ts.map +1 -0
  117. package/lib/services/workout/Session/WorkoutSessionService.js +95 -0
  118. package/lib/services/workout/Session/WorkoutSessionService.js.map +1 -0
  119. package/lib/services/workout/Session/WorkoutSessionService.ts +136 -0
  120. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.d.ts +45 -0
  121. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.d.ts.map +1 -0
  122. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.js +69 -0
  123. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.js.map +1 -0
  124. package/lib/services/workout/SessionExercise/WorkoutSessionExerciseService.ts +77 -0
  125. package/lib/services/workout/Set/WorkoutSetService.d.ts +36 -0
  126. package/lib/services/workout/Set/WorkoutSetService.d.ts.map +1 -0
  127. package/lib/services/workout/Set/WorkoutSetService.js +90 -0
  128. package/lib/services/workout/Set/WorkoutSetService.js.map +1 -0
  129. package/lib/services/workout/Set/WorkoutSetService.ts +153 -0
  130. package/lib/services/workout/util/SFR/WorkoutSFRService.d.ts +29 -0
  131. package/lib/services/workout/util/SFR/WorkoutSFRService.d.ts.map +1 -0
  132. package/lib/services/workout/util/SFR/WorkoutSFRService.js +50 -0
  133. package/lib/services/workout/util/SFR/WorkoutSFRService.js.map +1 -0
  134. package/lib/services/workout/util/SFR/WorkoutSFRService.ts +61 -0
  135. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.d.ts +48 -0
  136. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.d.ts.map +1 -0
  137. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.js +261 -0
  138. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.js.map +1 -0
  139. package/lib/services/workout/util/VolumePlanning/WorkoutVolumePlanningService.ts +339 -0
  140. package/package.json +4 -3
@@ -0,0 +1,339 @@
1
+ import type { UUID } from 'crypto';
2
+ import type { CalibrationExercisePair } from '../../../../documents/workout/WorkoutExerciseCalibration.js';
3
+ import type { WorkoutSessionExercise } from '../../../../documents/workout/WorkoutSessionExercise.js';
4
+ import type WorkoutMesocyclePlanContext from '../../Mesocycle/WorkoutMesocyclePlanContext.js';
5
+ import WorkoutSessionExerciseService from '../../SessionExercise/WorkoutSessionExerciseService.js';
6
+
7
+ /**
8
+ * A service for handling volume planning operations across microcycles.
9
+ *
10
+ * SCOPE: Microcycle-level volume distribution (calculating set counts per exercise)
11
+ *
12
+ * RESPONSIBILITIES:
13
+ * - Calculate set counts for exercises across a microcycle
14
+ * - Apply progressive overload rules (baseline + historical adjustments)
15
+ * - Handle recovery exercise identification
16
+ * - Enforce volume limits (per exercise, per muscle group per session)
17
+ *
18
+ * RELATED SERVICES:
19
+ * - {@link WorkoutMicrocycleService} - Calls this to get set plans before generating sessions
20
+ * - {@link WorkoutSessionService} - Uses the output to generate actual sessions
21
+ * - {@link WorkoutSessionExerciseService} - Used to calculate SFR and recovery recommendations
22
+ */
23
+ export default class WorkoutVolumePlanningService {
24
+ private static readonly MAX_SETS_PER_EXERCISE = 8;
25
+ private static readonly MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION = 10;
26
+
27
+ /**
28
+ * Calculates the set plan for an entire microcycle.
29
+ */
30
+ static calculateSetPlanForMicrocycle(
31
+ context: WorkoutMesocyclePlanContext,
32
+ microcycleIndex: number,
33
+ isDeloadMicrocycle: boolean
34
+ ): { exerciseIdToSetCount: Map<UUID, number>; recoveryExerciseIds: Set<UUID> } {
35
+ if (!context.muscleGroupToExercisePairsMap) {
36
+ throw new Error(
37
+ 'WorkoutMesocyclePlanContext.muscleGroupToExercisePairsMap is not initialized. This should be set during mesocycle planning.'
38
+ );
39
+ }
40
+
41
+ const exerciseIdToSetCount = new Map<UUID, number>();
42
+ const recoveryExerciseIds = new Set<UUID>();
43
+
44
+ context.muscleGroupToExercisePairsMap.values().forEach((muscleGroupExercisePairs) => {
45
+ const result = this.calculateSetCountForEachExerciseInMuscleGroup(
46
+ context,
47
+ microcycleIndex,
48
+ muscleGroupExercisePairs,
49
+ isDeloadMicrocycle
50
+ );
51
+ for (const [workoutExerciseId, setCount] of result.exerciseIdToSetCount) {
52
+ exerciseIdToSetCount.set(workoutExerciseId, setCount);
53
+ }
54
+ for (const recoveryExerciseId of result.recoveryExerciseIds) {
55
+ recoveryExerciseIds.add(recoveryExerciseId);
56
+ }
57
+ });
58
+
59
+ return { exerciseIdToSetCount, recoveryExerciseIds };
60
+ }
61
+
62
+ /**
63
+ * Calculates the set count for each exercise in a particular muscle group for this microcycle.
64
+ *
65
+ * If there is no previous microcycle data for the muscle group, this falls back to
66
+ * the baseline progression rules.
67
+ */
68
+ private static calculateSetCountForEachExerciseInMuscleGroup(
69
+ context: WorkoutMesocyclePlanContext,
70
+ microcycleIndex: number,
71
+ muscleGroupExercisePairs: CalibrationExercisePair[],
72
+ isDeloadMicrocycle: boolean
73
+ ): { exerciseIdToSetCount: Map<UUID, number>; recoveryExerciseIds: Set<UUID> } {
74
+ const exerciseIdToSetCount = new Map<UUID, number>();
75
+ const recoveryExerciseIds = new Set<UUID>();
76
+ const sessionIndexToExerciseIds = new Map<number, UUID[]>();
77
+
78
+ // 1. Calculate baselines for all exercises in muscle group
79
+ muscleGroupExercisePairs.forEach((pair, index) => {
80
+ const baseline = this.calculateBaselineSetCount(
81
+ microcycleIndex,
82
+ muscleGroupExercisePairs.length,
83
+ index,
84
+ isDeloadMicrocycle
85
+ );
86
+ exerciseIdToSetCount.set(pair.exercise._id, Math.min(baseline, this.MAX_SETS_PER_EXERCISE));
87
+
88
+ // Build out the map for session indices to the array of exercise IDs as it pertains to this
89
+ // muscle group.
90
+ if (!context.exerciseIdToSessionIndex) return;
91
+ const exerciseSessionIndex = context.exerciseIdToSessionIndex.get(pair.exercise._id);
92
+ if (exerciseSessionIndex === undefined) return;
93
+ const existingExerciseIdsForSession =
94
+ sessionIndexToExerciseIds.get(exerciseSessionIndex) || [];
95
+ existingExerciseIdsForSession.push(pair.exercise._id);
96
+ sessionIndexToExerciseIds.set(exerciseSessionIndex, existingExerciseIdsForSession);
97
+ });
98
+
99
+ // 2. Resolve historical performance data
100
+ // Return if no previous microcycle
101
+ let previousMicrocycleIndex = microcycleIndex - 1;
102
+ let previousMicrocycle = context.microcyclesInOrder[previousMicrocycleIndex];
103
+ if (!previousMicrocycle) return { exerciseIdToSetCount, recoveryExerciseIds };
104
+
105
+ // Map previous session exercises
106
+ const exerciseIds = new Set(muscleGroupExercisePairs.map((p) => p.exercise._id));
107
+ const exerciseIdToPrevSessionExercise = new Map<UUID, WorkoutSessionExercise>();
108
+ const foundExerciseIds = new Set<UUID>();
109
+ const exercisesThatWerePreviouslyInRecovery = new Set<UUID>();
110
+ // Loop through each previous microcycle until we find all exercises or run out of microcycles
111
+ while (exerciseIdToPrevSessionExercise.size < exerciseIds.size && previousMicrocycle) {
112
+ // Check if the previous microcycle is complete; if not, we cannot use its data
113
+ const lastSessionId =
114
+ previousMicrocycle.sessionOrder[previousMicrocycle.sessionOrder.length - 1];
115
+ const microcycleIsComplete = context.sessionMap.get(lastSessionId)?.complete;
116
+ if (!microcycleIsComplete) {
117
+ break;
118
+ }
119
+
120
+ // Start with session order
121
+ for (const sessionId of previousMicrocycle.sessionOrder) {
122
+ const session = context.sessionMap.get(sessionId);
123
+ if (!session) continue;
124
+ // Get the session exercises for this session
125
+ for (const sessionExerciseId of session.sessionExerciseOrder) {
126
+ const sessionExercise = context.sessionExerciseMap.get(sessionExerciseId);
127
+ // Map if in our muscle group && it isn't a recovery exercise
128
+ if (
129
+ sessionExercise &&
130
+ exerciseIds.has(sessionExercise.workoutExerciseId) &&
131
+ !foundExerciseIds.has(sessionExercise.workoutExerciseId) &&
132
+ !sessionExercise.isRecoveryExercise
133
+ ) {
134
+ exerciseIdToPrevSessionExercise.set(sessionExercise.workoutExerciseId, sessionExercise);
135
+ foundExerciseIds.add(sessionExercise.workoutExerciseId);
136
+ if (previousMicrocycleIndex < microcycleIndex - 1) {
137
+ exercisesThatWerePreviouslyInRecovery.add(sessionExercise.workoutExerciseId);
138
+ }
139
+ }
140
+ }
141
+ }
142
+ // Move to earlier microcycle
143
+ previousMicrocycleIndex = previousMicrocycleIndex - 1;
144
+ previousMicrocycle = context.microcyclesInOrder[previousMicrocycleIndex];
145
+ }
146
+ if (exerciseIdToPrevSessionExercise.size === 0)
147
+ return { exerciseIdToSetCount, recoveryExerciseIds };
148
+
149
+ // Update baseline with historical set counts when available
150
+ muscleGroupExercisePairs.forEach((pair) => {
151
+ const previousSessionExercise = exerciseIdToPrevSessionExercise.get(pair.exercise._id);
152
+ if (previousSessionExercise) {
153
+ exerciseIdToSetCount.set(
154
+ pair.exercise._id,
155
+ Math.min(previousSessionExercise.setOrder.length, this.MAX_SETS_PER_EXERCISE)
156
+ );
157
+ }
158
+ });
159
+
160
+ /**
161
+ * Determines if the session for the given exercise is already capped for this muscle group.
162
+ */
163
+ function sessionIsCapped(exerciseId: UUID): boolean {
164
+ if (!context.exerciseIdToSessionIndex) {
165
+ throw new Error(
166
+ 'WorkoutMesocyclePlanContext.exerciseIdToSessionIndex is not initialized. This should be set during mesocycle planning.'
167
+ );
168
+ }
169
+ const sessionIndex = context.exerciseIdToSessionIndex.get(exerciseId);
170
+ if (sessionIndex === undefined) return false;
171
+
172
+ const exerciseIdsInSession = sessionIndexToExerciseIds.get(sessionIndex);
173
+ if (!exerciseIdsInSession) return false;
174
+ let totalSetsInSession = 0;
175
+ exerciseIdsInSession.forEach((id) => {
176
+ // Use the sets from the previous microcycle's session exercise
177
+ totalSetsInSession += exerciseIdToPrevSessionExercise.get(id)?.setOrder.length || 0;
178
+ });
179
+ return (
180
+ totalSetsInSession >= WorkoutVolumePlanningService.MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION
181
+ );
182
+ }
183
+
184
+ // 3. Determine sets to add and valid candidates
185
+ let totalSetsToAdd = 0;
186
+ const candidates: {
187
+ exerciseId: UUID;
188
+ sfr: number;
189
+ muscleGroupIndex: number;
190
+ previousSetCount: number;
191
+ }[] = [];
192
+ muscleGroupExercisePairs.forEach((pair, muscleGroupIndex) => {
193
+ const previousSessionExercise = exerciseIdToPrevSessionExercise.get(pair.exercise._id);
194
+ if (!previousSessionExercise) return;
195
+
196
+ let recommendation = null;
197
+ if (!exercisesThatWerePreviouslyInRecovery.has(pair.exercise._id)) {
198
+ recommendation =
199
+ WorkoutSessionExerciseService.getRecommendedSetAdditionsOrRecovery(
200
+ previousSessionExercise
201
+ );
202
+ } else {
203
+ // If previously in recovery, do not recommend adding sets this microcycle. Also, this is
204
+ // the only thing we are overriding. We still want to use the historical data for
205
+ // SFR calculations, even though that one was the one that triggered a recovery session.
206
+ // This should make it so that it is less likely to have sets added to it.
207
+ recommendation = 0;
208
+ }
209
+
210
+ if (recommendation === -1) {
211
+ recoveryExerciseIds.add(pair.exercise._id);
212
+ // Cut sets in half (rounded down, minimum 1) for recovery
213
+ const previousSetCount = previousSessionExercise.setOrder.length;
214
+ const recoverySets = Math.max(1, Math.floor(previousSetCount / 2));
215
+ exerciseIdToSetCount.set(pair.exercise._id, recoverySets);
216
+ } else if (recommendation != null && recommendation >= 0) {
217
+ totalSetsToAdd += recommendation;
218
+
219
+ // Consider as candidate if session is not already capped
220
+ if (
221
+ previousSessionExercise.setOrder.length < this.MAX_SETS_PER_EXERCISE &&
222
+ !sessionIsCapped(pair.exercise._id)
223
+ ) {
224
+ candidates.push({
225
+ exerciseId: pair.exercise._id,
226
+ // Don't error if SFR is null for now, just treat as very low
227
+ sfr:
228
+ WorkoutSessionExerciseService.getSFR(previousSessionExercise) ??
229
+ Number.NEGATIVE_INFINITY,
230
+ muscleGroupIndex,
231
+ previousSetCount: previousSessionExercise.setOrder.length
232
+ });
233
+ }
234
+ }
235
+ });
236
+
237
+ // Return if nothing to add or no candidates
238
+ if (totalSetsToAdd === 0 || candidates.length === 0) {
239
+ return { exerciseIdToSetCount, recoveryExerciseIds };
240
+ }
241
+
242
+ // 4. Distribute added sets based on SFR quality
243
+ // Sort by SFR descending, then by muscleGroupIndex ascending (as tie-breaker)
244
+ candidates.sort((candidateA, candidateB) =>
245
+ candidateA.sfr !== candidateB.sfr
246
+ ? candidateB.sfr - candidateA.sfr
247
+ : candidateA.muscleGroupIndex - candidateB.muscleGroupIndex
248
+ );
249
+
250
+ /**
251
+ * Gets the total sets currently planned for a session.
252
+ */
253
+ function getSessionTotal(exerciseId: UUID): number {
254
+ if (!context.exerciseIdToSessionIndex) return 0;
255
+ const sessionIndex = context.exerciseIdToSessionIndex.get(exerciseId);
256
+ if (sessionIndex === undefined) return 0;
257
+
258
+ const exerciseIdsInSession = sessionIndexToExerciseIds.get(sessionIndex);
259
+ if (!exerciseIdsInSession) return 0;
260
+
261
+ let total = 0;
262
+ exerciseIdsInSession.forEach((id) => {
263
+ total += exerciseIdToSetCount.get(id) || 0;
264
+ });
265
+ return total;
266
+ }
267
+
268
+ /**
269
+ * Attempts to add sets to an exercise, respecting all constraints.
270
+ * Returns the number of sets actually added.
271
+ */
272
+ function addSetsToExercise(exerciseId: UUID, setsToAdd: number): number {
273
+ const currentSets = exerciseIdToSetCount.get(exerciseId) || 0;
274
+ const sessionTotal = getSessionTotal(exerciseId);
275
+
276
+ const maxDueToExerciseLimit =
277
+ WorkoutVolumePlanningService.MAX_SETS_PER_EXERCISE - currentSets;
278
+ const maxDueToSessionLimit =
279
+ WorkoutVolumePlanningService.MAX_SETS_PER_MUSCLE_GROUP_PER_SESSION - sessionTotal;
280
+ // Hard limit of 2 to add to a particular exercise at once
281
+ const maxAddable = Math.min(setsToAdd, maxDueToExerciseLimit, maxDueToSessionLimit, 2);
282
+
283
+ if (maxAddable > 0) {
284
+ exerciseIdToSetCount.set(exerciseId, currentSets + maxAddable);
285
+ }
286
+ return maxAddable;
287
+ }
288
+
289
+ // Cap the actual sets to add to 3 total
290
+ let setsRemaining = totalSetsToAdd >= 3 ? 3 : totalSetsToAdd;
291
+ for (const candidate of candidates) {
292
+ const added = addSetsToExercise(candidate.exerciseId, setsRemaining);
293
+ setsRemaining -= added;
294
+ if (setsRemaining === 0) break;
295
+ }
296
+
297
+ return { exerciseIdToSetCount, recoveryExerciseIds };
298
+ }
299
+
300
+ /**
301
+ * Calculates the default number of sets for an exercise based on microcycle progression.
302
+ *
303
+ * Key rule: set progression is distributed across exercises that share the same primary muscle group
304
+ * for the entire microcycle, regardless of which session those exercises are in.
305
+ *
306
+ * Baseline: 2 sets per exercise in the muscle group.
307
+ * Progression: add 1 total set per microcycle per muscle group (distributed to earlier exercises
308
+ * in the muscle-group-wide ordering).
309
+ */
310
+ private static calculateBaselineSetCount(
311
+ microcycleIndex: number,
312
+ totalExercisesInMuscleGroupForMicrocycle: number,
313
+ exerciseIndexInMuscleGroupForMicrocycle: number,
314
+ isDeloadMicrocycle: boolean
315
+ ): number {
316
+ // Deload microcycle: half the sets from the previous microcycle, minimum 1 set.
317
+ if (isDeloadMicrocycle) {
318
+ const baselineSets = this.calculateBaselineSetCount(
319
+ microcycleIndex - 1,
320
+ totalExercisesInMuscleGroupForMicrocycle,
321
+ exerciseIndexInMuscleGroupForMicrocycle,
322
+ false
323
+ );
324
+ return Math.max(1, Math.floor(baselineSets / 2));
325
+ }
326
+
327
+ // Total sets to distribute for this muscle group in this microcycle.
328
+ // For now, add exactly +1 total set per microcycle per muscle group.
329
+ const totalSets = 2 * totalExercisesInMuscleGroupForMicrocycle + microcycleIndex;
330
+
331
+ // Distribute sets evenly, with earlier exercises getting extra sets from the remainder.
332
+ const baseSetsPerExercise = Math.floor(totalSets / totalExercisesInMuscleGroupForMicrocycle);
333
+ const remainder = totalSets % totalExercisesInMuscleGroupForMicrocycle;
334
+
335
+ return exerciseIndexInMuscleGroupForMicrocycle < remainder
336
+ ? baseSetsPerExercise + 1
337
+ : baseSetsPerExercise;
338
+ }
339
+ }
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.0.4",
5
+ "version": "4.1.1",
6
6
  "description": "A core database library used for personal projects",
7
7
  "packageManager": "pnpm@10.25.0",
8
8
  "type": "module",
@@ -13,6 +13,7 @@
13
13
  "watch": "nodemon --ignore lib/ -e ts --exec \"pnpm build:withoutClean && local-npm publish\"",
14
14
  "unpub:local": "local-npm unpublish || true",
15
15
  "lint": "eslint",
16
+ "check": "tsc --noEmit",
16
17
  "test": "vitest run",
17
18
  "preparePkg": "tb pkg prepare",
18
19
  "jsr:validate": "tb pkg validateJsr --allow-slow-types",
@@ -71,12 +72,12 @@
71
72
  "Node.js"
72
73
  ],
73
74
  "dependencies": {
74
- "@aneuhold/core-ts-lib": "^2.3.16",
75
+ "@aneuhold/core-ts-lib": "^2.3.17",
75
76
  "uuid": "^13.0.0",
76
77
  "zod": "^4.1.13"
77
78
  },
78
79
  "devDependencies": {
79
- "@aneuhold/local-npm-registry": "^0.2.23",
80
+ "@aneuhold/local-npm-registry": "^0.2.24",
80
81
  "@aneuhold/main-scripts": "^2.8.0",
81
82
  "@types/node": "^25.0.2",
82
83
  "eslint": "^9.39.2",