@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,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 = Object.assign(Object.assign({}, mockTemplate.config), { enableDistance: false });
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 = Object.assign({}, record);
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 = Object.assign(Object.assign({}, mockBaseExercise), { recordType: 'reps-only' });
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 = Object.assign(Object.assign({}, mockBaseExercise), { recordType: 'reps-only' });
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 = Object.assign(Object.assign({}, mockBaseExercise), { recordType: 'duration' });
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 = Object.assign(Object.assign({}, mockBaseExercise), { recordType: 'cardio-machine' });
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 = Object.assign(Object.assign({}, mockBaseExercise), { recordType: 'cardio-machine' });
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 = Object.assign(Object.assign({}, mockBaseExercise), { recordType: 'cardio-free' });
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 = Object.assign(Object.assign({}, mockBaseExercise), { recordType: 'cardio-free' });
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 = Object.assign(Object.assign({}, mockBaseExercise), { recordType: 'cardio-free' });
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);
@@ -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";
@@ -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
- * FOUR SUB-COMPONENTS:
11
+ * THREE ALWAYS-ACTIVE COMPONENTS + ONE CONDITIONAL:
12
12
  *
13
- * ┌─────────────────────┬────────┬──────────────────────────────────────────┐
14
- * │ Component │ Weight │ What it measures │
15
- * ├─────────────────────┼────────┼──────────────────────────────────────────┤
16
- * │ Completion │ 20% Did you finish all planned sets?
17
- * │ Consistency │ 35% Were sets stable or intentionally
18
- * │ progressive? (not random drops)
19
- * │ Effort Adequacy30% Were you in the productive effort zone?
20
- * │ (RPE 6–9 or RIR 1–4)
21
- * │ Rest Discipline 15% Did you respect optimal rest windows?
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 Discipline15% 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
- * Each sub-component produces 0–100. The final score is a weighted average.
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
- * DESIGN PRINCIPLES:
27
- * - Motivational: even a mediocre workout should score 40–60, not 10
28
- * - Honest: perfect scores require real effort and discipline
29
- * - Fair across types: cardio and strength use the same framework
30
- * - Transparent: the breakdown is returned so the UI can explain the score
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, IUserContext, IHistoricalContext } from "./types";
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 aren't in IParsedSet (which only contains completed sets).
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
- * @example
66
- * const { score, breakdown } = calculateQualityScore(parsed, raw, guardrails, false, userCtx, historyCtx);
67
- * // score: 81
68
- * // breakdown: { completion: 100, consistency: 75, effortAdequacy: 80, restDiscipline: 65 }
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, isStrictTimingModeScoring: boolean, userContext: IUserContext, historicalContext?: IHistoricalContext): {
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 {};