@dgpholdings/greatoak-shared 1.2.64 → 1.2.66
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/dist/__mocks__/user.mock.js +1 -1
- package/dist/constants/AiExerciseVocabulary.d.ts +61 -0
- package/dist/constants/AiExerciseVocabulary.js +209 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/types/TApiAuth.d.ts +3 -2
- package/dist/types/TApiUser.d.ts +4 -2
- package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +3 -3
- package/dist/utils/scoringWorkout/calculateQualityScore.js +40 -38
- package/dist/utils/scoringWorkout/helpers.d.ts +1 -1
- package/dist/utils/scoringWorkout/helpers.js +1 -1
- package/dist/utils/scoringWorkout/index.js +2 -3
- package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +57 -27
- package/dist/utils/scoringWorkout/types.d.ts +9 -6
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@ exports.mockUser = {
|
|
|
13
13
|
appLanguage: "en",
|
|
14
14
|
metricSystem: "EU",
|
|
15
15
|
fitnessLevel: "moderately-active",
|
|
16
|
-
|
|
16
|
+
fitnessGoals: ["hypertrophy"],
|
|
17
17
|
bodyFatPercentage: 15,
|
|
18
18
|
subscriptionStatus: "none",
|
|
19
19
|
subscriptionType: "trial", // Using a valid type from TSubscriptionType
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export declare const AI_FITNESS_GOALS: readonly ["strength", "hypertrophy", "fat_loss", "endurance", "mobility", "rehabilitation", "sport_performance", "general_fitness", "core_strength"];
|
|
2
|
+
export type TAiFitnessGoal = typeof AI_FITNESS_GOALS[number];
|
|
3
|
+
export declare const AI_FITNESS_GOAL_LABELS: Record<TAiFitnessGoal, string>;
|
|
4
|
+
export declare const AI_EQUIPMENT_TYPES: readonly ["none", "dumbbell", "barbell", "resistance_band", "cable_machine", "standard_machine", "suspension_trainer", "pull_up_bar", "bench", "kettlebell", "niche_machine", "exercise_ball", "ez_bar", "cardio_machine", "smith_machine", "weight_plate", "jump_rope", "battle_ropes", "medicine_ball", "ab_wheel", "plyo_box", "parallel_bars"];
|
|
5
|
+
export type TAiEquipmentType = typeof AI_EQUIPMENT_TYPES[number];
|
|
6
|
+
export declare const AI_HOME_EQUIPMENT: TAiEquipmentType[];
|
|
7
|
+
export declare const AI_MOVEMENT_PATTERNS: readonly ["push_horizontal", "push_vertical", "pull_horizontal", "pull_vertical", "squat", "hinge", "lunge", "rotation", "carry", "isolation", "plyo", "cardio", "mobility", "balance", "core_stability"];
|
|
8
|
+
export type TAiMovementPattern = typeof AI_MOVEMENT_PATTERNS[number];
|
|
9
|
+
export declare const AI_PUSH_PATTERNS: TAiMovementPattern[];
|
|
10
|
+
export declare const AI_PULL_PATTERNS: TAiMovementPattern[];
|
|
11
|
+
export declare const AI_LEG_PATTERNS: TAiMovementPattern[];
|
|
12
|
+
export declare const AI_CORE_PATTERNS: TAiMovementPattern[];
|
|
13
|
+
export declare const AI_WORKOUT_PLACEMENTS: readonly ["primary_compound", "secondary_compound", "accessory", "finisher", "warmup", "cooldown"];
|
|
14
|
+
export type TAiWorkoutPlacement = typeof AI_WORKOUT_PLACEMENTS[number];
|
|
15
|
+
export declare const AI_SESSION_ORDER: TAiWorkoutPlacement[];
|
|
16
|
+
export declare const AI_DIFFICULTY_LEVELS: readonly ["beginner", "intermediate", "advanced", "expert"];
|
|
17
|
+
export type TAiDifficultyLevel = typeof AI_DIFFICULTY_LEVELS[number];
|
|
18
|
+
export declare const AI_FITNESS_LEVELS: readonly ["beginner", "intermediate", "advanced"];
|
|
19
|
+
export type TAiFitnessLevel = typeof AI_FITNESS_LEVELS[number];
|
|
20
|
+
export declare const AI_DIFFICULTY_FILTER: Record<TAiFitnessLevel, TAiDifficultyLevel[]>;
|
|
21
|
+
export declare const AI_INTENSITY_LEVELS: readonly ["low", "moderate", "high"];
|
|
22
|
+
export type TAiIntensityLevel = typeof AI_INTENSITY_LEVELS[number];
|
|
23
|
+
export declare const AI_SLEEP_TO_MAX_CNS: Record<'good' | 'disturbed' | 'poor', TAiIntensityLevel>;
|
|
24
|
+
export declare const AI_MET_THRESHOLDS: {
|
|
25
|
+
readonly low: 3;
|
|
26
|
+
readonly moderate: 6;
|
|
27
|
+
readonly high: 6;
|
|
28
|
+
};
|
|
29
|
+
export declare const AI_BODY_AREAS: readonly ["lower back", "upper back", "shoulders", "neck", "elbows", "wrists", "hips", "hip flexors", "knees", "ankles", "hamstrings", "achilles tendon", "forearms", "groin", "calf muscles", "chest", "core"];
|
|
30
|
+
export type TAiBodyArea = typeof AI_BODY_AREAS[number];
|
|
31
|
+
export declare const AI_BODY_AREA_LABELS: Record<TAiBodyArea, string>;
|
|
32
|
+
export declare const AI_BODY_ISSUE_TYPES: readonly ["general_soreness", "joint_pain", "old_injury", "diagnosed_condition"];
|
|
33
|
+
export type TAiBodyIssueType = typeof AI_BODY_ISSUE_TYPES[number];
|
|
34
|
+
export declare const AI_BODY_ISSUE_LABELS: Record<TAiBodyIssueType, string>;
|
|
35
|
+
export declare const AI_POPULAR_THRESHOLD = 75;
|
|
36
|
+
export interface TAiUserInjury {
|
|
37
|
+
bodyArea: TAiBodyArea;
|
|
38
|
+
issueType: TAiBodyIssueType;
|
|
39
|
+
note?: string;
|
|
40
|
+
isActive: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface TAiUserProfile {
|
|
43
|
+
userId: string;
|
|
44
|
+
height: number;
|
|
45
|
+
weight: number;
|
|
46
|
+
bodyFatPercent?: number;
|
|
47
|
+
age: number;
|
|
48
|
+
fitnessGoals: TAiFitnessGoal[];
|
|
49
|
+
fitnessLevel: TAiFitnessLevel;
|
|
50
|
+
availableEquipment: TAiEquipmentType[];
|
|
51
|
+
trainsAtHome: boolean;
|
|
52
|
+
injuries: TAiUserInjury[];
|
|
53
|
+
avoidBodyAreas: TAiBodyArea[];
|
|
54
|
+
onboardingCompletedAt: string;
|
|
55
|
+
onboardingVersion: string;
|
|
56
|
+
}
|
|
57
|
+
export declare const AI_SLEEP_QUALITY_OPTIONS: readonly ["good", "disturbed", "poor"];
|
|
58
|
+
export type TAiSleepQuality = typeof AI_SLEEP_QUALITY_OPTIONS[number];
|
|
59
|
+
export declare const AI_SLEEP_QUALITY_LABELS: Record<TAiSleepQuality, string>;
|
|
60
|
+
export declare const AI_INTENT_CHIPS: readonly ["Leg day", "Upper body", "Full body", "Push day", "Pull day", "Core & abs", "Something light", "Surprise me"];
|
|
61
|
+
export type TAiIntentChip = typeof AI_INTENT_CHIPS[number];
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// shared/src/constants/AiExerciseVocabulary.ts
|
|
3
|
+
//
|
|
4
|
+
// Single source of truth for all fitness domain vocabulary used by the AI engine.
|
|
5
|
+
// Every value here exists in the Qdrant exercises collection payload.
|
|
6
|
+
//
|
|
7
|
+
// Verified against 701 active exercises from exercises-v2_3_PURE.json
|
|
8
|
+
// Do not add values here without also ensuring they exist in the data.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.AI_INTENT_CHIPS = exports.AI_SLEEP_QUALITY_LABELS = exports.AI_SLEEP_QUALITY_OPTIONS = exports.AI_POPULAR_THRESHOLD = exports.AI_BODY_ISSUE_LABELS = exports.AI_BODY_ISSUE_TYPES = exports.AI_BODY_AREA_LABELS = exports.AI_BODY_AREAS = exports.AI_MET_THRESHOLDS = exports.AI_SLEEP_TO_MAX_CNS = exports.AI_INTENSITY_LEVELS = exports.AI_DIFFICULTY_FILTER = exports.AI_FITNESS_LEVELS = exports.AI_DIFFICULTY_LEVELS = exports.AI_SESSION_ORDER = exports.AI_WORKOUT_PLACEMENTS = exports.AI_CORE_PATTERNS = exports.AI_LEG_PATTERNS = exports.AI_PULL_PATTERNS = exports.AI_PUSH_PATTERNS = exports.AI_MOVEMENT_PATTERNS = exports.AI_HOME_EQUIPMENT = exports.AI_EQUIPMENT_TYPES = exports.AI_FITNESS_GOAL_LABELS = exports.AI_FITNESS_GOALS = void 0;
|
|
11
|
+
// ── FITNESS GOALS ────────────────────────────────────────────────────────────
|
|
12
|
+
exports.AI_FITNESS_GOALS = [
|
|
13
|
+
'strength',
|
|
14
|
+
'hypertrophy',
|
|
15
|
+
'fat_loss',
|
|
16
|
+
'endurance',
|
|
17
|
+
'mobility',
|
|
18
|
+
'rehabilitation',
|
|
19
|
+
'sport_performance',
|
|
20
|
+
'general_fitness',
|
|
21
|
+
'core_strength',
|
|
22
|
+
];
|
|
23
|
+
exports.AI_FITNESS_GOAL_LABELS = {
|
|
24
|
+
strength: 'Get stronger',
|
|
25
|
+
hypertrophy: 'Build muscle',
|
|
26
|
+
fat_loss: 'Lose fat',
|
|
27
|
+
endurance: 'Improve endurance',
|
|
28
|
+
mobility: 'Improve flexibility & mobility',
|
|
29
|
+
rehabilitation: 'Recover from injury',
|
|
30
|
+
sport_performance: 'Sport performance',
|
|
31
|
+
general_fitness: 'General fitness',
|
|
32
|
+
core_strength: 'Build core strength',
|
|
33
|
+
};
|
|
34
|
+
// ── EQUIPMENT TYPES ──────────────────────────────────────────────────────────
|
|
35
|
+
exports.AI_EQUIPMENT_TYPES = [
|
|
36
|
+
'none',
|
|
37
|
+
'dumbbell',
|
|
38
|
+
'barbell',
|
|
39
|
+
'resistance_band',
|
|
40
|
+
'cable_machine',
|
|
41
|
+
'standard_machine',
|
|
42
|
+
'suspension_trainer',
|
|
43
|
+
'pull_up_bar',
|
|
44
|
+
'bench',
|
|
45
|
+
'kettlebell',
|
|
46
|
+
'niche_machine',
|
|
47
|
+
'exercise_ball',
|
|
48
|
+
'ez_bar',
|
|
49
|
+
'cardio_machine',
|
|
50
|
+
'smith_machine',
|
|
51
|
+
'weight_plate',
|
|
52
|
+
'jump_rope',
|
|
53
|
+
'battle_ropes',
|
|
54
|
+
'medicine_ball',
|
|
55
|
+
'ab_wheel',
|
|
56
|
+
'plyo_box',
|
|
57
|
+
'parallel_bars',
|
|
58
|
+
];
|
|
59
|
+
exports.AI_HOME_EQUIPMENT = [
|
|
60
|
+
'none',
|
|
61
|
+
'dumbbell',
|
|
62
|
+
'resistance_band',
|
|
63
|
+
'pull_up_bar',
|
|
64
|
+
'bench',
|
|
65
|
+
'kettlebell',
|
|
66
|
+
'exercise_ball',
|
|
67
|
+
'jump_rope',
|
|
68
|
+
];
|
|
69
|
+
// ── MOVEMENT PATTERNS ────────────────────────────────────────────────────────
|
|
70
|
+
exports.AI_MOVEMENT_PATTERNS = [
|
|
71
|
+
'push_horizontal',
|
|
72
|
+
'push_vertical',
|
|
73
|
+
'pull_horizontal',
|
|
74
|
+
'pull_vertical',
|
|
75
|
+
'squat',
|
|
76
|
+
'hinge',
|
|
77
|
+
'lunge',
|
|
78
|
+
'rotation',
|
|
79
|
+
'carry',
|
|
80
|
+
'isolation',
|
|
81
|
+
'plyo',
|
|
82
|
+
'cardio',
|
|
83
|
+
'mobility',
|
|
84
|
+
'balance',
|
|
85
|
+
'core_stability',
|
|
86
|
+
];
|
|
87
|
+
exports.AI_PUSH_PATTERNS = ['push_horizontal', 'push_vertical'];
|
|
88
|
+
exports.AI_PULL_PATTERNS = ['pull_horizontal', 'pull_vertical'];
|
|
89
|
+
exports.AI_LEG_PATTERNS = ['squat', 'hinge', 'lunge'];
|
|
90
|
+
exports.AI_CORE_PATTERNS = ['core_stability', 'rotation'];
|
|
91
|
+
// ── WORKOUT PLACEMENT ────────────────────────────────────────────────────────
|
|
92
|
+
exports.AI_WORKOUT_PLACEMENTS = [
|
|
93
|
+
'primary_compound',
|
|
94
|
+
'secondary_compound',
|
|
95
|
+
'accessory',
|
|
96
|
+
'finisher',
|
|
97
|
+
'warmup',
|
|
98
|
+
'cooldown',
|
|
99
|
+
];
|
|
100
|
+
exports.AI_SESSION_ORDER = [
|
|
101
|
+
'warmup',
|
|
102
|
+
'primary_compound',
|
|
103
|
+
'secondary_compound',
|
|
104
|
+
'accessory',
|
|
105
|
+
'finisher',
|
|
106
|
+
'cooldown',
|
|
107
|
+
];
|
|
108
|
+
// ── DIFFICULTY LEVELS ────────────────────────────────────────────────────────
|
|
109
|
+
exports.AI_DIFFICULTY_LEVELS = [
|
|
110
|
+
'beginner',
|
|
111
|
+
'intermediate',
|
|
112
|
+
'advanced',
|
|
113
|
+
'expert',
|
|
114
|
+
];
|
|
115
|
+
exports.AI_FITNESS_LEVELS = [
|
|
116
|
+
'beginner',
|
|
117
|
+
'intermediate',
|
|
118
|
+
'advanced',
|
|
119
|
+
];
|
|
120
|
+
exports.AI_DIFFICULTY_FILTER = {
|
|
121
|
+
beginner: ['beginner'],
|
|
122
|
+
intermediate: ['beginner', 'intermediate'],
|
|
123
|
+
advanced: ['beginner', 'intermediate', 'advanced', 'expert'],
|
|
124
|
+
};
|
|
125
|
+
// ── INTENSITY LEVELS ─────────────────────────────────────────────────────────
|
|
126
|
+
exports.AI_INTENSITY_LEVELS = ['low', 'moderate', 'high'];
|
|
127
|
+
exports.AI_SLEEP_TO_MAX_CNS = {
|
|
128
|
+
good: 'high',
|
|
129
|
+
disturbed: 'moderate',
|
|
130
|
+
poor: 'low',
|
|
131
|
+
};
|
|
132
|
+
// ── MET VALUES ───────────────────────────────────────────────────────────────
|
|
133
|
+
exports.AI_MET_THRESHOLDS = {
|
|
134
|
+
low: 3,
|
|
135
|
+
moderate: 6,
|
|
136
|
+
high: 6,
|
|
137
|
+
};
|
|
138
|
+
// ── BODY AREAS ───────────────────────────────────────────────────────────────
|
|
139
|
+
exports.AI_BODY_AREAS = [
|
|
140
|
+
'lower back',
|
|
141
|
+
'upper back',
|
|
142
|
+
'shoulders',
|
|
143
|
+
'neck',
|
|
144
|
+
'elbows',
|
|
145
|
+
'wrists',
|
|
146
|
+
'hips',
|
|
147
|
+
'hip flexors',
|
|
148
|
+
'knees',
|
|
149
|
+
'ankles',
|
|
150
|
+
'hamstrings',
|
|
151
|
+
'achilles tendon',
|
|
152
|
+
'forearms',
|
|
153
|
+
'groin',
|
|
154
|
+
'calf muscles',
|
|
155
|
+
'chest',
|
|
156
|
+
'core',
|
|
157
|
+
];
|
|
158
|
+
exports.AI_BODY_AREA_LABELS = {
|
|
159
|
+
'lower back': 'Lower back',
|
|
160
|
+
'upper back': 'Upper back',
|
|
161
|
+
'shoulders': 'Shoulder',
|
|
162
|
+
'neck': 'Neck',
|
|
163
|
+
'elbows': 'Elbow',
|
|
164
|
+
'wrists': 'Wrist',
|
|
165
|
+
'hips': 'Hip',
|
|
166
|
+
'hip flexors': 'Hip flexor',
|
|
167
|
+
'knees': 'Knee',
|
|
168
|
+
'ankles': 'Ankle',
|
|
169
|
+
'hamstrings': 'Hamstring',
|
|
170
|
+
'achilles tendon': 'Achilles / calf',
|
|
171
|
+
'forearms': 'Forearm',
|
|
172
|
+
'groin': 'Groin',
|
|
173
|
+
'calf muscles': 'Calf',
|
|
174
|
+
'chest': 'Chest',
|
|
175
|
+
'core': 'Core / abdomen',
|
|
176
|
+
};
|
|
177
|
+
// ── BODY ISSUE TYPES ─────────────────────────────────────────────────────────
|
|
178
|
+
exports.AI_BODY_ISSUE_TYPES = [
|
|
179
|
+
'general_soreness',
|
|
180
|
+
'joint_pain',
|
|
181
|
+
'old_injury',
|
|
182
|
+
'diagnosed_condition',
|
|
183
|
+
];
|
|
184
|
+
exports.AI_BODY_ISSUE_LABELS = {
|
|
185
|
+
general_soreness: 'General soreness or tightness',
|
|
186
|
+
joint_pain: 'Joint pain or clicking',
|
|
187
|
+
old_injury: 'Old / healed injury (just to note)',
|
|
188
|
+
diagnosed_condition: 'Doctor-diagnosed condition',
|
|
189
|
+
};
|
|
190
|
+
// ── POPULARITY ───────────────────────────────────────────────────────────────
|
|
191
|
+
exports.AI_POPULAR_THRESHOLD = 75;
|
|
192
|
+
// ── SLEEP QUALITY ────────────────────────────────────────────────────────────
|
|
193
|
+
exports.AI_SLEEP_QUALITY_OPTIONS = ['good', 'disturbed', 'poor'];
|
|
194
|
+
exports.AI_SLEEP_QUALITY_LABELS = {
|
|
195
|
+
good: 'Slept well',
|
|
196
|
+
disturbed: 'Lightly disturbed',
|
|
197
|
+
poor: 'Poor sleep',
|
|
198
|
+
};
|
|
199
|
+
// ── USER INTENT CHIPS ────────────────────────────────────────────────────────
|
|
200
|
+
exports.AI_INTENT_CHIPS = [
|
|
201
|
+
'Leg day',
|
|
202
|
+
'Upper body',
|
|
203
|
+
'Full body',
|
|
204
|
+
'Push day',
|
|
205
|
+
'Pull day',
|
|
206
|
+
'Core & abs',
|
|
207
|
+
'Something light',
|
|
208
|
+
'Surprise me',
|
|
209
|
+
];
|
package/dist/constants/index.js
CHANGED
package/dist/types/TApiAuth.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { TAiFitnessGoal } from "../constants/AiExerciseVocabulary";
|
|
2
|
+
import { TAuthType, TGender, TProfessionalCategory, TUserMetric, TUserType, TActivityLevel } from "./TApiUser";
|
|
2
3
|
export type TOnboardingData = {
|
|
3
4
|
userWeightKg: number;
|
|
4
5
|
dob: Date;
|
|
@@ -6,7 +7,7 @@ export type TOnboardingData = {
|
|
|
6
7
|
userHeightCm: number;
|
|
7
8
|
metricSystem: "US" | "EU";
|
|
8
9
|
bodyFatPercentage: number;
|
|
9
|
-
|
|
10
|
+
fitnessGoals: TAiFitnessGoal[];
|
|
10
11
|
fitnessLevel: TActivityLevel;
|
|
11
12
|
gdprEssential: boolean;
|
|
12
13
|
gdprAnalytics: boolean;
|
package/dist/types/TApiUser.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import type { TAiFitnessGoal } from "../constants/AiExerciseVocabulary";
|
|
1
2
|
import { TGdprData } from "./commonTypes";
|
|
2
3
|
export type TAuthType = "email" | "apple" | "anonymous" | "token";
|
|
3
4
|
export type TGender = "male" | "female" | "unmentioned";
|
|
4
5
|
export type TUserType = "regular" | "professional";
|
|
5
6
|
export type TProfessionalCategory = "fitness" | "wellness" | "medical" | "sports" | "other";
|
|
6
7
|
export type TUserTrainerType = "personal_trainer" | "strength_conditioning_coach" | "gym_instructor" | "fitness_coach" | "bodybuilding_coach" | "crossfit_coach" | "functional_training_specialist" | "calisthenics_trainer" | "yoga_instructor" | "pilates_instructor" | "meditation_coach" | "mindfulness_practitioner" | "breathwork_coach" | "physiotherapist" | "sports_physiotherapist" | "rehabilitation_specialist" | "chiropractor" | "orthopedic_doctor" | "sports_medicine_doctor" | "general_practitioner" | "occupational_therapist" | "kinesiologist" | "sports_coach" | "running_coach" | "cycling_coach" | "swimming_coach" | "athletic_trainer" | "performance_specialist" | "nutritionist" | "dietitian" | "health_coach" | "wellness_consultant" | "lifestyle_medicine_practitioner" | "physical_education_teacher" | "fitness_influencer" | "exercise_scientist" | "student_professional" | "other";
|
|
7
|
-
|
|
8
|
+
/** @deprecated Use TAiFitnessGoal instead */
|
|
9
|
+
export type TFitnessGoal = TAiFitnessGoal;
|
|
8
10
|
export type TSubscriptionStatus = "active" | "cancelled" | "expired" | "trial" | "grace" | "none";
|
|
9
11
|
export type TSubscriptionType = "monthly" | "yearly" | "trial";
|
|
10
12
|
export type TSubscriptionTier = "enthusiast" | "pro" | "none";
|
|
@@ -19,7 +21,7 @@ export type TUserMetric = {
|
|
|
19
21
|
professionType?: TProfessionalCategory;
|
|
20
22
|
gender: TGender;
|
|
21
23
|
appLanguage: string;
|
|
22
|
-
|
|
24
|
+
fitnessGoals: TAiFitnessGoal[];
|
|
23
25
|
weightKg?: number;
|
|
24
26
|
heightCm?: number;
|
|
25
27
|
emailAddress?: string;
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
* - Fair across types: cardio and strength use the same framework
|
|
30
30
|
* - Transparent: the breakdown is returned so the UI can explain the score
|
|
31
31
|
*/
|
|
32
|
-
import type { IParsedSet,
|
|
32
|
+
import type { IParsedSet, IUserContext, IHistoricalContext } from "./types";
|
|
33
33
|
import type { ITimingGuardrails } from "./parseRecords";
|
|
34
|
+
import type { TAiFitnessGoal } from "../../constants/AiExerciseVocabulary";
|
|
34
35
|
/**
|
|
35
36
|
* Raw record shape — we need the original RPE/RIR strings and isDone flag
|
|
36
37
|
* that aren't in IParsedSet (which only contains completed sets).
|
|
@@ -67,7 +68,6 @@ interface IRawRecord {
|
|
|
67
68
|
* // breakdown: { completion: 100, consistency: 75, effortAdequacy: 80, restDiscipline: 65 }
|
|
68
69
|
*/
|
|
69
70
|
export declare function calculateQualityScore(parsedSets: IParsedSet[], rawRecords: IRawRecord[], timingGuardrails: ITimingGuardrails | undefined, isStrictTimingModeScoring: boolean, userContext: IUserContext, historicalContext?: IHistoricalContext): {
|
|
70
|
-
|
|
71
|
-
breakdown: IQualityBreakdown;
|
|
71
|
+
scoresByGoal: Partial<Record<TAiFitnessGoal, import("./types").IScoreByGoal>>;
|
|
72
72
|
};
|
|
73
73
|
export {};
|
|
@@ -25,58 +25,60 @@ const helpers_1 = require("./helpers");
|
|
|
25
25
|
*/
|
|
26
26
|
function calculateQualityScore(parsedSets, rawRecords, timingGuardrails, isStrictTimingModeScoring, userContext, historicalContext) {
|
|
27
27
|
var _a;
|
|
28
|
-
// Edge case: no records at all
|
|
29
|
-
if (rawRecords.length === 0) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// from consistency/rest defaults
|
|
43
|
-
if (parsedSets.length === 0) {
|
|
28
|
+
// Edge case: no records at all or NONE were completed
|
|
29
|
+
if (rawRecords.length === 0 || parsedSets.length === 0) {
|
|
30
|
+
const emptyScoresByGoal = {};
|
|
31
|
+
for (const goal of userContext.fitnessGoals) {
|
|
32
|
+
emptyScoresByGoal[goal] = {
|
|
33
|
+
score: 0,
|
|
34
|
+
qualityBreakdown: {
|
|
35
|
+
completion: 0,
|
|
36
|
+
consistency: 0,
|
|
37
|
+
effortAdequacy: 0,
|
|
38
|
+
restDiscipline: 0,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
44
42
|
return {
|
|
45
|
-
|
|
46
|
-
breakdown: {
|
|
47
|
-
completion: 0,
|
|
48
|
-
consistency: 0,
|
|
49
|
-
effortAdequacy: 0,
|
|
50
|
-
restDiscipline: 0,
|
|
51
|
-
},
|
|
43
|
+
scoresByGoal: emptyScoresByGoal,
|
|
52
44
|
};
|
|
53
45
|
}
|
|
54
46
|
const completion = calculateCompletion(parsedSets, rawRecords);
|
|
55
47
|
const consistency = calculateConsistency(parsedSets, historicalContext);
|
|
56
48
|
const effortAdequacy = calculateEffortAdequacy(rawRecords);
|
|
57
49
|
const restDiscipline = isStrictTimingModeScoring ? calculateRestDiscipline(parsedSets, timingGuardrails) : constants_1.REST_NO_DATA_SCORE;
|
|
50
|
+
const breakdownBase = {
|
|
51
|
+
completion: Math.round(completion),
|
|
52
|
+
consistency: Math.round(consistency),
|
|
53
|
+
effortAdequacy: Math.round(effortAdequacy),
|
|
54
|
+
restDiscipline: Math.round(restDiscipline),
|
|
55
|
+
};
|
|
58
56
|
// P2-3: Dynamic Quality Weights based on fitnessGoal
|
|
59
57
|
const goalWeights = {
|
|
60
58
|
strength: { completion: 0.15, consistency: 0.25, effortAdequacy: 0.45, restDiscipline: 0.15 },
|
|
61
59
|
hypertrophy: { completion: 0.20, consistency: 0.35, effortAdequacy: 0.30, restDiscipline: 0.15 },
|
|
62
60
|
endurance: { completion: 0.25, consistency: 0.40, effortAdequacy: 0.20, restDiscipline: 0.15 },
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
fat_loss: { completion: 0.30, consistency: 0.30, effortAdequacy: 0.25, restDiscipline: 0.15 },
|
|
62
|
+
mobility: { completion: 0.40, consistency: 0.35, effortAdequacy: 0.10, restDiscipline: 0.15 },
|
|
63
|
+
general_fitness: { completion: 0.20, consistency: 0.35, effortAdequacy: 0.30, restDiscipline: 0.15 },
|
|
64
|
+
rehabilitation: { completion: 0.40, consistency: 0.40, effortAdequacy: 0.10, restDiscipline: 0.10 },
|
|
65
|
+
sport_performance: { completion: 0.20, consistency: 0.30, effortAdequacy: 0.35, restDiscipline: 0.15 },
|
|
66
|
+
core_strength: { completion: 0.25, consistency: 0.35, effortAdequacy: 0.25, restDiscipline: 0.15 },
|
|
66
67
|
};
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
const scoresByGoal = {};
|
|
69
|
+
for (const goal of userContext.fitnessGoals) {
|
|
70
|
+
const goalWeight = (_a = goalWeights[goal]) !== null && _a !== void 0 ? _a : constants_1.QUALITY_WEIGHTS;
|
|
71
|
+
const goalScore = Math.round(completion * goalWeight.completion +
|
|
72
|
+
consistency * goalWeight.consistency +
|
|
73
|
+
effortAdequacy * goalWeight.effortAdequacy +
|
|
74
|
+
restDiscipline * goalWeight.restDiscipline);
|
|
75
|
+
scoresByGoal[goal] = {
|
|
76
|
+
score: (0, helpers_1.clamp)(goalScore, 0, 100),
|
|
77
|
+
qualityBreakdown: breakdownBase,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
72
80
|
return {
|
|
73
|
-
|
|
74
|
-
breakdown: {
|
|
75
|
-
completion: Math.round(completion),
|
|
76
|
-
consistency: Math.round(consistency),
|
|
77
|
-
effortAdequacy: Math.round(effortAdequacy),
|
|
78
|
-
restDiscipline: Math.round(restDiscipline),
|
|
79
|
-
},
|
|
81
|
+
scoresByGoal,
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
84
|
// ---------------------------------------------------------------------------
|
|
@@ -117,7 +117,7 @@ export declare function extractUserContext(user: {
|
|
|
117
117
|
gender?: TGender;
|
|
118
118
|
dob?: Date | string;
|
|
119
119
|
fitnessLevel?: import("../../types").TActivityLevel;
|
|
120
|
-
|
|
120
|
+
fitnessGoals?: import("../../constants/AiExerciseVocabulary").TAiFitnessGoal[];
|
|
121
121
|
bodyFatPercentage?: number;
|
|
122
122
|
}): IUserContext;
|
|
123
123
|
/**
|
|
@@ -228,7 +228,7 @@ function extractUserContext(user) {
|
|
|
228
228
|
gender: user.gender || "unmentioned",
|
|
229
229
|
age,
|
|
230
230
|
fitnessLevel: (_a = user.fitnessLevel) !== null && _a !== void 0 ? _a : "moderately-active",
|
|
231
|
-
|
|
231
|
+
fitnessGoals: (_b = user.fitnessGoals) !== null && _b !== void 0 ? _b : ["general_fitness"],
|
|
232
232
|
bodyFatPercentage: (_c = user.bodyFatPercentage) !== null && _c !== void 0 ? _c : 20,
|
|
233
233
|
};
|
|
234
234
|
}
|
|
@@ -49,12 +49,11 @@ const calculateExerciseScoreV2 = (param) => {
|
|
|
49
49
|
}, userContext, exercise.timingGuardrails, historicalContext);
|
|
50
50
|
// Pillar 3: Quality Score → score
|
|
51
51
|
const isStrictTimingModeScoring = record.some((r) => r.isStrictMode);
|
|
52
|
-
const {
|
|
52
|
+
const { scoresByGoal } = (0, calculateQualityScore_1.calculateQualityScore)(parsedSets, record, exercise.timingGuardrails, isStrictTimingModeScoring, userContext, historicalContext);
|
|
53
53
|
return {
|
|
54
|
-
score,
|
|
55
54
|
muscleScores,
|
|
56
55
|
calorieBurn: 0, // Placeholder until Pillar 1 is fully wired in Phase 3
|
|
57
|
-
|
|
56
|
+
scoresByGoal,
|
|
58
57
|
};
|
|
59
58
|
};
|
|
60
59
|
exports.calculateExerciseScoreV2 = calculateExerciseScoreV2;
|
|
@@ -36,11 +36,12 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
36
36
|
// Without fix (totalSets=records.length=4): Set1 index=1 !== 3 → gets typical (120s).
|
|
37
37
|
const parsed = (0, parseRecords_1.parseRecords)(strictRecords, exercise.timingGuardrails);
|
|
38
38
|
(0, vitest_1.expect)(parsed[1].restDurationSecs).toBeNull();
|
|
39
|
-
const {
|
|
39
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
40
40
|
exercise,
|
|
41
41
|
record: strictRecords,
|
|
42
42
|
user: user_mock_1.mockUser,
|
|
43
43
|
});
|
|
44
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
44
45
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(50); // 2/4 done
|
|
45
46
|
// Only Set0 (120s, optimal) contributes to rest — Set1 is correctly excluded
|
|
46
47
|
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBeGreaterThan(95);
|
|
@@ -48,21 +49,23 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
48
49
|
(0, vitest_1.it)("P1-2: mockTemplateWeightRepsFailedSet - zero rep set still counts towards completion if isDone is true", () => {
|
|
49
50
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsFailedSet;
|
|
50
51
|
const exercise = exercises_mock_1.mockExerciseWeightReps;
|
|
51
|
-
const {
|
|
52
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
52
53
|
exercise,
|
|
53
54
|
record: template.initialRecords,
|
|
54
55
|
user: user_mock_1.mockUser,
|
|
55
56
|
});
|
|
57
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
56
58
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(67); // 2/3 done
|
|
57
59
|
});
|
|
58
60
|
(0, vitest_1.it)("P1-1 + P1-3: mockTemplateLazyLogger - handles missing data gracefully using fallbacks", () => {
|
|
59
61
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.lazyLogger;
|
|
60
62
|
const exercise = exercises_mock_1.mockExerciseWeightReps;
|
|
61
|
-
const {
|
|
63
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
62
64
|
exercise,
|
|
63
65
|
record: template.initialRecords,
|
|
64
66
|
user: user_mock_1.mockUser,
|
|
65
67
|
});
|
|
68
|
+
const { score, qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
66
69
|
(0, vitest_1.expect)(score).toBeGreaterThan(50);
|
|
67
70
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(100);
|
|
68
71
|
});
|
|
@@ -72,11 +75,12 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
72
75
|
// Use 600s rest. 600 > 300 * 1.5, so it gets clamped to typical (120s)
|
|
73
76
|
// 120s is in the optimal range (90-180), so rest score should be 100.
|
|
74
77
|
const strictRecords = template.initialRecords.map((r, i) => (Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: i === 0 ? 600 : 120 })));
|
|
75
|
-
const {
|
|
78
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
76
79
|
exercise,
|
|
77
80
|
record: strictRecords,
|
|
78
81
|
user: user_mock_1.mockUser,
|
|
79
82
|
});
|
|
83
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
80
84
|
// Both sets end up evaluating as 120s (optimal) due to clamping + explicit
|
|
81
85
|
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(100);
|
|
82
86
|
});
|
|
@@ -85,22 +89,24 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
85
89
|
const exercise = exercises_mock_1.mockExerciseWeightReps;
|
|
86
90
|
// 350s is within grace bounds (450s) but outside acceptableMax (300s).
|
|
87
91
|
const strictRecords = template.initialRecords.map((r, i) => (Object.assign(Object.assign({}, r), { isStrictMode: true, restDurationSecs: i === 0 ? 350 : 120 })));
|
|
88
|
-
const {
|
|
92
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
89
93
|
exercise,
|
|
90
94
|
record: strictRecords,
|
|
91
95
|
user: user_mock_1.mockUser,
|
|
92
96
|
});
|
|
97
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
93
98
|
// Set 1 (350s) -> 50. Set 2 (120s) -> 100. Avg = 75. Plus 5 point bonus = 80.
|
|
94
99
|
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(80);
|
|
95
100
|
});
|
|
96
101
|
(0, vitest_1.it)("mockTemplateAllUndone - Score = 0, no errors", () => {
|
|
97
102
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.allUndone;
|
|
98
103
|
const exercise = exercises_mock_1.mockExerciseWeightReps;
|
|
99
|
-
const {
|
|
104
|
+
const { scoresByGoal, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
|
|
100
105
|
exercise,
|
|
101
106
|
record: template.initialRecords,
|
|
102
107
|
user: user_mock_1.mockUser,
|
|
103
108
|
});
|
|
109
|
+
const { score, qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
104
110
|
(0, vitest_1.expect)(score).toBe(0);
|
|
105
111
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(0);
|
|
106
112
|
(0, vitest_1.expect)(qualityBreakdown.consistency).toBe(0);
|
|
@@ -119,11 +125,12 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
119
125
|
auxWeightKg: "0",
|
|
120
126
|
}
|
|
121
127
|
];
|
|
122
|
-
const {
|
|
128
|
+
const { scoresByGoal, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
|
|
123
129
|
exercise,
|
|
124
130
|
record: trivialRecord,
|
|
125
131
|
user: user_mock_1.mockUser,
|
|
126
132
|
});
|
|
133
|
+
const { score, qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
127
134
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(100);
|
|
128
135
|
(0, vitest_1.expect)(score).toBe(87);
|
|
129
136
|
// Check fatigue pillar correctness
|
|
@@ -166,11 +173,12 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
166
173
|
});
|
|
167
174
|
(0, vitest_1.it)("no guardrails doesn't crash: mockExerciseNoGuardrails scores > 0", () => {
|
|
168
175
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard;
|
|
169
|
-
const {
|
|
176
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
170
177
|
exercise: exercises_mock_1.mockExerciseNoGuardrails,
|
|
171
178
|
record: template.initialRecords,
|
|
172
179
|
user: user_mock_1.mockUser,
|
|
173
180
|
});
|
|
181
|
+
const { score } = scoresByGoal.hypertrophy;
|
|
174
182
|
(0, vitest_1.expect)(score).toBeGreaterThan(0);
|
|
175
183
|
});
|
|
176
184
|
(0, vitest_1.it)("RPE/RIR scoring: effortAdequacy calculates correctly for various inputs", () => {
|
|
@@ -180,21 +188,23 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
180
188
|
{ type: "weight-reps", isDone: true, isStrictMode: false, kg: "50", reps: "10", rir: "0" }, // 80 (Distance = 1 -> 100 - 1*20 = 80)
|
|
181
189
|
{ type: "weight-reps", isDone: true, isStrictMode: false, kg: "50", reps: "10" } // 70 (No data)
|
|
182
190
|
];
|
|
183
|
-
const {
|
|
191
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
184
192
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
185
193
|
record: records,
|
|
186
194
|
user: user_mock_1.mockUser,
|
|
187
195
|
});
|
|
196
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
188
197
|
// Average: (100 + 60 + 80 + 70) / 4 = 77.5 -> 78
|
|
189
198
|
(0, vitest_1.expect)(qualityBreakdown.effortAdequacy).toBe(78);
|
|
190
199
|
});
|
|
191
200
|
(0, vitest_1.it)("Progressive overload detection: mockTemplateWeightRepsMicroLoaded -> consistency >= 75", () => {
|
|
192
201
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsMicroLoaded;
|
|
193
|
-
const {
|
|
202
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
194
203
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
195
204
|
record: template.initialRecords,
|
|
196
205
|
user: user_mock_1.mockUser,
|
|
197
206
|
});
|
|
207
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
198
208
|
(0, vitest_1.expect)(qualityBreakdown.consistency).toBeGreaterThanOrEqual(75);
|
|
199
209
|
});
|
|
200
210
|
(0, vitest_1.it)("Single-set rest is neutral: mockTemplateSingleSet -> restDiscipline = 75", () => {
|
|
@@ -203,21 +213,23 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
203
213
|
const { restDurationSecs } = r, rest = __rest(r, ["restDurationSecs"]); // remove rest data so it becomes the null case
|
|
204
214
|
return Object.assign(Object.assign({}, rest), { isStrictMode: true });
|
|
205
215
|
});
|
|
206
|
-
const {
|
|
216
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
207
217
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
208
218
|
record: strictRecords,
|
|
209
219
|
user: user_mock_1.mockUser,
|
|
210
220
|
});
|
|
221
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
211
222
|
// Single set → last set gets null rest → setsWithRest=[] → REST_NO_DATA_SCORE (75)
|
|
212
223
|
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(75);
|
|
213
224
|
});
|
|
214
225
|
(0, vitest_1.it)("Non-sequential completion: mockTemplateNonSequential -> completion = 67%", () => {
|
|
215
226
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.nonSequential;
|
|
216
|
-
const {
|
|
227
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
217
228
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
218
229
|
record: template.initialRecords,
|
|
219
230
|
user: user_mock_1.mockUser,
|
|
220
231
|
});
|
|
232
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
221
233
|
(0, vitest_1.expect)(qualityBreakdown.completion).toBe(67);
|
|
222
234
|
});
|
|
223
235
|
(0, vitest_1.it)("IScoreResult has 4 fields: calorieBurn and qualityBreakdown are present", () => {
|
|
@@ -227,10 +239,9 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
227
239
|
record: template.initialRecords,
|
|
228
240
|
user: user_mock_1.mockUser,
|
|
229
241
|
});
|
|
230
|
-
(0, vitest_1.expect)(result).toHaveProperty("
|
|
242
|
+
(0, vitest_1.expect)(result).toHaveProperty("scoresByGoal");
|
|
231
243
|
(0, vitest_1.expect)(result).toHaveProperty("muscleScores");
|
|
232
244
|
(0, vitest_1.expect)(result).toHaveProperty("calorieBurn");
|
|
233
|
-
(0, vitest_1.expect)(result).toHaveProperty("qualityBreakdown");
|
|
234
245
|
});
|
|
235
246
|
(0, vitest_1.it)("Cardio exercises: mockTemplateCardioMachine/Free score > 0 and use correct muscles", () => {
|
|
236
247
|
const machineResult = (0, index_1.calculateExerciseScoreV2)({
|
|
@@ -238,33 +249,35 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
238
249
|
record: templateExercises_mock_1.mockTemplateExercisesDictionary.cardioMachine.initialRecords,
|
|
239
250
|
user: user_mock_1.mockUser,
|
|
240
251
|
});
|
|
241
|
-
(0, vitest_1.expect)(machineResult.score).toBeGreaterThan(0);
|
|
252
|
+
(0, vitest_1.expect)(machineResult.scoresByGoal.hypertrophy.score).toBeGreaterThan(0);
|
|
242
253
|
(0, vitest_1.expect)(machineResult.muscleScores).toHaveProperty("quadriceps");
|
|
243
254
|
const freeResult = (0, index_1.calculateExerciseScoreV2)({
|
|
244
255
|
exercise: exercises_mock_1.mockExerciseCardioFree,
|
|
245
256
|
record: templateExercises_mock_1.mockTemplateExercisesDictionary.cardioFree.initialRecords,
|
|
246
257
|
user: user_mock_1.mockUser,
|
|
247
258
|
});
|
|
248
|
-
(0, vitest_1.expect)(freeResult.score).toBeGreaterThan(0);
|
|
259
|
+
(0, vitest_1.expect)(freeResult.scoresByGoal.hypertrophy.score).toBeGreaterThan(0);
|
|
249
260
|
(0, vitest_1.expect)(freeResult.muscleScores).toHaveProperty("quadriceps");
|
|
250
261
|
});
|
|
251
262
|
(0, vitest_1.it)("reps-only with aux weight: mockTemplateRepsOnly -> score > 0, pectoralis-major mapped", () => {
|
|
252
263
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.repsOnly;
|
|
253
|
-
const {
|
|
264
|
+
const { scoresByGoal, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
|
|
254
265
|
exercise: exercises_mock_1.mockExerciseRepsOnly,
|
|
255
266
|
record: template.initialRecords,
|
|
256
267
|
user: user_mock_1.mockUser,
|
|
257
268
|
});
|
|
269
|
+
const { score } = scoresByGoal.hypertrophy;
|
|
258
270
|
(0, vitest_1.expect)(score).toBeGreaterThan(0);
|
|
259
271
|
(0, vitest_1.expect)(muscleScores).toHaveProperty("pectoralis-major");
|
|
260
272
|
});
|
|
261
273
|
(0, vitest_1.it)("Duration exercise: mockTemplateDuration -> abs-lower/abs-upper in muscleScores", () => {
|
|
262
274
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.duration;
|
|
263
|
-
const {
|
|
275
|
+
const { scoresByGoal, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
|
|
264
276
|
exercise: exercises_mock_1.mockExerciseDuration,
|
|
265
277
|
record: template.initialRecords,
|
|
266
278
|
user: user_mock_1.mockUser,
|
|
267
279
|
});
|
|
280
|
+
const { score } = scoresByGoal.hypertrophy;
|
|
268
281
|
(0, vitest_1.expect)(score).toBeGreaterThan(0);
|
|
269
282
|
(0, vitest_1.expect)(muscleScores).toHaveProperty("abs-lower");
|
|
270
283
|
(0, vitest_1.expect)(muscleScores).toHaveProperty("abs-upper");
|
|
@@ -272,13 +285,30 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
272
285
|
(0, vitest_1.it)("Default restDiscipline (non-strict): Any non-strict call -> restDiscipline === 75", () => {
|
|
273
286
|
const template = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard;
|
|
274
287
|
// Standard mock is `isStrictMode: false`
|
|
275
|
-
const {
|
|
288
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
276
289
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
277
290
|
record: template.initialRecords,
|
|
278
291
|
user: user_mock_1.mockUser,
|
|
279
292
|
});
|
|
293
|
+
const { qualityBreakdown } = scoresByGoal.hypertrophy;
|
|
280
294
|
(0, vitest_1.expect)(qualityBreakdown.restDiscipline).toBe(75); // REST_NO_DATA_SCORE from constants.ts
|
|
281
295
|
});
|
|
296
|
+
(0, vitest_1.it)("Parallel scoring: strength score differs from endurance score for identical heavy workout", () => {
|
|
297
|
+
const records = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsStandard.initialRecords;
|
|
298
|
+
// Give user two vastly different goals
|
|
299
|
+
const multiGoalUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessGoals: ["strength", "endurance"] });
|
|
300
|
+
const { scoresByGoal } = (0, index_1.calculateExerciseScoreV2)({
|
|
301
|
+
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
302
|
+
record: records,
|
|
303
|
+
user: multiGoalUser,
|
|
304
|
+
});
|
|
305
|
+
(0, vitest_1.expect)(scoresByGoal).toHaveProperty("strength");
|
|
306
|
+
(0, vitest_1.expect)(scoresByGoal).toHaveProperty("endurance");
|
|
307
|
+
const strengthScore = scoresByGoal.strength.score;
|
|
308
|
+
const enduranceScore = scoresByGoal.endurance.score;
|
|
309
|
+
// The weights and effort math naturally produce different results
|
|
310
|
+
(0, vitest_1.expect)(strengthScore).not.toEqual(enduranceScore);
|
|
311
|
+
});
|
|
282
312
|
});
|
|
283
313
|
(0, vitest_1.describe)("Scoring Engine Phase 2 Integration Tests", () => {
|
|
284
314
|
// ---------------------------------------------------------------------------
|
|
@@ -309,8 +339,8 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
309
339
|
// 2 of 4 sets done (50% completion) with RPE 8 (effort in productive zone)
|
|
310
340
|
// strength weights effortAdequacy 0.45 vs hypertrophy 0.30 — effort matters more
|
|
311
341
|
const partialWithEffort = templateExercises_mock_1.mockTemplateExercisesDictionary.weightRepsPartial.initialRecords.map((r) => (Object.assign(Object.assign({}, r), { rpe: "8" })));
|
|
312
|
-
const strengthUser = Object.assign(Object.assign({}, user_mock_1.mockUser), {
|
|
313
|
-
const hypertrophyUser = Object.assign(Object.assign({}, user_mock_1.mockUser), {
|
|
342
|
+
const strengthUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessGoals: ["strength"] });
|
|
343
|
+
const hypertrophyUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessGoals: ["hypertrophy"] });
|
|
314
344
|
const strengthResult = (0, index_1.calculateExerciseScoreV2)({
|
|
315
345
|
exercise: exercises_mock_1.mockExerciseWeightReps,
|
|
316
346
|
record: partialWithEffort,
|
|
@@ -324,9 +354,9 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
324
354
|
// completion=50, consistency=100, effortAdequacy=100, restDiscipline=75
|
|
325
355
|
// strength: 50×0.15 + 100×0.25 + 100×0.45 + 75×0.15 = 88.75 → 89
|
|
326
356
|
// hypertrophy: 50×0.20 + 100×0.35 + 100×0.30 + 75×0.15 = 86.25 → 86
|
|
327
|
-
(0, vitest_1.expect)(strengthResult.score).toBe(89);
|
|
328
|
-
(0, vitest_1.expect)(hypertrophyResult.score).toBe(86);
|
|
329
|
-
(0, vitest_1.expect)(strengthResult.score).toBeGreaterThan(hypertrophyResult.score);
|
|
357
|
+
(0, vitest_1.expect)(strengthResult.scoresByGoal.strength.score).toBe(89);
|
|
358
|
+
(0, vitest_1.expect)(hypertrophyResult.scoresByGoal.hypertrophy.score).toBe(86);
|
|
359
|
+
(0, vitest_1.expect)(strengthResult.scoresByGoal.strength.score).toBeGreaterThan(hypertrophyResult.scoresByGoal.hypertrophy.score);
|
|
330
360
|
});
|
|
331
361
|
// ---------------------------------------------------------------------------
|
|
332
362
|
// P2-5: estimatedOneRepMax in referenceMax
|
|
@@ -375,8 +405,8 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
375
405
|
user: user_mock_1.mockUser,
|
|
376
406
|
historicalContext: { previousSessionVolume: 1000 },
|
|
377
407
|
});
|
|
378
|
-
(0, vitest_1.expect)(withoutContext.qualityBreakdown.consistency).toBe(20); // CONSISTENCY_MIN_SCORE
|
|
379
|
-
(0, vitest_1.expect)(withContext.qualityBreakdown.consistency).toBe(75); // PROGRESSIVE_OVERLOAD_FLOOR
|
|
408
|
+
(0, vitest_1.expect)(withoutContext.scoresByGoal.hypertrophy.qualityBreakdown.consistency).toBe(20); // CONSISTENCY_MIN_SCORE
|
|
409
|
+
(0, vitest_1.expect)(withContext.scoresByGoal.hypertrophy.qualityBreakdown.consistency).toBe(75); // PROGRESSIVE_OVERLOAD_FLOOR
|
|
380
410
|
});
|
|
381
411
|
(0, vitest_1.it)("P2-6: not beating previous session volume does NOT trigger the floor", () => {
|
|
382
412
|
const inconsistentRecords = [
|
|
@@ -391,7 +421,7 @@ const user_mock_1 = require("../../__mocks__/user.mock");
|
|
|
391
421
|
user: user_mock_1.mockUser,
|
|
392
422
|
historicalContext: { previousSessionVolume: 3000 },
|
|
393
423
|
});
|
|
394
|
-
(0, vitest_1.expect)(result.qualityBreakdown.consistency).toBe(20); // no floor applied
|
|
424
|
+
(0, vitest_1.expect)(result.scoresByGoal.hypertrophy.qualityBreakdown.consistency).toBe(20); // no floor applied
|
|
395
425
|
});
|
|
396
426
|
// ---------------------------------------------------------------------------
|
|
397
427
|
// P2-10: trainingAgeBracket RIR attenuation + referenceMax scaling
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
* This file defines the output shape and internal types used across all three
|
|
8
8
|
* scoring pillars (Calories, Muscle Fatigue, Quality).
|
|
9
9
|
*/
|
|
10
|
-
import { TActivityLevel,
|
|
10
|
+
import { TActivityLevel, TGender, TRecord } from "../../types";
|
|
11
|
+
import type { TAiFitnessGoal } from "../../constants/AiExerciseVocabulary";
|
|
11
12
|
/**
|
|
12
13
|
* A single scored session with real per-muscle fatigue values.
|
|
13
14
|
* muscleScores = {} for pre-P3-1 sessions (backward compat fallback).
|
|
@@ -44,16 +45,18 @@ export interface IQualityBreakdown {
|
|
|
44
45
|
/**
|
|
45
46
|
* The final result returned by calculateExerciseScore.
|
|
46
47
|
*
|
|
47
|
-
* - score: 0–100 overall quality of execution
|
|
48
48
|
* - muscleScores: per-muscle fatigue map (keyed by EBodyParts key, value 0–100)
|
|
49
49
|
* - calorieBurn: estimated kilocalories burned (gross, including EPOC)
|
|
50
|
-
* -
|
|
50
|
+
* - scoresByGoal: branched scores for each of the user's selected goals
|
|
51
51
|
*/
|
|
52
|
-
export interface
|
|
52
|
+
export interface IScoreByGoal {
|
|
53
53
|
score: number;
|
|
54
|
+
qualityBreakdown: IQualityBreakdown;
|
|
55
|
+
}
|
|
56
|
+
export interface IScoreResult {
|
|
54
57
|
muscleScores: Record<string, number>;
|
|
55
58
|
calorieBurn: number;
|
|
56
|
-
|
|
59
|
+
scoresByGoal: Partial<Record<TAiFitnessGoal, IScoreByGoal>>;
|
|
57
60
|
}
|
|
58
61
|
export type TTrainingAgeBracket = "beginner" | "intermediate" | "advanced";
|
|
59
62
|
/**
|
|
@@ -118,6 +121,6 @@ export interface IUserContext {
|
|
|
118
121
|
gender: TGender;
|
|
119
122
|
age: number;
|
|
120
123
|
fitnessLevel: TActivityLevel;
|
|
121
|
-
|
|
124
|
+
fitnessGoals: TAiFitnessGoal[];
|
|
122
125
|
bodyFatPercentage: number;
|
|
123
126
|
}
|