@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.
- package/README.md +148 -148
- package/dist/__mocks__/exercises.mock.js +1 -0
- package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
- package/dist/types/TApiClientConstellation.d.ts +33 -0
- package/dist/types/TApiClientConstellation.js +13 -0
- package/dist/types/TApiExercise.d.ts +5 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
- package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
- package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
- package/dist/utils/constellation/evaluateConstellation.js +135 -0
- package/dist/utils/constellation/index.d.ts +17 -0
- package/dist/utils/constellation/index.js +26 -0
- package/dist/utils/constellation/levelThresholds.d.ts +99 -0
- package/dist/utils/constellation/levelThresholds.js +123 -0
- package/dist/utils/constellation/starFoundation.d.ts +25 -0
- package/dist/utils/constellation/starFoundation.js +54 -0
- package/dist/utils/constellation/stars/consistency.d.ts +29 -0
- package/dist/utils/constellation/stars/consistency.js +142 -0
- package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
- package/dist/utils/constellation/stars/lowerBody.js +30 -0
- package/dist/utils/constellation/stars/pull.d.ts +11 -0
- package/dist/utils/constellation/stars/pull.js +24 -0
- package/dist/utils/constellation/stars/push.d.ts +11 -0
- package/dist/utils/constellation/stars/push.js +24 -0
- package/dist/utils/constellation/stars/quality.d.ts +19 -0
- package/dist/utils/constellation/stars/quality.js +98 -0
- package/dist/utils/constellation/stars/recovery.d.ts +29 -0
- package/dist/utils/constellation/stars/recovery.js +169 -0
- package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
- package/dist/utils/constellation/strengthStarHelpers.js +104 -0
- package/dist/utils/constellation/types.d.ts +124 -0
- package/dist/utils/constellation/types.js +18 -0
- package/dist/utils/index.d.ts +5 -3
- package/dist/utils/index.js +1 -0
- package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
- package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
- package/dist/utils/scoringWorkout/constants.d.ts +20 -6
- package/dist/utils/scoringWorkout/constants.js +23 -9
- package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
- package/dist/utils/scoringWorkout/helpers.js +24 -18
- package/dist/utils/scoringWorkout/index.d.ts +12 -8
- package/dist/utils/scoringWorkout/index.js +23 -15
- package/dist/utils/scoringWorkout/parseRecords.js +4 -3
- package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +210 -172
- package/dist/utils/scoringWorkout/types.d.ts +34 -14
- package/package.json +31 -31
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
- package/dist/utils/scaleProPlan.util.d.ts +0 -9
- package/dist/utils/scaleProPlan.util.js +0 -139
- package/dist/utils/scoring/calculateCalories.d.ts +0 -67
- package/dist/utils/scoring/calculateCalories.js +0 -345
- package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
- package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
- package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
- package/dist/utils/scoring/calculateQualityScore.js +0 -334
- package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
- package/dist/utils/scoring/calculateTotalVolume.js +0 -73
- package/dist/utils/scoring/constants.d.ts +0 -211
- package/dist/utils/scoring/constants.js +0 -247
- package/dist/utils/scoring/helpers.d.ts +0 -119
- package/dist/utils/scoring/helpers.js +0 -229
- package/dist/utils/scoring/index.d.ts +0 -28
- package/dist/utils/scoring/index.js +0 -47
- package/dist/utils/scoring/parseRecords.d.ts +0 -98
- package/dist/utils/scoring/parseRecords.js +0 -284
- package/dist/utils/scoring/types.d.ts +0 -86
- package/dist/utils/scoring/types.js +0 -11
- package/dist/utils/scoring.utils.d.ts +0 -14
- package/dist/utils/scoring.utils.js +0 -243
- /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
- /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
|
+
}
|