@dgpholdings/greatoak-shared 1.2.86 → 1.2.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +148 -148
  2. package/dist/__mocks__/catalog.fixture.d.ts +2 -0
  3. package/dist/__mocks__/catalog.fixture.js +208 -0
  4. package/dist/__mocks__/exercises.mock.d.ts +4 -11
  5. package/dist/__mocks__/exercises.mock.js +82 -41
  6. package/dist/__mocks__/sessions.mock.d.ts +28 -0
  7. package/dist/__mocks__/sessions.mock.js +394 -0
  8. package/dist/__mocks__/testIds.d.ts +9 -0
  9. package/dist/__mocks__/testIds.js +13 -0
  10. package/dist/__mocks__/user.mock.js +3 -1
  11. package/dist/constants/goalJourney.d.ts +108 -0
  12. package/dist/constants/goalJourney.js +443 -0
  13. package/dist/constants/index.d.ts +1 -0
  14. package/dist/constants/index.js +1 -0
  15. package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
  16. package/dist/types/TApiClientConstellation.d.ts +33 -0
  17. package/dist/types/TApiClientConstellation.js +13 -0
  18. package/dist/types/TApiExercise.d.ts +5 -3
  19. package/dist/types/TApiUser.d.ts +2 -0
  20. package/dist/types/index.d.ts +1 -0
  21. package/dist/utils/adoptionEngine/scaleProPlan.util.js +9 -4
  22. package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
  23. package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
  24. package/dist/utils/constellation/computeNormalisedLoad.test.d.ts +1 -0
  25. package/dist/utils/constellation/computeNormalisedLoad.test.js +218 -0
  26. package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
  27. package/dist/utils/constellation/evaluateConstellation.js +135 -0
  28. package/dist/utils/constellation/evaluateConstellation.test.d.ts +1 -0
  29. package/dist/utils/constellation/evaluateConstellation.test.js +93 -0
  30. package/dist/utils/constellation/index.d.ts +18 -0
  31. package/dist/utils/constellation/index.js +29 -0
  32. package/dist/utils/constellation/levelThresholds.d.ts +99 -0
  33. package/dist/utils/constellation/levelThresholds.js +123 -0
  34. package/dist/utils/constellation/starFoundation.d.ts +25 -0
  35. package/dist/utils/constellation/starFoundation.js +54 -0
  36. package/dist/utils/constellation/starFoundation.test.d.ts +1 -0
  37. package/dist/utils/constellation/starFoundation.test.js +75 -0
  38. package/dist/utils/constellation/stars/consistency.d.ts +29 -0
  39. package/dist/utils/constellation/stars/consistency.js +142 -0
  40. package/dist/utils/constellation/stars/consistency.test.d.ts +1 -0
  41. package/dist/utils/constellation/stars/consistency.test.js +94 -0
  42. package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
  43. package/dist/utils/constellation/stars/lowerBody.js +30 -0
  44. package/dist/utils/constellation/stars/pull.d.ts +11 -0
  45. package/dist/utils/constellation/stars/pull.js +24 -0
  46. package/dist/utils/constellation/stars/push.d.ts +11 -0
  47. package/dist/utils/constellation/stars/push.js +24 -0
  48. package/dist/utils/constellation/stars/quality.d.ts +19 -0
  49. package/dist/utils/constellation/stars/quality.js +98 -0
  50. package/dist/utils/constellation/stars/quality.test.d.ts +1 -0
  51. package/dist/utils/constellation/stars/quality.test.js +113 -0
  52. package/dist/utils/constellation/stars/recovery.d.ts +29 -0
  53. package/dist/utils/constellation/stars/recovery.js +169 -0
  54. package/dist/utils/constellation/stars/recovery.test.d.ts +1 -0
  55. package/dist/utils/constellation/stars/recovery.test.js +131 -0
  56. package/dist/utils/constellation/strengthStar.test.d.ts +1 -0
  57. package/dist/utils/constellation/strengthStar.test.js +190 -0
  58. package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
  59. package/dist/utils/constellation/strengthStarHelpers.js +104 -0
  60. package/dist/utils/constellation/types.d.ts +124 -0
  61. package/dist/utils/constellation/types.js +18 -0
  62. package/dist/utils/exerciseRecord/recordValidator.integration.test.js +1 -1
  63. package/dist/utils/exerciseRecord/recordValidator.js +1 -1
  64. package/dist/utils/exerciseRecord/recordValidator.test.js +8 -8
  65. package/dist/utils/index.d.ts +5 -3
  66. package/dist/utils/index.js +1 -0
  67. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
  68. package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
  69. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
  70. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
  71. package/dist/utils/scoringWorkout/constants.d.ts +20 -6
  72. package/dist/utils/scoringWorkout/constants.js +23 -9
  73. package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
  74. package/dist/utils/scoringWorkout/helpers.js +24 -18
  75. package/dist/utils/scoringWorkout/index.d.ts +12 -8
  76. package/dist/utils/scoringWorkout/index.js +23 -15
  77. package/dist/utils/scoringWorkout/parseRecords.js +4 -3
  78. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +223 -185
  79. package/dist/utils/scoringWorkout/types.d.ts +34 -14
  80. package/package.json +31 -31
  81. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
  82. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
  83. package/dist/utils/scaleProPlan.util.d.ts +0 -9
  84. package/dist/utils/scaleProPlan.util.js +0 -139
  85. package/dist/utils/scoring/calculateCalories.d.ts +0 -67
  86. package/dist/utils/scoring/calculateCalories.js +0 -345
  87. package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
  88. package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
  89. package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
  90. package/dist/utils/scoring/calculateQualityScore.js +0 -334
  91. package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
  92. package/dist/utils/scoring/calculateTotalVolume.js +0 -73
  93. package/dist/utils/scoring/constants.d.ts +0 -211
  94. package/dist/utils/scoring/constants.js +0 -247
  95. package/dist/utils/scoring/helpers.d.ts +0 -119
  96. package/dist/utils/scoring/helpers.js +0 -229
  97. package/dist/utils/scoring/index.d.ts +0 -28
  98. package/dist/utils/scoring/index.js +0 -47
  99. package/dist/utils/scoring/parseRecords.d.ts +0 -98
  100. package/dist/utils/scoring/parseRecords.js +0 -284
  101. package/dist/utils/scoring/types.d.ts +0 -86
  102. package/dist/utils/scoring/types.js +0 -11
  103. package/dist/utils/scoring.utils.d.ts +0 -14
  104. package/dist/utils/scoring.utils.js +0 -243
  105. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
  106. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.js → calculateMuscleFatigue.js} +0 -0
@@ -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,94 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // shared/src/utils/constellation/stars/consistency.test.ts
4
+ const vitest_1 = require("vitest");
5
+ const starFoundation_1 = require("../starFoundation");
6
+ const consistency_1 = require("../stars/consistency");
7
+ const NOW = Date.UTC(2026, 5, 1);
8
+ const day = (d) => NOW - d * 86400000;
9
+ const set = (isDone = true) => ({
10
+ type: "weight-reps",
11
+ kg: "20",
12
+ reps: "5",
13
+ isDone,
14
+ isStrictMode: false,
15
+ });
16
+ const sess = (d, isDone = true) => ({
17
+ date: day(d),
18
+ exercises: [{ exerciseId: "x", records: [set(isDone)] }],
19
+ });
20
+ const ctx = (sessions, level = 1) => ({
21
+ sessions,
22
+ scoredSessions: [],
23
+ exerciseCatalog: {},
24
+ user: { weightKg: 80 },
25
+ now: NOW,
26
+ level,
27
+ });
28
+ (0, vitest_1.describe)("consistency — new vs lapsed user", () => {
29
+ (0, vitest_1.it)("no sessions → getStarted, brightness 0", () => {
30
+ const r = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx([]));
31
+ (0, vitest_1.expect)(r.brightness).toBe(0);
32
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.consistency.gap.getStarted");
33
+ });
34
+ (0, vitest_1.it)("one old session (20d ago), none since → broken", () => {
35
+ const r = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx([sess(20)]));
36
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.consistency.gap.broken");
37
+ });
38
+ });
39
+ (0, vitest_1.describe)("consistency — full habit", () => {
40
+ (0, vitest_1.it)("8 well-spaced sessions in 28d, recent → full", () => {
41
+ const days = [1, 4, 8, 11, 15, 18, 22, 25];
42
+ const r = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx(days.map((d) => sess(d))));
43
+ (0, vitest_1.expect)(r.brightness).toBe(100);
44
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.consistency.gap.full");
45
+ });
46
+ (0, vitest_1.it)("fewer than required → partial + moreSessions gap", () => {
47
+ const r = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx([sess(1), sess(4), sess(8), sess(11)]));
48
+ (0, vitest_1.expect)(r.brightness).toBeGreaterThan(0);
49
+ (0, vitest_1.expect)(r.brightness).toBeLessThan(100);
50
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.consistency.gap.moreSessions");
51
+ });
52
+ });
53
+ (0, vitest_1.describe)("consistency — counting rules", () => {
54
+ (0, vitest_1.it)("sessions with no completed set don't count", () => {
55
+ const r = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx([sess(1, false), sess(4, false)]));
56
+ (0, vitest_1.expect)(r.brightness).toBe(0);
57
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.consistency.gap.getStarted");
58
+ });
59
+ (0, vitest_1.it)("two sessions same calendar day count once", () => {
60
+ const twoSameDay = [
61
+ { date: day(2), exercises: [{ exerciseId: "x", records: [set()] }] },
62
+ {
63
+ date: day(2) + 3600000,
64
+ exercises: [{ exerciseId: "y", records: [set()] }],
65
+ },
66
+ ];
67
+ const r = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx(twoSameDay));
68
+ // 1 unique day toward 8 → 12.5% * gap (trailing 2d ok) → ~13
69
+ (0, vitest_1.expect)(r.currentState.value).toBe(1);
70
+ });
71
+ });
72
+ (0, vitest_1.describe)("consistency — trailing gap behaviour", () => {
73
+ (0, vitest_1.it)("cooling warning as trailing gap approaches max", () => {
74
+ // 8 sessions but last was 4 days ago (warningAtIdleDays=4, maxGap=5)
75
+ const days = [4, 7, 10, 13, 16, 19, 22, 25];
76
+ const r = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx(days.map((d) => sess(d))));
77
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.consistency.gap.cooling");
78
+ });
79
+ (0, vitest_1.it)("trailing gap beyond max decays brightness below count-only", () => {
80
+ // enough sessions for full count, but last 10 days ago → gap penalty
81
+ const days = [10, 12, 14, 16, 18, 20, 22, 24];
82
+ const r = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx(days.map((d) => sess(d))));
83
+ (0, vitest_1.expect)(r.brightness).toBeLessThan(100);
84
+ });
85
+ });
86
+ (0, vitest_1.describe)("consistency — level scaling", () => {
87
+ (0, vitest_1.it)("8 sessions fills L1 but not L2 (needs 12)", () => {
88
+ const days = [1, 4, 8, 11, 15, 18, 22, 25];
89
+ const l1 = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx(days.map((d) => sess(d)), 1));
90
+ const l2 = (0, starFoundation_1.resolveStar)(consistency_1.consistencyStar, ctx(days.map((d) => sess(d)), 2));
91
+ (0, vitest_1.expect)(l1.brightness).toBe(100);
92
+ (0, vitest_1.expect)(l2.brightness).toBeLessThan(100);
93
+ });
94
+ });
@@ -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;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ // utils/constellation/stars/lowerBody.ts — Lower Body Star
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Lower Body Star
6
+ * ============================================================================
7
+ *
8
+ * Lower-body strength across squat, lunge, and hinge patterns combined, any
9
+ * equipment. Squats, goblet squats, leg press, lunges, deadlifts, RDLs, hip
10
+ * thrusts, kettlebell swings — all count. Best load across all three lights it.
11
+ * (Knee-vs-hip balance nuance deferred to the future goal layer.)
12
+ *
13
+ * Threshold per level from STRENGTH_THRESHOLDS.lowerBody, resolved by sex.
14
+ * Leg press is NOT special-cased — it is simply a squat-pattern exercise.
15
+ * Bodyweight lower-body work loads via the exercise's weightMultiplier, so home
16
+ * users can reach the threshold honestly.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.lowerBodyStar = void 0;
20
+ const strengthStarHelpers_1 = require("../strengthStarHelpers");
21
+ const LOWER_BODY_PATTERNS = ["squat", "lunge", "hinge"];
22
+ exports.lowerBodyStar = {
23
+ id: "lowerBody",
24
+ displayName: { translationKey: "constellation.lowerBody.name" },
25
+ color: "#40b060", // earth green — the foundation everything sits on
26
+ figurePosition: "base",
27
+ objective: { translationKey: "constellation.lowerBody.objective" },
28
+ rationale: { translationKey: "constellation.lowerBody.rationale" },
29
+ evaluate: (ctx) => (0, strengthStarHelpers_1.evaluateStrengthStar)(ctx, "lowerBody", LOWER_BODY_PATTERNS, "constellation.lowerBody"),
30
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Pull Star
4
+ * ============================================================================
5
+ *
6
+ * Pulling strength across all pull-pattern exercises (horizontal + vertical),
7
+ * any equipment. Rows, cable rows, lat pulldowns, pull-ups, chin-ups — all count.
8
+ * Threshold per level from STRENGTH_THRESHOLDS.pull, resolved by sex.
9
+ */
10
+ import type { TStarDefinition } from "../types";
11
+ export declare const pullStar: TStarDefinition;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ // utils/constellation/stars/pull.ts — Pull Star
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Pull Star
6
+ * ============================================================================
7
+ *
8
+ * Pulling strength across all pull-pattern exercises (horizontal + vertical),
9
+ * any equipment. Rows, cable rows, lat pulldowns, pull-ups, chin-ups — all count.
10
+ * Threshold per level from STRENGTH_THRESHOLDS.pull, resolved by sex.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.pullStar = void 0;
14
+ const strengthStarHelpers_1 = require("../strengthStarHelpers");
15
+ const PULL_PATTERNS = ["pull_horizontal", "pull_vertical"];
16
+ exports.pullStar = {
17
+ id: "pull",
18
+ displayName: { translationKey: "constellation.pull.name" },
19
+ color: "#2060ff", // cool blue — drawing resistance in
20
+ figurePosition: "rightArm",
21
+ objective: { translationKey: "constellation.pull.objective" },
22
+ rationale: { translationKey: "constellation.pull.rationale" },
23
+ evaluate: (ctx) => (0, strengthStarHelpers_1.evaluateStrengthStar)(ctx, "pull", PULL_PATTERNS, "constellation.pull"),
24
+ };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Push Star
4
+ * ============================================================================
5
+ *
6
+ * Pushing strength across all push-pattern exercises (horizontal + vertical),
7
+ * any equipment. Bench, machine press, push-ups, overhead press — all count.
8
+ * Threshold per level from STRENGTH_THRESHOLDS.push, resolved by sex.
9
+ */
10
+ import type { TStarDefinition } from "../types";
11
+ export declare const pushStar: TStarDefinition;
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ // utils/constellation/stars/push.ts — Push Star
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Push Star
6
+ * ============================================================================
7
+ *
8
+ * Pushing strength across all push-pattern exercises (horizontal + vertical),
9
+ * any equipment. Bench, machine press, push-ups, overhead press — all count.
10
+ * Threshold per level from STRENGTH_THRESHOLDS.push, resolved by sex.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.pushStar = void 0;
14
+ const strengthStarHelpers_1 = require("../strengthStarHelpers");
15
+ const PUSH_PATTERNS = ["push_horizontal", "push_vertical"];
16
+ exports.pushStar = {
17
+ id: "push",
18
+ displayName: { translationKey: "constellation.push.name" },
19
+ color: "#ff4820", // warm red — forward force
20
+ figurePosition: "leftArm",
21
+ objective: { translationKey: "constellation.push.objective" },
22
+ rationale: { translationKey: "constellation.push.rationale" },
23
+ evaluate: (ctx) => (0, strengthStarHelpers_1.evaluateStrengthStar)(ctx, "push", PUSH_PATTERNS, "constellation.push"),
24
+ };
@@ -0,0 +1,19 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Quality Star (the core)
4
+ * ============================================================================
5
+ *
6
+ * Execution quality. Brightness reflects how many of the user's recent sessions
7
+ * were executed well. Reads PRE-SCORED sessions from context (the orchestrator
8
+ * scores once); this star never calls the scoring engine itself.
9
+ *
10
+ * Session quality = simple average of its exercises' quality scores (0-100).
11
+ * Star brightness = count-based: of the last N sessions, how many cleared the
12
+ * quality bar, scaled toward a target count. Count-based (not
13
+ * a consecutive streak) so a single off day never zeroes the
14
+ * star — inviting, not punishing.
15
+ *
16
+ * Thresholds come from QUALITY_THRESHOLDS[level].
17
+ */
18
+ import type { TStarDefinition } from "../types";
19
+ export declare const qualityStar: TStarDefinition;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ // utils/constellation/stars/quality.ts — Quality Star (the core)
3
+ /**
4
+ * ============================================================================
5
+ * FITFRIX CONSTELLATION — Quality Star (the core)
6
+ * ============================================================================
7
+ *
8
+ * Execution quality. Brightness reflects how many of the user's recent sessions
9
+ * were executed well. Reads PRE-SCORED sessions from context (the orchestrator
10
+ * scores once); this star never calls the scoring engine itself.
11
+ *
12
+ * Session quality = simple average of its exercises' quality scores (0-100).
13
+ * Star brightness = count-based: of the last N sessions, how many cleared the
14
+ * quality bar, scaled toward a target count. Count-based (not
15
+ * a consecutive streak) so a single off day never zeroes the
16
+ * star — inviting, not punishing.
17
+ *
18
+ * Thresholds come from QUALITY_THRESHOLDS[level].
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.qualityStar = void 0;
22
+ const levelThresholds_1 = require("../levelThresholds");
23
+ // --- Session quality --------------------------------------------------------
24
+ /**
25
+ * Average score across a scored session's scorable exercises. Returns null if
26
+ * the session has no scorable exercises (all missing/unscorable).
27
+ */
28
+ function sessionQuality(session) {
29
+ const scores = [];
30
+ for (const ex of session.exercises) {
31
+ if (typeof ex.score === "number")
32
+ scores.push(ex.score);
33
+ }
34
+ if (scores.length === 0)
35
+ return null;
36
+ return scores.reduce((s, v) => s + v, 0) / scores.length;
37
+ }
38
+ // --- Detail builders --------------------------------------------------------
39
+ function clearingMeasure(clearing, target) {
40
+ return {
41
+ value: clearing,
42
+ unit: "sessions",
43
+ display: {
44
+ translationKey: "constellation.quality.measure.clearing",
45
+ params: [Math.min(clearing, target), target],
46
+ },
47
+ };
48
+ }
49
+ function buildGap(clearing, considered, target, bar) {
50
+ if (considered === 0) {
51
+ return { translationKey: "constellation.quality.gap.noData" };
52
+ }
53
+ if (clearing >= target) {
54
+ return { translationKey: "constellation.quality.gap.full" };
55
+ }
56
+ return {
57
+ translationKey: "constellation.quality.gap.moreQuality",
58
+ params: [target - clearing, bar],
59
+ };
60
+ }
61
+ // --- Evaluator --------------------------------------------------------------
62
+ function evaluateQuality(ctx) {
63
+ const { bar, windowSessions, targetClearing } = levelThresholds_1.QUALITY_THRESHOLDS[ctx.level];
64
+ // Most recent N scored sessions (descending by date). Future sessions were
65
+ // already excluded by the orchestrator when scoring.
66
+ const recent = [...ctx.scoredSessions]
67
+ .sort((a, b) => b.date - a.date)
68
+ .slice(0, windowSessions);
69
+ let clearing = 0;
70
+ let considered = 0;
71
+ for (const session of recent) {
72
+ const q = sessionQuality(session);
73
+ if (q === null)
74
+ continue; // unscorable session counts neither way
75
+ considered++;
76
+ if (q >= bar)
77
+ clearing++;
78
+ }
79
+ const brightness = Math.min(100, (clearing / targetClearing) * 100);
80
+ return {
81
+ brightness,
82
+ detail: {
83
+ currentState: clearingMeasure(clearing, targetClearing),
84
+ target: clearingMeasure(targetClearing, targetClearing),
85
+ gap: buildGap(clearing, considered, targetClearing, bar),
86
+ },
87
+ };
88
+ }
89
+ // --- Definition -------------------------------------------------------------
90
+ exports.qualityStar = {
91
+ id: "quality",
92
+ displayName: { translationKey: "constellation.quality.name" },
93
+ color: "#00c4b4", // teal — precision, cool focus
94
+ figurePosition: "core",
95
+ objective: { translationKey: "constellation.quality.objective" },
96
+ rationale: { translationKey: "constellation.quality.rationale" },
97
+ evaluate: evaluateQuality,
98
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // shared/src/utils/constellation/stars/quality.test.ts
4
+ const vitest_1 = require("vitest");
5
+ const starFoundation_1 = require("../starFoundation");
6
+ const quality_1 = require("../stars/quality");
7
+ const NOW = Date.UTC(2026, 5, 1);
8
+ const day = (d) => NOW - d * 86400000;
9
+ /** A scored session whose single exercise has the given score. */
10
+ const scored = (d, score) => ({
11
+ date: day(d),
12
+ exercises: [{ exerciseId: "x", score, muscleScores: {} }],
13
+ });
14
+ const ctx = (scoredSessions, level = 1) => ({
15
+ sessions: [],
16
+ scoredSessions,
17
+ exerciseCatalog: {},
18
+ user: {},
19
+ now: NOW,
20
+ level,
21
+ });
22
+ (0, vitest_1.describe)("quality — count-based brightness (L1 bar 70, target 5)", () => {
23
+ (0, vitest_1.it)("5 of 5 sessions clear → full", () => {
24
+ const s = [
25
+ scored(1, 80),
26
+ scored(3, 75),
27
+ scored(5, 90),
28
+ scored(7, 72),
29
+ scored(9, 85),
30
+ ];
31
+ const r = (0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx(s));
32
+ (0, vitest_1.expect)(r.brightness).toBe(100);
33
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.quality.gap.full");
34
+ });
35
+ (0, vitest_1.it)("3 of 5 clear → 60%", () => {
36
+ const s = [
37
+ scored(1, 80),
38
+ scored(3, 60),
39
+ scored(5, 90),
40
+ scored(7, 50),
41
+ scored(9, 75),
42
+ ];
43
+ const r = (0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx(s));
44
+ (0, vitest_1.expect)(r.brightness).toBeCloseTo(60, 0);
45
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.quality.gap.moreQuality");
46
+ });
47
+ (0, vitest_1.it)("none clear → 0", () => {
48
+ const s = [scored(1, 50), scored(3, 40)];
49
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx(s)).brightness).toBe(0);
50
+ });
51
+ });
52
+ (0, vitest_1.describe)("quality — the bar boundary (>= 70 clears)", () => {
53
+ (0, vitest_1.it)("exactly 70 clears", () => {
54
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx([scored(1, 70)])).currentState.value).toBe(1);
55
+ });
56
+ (0, vitest_1.it)("69 does not clear", () => {
57
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx([scored(1, 69)])).currentState.value).toBe(0);
58
+ });
59
+ });
60
+ (0, vitest_1.describe)("quality — session averaging & unscorable handling", () => {
61
+ (0, vitest_1.it)("session score is the average of its exercises", () => {
62
+ const s = [
63
+ {
64
+ date: day(1),
65
+ exercises: [
66
+ { exerciseId: "a", score: 60, muscleScores: {} },
67
+ { exerciseId: "b", score: 80, muscleScores: {} }, // avg 70 → clears
68
+ ],
69
+ },
70
+ ];
71
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx(s)).currentState.value).toBe(1);
72
+ });
73
+ (0, vitest_1.it)("sessions with no scorable exercises are skipped (neither clear nor fail)", () => {
74
+ const s = [scored(1, null), scored(3, 80)];
75
+ const r = (0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx(s));
76
+ (0, vitest_1.expect)(r.currentState.value).toBe(1); // only the scorable one counts
77
+ });
78
+ (0, vitest_1.it)("no data at all → noData gap", () => {
79
+ const r = (0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx([scored(1, null)]));
80
+ (0, vitest_1.expect)(r.gap.translationKey).toBe("constellation.quality.gap.noData");
81
+ });
82
+ });
83
+ (0, vitest_1.describe)("quality — window & level scaling", () => {
84
+ (0, vitest_1.it)("only the most recent N sessions are considered", () => {
85
+ // L1 window 5: 5 recent good + 3 older bad → still full (bad ones out of window)
86
+ const s = [
87
+ scored(1, 90),
88
+ scored(3, 90),
89
+ scored(5, 90),
90
+ scored(7, 90),
91
+ scored(9, 90),
92
+ scored(20, 10),
93
+ scored(22, 10),
94
+ scored(24, 10),
95
+ ];
96
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx(s)).brightness).toBe(100);
97
+ });
98
+ (0, vitest_1.it)("L2 demands a higher bar (78) so 75s stop clearing", () => {
99
+ const s = [
100
+ scored(1, 75),
101
+ scored(3, 75),
102
+ scored(5, 75),
103
+ scored(7, 75),
104
+ scored(9, 75),
105
+ scored(11, 75),
106
+ scored(13, 75),
107
+ scored(15, 75),
108
+ ];
109
+ const l1 = (0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx(s, 1));
110
+ const l2 = (0, starFoundation_1.resolveStar)(quality_1.qualityStar, ctx(s, 2));
111
+ (0, vitest_1.expect)(l1.brightness).toBeGreaterThan(l2.brightness);
112
+ });
113
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * ============================================================================
3
+ * FITFRIX CONSTELLATION — Recovery Star (the crown)
4
+ * ============================================================================
5
+ *
6
+ * Recovery intelligence: does the user let trained muscles fully recover, not
7
+ * just grind through fatigue? Brightness reflects how many RECOVERY CYCLES the
8
+ * user achieved in the trailing window.
9
+ *
10
+ * ZONE-BASED (not all-or-nothing). The body is split into three independent
11
+ * recovery zones — upper, lower, core. A split-routine lifter (e.g. Push/Pull/
12
+ * Legs) almost never has the WHOLE body fresh at once, yet is recovering
13
+ * intelligently zone by zone; an all-muscles-at-once rule would wrongly score
14
+ * them near zero. So we count cycles PER ZONE.
15
+ *
16
+ * A zone "cycle" = a transition where that zone goes fatigued -> recovered
17
+ * (a multi-day rest counts once). Crucially, a zone only earns cycles if the
18
+ * user actually TRAINS it in the window — an untrained zone is "always fresh"
19
+ * and must not hand out free recovery credit. Total cycles = sum across the
20
+ * zones the user trains, capped at the level target.
21
+ *
22
+ * Reads PRE-SCORED sessions from context (orchestrator scores once); builds the
23
+ * enriched history the fatigue model consumes, then replays the model across
24
+ * each day of the window. This star never calls the scoring engine itself.
25
+ *
26
+ * Thresholds come from RECOVERY_THRESHOLDS[level].
27
+ */
28
+ import type { TStarDefinition } from "../types";
29
+ export declare const recoveryStar: TStarDefinition;