@dgpholdings/greatoak-shared 1.2.86 → 1.2.88
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__/catalog.fixture.d.ts +2 -0
- package/dist/__mocks__/catalog.fixture.js +208 -0
- package/dist/__mocks__/exercises.mock.d.ts +4 -11
- package/dist/__mocks__/exercises.mock.js +82 -41
- package/dist/__mocks__/sessions.mock.d.ts +28 -0
- package/dist/__mocks__/sessions.mock.js +394 -0
- package/dist/__mocks__/testIds.d.ts +9 -0
- package/dist/__mocks__/testIds.js +13 -0
- package/dist/__mocks__/user.mock.js +3 -1
- package/dist/constants/goalJourney.d.ts +108 -0
- package/dist/constants/goalJourney.js +443 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.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/TApiUser.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/adoptionEngine/scaleProPlan.util.js +9 -4
- package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
- package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
- package/dist/utils/constellation/computeNormalisedLoad.test.d.ts +1 -0
- package/dist/utils/constellation/computeNormalisedLoad.test.js +218 -0
- package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
- package/dist/utils/constellation/evaluateConstellation.js +135 -0
- package/dist/utils/constellation/evaluateConstellation.test.d.ts +1 -0
- package/dist/utils/constellation/evaluateConstellation.test.js +93 -0
- package/dist/utils/constellation/index.d.ts +18 -0
- package/dist/utils/constellation/index.js +29 -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/starFoundation.test.d.ts +1 -0
- package/dist/utils/constellation/starFoundation.test.js +75 -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/consistency.test.d.ts +1 -0
- package/dist/utils/constellation/stars/consistency.test.js +94 -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/quality.test.d.ts +1 -0
- package/dist/utils/constellation/stars/quality.test.js +113 -0
- package/dist/utils/constellation/stars/recovery.d.ts +29 -0
- package/dist/utils/constellation/stars/recovery.js +169 -0
- package/dist/utils/constellation/stars/recovery.test.d.ts +1 -0
- package/dist/utils/constellation/stars/recovery.test.js +131 -0
- package/dist/utils/constellation/strengthStar.test.d.ts +1 -0
- package/dist/utils/constellation/strengthStar.test.js +190 -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/exerciseRecord/recordValidator.integration.test.js +1 -1
- package/dist/utils/exerciseRecord/recordValidator.js +1 -1
- package/dist/utils/exerciseRecord/recordValidator.test.js +8 -8
- 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 +223 -185
- 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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// shared/src/utils/constellation/stars/recovery.test.ts
|
|
4
|
+
const starFoundation_1 = require("../starFoundation");
|
|
5
|
+
const recovery_1 = require("../stars/recovery");
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const NOW = Date.UTC(2026, 5, 1);
|
|
8
|
+
const day = (d) => NOW - d * 86400000;
|
|
9
|
+
// Exercises with muscles in specific zones (upper: pec/lat; lower: quad/ham/glute; core: abs).
|
|
10
|
+
const catalog = {
|
|
11
|
+
bench: {
|
|
12
|
+
exerciseId: "bench",
|
|
13
|
+
name: "Bench",
|
|
14
|
+
primaryMuscles: ["pectoralis-major"],
|
|
15
|
+
secondaryMuscles: ["deltoids-anterior"],
|
|
16
|
+
difficultyLevel: 3,
|
|
17
|
+
movementPattern: "push_horizontal",
|
|
18
|
+
status: "active",
|
|
19
|
+
},
|
|
20
|
+
squat: {
|
|
21
|
+
exerciseId: "squat",
|
|
22
|
+
name: "Squat",
|
|
23
|
+
primaryMuscles: ["quadriceps"],
|
|
24
|
+
secondaryMuscles: ["glutes-maximus"],
|
|
25
|
+
difficultyLevel: 3,
|
|
26
|
+
movementPattern: "squat",
|
|
27
|
+
status: "active",
|
|
28
|
+
},
|
|
29
|
+
row: {
|
|
30
|
+
exerciseId: "row",
|
|
31
|
+
name: "Row",
|
|
32
|
+
primaryMuscles: ["latissimus-dorsi"],
|
|
33
|
+
secondaryMuscles: [],
|
|
34
|
+
difficultyLevel: 3,
|
|
35
|
+
movementPattern: "pull_horizontal",
|
|
36
|
+
status: "active",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const scoredEx = (id, muscles) => ({
|
|
40
|
+
exerciseId: id,
|
|
41
|
+
score: 75,
|
|
42
|
+
muscleScores: Object.fromEntries(muscles.map((m) => [m, 70])),
|
|
43
|
+
});
|
|
44
|
+
const ctx = (scoredSessions, level = 1) => ({
|
|
45
|
+
sessions: [],
|
|
46
|
+
scoredSessions,
|
|
47
|
+
exerciseCatalog: catalog,
|
|
48
|
+
user: { weightKg: 80, gender: "male" },
|
|
49
|
+
now: NOW,
|
|
50
|
+
level,
|
|
51
|
+
});
|
|
52
|
+
(0, vitest_1.describe)("recovery — no data", () => {
|
|
53
|
+
(0, vitest_1.it)("empty history → noData gap, 0 brightness", () => {
|
|
54
|
+
const r = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx([]));
|
|
55
|
+
(0, vitest_1.expect)(r.brightness).toBe(0);
|
|
56
|
+
(0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.recovery.gap.noData");
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
(0, vitest_1.describe)("recovery — earns cycles from trained zones", () => {
|
|
60
|
+
(0, vitest_1.it)("a split lifter (alternating zones, spaced) earns recovery cycles", () => {
|
|
61
|
+
// train each zone, leave gaps so it recovers, train again → transitions
|
|
62
|
+
const s = [];
|
|
63
|
+
const sched = [
|
|
64
|
+
[40, "bench", ["pectoralis-major"]],
|
|
65
|
+
[37, "row", ["latissimus-dorsi"]],
|
|
66
|
+
[34, "squat", ["quadriceps"]],
|
|
67
|
+
[27, "bench", ["pectoralis-major"]],
|
|
68
|
+
[24, "row", ["latissimus-dorsi"]],
|
|
69
|
+
[21, "squat", ["quadriceps"]],
|
|
70
|
+
[14, "bench", ["pectoralis-major"]],
|
|
71
|
+
[11, "row", ["latissimus-dorsi"]],
|
|
72
|
+
[8, "squat", ["quadriceps"]],
|
|
73
|
+
];
|
|
74
|
+
for (const [d, id, m] of sched)
|
|
75
|
+
s.push({ date: day(d), exercises: [scoredEx(id, m)] });
|
|
76
|
+
const r = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s));
|
|
77
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
78
|
+
(0, vitest_1.expect)(r.currentState.value).toBeGreaterThanOrEqual(1);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.describe)("recovery — penalises the daily grinder", () => {
|
|
82
|
+
(0, vitest_1.it)("training all zones every day → low/zero recovery", () => {
|
|
83
|
+
const s = [];
|
|
84
|
+
for (let d = 40; d >= 0; d--) {
|
|
85
|
+
s.push({
|
|
86
|
+
date: day(d),
|
|
87
|
+
exercises: [
|
|
88
|
+
scoredEx("bench", ["pectoralis-major"]),
|
|
89
|
+
scoredEx("squat", ["quadriceps"]),
|
|
90
|
+
scoredEx("row", ["latissimus-dorsi"]),
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
const r = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s));
|
|
95
|
+
(0, vitest_1.expect)(r.brightness).toBeLessThan(50);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
(0, vitest_1.describe)("recovery — untrained zones earn no free credit", () => {
|
|
99
|
+
(0, vitest_1.it)("training only upper does not manufacture lower/core cycles", () => {
|
|
100
|
+
// only upper trained, spaced so it cycles a few times
|
|
101
|
+
const s = [];
|
|
102
|
+
for (const d of [40, 33, 26, 19, 12, 5])
|
|
103
|
+
s.push({
|
|
104
|
+
date: day(d),
|
|
105
|
+
exercises: [scoredEx("bench", ["pectoralis-major"])],
|
|
106
|
+
});
|
|
107
|
+
const r = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s));
|
|
108
|
+
// cycles come ONLY from the upper zone; if lower/core counted freely,
|
|
109
|
+
// brightness would be far higher. Bounded sanity check:
|
|
110
|
+
(0, vitest_1.expect)(r.currentState.value).toBeGreaterThanOrEqual(1);
|
|
111
|
+
(0, vitest_1.expect)(r.brightness).toBeLessThanOrEqual(100);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.describe)("recovery — level scaling", () => {
|
|
115
|
+
(0, vitest_1.it)("same history is dimmer at L2 (needs more cycles)", () => {
|
|
116
|
+
const s = [];
|
|
117
|
+
const sched = [
|
|
118
|
+
[40, "bench", ["pectoralis-major"]],
|
|
119
|
+
[34, "squat", ["quadriceps"]],
|
|
120
|
+
[27, "bench", ["pectoralis-major"]],
|
|
121
|
+
[21, "squat", ["quadriceps"]],
|
|
122
|
+
[14, "bench", ["pectoralis-major"]],
|
|
123
|
+
[8, "squat", ["quadriceps"]],
|
|
124
|
+
];
|
|
125
|
+
for (const [d, id, m] of sched)
|
|
126
|
+
s.push({ date: day(d), exercises: [scoredEx(id, m)] });
|
|
127
|
+
const l1 = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s, 1));
|
|
128
|
+
const l2 = (0, starFoundation_1.resolveStar)(recovery_1.recoveryStar, ctx(s, 2));
|
|
129
|
+
(0, vitest_1.expect)(l2.brightness).toBeLessThanOrEqual(l1.brightness);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// shared/src/utils/constellation/__tests__/strengthStars.test.ts
|
|
4
|
+
const starFoundation_1 = require("./starFoundation");
|
|
5
|
+
const push_1 = require("./stars/push");
|
|
6
|
+
const pull_1 = require("./stars/pull");
|
|
7
|
+
const lowerBody_1 = require("./stars/lowerBody");
|
|
8
|
+
const vitest_1 = require("vitest");
|
|
9
|
+
const NOW = Date.UTC(2026, 5, 1);
|
|
10
|
+
const day = (d) => NOW - d * 86400000;
|
|
11
|
+
const wr = (kg, reps) => ({ type: "weight-reps", kg, reps, isDone: true, isStrictMode: false });
|
|
12
|
+
const ro = (reps, aux = "0") => ({
|
|
13
|
+
type: "reps-only",
|
|
14
|
+
reps,
|
|
15
|
+
auxWeightKg: aux,
|
|
16
|
+
isDone: true,
|
|
17
|
+
isStrictMode: false,
|
|
18
|
+
});
|
|
19
|
+
const ex = (id, mp, over = {}) => ({
|
|
20
|
+
exerciseId: id,
|
|
21
|
+
name: id,
|
|
22
|
+
primaryMuscles: [],
|
|
23
|
+
secondaryMuscles: [],
|
|
24
|
+
difficultyLevel: 3,
|
|
25
|
+
movementPattern: mp,
|
|
26
|
+
status: "active",
|
|
27
|
+
...over,
|
|
28
|
+
});
|
|
29
|
+
const catalog = {
|
|
30
|
+
bench: ex("bench", "push_horizontal"),
|
|
31
|
+
ohp: ex("ohp", "push_vertical"),
|
|
32
|
+
row: ex("row", "pull_horizontal"),
|
|
33
|
+
pullup: ex("pullup", "pull_vertical", {
|
|
34
|
+
weightMultiplier: { male: 1.0, female: 0.95, default: 0.97 },
|
|
35
|
+
}),
|
|
36
|
+
squat: ex("squat", "squat"),
|
|
37
|
+
rdl: ex("rdl", "hinge"),
|
|
38
|
+
lunge: ex("lunge", "lunge"),
|
|
39
|
+
pistol: ex("pistol", "squat", {
|
|
40
|
+
weightMultiplier: { male: 0.95, female: 0.9, default: 0.9 },
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
const ctx = (sessions, user = {}, level = 1) => ({
|
|
44
|
+
sessions,
|
|
45
|
+
scoredSessions: [],
|
|
46
|
+
exerciseCatalog: catalog,
|
|
47
|
+
user: { weightKg: 80, gender: "male", ...user },
|
|
48
|
+
now: NOW,
|
|
49
|
+
level,
|
|
50
|
+
});
|
|
51
|
+
(0, vitest_1.describe)("push star — pattern filtering & thresholds", () => {
|
|
52
|
+
(0, vitest_1.it)("lights from a strong bench (L1 0.60 male)", () => {
|
|
53
|
+
const r = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx([
|
|
54
|
+
{
|
|
55
|
+
date: day(2),
|
|
56
|
+
exercises: [{ exerciseId: "bench", records: [wr("60", "5")] }],
|
|
57
|
+
},
|
|
58
|
+
]));
|
|
59
|
+
// 60x5 → 70/80 = 0.875 vs 0.60 → capped 100
|
|
60
|
+
(0, vitest_1.expect)(r.brightness).toBe(100);
|
|
61
|
+
(0, vitest_1.expect)(r.tier).toBe(3);
|
|
62
|
+
(0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.push.gap.full");
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.it)("counts vertical push (OHP) too", () => {
|
|
65
|
+
const r = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx([
|
|
66
|
+
{
|
|
67
|
+
date: day(2),
|
|
68
|
+
exercises: [{ exerciseId: "ohp", records: [wr("40", "5")] }],
|
|
69
|
+
},
|
|
70
|
+
]));
|
|
71
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)("ignores pull movements", () => {
|
|
74
|
+
const r = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx([
|
|
75
|
+
{
|
|
76
|
+
date: day(2),
|
|
77
|
+
exercises: [{ exerciseId: "row", records: [wr("100", "5")] }],
|
|
78
|
+
},
|
|
79
|
+
]));
|
|
80
|
+
(0, vitest_1.expect)(r.brightness).toBe(0);
|
|
81
|
+
});
|
|
82
|
+
(0, vitest_1.it)("partial brightness below threshold gives a toGo gap", () => {
|
|
83
|
+
const r = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx([
|
|
84
|
+
{
|
|
85
|
+
date: day(2),
|
|
86
|
+
exercises: [{ exerciseId: "bench", records: [wr("25", "5")] }],
|
|
87
|
+
},
|
|
88
|
+
]));
|
|
89
|
+
// 25x5 → 29.2/80 = 0.365 vs 0.60 → ~61%
|
|
90
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
91
|
+
(0, vitest_1.expect)(r.brightness).toBeLessThan(100);
|
|
92
|
+
(0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.push.gap.toGo");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
(0, vitest_1.describe)("strength — gendered thresholds", () => {
|
|
96
|
+
(0, vitest_1.it)("female has a lower bar (same lift lights more)", () => {
|
|
97
|
+
const sessions = [
|
|
98
|
+
{
|
|
99
|
+
date: day(2),
|
|
100
|
+
exercises: [{ exerciseId: "bench", records: [wr("30", "5")] }],
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
const male = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, { gender: "male" }));
|
|
104
|
+
const female = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, { gender: "female" }));
|
|
105
|
+
(0, vitest_1.expect)(female.brightness).toBeGreaterThanOrEqual(male.brightness);
|
|
106
|
+
});
|
|
107
|
+
(0, vitest_1.it)("unmentioned gender resolves to the male (higher) bar", () => {
|
|
108
|
+
const sessions = [
|
|
109
|
+
{
|
|
110
|
+
date: day(2),
|
|
111
|
+
exercises: [{ exerciseId: "bench", records: [wr("30", "5")] }],
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
const male = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, { gender: "male" }));
|
|
115
|
+
const unmentioned = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, { gender: undefined }));
|
|
116
|
+
(0, vitest_1.expect)(unmentioned.brightness).toBe(male.brightness);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.describe)("strength — level scaling", () => {
|
|
120
|
+
(0, vitest_1.it)("a fixed lift is dimmer at L2 than L1 (higher bar)", () => {
|
|
121
|
+
const sessions = [
|
|
122
|
+
{
|
|
123
|
+
date: day(2),
|
|
124
|
+
exercises: [{ exerciseId: "bench", records: [wr("40", "5")] }],
|
|
125
|
+
},
|
|
126
|
+
];
|
|
127
|
+
// 40x5 → 46.7/80 = 0.583. L1 push 0.60 → ~97%; L2 push 1.0 → ~58%
|
|
128
|
+
const l1 = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, {}, 1));
|
|
129
|
+
const l2 = (0, starFoundation_1.resolveStar)(push_1.pushStar, ctx(sessions, {}, 2));
|
|
130
|
+
(0, vitest_1.expect)(l2.brightness).toBeLessThan(l1.brightness);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
(0, vitest_1.describe)("pull star", () => {
|
|
134
|
+
(0, vitest_1.it)("lights from rows", () => {
|
|
135
|
+
const r = (0, starFoundation_1.resolveStar)(pull_1.pullStar, ctx([
|
|
136
|
+
{
|
|
137
|
+
date: day(2),
|
|
138
|
+
exercises: [{ exerciseId: "row", records: [wr("50", "5")] }],
|
|
139
|
+
},
|
|
140
|
+
]));
|
|
141
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
142
|
+
});
|
|
143
|
+
(0, vitest_1.it)("bodyweight pull-ups light via weightMultiplier", () => {
|
|
144
|
+
const r = (0, starFoundation_1.resolveStar)(pull_1.pullStar, ctx([
|
|
145
|
+
{
|
|
146
|
+
date: day(2),
|
|
147
|
+
exercises: [{ exerciseId: "pullup", records: [ro("8")] }],
|
|
148
|
+
},
|
|
149
|
+
]));
|
|
150
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
(0, vitest_1.describe)("lowerBody star — combined patterns & home reachability", () => {
|
|
154
|
+
(0, vitest_1.it)("squat, hinge, and lunge all count", () => {
|
|
155
|
+
for (const id of ["squat", "rdl", "lunge"]) {
|
|
156
|
+
const r = (0, starFoundation_1.resolveStar)(lowerBody_1.lowerBodyStar, ctx([
|
|
157
|
+
{
|
|
158
|
+
date: day(2),
|
|
159
|
+
exercises: [{ exerciseId: id, records: [wr("80", "5")] }],
|
|
160
|
+
},
|
|
161
|
+
]));
|
|
162
|
+
(0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
(0, vitest_1.it)("HOME user reaches full lower body via bodyweight pistol squats", () => {
|
|
166
|
+
const r = (0, starFoundation_1.resolveStar)(lowerBody_1.lowerBodyStar, ctx([
|
|
167
|
+
{
|
|
168
|
+
date: day(2),
|
|
169
|
+
exercises: [{ exerciseId: "pistol", records: [ro("8")] }],
|
|
170
|
+
},
|
|
171
|
+
], { weightKg: 75 }));
|
|
172
|
+
// pistol 0.95*75=71.25; 1RM=71.25*(1+8/30)=90.25; /75=1.20 vs L1 1.0 → 100
|
|
173
|
+
(0, vitest_1.expect)(r.brightness).toBe(100);
|
|
174
|
+
});
|
|
175
|
+
(0, vitest_1.it)("takes the best across the three patterns", () => {
|
|
176
|
+
const sessions = [
|
|
177
|
+
{
|
|
178
|
+
date: day(5),
|
|
179
|
+
exercises: [{ exerciseId: "squat", records: [wr("50", "5")] }],
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
date: day(2),
|
|
183
|
+
exercises: [{ exerciseId: "rdl", records: [wr("100", "5")] }],
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const r = (0, starFoundation_1.resolveStar)(lowerBody_1.lowerBodyStar, ctx(sessions));
|
|
187
|
+
// rdl 100x5 → 116.7/80 = 1.46 → capped 100
|
|
188
|
+
(0, vitest_1.expect)(r.brightness).toBe(100);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -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
|
+
}
|