@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.
Files changed (79) hide show
  1. package/README.md +148 -148
  2. package/dist/__mocks__/exercises.mock.js +1 -0
  3. package/dist/constants/index.d.ts +1 -0
  4. package/dist/constants/index.js +1 -0
  5. package/dist/constants/quickStartIntents.d.ts +19 -0
  6. package/dist/constants/quickStartIntents.js +39 -0
  7. package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
  8. package/dist/types/TApiClientConstellation.d.ts +33 -0
  9. package/dist/types/TApiClientConstellation.js +13 -0
  10. package/dist/types/TApiExercise.d.ts +5 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
  13. package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
  14. package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
  15. package/dist/utils/constellation/evaluateConstellation.js +135 -0
  16. package/dist/utils/constellation/index.d.ts +17 -0
  17. package/dist/utils/constellation/index.js +26 -0
  18. package/dist/utils/constellation/levelThresholds.d.ts +99 -0
  19. package/dist/utils/constellation/levelThresholds.js +123 -0
  20. package/dist/utils/constellation/starFoundation.d.ts +25 -0
  21. package/dist/utils/constellation/starFoundation.js +54 -0
  22. package/dist/utils/constellation/stars/consistency.d.ts +29 -0
  23. package/dist/utils/constellation/stars/consistency.js +142 -0
  24. package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
  25. package/dist/utils/constellation/stars/lowerBody.js +30 -0
  26. package/dist/utils/constellation/stars/pull.d.ts +11 -0
  27. package/dist/utils/constellation/stars/pull.js +24 -0
  28. package/dist/utils/constellation/stars/push.d.ts +11 -0
  29. package/dist/utils/constellation/stars/push.js +24 -0
  30. package/dist/utils/constellation/stars/quality.d.ts +19 -0
  31. package/dist/utils/constellation/stars/quality.js +98 -0
  32. package/dist/utils/constellation/stars/recovery.d.ts +29 -0
  33. package/dist/utils/constellation/stars/recovery.js +169 -0
  34. package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
  35. package/dist/utils/constellation/strengthStarHelpers.js +104 -0
  36. package/dist/utils/constellation/types.d.ts +124 -0
  37. package/dist/utils/constellation/types.js +18 -0
  38. package/dist/utils/index.d.ts +5 -3
  39. package/dist/utils/index.js +1 -0
  40. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
  41. package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
  42. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
  43. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
  44. package/dist/utils/scoringWorkout/constants.d.ts +20 -6
  45. package/dist/utils/scoringWorkout/constants.js +23 -9
  46. package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
  47. package/dist/utils/scoringWorkout/helpers.js +24 -18
  48. package/dist/utils/scoringWorkout/index.d.ts +12 -8
  49. package/dist/utils/scoringWorkout/index.js +23 -15
  50. package/dist/utils/scoringWorkout/parseRecords.js +4 -3
  51. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +210 -172
  52. package/dist/utils/scoringWorkout/types.d.ts +34 -14
  53. package/package.json +31 -31
  54. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
  55. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
  56. package/dist/utils/scaleProPlan.util.d.ts +0 -9
  57. package/dist/utils/scaleProPlan.util.js +0 -139
  58. package/dist/utils/scoring/calculateCalories.d.ts +0 -67
  59. package/dist/utils/scoring/calculateCalories.js +0 -345
  60. package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
  61. package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
  62. package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
  63. package/dist/utils/scoring/calculateQualityScore.js +0 -334
  64. package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
  65. package/dist/utils/scoring/calculateTotalVolume.js +0 -73
  66. package/dist/utils/scoring/constants.d.ts +0 -211
  67. package/dist/utils/scoring/constants.js +0 -247
  68. package/dist/utils/scoring/helpers.d.ts +0 -119
  69. package/dist/utils/scoring/helpers.js +0 -229
  70. package/dist/utils/scoring/index.d.ts +0 -28
  71. package/dist/utils/scoring/index.js +0 -47
  72. package/dist/utils/scoring/parseRecords.d.ts +0 -98
  73. package/dist/utils/scoring/parseRecords.js +0 -284
  74. package/dist/utils/scoring/types.d.ts +0 -86
  75. package/dist/utils/scoring/types.js +0 -11
  76. package/dist/utils/scoring.utils.d.ts +0 -14
  77. package/dist/utils/scoring.utils.js +0 -243
  78. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
  79. /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
+ }