@dgpholdings/greatoak-shared 1.2.87 → 1.2.89

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 (37) hide show
  1. package/dist/__mocks__/catalog.fixture.d.ts +2 -0
  2. package/dist/__mocks__/catalog.fixture.js +208 -0
  3. package/dist/__mocks__/exercises.mock.d.ts +4 -11
  4. package/dist/__mocks__/exercises.mock.js +82 -42
  5. package/dist/__mocks__/sessions.mock.d.ts +28 -0
  6. package/dist/__mocks__/sessions.mock.js +394 -0
  7. package/dist/__mocks__/testIds.d.ts +9 -0
  8. package/dist/__mocks__/testIds.js +13 -0
  9. package/dist/__mocks__/user.mock.js +3 -1
  10. package/dist/constants/goalJourney.d.ts +108 -0
  11. package/dist/constants/goalJourney.js +443 -0
  12. package/dist/constants/index.d.ts +1 -0
  13. package/dist/constants/index.js +1 -0
  14. package/dist/types/TApiUser.d.ts +4 -2
  15. package/dist/utils/adoptionEngine/scaleProPlan.util.js +9 -4
  16. package/dist/utils/constellation/computeNormalisedLoad.test.d.ts +1 -0
  17. package/dist/utils/constellation/computeNormalisedLoad.test.js +218 -0
  18. package/dist/utils/constellation/evaluateConstellation.js +1 -1
  19. package/dist/utils/constellation/evaluateConstellation.test.d.ts +1 -0
  20. package/dist/utils/constellation/evaluateConstellation.test.js +93 -0
  21. package/dist/utils/constellation/index.d.ts +1 -0
  22. package/dist/utils/constellation/index.js +4 -1
  23. package/dist/utils/constellation/starFoundation.test.d.ts +1 -0
  24. package/dist/utils/constellation/starFoundation.test.js +75 -0
  25. package/dist/utils/constellation/stars/consistency.test.d.ts +1 -0
  26. package/dist/utils/constellation/stars/consistency.test.js +94 -0
  27. package/dist/utils/constellation/stars/quality.test.d.ts +1 -0
  28. package/dist/utils/constellation/stars/quality.test.js +113 -0
  29. package/dist/utils/constellation/stars/recovery.test.d.ts +1 -0
  30. package/dist/utils/constellation/stars/recovery.test.js +131 -0
  31. package/dist/utils/constellation/strengthStar.test.d.ts +1 -0
  32. package/dist/utils/constellation/strengthStar.test.js +190 -0
  33. package/dist/utils/exerciseRecord/recordValidator.integration.test.js +1 -1
  34. package/dist/utils/exerciseRecord/recordValidator.js +1 -1
  35. package/dist/utils/exerciseRecord/recordValidator.test.js +8 -8
  36. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +19 -19
  37. package/package.json +1 -1
@@ -0,0 +1,218 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // shared/src/utils/constellation/computeNormalisedLoad.test.ts
4
+ const computeNormalisedLoad_1 = require("./computeNormalisedLoad");
5
+ const vitest_1 = require("vitest");
6
+ const NOW = Date.UTC(2026, 5, 1);
7
+ const day = (d) => NOW - d * 86400000;
8
+ const wr = (kg, reps, isDone = true) => ({ type: "weight-reps", kg, reps, isDone, isStrictMode: false });
9
+ const ro = (reps, auxWeightKg = "0", isDone = true) => ({
10
+ type: "reps-only",
11
+ reps,
12
+ auxWeightKg,
13
+ isDone,
14
+ isStrictMode: false,
15
+ });
16
+ const ex = (over) => ({
17
+ exerciseId: "e",
18
+ name: "E",
19
+ primaryMuscles: [],
20
+ secondaryMuscles: [],
21
+ difficultyLevel: 3,
22
+ movementPattern: "push_horizontal",
23
+ status: "active",
24
+ ...over,
25
+ });
26
+ (0, vitest_1.describe)("estimateOneRepMax — Epley with rep cap", () => {
27
+ (0, vitest_1.it)("60kg x5 ≈ 70kg", () => (0, vitest_1.expect)((0, computeNormalisedLoad_1.estimateOneRepMax)(60, 5)).toBeCloseTo(70, 1));
28
+ (0, vitest_1.it)("70kg x1 ≈ 72.33kg", () => (0, vitest_1.expect)((0, computeNormalisedLoad_1.estimateOneRepMax)(70, 1)).toBeCloseTo(72.33, 1));
29
+ (0, vitest_1.it)("reps capped at 12: 50x30 == 50x12", () => (0, vitest_1.expect)((0, computeNormalisedLoad_1.estimateOneRepMax)(50, 30)).toBe((0, computeNormalisedLoad_1.estimateOneRepMax)(50, computeNormalisedLoad_1.EPLEY_REP_CAP)));
30
+ (0, vitest_1.it)("zero or negative weight → 0", () => {
31
+ (0, vitest_1.expect)((0, computeNormalisedLoad_1.estimateOneRepMax)(0, 5)).toBe(0);
32
+ (0, vitest_1.expect)((0, computeNormalisedLoad_1.estimateOneRepMax)(-10, 5)).toBe(0);
33
+ });
34
+ (0, vitest_1.it)("zero reps → 0", () => (0, vitest_1.expect)((0, computeNormalisedLoad_1.estimateOneRepMax)(60, 0)).toBe(0));
35
+ });
36
+ (0, vitest_1.describe)("computeNormalisedLoad — weighted sets", () => {
37
+ const catalog = { bench: ex({ exerciseId: "bench" }) };
38
+ const ids = new Set(["bench"]);
39
+ const sessions = [
40
+ {
41
+ date: day(2),
42
+ exercises: [{ exerciseId: "bench", records: [wr("60", "5")] }],
43
+ },
44
+ ];
45
+ (0, vitest_1.it)("best 1RM as bodyweight ratio (60x5 @80kg → ~0.875)", () => {
46
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(ids, sessions, catalog, 80, "male", day(56), NOW);
47
+ (0, vitest_1.expect)(r.ratio).toBeCloseTo(0.875, 2);
48
+ (0, vitest_1.expect)(r.bestExerciseId).toBe("bench");
49
+ });
50
+ (0, vitest_1.it)("picks the best set across sessions", () => {
51
+ const multi = [
52
+ {
53
+ date: day(10),
54
+ exercises: [{ exerciseId: "bench", records: [wr("60", "5")] }],
55
+ },
56
+ {
57
+ date: day(2),
58
+ exercises: [{ exerciseId: "bench", records: [wr("80", "5")] }],
59
+ },
60
+ ];
61
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(ids, multi, catalog, 80, "male", day(56), NOW);
62
+ (0, vitest_1.expect)(r.bestOneRepMaxKg).toBeCloseTo(93.33, 1);
63
+ });
64
+ (0, vitest_1.it)("ignores sessions outside the window", () => {
65
+ const old = [
66
+ {
67
+ date: day(70),
68
+ exercises: [{ exerciseId: "bench", records: [wr("100", "5")] }],
69
+ },
70
+ ];
71
+ (0, vitest_1.expect)((0, computeNormalisedLoad_1.computeNormalisedLoad)(ids, old, catalog, 80, "male", day(56), NOW).ratio).toBe(0);
72
+ });
73
+ (0, vitest_1.it)("ignores non-matching exercise ids", () => {
74
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(new Set(["other"]), sessions, catalog, 80, "male", day(56), NOW);
75
+ (0, vitest_1.expect)(r.ratio).toBe(0);
76
+ });
77
+ (0, vitest_1.it)("ignores incomplete sets (isDone false)", () => {
78
+ const undone = [
79
+ {
80
+ date: day(2),
81
+ exercises: [{ exerciseId: "bench", records: [wr("100", "5", false)] }],
82
+ },
83
+ ];
84
+ (0, vitest_1.expect)((0, computeNormalisedLoad_1.computeNormalisedLoad)(ids, undone, catalog, 80, "male", day(56), NOW)
85
+ .ratio).toBe(0);
86
+ });
87
+ });
88
+ (0, vitest_1.describe)("computeNormalisedLoad — unilateral doubling", () => {
89
+ (0, vitest_1.it)("unilateral weight is doubled for total load", () => {
90
+ const catalog = { uni: ex({ exerciseId: "uni", isUnilateral: true }) };
91
+ const sessions = [
92
+ {
93
+ date: day(2),
94
+ exercises: [{ exerciseId: "uni", records: [wr("30", "5")] }],
95
+ },
96
+ ];
97
+ // 30*2=60 → 1RM 70 → /80 = 0.875
98
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(new Set(["uni"]), sessions, catalog, 80, "male", day(56), NOW);
99
+ (0, vitest_1.expect)(r.ratio).toBeCloseTo(0.875, 2);
100
+ });
101
+ });
102
+ (0, vitest_1.describe)("computeNormalisedLoad — bodyweight loading", () => {
103
+ (0, vitest_1.it)("uses weightMultiplier (male) for reps-only", () => {
104
+ const catalog = {
105
+ pull: ex({
106
+ exerciseId: "pull",
107
+ weightMultiplier: { male: 1.0, female: 0.95, default: 0.97 },
108
+ }),
109
+ };
110
+ const sessions = [
111
+ { date: day(2), exercises: [{ exerciseId: "pull", records: [ro("8")] }] },
112
+ ];
113
+ // load = 1.0*80 = 80; 1RM = 80*(1+8/30)=101.3; /80 = 1.27
114
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(new Set(["pull"]), sessions, catalog, 80, "male", day(56), NOW);
115
+ (0, vitest_1.expect)(r.ratio).toBeCloseTo(1.27, 1);
116
+ });
117
+ (0, vitest_1.it)("falls back to bodyweightDependency when no weightMultiplier", () => {
118
+ const catalog = {
119
+ dip: ex({ exerciseId: "dip", bodyweightDependency: "high" }),
120
+ };
121
+ const sessions = [
122
+ { date: day(2), exercises: [{ exerciseId: "dip", records: [ro("8")] }] },
123
+ ];
124
+ // high → 0.9*80=72; 1RM=72*(1+8/30)=91.2; /80=1.14
125
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(new Set(["dip"]), sessions, catalog, 80, "male", day(56), NOW);
126
+ (0, vitest_1.expect)(r.ratio).toBeCloseTo(1.14, 1);
127
+ });
128
+ (0, vitest_1.it)("added aux weight stacks on bodyweight load", () => {
129
+ const catalog = {
130
+ pull: ex({
131
+ exerciseId: "pull",
132
+ weightMultiplier: { male: 1.0, female: 0.95, default: 0.97 },
133
+ }),
134
+ };
135
+ const sessions = [
136
+ {
137
+ date: day(2),
138
+ exercises: [{ exerciseId: "pull", records: [ro("5", "20")] }],
139
+ },
140
+ ];
141
+ // load = 80 + 20 = 100; 1RM=100*(1+5/30)=116.7; /80=1.46
142
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(new Set(["pull"]), sessions, catalog, 80, "male", day(56), NOW);
143
+ (0, vitest_1.expect)(r.ratio).toBeCloseTo(1.46, 1);
144
+ });
145
+ (0, vitest_1.it)("female weightMultiplier column is used for female users", () => {
146
+ const catalog = {
147
+ pull: ex({
148
+ exerciseId: "pull",
149
+ weightMultiplier: { male: 1.0, female: 0.5, default: 0.97 },
150
+ }),
151
+ };
152
+ const sessions = [
153
+ { date: day(2), exercises: [{ exerciseId: "pull", records: [ro("8")] }] },
154
+ ];
155
+ const male = (0, computeNormalisedLoad_1.computeNormalisedLoad)(new Set(["pull"]), sessions, catalog, 80, "male", day(56), NOW);
156
+ const female = (0, computeNormalisedLoad_1.computeNormalisedLoad)(new Set(["pull"]), sessions, catalog, 80, "female", day(56), NOW);
157
+ (0, vitest_1.expect)(female.ratio).toBeLessThan(male.ratio);
158
+ });
159
+ });
160
+ (0, vitest_1.describe)("computeNormalisedLoad — safeParseFloat guards", () => {
161
+ const catalog = { bench: ex({ exerciseId: "bench" }) };
162
+ const ids = new Set(["bench"]);
163
+ (0, vitest_1.it)("malformed kg → 0 load, finite ratio", () => {
164
+ const s = [
165
+ {
166
+ date: day(2),
167
+ exercises: [{ exerciseId: "bench", records: [wr("abc", "5")] }],
168
+ },
169
+ ];
170
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(ids, s, catalog, 80, "male", day(56), NOW);
171
+ (0, vitest_1.expect)(Number.isFinite(r.ratio)).toBe(true);
172
+ (0, vitest_1.expect)(r.ratio).toBe(0);
173
+ });
174
+ (0, vitest_1.it)("negative reps → 0 load", () => {
175
+ const s = [
176
+ {
177
+ date: day(2),
178
+ exercises: [{ exerciseId: "bench", records: [wr("60", "-5")] }],
179
+ },
180
+ ];
181
+ (0, vitest_1.expect)((0, computeNormalisedLoad_1.computeNormalisedLoad)(ids, s, catalog, 80, "male", day(56), NOW).ratio).toBe(0);
182
+ });
183
+ (0, vitest_1.it)("zero bodyweight falls back to default (no divide-by-zero)", () => {
184
+ const s = [
185
+ {
186
+ date: day(2),
187
+ exercises: [{ exerciseId: "bench", records: [wr("60", "5")] }],
188
+ },
189
+ ];
190
+ const r = (0, computeNormalisedLoad_1.computeNormalisedLoad)(ids, s, catalog, 0, "male", day(56), NOW);
191
+ (0, vitest_1.expect)(Number.isFinite(r.ratio)).toBe(true);
192
+ });
193
+ });
194
+ (0, vitest_1.describe)("computeNormalisedLoad — non-strength record types contribute 0", () => {
195
+ (0, vitest_1.it)("duration set → 0", () => {
196
+ const catalog = { plank: ex({ exerciseId: "plank" }) };
197
+ const s = [
198
+ {
199
+ date: day(2),
200
+ exercises: [
201
+ {
202
+ exerciseId: "plank",
203
+ records: [
204
+ {
205
+ type: "duration",
206
+ durationMmSs: "01:00",
207
+ auxWeightKg: "0",
208
+ isDone: true,
209
+ isStrictMode: false,
210
+ },
211
+ ],
212
+ },
213
+ ],
214
+ },
215
+ ];
216
+ (0, vitest_1.expect)((0, computeNormalisedLoad_1.computeNormalisedLoad)(new Set(["plank"]), s, catalog, 80, "male", day(56), NOW).ratio).toBe(0);
217
+ });
218
+ });
@@ -78,7 +78,7 @@ function scoreAllSessions(input) {
78
78
  muscleScores: (_a = result.muscleScores) !== null && _a !== void 0 ? _a : {},
79
79
  });
80
80
  }
81
- catch (_b) {
81
+ catch {
82
82
  exercises.push({
83
83
  exerciseId: ex.exerciseId,
84
84
  score: null,
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // shared/src/utils/constellation/evaluateConstellation.test.ts
4
+ const evaluateConstellation_1 = require("./evaluateConstellation");
5
+ const vitest_1 = require("vitest");
6
+ const NOW = Date.UTC(2026, 5, 1);
7
+ const day = (d) => NOW - d * 86400000;
8
+ const wr = (kg, reps) => ({ type: "weight-reps", kg, reps, isDone: true, isStrictMode: false });
9
+ const catalog = {
10
+ bench: {
11
+ exerciseId: "bench",
12
+ name: "Bench",
13
+ primaryMuscles: ["pectoralis-major"],
14
+ secondaryMuscles: [],
15
+ difficultyLevel: 3,
16
+ movementPattern: "push_horizontal",
17
+ status: "active",
18
+ },
19
+ };
20
+ const baseInput = (over = {}) => ({
21
+ sessions: [
22
+ {
23
+ date: day(2),
24
+ exercises: [{ exerciseId: "bench", records: [wr("60", "5")] }],
25
+ },
26
+ ],
27
+ exerciseCatalog: catalog,
28
+ user: { weightKg: 80, gender: "male" },
29
+ now: NOW,
30
+ ...over,
31
+ });
32
+ (0, vitest_1.describe)("orchestrator — now guard", () => {
33
+ (0, vitest_1.it)("throws on NaN now", () => {
34
+ (0, vitest_1.expect)(() => (0, evaluateConstellation_1.evaluateConstellation)(baseInput({ now: NaN }))).toThrow();
35
+ });
36
+ (0, vitest_1.it)("throws on Infinity now", () => {
37
+ (0, vitest_1.expect)(() => (0, evaluateConstellation_1.evaluateConstellation)(baseInput({ now: Infinity }))).toThrow();
38
+ });
39
+ (0, vitest_1.it)("accepts a finite now", () => {
40
+ (0, vitest_1.expect)((0, evaluateConstellation_1.evaluateConstellation)(baseInput()).stars).toHaveLength(6);
41
+ });
42
+ });
43
+ (0, vitest_1.describe)("orchestrator — shape & aggregate", () => {
44
+ (0, vitest_1.it)("returns six stars and a figure", () => {
45
+ const s = (0, evaluateConstellation_1.evaluateConstellation)(baseInput());
46
+ (0, vitest_1.expect)(s.stars).toHaveLength(6);
47
+ (0, vitest_1.expect)(s.figure.auraIntensity).toBeGreaterThanOrEqual(0);
48
+ (0, vitest_1.expect)(s.figure.auraIntensity).toBeLessThanOrEqual(100);
49
+ });
50
+ (0, vitest_1.it)("auraIntensity equals the mean star brightness", () => {
51
+ const s = (0, evaluateConstellation_1.evaluateConstellation)(baseInput());
52
+ const mean = s.stars.reduce((a, x) => a + x.brightness, 0) / s.stars.length;
53
+ (0, vitest_1.expect)(s.figure.auraIntensity).toBeCloseTo(Math.round(mean * 10) / 10, 1);
54
+ });
55
+ (0, vitest_1.it)("fullStarCount matches tier-3 stars", () => {
56
+ const s = (0, evaluateConstellation_1.evaluateConstellation)(baseInput());
57
+ const full = s.stars.filter((x) => x.tier === 3).length;
58
+ (0, vitest_1.expect)(s.figure.fullStarCount).toBe(full);
59
+ });
60
+ (0, vitest_1.it)("evaluatedAt is the ISO of now", () => {
61
+ const s = (0, evaluateConstellation_1.evaluateConstellation)(baseInput());
62
+ (0, vitest_1.expect)(s.evaluatedAt).toBe(new Date(NOW).toISOString());
63
+ });
64
+ });
65
+ (0, vitest_1.describe)("orchestrator — level clamping", () => {
66
+ (0, vitest_1.it)("undefined level → 1", () => (0, vitest_1.expect)((0, evaluateConstellation_1.evaluateConstellation)(baseInput()).currentLevel).toBe(1));
67
+ (0, vitest_1.it)("out-of-range level → clamped to 3", () => (0, vitest_1.expect)((0, evaluateConstellation_1.evaluateConstellation)(baseInput({ currentLevel: 99 })).currentLevel).toBe(3));
68
+ (0, vitest_1.it)("level 2 echoed", () => (0, vitest_1.expect)((0, evaluateConstellation_1.evaluateConstellation)(baseInput({ currentLevel: 2 })).currentLevel).toBe(2));
69
+ });
70
+ (0, vitest_1.describe)("orchestrator — future sessions excluded from scoring", () => {
71
+ (0, vitest_1.it)("a session dated after now does not light push", () => {
72
+ const future = (0, evaluateConstellation_1.evaluateConstellation)(baseInput({
73
+ sessions: [
74
+ {
75
+ date: NOW + 86400000,
76
+ exercises: [{ exerciseId: "bench", records: [wr("100", "5")] }],
77
+ },
78
+ ],
79
+ }));
80
+ (0, vitest_1.expect)(future.stars.find((s) => s.id === "push").brightness).toBe(0);
81
+ });
82
+ (0, vitest_1.it)("a present session does light push (control)", () => {
83
+ (0, vitest_1.expect)((0, evaluateConstellation_1.evaluateConstellation)(baseInput()).stars.find((s) => s.id === "push")
84
+ .brightness).toBeGreaterThan(0);
85
+ });
86
+ });
87
+ (0, vitest_1.describe)("orchestrator — empty input is safe", () => {
88
+ (0, vitest_1.it)("no sessions → all stars resolve, none throw", () => {
89
+ const s = (0, evaluateConstellation_1.evaluateConstellation)(baseInput({ sessions: [] }));
90
+ (0, vitest_1.expect)(s.stars).toHaveLength(6);
91
+ (0, vitest_1.expect)(s.stars.every((x) => Number.isFinite(x.brightness))).toBe(true);
92
+ });
93
+ });
@@ -15,3 +15,4 @@
15
15
  export { evaluateConstellation } from "./evaluateConstellation";
16
16
  export type { TConstellationInput, TConstellationState, TConstellationFigure, TResolvedStar, TStarId, TStarTier, TFigurePosition, TStarMeasure, TLocalizedText, TSessionRecord, } from "./types";
17
17
  export { TIER_THRESHOLDS, deriveTier } from "./starFoundation";
18
+ export { MAX_LEVEL as CONSTELLATION_USER_MAX_LEVEL, clampLevel as constellationClampLevel, type TLevel as TConstellationUserLevel, } from "./levelThresholds";
@@ -15,7 +15,7 @@
15
15
  * implementation detail and intentionally not re-exported.
16
16
  */
17
17
  Object.defineProperty(exports, "__esModule", { value: true });
18
- exports.deriveTier = exports.TIER_THRESHOLDS = exports.evaluateConstellation = void 0;
18
+ exports.constellationClampLevel = exports.CONSTELLATION_USER_MAX_LEVEL = exports.deriveTier = exports.TIER_THRESHOLDS = exports.evaluateConstellation = void 0;
19
19
  // The single entry point.
20
20
  var evaluateConstellation_1 = require("./evaluateConstellation");
21
21
  Object.defineProperty(exports, "evaluateConstellation", { enumerable: true, get: function () { return evaluateConstellation_1.evaluateConstellation; } });
@@ -24,3 +24,6 @@ Object.defineProperty(exports, "evaluateConstellation", { enumerable: true, get:
24
24
  var starFoundation_1 = require("./starFoundation");
25
25
  Object.defineProperty(exports, "TIER_THRESHOLDS", { enumerable: true, get: function () { return starFoundation_1.TIER_THRESHOLDS; } });
26
26
  Object.defineProperty(exports, "deriveTier", { enumerable: true, get: function () { return starFoundation_1.deriveTier; } });
27
+ var levelThresholds_1 = require("./levelThresholds");
28
+ Object.defineProperty(exports, "CONSTELLATION_USER_MAX_LEVEL", { enumerable: true, get: function () { return levelThresholds_1.MAX_LEVEL; } });
29
+ Object.defineProperty(exports, "constellationClampLevel", { enumerable: true, get: function () { return levelThresholds_1.clampLevel; } });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // shared/src/utils/constellation/starFoundation.test.ts
4
+ const starFoundation_1 = require("./starFoundation");
5
+ const levelThresholds_1 = require("./levelThresholds");
6
+ const vitest_1 = require("vitest");
7
+ (0, vitest_1.describe)("deriveTier — brightness band boundaries", () => {
8
+ (0, vitest_1.it)("0 brightness → tier 0 (dormant)", () => (0, vitest_1.expect)((0, starFoundation_1.deriveTier)(0)).toBe(0));
9
+ (0, vitest_1.it)("just below faint (0.9) → tier 0", () => (0, vitest_1.expect)((0, starFoundation_1.deriveTier)(0.9)).toBe(0));
10
+ (0, vitest_1.it)("exactly faint (1) → tier 1", () => (0, vitest_1.expect)((0, starFoundation_1.deriveTier)(1)).toBe(1));
11
+ (0, vitest_1.it)("mid faint (33) → tier 1", () => (0, vitest_1.expect)((0, starFoundation_1.deriveTier)(33)).toBe(1));
12
+ (0, vitest_1.it)("exactly rising (34) → tier 2", () => (0, vitest_1.expect)((0, starFoundation_1.deriveTier)(34)).toBe(2));
13
+ (0, vitest_1.it)("just below full (99) → tier 2", () => (0, vitest_1.expect)((0, starFoundation_1.deriveTier)(99)).toBe(2));
14
+ (0, vitest_1.it)("exactly full (100) → tier 3", () => (0, vitest_1.expect)((0, starFoundation_1.deriveTier)(100)).toBe(3));
15
+ (0, vitest_1.it)("thresholds are the documented values", () => {
16
+ (0, vitest_1.expect)(starFoundation_1.TIER_THRESHOLDS.faint).toBe(1);
17
+ (0, vitest_1.expect)(starFoundation_1.TIER_THRESHOLDS.rising).toBe(34);
18
+ (0, vitest_1.expect)(starFoundation_1.TIER_THRESHOLDS.full).toBe(100);
19
+ });
20
+ });
21
+ (0, vitest_1.describe)("resolveStar — clamps brightness and packages output", () => {
22
+ const ctx = {};
23
+ const make = (brightness) => ({
24
+ id: "push",
25
+ displayName: { translationKey: "constellation.push.name" },
26
+ color: "#fff",
27
+ figurePosition: "leftArm",
28
+ objective: { translationKey: "constellation.push.objective" },
29
+ rationale: { translationKey: "constellation.push.rationale" },
30
+ evaluate: () => ({
31
+ brightness,
32
+ detail: {
33
+ currentState: {
34
+ value: 0,
35
+ unit: "bw_ratio",
36
+ display: { translationKey: "x" },
37
+ },
38
+ target: {
39
+ value: 1,
40
+ unit: "bw_ratio",
41
+ display: { translationKey: "x" },
42
+ },
43
+ gap: { translationKey: "g" },
44
+ },
45
+ }),
46
+ });
47
+ (0, vitest_1.it)("clamps brightness > 100 down to 100", () => {
48
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(make(150), ctx).brightness).toBe(100);
49
+ });
50
+ (0, vitest_1.it)("clamps negative brightness up to 0", () => {
51
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(make(-20), ctx).brightness).toBe(0);
52
+ });
53
+ (0, vitest_1.it)("derives tier from the clamped value", () => {
54
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(make(150), ctx).tier).toBe(3);
55
+ (0, vitest_1.expect)((0, starFoundation_1.resolveStar)(make(-20), ctx).tier).toBe(0);
56
+ });
57
+ (0, vitest_1.it)("passes through identity + i18n fields unchanged", () => {
58
+ const r = (0, starFoundation_1.resolveStar)(make(50), ctx);
59
+ (0, vitest_1.expect)(r.id).toBe("push");
60
+ (0, vitest_1.expect)(r.figurePosition).toBe("leftArm");
61
+ (0, vitest_1.expect)(r.displayName.translationKey).toBe("constellation.push.name");
62
+ });
63
+ });
64
+ (0, vitest_1.describe)("clampLevel — valid level range", () => {
65
+ (0, vitest_1.it)("undefined → 1", () => (0, vitest_1.expect)((0, levelThresholds_1.clampLevel)(undefined)).toBe(1));
66
+ (0, vitest_1.it)("0 → 1", () => (0, vitest_1.expect)((0, levelThresholds_1.clampLevel)(0)).toBe(1));
67
+ (0, vitest_1.it)("negative → 1", () => (0, vitest_1.expect)((0, levelThresholds_1.clampLevel)(-3)).toBe(1));
68
+ (0, vitest_1.it)("1/2/3 pass through", () => {
69
+ (0, vitest_1.expect)((0, levelThresholds_1.clampLevel)(1)).toBe(1);
70
+ (0, vitest_1.expect)((0, levelThresholds_1.clampLevel)(2)).toBe(2);
71
+ (0, vitest_1.expect)((0, levelThresholds_1.clampLevel)(3)).toBe(3);
72
+ });
73
+ (0, vitest_1.it)("above max → 3", () => (0, vitest_1.expect)((0, levelThresholds_1.clampLevel)(99)).toBe(3));
74
+ (0, vitest_1.it)("fractional floored", () => (0, vitest_1.expect)((0, levelThresholds_1.clampLevel)(2.7)).toBe(2));
75
+ });
@@ -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 @@
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 @@
1
+ export {};