@dgpholdings/greatoak-shared 1.2.92 → 1.2.93

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.
@@ -5,7 +5,6 @@ const testIds_1 = require("./testIds");
5
5
  exports.mockUser = {
6
6
  // TUserPreferences fields (flat in TUserMetric)
7
7
  preferredDurationMinutes: 45,
8
- fitnessGoals: ["hypertrophy"],
9
8
  preferredEquipment: ["dumbbell", "barbell"],
10
9
  preferCompoundMovements: true,
11
10
  includeWarmup: "ai_decide",
@@ -1,3 +1,2 @@
1
1
  export * from "./BodyCategories";
2
2
  export * from "./AiExerciseVocabulary";
3
- export * from "./goalJourney";
@@ -16,4 +16,3 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./BodyCategories"), exports);
18
18
  __exportStar(require("./AiExerciseVocabulary"), exports);
19
- __exportStar(require("./goalJourney"), exports);
@@ -1,4 +1,5 @@
1
1
  import { TAiEnergyLevel, TAiUserInjury, TQuickStartEquipmentGroupExplicit } from "../constants/AiExerciseVocabulary";
2
+ import { TTrainingPhase, TUserFitnessGoal } from "./goalFeature";
2
3
  import { TTemplate } from "./TApiTemplateData";
3
4
  export type TApiAiQuickStartWorkoutReq = {
4
5
  /**
@@ -62,6 +63,12 @@ export type TApiAiQuickStartWorkoutReq = {
62
63
  * backend treats it as "no equipment filter" — i.e. all candidates eligible.
63
64
  */
64
65
  equipmentSelectionOverride: TQuickStartEquipmentSelection;
66
+ goalContext?: {
67
+ activeGoal: TUserFitnessGoal;
68
+ currentPhase: TTrainingPhase;
69
+ currentWeek: number;
70
+ phaseName: string;
71
+ };
65
72
  };
66
73
  export type TQuickStartEquipmentSelection = {
67
74
  type: "preferences";
@@ -1,5 +1,4 @@
1
- import type { TAiFitnessGoal } from "../constants/AiExerciseVocabulary";
2
- import { TAuthType, TGender, TProfessionalCategory, TUserMetric, TUserType, TInjuryInfo } from "./TApiUser";
1
+ import { TAuthType, TGender, TInjuryInfo, TProfessionalCategory, TUserMetric, TUserType } from "./TApiUser";
3
2
  import { TActivityLevel } from "./TUserPreferences";
4
3
  /**
5
4
  * Anonymous signup request.
@@ -25,7 +24,6 @@ export type TApiSignupAnonymousReq = {
25
24
  gender: TGender;
26
25
  bodyFatPercentage: number;
27
26
  metricSystem: "US" | "EU";
28
- fitnessGoals: TAiFitnessGoal[];
29
27
  fitnessLevel: TActivityLevel;
30
28
  injuryInfo?: TInjuryInfo;
31
29
  gdprAnalytics: boolean;
@@ -25,8 +25,8 @@ export type TUserMetric = TUserPreferences & {
25
25
  userType: TUserType;
26
26
  professionType?: TProfessionalCategory;
27
27
  gender: TGender;
28
- weightKg?: number;
29
- heightCm?: number;
28
+ weightKg: number;
29
+ heightCm: number;
30
30
  emailAddress?: string;
31
31
  injuryInfo?: TInjuryInfo;
32
32
  subscriptionStatus: TSubscriptionStatus;
@@ -1,11 +1,10 @@
1
- import { TAiEquipmentType, TAiFitnessGoal } from "../constants";
1
+ import { TAiEquipmentType } from "../constants";
2
2
  import { TBodyPart } from "./TApiExercise";
3
3
  export type TPreferredDuration = 20 | 30 | 45 | 60 | 90 | 120;
4
4
  export type TAIDecision = "yes" | "no" | "ai_decide";
5
5
  export type TWarmupStyle = "dynamic_stretching" | "light_cardio";
6
6
  export type TStablePreferences = {
7
7
  preferredDurationMinutes: TPreferredDuration;
8
- fitnessGoals: TAiFitnessGoal[];
9
8
  preferredEquipment: TAiEquipmentType[];
10
9
  preferCompoundMovements: boolean;
11
10
  includeWarmup: TAIDecision;
@@ -13,7 +12,7 @@ export type TStablePreferences = {
13
12
  warmupStyle: TWarmupStyle | null;
14
13
  favouriteMuscleGroups: TBodyPart[];
15
14
  blacklistedExerciseIds: string[];
16
- fitnessLevel?: TActivityLevel;
15
+ fitnessLevel: TActivityLevel;
17
16
  bodyFatPercentage?: number;
18
17
  metricSystem: "US" | "EU";
19
18
  appLanguage: string;
@@ -0,0 +1,64 @@
1
+ import { TWorkoutIntent } from "../../constants";
2
+ import { TDaysPerWeek, TSessionIntent, TTrainingPhase, TUserFitnessGoal } from "./goalJourney";
3
+ export type TGoalProfile = {
4
+ activeGoal: TUserFitnessGoal;
5
+ goalStartDate: string;
6
+ daysPerWeek: TDaysPerWeek;
7
+ phaseCompletions: {
8
+ phase: TTrainingPhase;
9
+ completedAt: string;
10
+ celebrationShown: boolean;
11
+ }[];
12
+ previousGoals: {
13
+ goal: TUserFitnessGoal;
14
+ startDate: string;
15
+ endDate: string;
16
+ weeksCompleted: number;
17
+ reasonEnded: "completed" | "changed";
18
+ }[];
19
+ };
20
+ export type TGoalContext = {
21
+ goal: TUserFitnessGoal;
22
+ daysPerWeek: TDaysPerWeek;
23
+ currentPhase: TTrainingPhase;
24
+ currentWeek: number;
25
+ weeklyStructure: TSessionIntent[];
26
+ lastSessionDate: string;
27
+ lastSessionIntent: TSessionIntent;
28
+ sessionsCompletedThisWeek: number;
29
+ totalSessionsSinceGoalStart: number;
30
+ sessionsInLast4Weeks: number;
31
+ };
32
+ export type TIntentRecommendation = {
33
+ intent: TWorkoutIntent;
34
+ confidence: number;
35
+ rationale: string;
36
+ decisionPath: "global_recovery" | "returning_user" | "gap_bridge" | "structure_follow" | "muscle_override";
37
+ intentScores: Partial<Record<TWorkoutIntent, number>>;
38
+ };
39
+ export type TGoalSessionSummary = {
40
+ sessionId: string;
41
+ createdAt: string;
42
+ intentId: TWorkoutIntent | undefined;
43
+ exercises: {
44
+ exerciseId: string;
45
+ thumbnailUrl: string;
46
+ name: string;
47
+ }[];
48
+ };
49
+ export type TApiGoalContextRes = {
50
+ status: 200;
51
+ goalProfile: TGoalProfile;
52
+ goalContext: TGoalContext;
53
+ recommendation: TIntentRecommendation;
54
+ recentSessions: TGoalSessionSummary[];
55
+ weeklySessionCounts: {
56
+ weekNumber: number;
57
+ weekStartDate: string;
58
+ count: number;
59
+ }[];
60
+ newPhaseUnlocked?: TTrainingPhase;
61
+ } | {
62
+ status: 400 | 401 | 404 | 500;
63
+ message: string;
64
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,111 @@
1
+ import { TActivityLevel } from "..";
2
+ import { TAiFitnessGoal, TWorkoutIntent } from "../../constants/AiExerciseVocabulary";
3
+ export declare const USER_FITNESS_GOALS: readonly ["lose_weight", "build_muscle", "lean_and_toned", "glutes_and_legs", "get_stronger", "boost_endurance", "athletic_performance", "general_fitness", "posture_and_back", "stress_relief", "rehabilitation", "core_strength", "mobility_flexibility"];
4
+ export type TUserFitnessGoal = (typeof USER_FITNESS_GOALS)[number];
5
+ export type TSessionIntent = TWorkoutIntent | "rest";
6
+ export declare const GOAL_SESSION_INTENTS: Record<TUserFitnessGoal, TWeeklyIntentTemplate>;
7
+ export declare const GOAL_TO_AI_FITNESS_GOAL: Record<TUserFitnessGoal, TAiFitnessGoal>;
8
+ export declare const TRAINING_PHASES: readonly ["foundation", "momentum", "push", "peak"];
9
+ export type TTrainingPhase = (typeof TRAINING_PHASES)[number];
10
+ export type TPhaseDisplay = {
11
+ /** i18n key for the phase name — e.g. "goalPhase.foundation.name" */
12
+ nameKey: string;
13
+ /** English fallback for nameKey */
14
+ nameEn: string;
15
+ /** i18n key for the short subtitle — e.g. "goalPhase.foundation.subtitle" */
16
+ subtitleKey: string;
17
+ /** English fallback for subtitleKey */
18
+ subtitleEn: string;
19
+ /** i18n key for the longer purpose description shown in phase detail */
20
+ purposeKey: string;
21
+ /** English fallback for purposeKey */
22
+ purposeEn: string;
23
+ /** i18n key for the week range label — e.g. "goalPhase.foundation.duration" */
24
+ durationKey: string;
25
+ /** English fallback for durationKey */
26
+ durationEn: string;
27
+ /** Inclusive week range [startWeek, endWeek]. Phase 4 end is open (99). */
28
+ weekRange: [number, number];
29
+ /**
30
+ * Reference intensity as a percentage (0–100).
31
+ * For UI progress bars / intensity indicators only — not used in algorithm.
32
+ */
33
+ intensityPercent: number;
34
+ };
35
+ export declare const PHASE_DISPLAY: Record<TTrainingPhase, TPhaseDisplay>;
36
+ export type TIntentDisplay = {
37
+ /** i18n key for the full label — e.g. "sessionIntent.push.label" */
38
+ labelKey: string;
39
+ /** English fallback for labelKey */
40
+ labelEn: string;
41
+ /**
42
+ * i18n key for the short label used in compact journey nodes.
43
+ * e.g. "sessionIntent.push.shortLabel"
44
+ */
45
+ shortLabelKey: string;
46
+ /** English fallback for shortLabelKey */
47
+ shortLabelEn: string;
48
+ /**
49
+ * Emoji for the session node icon.
50
+ * Not translated — emoji is universal.
51
+ */
52
+ emoji: string;
53
+ };
54
+ export declare const INTENT_DISPLAY: Record<Exclude<TSessionIntent, "arms" | "back" | "unstructured">, TIntentDisplay>;
55
+ export type TGoalDisplay = {
56
+ /** i18n key for the goal name — e.g. "userGoal.loseWeight.name" */
57
+ nameKey: string;
58
+ /** English fallback for nameKey */
59
+ nameEn: string;
60
+ /** i18n key for the short description shown on goal selection screen */
61
+ descriptionKey: string;
62
+ /** English fallback for descriptionKey */
63
+ descriptionEn: string;
64
+ /**
65
+ * i18n key for the motivational tagline shown on the goal journey screen.
66
+ * e.g. "userGoal.loseWeight.tagline"
67
+ */
68
+ taglineKey: string;
69
+ /** English fallback for taglineKey */
70
+ taglineEn: string;
71
+ /** Emoji representing the goal — universal, not translated */
72
+ emoji: string;
73
+ /**
74
+ * Goal category — used to group goals on the selection screen.
75
+ * Category label itself is also translated via categoryKey.
76
+ */
77
+ category: "appearance" | "performance" | "health" | "focused";
78
+ };
79
+ export declare const GOAL_DISPLAY: Record<TUserFitnessGoal, TGoalDisplay>;
80
+ export type TGoalCategory = TGoalDisplay["category"];
81
+ export type TGoalCategoryDisplay = {
82
+ labelKey: string;
83
+ labelEn: string;
84
+ };
85
+ export declare const GOAL_CATEGORY_DISPLAY: Record<TGoalCategory, TGoalCategoryDisplay>;
86
+ export declare const PHASE_ENTRY_CREDIT: Record<TActivityLevel, number>;
87
+ export declare const RECENCY_CREDIT: Record<TActivityLevel, number>;
88
+ /**
89
+ * Derive training phase from time + volume + recency.
90
+ * All three must pass — week count alone is NOT sufficient.
91
+ * A user doing 12 sessions over one year has not built a training base.
92
+ *
93
+ * On goal start day (no sessions yet), pass:
94
+ * totalSessions = PHASE_ENTRY_CREDIT[user.fitnessLevel]
95
+ * sessionsInLast4Weeks = RECENCY_CREDIT[user.fitnessLevel]
96
+ */
97
+ export declare function derivePhase(weekNumber: number, totalSessions: number, sessionsInLast4Weeks: number): TTrainingPhase;
98
+ export declare const DAYS_PER_WEEK_OPTIONS: readonly [2, 3, 4, 5];
99
+ export type TDaysPerWeek = (typeof DAYS_PER_WEEK_OPTIONS)[number];
100
+ export type TDurationHint = "short" | "medium" | "long";
101
+ export type TWeeklyIntentTemplate = [
102
+ TSessionIntent,
103
+ TSessionIntent,
104
+ TSessionIntent,
105
+ TSessionIntent,
106
+ TSessionIntent,
107
+ TSessionIntent,
108
+ TSessionIntent
109
+ ];
110
+ export type TGoalPhaseHints = Record<TTrainingPhase, string>;
111
+ export declare const GOAL_PHASE_HINTS: Record<TUserFitnessGoal, TGoalPhaseHints>;
@@ -0,0 +1,595 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GOAL_PHASE_HINTS = exports.DAYS_PER_WEEK_OPTIONS = exports.RECENCY_CREDIT = exports.PHASE_ENTRY_CREDIT = exports.GOAL_CATEGORY_DISPLAY = exports.GOAL_DISPLAY = exports.INTENT_DISPLAY = exports.PHASE_DISPLAY = exports.TRAINING_PHASES = exports.GOAL_TO_AI_FITNESS_GOAL = exports.GOAL_SESSION_INTENTS = exports.USER_FITNESS_GOALS = void 0;
4
+ exports.derivePhase = derivePhase;
5
+ exports.USER_FITNESS_GOALS = [
6
+ // Appearance
7
+ "lose_weight",
8
+ "build_muscle",
9
+ "lean_and_toned",
10
+ "glutes_and_legs",
11
+ // Performance
12
+ "get_stronger",
13
+ "boost_endurance",
14
+ "athletic_performance",
15
+ // Health
16
+ "general_fitness",
17
+ "posture_and_back",
18
+ "stress_relief",
19
+ "rehabilitation",
20
+ // Focused
21
+ "core_strength",
22
+ "mobility_flexibility",
23
+ ];
24
+ // The canonical weekly template per goal (7 slots, Mon→Sun)
25
+ // Phase-aware structures live in the app layer, not shared
26
+ // This is the "default" / Phase 2-3 reference structure
27
+ exports.GOAL_SESSION_INTENTS = {
28
+ lose_weight: [
29
+ "cardio",
30
+ "full-body",
31
+ "rest",
32
+ "cardio",
33
+ "full-body",
34
+ "cardio",
35
+ "rest",
36
+ ],
37
+ build_muscle: ["push", "pull", "legs", "rest", "upper", "lower", "rest"],
38
+ lean_and_toned: [
39
+ "full-body",
40
+ "cardio",
41
+ "full-body",
42
+ "rest",
43
+ "full-body",
44
+ "cardio",
45
+ "rest",
46
+ ],
47
+ glutes_and_legs: ["legs", "upper", "legs", "rest", "legs", "cardio", "rest"],
48
+ get_stronger: ["push", "pull", "legs", "rest", "push", "pull", "rest"],
49
+ boost_endurance: [
50
+ "cardio",
51
+ "legs",
52
+ "cardio",
53
+ "upper",
54
+ "cardio",
55
+ "full-body",
56
+ "rest",
57
+ ],
58
+ athletic_performance: [
59
+ "legs",
60
+ "upper",
61
+ "cardio",
62
+ "core",
63
+ "full-body",
64
+ "rest",
65
+ "rest",
66
+ ],
67
+ general_fitness: [
68
+ "full-body",
69
+ "rest",
70
+ "full-body",
71
+ "cardio",
72
+ "full-body",
73
+ "rest",
74
+ "rest",
75
+ ],
76
+ posture_and_back: [
77
+ "pull",
78
+ "core",
79
+ "mobility",
80
+ "pull",
81
+ "core",
82
+ "mobility",
83
+ "rest",
84
+ ],
85
+ stress_relief: [
86
+ "cardio",
87
+ "mobility",
88
+ "full-body",
89
+ "rest",
90
+ "cardio",
91
+ "mobility",
92
+ "rest",
93
+ ],
94
+ rehabilitation: [
95
+ "mobility",
96
+ "core",
97
+ "mobility",
98
+ "rest",
99
+ "mobility",
100
+ "core",
101
+ "rest",
102
+ ],
103
+ core_strength: ["core", "pull", "legs", "core", "push", "rest", "rest"],
104
+ mobility_flexibility: [
105
+ "mobility",
106
+ "full-body",
107
+ "mobility",
108
+ "rest",
109
+ "mobility",
110
+ "full-body",
111
+ "rest",
112
+ ],
113
+ };
114
+ exports.GOAL_TO_AI_FITNESS_GOAL = {
115
+ lose_weight: "fat_loss",
116
+ build_muscle: "hypertrophy",
117
+ lean_and_toned: "fat_loss",
118
+ glutes_and_legs: "hypertrophy",
119
+ get_stronger: "strength",
120
+ boost_endurance: "endurance",
121
+ athletic_performance: "sport_performance",
122
+ general_fitness: "general_fitness",
123
+ posture_and_back: "strength",
124
+ stress_relief: "general_fitness",
125
+ rehabilitation: "rehabilitation",
126
+ core_strength: "core_strength",
127
+ mobility_flexibility: "mobility",
128
+ };
129
+ // Foundation → Momentum → Push → Peak
130
+ exports.TRAINING_PHASES = [
131
+ "foundation",
132
+ "momentum",
133
+ "push",
134
+ "peak",
135
+ ];
136
+ exports.PHASE_DISPLAY = {
137
+ foundation: {
138
+ nameKey: "goalPhase.foundation.name",
139
+ nameEn: "Foundation",
140
+ subtitleKey: "goalPhase.foundation.subtitle",
141
+ subtitleEn: "Build the base",
142
+ purposeKey: "goalPhase.foundation.purpose",
143
+ purposeEn: "Preparing your joints, learning movement patterns, and building the work capacity for what's ahead. Every great physique starts here — don't skip it.",
144
+ durationKey: "goalPhase.foundation.duration",
145
+ durationEn: "Weeks 1–2",
146
+ weekRange: [1, 2],
147
+ intensityPercent: 40,
148
+ },
149
+ momentum: {
150
+ nameKey: "goalPhase.momentum.name",
151
+ nameEn: "Momentum",
152
+ subtitleKey: "goalPhase.momentum.subtitle",
153
+ subtitleEn: "Volume builds here",
154
+ purposeKey: "goalPhase.momentum.purpose",
155
+ purposeEn: "Your body has adapted. Now we increase the work. Splits begin, sessions get more specific. This is where the habit locks in.",
156
+ durationKey: "goalPhase.momentum.duration",
157
+ durationEn: "Weeks 3–5",
158
+ weekRange: [3, 5],
159
+ intensityPercent: 60,
160
+ },
161
+ push: {
162
+ nameKey: "goalPhase.push.name",
163
+ nameEn: "Push",
164
+ subtitleKey: "goalPhase.push.subtitle",
165
+ subtitleEn: "This is where change happens",
166
+ purposeKey: "goalPhase.push.purpose",
167
+ purposeEn: "Goal-specific intensity. Your muscles are conditioned — now we challenge them. Expect to feel this one.",
168
+ durationKey: "goalPhase.push.duration",
169
+ durationEn: "Weeks 6–8",
170
+ weekRange: [6, 8],
171
+ intensityPercent: 80,
172
+ },
173
+ peak: {
174
+ nameKey: "goalPhase.peak.name",
175
+ nameEn: "Peak",
176
+ subtitleKey: "goalPhase.peak.subtitle",
177
+ subtitleEn: "Maximum output",
178
+ purposeKey: "goalPhase.peak.purpose",
179
+ purposeEn: "Highest specificity to your goal. You have earned this — now execute. A deload is built into the final week.",
180
+ durationKey: "goalPhase.peak.duration",
181
+ durationEn: "Weeks 9+",
182
+ weekRange: [9, 99],
183
+ intensityPercent: 95,
184
+ },
185
+ };
186
+ exports.INTENT_DISPLAY = {
187
+ push: {
188
+ labelKey: "sessionIntent.push.label",
189
+ labelEn: "Push Day",
190
+ shortLabelKey: "sessionIntent.push.shortLabel",
191
+ shortLabelEn: "Push",
192
+ emoji: "💪",
193
+ },
194
+ pull: {
195
+ labelKey: "sessionIntent.pull.label",
196
+ labelEn: "Pull Day",
197
+ shortLabelKey: "sessionIntent.pull.shortLabel",
198
+ shortLabelEn: "Pull",
199
+ emoji: "🔗",
200
+ },
201
+ legs: {
202
+ labelKey: "sessionIntent.legs.label",
203
+ labelEn: "Leg Day",
204
+ shortLabelKey: "sessionIntent.legs.shortLabel",
205
+ shortLabelEn: "Legs",
206
+ emoji: "🦵",
207
+ },
208
+ lower: {
209
+ labelKey: "sessionIntent.lower.label",
210
+ labelEn: "Lower Body",
211
+ shortLabelKey: "sessionIntent.lower.shortLabel",
212
+ shortLabelEn: "Lower",
213
+ emoji: "🦵",
214
+ },
215
+ upper: {
216
+ labelKey: "sessionIntent.upper.label",
217
+ labelEn: "Upper Body",
218
+ shortLabelKey: "sessionIntent.upper.shortLabel",
219
+ shortLabelEn: "Upper",
220
+ emoji: "💪",
221
+ },
222
+ core: {
223
+ labelKey: "sessionIntent.core.label",
224
+ labelEn: "Core & Abs",
225
+ shortLabelKey: "sessionIntent.core.shortLabel",
226
+ shortLabelEn: "Core",
227
+ emoji: "⚡",
228
+ },
229
+ cardio: {
230
+ labelKey: "sessionIntent.cardio.label",
231
+ labelEn: "Cardio",
232
+ shortLabelKey: "sessionIntent.cardio.shortLabel",
233
+ shortLabelEn: "Cardio",
234
+ emoji: "🏃",
235
+ },
236
+ strength: {
237
+ labelKey: "sessionIntent.strength.label",
238
+ labelEn: "Strength",
239
+ shortLabelKey: "sessionIntent.strength.shortLabel",
240
+ shortLabelEn: "Strength",
241
+ emoji: "🏋️",
242
+ },
243
+ fatburn: {
244
+ labelKey: "sessionIntent.fatburn.label",
245
+ labelEn: "Fat Burn",
246
+ shortLabelKey: "sessionIntent.fatburn.shortLabel",
247
+ shortLabelEn: "Burn",
248
+ emoji: "🔥",
249
+ },
250
+ mobility: {
251
+ labelKey: "sessionIntent.mobility.label",
252
+ labelEn: "Mobility",
253
+ shortLabelKey: "sessionIntent.mobility.shortLabel",
254
+ shortLabelEn: "Mobility",
255
+ emoji: "🧘",
256
+ },
257
+ "full-body": {
258
+ labelKey: "sessionIntent.fullBody.label",
259
+ labelEn: "Full Body",
260
+ shortLabelKey: "sessionIntent.fullBody.shortLabel",
261
+ shortLabelEn: "Full",
262
+ emoji: "⭐",
263
+ },
264
+ rest: {
265
+ labelKey: "sessionIntent.rest.label",
266
+ labelEn: "Rest Day",
267
+ shortLabelKey: "sessionIntent.rest.shortLabel",
268
+ shortLabelEn: "Rest",
269
+ emoji: "😴",
270
+ },
271
+ };
272
+ exports.GOAL_DISPLAY = {
273
+ // ── Appearance ────────────────────────────────────────────────────────────
274
+ lose_weight: {
275
+ nameKey: "userGoal.loseWeight.name",
276
+ nameEn: "Lose Weight",
277
+ descriptionKey: "userGoal.loseWeight.description",
278
+ descriptionEn: "Burn fat and drop weight through cardio and full-body training.",
279
+ taglineKey: "userGoal.loseWeight.tagline",
280
+ taglineEn: "Every session burns, every week counts.",
281
+ emoji: "🔥",
282
+ category: "appearance",
283
+ },
284
+ build_muscle: {
285
+ nameKey: "userGoal.buildMuscle.name",
286
+ nameEn: "Build Muscle",
287
+ descriptionKey: "userGoal.buildMuscle.description",
288
+ descriptionEn: "Add size and strength with structured push, pull, and leg splits.",
289
+ taglineKey: "userGoal.buildMuscle.tagline",
290
+ taglineEn: "Progressive overload. Every rep builds the next.",
291
+ emoji: "💪",
292
+ category: "appearance",
293
+ },
294
+ lean_and_toned: {
295
+ nameKey: "userGoal.leanAndToned.name",
296
+ nameEn: "Get Lean & Toned",
297
+ descriptionKey: "userGoal.leanAndToned.description",
298
+ descriptionEn: "Burn fat while preserving muscle for a lean, defined look.",
299
+ taglineKey: "userGoal.leanAndToned.tagline",
300
+ taglineEn: "Defined. Lean. Confident.",
301
+ emoji: "✨",
302
+ category: "appearance",
303
+ },
304
+ glutes_and_legs: {
305
+ nameKey: "userGoal.glutesAndLegs.name",
306
+ nameEn: "Grow Glutes & Legs",
307
+ descriptionKey: "userGoal.glutesAndLegs.description",
308
+ descriptionEn: "Build powerful glutes and legs with targeted lower-body training.",
309
+ taglineKey: "userGoal.glutesAndLegs.tagline",
310
+ taglineEn: "Strong legs carry everything.",
311
+ emoji: "🦵",
312
+ category: "appearance",
313
+ },
314
+ // ── Performance ───────────────────────────────────────────────────────────
315
+ get_stronger: {
316
+ nameKey: "userGoal.getStronger.name",
317
+ nameEn: "Get Stronger",
318
+ descriptionKey: "userGoal.getStronger.description",
319
+ descriptionEn: "Build real-world strength through compound lifts and progressive overload.",
320
+ taglineKey: "userGoal.getStronger.tagline",
321
+ taglineEn: "Lift more. Be more.",
322
+ emoji: "🏋️",
323
+ category: "performance",
324
+ },
325
+ boost_endurance: {
326
+ nameKey: "userGoal.boostEndurance.name",
327
+ nameEn: "Build Endurance",
328
+ descriptionKey: "userGoal.boostEndurance.description",
329
+ descriptionEn: "Increase stamina and cardiovascular capacity through progressive cardio and leg work.",
330
+ taglineKey: "userGoal.boostEndurance.tagline",
331
+ taglineEn: "Go further. Last longer.",
332
+ emoji: "🏃",
333
+ category: "performance",
334
+ },
335
+ athletic_performance: {
336
+ nameKey: "userGoal.athleticPerformance.name",
337
+ nameEn: "Athletic Performance",
338
+ descriptionKey: "userGoal.athleticPerformance.description",
339
+ descriptionEn: "Train like an athlete — explosive power, speed, agility, and conditioning.",
340
+ taglineKey: "userGoal.athleticPerformance.tagline",
341
+ taglineEn: "Train hard. Perform harder.",
342
+ emoji: "⚡",
343
+ category: "performance",
344
+ },
345
+ // ── Health ────────────────────────────────────────────────────────────────
346
+ general_fitness: {
347
+ nameKey: "userGoal.generalFitness.name",
348
+ nameEn: "Stay Active & Healthy",
349
+ descriptionKey: "userGoal.generalFitness.description",
350
+ descriptionEn: "A balanced mix of strength, cardio, and mobility for a healthy, active lifestyle.",
351
+ taglineKey: "userGoal.generalFitness.tagline",
352
+ taglineEn: "Move well. Feel well. Live well.",
353
+ emoji: "🌟",
354
+ category: "health",
355
+ },
356
+ posture_and_back: {
357
+ nameKey: "userGoal.postureAndBack.name",
358
+ nameEn: "Fix Posture & Back",
359
+ descriptionKey: "userGoal.postureAndBack.description",
360
+ descriptionEn: "Strengthen the posterior chain and core to fix posture and eliminate back pain.",
361
+ taglineKey: "userGoal.postureAndBack.tagline",
362
+ taglineEn: "Stand tall. Move pain-free.",
363
+ emoji: "🧍",
364
+ category: "health",
365
+ },
366
+ stress_relief: {
367
+ nameKey: "userGoal.stressRelief.name",
368
+ nameEn: "Reduce Stress",
369
+ descriptionKey: "userGoal.stressRelief.description",
370
+ descriptionEn: "Use movement to reset your mind — cardio endorphins and mobility to decompress.",
371
+ taglineKey: "userGoal.stressRelief.tagline",
372
+ taglineEn: "Move it out. Breathe it out.",
373
+ emoji: "🧘",
374
+ category: "health",
375
+ },
376
+ rehabilitation: {
377
+ nameKey: "userGoal.rehabilitation.name",
378
+ nameEn: "Recover from Injury",
379
+ descriptionKey: "userGoal.rehabilitation.description",
380
+ descriptionEn: "Gentle, progressive sessions to rebuild strength and movement after injury.",
381
+ taglineKey: "userGoal.rehabilitation.tagline",
382
+ taglineEn: "Slow and steady. Built to last.",
383
+ emoji: "🩹",
384
+ category: "health",
385
+ },
386
+ // ── Focused ───────────────────────────────────────────────────────────────
387
+ core_strength: {
388
+ nameKey: "userGoal.coreStrength.name",
389
+ nameEn: "Build a Strong Core",
390
+ descriptionKey: "userGoal.coreStrength.description",
391
+ descriptionEn: "Develop deep core stability and strength — the foundation of every movement.",
392
+ taglineKey: "userGoal.coreStrength.tagline",
393
+ taglineEn: "Strong core. Strong everything.",
394
+ emoji: "🎯",
395
+ category: "focused",
396
+ },
397
+ mobility_flexibility: {
398
+ nameKey: "userGoal.mobilityFlexibility.name",
399
+ nameEn: "Improve Flexibility",
400
+ descriptionKey: "userGoal.mobilityFlexibility.description",
401
+ descriptionEn: "Increase range of motion and joint health through consistent mobility work.",
402
+ taglineKey: "userGoal.mobilityFlexibility.tagline",
403
+ taglineEn: "Move freely. Age well.",
404
+ emoji: "🌀",
405
+ category: "focused",
406
+ },
407
+ };
408
+ exports.GOAL_CATEGORY_DISPLAY = {
409
+ appearance: {
410
+ labelKey: "goalCategory.appearance.label",
411
+ labelEn: "Appearance",
412
+ },
413
+ performance: {
414
+ labelKey: "goalCategory.performance.label",
415
+ labelEn: "Performance",
416
+ },
417
+ health: {
418
+ labelKey: "goalCategory.health.label",
419
+ labelEn: "Health & Wellbeing",
420
+ },
421
+ focused: {
422
+ labelKey: "goalCategory.focused.label",
423
+ labelEn: "Focused Training",
424
+ },
425
+ };
426
+ // ---------------------------------------------------------------------------
427
+ // derivePhase — pure helper
428
+ // Shared between Lambda and client — lives here so neither duplicates it
429
+ // ---------------------------------------------------------------------------
430
+ exports.PHASE_ENTRY_CREDIT = {
431
+ sedentary: 0,
432
+ "lightly-active": 0,
433
+ "moderately-active": 6, // enters Momentum directly
434
+ "very-active": 12, // enters Push directly
435
+ };
436
+ exports.RECENCY_CREDIT = {
437
+ sedentary: 0,
438
+ "lightly-active": 0,
439
+ "moderately-active": 4,
440
+ "very-active": 8,
441
+ };
442
+ /**
443
+ * Derive training phase from time + volume + recency.
444
+ * All three must pass — week count alone is NOT sufficient.
445
+ * A user doing 12 sessions over one year has not built a training base.
446
+ *
447
+ * On goal start day (no sessions yet), pass:
448
+ * totalSessions = PHASE_ENTRY_CREDIT[user.fitnessLevel]
449
+ * sessionsInLast4Weeks = RECENCY_CREDIT[user.fitnessLevel]
450
+ */
451
+ function derivePhase(weekNumber, totalSessions, sessionsInLast4Weeks) {
452
+ // Foundation → needs minimum 2 weeks, 6 sessions, 1 session/week pace
453
+ if (weekNumber < 2 || totalSessions < 6 || sessionsInLast4Weeks < 4)
454
+ return "foundation";
455
+ // Momentum → needs 6 weeks, 12 sessions, ~1.5 sessions/week pace
456
+ if (weekNumber < 6 || totalSessions < 12 || sessionsInLast4Weeks < 6)
457
+ return "momentum";
458
+ // Push → needs 9 weeks, 20 sessions, ~2 sessions/week pace
459
+ if (weekNumber < 9 || totalSessions < 20 || sessionsInLast4Weeks < 8)
460
+ return "push";
461
+ return "peak";
462
+ }
463
+ exports.DAYS_PER_WEEK_OPTIONS = [2, 3, 4, 5];
464
+ exports.GOAL_PHASE_HINTS = {
465
+ lose_weight: {
466
+ // "Build the cardio habit"
467
+ foundation: "goalPhaseHint.loseWeight.foundation.buildCardioHabit",
468
+ // "Intensity picks up"
469
+ momentum: "goalPhaseHint.loseWeight.momentum.intensityPicksUp",
470
+ // "Fat burning at its best"
471
+ push: "goalPhaseHint.loseWeight.push.fatBurningAtBest",
472
+ // "Final push to your goal"
473
+ peak: "goalPhaseHint.loseWeight.peak.finalPushToGoal",
474
+ },
475
+ build_muscle: {
476
+ // "Learn the fundamental movements"
477
+ foundation: "goalPhaseHint.buildMuscle.foundation.learnFundamentalMovements",
478
+ // "Volume increases, splits begin"
479
+ momentum: "goalPhaseHint.buildMuscle.momentum.volumeIncreaseSplitsBegin",
480
+ // "Real muscle growth happens here"
481
+ push: "goalPhaseHint.buildMuscle.push.realMuscleGrowthHappensHere",
482
+ // "Push to your peak output"
483
+ peak: "goalPhaseHint.buildMuscle.peak.pushToPeakOutput",
484
+ },
485
+ lean_and_toned: {
486
+ // "Start moving consistently"
487
+ foundation: "goalPhaseHint.leanAndToned.foundation.startMovingConsistently",
488
+ // "Splits begin, cardio added"
489
+ momentum: "goalPhaseHint.leanAndToned.momentum.splitsBeginCardioAdded",
490
+ // "Definition starts to show"
491
+ push: "goalPhaseHint.leanAndToned.push.definitionStartsToShow",
492
+ // "Your best shape yet"
493
+ peak: "goalPhaseHint.leanAndToned.peak.yourBestShapeYet",
494
+ },
495
+ glutes_and_legs: {
496
+ // "Learn hip-hinge and squat patterns"
497
+ foundation: "goalPhaseHint.glutesAndLegs.foundation.learnHipHingeSquatPatterns",
498
+ // "Volume builds in lower body"
499
+ momentum: "goalPhaseHint.glutesAndLegs.momentum.volumeBuildsLowerBody",
500
+ // "Legs up to 3 times per week"
501
+ push: "goalPhaseHint.glutesAndLegs.push.legsThreeTimesPerWeek",
502
+ // "Maximum lower body output"
503
+ peak: "goalPhaseHint.glutesAndLegs.peak.maximumLowerBodyOutput",
504
+ },
505
+ get_stronger: {
506
+ // "Master the big compound lifts"
507
+ foundation: "goalPhaseHint.getStronger.foundation.masterBigCompoundLifts",
508
+ // "Load increases week on week"
509
+ momentum: "goalPhaseHint.getStronger.momentum.loadIncreasesWeekOnWeek",
510
+ // "Heavy compound focus"
511
+ push: "goalPhaseHint.getStronger.push.heavyCompoundFocus",
512
+ // "Strength peak — test your limits"
513
+ peak: "goalPhaseHint.getStronger.peak.strengthPeakTestYourLimits",
514
+ },
515
+ boost_endurance: {
516
+ // "Build base aerobic fitness"
517
+ foundation: "goalPhaseHint.boostEndurance.foundation.buildBaseAerobicFitness",
518
+ // "Duration and distance increase"
519
+ momentum: "goalPhaseHint.boostEndurance.momentum.durationDistanceIncrease",
520
+ // "High volume cardio sessions"
521
+ push: "goalPhaseHint.boostEndurance.push.highVolumeCardioSessions",
522
+ // "Peak cardiovascular output"
523
+ peak: "goalPhaseHint.boostEndurance.peak.peakCardiovascularOutput",
524
+ },
525
+ athletic_performance: {
526
+ // "Movement quality comes first"
527
+ foundation: "goalPhaseHint.athleticPerformance.foundation.movementQualityFirst",
528
+ // "Power and speed sessions begin"
529
+ momentum: "goalPhaseHint.athleticPerformance.momentum.powerSpeedSessionsBegin",
530
+ // "Sport specific intensity"
531
+ push: "goalPhaseHint.athleticPerformance.push.sportSpecificIntensity",
532
+ // "Peak athletic output"
533
+ peak: "goalPhaseHint.athleticPerformance.peak.peakAthleticOutput",
534
+ },
535
+ general_fitness: {
536
+ // "Build the habit"
537
+ foundation: "goalPhaseHint.generalFitness.foundation.buildTheHabit",
538
+ // "Add more variety"
539
+ momentum: "goalPhaseHint.generalFitness.momentum.addMoreVariety",
540
+ // "Consistent challenge"
541
+ push: "goalPhaseHint.generalFitness.push.consistentChallenge",
542
+ // "Sustained healthy output"
543
+ peak: "goalPhaseHint.generalFitness.peak.sustainedHealthyOutput",
544
+ },
545
+ posture_and_back: {
546
+ // "Activate neglected muscles"
547
+ foundation: "goalPhaseHint.postureAndBack.foundation.activateNeglectedMuscles",
548
+ // "Posterior chain strengthens"
549
+ momentum: "goalPhaseHint.postureAndBack.momentum.posteriorChainStrengthens",
550
+ // "Pull heavy split begins"
551
+ push: "goalPhaseHint.postureAndBack.push.pullHeavySplitBegins",
552
+ // "Posture transformation complete"
553
+ peak: "goalPhaseHint.postureAndBack.peak.postureTransformationComplete",
554
+ },
555
+ stress_relief: {
556
+ // "Low pressure movement"
557
+ foundation: "goalPhaseHint.stressRelief.foundation.lowPressureMovement",
558
+ // "Rhythm and routine develop"
559
+ momentum: "goalPhaseHint.stressRelief.momentum.rhythmAndRoutineDevelop",
560
+ // "Endorphin peak"
561
+ push: "goalPhaseHint.stressRelief.push.endorphinPeak",
562
+ // "Sustained stress release"
563
+ peak: "goalPhaseHint.stressRelief.peak.sustainedStressRelease",
564
+ },
565
+ rehabilitation: {
566
+ // "Safe mobility work only"
567
+ foundation: "goalPhaseHint.rehabilitation.foundation.safeMobilityWorkOnly",
568
+ // "Strength carefully reintroduced"
569
+ momentum: "goalPhaseHint.rehabilitation.momentum.strengthCarefullyReintroduced",
570
+ // "Full movement patterns restored"
571
+ push: "goalPhaseHint.rehabilitation.push.fullMovementPatternsRestored",
572
+ // "Back to full function"
573
+ peak: "goalPhaseHint.rehabilitation.peak.backToFullFunction",
574
+ },
575
+ core_strength: {
576
+ // "Core activation patterns"
577
+ foundation: "goalPhaseHint.coreStrength.foundation.coreActivationPatterns",
578
+ // "Volume and difficulty rise"
579
+ momentum: "goalPhaseHint.coreStrength.momentum.volumeAndDifficultyRise",
580
+ // "Heavy compound core work"
581
+ push: "goalPhaseHint.coreStrength.push.heavyCompoundCoreWork",
582
+ // "Maximum core output"
583
+ peak: "goalPhaseHint.coreStrength.peak.maximumCoreOutput",
584
+ },
585
+ mobility_flexibility: {
586
+ // "Daily mobility habit"
587
+ foundation: "goalPhaseHint.mobilityFlexibility.foundation.dailyMobilityHabit",
588
+ // "Range of motion grows"
589
+ momentum: "goalPhaseHint.mobilityFlexibility.momentum.rangeOfMotionGrows",
590
+ // "Full body flexibility"
591
+ push: "goalPhaseHint.mobilityFlexibility.push.fullBodyFlexibility",
592
+ // "Elite movement quality"
593
+ peak: "goalPhaseHint.mobilityFlexibility.peak.eliteMovementQuality",
594
+ },
595
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./goalJourney";
2
+ export type * from "./TApiGoalContextRes";
3
+ export * from "./utilGoal";
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ // shared/src/types/goalFeature/index.ts
18
+ __exportStar(require("./goalJourney"), exports);
19
+ __exportStar(require("./utilGoal"), exports);
@@ -0,0 +1,22 @@
1
+ import { TMuscleFatigueResult } from "../../utils";
2
+ import { TSessionIntent } from "./goalJourney";
3
+ import { TGoalContext, TGoalProfile, TIntentRecommendation } from "./TApiGoalContextRes";
4
+ export declare function resolveGoalContext(goalProfile: TGoalProfile, recentSessions: {
5
+ intentId: TSessionIntent | undefined;
6
+ createdAt: string;
7
+ }[], now?: Date): TGoalContext | null;
8
+ /**
9
+ * Recommend the next session intent.
10
+ *
11
+ * Decision pipeline (strict priority):
12
+ * 1. GLOBAL RECOVERY avgFatigue > 72 → "mobility"
13
+ * 2. RETURNING USER gap >= 7 days → first non-rest slot
14
+ * 3. GAP BRIDGE gap 3-7 days → most overdue slot
15
+ * 4. STRUCTURE FOLLOW gap < 3 days → next slot (circular)
16
+ * 5. MUSCLE OVERRIDE cluster fatigue > 65 → highest-readiness alt
17
+ *
18
+ * Output is always a valid TWorkoutIntent.
19
+ * Never outputs: "rest", "arms", "back", "unstructured".
20
+ * rationale is an i18n key — resolve via t() on the FE.
21
+ */
22
+ export declare function recommendNextIntent(fatigue: TMuscleFatigueResult, goalContext: TGoalContext): TIntentRecommendation;
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveGoalContext = resolveGoalContext;
4
+ exports.recommendNextIntent = recommendNextIntent;
5
+ const goalJourney_1 = require("./goalJourney");
6
+ function resolveGoalContext(goalProfile, recentSessions, now = new Date()) {
7
+ var _a, _b;
8
+ if (!(goalProfile === null || goalProfile === void 0 ? void 0 : goalProfile.activeGoal) || !(goalProfile === null || goalProfile === void 0 ? void 0 : goalProfile.goalStartDate))
9
+ return null;
10
+ const goalStart = new Date(goalProfile.goalStartDate);
11
+ const msPerDay = 86400000;
12
+ const daysSince = (now.getTime() - goalStart.getTime()) / msPerDay;
13
+ const currentWeek = Math.max(1, Math.ceil(daysSince / 7));
14
+ // Sessions this calendar week (Monday 00:00 UTC)
15
+ const monday = new Date(now);
16
+ monday.setUTCDate(now.getUTCDate() - ((now.getUTCDay() + 6) % 7));
17
+ monday.setUTCHours(0, 0, 0, 0);
18
+ const last4WeeksCutoff = new Date(now.getTime() - 28 * msPerDay);
19
+ const goalStartTime = goalStart.getTime();
20
+ const sessionsSinceGoalStart = recentSessions.filter((s) => new Date(s.createdAt).getTime() >= goalStartTime);
21
+ const totalSince = sessionsSinceGoalStart.length;
22
+ const sessionsIn4Weeks = sessionsSinceGoalStart.filter((s) => new Date(s.createdAt) >= last4WeeksCutoff).length;
23
+ const sessionsThisWeek = sessionsSinceGoalStart.filter((s) => new Date(s.createdAt) >= monday).length;
24
+ const currentPhase = (0, goalJourney_1.derivePhase)(currentWeek, totalSince, sessionsIn4Weeks);
25
+ const weeklyStructure = [...goalJourney_1.GOAL_SESSION_INTENTS[goalProfile.activeGoal]];
26
+ // Edge case: no sessions yet — use first non-rest slot as entry point
27
+ if (recentSessions.length === 0) {
28
+ const firstActive = (_a = weeklyStructure.find((s) => s !== "rest")) !== null && _a !== void 0 ? _a : "full-body";
29
+ return {
30
+ goal: goalProfile.activeGoal,
31
+ daysPerWeek: goalProfile.daysPerWeek,
32
+ currentPhase,
33
+ currentWeek,
34
+ weeklyStructure,
35
+ lastSessionDate: goalProfile.goalStartDate,
36
+ lastSessionIntent: firstActive,
37
+ sessionsCompletedThisWeek: 0,
38
+ totalSessionsSinceGoalStart: 0,
39
+ sessionsInLast4Weeks: 0,
40
+ };
41
+ }
42
+ const lastSession = recentSessions[0]; // sorted desc by createdAt
43
+ return {
44
+ goal: goalProfile.activeGoal,
45
+ daysPerWeek: goalProfile.daysPerWeek,
46
+ currentPhase,
47
+ currentWeek,
48
+ weeklyStructure,
49
+ lastSessionDate: lastSession.createdAt,
50
+ lastSessionIntent: (_b = lastSession.intentId) !== null && _b !== void 0 ? _b : "unstructured",
51
+ sessionsCompletedThisWeek: sessionsThisWeek,
52
+ totalSessionsSinceGoalStart: totalSince,
53
+ sessionsInLast4Weeks: sessionsIn4Weeks,
54
+ };
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Muscle → intent cluster
58
+ // Keyed by keyof typeof EBodyParts — TypeScript enforces valid muscle keys
59
+ // ---------------------------------------------------------------------------
60
+ const MUSCLE_TO_INTENTS = {
61
+ "serratus-anterior": ["push", "upper"],
62
+ "tricep-brachii-long": ["push", "upper"],
63
+ "tricep-brachii-lateral": ["push", "upper"],
64
+ "latissimus-dorsi": ["pull", "upper"],
65
+ rhomboids: ["pull", "upper"],
66
+ trapezius: ["pull", "upper"],
67
+ "bicep-long-outer": ["pull", "upper"],
68
+ "bicep-short-inner": ["pull", "upper"],
69
+ "deltoids-posterior": ["pull", "upper"],
70
+ "fore-arm-inner": ["pull", "upper"],
71
+ "fore-arm-outer": ["pull", "upper"],
72
+ "lower-back": ["core"],
73
+ quadriceps: ["legs", "lower", "cardio", "fatburn"],
74
+ hamstrings: ["legs", "lower", "cardio", "fatburn"],
75
+ "glutes-maximus": ["legs", "lower", "cardio", "fatburn"], // burpees, jump squats
76
+ "glutes-medius": ["legs", "lower", "cardio", "fatburn"],
77
+ "medius-upper": ["legs", "lower", "cardio", "fatburn"],
78
+ "maximus-lower": ["legs", "lower", "cardio", "fatburn"],
79
+ adductors: ["legs", "lower", "cardio", "fatburn"],
80
+ "calf-inner": ["legs", "lower", "cardio"],
81
+ "calf-outer": ["legs", "lower", "cardio"],
82
+ calves: ["legs", "lower", "cardio"],
83
+ "pectoralis-major": ["push", "upper", "fatburn"],
84
+ "deltoids-anterior": ["push", "upper", "fatburn"],
85
+ "deltoids-middle": ["push", "upper", "fatburn"],
86
+ "abs-upper": ["core", "fatburn"],
87
+ "abs-lower": ["core", "fatburn"],
88
+ obliques: ["core", "fatburn"],
89
+ };
90
+ const MUSCLE_SCORED_INTENTS = [
91
+ "push",
92
+ "pull",
93
+ "legs",
94
+ "lower",
95
+ "upper",
96
+ "core",
97
+ "cardio",
98
+ "fatburn",
99
+ ];
100
+ // ---------------------------------------------------------------------------
101
+ // Goal → intent readiness bias
102
+ // Modest nudge — never overrides the structural decision in steps 1-3
103
+ // ---------------------------------------------------------------------------
104
+ const GOAL_INTENT_BIAS = {
105
+ strength: { push: 10, pull: 10, legs: 8 },
106
+ hypertrophy: { push: 10, pull: 10, legs: 10 },
107
+ fat_loss: { cardio: 20, fatburn: 18, "full-body": 10 },
108
+ endurance: { cardio: 25, lower: 8 },
109
+ mobility: { mobility: 30, core: 8 },
110
+ rehabilitation: { mobility: 25, core: 12 },
111
+ sport_performance: { legs: 12, core: 10, cardio: 8 },
112
+ general_fitness: { "full-body": 15, cardio: 8 },
113
+ core_strength: { core: 25, pull: 6, legs: 6 },
114
+ };
115
+ const INTENT_RATIONALE = {
116
+ push: "intentRationale.push",
117
+ pull: "intentRationale.pull",
118
+ legs: "intentRationale.legs",
119
+ lower: "intentRationale.lower",
120
+ upper: "intentRationale.upper",
121
+ core: "intentRationale.core",
122
+ cardio: "intentRationale.cardio",
123
+ strength: "intentRationale.strength",
124
+ fatburn: "intentRationale.fatburn",
125
+ mobility: "intentRationale.mobility",
126
+ "full-body": "intentRationale.fullBody",
127
+ };
128
+ const GLOBAL_RECOVERY_THRESHOLD = 72;
129
+ const CLUSTER_FATIGUE_HARD_CAP = 65;
130
+ const RETURNING_USER_GAP_DAYS = 7;
131
+ const GAP_BRIDGE_THRESHOLD_DAYS = 3;
132
+ const HIGH_CONFIDENCE_GAP = 15;
133
+ function daysSince(isoDate, now = new Date()) {
134
+ return (now.getTime() - new Date(isoDate).getTime()) / 86400000;
135
+ }
136
+ function globalAvgFatigue(fatigue) {
137
+ const values = Object.values(fatigue).map((v) => v.fatigue);
138
+ if (values.length === 0)
139
+ return 0;
140
+ return values.reduce((a, b) => a + b, 0) / values.length;
141
+ }
142
+ function clusterAvgFatigue(intent, fatigue) {
143
+ const muscles = Object.entries(MUSCLE_TO_INTENTS)
144
+ .filter(([, intents]) => intents.includes(intent))
145
+ .map(([muscle]) => muscle);
146
+ if (muscles.length === 0)
147
+ return 0;
148
+ const scores = muscles.map((m) => { var _a, _b; return (_b = (_a = fatigue[m]) === null || _a === void 0 ? void 0 : _a.fatigue) !== null && _b !== void 0 ? _b : 0; });
149
+ return scores.reduce((a, b) => a + b, 0) / scores.length;
150
+ }
151
+ function buildReadinessScores(fatigue, goalContext) {
152
+ var _a, _b, _c, _d, _e, _f;
153
+ const bias = (_a = GOAL_INTENT_BIAS[goalJourney_1.GOAL_TO_AI_FITNESS_GOAL[goalContext.goal]]) !== null && _a !== void 0 ? _a : {};
154
+ const scores = {};
155
+ for (const intent of MUSCLE_SCORED_INTENTS) {
156
+ scores[intent] =
157
+ 100 - clusterAvgFatigue(intent, fatigue) + ((_b = bias[intent]) !== null && _b !== void 0 ? _b : 0);
158
+ }
159
+ scores["full-body"] = 50 + ((_c = bias["full-body"]) !== null && _c !== void 0 ? _c : 0);
160
+ scores["strength"] = 50 + ((_d = bias["strength"]) !== null && _d !== void 0 ? _d : 0);
161
+ scores["fatburn"] = 50 + ((_e = bias["fatburn"]) !== null && _e !== void 0 ? _e : 0);
162
+ scores["mobility"] = 60 + ((_f = bias["mobility"]) !== null && _f !== void 0 ? _f : 0);
163
+ return scores;
164
+ }
165
+ function topByScore(scores) {
166
+ return Object.entries(scores).sort(([, a], [, b]) => b - a)[0][0];
167
+ }
168
+ function computeConfidence(scores) {
169
+ var _a;
170
+ const sorted = Object.values(scores).sort((a, b) => b - a);
171
+ const gap = sorted[0] - ((_a = sorted[1]) !== null && _a !== void 0 ? _a : 0);
172
+ if (gap >= HIGH_CONFIDENCE_GAP)
173
+ return Math.min(0.95, 0.7 + gap / 100);
174
+ return Math.max(0.4, 0.5 + gap / 100);
175
+ }
176
+ function validateOrFallback(desired, scores, fatigue) {
177
+ if (desired === "rest") {
178
+ return { intent: topByScore(scores), wasOverridden: true };
179
+ }
180
+ const desiredRec = desired;
181
+ if (MUSCLE_SCORED_INTENTS.includes(desiredRec)) {
182
+ if (clusterAvgFatigue(desiredRec, fatigue) <= CLUSTER_FATIGUE_HARD_CAP) {
183
+ return { intent: desiredRec, wasOverridden: false };
184
+ }
185
+ return { intent: topByScore(scores), wasOverridden: true };
186
+ }
187
+ return { intent: desiredRec, wasOverridden: false };
188
+ }
189
+ /**
190
+ * Find the next non-rest slot in weeklyStructure after afterIntent.
191
+ * Circular — wraps around. Used by both gap bridge and structure follow.
192
+ * Returns "full-body" if structure is entirely rest days (defensive).
193
+ */
194
+ function nextNonRestSlot(weeklyStructure, afterIntent) {
195
+ const lastIdx = weeklyStructure.findIndex((s) => s === afterIntent && s !== "rest");
196
+ const start = lastIdx === -1 ? 0 : (lastIdx + 1) % weeklyStructure.length;
197
+ for (let i = 0; i < weeklyStructure.length; i++) {
198
+ const slot = weeklyStructure[(start + i) % weeklyStructure.length];
199
+ if (slot !== "rest")
200
+ return slot;
201
+ }
202
+ return "full-body";
203
+ }
204
+ // ---------------------------------------------------------------------------
205
+ // Public API
206
+ // ---------------------------------------------------------------------------
207
+ /**
208
+ * Recommend the next session intent.
209
+ *
210
+ * Decision pipeline (strict priority):
211
+ * 1. GLOBAL RECOVERY avgFatigue > 72 → "mobility"
212
+ * 2. RETURNING USER gap >= 7 days → first non-rest slot
213
+ * 3. GAP BRIDGE gap 3-7 days → most overdue slot
214
+ * 4. STRUCTURE FOLLOW gap < 3 days → next slot (circular)
215
+ * 5. MUSCLE OVERRIDE cluster fatigue > 65 → highest-readiness alt
216
+ *
217
+ * Output is always a valid TWorkoutIntent.
218
+ * Never outputs: "rest", "arms", "back", "unstructured".
219
+ * rationale is an i18n key — resolve via t() on the FE.
220
+ */
221
+ function recommendNextIntent(fatigue, goalContext) {
222
+ var _a;
223
+ const { weeklyStructure, lastSessionDate, lastSessionIntent } = goalContext;
224
+ const gap = daysSince(lastSessionDate);
225
+ const avgFat = globalAvgFatigue(fatigue);
226
+ const scores = buildReadinessScores(fatigue, goalContext);
227
+ // ── 1. GLOBAL RECOVERY ───────────────────────────────────────────────────
228
+ if (avgFat >= GLOBAL_RECOVERY_THRESHOLD) {
229
+ return {
230
+ intent: "mobility",
231
+ confidence: 0.92,
232
+ rationale: INTENT_RATIONALE["mobility"],
233
+ decisionPath: "global_recovery",
234
+ intentScores: scores,
235
+ };
236
+ }
237
+ // ── 2. RETURNING USER ────────────────────────────────────────────────────
238
+ if (gap >= RETURNING_USER_GAP_DAYS) {
239
+ const reEntry = (_a = weeklyStructure.find((s) => s !== "rest")) !== null && _a !== void 0 ? _a : "full-body";
240
+ const { intent } = validateOrFallback(reEntry, scores, fatigue);
241
+ return {
242
+ intent,
243
+ confidence: 0.82,
244
+ rationale: INTENT_RATIONALE[intent],
245
+ decisionPath: "returning_user",
246
+ intentScores: scores,
247
+ };
248
+ }
249
+ // ── 3. GAP BRIDGE ────────────────────────────────────────────────────────
250
+ if (gap >= GAP_BRIDGE_THRESHOLD_DAYS) {
251
+ const missed = nextNonRestSlot(weeklyStructure, lastSessionIntent);
252
+ const { intent, wasOverridden } = validateOrFallback(missed, scores, fatigue);
253
+ return {
254
+ intent,
255
+ confidence: computeConfidence(scores),
256
+ rationale: INTENT_RATIONALE[intent],
257
+ decisionPath: wasOverridden ? "muscle_override" : "gap_bridge",
258
+ intentScores: scores,
259
+ };
260
+ }
261
+ // ── 4. STRUCTURE FOLLOW + 5. MUSCLE OVERRIDE ─────────────────────────────
262
+ const next = nextNonRestSlot(weeklyStructure, lastSessionIntent);
263
+ const { intent, wasOverridden } = validateOrFallback(next, scores, fatigue);
264
+ return {
265
+ intent,
266
+ confidence: computeConfidence(scores),
267
+ rationale: INTENT_RATIONALE[intent],
268
+ decisionPath: wasOverridden ? "muscle_override" : "structure_follow",
269
+ intentScores: scores,
270
+ };
271
+ }
@@ -16,3 +16,4 @@ export type * from "./TApiAiExerciseAnalysis";
16
16
  export type * from "./TApiAdminAiHelp";
17
17
  export type * from "./TApiAiQuickStartWorkout";
18
18
  export type * from "./TApiClientConstellation";
19
+ export * from "./goalFeature";
@@ -1,2 +1,18 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ // Features
18
+ __exportStar(require("./goalFeature"), exports);
@@ -1,18 +1,4 @@
1
- /**
2
- * ============================================================================
3
- * FITFRIX EXERCISE SCORING SYSTEM — Helpers
4
- * ============================================================================
5
- *
6
- * Pure utility functions with no business logic. These handle:
7
- * - Safe parsing of string values from TRecord
8
- * - Duration parsing ("MM:SS" → seconds)
9
- * - Clamping and validation
10
- * - Effort fraction conversion (RPE/RIR → 0–1)
11
- * - User context extraction with fallbacks
12
- *
13
- * All functions are deterministic and side-effect free.
14
- */
15
- import { TGender } from "../../types";
1
+ import { TActivityLevel, TGender } from "../../types";
16
2
  import type { IUserContext } from "./types";
17
3
  /**
18
4
  * Safely parse a string to a float.
@@ -112,12 +98,11 @@ export declare function getEffortFraction(record: {
112
98
  * @param user - The raw TUserMetric object
113
99
  */
114
100
  export declare function extractUserContext(user: {
115
- weightKg?: number;
116
- heightCm?: number;
117
- gender?: TGender;
118
- dob?: Date | string;
119
- fitnessLevel?: import("../../types").TActivityLevel;
120
- fitnessGoals?: import("../../constants/AiExerciseVocabulary").TAiFitnessGoal[];
101
+ weightKg: number;
102
+ heightCm: number;
103
+ gender: TGender;
104
+ dob: Date | string;
105
+ fitnessLevel: TActivityLevel;
121
106
  bodyFatPercentage?: number;
122
107
  }): IUserContext;
123
108
  /**
@@ -199,7 +199,7 @@ function getEffortFraction(record) {
199
199
  * @param user - The raw TUserMetric object
200
200
  */
201
201
  function extractUserContext(user) {
202
- var _a, _b, _c;
202
+ var _a, _b;
203
203
  const weightKg = user.weightKg && user.weightKg > 20 && user.weightKg < 300
204
204
  ? user.weightKg
205
205
  : constants_1.DEFAULT_USER_WEIGHT_KG;
@@ -213,8 +213,7 @@ function extractUserContext(user) {
213
213
  gender: user.gender || "unmentioned",
214
214
  age,
215
215
  fitnessLevel: (_a = user.fitnessLevel) !== null && _a !== void 0 ? _a : "moderately-active",
216
- fitnessGoals: (_b = user.fitnessGoals) !== null && _b !== void 0 ? _b : ["general_fitness"],
217
- bodyFatPercentage: (_c = user.bodyFatPercentage) !== null && _c !== void 0 ? _c : 20,
216
+ bodyFatPercentage: (_b = user.bodyFatPercentage) !== null && _b !== void 0 ? _b : 20,
218
217
  };
219
218
  }
220
219
  /**
@@ -39,7 +39,6 @@ const calculateExerciseScoreV2 = (param) => {
39
39
  const { exercise, record, user, historicalContext } = param;
40
40
  const userContext = (0, helpers_1.extractUserContext)(user);
41
41
  const parsedSets = (0, parseRecords_1.parseRecords)(record, exercise.timingGuardrails, historicalContext);
42
- // ── Pillar 2: Muscle Fatigue ─────────────────────────────────────────────
43
42
  const muscleScores = (0, calculateMuscleFatigue_1.calculateMuscleFatigue)(parsedSets, {
44
43
  primaryMuscles: exercise.primaryMuscles,
45
44
  secondaryMuscles: exercise.secondaryMuscles,
@@ -51,11 +50,9 @@ const calculateExerciseScoreV2 = (param) => {
51
50
  scoringSpecialHandling: exercise.scoringSpecialHandling,
52
51
  isUnilateral: exercise.isUnilateral,
53
52
  }, userContext, exercise.timingGuardrails, historicalContext);
54
- // ── Pillar 3: Quality Score ──────────────────────────────────────────────
55
53
  // userContext is intentionally not passed — quality scoring is goal-agnostic.
56
54
  // Goal-specific logic lives in the gate system and quick plan generator.
57
55
  const { score, qualityBreakdown, restDisciplineActive } = (0, calculateQualityScore_1.calculateQualityScore)(parsedSets, record, exercise.timingGuardrails, historicalContext);
58
- // ── Result (Option A flat shape) ─────────────────────────────────────────
59
56
  return {
60
57
  score,
61
58
  qualityBreakdown,
@@ -8,7 +8,6 @@
8
8
  * scoring pillars (Calories, Muscle Fatigue, Quality).
9
9
  */
10
10
  import { TActivityLevel, TGender, TRecord } from "../../types";
11
- import type { TAiFitnessGoal } from "../../constants/AiExerciseVocabulary";
12
11
  /**
13
12
  * A single scored session with real per-muscle fatigue values.
14
13
  * muscleScores = {} for pre-P3-1 sessions (backward compat fallback).
@@ -141,6 +140,5 @@ export interface IUserContext {
141
140
  gender: TGender;
142
141
  age: number;
143
142
  fitnessLevel: TActivityLevel;
144
- fitnessGoals: TAiFitnessGoal[];
145
143
  bodyFatPercentage: number;
146
144
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dgpholdings/greatoak-shared",
3
- "version": "1.2.92",
3
+ "version": "1.2.93",
4
4
  "description": "Shared TypeScript types and utilities for @dgpholdings projects",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",