@dgpholdings/greatoak-shared 1.2.86 → 1.2.87

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 (75) hide show
  1. package/README.md +148 -148
  2. package/dist/__mocks__/exercises.mock.js +1 -0
  3. package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
  4. package/dist/types/TApiClientConstellation.d.ts +33 -0
  5. package/dist/types/TApiClientConstellation.js +13 -0
  6. package/dist/types/TApiExercise.d.ts +5 -3
  7. package/dist/types/index.d.ts +1 -0
  8. package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
  9. package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
  10. package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
  11. package/dist/utils/constellation/evaluateConstellation.js +135 -0
  12. package/dist/utils/constellation/index.d.ts +17 -0
  13. package/dist/utils/constellation/index.js +26 -0
  14. package/dist/utils/constellation/levelThresholds.d.ts +99 -0
  15. package/dist/utils/constellation/levelThresholds.js +123 -0
  16. package/dist/utils/constellation/starFoundation.d.ts +25 -0
  17. package/dist/utils/constellation/starFoundation.js +54 -0
  18. package/dist/utils/constellation/stars/consistency.d.ts +29 -0
  19. package/dist/utils/constellation/stars/consistency.js +142 -0
  20. package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
  21. package/dist/utils/constellation/stars/lowerBody.js +30 -0
  22. package/dist/utils/constellation/stars/pull.d.ts +11 -0
  23. package/dist/utils/constellation/stars/pull.js +24 -0
  24. package/dist/utils/constellation/stars/push.d.ts +11 -0
  25. package/dist/utils/constellation/stars/push.js +24 -0
  26. package/dist/utils/constellation/stars/quality.d.ts +19 -0
  27. package/dist/utils/constellation/stars/quality.js +98 -0
  28. package/dist/utils/constellation/stars/recovery.d.ts +29 -0
  29. package/dist/utils/constellation/stars/recovery.js +169 -0
  30. package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
  31. package/dist/utils/constellation/strengthStarHelpers.js +104 -0
  32. package/dist/utils/constellation/types.d.ts +124 -0
  33. package/dist/utils/constellation/types.js +18 -0
  34. package/dist/utils/index.d.ts +5 -3
  35. package/dist/utils/index.js +1 -0
  36. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
  37. package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
  38. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
  39. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
  40. package/dist/utils/scoringWorkout/constants.d.ts +20 -6
  41. package/dist/utils/scoringWorkout/constants.js +23 -9
  42. package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
  43. package/dist/utils/scoringWorkout/helpers.js +24 -18
  44. package/dist/utils/scoringWorkout/index.d.ts +12 -8
  45. package/dist/utils/scoringWorkout/index.js +23 -15
  46. package/dist/utils/scoringWorkout/parseRecords.js +4 -3
  47. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +210 -172
  48. package/dist/utils/scoringWorkout/types.d.ts +34 -14
  49. package/package.json +31 -31
  50. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
  51. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
  52. package/dist/utils/scaleProPlan.util.d.ts +0 -9
  53. package/dist/utils/scaleProPlan.util.js +0 -139
  54. package/dist/utils/scoring/calculateCalories.d.ts +0 -67
  55. package/dist/utils/scoring/calculateCalories.js +0 -345
  56. package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
  57. package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
  58. package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
  59. package/dist/utils/scoring/calculateQualityScore.js +0 -334
  60. package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
  61. package/dist/utils/scoring/calculateTotalVolume.js +0 -73
  62. package/dist/utils/scoring/constants.d.ts +0 -211
  63. package/dist/utils/scoring/constants.js +0 -247
  64. package/dist/utils/scoring/helpers.d.ts +0 -119
  65. package/dist/utils/scoring/helpers.js +0 -229
  66. package/dist/utils/scoring/index.d.ts +0 -28
  67. package/dist/utils/scoring/index.js +0 -47
  68. package/dist/utils/scoring/parseRecords.d.ts +0 -98
  69. package/dist/utils/scoring/parseRecords.js +0 -284
  70. package/dist/utils/scoring/types.d.ts +0 -86
  71. package/dist/utils/scoring/types.js +0 -11
  72. package/dist/utils/scoring.utils.d.ts +0 -14
  73. package/dist/utils/scoring.utils.js +0 -243
  74. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
  75. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.js → calculateMuscleFatigue.js} +0 -0
package/README.md CHANGED
@@ -1,149 +1,149 @@
1
- # @dgpholdings/greatoak-shared
2
-
3
- The shared library for the Fitfrix ecosystem. Contains all types, utilities, and the scoring engine used across:
4
-
5
- - `v2/fitfrix` — the mobile app (React Native / Expo)
6
- - `backend/api-service` — the NestJS backend
7
- - `fitfrix.com` — the web/landing (Vite + React)
8
-
9
- Any type, constant, or utility that is used by more than one package lives here. If you're about to define a type locally in a consuming app — check here first.
10
-
11
- ---
12
-
13
- ## Install / Import
14
-
15
- ```ts
16
- // Types
17
- import type { TExercise, TRecord, TUserMetric } from '@dgpholdings/greatoak-shared';
18
-
19
- // Utils
20
- import { calculateExerciseScoreV2, calculateTotalVolume, isDefined, toError } from '@dgpholdings/greatoak-shared';
21
- ```
22
-
23
- ---
24
-
25
- ## What's Inside
26
-
27
- | Category | Location | Doc |
28
- |---|---|---|
29
- | **Types** | `src/types/` | [docs/types.md](docs/types.md) |
30
- | **Utils** | `src/utils/` | [docs/utils.md](docs/utils.md) |
31
- | **Scoring Engine** | `src/utils/scoring/` | [docs/scoring.md](docs/scoring.md) |
32
-
33
- ---
34
-
35
- ## Types — Quick Reference
36
-
37
- | File | What it defines |
38
- |---|---|
39
- | `commonTypes.ts` | `TRecord`, `TExerciseConfig`, `TTemplateExercise`, `TGdprData`, `TDayKey` |
40
- | `TApiExercise.ts` | `TExercise`, `TBodyPart`, `EBodyParts`, `TTrainingType`, `TTimingGuardrails` |
41
- | `TApiUser.ts` | `TUserMetric`, `TFitnessGoal`, `TUserType`, `TSubscriptionStatus`, `TGender` |
42
- | `TApiAuth.ts` | Signup/signin request & response types (`TApiSignupAnonymousReq`, `TSignInRes`, etc.) |
43
- | `TApiExerciseRecord.ts` | Record save/fetch request & response types |
44
- | `TApiTemplateData.ts` | `TTemplate`, `TTemplateDb`, `TTemplateData`, `TExerciseLatestRecord` |
45
- | `TApiProPlan.ts` | `TProPlan`, pro plan CRUD request & response types |
46
- | `TApiTemplateShop.ts` | `TTemplateShopDb`, shop CRUD & shared plan types |
47
- | `TApiBillingPlan.ts` | `TBillingPlan`, `TBillingCountries`, billing API types |
48
- | `TApiRevenueCat.ts` | `TApiRevenueCatWebhookReq`, RevenueCat event types |
49
- | `TApiClientProgress.ts` | `TClientWorkoutHistory`, trainer dashboard types |
50
-
51
- ---
52
-
53
- ## Utils — Quick Reference
54
-
55
- | Export | What it does |
56
- |---|---|
57
- | `calculateExerciseScoreV2` | Full exercise scoring → `{ score, muscleScores }` |
58
- | `calculateTotalVolume` | Total workout volume for charts |
59
- | `isDefined` / `isDefinedNumber` | Null/undefined type guards |
60
- | `toError` | Converts unknown catch values to `Error` |
61
- | `toNumber` | Safe string/number → `number \| undefined` |
62
- | `mmssToSecs` | `"MM:SS"` → seconds |
63
- | `getDaysAndHoursDifference` | Date diff → `{ days, hours }` |
64
- | `isUserAllowedToUpdate` | Profile update rate-limit guard |
65
- | `countryToCurrencyCode` | Country code → ISO currency string |
66
- | `slugifyText` | Text → URL-safe slug |
67
- | `generatePlanCode` | Generates a unique 9-char Crockford Base32 plan code |
68
- | `maskEmail` / `isEmail` / `isAnonymousEmail` | Email utilities |
69
- | `NOOP` | Empty function `() => {}` |
70
-
71
- ---
72
-
73
- ## Source Structure
74
-
75
- ```
76
- shared/
77
- ├── src/
78
- │ ├── types/
79
- │ │ ├── index.ts # Re-exports all types
80
- │ │ ├── commonTypes.ts # TRecord, TExerciseConfig, TTemplateExercise, TGdprData, TDayKey
81
- │ │ ├── TApiExercise.ts # TExercise and related
82
- │ │ ├── TApiUser.ts # TUserMetric and related
83
- │ │ ├── TApiAuth.ts # Auth flows
84
- │ │ ├── TApiExerciseRecord.ts # Workout record save/fetch
85
- │ │ ├── TApiTemplateData.ts # User workout templates
86
- │ │ ├── TApiProPlan.ts # Pro/trainer plans
87
- │ │ ├── TApiTemplateShop.ts # Template shop (legacy, see TApiProPlan)
88
- │ │ ├── TApiBillingPlan.ts # Billing plans
89
- │ │ ├── TApiRevenueCat.ts # In-app purchase webhooks
90
- │ │ └── TApiClientProgress.ts # Trainer client tracking
91
- │ └── utils/
92
- │ ├── index.ts # Re-exports all utils
93
- │ ├── billing.utils.ts
94
- │ ├── email.utils.ts
95
- │ ├── isDefined.utils.ts
96
- │ ├── noop.utils.ts
97
- │ ├── number.util.ts
98
- │ ├── planCode.util.ts
99
- │ ├── slugify.util.ts
100
- │ ├── time.util.ts
101
- │ ├── toError.util.ts
102
- │ └── scoring/ # Exercise scoring engine
103
- │ └── README.md # Full scoring documentation
104
- ├── docs/
105
- │ ├── types.md # All types documented in detail
106
- │ ├── utils.md # All utils documented with examples
107
- │ └── scoring.md # Scoring system overview
108
- └── README.md # This file
109
- ```
110
-
111
- ---
112
-
113
- ## Key Design Rules
114
-
115
- - **No local duplication** — if a type or utility is needed by ≥2 packages, it belongs here
116
- - **No framework code** — this package must stay framework-agnostic (no React, no NestJS decorators)
117
- - **Strict types** — no `any`. Use generics, discriminated unions, or `unknown`
118
- - **Scoring is the business core** — the scoring engine drives the muscle fatigue diagram and progress charts. Read [docs/scoring.md](docs/scoring.md) before touching it
119
-
120
-
121
- ## Renewing token
122
-
123
- 1. Login using the token (recommended way)
124
-
125
- In your Git Bash, run:
126
-
127
- `npm login`
128
-
129
- It will prompt:
130
-
131
- Username:
132
- Password:
133
- Email:
134
-
135
- Use this:
136
-
137
- Username → your npm username
138
-
139
- Password → paste the granular token (NOT your npm password)
140
-
141
- Email → your npm email
142
-
143
- After that, npm stores the token in:
144
-
145
- `shared\.npmrc`
146
-
147
- Then publishing works normally:
148
-
1
+ # @dgpholdings/greatoak-shared
2
+
3
+ The shared library for the Fitfrix ecosystem. Contains all types, utilities, and the scoring engine used across:
4
+
5
+ - `v2/fitfrix` — the mobile app (React Native / Expo)
6
+ - `backend/api-service` — the NestJS backend
7
+ - `fitfrix.com` — the web/landing (Vite + React)
8
+
9
+ Any type, constant, or utility that is used by more than one package lives here. If you're about to define a type locally in a consuming app — check here first.
10
+
11
+ ---
12
+
13
+ ## Install / Import
14
+
15
+ ```ts
16
+ // Types
17
+ import type { TExercise, TRecord, TUserMetric } from '@dgpholdings/greatoak-shared';
18
+
19
+ // Utils
20
+ import { calculateExerciseScoreV2, calculateTotalVolume, isDefined, toError } from '@dgpholdings/greatoak-shared';
21
+ ```
22
+
23
+ ---
24
+
25
+ ## What's Inside
26
+
27
+ | Category | Location | Doc |
28
+ |---|---|---|
29
+ | **Types** | `src/types/` | [docs/types.md](docs/types.md) |
30
+ | **Utils** | `src/utils/` | [docs/utils.md](docs/utils.md) |
31
+ | **Scoring Engine** | `src/utils/scoring/` | [docs/scoring.md](docs/scoring.md) |
32
+
33
+ ---
34
+
35
+ ## Types — Quick Reference
36
+
37
+ | File | What it defines |
38
+ |---|---|
39
+ | `commonTypes.ts` | `TRecord`, `TExerciseConfig`, `TTemplateExercise`, `TGdprData`, `TDayKey` |
40
+ | `TApiExercise.ts` | `TExercise`, `TBodyPart`, `EBodyParts`, `TTrainingType`, `TTimingGuardrails` |
41
+ | `TApiUser.ts` | `TUserMetric`, `TFitnessGoal`, `TUserType`, `TSubscriptionStatus`, `TGender` |
42
+ | `TApiAuth.ts` | Signup/signin request & response types (`TApiSignupAnonymousReq`, `TSignInRes`, etc.) |
43
+ | `TApiExerciseRecord.ts` | Record save/fetch request & response types |
44
+ | `TApiTemplateData.ts` | `TTemplate`, `TTemplateDb`, `TTemplateData`, `TExerciseLatestRecord` |
45
+ | `TApiProPlan.ts` | `TProPlan`, pro plan CRUD request & response types |
46
+ | `TApiTemplateShop.ts` | `TTemplateShopDb`, shop CRUD & shared plan types |
47
+ | `TApiBillingPlan.ts` | `TBillingPlan`, `TBillingCountries`, billing API types |
48
+ | `TApiRevenueCat.ts` | `TApiRevenueCatWebhookReq`, RevenueCat event types |
49
+ | `TApiClientProgress.ts` | `TClientWorkoutHistory`, trainer dashboard types |
50
+
51
+ ---
52
+
53
+ ## Utils — Quick Reference
54
+
55
+ | Export | What it does |
56
+ |---|---|
57
+ | `calculateExerciseScoreV2` | Full exercise scoring → `{ score, muscleScores }` |
58
+ | `calculateTotalVolume` | Total workout volume for charts |
59
+ | `isDefined` / `isDefinedNumber` | Null/undefined type guards |
60
+ | `toError` | Converts unknown catch values to `Error` |
61
+ | `toNumber` | Safe string/number → `number \| undefined` |
62
+ | `mmssToSecs` | `"MM:SS"` → seconds |
63
+ | `getDaysAndHoursDifference` | Date diff → `{ days, hours }` |
64
+ | `isUserAllowedToUpdate` | Profile update rate-limit guard |
65
+ | `countryToCurrencyCode` | Country code → ISO currency string |
66
+ | `slugifyText` | Text → URL-safe slug |
67
+ | `generatePlanCode` | Generates a unique 9-char Crockford Base32 plan code |
68
+ | `maskEmail` / `isEmail` / `isAnonymousEmail` | Email utilities |
69
+ | `NOOP` | Empty function `() => {}` |
70
+
71
+ ---
72
+
73
+ ## Source Structure
74
+
75
+ ```
76
+ shared/
77
+ ├── src/
78
+ │ ├── types/
79
+ │ │ ├── index.ts # Re-exports all types
80
+ │ │ ├── commonTypes.ts # TRecord, TExerciseConfig, TTemplateExercise, TGdprData, TDayKey
81
+ │ │ ├── TApiExercise.ts # TExercise and related
82
+ │ │ ├── TApiUser.ts # TUserMetric and related
83
+ │ │ ├── TApiAuth.ts # Auth flows
84
+ │ │ ├── TApiExerciseRecord.ts # Workout record save/fetch
85
+ │ │ ├── TApiTemplateData.ts # User workout templates
86
+ │ │ ├── TApiProPlan.ts # Pro/trainer plans
87
+ │ │ ├── TApiTemplateShop.ts # Template shop (legacy, see TApiProPlan)
88
+ │ │ ├── TApiBillingPlan.ts # Billing plans
89
+ │ │ ├── TApiRevenueCat.ts # In-app purchase webhooks
90
+ │ │ └── TApiClientProgress.ts # Trainer client tracking
91
+ │ └── utils/
92
+ │ ├── index.ts # Re-exports all utils
93
+ │ ├── billing.utils.ts
94
+ │ ├── email.utils.ts
95
+ │ ├── isDefined.utils.ts
96
+ │ ├── noop.utils.ts
97
+ │ ├── number.util.ts
98
+ │ ├── planCode.util.ts
99
+ │ ├── slugify.util.ts
100
+ │ ├── time.util.ts
101
+ │ ├── toError.util.ts
102
+ │ └── scoring/ # Exercise scoring engine
103
+ │ └── README.md # Full scoring documentation
104
+ ├── docs/
105
+ │ ├── types.md # All types documented in detail
106
+ │ ├── utils.md # All utils documented with examples
107
+ │ └── scoring.md # Scoring system overview
108
+ └── README.md # This file
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Key Design Rules
114
+
115
+ - **No local duplication** — if a type or utility is needed by ≥2 packages, it belongs here
116
+ - **No framework code** — this package must stay framework-agnostic (no React, no NestJS decorators)
117
+ - **Strict types** — no `any`. Use generics, discriminated unions, or `unknown`
118
+ - **Scoring is the business core** — the scoring engine drives the muscle fatigue diagram and progress charts. Read [docs/scoring.md](docs/scoring.md) before touching it
119
+
120
+
121
+ ## Renewing token
122
+
123
+ 1. Login using the token (recommended way)
124
+
125
+ In your Git Bash, run:
126
+
127
+ `npm login`
128
+
129
+ It will prompt:
130
+
131
+ Username:
132
+ Password:
133
+ Email:
134
+
135
+ Use this:
136
+
137
+ Username → your npm username
138
+
139
+ Password → paste the granular token (NOT your npm password)
140
+
141
+ Email → your npm email
142
+
143
+ After that, npm stores the token in:
144
+
145
+ `shared\.npmrc`
146
+
147
+ Then publishing works normally:
148
+
149
149
  `npm publish`
@@ -5,6 +5,7 @@ exports.mockExercisesDictionary = exports.mockExerciseNoGuardrails = exports.moc
5
5
  * MOCK EXERCISE: Weight-Reps
6
6
  * Simulates a standard compound lift (e.g., Barbell Squat).
7
7
  */
8
+ // @ts-ignore
8
9
  exports.mockExerciseWeightReps = {
9
10
  exerciseId: "mock-exercise-weight-reps-123",
10
11
  name: "Generic Barbell Squat",
@@ -1,3 +1,4 @@
1
+ import { TAiMovementPattern } from "../constants";
1
2
  export type TConfidence = "high" | "medium" | null;
2
3
  export type TNameReviewStatus = "pending" | "approved";
3
4
  export type TQdrantSyncStatus = "pending" | "synced" | "failed";
@@ -66,7 +67,7 @@ export type TApiAiExerciseAnalysis = {
66
67
  exercise_name: string;
67
68
  alternative_names: string[];
68
69
  movement_description: string;
69
- movement_pattern: string;
70
+ movement_pattern: TAiMovementPattern;
70
71
  exercise_type: string;
71
72
  contraction_type: string;
72
73
  plane_of_motion: string[];
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ============================================================================
3
+ * API CONTRACT — Client Constellation
4
+ * ============================================================================
5
+ *
6
+ * Request/response types for GET /api/constellation. The response is the
7
+ * render-ready TConstellationState produced by the constellation util; these
8
+ * types are re-exported here so the backend (and app) consume one stable
9
+ * contract from the published package.
10
+ */
11
+ import type { TConstellationState } from "../utils/constellation/types";
12
+ export type { TConstellationState };
13
+ /**
14
+ * Request: no body. The user is resolved from the JWT. `level` is optional and
15
+ * may be supplied as a query param to preview a specific level; it defaults to
16
+ * the user's current level (1) and is clamped to a valid level server-side.
17
+ */
18
+ export type TApiClientConstellationReq = {
19
+ level?: number;
20
+ };
21
+ /**
22
+ * Response envelope, matching the shape your other controllers return
23
+ * (status + state + message) with the constellation payload under `data`.
24
+ */
25
+ export type TApiClientConstellationRes = {
26
+ status: 200;
27
+ state: "success";
28
+ data: TConstellationState;
29
+ } | {
30
+ status: 401 | 500;
31
+ state: "failed";
32
+ message: string;
33
+ };
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ // shared/src/types/TApiClientConstellation.ts
3
+ /**
4
+ * ============================================================================
5
+ * API CONTRACT — Client Constellation
6
+ * ============================================================================
7
+ *
8
+ * Request/response types for GET /api/constellation. The response is the
9
+ * render-ready TConstellationState produced by the constellation util; these
10
+ * types are re-exported here so the backend (and app) consume one stable
11
+ * contract from the published package.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,3 +1,4 @@
1
+ import { TAiMovementPattern } from "../constants";
1
2
  import { TRecord } from "./commonTypes";
2
3
  export type TBodyPart = "Chest" | "Core" | "Shoulders" | "Back" | "Biceps" | "Triceps" | "Forearms" | "Legs" | "Glutes" | "Calves" | "Hamstrings" | "Quadriceps" | "Lower Back" | "Trapezius";
3
4
  export declare enum EBodyParts {
@@ -141,9 +142,10 @@ export type TExercise = {
141
142
  female: number;
142
143
  default: number;
143
144
  };
144
- movementPattern?: "push" | "pull" | "hinge" | "squat" | "lunge" | "rotation" | "carry" | "isolation";
145
- stabilityDemand?: "low" | "moderate" | "high";
146
- progressionTier?: "beginner" | "intermediate" | "advanced" | "elite";
145
+ movementPattern: TAiMovementPattern;
146
+ exerciseType: "isolation" | "compound" | "bodyweight" | "mobility" | "plyometric" | "isometric" | "cardio";
147
+ stabilityDemand: "low" | "moderate" | "high";
148
+ progressionTier: "beginner" | "intermediate" | "advanced" | "elite";
147
149
  };
148
150
  export type TBodyPartExercises = Record<TBodyPart, TExercise[]>;
149
151
  export type TApiCreateOrUpdateExerciseReq = {
@@ -15,3 +15,4 @@ export type * from "./TApiSales";
15
15
  export type * from "./TApiAiExerciseAnalysis";
16
16
  export type * from "./TApiAdminAiHelp";
17
17
  export type * from "./TApiAiQuickStartWorkout";
18
+ export type * from "./TApiClientConstellation";
@@ -0,0 +1,48 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Normalised Load (strength-star helper)
4
+ * ============================================================================
5
+ *
6
+ * Turns a user's sessions into a single strength number for a movement pattern:
7
+ * the best estimated-1RM achieved on any matching exercise in the rolling
8
+ * window, expressed as a ratio of bodyweight.
9
+ *
10
+ * Estimated-1RM (Epley, rep-capped at 12) is used instead of raw top weight so
11
+ * that 60kg x5 ~ 70kg x1 — it rewards productive training over ego-lifting.
12
+ *
13
+ * Bodyweight loading (the home-user path): a bodyweight exercise's effective
14
+ * load is the REAL fraction of bodyweight it moves, taken from the exercise's
15
+ * own `weightMultiplier` (per-sex), falling back to a coarse map keyed off
16
+ * `bodyweightDependency`. This is what lets a home user honestly reach a
17
+ * strength threshold with pull-ups / pistol squats etc., instead of being
18
+ * capped by a flat guess.
19
+ *
20
+ * Returns a bodyweight ratio (e.g. 0.78 = "0.78x bodyweight"). 0 = no data.
21
+ */
22
+ import type { TExercise } from "../../types";
23
+ import type { TSessionRecord } from "./types";
24
+ /** Epley reps cap: reps beyond this don't increase the 1RM estimate. */
25
+ export declare const EPLEY_REP_CAP = 12;
26
+ /** Estimated 1RM via Epley, with reps capped to avoid endurance-range inflation. */
27
+ export declare function estimateOneRepMax(weightKg: number, reps: number): number;
28
+ export interface INormalisedLoadResult {
29
+ /** Best estimated-1RM in the window as a ratio of bodyweight. 0 if none. */
30
+ ratio: number;
31
+ /** The raw best estimated-1RM in kg (pre-normalisation), for detail display. */
32
+ bestOneRepMaxKg: number;
33
+ /** Exercise id that produced the best load (for "your best: Bench Press"). */
34
+ bestExerciseId?: string;
35
+ }
36
+ /**
37
+ * Compute the best normalised load for a set of matching exercises within the
38
+ * window.
39
+ *
40
+ * @param matchingExerciseIds Exercise ids belonging to the target pattern.
41
+ * @param sessions All sessions.
42
+ * @param exerciseCatalog Full catalog (unfiltered).
43
+ * @param bodyweightKg User bodyweight (falls back to default).
44
+ * @param gender User sex (selects the weightMultiplier column).
45
+ * @param windowStartMs Only sessions on/after this count.
46
+ * @param now Upper bound (sessions after `now` ignored).
47
+ */
48
+ export declare function computeNormalisedLoad(matchingExerciseIds: Set<string>, sessions: TSessionRecord[], exerciseCatalog: Record<string, TExercise>, bodyweightKg: number, gender: string | undefined, windowStartMs: number, now: number): INormalisedLoadResult;
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ // utils/constellation/computeNormalisedLoad.ts — Normalised Load (strength-star helper)
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Normalised Load (strength-star helper)
6
+ * ============================================================================
7
+ *
8
+ * Turns a user's sessions into a single strength number for a movement pattern:
9
+ * the best estimated-1RM achieved on any matching exercise in the rolling
10
+ * window, expressed as a ratio of bodyweight.
11
+ *
12
+ * Estimated-1RM (Epley, rep-capped at 12) is used instead of raw top weight so
13
+ * that 60kg x5 ~ 70kg x1 — it rewards productive training over ego-lifting.
14
+ *
15
+ * Bodyweight loading (the home-user path): a bodyweight exercise's effective
16
+ * load is the REAL fraction of bodyweight it moves, taken from the exercise's
17
+ * own `weightMultiplier` (per-sex), falling back to a coarse map keyed off
18
+ * `bodyweightDependency`. This is what lets a home user honestly reach a
19
+ * strength threshold with pull-ups / pistol squats etc., instead of being
20
+ * capped by a flat guess.
21
+ *
22
+ * Returns a bodyweight ratio (e.g. 0.78 = "0.78x bodyweight"). 0 = no data.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.EPLEY_REP_CAP = void 0;
26
+ exports.estimateOneRepMax = estimateOneRepMax;
27
+ exports.computeNormalisedLoad = computeNormalisedLoad;
28
+ // --- Tunables -------------------------------------------------------------
29
+ /** Epley reps cap: reps beyond this don't increase the 1RM estimate. */
30
+ exports.EPLEY_REP_CAP = 12;
31
+ /** Default bodyweight when the user profile lacks one. */
32
+ const DEFAULT_BODYWEIGHT_KG = 70;
33
+ /**
34
+ * Fallback bodyweight-load fractions when an exercise has no `weightMultiplier`.
35
+ * Keyed off `bodyweightDependency`: how much of your own body the move loads.
36
+ * high ~ pull-ups, dips, pistols (most of bodyweight)
37
+ * medium ~ lunges, inverted rows (about half)
38
+ * low ~ braced cable work (little)
39
+ * none ~ machine/bar carries the load — bodyweight isn't the resistance
40
+ */
41
+ const BW_DEPENDENCY_FRACTION = {
42
+ high: 0.9,
43
+ medium: 0.6,
44
+ low: 0.3,
45
+ none: 0,
46
+ };
47
+ // --- Epley -----------------------------------------------------------------
48
+ /**
49
+ * Parse a numeric string from a record, guarding against NaN and negatives.
50
+ * parseFloat("abc") is NaN; a logged weight/reps should never be negative.
51
+ */
52
+ function safeParseFloat(value, fallback = 0) {
53
+ const parsed = parseFloat(value !== null && value !== void 0 ? value : "");
54
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
55
+ }
56
+ /** Estimated 1RM via Epley, with reps capped to avoid endurance-range inflation. */
57
+ function estimateOneRepMax(weightKg, reps) {
58
+ if (weightKg <= 0 || reps <= 0)
59
+ return 0;
60
+ const cappedReps = Math.min(reps, exports.EPLEY_REP_CAP);
61
+ return weightKg * (1 + cappedReps / 30);
62
+ }
63
+ // --- Bodyweight load fraction ----------------------------------------------
64
+ /**
65
+ * The fraction of bodyweight a bodyweight/reps-only exercise loads onto the
66
+ * target muscles. Prefers the exercise's own per-sex `weightMultiplier`; falls
67
+ * back to the `bodyweightDependency` map; final fallback is a mild 0.5.
68
+ */
69
+ function bodyweightLoadFraction(exercise, gender) {
70
+ const wm = exercise.weightMultiplier;
71
+ if (wm) {
72
+ if (gender === "female")
73
+ return wm.female;
74
+ if (gender === "male")
75
+ return wm.male;
76
+ return wm.default;
77
+ }
78
+ const dep = exercise.bodyweightDependency;
79
+ if (dep && dep in BW_DEPENDENCY_FRACTION)
80
+ return BW_DEPENDENCY_FRACTION[dep];
81
+ return 0.5;
82
+ }
83
+ // --- Per-set effective load ------------------------------------------------
84
+ /**
85
+ * Best estimated-1RM contribution of a single completed set, in kg.
86
+ * Switches on the TRecord discriminated union (no casts). Non-strength record
87
+ * types (duration, cardio) contribute 0.
88
+ */
89
+ function setEstimatedLoadKg(set, exercise, bodyweightKg, gender) {
90
+ if (!set.isDone)
91
+ return 0;
92
+ if (set.type === "weight-reps") {
93
+ const kg = safeParseFloat(set.kg);
94
+ const reps = safeParseFloat(set.reps);
95
+ // Unilateral: weight is logged per side -> double for total mechanical load.
96
+ const totalKg = kg * (exercise.isUnilateral ? 2 : 1);
97
+ return estimateOneRepMax(totalKg, reps);
98
+ }
99
+ if (set.type === "reps-only") {
100
+ const reps = safeParseFloat(set.reps);
101
+ const auxKg = safeParseFloat(set.auxWeightKg);
102
+ const fraction = bodyweightLoadFraction(exercise, gender);
103
+ const bodyweightLoad = fraction * bodyweightKg;
104
+ // Added weight (weighted pull-ups/dips) stacks on the bodyweight load.
105
+ const effectiveWeight = bodyweightLoad + auxKg;
106
+ return estimateOneRepMax(effectiveWeight, reps);
107
+ }
108
+ // duration / cardio-machine / cardio-free are not strength loads.
109
+ return 0;
110
+ }
111
+ /**
112
+ * Compute the best normalised load for a set of matching exercises within the
113
+ * window.
114
+ *
115
+ * @param matchingExerciseIds Exercise ids belonging to the target pattern.
116
+ * @param sessions All sessions.
117
+ * @param exerciseCatalog Full catalog (unfiltered).
118
+ * @param bodyweightKg User bodyweight (falls back to default).
119
+ * @param gender User sex (selects the weightMultiplier column).
120
+ * @param windowStartMs Only sessions on/after this count.
121
+ * @param now Upper bound (sessions after `now` ignored).
122
+ */
123
+ function computeNormalisedLoad(matchingExerciseIds, sessions, exerciseCatalog, bodyweightKg, gender, windowStartMs, now) {
124
+ const bw = bodyweightKg > 0 ? bodyweightKg : DEFAULT_BODYWEIGHT_KG;
125
+ let bestKg = 0;
126
+ let bestExerciseId;
127
+ for (const session of sessions) {
128
+ if (session.date < windowStartMs || session.date > now)
129
+ continue;
130
+ for (const ex of session.exercises) {
131
+ if (!matchingExerciseIds.has(ex.exerciseId))
132
+ continue;
133
+ const exercise = exerciseCatalog[ex.exerciseId];
134
+ if (!exercise)
135
+ continue;
136
+ for (const set of ex.records) {
137
+ const loadKg = setEstimatedLoadKg(set, exercise, bw, gender);
138
+ if (loadKg > bestKg) {
139
+ bestKg = loadKg;
140
+ bestExerciseId = ex.exerciseId;
141
+ }
142
+ }
143
+ }
144
+ }
145
+ return {
146
+ ratio: bestKg / bw,
147
+ bestOneRepMaxKg: bestKg,
148
+ bestExerciseId,
149
+ };
150
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Orchestrator
4
+ * ============================================================================
5
+ *
6
+ * The single entry point. Takes raw input (sessions, catalog, user, now) and
7
+ * returns a render-ready TConstellationState: six resolved stars plus the
8
+ * figure aggregate that drives the silhouette's aura.
9
+ *
10
+ * Purely live — everything is recomputed from raw records on every call.
11
+ * Nothing is persisted; the same input always yields the same output.
12
+ *
13
+ * Scoring happens ONCE here: every (session, exercise) is scored a single time
14
+ * into scoredSessions, which both quality and recovery read — no double work.
15
+ *
16
+ * Runs server-side (Lambda) so the evaluation logic can evolve independently of
17
+ * app releases.
18
+ */
19
+ import type { TConstellationInput, TConstellationState } from "./types";
20
+ /**
21
+ * Evaluate the full constellation from raw input.
22
+ *
23
+ * @param input sessions + exercise catalog (UNFILTERED) + user + now + level
24
+ * @returns render-ready state: six stars + figure aggregate
25
+ * @throws if `now` is not a finite timestamp
26
+ */
27
+ export declare function evaluateConstellation(input: TConstellationInput): TConstellationState;