@dgpholdings/greatoak-shared 1.2.85 → 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/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/quickStartIntents.d.ts +19 -0
- package/dist/constants/quickStartIntents.js +39 -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,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/constellation/computeNormalisedLoad.ts — Normalised Load (strength-star helper)
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX CONSTELLATION — Normalised Load (strength-star helper)
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* Turns a user's sessions into a single strength number for a movement pattern:
|
|
9
|
+
* the best estimated-1RM achieved on any matching exercise in the rolling
|
|
10
|
+
* window, expressed as a ratio of bodyweight.
|
|
11
|
+
*
|
|
12
|
+
* Estimated-1RM (Epley, rep-capped at 12) is used instead of raw top weight so
|
|
13
|
+
* that 60kg x5 ~ 70kg x1 — it rewards productive training over ego-lifting.
|
|
14
|
+
*
|
|
15
|
+
* Bodyweight loading (the home-user path): a bodyweight exercise's effective
|
|
16
|
+
* load is the REAL fraction of bodyweight it moves, taken from the exercise's
|
|
17
|
+
* own `weightMultiplier` (per-sex), falling back to a coarse map keyed off
|
|
18
|
+
* `bodyweightDependency`. This is what lets a home user honestly reach a
|
|
19
|
+
* strength threshold with pull-ups / pistol squats etc., instead of being
|
|
20
|
+
* capped by a flat guess.
|
|
21
|
+
*
|
|
22
|
+
* Returns a bodyweight ratio (e.g. 0.78 = "0.78x bodyweight"). 0 = no data.
|
|
23
|
+
*/
|
|
24
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
+
exports.EPLEY_REP_CAP = void 0;
|
|
26
|
+
exports.estimateOneRepMax = estimateOneRepMax;
|
|
27
|
+
exports.computeNormalisedLoad = computeNormalisedLoad;
|
|
28
|
+
// --- Tunables -------------------------------------------------------------
|
|
29
|
+
/** Epley reps cap: reps beyond this don't increase the 1RM estimate. */
|
|
30
|
+
exports.EPLEY_REP_CAP = 12;
|
|
31
|
+
/** Default bodyweight when the user profile lacks one. */
|
|
32
|
+
const DEFAULT_BODYWEIGHT_KG = 70;
|
|
33
|
+
/**
|
|
34
|
+
* Fallback bodyweight-load fractions when an exercise has no `weightMultiplier`.
|
|
35
|
+
* Keyed off `bodyweightDependency`: how much of your own body the move loads.
|
|
36
|
+
* high ~ pull-ups, dips, pistols (most of bodyweight)
|
|
37
|
+
* medium ~ lunges, inverted rows (about half)
|
|
38
|
+
* low ~ braced cable work (little)
|
|
39
|
+
* none ~ machine/bar carries the load — bodyweight isn't the resistance
|
|
40
|
+
*/
|
|
41
|
+
const BW_DEPENDENCY_FRACTION = {
|
|
42
|
+
high: 0.9,
|
|
43
|
+
medium: 0.6,
|
|
44
|
+
low: 0.3,
|
|
45
|
+
none: 0,
|
|
46
|
+
};
|
|
47
|
+
// --- Epley -----------------------------------------------------------------
|
|
48
|
+
/**
|
|
49
|
+
* Parse a numeric string from a record, guarding against NaN and negatives.
|
|
50
|
+
* parseFloat("abc") is NaN; a logged weight/reps should never be negative.
|
|
51
|
+
*/
|
|
52
|
+
function safeParseFloat(value, fallback = 0) {
|
|
53
|
+
const parsed = parseFloat(value !== null && value !== void 0 ? value : "");
|
|
54
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
|
|
55
|
+
}
|
|
56
|
+
/** Estimated 1RM via Epley, with reps capped to avoid endurance-range inflation. */
|
|
57
|
+
function estimateOneRepMax(weightKg, reps) {
|
|
58
|
+
if (weightKg <= 0 || reps <= 0)
|
|
59
|
+
return 0;
|
|
60
|
+
const cappedReps = Math.min(reps, exports.EPLEY_REP_CAP);
|
|
61
|
+
return weightKg * (1 + cappedReps / 30);
|
|
62
|
+
}
|
|
63
|
+
// --- Bodyweight load fraction ----------------------------------------------
|
|
64
|
+
/**
|
|
65
|
+
* The fraction of bodyweight a bodyweight/reps-only exercise loads onto the
|
|
66
|
+
* target muscles. Prefers the exercise's own per-sex `weightMultiplier`; falls
|
|
67
|
+
* back to the `bodyweightDependency` map; final fallback is a mild 0.5.
|
|
68
|
+
*/
|
|
69
|
+
function bodyweightLoadFraction(exercise, gender) {
|
|
70
|
+
const wm = exercise.weightMultiplier;
|
|
71
|
+
if (wm) {
|
|
72
|
+
if (gender === "female")
|
|
73
|
+
return wm.female;
|
|
74
|
+
if (gender === "male")
|
|
75
|
+
return wm.male;
|
|
76
|
+
return wm.default;
|
|
77
|
+
}
|
|
78
|
+
const dep = exercise.bodyweightDependency;
|
|
79
|
+
if (dep && dep in BW_DEPENDENCY_FRACTION)
|
|
80
|
+
return BW_DEPENDENCY_FRACTION[dep];
|
|
81
|
+
return 0.5;
|
|
82
|
+
}
|
|
83
|
+
// --- Per-set effective load ------------------------------------------------
|
|
84
|
+
/**
|
|
85
|
+
* Best estimated-1RM contribution of a single completed set, in kg.
|
|
86
|
+
* Switches on the TRecord discriminated union (no casts). Non-strength record
|
|
87
|
+
* types (duration, cardio) contribute 0.
|
|
88
|
+
*/
|
|
89
|
+
function setEstimatedLoadKg(set, exercise, bodyweightKg, gender) {
|
|
90
|
+
if (!set.isDone)
|
|
91
|
+
return 0;
|
|
92
|
+
if (set.type === "weight-reps") {
|
|
93
|
+
const kg = safeParseFloat(set.kg);
|
|
94
|
+
const reps = safeParseFloat(set.reps);
|
|
95
|
+
// Unilateral: weight is logged per side -> double for total mechanical load.
|
|
96
|
+
const totalKg = kg * (exercise.isUnilateral ? 2 : 1);
|
|
97
|
+
return estimateOneRepMax(totalKg, reps);
|
|
98
|
+
}
|
|
99
|
+
if (set.type === "reps-only") {
|
|
100
|
+
const reps = safeParseFloat(set.reps);
|
|
101
|
+
const auxKg = safeParseFloat(set.auxWeightKg);
|
|
102
|
+
const fraction = bodyweightLoadFraction(exercise, gender);
|
|
103
|
+
const bodyweightLoad = fraction * bodyweightKg;
|
|
104
|
+
// Added weight (weighted pull-ups/dips) stacks on the bodyweight load.
|
|
105
|
+
const effectiveWeight = bodyweightLoad + auxKg;
|
|
106
|
+
return estimateOneRepMax(effectiveWeight, reps);
|
|
107
|
+
}
|
|
108
|
+
// duration / cardio-machine / cardio-free are not strength loads.
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Compute the best normalised load for a set of matching exercises within the
|
|
113
|
+
* window.
|
|
114
|
+
*
|
|
115
|
+
* @param matchingExerciseIds Exercise ids belonging to the target pattern.
|
|
116
|
+
* @param sessions All sessions.
|
|
117
|
+
* @param exerciseCatalog Full catalog (unfiltered).
|
|
118
|
+
* @param bodyweightKg User bodyweight (falls back to default).
|
|
119
|
+
* @param gender User sex (selects the weightMultiplier column).
|
|
120
|
+
* @param windowStartMs Only sessions on/after this count.
|
|
121
|
+
* @param now Upper bound (sessions after `now` ignored).
|
|
122
|
+
*/
|
|
123
|
+
function computeNormalisedLoad(matchingExerciseIds, sessions, exerciseCatalog, bodyweightKg, gender, windowStartMs, now) {
|
|
124
|
+
const bw = bodyweightKg > 0 ? bodyweightKg : DEFAULT_BODYWEIGHT_KG;
|
|
125
|
+
let bestKg = 0;
|
|
126
|
+
let bestExerciseId;
|
|
127
|
+
for (const session of sessions) {
|
|
128
|
+
if (session.date < windowStartMs || session.date > now)
|
|
129
|
+
continue;
|
|
130
|
+
for (const ex of session.exercises) {
|
|
131
|
+
if (!matchingExerciseIds.has(ex.exerciseId))
|
|
132
|
+
continue;
|
|
133
|
+
const exercise = exerciseCatalog[ex.exerciseId];
|
|
134
|
+
if (!exercise)
|
|
135
|
+
continue;
|
|
136
|
+
for (const set of ex.records) {
|
|
137
|
+
const loadKg = setEstimatedLoadKg(set, exercise, bw, gender);
|
|
138
|
+
if (loadKg > bestKg) {
|
|
139
|
+
bestKg = loadKg;
|
|
140
|
+
bestExerciseId = ex.exerciseId;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
ratio: bestKg / bw,
|
|
147
|
+
bestOneRepMaxKg: bestKg,
|
|
148
|
+
bestExerciseId,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX CONSTELLATION — Orchestrator
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* The single entry point. Takes raw input (sessions, catalog, user, now) and
|
|
7
|
+
* returns a render-ready TConstellationState: six resolved stars plus the
|
|
8
|
+
* figure aggregate that drives the silhouette's aura.
|
|
9
|
+
*
|
|
10
|
+
* Purely live — everything is recomputed from raw records on every call.
|
|
11
|
+
* Nothing is persisted; the same input always yields the same output.
|
|
12
|
+
*
|
|
13
|
+
* Scoring happens ONCE here: every (session, exercise) is scored a single time
|
|
14
|
+
* into scoredSessions, which both quality and recovery read — no double work.
|
|
15
|
+
*
|
|
16
|
+
* Runs server-side (Lambda) so the evaluation logic can evolve independently of
|
|
17
|
+
* app releases.
|
|
18
|
+
*/
|
|
19
|
+
import type { TConstellationInput, TConstellationState } from "./types";
|
|
20
|
+
/**
|
|
21
|
+
* Evaluate the full constellation from raw input.
|
|
22
|
+
*
|
|
23
|
+
* @param input sessions + exercise catalog (UNFILTERED) + user + now + level
|
|
24
|
+
* @returns render-ready state: six stars + figure aggregate
|
|
25
|
+
* @throws if `now` is not a finite timestamp
|
|
26
|
+
*/
|
|
27
|
+
export declare function evaluateConstellation(input: TConstellationInput): TConstellationState;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/constellation/evaluateConstellation.ts — Orchestrator
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX CONSTELLATION — Orchestrator
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* The single entry point. Takes raw input (sessions, catalog, user, now) and
|
|
9
|
+
* returns a render-ready TConstellationState: six resolved stars plus the
|
|
10
|
+
* figure aggregate that drives the silhouette's aura.
|
|
11
|
+
*
|
|
12
|
+
* Purely live — everything is recomputed from raw records on every call.
|
|
13
|
+
* Nothing is persisted; the same input always yields the same output.
|
|
14
|
+
*
|
|
15
|
+
* Scoring happens ONCE here: every (session, exercise) is scored a single time
|
|
16
|
+
* into scoredSessions, which both quality and recovery read — no double work.
|
|
17
|
+
*
|
|
18
|
+
* Runs server-side (Lambda) so the evaluation logic can evolve independently of
|
|
19
|
+
* app releases.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.evaluateConstellation = evaluateConstellation;
|
|
23
|
+
const scoringWorkout_1 = require("../scoringWorkout");
|
|
24
|
+
const starFoundation_1 = require("./starFoundation");
|
|
25
|
+
const levelThresholds_1 = require("./levelThresholds");
|
|
26
|
+
const consistency_1 = require("./stars/consistency");
|
|
27
|
+
const quality_1 = require("./stars/quality");
|
|
28
|
+
const push_1 = require("./stars/push");
|
|
29
|
+
const pull_1 = require("./stars/pull");
|
|
30
|
+
const lowerBody_1 = require("./stars/lowerBody");
|
|
31
|
+
const recovery_1 = require("./stars/recovery");
|
|
32
|
+
/**
|
|
33
|
+
* The six stars, in figure reading order: heart (consistency) and core
|
|
34
|
+
* (quality) first, then the limbs (push/pull/lowerBody), then the crown
|
|
35
|
+
* (recovery). Appending a level/goal star later = adding to this list.
|
|
36
|
+
*/
|
|
37
|
+
const STAR_DEFINITIONS = [
|
|
38
|
+
consistency_1.consistencyStar,
|
|
39
|
+
quality_1.qualityStar,
|
|
40
|
+
push_1.pushStar,
|
|
41
|
+
pull_1.pullStar,
|
|
42
|
+
lowerBody_1.lowerBodyStar,
|
|
43
|
+
recovery_1.recoveryStar,
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Score every (session, exercise) exactly once. Shared by quality (uses
|
|
47
|
+
* `score`) and recovery (uses `muscleScores`). An exercise missing from the
|
|
48
|
+
* catalog, or one the engine cannot score, yields score=null + empty muscles
|
|
49
|
+
* and is skipped downstream.
|
|
50
|
+
*/
|
|
51
|
+
function scoreAllSessions(input) {
|
|
52
|
+
var _a;
|
|
53
|
+
const { sessions, exerciseCatalog, user, now } = input;
|
|
54
|
+
const scored = [];
|
|
55
|
+
for (const session of sessions) {
|
|
56
|
+
if (session.date > now)
|
|
57
|
+
continue; // ignore future-dated sessions
|
|
58
|
+
const exercises = [];
|
|
59
|
+
for (const ex of session.exercises) {
|
|
60
|
+
const exercise = exerciseCatalog[ex.exerciseId];
|
|
61
|
+
if (!exercise) {
|
|
62
|
+
exercises.push({
|
|
63
|
+
exerciseId: ex.exerciseId,
|
|
64
|
+
score: null,
|
|
65
|
+
muscleScores: {},
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const result = (0, scoringWorkout_1.calculateExerciseScoreV2)({
|
|
71
|
+
exercise,
|
|
72
|
+
record: ex.records,
|
|
73
|
+
user: user,
|
|
74
|
+
});
|
|
75
|
+
exercises.push({
|
|
76
|
+
exerciseId: ex.exerciseId,
|
|
77
|
+
score: typeof result.score === "number" ? result.score : null,
|
|
78
|
+
muscleScores: (_a = result.muscleScores) !== null && _a !== void 0 ? _a : {},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (_b) {
|
|
82
|
+
exercises.push({
|
|
83
|
+
exerciseId: ex.exerciseId,
|
|
84
|
+
score: null,
|
|
85
|
+
muscleScores: {},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
scored.push({ date: session.date, exercises });
|
|
90
|
+
}
|
|
91
|
+
return scored;
|
|
92
|
+
}
|
|
93
|
+
/** Build the figure aggregate from the resolved stars. */
|
|
94
|
+
function computeFigure(stars) {
|
|
95
|
+
if (stars.length === 0) {
|
|
96
|
+
return { auraIntensity: 0, fullStarCount: 0 };
|
|
97
|
+
}
|
|
98
|
+
const totalBrightness = stars.reduce((sum, s) => sum + s.brightness, 0);
|
|
99
|
+
const auraIntensity = totalBrightness / stars.length;
|
|
100
|
+
const fullStarCount = stars.filter((s) => s.tier === 3).length;
|
|
101
|
+
return {
|
|
102
|
+
auraIntensity: Math.round(auraIntensity * 10) / 10,
|
|
103
|
+
fullStarCount,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Evaluate the full constellation from raw input.
|
|
108
|
+
*
|
|
109
|
+
* @param input sessions + exercise catalog (UNFILTERED) + user + now + level
|
|
110
|
+
* @returns render-ready state: six stars + figure aggregate
|
|
111
|
+
* @throws if `now` is not a finite timestamp
|
|
112
|
+
*/
|
|
113
|
+
function evaluateConstellation(input) {
|
|
114
|
+
if (!Number.isFinite(input.now)) {
|
|
115
|
+
throw new Error("evaluateConstellation: `now` must be a finite UNIX ms timestamp");
|
|
116
|
+
}
|
|
117
|
+
const level = (0, levelThresholds_1.clampLevel)(input.currentLevel);
|
|
118
|
+
const scoredSessions = scoreAllSessions(input);
|
|
119
|
+
const ctx = {
|
|
120
|
+
sessions: input.sessions,
|
|
121
|
+
scoredSessions,
|
|
122
|
+
exerciseCatalog: input.exerciseCatalog,
|
|
123
|
+
user: input.user,
|
|
124
|
+
now: input.now,
|
|
125
|
+
level,
|
|
126
|
+
};
|
|
127
|
+
const stars = STAR_DEFINITIONS.map((def) => (0, starFoundation_1.resolveStar)(def, ctx));
|
|
128
|
+
const figure = computeFigure(stars);
|
|
129
|
+
return {
|
|
130
|
+
stars,
|
|
131
|
+
figure,
|
|
132
|
+
currentLevel: level,
|
|
133
|
+
evaluatedAt: new Date(input.now).toISOString(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX CONSTELLATION — Public API
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* The constellation module's surface. Consumers (the Lambda, tests) import from
|
|
7
|
+
* here, not from internal files.
|
|
8
|
+
*
|
|
9
|
+
* import { evaluateConstellation } from ".../constellation";
|
|
10
|
+
* const state = evaluateConstellation({ sessions, exerciseCatalog, user, now });
|
|
11
|
+
*
|
|
12
|
+
* Everything else (individual stars, helpers, the normalised-load math) is an
|
|
13
|
+
* implementation detail and intentionally not re-exported.
|
|
14
|
+
*/
|
|
15
|
+
export { evaluateConstellation } from "./evaluateConstellation";
|
|
16
|
+
export type { TConstellationInput, TConstellationState, TConstellationFigure, TResolvedStar, TStarId, TStarTier, TFigurePosition, TStarMeasure, TLocalizedText, TSessionRecord, } from "./types";
|
|
17
|
+
export { TIER_THRESHOLDS, deriveTier } from "./starFoundation";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/constellation/index.ts — Public API
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX CONSTELLATION — Public API
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* The constellation module's surface. Consumers (the Lambda, tests) import from
|
|
9
|
+
* here, not from internal files.
|
|
10
|
+
*
|
|
11
|
+
* import { evaluateConstellation } from ".../constellation";
|
|
12
|
+
* const state = evaluateConstellation({ sessions, exerciseCatalog, user, now });
|
|
13
|
+
*
|
|
14
|
+
* Everything else (individual stars, helpers, the normalised-load math) is an
|
|
15
|
+
* implementation detail and intentionally not re-exported.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.deriveTier = exports.TIER_THRESHOLDS = exports.evaluateConstellation = void 0;
|
|
19
|
+
// The single entry point.
|
|
20
|
+
var evaluateConstellation_1 = require("./evaluateConstellation");
|
|
21
|
+
Object.defineProperty(exports, "evaluateConstellation", { enumerable: true, get: function () { return evaluateConstellation_1.evaluateConstellation; } });
|
|
22
|
+
// Tier thresholds are exported so the app can label tiers consistently with
|
|
23
|
+
// the server (dormant / faint / rising / full) without hard-coding the cutoffs.
|
|
24
|
+
var starFoundation_1 = require("./starFoundation");
|
|
25
|
+
Object.defineProperty(exports, "TIER_THRESHOLDS", { enumerable: true, get: function () { return starFoundation_1.TIER_THRESHOLDS; } });
|
|
26
|
+
Object.defineProperty(exports, "deriveTier", { enumerable: true, get: function () { return starFoundation_1.deriveTier; } });
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX CONSTELLATION — Level Threshold Ladder
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* The single source of truth for what "full" means at each level, for every
|
|
7
|
+
* star. Stars read their row by the user's current level; nothing about a
|
|
8
|
+
* threshold is hard-coded inside star logic.
|
|
9
|
+
*
|
|
10
|
+
* Design intent (coach's calibration):
|
|
11
|
+
* Level 1 = committed beginner (reachable in ~2-3 months of honest work)
|
|
12
|
+
* Level 2 = solid intermediate (~6-12 months)
|
|
13
|
+
* Level 3 = advanced (multi-year, genuinely strong)
|
|
14
|
+
*
|
|
15
|
+
* Strength thresholds are estimated-1RM as a ratio of bodyweight, by sex.
|
|
16
|
+
* `unmentioned` gender is resolved to `male` (the higher bar) elsewhere, so a
|
|
17
|
+
* star is never too-easily lit for an unspecified profile.
|
|
18
|
+
*
|
|
19
|
+
* Calibration note (v1.1): these are first-pass values to be tuned against real
|
|
20
|
+
* brightness distributions post-launch. L1 pull was lowered from 0.65 -> 0.50
|
|
21
|
+
* because 0.65x BW pull excluded the large beginner segment that cannot yet do
|
|
22
|
+
* bodyweight pull-ups; L1 push eased 0.75 -> 0.60 so the first milestone lands
|
|
23
|
+
* in ~6-8 weeks rather than feeling intermediate. Lower body held at 1.0x — the
|
|
24
|
+
* bodyweight-load path lets home users reach it via pistol squats/lunges.
|
|
25
|
+
*
|
|
26
|
+
* Adding a level later = adding a key to each record. Star code does not change.
|
|
27
|
+
* The future goal layer can multiply these per-goal without touching this file.
|
|
28
|
+
*/
|
|
29
|
+
export type TLevel = 1 | 2 | 3;
|
|
30
|
+
export declare const MAX_LEVEL: TLevel;
|
|
31
|
+
export interface ISexThreshold {
|
|
32
|
+
male: number;
|
|
33
|
+
female: number;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Bodyweight-ratio thresholds for a full strength star, per level.
|
|
37
|
+
*
|
|
38
|
+
* Push (bench/press family):
|
|
39
|
+
* L1 0.60 — a committed beginner pressing meaningful load (~6-8 weeks)
|
|
40
|
+
* L2 1.00 — pressing ~bodyweight (classic intermediate bench)
|
|
41
|
+
* L3 1.40 — advanced pressing strength
|
|
42
|
+
* Pull (row/pulldown/pull-up family):
|
|
43
|
+
* L1 0.50 — reachable beginner pulling (rows / pulldowns), pre-pull-up
|
|
44
|
+
* L2 0.85 — strong rows / clean bodyweight pull-ups
|
|
45
|
+
* L3 1.20 — advanced (heavy weighted pull-ups)
|
|
46
|
+
* Lower (squat/hinge/lunge family):
|
|
47
|
+
* L1 1.00 — beginner squatting ~bodyweight (reachable via bodyweight work too)
|
|
48
|
+
* L2 1.50 — intermediate
|
|
49
|
+
* L3 2.00 — advanced (a 2x bodyweight squat is a real milestone)
|
|
50
|
+
*/
|
|
51
|
+
export declare const STRENGTH_THRESHOLDS: Record<"push" | "pull" | "lowerBody", Record<TLevel, ISexThreshold>>;
|
|
52
|
+
export interface IConsistencyThreshold {
|
|
53
|
+
/** Training days required within the window for full brightness. */
|
|
54
|
+
requiredSessions: number;
|
|
55
|
+
/** Trailing window length in days. */
|
|
56
|
+
windowDays: number;
|
|
57
|
+
/** Maximum idle gap (days) allowed before the no-gap condition breaks. */
|
|
58
|
+
maxGapDays: number;
|
|
59
|
+
/** Trailing idle days at which the "cooling" warning begins. */
|
|
60
|
+
warningAtIdleDays: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* L1 ~2x/week habit; L2 ~3x/week; L3 ~4x/week. Window stays 28 days so it always
|
|
64
|
+
* reads "this month's habit".
|
|
65
|
+
*
|
|
66
|
+
* L3 maxGap relaxed 3 -> 4 (v1.1): a 3-day cap punished legitimate splits and
|
|
67
|
+
* planned deloads. 4 still keeps higher-level gap discipline tighter than L1/L2
|
|
68
|
+
* without penalising smart programming.
|
|
69
|
+
*/
|
|
70
|
+
export declare const CONSISTENCY_THRESHOLDS: Record<TLevel, IConsistencyThreshold>;
|
|
71
|
+
export interface IQualityThreshold {
|
|
72
|
+
/** A session "clears" at or above this score (0-100). */
|
|
73
|
+
bar: number;
|
|
74
|
+
/** Number of most-recent sessions considered. */
|
|
75
|
+
windowSessions: number;
|
|
76
|
+
/** How many of the window must clear for full brightness. */
|
|
77
|
+
targetClearing: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* L1 forgiving bar (70) over a short window; higher levels demand both a higher
|
|
81
|
+
* bar and a longer clean streak.
|
|
82
|
+
*/
|
|
83
|
+
export declare const QUALITY_THRESHOLDS: Record<TLevel, IQualityThreshold>;
|
|
84
|
+
export interface IRecoveryThreshold {
|
|
85
|
+
/** Recovery cycles needed for full brightness (summed across trained zones). */
|
|
86
|
+
targetCycles: number;
|
|
87
|
+
/** Trailing window length in days. */
|
|
88
|
+
windowDays: number;
|
|
89
|
+
/** A muscle is "recovered" below this fatigue value (0-100). */
|
|
90
|
+
recoveredBelow: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Cycles are counted PER trained zone (upper/lower/core), so targets reflect
|
|
94
|
+
* summed zone cycles. Recovery is meaningful only alongside training volume — a
|
|
95
|
+
* no-train user earns nothing (untrained zones give no credit).
|
|
96
|
+
*/
|
|
97
|
+
export declare const RECOVERY_THRESHOLDS: Record<TLevel, IRecoveryThreshold>;
|
|
98
|
+
/** Clamp any incoming level into the valid 1..MAX_LEVEL range. */
|
|
99
|
+
export declare function clampLevel(level: number | undefined): TLevel;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/constellation/levelThresholds.ts — Per-level threshold ladder
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX CONSTELLATION — Level Threshold Ladder
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* The single source of truth for what "full" means at each level, for every
|
|
9
|
+
* star. Stars read their row by the user's current level; nothing about a
|
|
10
|
+
* threshold is hard-coded inside star logic.
|
|
11
|
+
*
|
|
12
|
+
* Design intent (coach's calibration):
|
|
13
|
+
* Level 1 = committed beginner (reachable in ~2-3 months of honest work)
|
|
14
|
+
* Level 2 = solid intermediate (~6-12 months)
|
|
15
|
+
* Level 3 = advanced (multi-year, genuinely strong)
|
|
16
|
+
*
|
|
17
|
+
* Strength thresholds are estimated-1RM as a ratio of bodyweight, by sex.
|
|
18
|
+
* `unmentioned` gender is resolved to `male` (the higher bar) elsewhere, so a
|
|
19
|
+
* star is never too-easily lit for an unspecified profile.
|
|
20
|
+
*
|
|
21
|
+
* Calibration note (v1.1): these are first-pass values to be tuned against real
|
|
22
|
+
* brightness distributions post-launch. L1 pull was lowered from 0.65 -> 0.50
|
|
23
|
+
* because 0.65x BW pull excluded the large beginner segment that cannot yet do
|
|
24
|
+
* bodyweight pull-ups; L1 push eased 0.75 -> 0.60 so the first milestone lands
|
|
25
|
+
* in ~6-8 weeks rather than feeling intermediate. Lower body held at 1.0x — the
|
|
26
|
+
* bodyweight-load path lets home users reach it via pistol squats/lunges.
|
|
27
|
+
*
|
|
28
|
+
* Adding a level later = adding a key to each record. Star code does not change.
|
|
29
|
+
* The future goal layer can multiply these per-goal without touching this file.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.RECOVERY_THRESHOLDS = exports.QUALITY_THRESHOLDS = exports.CONSISTENCY_THRESHOLDS = exports.STRENGTH_THRESHOLDS = exports.MAX_LEVEL = void 0;
|
|
33
|
+
exports.clampLevel = clampLevel;
|
|
34
|
+
exports.MAX_LEVEL = 3;
|
|
35
|
+
/**
|
|
36
|
+
* Bodyweight-ratio thresholds for a full strength star, per level.
|
|
37
|
+
*
|
|
38
|
+
* Push (bench/press family):
|
|
39
|
+
* L1 0.60 — a committed beginner pressing meaningful load (~6-8 weeks)
|
|
40
|
+
* L2 1.00 — pressing ~bodyweight (classic intermediate bench)
|
|
41
|
+
* L3 1.40 — advanced pressing strength
|
|
42
|
+
* Pull (row/pulldown/pull-up family):
|
|
43
|
+
* L1 0.50 — reachable beginner pulling (rows / pulldowns), pre-pull-up
|
|
44
|
+
* L2 0.85 — strong rows / clean bodyweight pull-ups
|
|
45
|
+
* L3 1.20 — advanced (heavy weighted pull-ups)
|
|
46
|
+
* Lower (squat/hinge/lunge family):
|
|
47
|
+
* L1 1.00 — beginner squatting ~bodyweight (reachable via bodyweight work too)
|
|
48
|
+
* L2 1.50 — intermediate
|
|
49
|
+
* L3 2.00 — advanced (a 2x bodyweight squat is a real milestone)
|
|
50
|
+
*/
|
|
51
|
+
exports.STRENGTH_THRESHOLDS = {
|
|
52
|
+
push: {
|
|
53
|
+
1: { male: 0.6, female: 0.45 },
|
|
54
|
+
2: { male: 1.0, female: 0.7 },
|
|
55
|
+
3: { male: 1.4, female: 0.95 },
|
|
56
|
+
},
|
|
57
|
+
pull: {
|
|
58
|
+
1: { male: 0.5, female: 0.38 },
|
|
59
|
+
2: { male: 0.85, female: 0.6 },
|
|
60
|
+
3: { male: 1.2, female: 0.85 },
|
|
61
|
+
},
|
|
62
|
+
lowerBody: {
|
|
63
|
+
1: { male: 1.0, female: 0.75 },
|
|
64
|
+
2: { male: 1.5, female: 1.1 },
|
|
65
|
+
3: { male: 2.0, female: 1.5 },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* L1 ~2x/week habit; L2 ~3x/week; L3 ~4x/week. Window stays 28 days so it always
|
|
70
|
+
* reads "this month's habit".
|
|
71
|
+
*
|
|
72
|
+
* L3 maxGap relaxed 3 -> 4 (v1.1): a 3-day cap punished legitimate splits and
|
|
73
|
+
* planned deloads. 4 still keeps higher-level gap discipline tighter than L1/L2
|
|
74
|
+
* without penalising smart programming.
|
|
75
|
+
*/
|
|
76
|
+
exports.CONSISTENCY_THRESHOLDS = {
|
|
77
|
+
1: {
|
|
78
|
+
requiredSessions: 8,
|
|
79
|
+
windowDays: 28,
|
|
80
|
+
maxGapDays: 5,
|
|
81
|
+
warningAtIdleDays: 4,
|
|
82
|
+
},
|
|
83
|
+
2: {
|
|
84
|
+
requiredSessions: 12,
|
|
85
|
+
windowDays: 28,
|
|
86
|
+
maxGapDays: 4,
|
|
87
|
+
warningAtIdleDays: 3,
|
|
88
|
+
},
|
|
89
|
+
3: {
|
|
90
|
+
requiredSessions: 16,
|
|
91
|
+
windowDays: 28,
|
|
92
|
+
maxGapDays: 4,
|
|
93
|
+
warningAtIdleDays: 3,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* L1 forgiving bar (70) over a short window; higher levels demand both a higher
|
|
98
|
+
* bar and a longer clean streak.
|
|
99
|
+
*/
|
|
100
|
+
exports.QUALITY_THRESHOLDS = {
|
|
101
|
+
1: { bar: 70, windowSessions: 5, targetClearing: 5 },
|
|
102
|
+
2: { bar: 78, windowSessions: 8, targetClearing: 7 },
|
|
103
|
+
3: { bar: 85, windowSessions: 10, targetClearing: 9 },
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Cycles are counted PER trained zone (upper/lower/core), so targets reflect
|
|
107
|
+
* summed zone cycles. Recovery is meaningful only alongside training volume — a
|
|
108
|
+
* no-train user earns nothing (untrained zones give no credit).
|
|
109
|
+
*/
|
|
110
|
+
exports.RECOVERY_THRESHOLDS = {
|
|
111
|
+
1: { targetCycles: 3, windowDays: 56, recoveredBelow: 25 },
|
|
112
|
+
2: { targetCycles: 5, windowDays: 56, recoveredBelow: 25 },
|
|
113
|
+
3: { targetCycles: 7, windowDays: 56, recoveredBelow: 25 },
|
|
114
|
+
};
|
|
115
|
+
// --- Level helper -----------------------------------------------------------
|
|
116
|
+
/** Clamp any incoming level into the valid 1..MAX_LEVEL range. */
|
|
117
|
+
function clampLevel(level) {
|
|
118
|
+
if (!level || level < 1)
|
|
119
|
+
return 1;
|
|
120
|
+
if (level > exports.MAX_LEVEL)
|
|
121
|
+
return exports.MAX_LEVEL;
|
|
122
|
+
return Math.floor(level);
|
|
123
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX CONSTELLATION — Star Foundation
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Shared pure glue. Types come from ./types. Behaviour only:
|
|
7
|
+
* - deriveTier: continuous brightness -> discrete band (0-3)
|
|
8
|
+
* - resolveStar: definition + context -> render-ready TResolvedStar
|
|
9
|
+
*/
|
|
10
|
+
import type { TStarTier, TStarDefinition, TStarContext, TResolvedStar } from "./types";
|
|
11
|
+
/** Brightness lower-bounds per tier. Tunable in one place. */
|
|
12
|
+
export declare const TIER_THRESHOLDS: {
|
|
13
|
+
readonly faint: 1;
|
|
14
|
+
readonly rising: 34;
|
|
15
|
+
readonly full: 100;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* 0 dormant (<1) - 1 faint (1-33) - 2 rising (34-99) - 3 full (>=100).
|
|
19
|
+
*/
|
|
20
|
+
export declare function deriveTier(brightness: number): TStarTier;
|
|
21
|
+
/**
|
|
22
|
+
* Run a star definition through its evaluator into the render-ready shape.
|
|
23
|
+
* Brightness clamped 0-100 here so evaluators don't each have to.
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveStar(def: TStarDefinition, ctx: TStarContext): TResolvedStar;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/constellation/starFoundation.ts — Star Foundation
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX CONSTELLATION — Star Foundation
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* Shared pure glue. Types come from ./types. Behaviour only:
|
|
9
|
+
* - deriveTier: continuous brightness -> discrete band (0-3)
|
|
10
|
+
* - resolveStar: definition + context -> render-ready TResolvedStar
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.TIER_THRESHOLDS = void 0;
|
|
14
|
+
exports.deriveTier = deriveTier;
|
|
15
|
+
exports.resolveStar = resolveStar;
|
|
16
|
+
/** Brightness lower-bounds per tier. Tunable in one place. */
|
|
17
|
+
exports.TIER_THRESHOLDS = {
|
|
18
|
+
faint: 1,
|
|
19
|
+
rising: 34,
|
|
20
|
+
full: 100,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* 0 dormant (<1) - 1 faint (1-33) - 2 rising (34-99) - 3 full (>=100).
|
|
24
|
+
*/
|
|
25
|
+
function deriveTier(brightness) {
|
|
26
|
+
if (brightness >= exports.TIER_THRESHOLDS.full)
|
|
27
|
+
return 3;
|
|
28
|
+
if (brightness >= exports.TIER_THRESHOLDS.rising)
|
|
29
|
+
return 2;
|
|
30
|
+
if (brightness >= exports.TIER_THRESHOLDS.faint)
|
|
31
|
+
return 1;
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Run a star definition through its evaluator into the render-ready shape.
|
|
36
|
+
* Brightness clamped 0-100 here so evaluators don't each have to.
|
|
37
|
+
*/
|
|
38
|
+
function resolveStar(def, ctx) {
|
|
39
|
+
const { brightness, detail } = def.evaluate(ctx);
|
|
40
|
+
const clamped = Math.max(0, Math.min(100, brightness));
|
|
41
|
+
return {
|
|
42
|
+
id: def.id,
|
|
43
|
+
displayName: def.displayName,
|
|
44
|
+
color: def.color,
|
|
45
|
+
figurePosition: def.figurePosition,
|
|
46
|
+
brightness: clamped,
|
|
47
|
+
tier: deriveTier(clamped),
|
|
48
|
+
objective: def.objective,
|
|
49
|
+
rationale: def.rationale,
|
|
50
|
+
currentState: detail.currentState,
|
|
51
|
+
target: detail.target,
|
|
52
|
+
gap: detail.gap,
|
|
53
|
+
};
|
|
54
|
+
}
|