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