@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.
@@ -13,7 +13,7 @@ exports.mockUser = {
13
13
  appLanguage: "en",
14
14
  metricSystem: "EU",
15
15
  fitnessLevel: "moderately-active",
16
- fitnessGoal: "hypertrophy",
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
+ ];
@@ -1 +1,2 @@
1
1
  export * from "./BodyCategories";
2
+ export * from "./AiExerciseVocabulary";
@@ -15,3 +15,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./BodyCategories"), exports);
18
+ __exportStar(require("./AiExerciseVocabulary"), exports);
@@ -1,4 +1,5 @@
1
- import { TAuthType, TFitnessGoal, TGender, TProfessionalCategory, TUserMetric, TUserType, TActivityLevel } from "./TApiUser";
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
- fitnessGoal: TFitnessGoal;
10
+ fitnessGoals: TAiFitnessGoal[];
10
11
  fitnessLevel: TActivityLevel;
11
12
  gdprEssential: boolean;
12
13
  gdprAnalytics: boolean;
@@ -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
- export type TFitnessGoal = "strength" | "hypertrophy" | "endurance" | "general" | "fat_burn" | "flexibility";
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
- fitnessGoal: TFitnessGoal;
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, IQualityBreakdown, IUserContext, IHistoricalContext } from "./types";
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
- score: number;
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
- return {
31
- score: 0,
32
- breakdown: {
33
- completion: 0,
34
- consistency: 0,
35
- effortAdequacy: 0,
36
- restDiscipline: 0,
37
- },
38
- };
39
- }
40
- // Edge case: records exist but NONE were completed
41
- // If you didn't do a single set, the score is 0 — no partial credit
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
- score: 0,
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
- fat_burn: { completion: 0.30, consistency: 0.30, effortAdequacy: 0.25, restDiscipline: 0.15 },
64
- flexibility: { completion: 0.40, consistency: 0.35, effortAdequacy: 0.10, restDiscipline: 0.15 },
65
- general: { completion: 0.20, consistency: 0.35, effortAdequacy: 0.30, restDiscipline: 0.15 },
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 weights = (_a = goalWeights[userContext.fitnessGoal]) !== null && _a !== void 0 ? _a : constants_1.QUALITY_WEIGHTS;
68
- const score = Math.round(completion * weights.completion +
69
- consistency * weights.consistency +
70
- effortAdequacy * weights.effortAdequacy +
71
- restDiscipline * weights.restDiscipline);
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
- score: (0, helpers_1.clamp)(score, 0, 100),
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
- fitnessGoal?: import("../../types").TFitnessGoal;
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
- fitnessGoal: (_b = user.fitnessGoal) !== null && _b !== void 0 ? _b : "general",
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 { score, breakdown } = (0, calculateQualityScore_1.calculateQualityScore)(parsedSets, record, exercise.timingGuardrails, isStrictTimingModeScoring, userContext, historicalContext);
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
- qualityBreakdown: breakdown,
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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 { score, qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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 { score, qualityBreakdown, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
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 { score, qualityBreakdown, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
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 { score } = (0, index_1.calculateExerciseScoreV2)({
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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("score");
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 { score, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
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 { score, muscleScores } = (0, index_1.calculateExerciseScoreV2)({
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 { qualityBreakdown } = (0, index_1.calculateExerciseScoreV2)({
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), { fitnessGoal: "strength" });
313
- const hypertrophyUser = Object.assign(Object.assign({}, user_mock_1.mockUser), { fitnessGoal: "hypertrophy" });
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, TFitnessGoal, TGender, TRecord } from "../../types";
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
- * - qualityBreakdown: transparent sub-scores for the UI
50
+ * - scoresByGoal: branched scores for each of the user's selected goals
51
51
  */
52
- export interface IScoreResult {
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
- qualityBreakdown: IQualityBreakdown;
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
- fitnessGoal: TFitnessGoal;
124
+ fitnessGoals: TAiFitnessGoal[];
122
125
  bodyFatPercentage: number;
123
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.2.64",
3
+ "version": "1.2.66",
4
4
  "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",