@dgpholdings/greatoak-shared 1.2.86 → 1.2.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +148 -148
  2. package/dist/__mocks__/exercises.mock.js +1 -0
  3. package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
  4. package/dist/types/TApiClientConstellation.d.ts +33 -0
  5. package/dist/types/TApiClientConstellation.js +13 -0
  6. package/dist/types/TApiExercise.d.ts +5 -3
  7. package/dist/types/index.d.ts +1 -0
  8. package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
  9. package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
  10. package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
  11. package/dist/utils/constellation/evaluateConstellation.js +135 -0
  12. package/dist/utils/constellation/index.d.ts +17 -0
  13. package/dist/utils/constellation/index.js +26 -0
  14. package/dist/utils/constellation/levelThresholds.d.ts +99 -0
  15. package/dist/utils/constellation/levelThresholds.js +123 -0
  16. package/dist/utils/constellation/starFoundation.d.ts +25 -0
  17. package/dist/utils/constellation/starFoundation.js +54 -0
  18. package/dist/utils/constellation/stars/consistency.d.ts +29 -0
  19. package/dist/utils/constellation/stars/consistency.js +142 -0
  20. package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
  21. package/dist/utils/constellation/stars/lowerBody.js +30 -0
  22. package/dist/utils/constellation/stars/pull.d.ts +11 -0
  23. package/dist/utils/constellation/stars/pull.js +24 -0
  24. package/dist/utils/constellation/stars/push.d.ts +11 -0
  25. package/dist/utils/constellation/stars/push.js +24 -0
  26. package/dist/utils/constellation/stars/quality.d.ts +19 -0
  27. package/dist/utils/constellation/stars/quality.js +98 -0
  28. package/dist/utils/constellation/stars/recovery.d.ts +29 -0
  29. package/dist/utils/constellation/stars/recovery.js +169 -0
  30. package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
  31. package/dist/utils/constellation/strengthStarHelpers.js +104 -0
  32. package/dist/utils/constellation/types.d.ts +124 -0
  33. package/dist/utils/constellation/types.js +18 -0
  34. package/dist/utils/index.d.ts +5 -3
  35. package/dist/utils/index.js +1 -0
  36. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
  37. package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
  38. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
  39. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
  40. package/dist/utils/scoringWorkout/constants.d.ts +20 -6
  41. package/dist/utils/scoringWorkout/constants.js +23 -9
  42. package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
  43. package/dist/utils/scoringWorkout/helpers.js +24 -18
  44. package/dist/utils/scoringWorkout/index.d.ts +12 -8
  45. package/dist/utils/scoringWorkout/index.js +23 -15
  46. package/dist/utils/scoringWorkout/parseRecords.js +4 -3
  47. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +210 -172
  48. package/dist/utils/scoringWorkout/types.d.ts +34 -14
  49. package/package.json +31 -31
  50. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
  51. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
  52. package/dist/utils/scaleProPlan.util.d.ts +0 -9
  53. package/dist/utils/scaleProPlan.util.js +0 -139
  54. package/dist/utils/scoring/calculateCalories.d.ts +0 -67
  55. package/dist/utils/scoring/calculateCalories.js +0 -345
  56. package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
  57. package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
  58. package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
  59. package/dist/utils/scoring/calculateQualityScore.js +0 -334
  60. package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
  61. package/dist/utils/scoring/calculateTotalVolume.js +0 -73
  62. package/dist/utils/scoring/constants.d.ts +0 -211
  63. package/dist/utils/scoring/constants.js +0 -247
  64. package/dist/utils/scoring/helpers.d.ts +0 -119
  65. package/dist/utils/scoring/helpers.js +0 -229
  66. package/dist/utils/scoring/index.d.ts +0 -28
  67. package/dist/utils/scoring/index.js +0 -47
  68. package/dist/utils/scoring/parseRecords.d.ts +0 -98
  69. package/dist/utils/scoring/parseRecords.js +0 -284
  70. package/dist/utils/scoring/types.d.ts +0 -86
  71. package/dist/utils/scoring/types.js +0 -11
  72. package/dist/utils/scoring.utils.d.ts +0 -14
  73. package/dist/utils/scoring.utils.js +0 -243
  74. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
  75. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.js → calculateMuscleFatigue.js} +0 -0
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ // utils/constellation/stars/lowerBody.ts — Lower Body Star
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Lower Body Star
6
+ * ============================================================================
7
+ *
8
+ * Lower-body strength across squat, lunge, and hinge patterns combined, any
9
+ * equipment. Squats, goblet squats, leg press, lunges, deadlifts, RDLs, hip
10
+ * thrusts, kettlebell swings — all count. Best load across all three lights it.
11
+ * (Knee-vs-hip balance nuance deferred to the future goal layer.)
12
+ *
13
+ * Threshold per level from STRENGTH_THRESHOLDS.lowerBody, resolved by sex.
14
+ * Leg press is NOT special-cased — it is simply a squat-pattern exercise.
15
+ * Bodyweight lower-body work loads via the exercise's weightMultiplier, so home
16
+ * users can reach the threshold honestly.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.lowerBodyStar = void 0;
20
+ const strengthStarHelpers_1 = require("../strengthStarHelpers");
21
+ const LOWER_BODY_PATTERNS = ["squat", "lunge", "hinge"];
22
+ exports.lowerBodyStar = {
23
+ id: "lowerBody",
24
+ displayName: { translationKey: "constellation.lowerBody.name" },
25
+ color: "#40b060", // earth green — the foundation everything sits on
26
+ figurePosition: "base",
27
+ objective: { translationKey: "constellation.lowerBody.objective" },
28
+ rationale: { translationKey: "constellation.lowerBody.rationale" },
29
+ evaluate: (ctx) => (0, strengthStarHelpers_1.evaluateStrengthStar)(ctx, "lowerBody", LOWER_BODY_PATTERNS, "constellation.lowerBody"),
30
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Pull Star
4
+ * ============================================================================
5
+ *
6
+ * Pulling strength across all pull-pattern exercises (horizontal + vertical),
7
+ * any equipment. Rows, cable rows, lat pulldowns, pull-ups, chin-ups — all count.
8
+ * Threshold per level from STRENGTH_THRESHOLDS.pull, resolved by sex.
9
+ */
10
+ import type { TStarDefinition } from "../types";
11
+ export declare const pullStar: TStarDefinition;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ // utils/constellation/stars/pull.ts — Pull Star
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Pull Star
6
+ * ============================================================================
7
+ *
8
+ * Pulling strength across all pull-pattern exercises (horizontal + vertical),
9
+ * any equipment. Rows, cable rows, lat pulldowns, pull-ups, chin-ups — all count.
10
+ * Threshold per level from STRENGTH_THRESHOLDS.pull, resolved by sex.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.pullStar = void 0;
14
+ const strengthStarHelpers_1 = require("../strengthStarHelpers");
15
+ const PULL_PATTERNS = ["pull_horizontal", "pull_vertical"];
16
+ exports.pullStar = {
17
+ id: "pull",
18
+ displayName: { translationKey: "constellation.pull.name" },
19
+ color: "#2060ff", // cool blue — drawing resistance in
20
+ figurePosition: "rightArm",
21
+ objective: { translationKey: "constellation.pull.objective" },
22
+ rationale: { translationKey: "constellation.pull.rationale" },
23
+ evaluate: (ctx) => (0, strengthStarHelpers_1.evaluateStrengthStar)(ctx, "pull", PULL_PATTERNS, "constellation.pull"),
24
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Push Star
4
+ * ============================================================================
5
+ *
6
+ * Pushing strength across all push-pattern exercises (horizontal + vertical),
7
+ * any equipment. Bench, machine press, push-ups, overhead press — all count.
8
+ * Threshold per level from STRENGTH_THRESHOLDS.push, resolved by sex.
9
+ */
10
+ import type { TStarDefinition } from "../types";
11
+ export declare const pushStar: TStarDefinition;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ // utils/constellation/stars/push.ts — Push Star
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Push Star
6
+ * ============================================================================
7
+ *
8
+ * Pushing strength across all push-pattern exercises (horizontal + vertical),
9
+ * any equipment. Bench, machine press, push-ups, overhead press — all count.
10
+ * Threshold per level from STRENGTH_THRESHOLDS.push, resolved by sex.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.pushStar = void 0;
14
+ const strengthStarHelpers_1 = require("../strengthStarHelpers");
15
+ const PUSH_PATTERNS = ["push_horizontal", "push_vertical"];
16
+ exports.pushStar = {
17
+ id: "push",
18
+ displayName: { translationKey: "constellation.push.name" },
19
+ color: "#ff4820", // warm red — forward force
20
+ figurePosition: "leftArm",
21
+ objective: { translationKey: "constellation.push.objective" },
22
+ rationale: { translationKey: "constellation.push.rationale" },
23
+ evaluate: (ctx) => (0, strengthStarHelpers_1.evaluateStrengthStar)(ctx, "push", PUSH_PATTERNS, "constellation.push"),
24
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Quality Star (the core)
4
+ * ============================================================================
5
+ *
6
+ * Execution quality. Brightness reflects how many of the user's recent sessions
7
+ * were executed well. Reads PRE-SCORED sessions from context (the orchestrator
8
+ * scores once); this star never calls the scoring engine itself.
9
+ *
10
+ * Session quality = simple average of its exercises' quality scores (0-100).
11
+ * Star brightness = count-based: of the last N sessions, how many cleared the
12
+ * quality bar, scaled toward a target count. Count-based (not
13
+ * a consecutive streak) so a single off day never zeroes the
14
+ * star — inviting, not punishing.
15
+ *
16
+ * Thresholds come from QUALITY_THRESHOLDS[level].
17
+ */
18
+ import type { TStarDefinition } from "../types";
19
+ export declare const qualityStar: TStarDefinition;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ // utils/constellation/stars/quality.ts — Quality Star (the core)
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Quality Star (the core)
6
+ * ============================================================================
7
+ *
8
+ * Execution quality. Brightness reflects how many of the user's recent sessions
9
+ * were executed well. Reads PRE-SCORED sessions from context (the orchestrator
10
+ * scores once); this star never calls the scoring engine itself.
11
+ *
12
+ * Session quality = simple average of its exercises' quality scores (0-100).
13
+ * Star brightness = count-based: of the last N sessions, how many cleared the
14
+ * quality bar, scaled toward a target count. Count-based (not
15
+ * a consecutive streak) so a single off day never zeroes the
16
+ * star — inviting, not punishing.
17
+ *
18
+ * Thresholds come from QUALITY_THRESHOLDS[level].
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.qualityStar = void 0;
22
+ const levelThresholds_1 = require("../levelThresholds");
23
+ // --- Session quality --------------------------------------------------------
24
+ /**
25
+ * Average score across a scored session's scorable exercises. Returns null if
26
+ * the session has no scorable exercises (all missing/unscorable).
27
+ */
28
+ function sessionQuality(session) {
29
+ const scores = [];
30
+ for (const ex of session.exercises) {
31
+ if (typeof ex.score === "number")
32
+ scores.push(ex.score);
33
+ }
34
+ if (scores.length === 0)
35
+ return null;
36
+ return scores.reduce((s, v) => s + v, 0) / scores.length;
37
+ }
38
+ // --- Detail builders --------------------------------------------------------
39
+ function clearingMeasure(clearing, target) {
40
+ return {
41
+ value: clearing,
42
+ unit: "sessions",
43
+ display: {
44
+ translationKey: "constellation.quality.measure.clearing",
45
+ params: [Math.min(clearing, target), target],
46
+ },
47
+ };
48
+ }
49
+ function buildGap(clearing, considered, target, bar) {
50
+ if (considered === 0) {
51
+ return { translationKey: "constellation.quality.gap.noData" };
52
+ }
53
+ if (clearing >= target) {
54
+ return { translationKey: "constellation.quality.gap.full" };
55
+ }
56
+ return {
57
+ translationKey: "constellation.quality.gap.moreQuality",
58
+ params: [target - clearing, bar],
59
+ };
60
+ }
61
+ // --- Evaluator --------------------------------------------------------------
62
+ function evaluateQuality(ctx) {
63
+ const { bar, windowSessions, targetClearing } = levelThresholds_1.QUALITY_THRESHOLDS[ctx.level];
64
+ // Most recent N scored sessions (descending by date). Future sessions were
65
+ // already excluded by the orchestrator when scoring.
66
+ const recent = [...ctx.scoredSessions]
67
+ .sort((a, b) => b.date - a.date)
68
+ .slice(0, windowSessions);
69
+ let clearing = 0;
70
+ let considered = 0;
71
+ for (const session of recent) {
72
+ const q = sessionQuality(session);
73
+ if (q === null)
74
+ continue; // unscorable session counts neither way
75
+ considered++;
76
+ if (q >= bar)
77
+ clearing++;
78
+ }
79
+ const brightness = Math.min(100, (clearing / targetClearing) * 100);
80
+ return {
81
+ brightness,
82
+ detail: {
83
+ currentState: clearingMeasure(clearing, targetClearing),
84
+ target: clearingMeasure(targetClearing, targetClearing),
85
+ gap: buildGap(clearing, considered, targetClearing, bar),
86
+ },
87
+ };
88
+ }
89
+ // --- Definition -------------------------------------------------------------
90
+ exports.qualityStar = {
91
+ id: "quality",
92
+ displayName: { translationKey: "constellation.quality.name" },
93
+ color: "#00c4b4", // teal — precision, cool focus
94
+ figurePosition: "core",
95
+ objective: { translationKey: "constellation.quality.objective" },
96
+ rationale: { translationKey: "constellation.quality.rationale" },
97
+ evaluate: evaluateQuality,
98
+ };
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Recovery Star (the crown)
4
+ * ============================================================================
5
+ *
6
+ * Recovery intelligence: does the user let trained muscles fully recover, not
7
+ * just grind through fatigue? Brightness reflects how many RECOVERY CYCLES the
8
+ * user achieved in the trailing window.
9
+ *
10
+ * ZONE-BASED (not all-or-nothing). The body is split into three independent
11
+ * recovery zones — upper, lower, core. A split-routine lifter (e.g. Push/Pull/
12
+ * Legs) almost never has the WHOLE body fresh at once, yet is recovering
13
+ * intelligently zone by zone; an all-muscles-at-once rule would wrongly score
14
+ * them near zero. So we count cycles PER ZONE.
15
+ *
16
+ * A zone "cycle" = a transition where that zone goes fatigued -> recovered
17
+ * (a multi-day rest counts once). Crucially, a zone only earns cycles if the
18
+ * user actually TRAINS it in the window — an untrained zone is "always fresh"
19
+ * and must not hand out free recovery credit. Total cycles = sum across the
20
+ * zones the user trains, capped at the level target.
21
+ *
22
+ * Reads PRE-SCORED sessions from context (orchestrator scores once); builds the
23
+ * enriched history the fatigue model consumes, then replays the model across
24
+ * each day of the window. This star never calls the scoring engine itself.
25
+ *
26
+ * Thresholds come from RECOVERY_THRESHOLDS[level].
27
+ */
28
+ import type { TStarDefinition } from "../types";
29
+ export declare const recoveryStar: TStarDefinition;
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ // utils/constellation/stars/recovery.ts — Recovery Star (the crown)
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Recovery Star (the crown)
6
+ * ============================================================================
7
+ *
8
+ * Recovery intelligence: does the user let trained muscles fully recover, not
9
+ * just grind through fatigue? Brightness reflects how many RECOVERY CYCLES the
10
+ * user achieved in the trailing window.
11
+ *
12
+ * ZONE-BASED (not all-or-nothing). The body is split into three independent
13
+ * recovery zones — upper, lower, core. A split-routine lifter (e.g. Push/Pull/
14
+ * Legs) almost never has the WHOLE body fresh at once, yet is recovering
15
+ * intelligently zone by zone; an all-muscles-at-once rule would wrongly score
16
+ * them near zero. So we count cycles PER ZONE.
17
+ *
18
+ * A zone "cycle" = a transition where that zone goes fatigued -> recovered
19
+ * (a multi-day rest counts once). Crucially, a zone only earns cycles if the
20
+ * user actually TRAINS it in the window — an untrained zone is "always fresh"
21
+ * and must not hand out free recovery credit. Total cycles = sum across the
22
+ * zones the user trains, capped at the level target.
23
+ *
24
+ * Reads PRE-SCORED sessions from context (orchestrator scores once); builds the
25
+ * enriched history the fatigue model consumes, then replays the model across
26
+ * each day of the window. This star never calls the scoring engine itself.
27
+ *
28
+ * Thresholds come from RECOVERY_THRESHOLDS[level].
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.recoveryStar = void 0;
32
+ const computeMuscleFatigueMap_1 = require("../../scoringWorkout/computeMuscleFatigueMap");
33
+ const levelThresholds_1 = require("../levelThresholds");
34
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
35
+ /**
36
+ * Recovery zones — independent regions that recover on their own clocks. Each
37
+ * lists the major muscle keys that define it. A zone is "recovered" on a given
38
+ * day when ALL of its listed muscles are below the fatigue threshold (or carry
39
+ * no tracked fatigue).
40
+ */
41
+ const RECOVERY_ZONES = {
42
+ upper: [
43
+ "pectoralis-major",
44
+ "latissimus-dorsi",
45
+ "deltoids-anterior",
46
+ "tricep-brachii-long",
47
+ ],
48
+ lower: ["quadriceps", "hamstrings", "glutes-maximus"],
49
+ core: ["abs-upper", "abs-lower", "obliques"],
50
+ };
51
+ const ZONE_NAMES = Object.keys(RECOVERY_ZONES);
52
+ // --- Enriched history (feed for the fatigue map) ----------------------------
53
+ /**
54
+ * Turn the orchestrator's pre-scored sessions into the fatigue model's inputs.
55
+ * Also records which zones the user actually trained (any tracked fatigue in a
56
+ * zone's muscles at any point), so untrained zones earn no recovery credit.
57
+ */
58
+ function buildEnrichedHistory(ctx) {
59
+ var _a;
60
+ var _b;
61
+ const exercises = {};
62
+ const byExercise = {};
63
+ const trainedMuscles = new Set();
64
+ for (const session of ctx.scoredSessions) {
65
+ for (const ex of session.exercises) {
66
+ if (ex.score === null)
67
+ continue;
68
+ const exercise = ctx.exerciseCatalog[ex.exerciseId];
69
+ if (!exercise)
70
+ continue;
71
+ if (!exercises[ex.exerciseId]) {
72
+ exercises[ex.exerciseId] = {
73
+ primaryMuscles: exercise.primaryMuscles,
74
+ secondaryMuscles: exercise.secondaryMuscles,
75
+ };
76
+ }
77
+ for (const m of exercise.primaryMuscles)
78
+ trainedMuscles.add(m);
79
+ for (const m of exercise.secondaryMuscles)
80
+ trainedMuscles.add(m);
81
+ ((_a = byExercise[_b = ex.exerciseId]) !== null && _a !== void 0 ? _a : (byExercise[_b] = [])).push({
82
+ score: ex.score,
83
+ recordDate: session.date,
84
+ muscleScores: ex.muscleScores,
85
+ });
86
+ }
87
+ }
88
+ const scoreHistory = Object.entries(byExercise).map(([exerciseId, recordScore]) => ({ exerciseId, recordScore }));
89
+ // A zone is "trained" if the user touched any of its muscles in the window.
90
+ const trainedZones = new Set();
91
+ for (const zone of ZONE_NAMES) {
92
+ if (RECOVERY_ZONES[zone].some((m) => trainedMuscles.has(m))) {
93
+ trainedZones.add(zone);
94
+ }
95
+ }
96
+ return { exercises, scoreHistory, trainedZones };
97
+ }
98
+ /** Is a single zone recovered (all its muscles below threshold) on a day's map? */
99
+ function isZoneRecovered(zone, fatigueMap, recoveredBelow) {
100
+ for (const muscle of RECOVERY_ZONES[zone]) {
101
+ const entry = fatigueMap[muscle];
102
+ if (entry && entry.fatigue >= recoveredBelow)
103
+ return false;
104
+ }
105
+ return true;
106
+ }
107
+ // --- Detail builders --------------------------------------------------------
108
+ function cyclesMeasure(cycles, target) {
109
+ return {
110
+ value: cycles,
111
+ unit: "cycles",
112
+ display: {
113
+ translationKey: "constellation.recovery.measure.cycles",
114
+ params: [Math.min(cycles, target), target],
115
+ },
116
+ };
117
+ }
118
+ function buildGap(cycles, target, hasHistory) {
119
+ if (!hasHistory)
120
+ return { translationKey: "constellation.recovery.gap.noData" };
121
+ if (cycles >= target)
122
+ return { translationKey: "constellation.recovery.gap.full" };
123
+ return {
124
+ translationKey: "constellation.recovery.gap.moreCycles",
125
+ params: [target - cycles],
126
+ };
127
+ }
128
+ // --- Evaluator --------------------------------------------------------------
129
+ function evaluateRecovery(ctx) {
130
+ const { targetCycles, windowDays, recoveredBelow } = levelThresholds_1.RECOVERY_THRESHOLDS[ctx.level];
131
+ const { exercises, scoreHistory, trainedZones } = buildEnrichedHistory(ctx);
132
+ const hasHistory = scoreHistory.length > 0;
133
+ const startDay = Math.floor((ctx.now - windowDays * MS_PER_DAY) / MS_PER_DAY);
134
+ const endDay = Math.floor(ctx.now / MS_PER_DAY);
135
+ // Per-zone cycle counting. Only zones the user trains can earn cycles.
136
+ // Seed each trained zone as "recovered" (pre-window baseline = fresh), so the
137
+ // first cycle is only credited after the zone has actually been fatigued.
138
+ let totalCycles = 0;
139
+ for (const zone of trainedZones) {
140
+ let wasRecovered = true;
141
+ for (let d = startDay; d <= endDay; d++) {
142
+ const dayDate = new Date(d * MS_PER_DAY + MS_PER_DAY / 2); // midday
143
+ const map = (0, computeMuscleFatigueMap_1.computeMuscleFatigueMap)(exercises, scoreHistory, ctx.user, dayDate);
144
+ const recovered = isZoneRecovered(zone, map, recoveredBelow);
145
+ if (recovered && !wasRecovered)
146
+ totalCycles++;
147
+ wasRecovered = recovered;
148
+ }
149
+ }
150
+ const brightness = Math.min(100, (totalCycles / targetCycles) * 100);
151
+ return {
152
+ brightness,
153
+ detail: {
154
+ currentState: cyclesMeasure(totalCycles, targetCycles),
155
+ target: cyclesMeasure(targetCycles, targetCycles),
156
+ gap: buildGap(totalCycles, targetCycles, hasHistory),
157
+ },
158
+ };
159
+ }
160
+ // --- Definition -------------------------------------------------------------
161
+ exports.recoveryStar = {
162
+ id: "recovery",
163
+ displayName: { translationKey: "constellation.recovery.name" },
164
+ color: "#c8d8ff", // silver-white — moonlight, the quiet phase
165
+ figurePosition: "crown",
166
+ objective: { translationKey: "constellation.recovery.objective" },
167
+ rationale: { translationKey: "constellation.recovery.rationale" },
168
+ evaluate: evaluateRecovery,
169
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Strength Star Helpers
4
+ * ============================================================================
5
+ *
6
+ * Shared by the three strength stars (push, pull, lowerBody). Each star is a
7
+ * thin definition that names its movement patterns and its threshold key; this
8
+ * helper does the rest: filter the catalog to those patterns, compute the
9
+ * normalised load, and turn the ratio into brightness + i18n detail.
10
+ *
11
+ * Thresholds come from STRENGTH_THRESHOLDS[key][level], resolved by sex.
12
+ */
13
+ import type { TUserMetric } from "../../types";
14
+ import { type ISexThreshold } from "./levelThresholds";
15
+ import type { TStarContext, TStarEvaluation, TLocalizedText } from "./types";
16
+ /** Rolling window for strength PRs: 8 weeks. */
17
+ export declare const STRENGTH_WINDOW_DAYS = 56;
18
+ /** Resolve the threshold ratio for this user. unmentioned -> male (higher bar). */
19
+ export declare function resolveThreshold(threshold: ISexThreshold, gender: string | undefined): number;
20
+ /** User bodyweight with fallback. */
21
+ export declare function userBodyweight(user: Partial<TUserMetric>): number;
22
+ /**
23
+ * Brightness from current ratio vs threshold, graded & continuous.
24
+ * brightness = (currentRatio / thresholdRatio) * 100, capped at 100.
25
+ * Faint from the first rep; full at threshold. No wall.
26
+ */
27
+ export declare function ratioToBrightness(currentRatio: number, thresholdRatio: number): number;
28
+ /**
29
+ * Build the "what's missing" gap text. At/above threshold -> a "full" message;
30
+ * otherwise the remaining kg on the user's best lift to reach the threshold.
31
+ */
32
+ export declare function buildGap(gapKeyPrefix: string, currentRatio: number, thresholdRatio: number, bodyweightKg: number): TLocalizedText;
33
+ /**
34
+ * The full strength-star evaluation, shared by push/pull/lowerBody.
35
+ *
36
+ * @param ctx star context (sessions, catalog, user, now, level)
37
+ * @param strengthKey which STRENGTH_THRESHOLDS row to use ("push" | "pull" | "lowerBody")
38
+ * @param matchingPatterns movement patterns this star counts
39
+ * @param gapKeyPrefix translation-key prefix (e.g. "constellation.push")
40
+ */
41
+ export declare function evaluateStrengthStar(ctx: TStarContext, strengthKey: "push" | "pull" | "lowerBody", matchingPatterns: string[], gapKeyPrefix: string): TStarEvaluation;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ // utils/constellation/strengthStarHelpers.ts — Strength Star Helpers
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Strength Star Helpers
6
+ * ============================================================================
7
+ *
8
+ * Shared by the three strength stars (push, pull, lowerBody). Each star is a
9
+ * thin definition that names its movement patterns and its threshold key; this
10
+ * helper does the rest: filter the catalog to those patterns, compute the
11
+ * normalised load, and turn the ratio into brightness + i18n detail.
12
+ *
13
+ * Thresholds come from STRENGTH_THRESHOLDS[key][level], resolved by sex.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.STRENGTH_WINDOW_DAYS = void 0;
17
+ exports.resolveThreshold = resolveThreshold;
18
+ exports.userBodyweight = userBodyweight;
19
+ exports.ratioToBrightness = ratioToBrightness;
20
+ exports.buildGap = buildGap;
21
+ exports.evaluateStrengthStar = evaluateStrengthStar;
22
+ const computeNormalisedLoad_1 = require("./computeNormalisedLoad");
23
+ const levelThresholds_1 = require("./levelThresholds");
24
+ /** Rolling window for strength PRs: 8 weeks. */
25
+ exports.STRENGTH_WINDOW_DAYS = 56;
26
+ const MS_PER_DAY = 24 * 60 * 60 * 1000;
27
+ const DEFAULT_BODYWEIGHT_KG = 70;
28
+ /** Resolve the threshold ratio for this user. unmentioned -> male (higher bar). */
29
+ function resolveThreshold(threshold, gender) {
30
+ return gender === "female" ? threshold.female : threshold.male;
31
+ }
32
+ /** User bodyweight with fallback. */
33
+ function userBodyweight(user) {
34
+ const bw = user.weightKg;
35
+ return bw && bw > 0 ? bw : DEFAULT_BODYWEIGHT_KG;
36
+ }
37
+ /**
38
+ * Brightness from current ratio vs threshold, graded & continuous.
39
+ * brightness = (currentRatio / thresholdRatio) * 100, capped at 100.
40
+ * Faint from the first rep; full at threshold. No wall.
41
+ */
42
+ function ratioToBrightness(currentRatio, thresholdRatio) {
43
+ if (thresholdRatio <= 0)
44
+ return 0;
45
+ return Math.min(100, (currentRatio / thresholdRatio) * 100);
46
+ }
47
+ /** Build a bodyweight-ratio measure for the detail panel. */
48
+ function bwRatioMeasure(ratio) {
49
+ const rounded = Math.round(ratio * 100) / 100;
50
+ return {
51
+ value: rounded,
52
+ unit: "bw_ratio",
53
+ display: {
54
+ translationKey: "constellation.measure.bwRatio",
55
+ params: [rounded],
56
+ },
57
+ };
58
+ }
59
+ /**
60
+ * Build the "what's missing" gap text. At/above threshold -> a "full" message;
61
+ * otherwise the remaining kg on the user's best lift to reach the threshold.
62
+ */
63
+ function buildGap(gapKeyPrefix, currentRatio, thresholdRatio, bodyweightKg) {
64
+ if (currentRatio >= thresholdRatio) {
65
+ return { translationKey: `${gapKeyPrefix}.gap.full` };
66
+ }
67
+ const remainingKg = Math.max(0, Math.round((thresholdRatio - currentRatio) * bodyweightKg));
68
+ return {
69
+ translationKey: `${gapKeyPrefix}.gap.toGo`,
70
+ params: [remainingKg],
71
+ };
72
+ }
73
+ /**
74
+ * The full strength-star evaluation, shared by push/pull/lowerBody.
75
+ *
76
+ * @param ctx star context (sessions, catalog, user, now, level)
77
+ * @param strengthKey which STRENGTH_THRESHOLDS row to use ("push" | "pull" | "lowerBody")
78
+ * @param matchingPatterns movement patterns this star counts
79
+ * @param gapKeyPrefix translation-key prefix (e.g. "constellation.push")
80
+ */
81
+ function evaluateStrengthStar(ctx, strengthKey, matchingPatterns, gapKeyPrefix) {
82
+ const bw = userBodyweight(ctx.user);
83
+ const gender = ctx.user.gender;
84
+ const thresholdRatio = resolveThreshold(levelThresholds_1.STRENGTH_THRESHOLDS[strengthKey][ctx.level], gender);
85
+ // Build the matching-exercise id set from the catalog by movement pattern.
86
+ const patternSet = new Set(matchingPatterns);
87
+ const matchingIds = new Set();
88
+ for (const id of Object.keys(ctx.exerciseCatalog)) {
89
+ const ex = ctx.exerciseCatalog[id];
90
+ if (patternSet.has(ex.movementPattern))
91
+ matchingIds.add(id);
92
+ }
93
+ const windowStart = ctx.now - exports.STRENGTH_WINDOW_DAYS * MS_PER_DAY;
94
+ const load = (0, computeNormalisedLoad_1.computeNormalisedLoad)(matchingIds, ctx.sessions, ctx.exerciseCatalog, bw, gender, windowStart, ctx.now);
95
+ const brightness = ratioToBrightness(load.ratio, thresholdRatio);
96
+ return {
97
+ brightness,
98
+ detail: {
99
+ currentState: bwRatioMeasure(load.ratio),
100
+ target: bwRatioMeasure(thresholdRatio),
101
+ gap: buildGap(gapKeyPrefix, load.ratio, thresholdRatio, bw),
102
+ },
103
+ };
104
+ }