@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,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
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX CONSTELLATION — Consistency Star (the heart)
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* The live "showing up" star. Brightness reflects the user's CURRENT training
|
|
7
|
+
* habit, recomputed every call from recent session dates. The one star meant to
|
|
8
|
+
* be maintained, not achieved — it brightens with a steady habit and dims when
|
|
9
|
+
* the user goes quiet.
|
|
10
|
+
*
|
|
11
|
+
* Full brightness requires BOTH:
|
|
12
|
+
* - requiredSessions training days within the trailing window, AND
|
|
13
|
+
* - the trailing idle gap (last session -> now) within maxGapDays.
|
|
14
|
+
*
|
|
15
|
+
* Brightness is graded below full so progress is always visible:
|
|
16
|
+
* - countComponent — progress toward the required session count
|
|
17
|
+
* - gapComponent — a live penalty as the TRAILING gap exceeds maxGapDays
|
|
18
|
+
*
|
|
19
|
+
* Note: brightness uses the TRAILING gap, not interior gaps. An old gap the user
|
|
20
|
+
* has since recovered from should not permanently dim a live habit signal.
|
|
21
|
+
* Interior gaps still inform the gap MESSAGE for context.
|
|
22
|
+
*
|
|
23
|
+
* Counting rule: a session counts as a training day only if it has >=1 completed
|
|
24
|
+
* set. Multiple sessions on one calendar day = one training day.
|
|
25
|
+
*
|
|
26
|
+
* Thresholds come from CONSISTENCY_THRESHOLDS[level].
|
|
27
|
+
*/
|
|
28
|
+
import type { TStarDefinition } from "../types";
|
|
29
|
+
export declare const consistencyStar: TStarDefinition;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/constellation/stars/consistency.ts — Consistency Star (the heart)
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX CONSTELLATION — Consistency Star (the heart)
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* The live "showing up" star. Brightness reflects the user's CURRENT training
|
|
9
|
+
* habit, recomputed every call from recent session dates. The one star meant to
|
|
10
|
+
* be maintained, not achieved — it brightens with a steady habit and dims when
|
|
11
|
+
* the user goes quiet.
|
|
12
|
+
*
|
|
13
|
+
* Full brightness requires BOTH:
|
|
14
|
+
* - requiredSessions training days within the trailing window, AND
|
|
15
|
+
* - the trailing idle gap (last session -> now) within maxGapDays.
|
|
16
|
+
*
|
|
17
|
+
* Brightness is graded below full so progress is always visible:
|
|
18
|
+
* - countComponent — progress toward the required session count
|
|
19
|
+
* - gapComponent — a live penalty as the TRAILING gap exceeds maxGapDays
|
|
20
|
+
*
|
|
21
|
+
* Note: brightness uses the TRAILING gap, not interior gaps. An old gap the user
|
|
22
|
+
* has since recovered from should not permanently dim a live habit signal.
|
|
23
|
+
* Interior gaps still inform the gap MESSAGE for context.
|
|
24
|
+
*
|
|
25
|
+
* Counting rule: a session counts as a training day only if it has >=1 completed
|
|
26
|
+
* set. Multiple sessions on one calendar day = one training day.
|
|
27
|
+
*
|
|
28
|
+
* Thresholds come from CONSISTENCY_THRESHOLDS[level].
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.consistencyStar = void 0;
|
|
32
|
+
const levelThresholds_1 = require("../levelThresholds");
|
|
33
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
34
|
+
// --- Helpers ----------------------------------------------------------------
|
|
35
|
+
/** Does a session have at least one completed set? (isDone is a guaranteed bool on TRecord) */
|
|
36
|
+
function hasCompletedSet(session) {
|
|
37
|
+
for (const ex of session.exercises) {
|
|
38
|
+
for (const set of ex.records) {
|
|
39
|
+
if (set.isDone)
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function countMeasure(sessions, required) {
|
|
46
|
+
return {
|
|
47
|
+
value: sessions,
|
|
48
|
+
unit: "sessions",
|
|
49
|
+
display: {
|
|
50
|
+
translationKey: "constellation.consistency.measure.sessions",
|
|
51
|
+
params: [Math.min(sessions, required), required],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function buildGap(sessionsInWindow, longestGapDays, trailingGapDays, required, maxGapDays, warningAtIdleDays) {
|
|
56
|
+
// Brand-new user (no training days in window): invite, don't scold. Without
|
|
57
|
+
// this, a zero-session user trips the "broken" branch (trailingGap = window).
|
|
58
|
+
if (sessionsInWindow === 0) {
|
|
59
|
+
return { translationKey: "constellation.consistency.gap.getStarted" };
|
|
60
|
+
}
|
|
61
|
+
// Cooling warning: trailing gap is creeping toward the max.
|
|
62
|
+
if (trailingGapDays >= warningAtIdleDays && trailingGapDays <= maxGapDays) {
|
|
63
|
+
const daysRemaining = maxGapDays - trailingGapDays + 1;
|
|
64
|
+
return {
|
|
65
|
+
translationKey: "constellation.consistency.gap.cooling",
|
|
66
|
+
params: [daysRemaining],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (longestGapDays > maxGapDays) {
|
|
70
|
+
return { translationKey: "constellation.consistency.gap.broken" };
|
|
71
|
+
}
|
|
72
|
+
const remaining = Math.max(0, required - sessionsInWindow);
|
|
73
|
+
if (remaining > 0) {
|
|
74
|
+
return {
|
|
75
|
+
translationKey: "constellation.consistency.gap.moreSessions",
|
|
76
|
+
params: [remaining],
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return { translationKey: "constellation.consistency.gap.full" };
|
|
80
|
+
}
|
|
81
|
+
// --- Evaluator --------------------------------------------------------------
|
|
82
|
+
function evaluateConsistency(ctx) {
|
|
83
|
+
const { requiredSessions, windowDays, maxGapDays, warningAtIdleDays } = levelThresholds_1.CONSISTENCY_THRESHOLDS[ctx.level];
|
|
84
|
+
const { now } = ctx;
|
|
85
|
+
const windowStart = now - windowDays * MS_PER_DAY;
|
|
86
|
+
const nowDay = Math.floor(now / MS_PER_DAY);
|
|
87
|
+
// Training days = sessions with >=1 completed set, deduped to calendar day,
|
|
88
|
+
// within the trailing window, sorted ascending. Upper bound is by CALENDAR
|
|
89
|
+
// DAY (<= today) so a session logged later today than `now` still counts.
|
|
90
|
+
const dayIndices = Array.from(new Set(ctx.sessions
|
|
91
|
+
.filter((s) => {
|
|
92
|
+
if (!hasCompletedSet(s))
|
|
93
|
+
return false;
|
|
94
|
+
const d = Math.floor(s.date / MS_PER_DAY);
|
|
95
|
+
return s.date > windowStart && d <= nowDay;
|
|
96
|
+
})
|
|
97
|
+
.map((s) => Math.floor(s.date / MS_PER_DAY)))).sort((a, b) => a - b);
|
|
98
|
+
const sessionsInWindow = dayIndices.length;
|
|
99
|
+
// Longest interior gap (for the message only).
|
|
100
|
+
let longestGapDays = 0;
|
|
101
|
+
for (let i = 1; i < dayIndices.length; i++) {
|
|
102
|
+
const gap = dayIndices[i] - dayIndices[i - 1];
|
|
103
|
+
if (gap > longestGapDays)
|
|
104
|
+
longestGapDays = gap;
|
|
105
|
+
}
|
|
106
|
+
// Trailing gap: last training day -> today. No sessions -> maximally idle.
|
|
107
|
+
const trailingGap = dayIndices.length > 0
|
|
108
|
+
? nowDay - dayIndices[dayIndices.length - 1]
|
|
109
|
+
: windowDays;
|
|
110
|
+
if (trailingGap > longestGapDays)
|
|
111
|
+
longestGapDays = trailingGap;
|
|
112
|
+
// Brightness: two multiplicative components, both 0..1.
|
|
113
|
+
const countComponent = Math.min(1, sessionsInWindow / requiredSessions);
|
|
114
|
+
let gapComponent;
|
|
115
|
+
if (trailingGap <= maxGapDays) {
|
|
116
|
+
gapComponent = 1; // within allowed idle window — no penalty
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const over = trailingGap - maxGapDays;
|
|
120
|
+
const decayRange = Math.max(1, windowDays - maxGapDays);
|
|
121
|
+
gapComponent = Math.max(0, 1 - over / decayRange);
|
|
122
|
+
}
|
|
123
|
+
const brightness = Math.min(100, countComponent * gapComponent * 100);
|
|
124
|
+
return {
|
|
125
|
+
brightness,
|
|
126
|
+
detail: {
|
|
127
|
+
currentState: countMeasure(sessionsInWindow, requiredSessions),
|
|
128
|
+
target: countMeasure(requiredSessions, requiredSessions),
|
|
129
|
+
gap: buildGap(sessionsInWindow, longestGapDays, trailingGap, requiredSessions, maxGapDays, warningAtIdleDays),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// --- Definition -------------------------------------------------------------
|
|
134
|
+
exports.consistencyStar = {
|
|
135
|
+
id: "consistency",
|
|
136
|
+
displayName: { translationKey: "constellation.consistency.name" },
|
|
137
|
+
color: "#ff8c00", // amber-gold — the spark of the flame, the heart
|
|
138
|
+
figurePosition: "heart",
|
|
139
|
+
objective: { translationKey: "constellation.consistency.objective" },
|
|
140
|
+
rationale: { translationKey: "constellation.consistency.rationale" },
|
|
141
|
+
evaluate: evaluateConsistency,
|
|
142
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX CONSTELLATION — Lower Body Star
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Lower-body strength across squat, lunge, and hinge patterns combined, any
|
|
7
|
+
* equipment. Squats, goblet squats, leg press, lunges, deadlifts, RDLs, hip
|
|
8
|
+
* thrusts, kettlebell swings — all count. Best load across all three lights it.
|
|
9
|
+
* (Knee-vs-hip balance nuance deferred to the future goal layer.)
|
|
10
|
+
*
|
|
11
|
+
* Threshold per level from STRENGTH_THRESHOLDS.lowerBody, resolved by sex.
|
|
12
|
+
* Leg press is NOT special-cased — it is simply a squat-pattern exercise.
|
|
13
|
+
* Bodyweight lower-body work loads via the exercise's weightMultiplier, so home
|
|
14
|
+
* users can reach the threshold honestly.
|
|
15
|
+
*/
|
|
16
|
+
import type { TStarDefinition } from "../types";
|
|
17
|
+
export declare const lowerBodyStar: TStarDefinition;
|