@dgpholdings/greatoak-shared 1.2.93 → 1.2.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__mocks__/user.mock.js +1 -0
- package/dist/constants/goalJourney.d.ts +14 -10
- package/dist/constants/goalJourney.js +173 -21
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/types/TApiAiQuickStartWorkout.d.ts +1 -1
- package/dist/types/TApiGoalContextRes.d.ts +64 -0
- package/dist/types/TApiGoalContextRes.js +2 -0
- package/dist/types/TApiUser.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +0 -16
- package/dist/utils/goalFeature/utilGoal.d.ts +22 -0
- package/dist/utils/goalFeature/utilGoal.js +271 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { TActivityLevel } from "../types";
|
|
1
2
|
import { TAiFitnessGoal, TWorkoutIntent } from "./AiExerciseVocabulary";
|
|
2
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"];
|
|
3
4
|
export type TUserFitnessGoal = (typeof USER_FITNESS_GOALS)[number];
|
|
4
5
|
export type TSessionIntent = TWorkoutIntent | "rest";
|
|
5
6
|
export declare const GOAL_SESSION_INTENTS: Record<TUserFitnessGoal, TWeeklyIntentTemplate>;
|
|
6
7
|
export declare const GOAL_TO_AI_FITNESS_GOAL: Record<TUserFitnessGoal, TAiFitnessGoal>;
|
|
7
|
-
export declare const TRAINING_PHASES: readonly [
|
|
8
|
+
export declare const TRAINING_PHASES: readonly ["foundation", "momentum", "push", "peak"];
|
|
8
9
|
export type TTrainingPhase = (typeof TRAINING_PHASES)[number];
|
|
9
10
|
export type TPhaseDisplay = {
|
|
10
11
|
/** i18n key for the phase name — e.g. "goalPhase.foundation.name" */
|
|
@@ -50,7 +51,7 @@ export type TIntentDisplay = {
|
|
|
50
51
|
*/
|
|
51
52
|
emoji: string;
|
|
52
53
|
};
|
|
53
|
-
export declare const INTENT_DISPLAY:
|
|
54
|
+
export declare const INTENT_DISPLAY: Record<Exclude<TSessionIntent, "arms" | "back" | "unstructured">, TIntentDisplay>;
|
|
54
55
|
export type TGoalDisplay = {
|
|
55
56
|
/** i18n key for the goal name — e.g. "userGoal.loseWeight.name" */
|
|
56
57
|
nameKey: string;
|
|
@@ -82,17 +83,18 @@ export type TGoalCategoryDisplay = {
|
|
|
82
83
|
labelEn: string;
|
|
83
84
|
};
|
|
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>;
|
|
85
88
|
/**
|
|
86
|
-
* Derive
|
|
87
|
-
*
|
|
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.
|
|
88
92
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* derivePhase(7) // → 3 (Push)
|
|
93
|
-
* derivePhase(10) // → 4 (Peak)
|
|
93
|
+
* On goal start day (no sessions yet), pass:
|
|
94
|
+
* totalSessions = PHASE_ENTRY_CREDIT[user.fitnessLevel]
|
|
95
|
+
* sessionsInLast4Weeks = RECENCY_CREDIT[user.fitnessLevel]
|
|
94
96
|
*/
|
|
95
|
-
export declare function derivePhase(weekNumber: number): TTrainingPhase;
|
|
97
|
+
export declare function derivePhase(weekNumber: number, totalSessions: number, sessionsInLast4Weeks: number): TTrainingPhase;
|
|
96
98
|
export declare const DAYS_PER_WEEK_OPTIONS: readonly [2, 3, 4, 5];
|
|
97
99
|
export type TDaysPerWeek = (typeof DAYS_PER_WEEK_OPTIONS)[number];
|
|
98
100
|
export type TDurationHint = "short" | "medium" | "long";
|
|
@@ -105,3 +107,5 @@ export type TWeeklyIntentTemplate = [
|
|
|
105
107
|
TSessionIntent,
|
|
106
108
|
TSessionIntent
|
|
107
109
|
];
|
|
110
|
+
export type TGoalPhaseHints = Record<TTrainingPhase, string>;
|
|
111
|
+
export declare const GOAL_PHASE_HINTS: Record<TUserFitnessGoal, TGoalPhaseHints>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DAYS_PER_WEEK_OPTIONS = 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;
|
|
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
4
|
exports.derivePhase = derivePhase;
|
|
5
5
|
exports.USER_FITNESS_GOALS = [
|
|
6
6
|
// Appearance
|
|
@@ -126,9 +126,15 @@ exports.GOAL_TO_AI_FITNESS_GOAL = {
|
|
|
126
126
|
core_strength: "core_strength",
|
|
127
127
|
mobility_flexibility: "mobility",
|
|
128
128
|
};
|
|
129
|
-
|
|
129
|
+
// Foundation → Momentum → Push → Peak
|
|
130
|
+
exports.TRAINING_PHASES = [
|
|
131
|
+
"foundation",
|
|
132
|
+
"momentum",
|
|
133
|
+
"push",
|
|
134
|
+
"peak",
|
|
135
|
+
];
|
|
130
136
|
exports.PHASE_DISPLAY = {
|
|
131
|
-
|
|
137
|
+
foundation: {
|
|
132
138
|
nameKey: "goalPhase.foundation.name",
|
|
133
139
|
nameEn: "Foundation",
|
|
134
140
|
subtitleKey: "goalPhase.foundation.subtitle",
|
|
@@ -140,7 +146,7 @@ exports.PHASE_DISPLAY = {
|
|
|
140
146
|
weekRange: [1, 2],
|
|
141
147
|
intensityPercent: 40,
|
|
142
148
|
},
|
|
143
|
-
|
|
149
|
+
momentum: {
|
|
144
150
|
nameKey: "goalPhase.momentum.name",
|
|
145
151
|
nameEn: "Momentum",
|
|
146
152
|
subtitleKey: "goalPhase.momentum.subtitle",
|
|
@@ -152,7 +158,7 @@ exports.PHASE_DISPLAY = {
|
|
|
152
158
|
weekRange: [3, 5],
|
|
153
159
|
intensityPercent: 60,
|
|
154
160
|
},
|
|
155
|
-
|
|
161
|
+
push: {
|
|
156
162
|
nameKey: "goalPhase.push.name",
|
|
157
163
|
nameEn: "Push",
|
|
158
164
|
subtitleKey: "goalPhase.push.subtitle",
|
|
@@ -164,7 +170,7 @@ exports.PHASE_DISPLAY = {
|
|
|
164
170
|
weekRange: [6, 8],
|
|
165
171
|
intensityPercent: 80,
|
|
166
172
|
},
|
|
167
|
-
|
|
173
|
+
peak: {
|
|
168
174
|
nameKey: "goalPhase.peak.name",
|
|
169
175
|
nameEn: "Peak",
|
|
170
176
|
subtitleKey: "goalPhase.peak.subtitle",
|
|
@@ -421,23 +427,169 @@ exports.GOAL_CATEGORY_DISPLAY = {
|
|
|
421
427
|
// derivePhase — pure helper
|
|
422
428
|
// Shared between Lambda and client — lives here so neither duplicates it
|
|
423
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
|
+
};
|
|
424
442
|
/**
|
|
425
|
-
* Derive
|
|
426
|
-
*
|
|
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.
|
|
427
446
|
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* derivePhase(7) // → 3 (Push)
|
|
432
|
-
* derivePhase(10) // → 4 (Peak)
|
|
447
|
+
* On goal start day (no sessions yet), pass:
|
|
448
|
+
* totalSessions = PHASE_ENTRY_CREDIT[user.fitnessLevel]
|
|
449
|
+
* sessionsInLast4Weeks = RECENCY_CREDIT[user.fitnessLevel]
|
|
433
450
|
*/
|
|
434
|
-
function derivePhase(weekNumber) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (weekNumber
|
|
440
|
-
return
|
|
441
|
-
|
|
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";
|
|
442
462
|
}
|
|
443
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
|
+
};
|
package/dist/constants/index.js
CHANGED
|
@@ -16,3 +16,4 @@ 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,5 +1,5 @@
|
|
|
1
1
|
import { TAiEnergyLevel, TAiUserInjury, TQuickStartEquipmentGroupExplicit } from "../constants/AiExerciseVocabulary";
|
|
2
|
-
import { TTrainingPhase, TUserFitnessGoal } from "
|
|
2
|
+
import { TTrainingPhase, TUserFitnessGoal } from "../constants/goalJourney";
|
|
3
3
|
import { TTemplate } from "./TApiTemplateData";
|
|
4
4
|
export type TApiAiQuickStartWorkoutReq = {
|
|
5
5
|
/**
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { TWorkoutIntent } from "../constants";
|
|
2
|
+
import { TDaysPerWeek, TSessionIntent, TTrainingPhase, TUserFitnessGoal } from "../constants/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
|
+
};
|
package/dist/types/TApiUser.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TAiFitnessGoal, TAiUserInjury } from "../constants/AiExerciseVocabulary";
|
|
2
2
|
import { TConstellationUserLevel } from "../utils";
|
|
3
3
|
import { TGdprData } from "./commonTypes";
|
|
4
|
+
import { TGoalProfile } from "./TApiGoalContextRes";
|
|
4
5
|
import { TUserPreferences } from "./TUserPreferences";
|
|
5
6
|
export type TAuthType = "email" | "apple" | "anonymous" | "token";
|
|
6
7
|
export type TGender = "male" | "female" | "unmentioned";
|
|
@@ -27,6 +28,7 @@ export type TUserMetric = TUserPreferences & {
|
|
|
27
28
|
gender: TGender;
|
|
28
29
|
weightKg: number;
|
|
29
30
|
heightCm: number;
|
|
31
|
+
goalProfile: TGoalProfile | undefined;
|
|
30
32
|
emailAddress?: string;
|
|
31
33
|
injuryInfo?: TInjuryInfo;
|
|
32
34
|
subscriptionStatus: TSubscriptionStatus;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -16,4 +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 "./
|
|
19
|
+
export type * from "./TApiGoalContextRes";
|
package/dist/types/index.js
CHANGED
|
@@ -1,18 +1,2 @@
|
|
|
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
|
-
};
|
|
16
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
// Features
|
|
18
|
-
__exportStar(require("./goalFeature"), exports);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { TMuscleFatigueResult } from "..";
|
|
2
|
+
import { TSessionIntent } from "../../constants/goalJourney";
|
|
3
|
+
import { TGoalContext, TGoalProfile, TIntentRecommendation } from "../../types/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("../../constants/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
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -52,3 +52,4 @@ __exportStar(require("./exerciseRecord/workoutMath"), exports);
|
|
|
52
52
|
__exportStar(require("./exerciseRecord/recordValidator"), exports);
|
|
53
53
|
__exportStar(require("./metricConversions"), exports);
|
|
54
54
|
__exportStar(require("./constellation"), exports);
|
|
55
|
+
__exportStar(require("./goalFeature/utilGoal"), exports);
|