@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.
- package/README.md +148 -148
- package/dist/__mocks__/catalog.fixture.d.ts +2 -0
- package/dist/__mocks__/catalog.fixture.js +208 -0
- package/dist/__mocks__/exercises.mock.d.ts +4 -11
- package/dist/__mocks__/exercises.mock.js +82 -41
- package/dist/__mocks__/sessions.mock.d.ts +28 -0
- package/dist/__mocks__/sessions.mock.js +394 -0
- package/dist/__mocks__/testIds.d.ts +9 -0
- package/dist/__mocks__/testIds.js +13 -0
- package/dist/__mocks__/user.mock.js +3 -1
- package/dist/constants/goalJourney.d.ts +108 -0
- package/dist/constants/goalJourney.js +443 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
- package/dist/types/TApiClientConstellation.d.ts +33 -0
- package/dist/types/TApiClientConstellation.js +13 -0
- package/dist/types/TApiExercise.d.ts +5 -3
- package/dist/types/TApiUser.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/adoptionEngine/scaleProPlan.util.js +9 -4
- package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
- package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
- package/dist/utils/constellation/computeNormalisedLoad.test.d.ts +1 -0
- package/dist/utils/constellation/computeNormalisedLoad.test.js +218 -0
- package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
- package/dist/utils/constellation/evaluateConstellation.js +135 -0
- package/dist/utils/constellation/evaluateConstellation.test.d.ts +1 -0
- package/dist/utils/constellation/evaluateConstellation.test.js +93 -0
- package/dist/utils/constellation/index.d.ts +18 -0
- package/dist/utils/constellation/index.js +29 -0
- package/dist/utils/constellation/levelThresholds.d.ts +99 -0
- package/dist/utils/constellation/levelThresholds.js +123 -0
- package/dist/utils/constellation/starFoundation.d.ts +25 -0
- package/dist/utils/constellation/starFoundation.js +54 -0
- package/dist/utils/constellation/starFoundation.test.d.ts +1 -0
- package/dist/utils/constellation/starFoundation.test.js +75 -0
- package/dist/utils/constellation/stars/consistency.d.ts +29 -0
- package/dist/utils/constellation/stars/consistency.js +142 -0
- package/dist/utils/constellation/stars/consistency.test.d.ts +1 -0
- package/dist/utils/constellation/stars/consistency.test.js +94 -0
- package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
- package/dist/utils/constellation/stars/lowerBody.js +30 -0
- package/dist/utils/constellation/stars/pull.d.ts +11 -0
- package/dist/utils/constellation/stars/pull.js +24 -0
- package/dist/utils/constellation/stars/push.d.ts +11 -0
- package/dist/utils/constellation/stars/push.js +24 -0
- package/dist/utils/constellation/stars/quality.d.ts +19 -0
- package/dist/utils/constellation/stars/quality.js +98 -0
- package/dist/utils/constellation/stars/quality.test.d.ts +1 -0
- package/dist/utils/constellation/stars/quality.test.js +113 -0
- package/dist/utils/constellation/stars/recovery.d.ts +29 -0
- package/dist/utils/constellation/stars/recovery.js +169 -0
- package/dist/utils/constellation/stars/recovery.test.d.ts +1 -0
- package/dist/utils/constellation/stars/recovery.test.js +131 -0
- package/dist/utils/constellation/strengthStar.test.d.ts +1 -0
- package/dist/utils/constellation/strengthStar.test.js +190 -0
- package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
- package/dist/utils/constellation/strengthStarHelpers.js +104 -0
- package/dist/utils/constellation/types.d.ts +124 -0
- package/dist/utils/constellation/types.js +18 -0
- package/dist/utils/exerciseRecord/recordValidator.integration.test.js +1 -1
- package/dist/utils/exerciseRecord/recordValidator.js +1 -1
- package/dist/utils/exerciseRecord/recordValidator.test.js +8 -8
- package/dist/utils/index.d.ts +5 -3
- package/dist/utils/index.js +1 -0
- package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
- package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
- package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
- package/dist/utils/scoringWorkout/constants.d.ts +20 -6
- package/dist/utils/scoringWorkout/constants.js +23 -9
- package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
- package/dist/utils/scoringWorkout/helpers.js +24 -18
- package/dist/utils/scoringWorkout/index.d.ts +12 -8
- package/dist/utils/scoringWorkout/index.js +23 -15
- package/dist/utils/scoringWorkout/parseRecords.js +4 -3
- package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +223 -185
- package/dist/utils/scoringWorkout/types.d.ts +34 -14
- package/package.json +31 -31
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
- package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
- package/dist/utils/scaleProPlan.util.d.ts +0 -9
- package/dist/utils/scaleProPlan.util.js +0 -139
- package/dist/utils/scoring/calculateCalories.d.ts +0 -67
- package/dist/utils/scoring/calculateCalories.js +0 -345
- package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
- package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
- package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
- package/dist/utils/scoring/calculateQualityScore.js +0 -334
- package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
- package/dist/utils/scoring/calculateTotalVolume.js +0 -73
- package/dist/utils/scoring/constants.d.ts +0 -211
- package/dist/utils/scoring/constants.js +0 -247
- package/dist/utils/scoring/helpers.d.ts +0 -119
- package/dist/utils/scoring/helpers.js +0 -229
- package/dist/utils/scoring/index.d.ts +0 -28
- package/dist/utils/scoring/index.js +0 -47
- package/dist/utils/scoring/parseRecords.d.ts +0 -98
- package/dist/utils/scoring/parseRecords.js +0 -284
- package/dist/utils/scoring/types.d.ts +0 -86
- package/dist/utils/scoring/types.js +0 -11
- package/dist/utils/scoring.utils.d.ts +0 -14
- package/dist/utils/scoring.utils.js +0 -243
- /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
- /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.js → calculateMuscleFatigue.js} +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ============================================================================
|
|
3
|
+
* FITFRIX CONSTELLATION — Model & API Contract
|
|
4
|
+
* ============================================================================
|
|
5
|
+
*
|
|
6
|
+
* Purely live: every value is recomputed from raw records each call. Nothing is
|
|
7
|
+
* persisted. brightness (0-100) is the only signal; tier is derived from it.
|
|
8
|
+
*
|
|
9
|
+
* i18n: all user-facing text is a TLocalizedText ({ translationKey, params }).
|
|
10
|
+
* Evaluators never emit raw display strings — the client renders the key with
|
|
11
|
+
* locale-correct formatting. Numeric measures send value + unit only.
|
|
12
|
+
*
|
|
13
|
+
* Scoring is computed ONCE by the orchestrator and shared via TStarContext
|
|
14
|
+
* (scoredSessions), so quality and recovery never re-run the scoring engine.
|
|
15
|
+
*/
|
|
16
|
+
import type { TExercise, TUserMetric, TRecord } from "../../types";
|
|
17
|
+
import type { TLevel } from "./levelThresholds";
|
|
18
|
+
export interface TLocalizedText {
|
|
19
|
+
translationKey: string;
|
|
20
|
+
/** Ordered interpolation values, e.g. [10] -> "about {0}kg more". */
|
|
21
|
+
params?: (string | number)[];
|
|
22
|
+
}
|
|
23
|
+
export type TStarId = "consistency" | "quality" | "push" | "pull" | "lowerBody" | "recovery";
|
|
24
|
+
export type TFigurePosition = "heart" | "core" | "leftArm" | "rightArm" | "base" | "crown";
|
|
25
|
+
/** 0 dormant - 1 faint - 2 rising - 3 full. Derived from brightness. */
|
|
26
|
+
export type TStarTier = 0 | 1 | 2 | 3;
|
|
27
|
+
/** Unit codes a measure can carry. Keep in sync with the client's unit labels. */
|
|
28
|
+
export type TMeasureUnit = "bw_ratio" | "sessions" | "cycles" | "days";
|
|
29
|
+
/**
|
|
30
|
+
* A numeric measure. The client formats `value` for locale and renders `unit`
|
|
31
|
+
* via its translation. `display` is a localized template if the unit needs
|
|
32
|
+
* prose around the number (e.g. "{0} of {1} days"); for plain values the client
|
|
33
|
+
* may format value+unit directly.
|
|
34
|
+
*/
|
|
35
|
+
export interface TStarMeasure {
|
|
36
|
+
value: number;
|
|
37
|
+
unit: TMeasureUnit;
|
|
38
|
+
display: TLocalizedText;
|
|
39
|
+
}
|
|
40
|
+
export interface TResolvedStar {
|
|
41
|
+
id: TStarId;
|
|
42
|
+
displayName: TLocalizedText;
|
|
43
|
+
color: string;
|
|
44
|
+
figurePosition: TFigurePosition;
|
|
45
|
+
brightness: number;
|
|
46
|
+
tier: TStarTier;
|
|
47
|
+
objective: TLocalizedText;
|
|
48
|
+
rationale: TLocalizedText;
|
|
49
|
+
currentState: TStarMeasure;
|
|
50
|
+
target: TStarMeasure;
|
|
51
|
+
gap: TLocalizedText;
|
|
52
|
+
}
|
|
53
|
+
export interface TConstellationFigure {
|
|
54
|
+
/** 0-100, avg brightness across stars -> aura intensity. */
|
|
55
|
+
auraIntensity: number;
|
|
56
|
+
/** Count of stars currently at full brightness (tier 3). */
|
|
57
|
+
fullStarCount: number;
|
|
58
|
+
}
|
|
59
|
+
export interface TConstellationState {
|
|
60
|
+
stars: TResolvedStar[];
|
|
61
|
+
figure: TConstellationFigure;
|
|
62
|
+
currentLevel: TLevel;
|
|
63
|
+
evaluatedAt: string;
|
|
64
|
+
}
|
|
65
|
+
export interface TSessionRecord {
|
|
66
|
+
date: number;
|
|
67
|
+
exercises: {
|
|
68
|
+
exerciseId: string;
|
|
69
|
+
records: TRecord[];
|
|
70
|
+
}[];
|
|
71
|
+
}
|
|
72
|
+
export interface TConstellationInput {
|
|
73
|
+
sessions: TSessionRecord[];
|
|
74
|
+
/** Full catalog - UNFILTERED (incl. inactive). */
|
|
75
|
+
exerciseCatalog: Record<string, TExercise>;
|
|
76
|
+
user: Partial<TUserMetric>;
|
|
77
|
+
now: number;
|
|
78
|
+
currentLevel?: number;
|
|
79
|
+
}
|
|
80
|
+
/** One exercise within a session, scored once by the orchestrator. */
|
|
81
|
+
export interface TScoredExercise {
|
|
82
|
+
exerciseId: string;
|
|
83
|
+
/** Quality score 0-100 from the scoring engine, or null if unscorable. */
|
|
84
|
+
score: number | null;
|
|
85
|
+
/** Per-muscle scores (fatigue input), empty if unscorable. */
|
|
86
|
+
muscleScores: Record<string, number>;
|
|
87
|
+
}
|
|
88
|
+
/** A session with each exercise pre-scored. */
|
|
89
|
+
export interface TScoredSession {
|
|
90
|
+
date: number;
|
|
91
|
+
exercises: TScoredExercise[];
|
|
92
|
+
}
|
|
93
|
+
export interface TStarDynamicDetail {
|
|
94
|
+
currentState: TStarMeasure;
|
|
95
|
+
target: TStarMeasure;
|
|
96
|
+
gap: TLocalizedText;
|
|
97
|
+
}
|
|
98
|
+
export interface TStarEvaluation {
|
|
99
|
+
brightness: number;
|
|
100
|
+
detail: TStarDynamicDetail;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Everything a star needs to evaluate. Scoring is pre-computed
|
|
104
|
+
* (scoredSessions) so quality/recovery don't re-run the engine. Raw sessions
|
|
105
|
+
* remain available for stars that work off dates/records directly (consistency,
|
|
106
|
+
* strength).
|
|
107
|
+
*/
|
|
108
|
+
export interface TStarContext {
|
|
109
|
+
sessions: TSessionRecord[];
|
|
110
|
+
scoredSessions: TScoredSession[];
|
|
111
|
+
exerciseCatalog: Record<string, TExercise>;
|
|
112
|
+
user: Partial<TUserMetric>;
|
|
113
|
+
now: number;
|
|
114
|
+
level: TLevel;
|
|
115
|
+
}
|
|
116
|
+
export interface TStarDefinition {
|
|
117
|
+
id: TStarId;
|
|
118
|
+
displayName: TLocalizedText;
|
|
119
|
+
color: string;
|
|
120
|
+
figurePosition: TFigurePosition;
|
|
121
|
+
objective: TLocalizedText;
|
|
122
|
+
rationale: TLocalizedText;
|
|
123
|
+
evaluate: (ctx: TStarContext) => TStarEvaluation;
|
|
124
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/constellation/types.ts — Model & API Contract
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX CONSTELLATION — Model & API Contract
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* Purely live: every value is recomputed from raw records each call. Nothing is
|
|
9
|
+
* persisted. brightness (0-100) is the only signal; tier is derived from it.
|
|
10
|
+
*
|
|
11
|
+
* i18n: all user-facing text is a TLocalizedText ({ translationKey, params }).
|
|
12
|
+
* Evaluators never emit raw display strings — the client renders the key with
|
|
13
|
+
* locale-correct formatting. Numeric measures send value + unit only.
|
|
14
|
+
*
|
|
15
|
+
* Scoring is computed ONCE by the orchestrator and shared via TStarContext
|
|
16
|
+
* (scoredSessions), so quality and recovery never re-run the scoring engine.
|
|
17
|
+
*/
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -41,7 +41,7 @@ const exercises_mock_1 = require("../../__mocks__/exercises.mock");
|
|
|
41
41
|
const mockTemplate = templateExercises_mock_1.mockTemplateExercisesDictionary.cardioFree;
|
|
42
42
|
const baseExercise = exercises_mock_1.mockExercisesDictionary[mockTemplate.exerciseId];
|
|
43
43
|
// Artificially disable enableDistance to prove regression safety
|
|
44
|
-
const hostileConfig =
|
|
44
|
+
const hostileConfig = { ...mockTemplate.config, enableDistance: false };
|
|
45
45
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(mockTemplate.initialRecords[0], baseExercise, hostileConfig);
|
|
46
46
|
(0, vitest_1.expect)(result.isValid).toBe(true);
|
|
47
47
|
// Distance is structurally required for cardio-free, the config cannot turn it off.
|
|
@@ -18,7 +18,7 @@ const validateAndSanitizeRecord = (record, baseExercise, config) => {
|
|
|
18
18
|
return { isValid: false, sanitizedRecord: null, errors };
|
|
19
19
|
}
|
|
20
20
|
// 3. Create a clean clone for sanitization
|
|
21
|
-
const sanitized =
|
|
21
|
+
const sanitized = { ...record };
|
|
22
22
|
// 4. Config-Driven Sanitization
|
|
23
23
|
// If a field is disabled in the TExerciseConfig, we remove it from the payload.
|
|
24
24
|
if (!config.enableRpe) {
|
|
@@ -85,7 +85,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
85
85
|
(0, vitest_1.expect)(result.sanitizedRecord).toHaveProperty('reps', '0');
|
|
86
86
|
});
|
|
87
87
|
(0, vitest_1.it)('validates numeric integrity of reps-only and collects error', () => {
|
|
88
|
-
const baseRepExercise =
|
|
88
|
+
const baseRepExercise = { ...mockBaseExercise, recordType: 'reps-only' };
|
|
89
89
|
const record = { type: 'reps-only', isDone: true, isStrictMode: false, reps: 'abc', auxWeightKg: '20' };
|
|
90
90
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseRepExercise, mockConfigAllEnabled);
|
|
91
91
|
(0, vitest_1.expect)(result.isValid).toBe(false);
|
|
@@ -94,7 +94,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
94
94
|
(0, vitest_1.expect)(result.errors[0]).toContain('Reps must be a parseable number');
|
|
95
95
|
});
|
|
96
96
|
(0, vitest_1.it)('zeroes out required auxWeightKg instead of deleting it on reps-only when disabled', () => {
|
|
97
|
-
const baseRepExercise =
|
|
97
|
+
const baseRepExercise = { ...mockBaseExercise, recordType: 'reps-only' };
|
|
98
98
|
const record = { type: 'reps-only', isDone: true, isStrictMode: false, reps: '10', auxWeightKg: '20' };
|
|
99
99
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseRepExercise, mockConfigAllDisabled);
|
|
100
100
|
(0, vitest_1.expect)(result.isValid).toBe(true);
|
|
@@ -102,14 +102,14 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
102
102
|
(0, vitest_1.expect)(result.sanitizedRecord).toHaveProperty('auxWeightKg', '');
|
|
103
103
|
});
|
|
104
104
|
(0, vitest_1.it)('validates duration type successfully (Happy Path)', () => {
|
|
105
|
-
const baseDurationExercise =
|
|
105
|
+
const baseDurationExercise = { ...mockBaseExercise, recordType: 'duration' };
|
|
106
106
|
const record = { type: 'duration', isDone: true, isStrictMode: false, durationMmSs: '05:00', auxWeightKg: '0' };
|
|
107
107
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseDurationExercise, mockConfigAllEnabled);
|
|
108
108
|
(0, vitest_1.expect)(result.isValid).toBe(true);
|
|
109
109
|
(0, vitest_1.expect)(result.sanitizedRecord).toHaveProperty('durationMmSs', '05:00');
|
|
110
110
|
});
|
|
111
111
|
(0, vitest_1.it)('rejects cardio-machine records with zero duration', () => {
|
|
112
|
-
const baseMachineExercise =
|
|
112
|
+
const baseMachineExercise = { ...mockBaseExercise, recordType: 'cardio-machine' };
|
|
113
113
|
const record = { type: 'cardio-machine', isDone: true, isStrictMode: false, durationMmSs: '00:00' };
|
|
114
114
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseMachineExercise, mockConfigAllEnabled);
|
|
115
115
|
(0, vitest_1.expect)(result.isValid).toBe(false);
|
|
@@ -117,7 +117,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
117
117
|
(0, vitest_1.expect)(result.errors[0]).toContain('Duration (MM:SS) must be present and non-zero');
|
|
118
118
|
});
|
|
119
119
|
(0, vitest_1.it)('strips optional fields from cardio-machine when disabled', () => {
|
|
120
|
-
const baseMachineExercise =
|
|
120
|
+
const baseMachineExercise = { ...mockBaseExercise, recordType: 'cardio-machine' };
|
|
121
121
|
const record = {
|
|
122
122
|
type: 'cardio-machine',
|
|
123
123
|
isDone: true,
|
|
@@ -139,7 +139,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
139
139
|
(0, vitest_1.expect)(result.sanitizedRecord).toHaveProperty('durationMmSs', '15:00');
|
|
140
140
|
});
|
|
141
141
|
(0, vitest_1.it)('validates duration format for cardio-free', () => {
|
|
142
|
-
const baseCardioExercise =
|
|
142
|
+
const baseCardioExercise = { ...mockBaseExercise, recordType: 'cardio-free' };
|
|
143
143
|
// Missing durationMmSs
|
|
144
144
|
const record = { type: 'cardio-free', isDone: true, isStrictMode: false, distance: '5', durationMmSs: '00:00' };
|
|
145
145
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseCardioExercise, mockConfigAllEnabled);
|
|
@@ -148,7 +148,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
148
148
|
(0, vitest_1.expect)(result.errors[0]).toContain('Duration (MM:SS) must be present and non-zero');
|
|
149
149
|
});
|
|
150
150
|
(0, vitest_1.it)('validates distance format for cardio-free', () => {
|
|
151
|
-
const baseCardioExercise =
|
|
151
|
+
const baseCardioExercise = { ...mockBaseExercise, recordType: 'cardio-free' };
|
|
152
152
|
const record = { type: 'cardio-free', isDone: true, isStrictMode: false, distance: 'invalid', durationMmSs: '15:00' };
|
|
153
153
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseCardioExercise, mockConfigAllEnabled);
|
|
154
154
|
(0, vitest_1.expect)(result.isValid).toBe(false);
|
|
@@ -156,7 +156,7 @@ const recordValidator_1 = require("./recordValidator");
|
|
|
156
156
|
(0, vitest_1.expect)(result.errors[0]).toContain('Distance must be a parseable number');
|
|
157
157
|
});
|
|
158
158
|
(0, vitest_1.it)('retains required distance on cardio-free even if enableDistance is false (Regression)', () => {
|
|
159
|
-
const baseCardioExercise =
|
|
159
|
+
const baseCardioExercise = { ...mockBaseExercise, recordType: 'cardio-free' };
|
|
160
160
|
const record = { type: 'cardio-free', isDone: true, isStrictMode: false, distance: '5', durationMmSs: '15:00' };
|
|
161
161
|
const result = (0, recordValidator_1.validateAndSanitizeRecord)(record, baseCardioExercise, mockConfigAllDisabled);
|
|
162
162
|
(0, vitest_1.expect)(result.isValid).toBe(true);
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -7,9 +7,11 @@ export { toError } from "./toError.util";
|
|
|
7
7
|
export { generatePlanCode } from "./planCode.util";
|
|
8
8
|
export { maskEmail, isAnonymousEmail, isEmail } from "./email.utils";
|
|
9
9
|
export { NOOP } from "./noop.utils";
|
|
10
|
-
export { calculateExerciseScoreV2, calculateTotalVolume, deriveTrainingAgeBracket, computeMuscleFatigueMap } from "./scoringWorkout";
|
|
11
|
-
export type { IHistoricalContext, TTrainingAgeBracket, TEnrichedSessionScore, TEnrichedExerciseRecord, TMuscleFatigueEntry, TMuscleFatigueResult } from "./scoringWorkout";
|
|
12
|
-
export { scaleProPlan, calculateBMI, calculateDayPlanDuration, calculateExerciseDurationSecs } from "./adoptionEngine/scaleProPlan.util";
|
|
10
|
+
export { calculateExerciseScoreV2, calculateTotalVolume, deriveTrainingAgeBracket, computeMuscleFatigueMap, } from "./scoringWorkout";
|
|
11
|
+
export type { IHistoricalContext, TTrainingAgeBracket, TEnrichedSessionScore, TEnrichedExerciseRecord, TMuscleFatigueEntry, TMuscleFatigueResult, } from "./scoringWorkout";
|
|
12
|
+
export { scaleProPlan, calculateBMI, calculateDayPlanDuration, calculateExerciseDurationSecs, } from "./adoptionEngine/scaleProPlan.util";
|
|
13
13
|
export * from "./exerciseRecord/workoutMath";
|
|
14
14
|
export * from "./exerciseRecord/recordValidator";
|
|
15
15
|
export * from "./metricConversions";
|
|
16
|
+
export * from "./constellation";
|
|
17
|
+
export type * from "./constellation/types";
|
package/dist/utils/index.js
CHANGED
|
@@ -51,3 +51,4 @@ Object.defineProperty(exports, "calculateExerciseDurationSecs", { enumerable: tr
|
|
|
51
51
|
__exportStar(require("./exerciseRecord/workoutMath"), exports);
|
|
52
52
|
__exportStar(require("./exerciseRecord/recordValidator"), exports);
|
|
53
53
|
__exportStar(require("./metricConversions"), exports);
|
|
54
|
+
__exportStar(require("./constellation"), exports);
|
|
@@ -8,33 +8,54 @@
|
|
|
8
8
|
* Quality is about execution: did you finish, stay consistent, push hard
|
|
9
9
|
* enough, and rest appropriately?
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* THREE ALWAYS-ACTIVE COMPONENTS + ONE CONDITIONAL:
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* │ Component │
|
|
15
|
-
*
|
|
16
|
-
* │ Completion │ 20%
|
|
17
|
-
* │ Consistency │ 35%
|
|
18
|
-
* │
|
|
19
|
-
* │
|
|
20
|
-
* │
|
|
21
|
-
* │
|
|
22
|
-
*
|
|
13
|
+
* ┌─────────────────────┬────────────┬──────────────────────────────────────────┐
|
|
14
|
+
* │ Component │ Base weight│ What it measures │
|
|
15
|
+
* ├─────────────────────┼────────────┼──────────────────────────────────────────┤
|
|
16
|
+
* │ Completion │ 20% │ Completed sets / planned sets │
|
|
17
|
+
* │ Consistency │ 35% │ Stable or intentionally progressive sets │
|
|
18
|
+
* │ Effort Adequacy │ 30% │ RPE/RIR proximity to productive failure │
|
|
19
|
+
* │ Rest Discipline │ 15% │ Rest periods within optimal windows │
|
|
20
|
+
* │ (conditional) │ or 0% │ Only scored when rest data exists. │
|
|
21
|
+
* │ │ │ Absent → weight redistributed to others. │
|
|
22
|
+
* └─────────────────────┴────────────┴──────────────────────────────────────────┘
|
|
23
23
|
*
|
|
24
|
-
*
|
|
24
|
+
* EFFORT SCORING (UPDATED):
|
|
25
|
+
* RPE/RIR is now captured on EVERY set via the post-set "How did that feel?"
|
|
26
|
+
* one-tap modal (Warm-up=RPE4 / Challenging=RPE7 / Maximum=RPE9). Because effort
|
|
27
|
+
* is now always present, scoring is GRADED and MONOTONIC: harder sets score
|
|
28
|
+
* strictly higher than easier ones, so Gate 2 can distinguish "going through the
|
|
29
|
+
* motions" from "genuinely training" — which the old flat "in-band = 100" logic
|
|
30
|
+
* could not (RPE 7 and RPE 9 both scored 100).
|
|
25
31
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* -
|
|
30
|
-
*
|
|
32
|
+
* RPE 9 → 100 RPE 7 → ~81 RPE 4 → ~53 RPE 1 → 25 (floor 20)
|
|
33
|
+
* RIR 0 → 100 RIR 3 → 76 RIR 6 → 52 RIR 10 → 20 (floor 20)
|
|
34
|
+
*
|
|
35
|
+
* A genuine warm-up-effort working set now drags the score down instead of
|
|
36
|
+
* floating at a neutral 50 — an "easy on every set" session correctly fails
|
|
37
|
+
* Gate 2. RIR is preferred over RPE when both are present (more objective).
|
|
38
|
+
*
|
|
39
|
+
* REST DISCIPLINE:
|
|
40
|
+
* Strict timing mode is not implemented in the app, so rest data is absent in
|
|
41
|
+
* most sessions. When present, rest contributes meaningfully. When absent, the
|
|
42
|
+
* 15% weight is redistributed proportionally across the three active components
|
|
43
|
+
* via dynamic normalisation — score cannot be biased by uncollected data.
|
|
44
|
+
*
|
|
45
|
+
* EFFORT-WEIGHTED AVERAGING:
|
|
46
|
+
* Effort adequacy weights the FINAL set most heavily. The last working set is
|
|
47
|
+
* where adequacy is truly determined — a strong finish (or a sandbagged one)
|
|
48
|
+
* is the clearest signal of whether the session stimulated adaptation.
|
|
49
|
+
*
|
|
50
|
+
* SINGLE SCORE OUTPUT:
|
|
51
|
+
* Returns one score, not per-goal. Goal-specific progression logic lives in the
|
|
52
|
+
* gate system and quick plan generator upstream.
|
|
31
53
|
*/
|
|
32
|
-
import type { IParsedSet,
|
|
54
|
+
import type { IParsedSet, IQualityBreakdown, IHistoricalContext } from "./types";
|
|
33
55
|
import type { ITimingGuardrails } from "./parseRecords";
|
|
34
|
-
import type { TAiFitnessGoal } from "../../constants/AiExerciseVocabulary";
|
|
35
56
|
/**
|
|
36
57
|
* Raw record shape — we need the original RPE/RIR strings and isDone flag
|
|
37
|
-
* that
|
|
58
|
+
* that are not present in IParsedSet (which contains only completed sets).
|
|
38
59
|
*/
|
|
39
60
|
interface IRawRecord {
|
|
40
61
|
type: string;
|
|
@@ -51,23 +72,25 @@ interface IRawRecord {
|
|
|
51
72
|
distance?: string;
|
|
52
73
|
restDurationSecs?: number;
|
|
53
74
|
}
|
|
75
|
+
export interface IQualityScoreResult {
|
|
76
|
+
/** Overall quality score 0–100. */
|
|
77
|
+
score: number;
|
|
78
|
+
/** Per-component breakdown — each sub-score is 0–100. */
|
|
79
|
+
qualityBreakdown: IQualityBreakdown;
|
|
80
|
+
/**
|
|
81
|
+
* True when rest discipline was included in the weighted score.
|
|
82
|
+
* False when no rest data was logged — weight redistributed to other components.
|
|
83
|
+
* Use this flag to gate the rest discipline bar in the UI.
|
|
84
|
+
*/
|
|
85
|
+
restDisciplineActive: boolean;
|
|
86
|
+
}
|
|
54
87
|
/**
|
|
55
|
-
* Calculate the overall quality score and its breakdown.
|
|
56
|
-
*
|
|
57
|
-
* @param parsedSets Cleaned sets (from parseRecords) — only completed sets
|
|
58
|
-
* @param rawRecords Original TRecord[] — needed for completion count (includes skipped)
|
|
59
|
-
* @param timingGuardrails Exercise's guardrails — for rest period validation
|
|
60
|
-
* @param isStrictTimingModeScoring If false, ignores the rest discipline penalty.
|
|
61
|
-
* @param userContext User context for dynamic weighting (P2-3)
|
|
62
|
-
* @param historicalContext Optional history for progressive overload detection (P2-6)
|
|
63
|
-
* @returns { score: 0–100, breakdown: { completion, consistency, effortAdequacy, restDiscipline } }
|
|
88
|
+
* Calculate the overall quality score and its component breakdown.
|
|
64
89
|
*
|
|
65
|
-
* @
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
90
|
+
* @param parsedSets Cleaned completed sets (output of parseRecords)
|
|
91
|
+
* @param rawRecords Original TRecord[] — needed for completion count and RPE/RIR
|
|
92
|
+
* @param timingGuardrails Exercise DB guardrails — for rest period validation
|
|
93
|
+
* @param historicalContext Optional cross-session context for overload detection
|
|
69
94
|
*/
|
|
70
|
-
export declare function calculateQualityScore(parsedSets: IParsedSet[], rawRecords: IRawRecord[], timingGuardrails: ITimingGuardrails | undefined,
|
|
71
|
-
scoresByGoal: Partial<Record<TAiFitnessGoal, import("./types").IScoreByGoal>>;
|
|
72
|
-
};
|
|
95
|
+
export declare function calculateQualityScore(parsedSets: IParsedSet[], rawRecords: IRawRecord[], timingGuardrails: ITimingGuardrails | undefined, historicalContext?: IHistoricalContext): IQualityScoreResult;
|
|
73
96
|
export {};
|