@dgpholdings/greatoak-shared 1.2.86 → 1.2.88

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 (106) hide show
  1. package/README.md +148 -148
  2. package/dist/__mocks__/catalog.fixture.d.ts +2 -0
  3. package/dist/__mocks__/catalog.fixture.js +208 -0
  4. package/dist/__mocks__/exercises.mock.d.ts +4 -11
  5. package/dist/__mocks__/exercises.mock.js +82 -41
  6. package/dist/__mocks__/sessions.mock.d.ts +28 -0
  7. package/dist/__mocks__/sessions.mock.js +394 -0
  8. package/dist/__mocks__/testIds.d.ts +9 -0
  9. package/dist/__mocks__/testIds.js +13 -0
  10. package/dist/__mocks__/user.mock.js +3 -1
  11. package/dist/constants/goalJourney.d.ts +108 -0
  12. package/dist/constants/goalJourney.js +443 -0
  13. package/dist/constants/index.d.ts +1 -0
  14. package/dist/constants/index.js +1 -0
  15. package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
  16. package/dist/types/TApiClientConstellation.d.ts +33 -0
  17. package/dist/types/TApiClientConstellation.js +13 -0
  18. package/dist/types/TApiExercise.d.ts +5 -3
  19. package/dist/types/TApiUser.d.ts +2 -0
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/utils/adoptionEngine/scaleProPlan.util.js +9 -4
  22. package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
  23. package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
  24. package/dist/utils/constellation/computeNormalisedLoad.test.d.ts +1 -0
  25. package/dist/utils/constellation/computeNormalisedLoad.test.js +218 -0
  26. package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
  27. package/dist/utils/constellation/evaluateConstellation.js +135 -0
  28. package/dist/utils/constellation/evaluateConstellation.test.d.ts +1 -0
  29. package/dist/utils/constellation/evaluateConstellation.test.js +93 -0
  30. package/dist/utils/constellation/index.d.ts +18 -0
  31. package/dist/utils/constellation/index.js +29 -0
  32. package/dist/utils/constellation/levelThresholds.d.ts +99 -0
  33. package/dist/utils/constellation/levelThresholds.js +123 -0
  34. package/dist/utils/constellation/starFoundation.d.ts +25 -0
  35. package/dist/utils/constellation/starFoundation.js +54 -0
  36. package/dist/utils/constellation/starFoundation.test.d.ts +1 -0
  37. package/dist/utils/constellation/starFoundation.test.js +75 -0
  38. package/dist/utils/constellation/stars/consistency.d.ts +29 -0
  39. package/dist/utils/constellation/stars/consistency.js +142 -0
  40. package/dist/utils/constellation/stars/consistency.test.d.ts +1 -0
  41. package/dist/utils/constellation/stars/consistency.test.js +94 -0
  42. package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
  43. package/dist/utils/constellation/stars/lowerBody.js +30 -0
  44. package/dist/utils/constellation/stars/pull.d.ts +11 -0
  45. package/dist/utils/constellation/stars/pull.js +24 -0
  46. package/dist/utils/constellation/stars/push.d.ts +11 -0
  47. package/dist/utils/constellation/stars/push.js +24 -0
  48. package/dist/utils/constellation/stars/quality.d.ts +19 -0
  49. package/dist/utils/constellation/stars/quality.js +98 -0
  50. package/dist/utils/constellation/stars/quality.test.d.ts +1 -0
  51. package/dist/utils/constellation/stars/quality.test.js +113 -0
  52. package/dist/utils/constellation/stars/recovery.d.ts +29 -0
  53. package/dist/utils/constellation/stars/recovery.js +169 -0
  54. package/dist/utils/constellation/stars/recovery.test.d.ts +1 -0
  55. package/dist/utils/constellation/stars/recovery.test.js +131 -0
  56. package/dist/utils/constellation/strengthStar.test.d.ts +1 -0
  57. package/dist/utils/constellation/strengthStar.test.js +190 -0
  58. package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
  59. package/dist/utils/constellation/strengthStarHelpers.js +104 -0
  60. package/dist/utils/constellation/types.d.ts +124 -0
  61. package/dist/utils/constellation/types.js +18 -0
  62. package/dist/utils/exerciseRecord/recordValidator.integration.test.js +1 -1
  63. package/dist/utils/exerciseRecord/recordValidator.js +1 -1
  64. package/dist/utils/exerciseRecord/recordValidator.test.js +8 -8
  65. package/dist/utils/index.d.ts +5 -3
  66. package/dist/utils/index.js +1 -0
  67. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
  68. package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
  69. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
  70. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
  71. package/dist/utils/scoringWorkout/constants.d.ts +20 -6
  72. package/dist/utils/scoringWorkout/constants.js +23 -9
  73. package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
  74. package/dist/utils/scoringWorkout/helpers.js +24 -18
  75. package/dist/utils/scoringWorkout/index.d.ts +12 -8
  76. package/dist/utils/scoringWorkout/index.js +23 -15
  77. package/dist/utils/scoringWorkout/parseRecords.js +4 -3
  78. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +223 -185
  79. package/dist/utils/scoringWorkout/types.d.ts +34 -14
  80. package/package.json +31 -31
  81. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
  82. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
  83. package/dist/utils/scaleProPlan.util.d.ts +0 -9
  84. package/dist/utils/scaleProPlan.util.js +0 -139
  85. package/dist/utils/scoring/calculateCalories.d.ts +0 -67
  86. package/dist/utils/scoring/calculateCalories.js +0 -345
  87. package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
  88. package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
  89. package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
  90. package/dist/utils/scoring/calculateQualityScore.js +0 -334
  91. package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
  92. package/dist/utils/scoring/calculateTotalVolume.js +0 -73
  93. package/dist/utils/scoring/constants.d.ts +0 -211
  94. package/dist/utils/scoring/constants.js +0 -247
  95. package/dist/utils/scoring/helpers.d.ts +0 -119
  96. package/dist/utils/scoring/helpers.js +0 -229
  97. package/dist/utils/scoring/index.d.ts +0 -28
  98. package/dist/utils/scoring/index.js +0 -47
  99. package/dist/utils/scoring/parseRecords.d.ts +0 -98
  100. package/dist/utils/scoring/parseRecords.js +0 -284
  101. package/dist/utils/scoring/types.d.ts +0 -86
  102. package/dist/utils/scoring/types.js +0 -11
  103. package/dist/utils/scoring.utils.d.ts +0 -14
  104. package/dist/utils/scoring.utils.js +0 -243
  105. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
  106. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.js → calculateMuscleFatigue.js} +0 -0
@@ -31,32 +31,49 @@ export type TMuscleFatigueResult = Record<string, TMuscleFatigueEntry>;
31
31
  /**
32
32
  * Quality score breakdown — lets the UI show users WHY they got their score.
33
33
  * Each sub-score is 0–100.
34
+ *
35
+ * Note: restDiscipline is 0 (and restDisciplineActive is false on IScoreResult)
36
+ * when the user did not log rest timing data. Check restDisciplineActive before
37
+ * rendering the rest discipline bar in the UI.
34
38
  */
35
39
  export interface IQualityBreakdown {
36
- /** Did the user complete all sets? */
40
+ /** Did the user complete all planned sets? */
37
41
  completion: number;
38
42
  /** Were sets consistent in output (or intentionally progressive)? */
39
43
  consistency: number;
40
- /** Was effort in the productive RPE/RIR zone? */
44
+ /**
45
+ * Was effort in the productive RPE/RIR zone?
46
+ * RPE and RIR are optional — users may choose not to log them.
47
+ * When absent, this sub-score is EFFORT_NO_DATA_SCORE (50) — neutral,
48
+ * never penalising omission.
49
+ */
41
50
  effortAdequacy: number;
42
- /** Were rest periods within the exercise's optimal range? */
51
+ /**
52
+ * Were rest periods within the exercise's optimal range?
53
+ * 0 when no rest data was logged (restDisciplineActive = false).
54
+ */
43
55
  restDiscipline: number;
44
56
  }
45
57
  /**
46
- * The final result returned by calculateExerciseScore.
58
+ * The final result returned by calculateExerciseScoreV2.
59
+ *
60
+ * Option A flat shape — one exercise produces one score and one breakdown.
61
+ * Goal-specific adaptation and progression logic live in the gate system
62
+ * and quick plan generator, not in the per-exercise score.
47
63
  *
48
- * - muscleScores: per-muscle fatigue map (keyed by EBodyParts key, value 0–100)
49
- * - calorieBurn: estimated kilocalories burned (gross, including EPOC)
50
- * - scoresByGoal: branched scores for each of the user's selected goals
64
+ * Fields:
65
+ * score — 0–100 quality of execution
66
+ * qualityBreakdown — per-component breakdown (completion/consistency/effort/rest)
67
+ * muscleScores — per-muscle fatigue (keyed by EBodyParts key, 0–100)
68
+ * calorieBurn — estimated kcal (placeholder: 0 until Phase 3)
69
+ * restDisciplineActive — false when no rest data logged; use to gate UI rendering
51
70
  */
52
- export interface IScoreByGoal {
71
+ export interface IScoreResult {
53
72
  score: number;
54
73
  qualityBreakdown: IQualityBreakdown;
55
- }
56
- export interface IScoreResult {
57
74
  muscleScores: Record<string, number>;
58
75
  calorieBurn: number;
59
- scoresByGoal: Partial<Record<TAiFitnessGoal, IScoreByGoal>>;
76
+ restDisciplineActive: boolean;
60
77
  }
61
78
  export type TTrainingAgeBracket = "beginner" | "intermediate" | "advanced";
62
79
  /**
@@ -85,7 +102,10 @@ export interface IParsedSet {
85
102
  type: TRecord["type"];
86
103
  /** Active work duration for this set in seconds (estimated or measured) */
87
104
  activeDurationSecs: number;
88
- /** Rest duration after this set in seconds (validated or fallback) */
105
+ /**
106
+ * Rest duration after this set in seconds (validated or fallback).
107
+ * null = last set in the exercise, or timing data was not logged.
108
+ */
89
109
  restDurationSecs: number | null;
90
110
  /**
91
111
  * Effort fraction: 0.0 (no effort) to 1.0 (max effort).
@@ -100,13 +120,13 @@ export interface IParsedSet {
100
120
  auxWeightKg?: number;
101
121
  /** duration: hold time in seconds */
102
122
  durationSecs?: number;
103
- /** cardio-machine: speed */
123
+ /** cardio-machine: speed in km/h */
104
124
  speed?: number;
105
125
  /** cardio-machine: incline percentage (Treadmill) */
106
126
  inclinePercentage?: number;
107
127
  /** cardio-machine: resistance level (Elliptical, Cycling) */
108
128
  resistanceLevel?: number;
109
- /** cardio-machine / cardio-free: distance (km) */
129
+ /** cardio-machine / cardio-free: distance in km */
110
130
  distance?: number;
111
131
  /** cardio-free / cardio-machine: session duration in seconds */
112
132
  cardioDurationSecs?: number;
package/package.json CHANGED
@@ -1,31 +1,31 @@
1
- {
2
- "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.2.86",
4
- "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
- "main": "./dist/index.js",
6
- "types": "./dist/index.d.ts",
7
- "files": [
8
- "dist/**/*"
9
- ],
10
- "scripts": {
11
- "build": "tsc",
12
- "pub": "npm run test && npm run build && npm version patch && npm publish",
13
- "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
14
- "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
15
- "test": "vitest run src",
16
- "test:watch": "vitest src"
17
- },
18
- "repository": {
19
- "type": "git",
20
- "url": "https://gitlab.com/greatoak/shared.git"
21
- },
22
- "author": "Siddhartha Chowdhury",
23
- "license": "MIT",
24
- "devDependencies": {
25
- "typescript": "^5.8.2",
26
- "vitest": "^4.1.3"
27
- },
28
- "dependencies": {
29
- "react-native-uuid": "^2.0.3"
30
- }
31
- }
1
+ {
2
+ "name": "@dgpholdings/greatoak-shared",
3
+ "version": "1.2.88",
4
+ "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "files": [
8
+ "dist/**/*"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "pub": "npm run test && npm run build && npm version patch && npm publish",
13
+ "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"",
14
+ "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md}\"",
15
+ "test": "vitest run src",
16
+ "test:watch": "vitest src"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://gitlab.com/greatoak/shared.git"
21
+ },
22
+ "author": "Siddhartha Chowdhury",
23
+ "license": "MIT",
24
+ "devDependencies": {
25
+ "typescript": "^5.8.2",
26
+ "vitest": "^4.1.3"
27
+ },
28
+ "dependencies": {
29
+ "react-native-uuid": "^2.0.3"
30
+ }
31
+ }
@@ -1,30 +0,0 @@
1
- import { TExercise } from "../../../types";
2
- /**
3
- * MOCK EXERCISE: Weight-Reps
4
- * Simulates a standard compound lift (e.g., Barbell Squat).
5
- */
6
- export declare const mockExerciseWeightReps: TExercise;
7
- /**
8
- * MOCK EXERCISE: Reps-Only
9
- * Simulates a bodyweight movement (e.g., Push-up).
10
- */
11
- export declare const mockExerciseRepsOnly: TExercise;
12
- /**
13
- * MOCK EXERCISE: Duration
14
- * Simulates an isometric hold (e.g., Plank).
15
- */
16
- export declare const mockExerciseDuration: TExercise;
17
- /**
18
- * MOCK EXERCISE: Cardio-Machine
19
- * Simulates a machine cardio session (e.g., Treadmill).
20
- */
21
- export declare const mockExerciseCardioMachine: TExercise;
22
- /**
23
- * MOCK EXERCISE: Cardio-Free
24
- * Simulates an outdoor/untracked cardio session (e.g., Outdoor Run).
25
- */
26
- export declare const mockExerciseCardioFree: TExercise;
27
- /**
28
- * Helper dictionary containing all mock exercises mapped by their ID.
29
- */
30
- export declare const mockExercisesDictionary: Record<string, TExercise>;
@@ -1,138 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mockExercisesDictionary = exports.mockExerciseCardioFree = exports.mockExerciseCardioMachine = exports.mockExerciseDuration = exports.mockExerciseRepsOnly = exports.mockExerciseWeightReps = void 0;
4
- /**
5
- * MOCK EXERCISE: Weight-Reps
6
- * Simulates a standard compound lift (e.g., Barbell Squat).
7
- */
8
- exports.mockExerciseWeightReps = {
9
- exerciseId: "mock-exercise-weight-reps-123",
10
- name: "Generic Barbell Squat",
11
- bodyPart: ["Legs"],
12
- recordType: "weight-reps",
13
- primaryMuscles: ["quadriceps"],
14
- secondaryMuscles: ["glutes-maximus"],
15
- trainingTypes: ["weight"],
16
- difficultyLevel: 2,
17
- hypertrophyLevel: 4,
18
- strengthGainLevel: 4,
19
- flexibilityLevel: 1,
20
- calorieBurnLevel: 3,
21
- stabilityLevel: 2,
22
- enduranceLevel: 1,
23
- metabolicData: {
24
- baseMET: 6.0,
25
- metRange: [4.0, 8.0],
26
- compoundMultiplier: 1.5,
27
- muscleGroupFactor: 2.0,
28
- intensityScaling: "exponential",
29
- epocFactor: 0.15,
30
- },
31
- timingGuardrails: {
32
- type: "weight-reps",
33
- stressRestBonus: 5,
34
- fatigueMultiplier: 1.2,
35
- setupTypicalSecs: 10,
36
- restPeriods: {
37
- minimum: 60,
38
- typical: 120,
39
- maximum: 300,
40
- optimalRange: [90, 180],
41
- },
42
- singleRep: {
43
- min: 2.0,
44
- max: 5.0,
45
- typical: 3.5,
46
- },
47
- },
48
- youtubeVideoUrl: ["https://youtube.com/watch?v=mock1"],
49
- modelVideoUrl: "https://example.com/mock-video.mp4",
50
- thumbnailUrl: "https://example.com/mock-thumb.png",
51
- instructionsHtml: "<ul><li>Mock instruction 1</li></ul>",
52
- importantTipsHtml: "<ul><li>Mock tip 1</li></ul>",
53
- popularityIndex: 90,
54
- };
55
- /**
56
- * MOCK EXERCISE: Reps-Only
57
- * Simulates a bodyweight movement (e.g., Push-up).
58
- */
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
- type: "reps-only",
61
- stressRestBonus: 2,
62
- fatigueMultiplier: 1.05,
63
- setupTypicalSecs: 5,
64
- restPeriods: {
65
- minimum: 30,
66
- typical: 60,
67
- maximum: 180,
68
- optimalRange: [45, 90],
69
- },
70
- singleRep: {
71
- min: 1.0,
72
- max: 3.0,
73
- typical: 1.8,
74
- },
75
- } });
76
- /**
77
- * MOCK EXERCISE: Duration
78
- * Simulates an isometric hold (e.g., Plank).
79
- */
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
- type: "duration",
82
- stressRestBonus: 3,
83
- fatigueMultiplier: 1.1,
84
- setupTypicalSecs: 5,
85
- restPeriods: {
86
- minimum: 30,
87
- typical: 60,
88
- maximum: 120,
89
- optimalRange: [45, 60],
90
- },
91
- setDuration: {
92
- min: 15,
93
- max: 300,
94
- typical: 60,
95
- },
96
- } });
97
- /**
98
- * MOCK EXERCISE: Cardio-Machine
99
- * Simulates a machine cardio session (e.g., Treadmill).
100
- */
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
- type: "cardio-machine",
103
- stressRestBonus: 0,
104
- fatigueMultiplier: 1.0,
105
- setupTypicalSecs: 15,
106
- restPeriods: {
107
- minimum: 0,
108
- typical: 0,
109
- maximum: 300,
110
- optimalRange: [0, 60],
111
- },
112
- } });
113
- /**
114
- * MOCK EXERCISE: Cardio-Free
115
- * Simulates an outdoor/untracked cardio session (e.g., Outdoor Run).
116
- */
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
- type: "cardio-free",
119
- stressRestBonus: 0,
120
- fatigueMultiplier: 1.0,
121
- setupTypicalSecs: 0,
122
- restPeriods: {
123
- minimum: 0,
124
- typical: 0,
125
- maximum: 300,
126
- optimalRange: [0, 0],
127
- },
128
- } });
129
- /**
130
- * Helper dictionary containing all mock exercises mapped by their ID.
131
- */
132
- exports.mockExercisesDictionary = {
133
- [exports.mockExerciseWeightReps.exerciseId]: exports.mockExerciseWeightReps,
134
- [exports.mockExerciseRepsOnly.exerciseId]: exports.mockExerciseRepsOnly,
135
- [exports.mockExerciseDuration.exerciseId]: exports.mockExerciseDuration,
136
- [exports.mockExerciseCardioMachine.exerciseId]: exports.mockExerciseCardioMachine,
137
- [exports.mockExerciseCardioFree.exerciseId]: exports.mockExerciseCardioFree,
138
- };
@@ -1,9 +0,0 @@
1
- import { TProPlan, TUserMetric, TExercise } from "../types";
2
- /**
3
- * Calculates the BMI (Body Mass Index).
4
- */
5
- export declare const calculateBMI: (weightKg: number, heightCm: number) => number;
6
- /**
7
- * The Adoption Engine: Safely scales a Master Pro Plan to fit an individual user's body type.
8
- */
9
- export declare const scaleProPlan: (masterPlan: TProPlan, userMetric: TUserMetric, exercisesDictionary: Record<string, TExercise>) => TProPlan;
@@ -1,139 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.scaleProPlan = exports.calculateBMI = void 0;
4
- const NO_THRESHOLD = Infinity;
5
- /**
6
- * Calculates the BMI (Body Mass Index).
7
- */
8
- const calculateBMI = (weightKg, heightCm) => {
9
- if (!weightKg || !heightCm || heightCm <= 0)
10
- return 0;
11
- const heightM = heightCm / 100;
12
- return Number((weightKg / (heightM * heightM)).toFixed(1));
13
- };
14
- exports.calculateBMI = calculateBMI;
15
- /**
16
- * Rounds a calculated weight load to the nearest sensible gym increment (e.g. 2.5kg).
17
- */
18
- const roundToNearest = (val, increment) => Math.round(val / increment) * increment;
19
- /**
20
- * Calculates effective BMI threshold based on the user's fitness activity level.
21
- * Active users carry more muscle, so their BMI threshold for bodyweight safety triggers is raised.
22
- */
23
- const getAdjustedBMIThreshold = (baseThreshold, activityLevel) => {
24
- if (!baseThreshold)
25
- return NO_THRESHOLD;
26
- switch (activityLevel) {
27
- case "very-active":
28
- return baseThreshold + 5; // e.g. Obese trigger moves from 30 to 35
29
- case "moderately-active":
30
- return baseThreshold + 3; // e.g. Obese trigger moves from 30 to 33
31
- case "lightly-active":
32
- return baseThreshold + 1;
33
- case "sedentary":
34
- default:
35
- return baseThreshold; // No adjustment
36
- }
37
- };
38
- /**
39
- * Modifies an individual record based on fallback logic (when no regression exercise exists, or for softer triggers).
40
- * Reduces volume or duration by roughly 30%.
41
- */
42
- const applyFallbackScalingToRecord = (record) => {
43
- const scaledRecord = Object.assign({}, record);
44
- const SCALE_FACTOR = 0.7;
45
- // Scale reps
46
- if (scaledRecord.type === "reps-only" || scaledRecord.type === "weight-reps") {
47
- const currentReps = parseInt(scaledRecord.reps, 10);
48
- if (!isNaN(currentReps) && currentReps > 0) {
49
- scaledRecord.reps = Math.max(1, Math.round(currentReps * SCALE_FACTOR)).toString();
50
- }
51
- }
52
- // Scale duration for all time-based records
53
- if (scaledRecord.type === "duration" ||
54
- scaledRecord.type === "cardio-machine" ||
55
- scaledRecord.type === "cardio-free") {
56
- const [mm, ss] = scaledRecord.durationMmSs.split(":").map(Number);
57
- if (!isNaN(mm) && !isNaN(ss)) {
58
- const totalSecs = mm * 60 + ss;
59
- const newSecs = Math.max(5, Math.round(totalSecs * SCALE_FACTOR)); // minimum 5 seconds
60
- const newMm = Math.floor(newSecs / 60).toString();
61
- const newMmPadded = newMm.length < 2 ? "0" + newMm : newMm;
62
- const newSs = (newSecs % 60).toString();
63
- const newSsPadded = newSs.length < 2 ? "0" + newSs : newSs;
64
- scaledRecord.durationMmSs = `${newMmPadded}:${newSsPadded}`;
65
- }
66
- }
67
- return scaledRecord;
68
- };
69
- /**
70
- * The Adoption Engine: Safely scales a Master Pro Plan to fit an individual user's body type.
71
- */
72
- const scaleProPlan = (masterPlan, userMetric, exercisesDictionary) => {
73
- // We don't mutate the original
74
- const tailoredPlan = JSON.parse(JSON.stringify(masterPlan));
75
- const { weightKg, heightCm, gender, fitnessLevel } = userMetric;
76
- const bmi = weightKg && heightCm ? (0, exports.calculateBMI)(weightKg, heightCm) : 0;
77
- tailoredPlan.days.forEach((day) => {
78
- day.exercises = day.exercises.map((templateExercise) => {
79
- const exerciseMeta = exercisesDictionary[templateExercise.exerciseId];
80
- // If we can't find metadata, return as-is
81
- if (!exerciseMeta)
82
- return templateExercise;
83
- let isModified = false;
84
- let newExerciseId = templateExercise.exerciseId;
85
- let newRecords = [...templateExercise.initialRecords];
86
- // 1. Check Safety Triggers (BMI & Bodyweight Dependency)
87
- if (bmi > 0 &&
88
- (exerciseMeta.bodyweightDependency === "high" || exerciseMeta.bodyweightDependency === "medium") &&
89
- exerciseMeta.bmiThresholds) {
90
- const obeseTrigger = getAdjustedBMIThreshold(exerciseMeta.bmiThresholds.obeseTrigger, fitnessLevel);
91
- const overweightTrigger = getAdjustedBMIThreshold(exerciseMeta.bmiThresholds.overweightTrigger, fitnessLevel);
92
- if (bmi >= obeseTrigger) {
93
- // Tier 1: Hard swap to regression (if available) AND scale down volume
94
- newRecords = newRecords.map(applyFallbackScalingToRecord);
95
- isModified = true;
96
- if (exerciseMeta.regressionExerciseId && exercisesDictionary[exerciseMeta.regressionExerciseId]) {
97
- newExerciseId = exerciseMeta.regressionExerciseId;
98
- }
99
- }
100
- else if (bmi >= overweightTrigger) {
101
- // Tier 2: Softer response. Keep the exercise, but scale down the volume/duration.
102
- newRecords = newRecords.map(applyFallbackScalingToRecord);
103
- isModified = true;
104
- }
105
- }
106
- // 2. Check Load Multipliers
107
- if (weightKg && exerciseMeta.weightMultiplier && newRecords.length > 0) {
108
- const multiplier = gender === "male"
109
- ? exerciseMeta.weightMultiplier.male
110
- : gender === "female"
111
- ? exerciseMeta.weightMultiplier.female
112
- : exerciseMeta.weightMultiplier.default;
113
- if (multiplier && multiplier > 0) {
114
- // Round to nearest 2.5kg to match real-world gym equipment
115
- const suggestedLoadKg = roundToNearest(weightKg * multiplier, 2.5);
116
- newRecords = newRecords.map((record) => {
117
- if (record.type === "weight-reps") {
118
- isModified = true;
119
- return Object.assign(Object.assign({}, record), { kg: suggestedLoadKg.toString() });
120
- }
121
- // Guard against applying load to a falsy "0" aux weight
122
- if (record.type === "reps-only" || record.type === "duration" || record.type === "cardio-free") {
123
- const currentAux = parseFloat(record.auxWeightKg || "0");
124
- if (!isNaN(currentAux) && currentAux > 0) {
125
- isModified = true;
126
- return Object.assign(Object.assign({}, record), { auxWeightKg: suggestedLoadKg.toString() });
127
- }
128
- }
129
- return record;
130
- });
131
- }
132
- }
133
- // Return the tailored template exercise
134
- return Object.assign(Object.assign({}, templateExercise), { exerciseId: newExerciseId, initialRecords: newRecords, isAutoScaled: isModified || undefined });
135
- });
136
- });
137
- return tailoredPlan;
138
- };
139
- exports.scaleProPlan = scaleProPlan;
@@ -1,67 +0,0 @@
1
- /**
2
- * ============================================================================
3
- * FITFRIX EXERCISE SCORING SYSTEM — Pillar 1: Calorie Burn
4
- * ============================================================================
5
- *
6
- * Estimates energy expenditure (kcal) for an exercise using the MET system.
7
- *
8
- * Formula (per set):
9
- * calories = effectiveMET × userWeightKg × (activeDurationSecs / 3600)
10
- *
11
- * Where effectiveMET is the exercise's baseMET adjusted for:
12
- * - Effort level (RPE/RIR → scales within metRange)
13
- * - Weight intensity (for weight-reps: how heavy relative to bodyweight)
14
- * - Duration category (for duration: short/medium/long multiplier)
15
- * - Speed (for cardio: interpolated from pace or speed range)
16
- * - Compound multiplier (multi-joint exercises burn more)
17
- *
18
- * After summing all sets:
19
- * - Rest calories are added (elevated MET during rest periods)
20
- * - EPOC (afterburn) is added as a percentage of work calories
21
- *
22
- * References:
23
- * - Ainsworth BE et al. "Compendium of Physical Activities" (2011)
24
- * - Katch, McArdle & Katch, "Exercise Physiology" (8th ed.)
25
- */
26
- import type { IParsedSet, IUserContext } from "./types";
27
- interface IMetabolicData {
28
- baseMET: number;
29
- metRange: [number, number];
30
- compoundMultiplier: number;
31
- muscleGroupFactor: number;
32
- intensityScaling: "linear" | "exponential" | "plateau";
33
- epocFactor: number;
34
- weightFactors?: {
35
- lightWeight: number;
36
- moderateWeight: number;
37
- heavyWeight: number;
38
- };
39
- durationFactors?: {
40
- shortDuration: number;
41
- mediumDuration: number;
42
- longDuration: number;
43
- };
44
- paceFactors?: Record<string, number>;
45
- lightWeight?: number;
46
- moderateWeight?: number;
47
- heavyWeight?: number;
48
- shortDuration?: number;
49
- mediumDuration?: number;
50
- longDuration?: number;
51
- }
52
- /**
53
- * Calculate total calorie burn for an exercise.
54
- *
55
- * @param sets Parsed & validated sets (from parseRecords)
56
- * @param metabolicData Exercise's metabolic configuration
57
- * @param user Validated user context
58
- * @param difficultyLevel Exercise difficulty (0–4), used for bodyweight estimation
59
- * @returns Total calories burned (kcal), rounded to 1 decimal
60
- *
61
- * @example
62
- * const calories = calculateCalories(parsedSets, exercise.metabolicData, userCtx, exercise.difficultyLevel);
63
- * // → 6.4 (for 3 light bench press sets)
64
- * // → 142.3 (for a 20-min treadmill run)
65
- */
66
- export declare function calculateCalories(sets: IParsedSet[], metabolicData: IMetabolicData, user: IUserContext, difficultyLevel: number, scoringSpecialHandling?: "plyometric" | "stretch-mobility" | "continuous-duration" | "loaded-carry"): number;
67
- export {};