@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.
- package/README.md +148 -148
- package/dist/__mocks__/exercises.mock.js +1 -0
- package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
- package/dist/types/TApiClientConstellation.d.ts +33 -0
- package/dist/types/TApiClientConstellation.js +13 -0
- package/dist/types/TApiExercise.d.ts +5 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
- package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
- package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
- package/dist/utils/constellation/evaluateConstellation.js +135 -0
- package/dist/utils/constellation/index.d.ts +17 -0
- package/dist/utils/constellation/index.js +26 -0
- package/dist/utils/constellation/levelThresholds.d.ts +99 -0
- package/dist/utils/constellation/levelThresholds.js +123 -0
- package/dist/utils/constellation/starFoundation.d.ts +25 -0
- package/dist/utils/constellation/starFoundation.js +54 -0
- package/dist/utils/constellation/stars/consistency.d.ts +29 -0
- package/dist/utils/constellation/stars/consistency.js +142 -0
- package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
- package/dist/utils/constellation/stars/lowerBody.js +30 -0
- package/dist/utils/constellation/stars/pull.d.ts +11 -0
- package/dist/utils/constellation/stars/pull.js +24 -0
- package/dist/utils/constellation/stars/push.d.ts +11 -0
- package/dist/utils/constellation/stars/push.js +24 -0
- package/dist/utils/constellation/stars/quality.d.ts +19 -0
- package/dist/utils/constellation/stars/quality.js +98 -0
- package/dist/utils/constellation/stars/recovery.d.ts +29 -0
- package/dist/utils/constellation/stars/recovery.js +169 -0
- package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
- package/dist/utils/constellation/strengthStarHelpers.js +104 -0
- package/dist/utils/constellation/types.d.ts +124 -0
- package/dist/utils/constellation/types.js +18 -0
- package/dist/utils/index.d.ts +5 -3
- package/dist/utils/index.js +1 -0
- package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
- package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
- package/dist/utils/scoringWorkout/constants.d.ts +20 -6
- package/dist/utils/scoringWorkout/constants.js +23 -9
- package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
- package/dist/utils/scoringWorkout/helpers.js +24 -18
- package/dist/utils/scoringWorkout/index.d.ts +12 -8
- package/dist/utils/scoringWorkout/index.js +23 -15
- package/dist/utils/scoringWorkout/parseRecords.js +4 -3
- package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +210 -172
- package/dist/utils/scoringWorkout/types.d.ts +34 -14
- package/package.json +31 -31
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
- package/dist/utils/scaleProPlan.util.d.ts +0 -9
- package/dist/utils/scaleProPlan.util.js +0 -139
- package/dist/utils/scoring/calculateCalories.d.ts +0 -67
- package/dist/utils/scoring/calculateCalories.js +0 -345
- package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
- package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
- package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
- package/dist/utils/scoring/calculateQualityScore.js +0 -334
- package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
- package/dist/utils/scoring/calculateTotalVolume.js +0 -73
- package/dist/utils/scoring/constants.d.ts +0 -211
- package/dist/utils/scoring/constants.js +0 -247
- package/dist/utils/scoring/helpers.d.ts +0 -119
- package/dist/utils/scoring/helpers.js +0 -229
- package/dist/utils/scoring/index.d.ts +0 -28
- package/dist/utils/scoring/index.js +0 -47
- package/dist/utils/scoring/parseRecords.d.ts +0 -98
- package/dist/utils/scoring/parseRecords.js +0 -284
- package/dist/utils/scoring/types.d.ts +0 -86
- package/dist/utils/scoring/types.js +0 -11
- package/dist/utils/scoring.utils.d.ts +0 -14
- package/dist/utils/scoring.utils.js +0 -243
- /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
- /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:
|
|
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
|
|
145
|
-
|
|
146
|
-
|
|
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 = {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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;
|