@dgpholdings/greatoak-shared 1.2.59 → 1.2.61

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.
@@ -8,6 +8,7 @@ exports.mockExercisesDictionary = exports.mockExerciseNoGuardrails = exports.moc
8
8
  exports.mockExerciseWeightReps = {
9
9
  exerciseId: "mock-exercise-weight-reps-123",
10
10
  name: "Generic Barbell Squat",
11
+ status: "active",
11
12
  bodyPart: ["Legs"],
12
13
  recordType: "weight-reps",
13
14
  primaryMuscles: ["quadriceps"],
@@ -56,7 +57,7 @@ exports.mockExerciseWeightReps = {
56
57
  * MOCK EXERCISE: Reps-Only
57
58
  * Simulates a bodyweight movement (e.g., Push-up).
58
59
  */
59
- exports.mockExerciseRepsOnly = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-reps-only-456", name: "Generic Push-Up", bodyPart: ["Chest", "Core"], recordType: "reps-only", primaryMuscles: ["pectoralis-major"], secondaryMuscles: ["tricep-brachii-lateral", "abs-lower"], trainingTypes: ["body-weight"], timingGuardrails: {
60
+ exports.mockExerciseRepsOnly = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-reps-only-456", name: "Generic Push-Up", status: "active", bodyPart: ["Chest", "Core"], recordType: "reps-only", primaryMuscles: ["pectoralis-major"], secondaryMuscles: ["tricep-brachii-lateral", "abs-lower"], trainingTypes: ["body-weight"], timingGuardrails: {
60
61
  type: "reps-only",
61
62
  stressRestBonus: 2,
62
63
  fatigueMultiplier: 1.05,
@@ -77,7 +78,7 @@ exports.mockExerciseRepsOnly = Object.assign(Object.assign({}, exports.mockExerc
77
78
  * MOCK EXERCISE: Duration
78
79
  * Simulates an isometric hold (e.g., Plank).
79
80
  */
80
- exports.mockExerciseDuration = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-duration-789", name: "Generic Forearm Plank", bodyPart: ["Core"], recordType: "duration", primaryMuscles: ["abs-lower", "abs-upper"], secondaryMuscles: ["lower-back"], trainingTypes: ["body-weight", "isometric"], timingGuardrails: {
81
+ exports.mockExerciseDuration = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-duration-789", name: "Generic Forearm Plank", status: "active", bodyPart: ["Core"], recordType: "duration", primaryMuscles: ["abs-lower", "abs-upper"], secondaryMuscles: ["lower-back"], trainingTypes: ["body-weight", "isometric"], timingGuardrails: {
81
82
  type: "duration",
82
83
  stressRestBonus: 3,
83
84
  fatigueMultiplier: 1.1,
@@ -98,7 +99,7 @@ exports.mockExerciseDuration = Object.assign(Object.assign({}, exports.mockExerc
98
99
  * MOCK EXERCISE: Cardio-Machine
99
100
  * Simulates a machine cardio session (e.g., Treadmill).
100
101
  */
101
- exports.mockExerciseCardioMachine = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-cardio-machine-101", name: "Generic Treadmill Run", bodyPart: ["Legs"], recordType: "cardio-machine", primaryMuscles: ["quadriceps", "hamstrings", "calves"], secondaryMuscles: ["glutes-maximus"], trainingTypes: ["cardio"], timingGuardrails: {
102
+ exports.mockExerciseCardioMachine = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-cardio-machine-101", name: "Generic Treadmill Run", status: "active", bodyPart: ["Legs"], recordType: "cardio-machine", primaryMuscles: ["quadriceps", "hamstrings", "calves"], secondaryMuscles: ["glutes-maximus"], trainingTypes: ["cardio"], timingGuardrails: {
102
103
  type: "cardio-machine",
103
104
  stressRestBonus: 0,
104
105
  fatigueMultiplier: 1.0,
@@ -114,7 +115,7 @@ exports.mockExerciseCardioMachine = Object.assign(Object.assign({}, exports.mock
114
115
  * MOCK EXERCISE: Cardio-Free
115
116
  * Simulates an outdoor/untracked cardio session (e.g., Outdoor Run).
116
117
  */
117
- exports.mockExerciseCardioFree = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-cardio-free-202", name: "Generic Outdoor Jog", bodyPart: ["Legs"], recordType: "cardio-free", primaryMuscles: ["quadriceps", "hamstrings", "calves"], secondaryMuscles: ["glutes-maximus"], trainingTypes: ["cardio"], timingGuardrails: {
118
+ exports.mockExerciseCardioFree = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-cardio-free-202", name: "Generic Outdoor Jog", status: "active", bodyPart: ["Legs"], recordType: "cardio-free", primaryMuscles: ["quadriceps", "hamstrings", "calves"], secondaryMuscles: ["glutes-maximus"], trainingTypes: ["cardio"], timingGuardrails: {
118
119
  type: "cardio-free",
119
120
  stressRestBonus: 0,
120
121
  fatigueMultiplier: 1.0,
@@ -130,7 +131,7 @@ exports.mockExerciseCardioFree = Object.assign(Object.assign({}, exports.mockExe
130
131
  * MOCK EXERCISE: No Guardrails
131
132
  * Simulates an older DB entry or custom exercise missing guardrails.
132
133
  */
133
- exports.mockExerciseNoGuardrails = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-no-guardrails-303", name: "Legacy Exercise", timingGuardrails: undefined });
134
+ exports.mockExerciseNoGuardrails = Object.assign(Object.assign({}, exports.mockExerciseWeightReps), { exerciseId: "mock-exercise-no-guardrails-303", name: "Legacy Exercise", status: "active", timingGuardrails: undefined });
134
135
  /**
135
136
  * Helper dictionary containing all mock exercises mapped by their ID.
136
137
  */
@@ -0,0 +1,92 @@
1
+ export type TConfidence = "high" | "medium" | null;
2
+ export type TNameReviewStatus = "pending" | "approved";
3
+ export type TQdrantSyncStatus = "pending" | "synced" | "failed";
4
+ export type TAiMuscles = {
5
+ primary: string[];
6
+ secondary: string[];
7
+ stabilizers: string[];
8
+ stretched: string[];
9
+ };
10
+ export type TAiJointInvolved = {
11
+ joint: string;
12
+ action: string;
13
+ rom_degrees: string;
14
+ stress_level: string;
15
+ };
16
+ export type TAiDifficulty = {
17
+ level: string;
18
+ coordination_demand: string;
19
+ balance_demand: string;
20
+ flexibility_demand: string;
21
+ strength_demand: string;
22
+ };
23
+ export type TAiEquipment = {
24
+ primary: string;
25
+ secondary: string[];
26
+ can_be_done_at_home: boolean;
27
+ minimal_equipment_alternative: string | null;
28
+ };
29
+ export type TAiSafety = {
30
+ injury_risk_areas: string[];
31
+ contraindicated_conditions: string[];
32
+ common_mistakes: string[];
33
+ prerequisite_mobility: string[];
34
+ };
35
+ export type TAiEnergyAndLoad = {
36
+ estimated_met: number;
37
+ typical_rep_range: string;
38
+ typical_set_range: string;
39
+ rest_period_seconds: string;
40
+ tempo_recommendation: string;
41
+ };
42
+ export type TAiFatigueProfile = {
43
+ cns_demand: string;
44
+ local_muscle_fatigue: string;
45
+ cardiovascular_demand: string;
46
+ recovery_time_hours: string;
47
+ };
48
+ export type TAiProgrammingContext = {
49
+ suitable_for_goals: string[];
50
+ workout_placement: string;
51
+ pairs_well_with: string[];
52
+ progressions: string[];
53
+ regressions: string[];
54
+ };
55
+ export type TAiDemographics = {
56
+ suitable_for_beginners: boolean;
57
+ suitable_for_elderly: boolean;
58
+ suitable_for_prenatal: boolean;
59
+ suitable_during_rehab: boolean;
60
+ age_group_notes: string;
61
+ };
62
+ export type TApiAiExerciseAnalysis = {
63
+ exerciseId: string;
64
+ isGivenNameConflicting: boolean;
65
+ exercise_name: string;
66
+ alternative_names: string[];
67
+ movement_description: string;
68
+ movement_pattern: string;
69
+ exercise_type: string;
70
+ contraction_type: string;
71
+ plane_of_motion: string[];
72
+ kinetic_chain: string;
73
+ laterality: string;
74
+ muscles: TAiMuscles;
75
+ joints_involved: TAiJointInvolved[];
76
+ difficulty: TAiDifficulty;
77
+ equipment: TAiEquipment;
78
+ safety: TAiSafety;
79
+ energy_and_load: TAiEnergyAndLoad;
80
+ fatigue_profile: TAiFatigueProfile;
81
+ programming_context: TAiProgrammingContext;
82
+ demographics: TAiDemographics;
83
+ form_notes: string;
84
+ embedding_text: string;
85
+ confidence: TConfidence;
86
+ nameReviewStatus: TNameReviewStatus;
87
+ qdrantSyncStatus: TQdrantSyncStatus;
88
+ enrichedAt: string;
89
+ aiProvider: string;
90
+ seedVersion: string;
91
+ };
92
+ export type TApiAiMetaListRecord = Pick<TApiAiExerciseAnalysis, "exerciseId" | "isGivenNameConflicting" | "confidence" | "nameReviewStatus">;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -123,6 +123,10 @@ export type TExercise = {
123
123
  isFavorite?: boolean;
124
124
  scoringSpecialHandling?: "plyometric" | "stretch-mobility" | "continuous-duration" | "loaded-carry";
125
125
  isFlaggedCorrection?: boolean;
126
+ /** True for single-arm / single-leg exercises. The user enters weight per side,
127
+ * so the scoring engine doubles it to get total mechanical load. */
128
+ isUnilateral?: boolean;
129
+ status: "active" | "inactive" | "archived";
126
130
  regressionExerciseId?: string;
127
131
  progressionExerciseId?: string;
128
132
  bodyweightDependency?: "high" | "medium" | "low" | "none";
@@ -135,9 +139,6 @@ export type TExercise = {
135
139
  female: number;
136
140
  default: number;
137
141
  };
138
- /** True for single-arm / single-leg exercises. The user enters weight per side,
139
- * so the scoring engine doubles it to get total mechanical load. */
140
- isUnilateral?: boolean;
141
142
  };
142
143
  export type TBodyPartExercises = Record<TBodyPart, TExercise[]>;
143
144
  export type TApiCreateOrUpdateExerciseReq = {
@@ -11,3 +11,4 @@ export type * from "./TApiRevenueCat";
11
11
  export type * from "./TApiClientProgress";
12
12
  export type * from "./TApiProPlan";
13
13
  export type * from "./TApiSales";
14
+ export type * from "./TApiAiExerciseAnalysis";
@@ -64,7 +64,7 @@ function calculateMuscleFatigue(sets, exercise, user, timingGuardrails, historic
64
64
  return {};
65
65
  const fatigueMultiplier = (_a = timingGuardrails === null || timingGuardrails === void 0 ? void 0 : timingGuardrails.fatigueMultiplier) !== null && _a !== void 0 ? _a : constants_1.FALLBACK_FATIGUE_MULTIPLIER;
66
66
  // --- Step 1–3: Compute cumulative fatigue stimulus ---
67
- const cumulativeFatigue = computeCumulativeFatigue(sets, exercise.difficultyLevel, user, fatigueMultiplier, exercise.scoringSpecialHandling, exercise.isUnilateral);
67
+ const cumulativeFatigue = computeCumulativeFatigue(sets, exercise.difficultyLevel, user, fatigueMultiplier, exercise.scoringSpecialHandling, exercise.isUnilateral, historicalContext);
68
68
  // --- Step 4: Distribute to muscles ---
69
69
  const rawMuscleFatigue = distributeFatigueToMuscles(cumulativeFatigue, exercise.primaryMuscles, exercise.secondaryMuscles);
70
70
  // --- Step 5: Normalize to 0–100 ---
@@ -88,7 +88,7 @@ function calculateMuscleFatigue(sets, exercise, user, timingGuardrails, historic
88
88
  * cardio: Duration × speed factor (represents sustained effort)
89
89
  * loaded-carry: Speed factor boosted by carried weight relative to bodyweight
90
90
  */
91
- function computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling, isUnilateral) {
91
+ function computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling, isUnilateral, historicalContext) {
92
92
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
93
93
  switch (set.type) {
94
94
  case "weight-reps": {
@@ -100,15 +100,26 @@ function computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling, i
100
100
  case "reps-only": {
101
101
  const reps = (_c = set.reps) !== null && _c !== void 0 ? _c : 0;
102
102
  const auxWeightKg = (_d = set.auxWeightKg) !== null && _d !== void 0 ? _d : 0;
103
- // Estimate how much bodyweight the exercise loads
104
- // difficulty 0 0%, difficulty 2 ~33%, difficulty 4 → 65%
105
- const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * user.weightKg;
106
- const totalLoad = bodyweightLoad + auxWeightKg;
107
- const baseVolume = totalLoad * reps;
108
- // Plyometric: impact forces and neuromotor demand exceed simple BW × reps
109
- if (scoringSpecialHandling === "plyometric") {
110
- return baseVolume * constants_1.PLYOMETRIC_LOAD_MULTIPLIER;
103
+ // Epley Adaptation (P4-1): when a historical 1RM equivalent is available
104
+ // (bodyweight × (1 + bestReps/30)), use it to derive relative intensity.
105
+ // This means a user doing 20/25 max pull-ups scores higher fatigue than a
106
+ // user doing 20/80 max — identical reps, very different physiological effort.
107
+ //
108
+ // Fallback: difficulty-based bodyweight fraction (pre-P4-1 behaviour).
109
+ // Used when no history exists (first session on this exercise).
110
+ let effectiveLoad;
111
+ if ((historicalContext === null || historicalContext === void 0 ? void 0 : historicalContext.estimatedOneRepMax) && user.weightKg > 0 && reps > 0) {
112
+ const sessionEpley = user.weightKg * (1 + reps / 30);
113
+ const relativeIntensity = Math.min(1.0, sessionEpley / historicalContext.estimatedOneRepMax);
114
+ effectiveLoad = relativeIntensity * user.weightKg + auxWeightKg;
115
+ }
116
+ else {
117
+ const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * user.weightKg;
118
+ effectiveLoad = bodyweightLoad + auxWeightKg;
111
119
  }
120
+ const baseVolume = effectiveLoad * reps;
121
+ if (scoringSpecialHandling === "plyometric")
122
+ return baseVolume * constants_1.PLYOMETRIC_LOAD_MULTIPLIER;
112
123
  return baseVolume;
113
124
  }
114
125
  case "duration": {
@@ -176,12 +187,12 @@ function computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling, i
176
187
  *
177
188
  * @returns Single number representing total fatigue stimulus
178
189
  */
179
- function computeCumulativeFatigue(sets, difficultyLevel, user, fatigueMultiplier, scoringSpecialHandling, isUnilateral) {
190
+ function computeCumulativeFatigue(sets, difficultyLevel, user, fatigueMultiplier, scoringSpecialHandling, isUnilateral, historicalContext) {
180
191
  let cumulative = 0;
181
192
  for (let i = 0; i < sets.length; i++) {
182
193
  const set = sets[i];
183
194
  // Step 1: Raw volume for this set
184
- const volumeLoad = computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling, isUnilateral);
195
+ const volumeLoad = computeVolumeLoad(set, difficultyLevel, user, scoringSpecialHandling, isUnilateral, historicalContext);
185
196
  // Step 2: Scale by effort and exercise fatigue multiplier
186
197
  const stimulus = volumeLoad * set.effortFraction * fatigueMultiplier;
187
198
  // Step 3: Apply diminishing returns decay
@@ -260,9 +271,17 @@ function computeReferenceMax(exerciseType, difficultyLevel, user, muscleGroupFac
260
271
  break;
261
272
  }
262
273
  case "reps-only": {
263
- const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * user.weightKg;
264
- const typicalAux = 5; // assume up to 5kg aux for reference
265
- singleSetMax = (bodyweightLoad + typicalAux) * 15;
274
+ // Epley Adaptation (P4-1): use the stored 1RM equivalent as the capacity
275
+ // anchor when available. This means the reference max scales to the
276
+ // individual's actual strength, not a population-average estimate.
277
+ if (historicalContext === null || historicalContext === void 0 ? void 0 : historicalContext.estimatedOneRepMax) {
278
+ singleSetMax = historicalContext.estimatedOneRepMax * constants_1.REFERENCE_MAX_REPS;
279
+ }
280
+ else {
281
+ // Fallback: difficulty-based bodyweight fraction
282
+ const bodyweightLoad = (difficultyLevel / 4) * constants_1.BW_FRACTION_SCALE * user.weightKg;
283
+ singleSetMax = (bodyweightLoad + 5) * 15;
284
+ }
266
285
  break;
267
286
  }
268
287
  case "duration": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.2.59",
3
+ "version": "1.2.61",
4
4
  "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",