@dgpholdings/greatoak-shared 1.2.85 → 1.2.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -148
- package/dist/__mocks__/exercises.mock.js +1 -0
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/quickStartIntents.d.ts +19 -0
- package/dist/constants/quickStartIntents.js +39 -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/index.d.ts +1 -0
- package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
- package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
- package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
- package/dist/utils/constellation/evaluateConstellation.js +135 -0
- package/dist/utils/constellation/index.d.ts +17 -0
- package/dist/utils/constellation/index.js +26 -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/stars/consistency.d.ts +29 -0
- package/dist/utils/constellation/stars/consistency.js +142 -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/recovery.d.ts +29 -0
- package/dist/utils/constellation/stars/recovery.js +169 -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/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 +210 -172
- 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,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// utils/constellation/strengthStarHelpers.ts — Strength Star Helpers
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* FITFRIX CONSTELLATION — Strength Star Helpers
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* Shared by the three strength stars (push, pull, lowerBody). Each star is a
|
|
9
|
+
* thin definition that names its movement patterns and its threshold key; this
|
|
10
|
+
* helper does the rest: filter the catalog to those patterns, compute the
|
|
11
|
+
* normalised load, and turn the ratio into brightness + i18n detail.
|
|
12
|
+
*
|
|
13
|
+
* Thresholds come from STRENGTH_THRESHOLDS[key][level], resolved by sex.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.STRENGTH_WINDOW_DAYS = void 0;
|
|
17
|
+
exports.resolveThreshold = resolveThreshold;
|
|
18
|
+
exports.userBodyweight = userBodyweight;
|
|
19
|
+
exports.ratioToBrightness = ratioToBrightness;
|
|
20
|
+
exports.buildGap = buildGap;
|
|
21
|
+
exports.evaluateStrengthStar = evaluateStrengthStar;
|
|
22
|
+
const computeNormalisedLoad_1 = require("./computeNormalisedLoad");
|
|
23
|
+
const levelThresholds_1 = require("./levelThresholds");
|
|
24
|
+
/** Rolling window for strength PRs: 8 weeks. */
|
|
25
|
+
exports.STRENGTH_WINDOW_DAYS = 56;
|
|
26
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
27
|
+
const DEFAULT_BODYWEIGHT_KG = 70;
|
|
28
|
+
/** Resolve the threshold ratio for this user. unmentioned -> male (higher bar). */
|
|
29
|
+
function resolveThreshold(threshold, gender) {
|
|
30
|
+
return gender === "female" ? threshold.female : threshold.male;
|
|
31
|
+
}
|
|
32
|
+
/** User bodyweight with fallback. */
|
|
33
|
+
function userBodyweight(user) {
|
|
34
|
+
const bw = user.weightKg;
|
|
35
|
+
return bw && bw > 0 ? bw : DEFAULT_BODYWEIGHT_KG;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Brightness from current ratio vs threshold, graded & continuous.
|
|
39
|
+
* brightness = (currentRatio / thresholdRatio) * 100, capped at 100.
|
|
40
|
+
* Faint from the first rep; full at threshold. No wall.
|
|
41
|
+
*/
|
|
42
|
+
function ratioToBrightness(currentRatio, thresholdRatio) {
|
|
43
|
+
if (thresholdRatio <= 0)
|
|
44
|
+
return 0;
|
|
45
|
+
return Math.min(100, (currentRatio / thresholdRatio) * 100);
|
|
46
|
+
}
|
|
47
|
+
/** Build a bodyweight-ratio measure for the detail panel. */
|
|
48
|
+
function bwRatioMeasure(ratio) {
|
|
49
|
+
const rounded = Math.round(ratio * 100) / 100;
|
|
50
|
+
return {
|
|
51
|
+
value: rounded,
|
|
52
|
+
unit: "bw_ratio",
|
|
53
|
+
display: {
|
|
54
|
+
translationKey: "constellation.measure.bwRatio",
|
|
55
|
+
params: [rounded],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the "what's missing" gap text. At/above threshold -> a "full" message;
|
|
61
|
+
* otherwise the remaining kg on the user's best lift to reach the threshold.
|
|
62
|
+
*/
|
|
63
|
+
function buildGap(gapKeyPrefix, currentRatio, thresholdRatio, bodyweightKg) {
|
|
64
|
+
if (currentRatio >= thresholdRatio) {
|
|
65
|
+
return { translationKey: `${gapKeyPrefix}.gap.full` };
|
|
66
|
+
}
|
|
67
|
+
const remainingKg = Math.max(0, Math.round((thresholdRatio - currentRatio) * bodyweightKg));
|
|
68
|
+
return {
|
|
69
|
+
translationKey: `${gapKeyPrefix}.gap.toGo`,
|
|
70
|
+
params: [remainingKg],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The full strength-star evaluation, shared by push/pull/lowerBody.
|
|
75
|
+
*
|
|
76
|
+
* @param ctx star context (sessions, catalog, user, now, level)
|
|
77
|
+
* @param strengthKey which STRENGTH_THRESHOLDS row to use ("push" | "pull" | "lowerBody")
|
|
78
|
+
* @param matchingPatterns movement patterns this star counts
|
|
79
|
+
* @param gapKeyPrefix translation-key prefix (e.g. "constellation.push")
|
|
80
|
+
*/
|
|
81
|
+
function evaluateStrengthStar(ctx, strengthKey, matchingPatterns, gapKeyPrefix) {
|
|
82
|
+
const bw = userBodyweight(ctx.user);
|
|
83
|
+
const gender = ctx.user.gender;
|
|
84
|
+
const thresholdRatio = resolveThreshold(levelThresholds_1.STRENGTH_THRESHOLDS[strengthKey][ctx.level], gender);
|
|
85
|
+
// Build the matching-exercise id set from the catalog by movement pattern.
|
|
86
|
+
const patternSet = new Set(matchingPatterns);
|
|
87
|
+
const matchingIds = new Set();
|
|
88
|
+
for (const id of Object.keys(ctx.exerciseCatalog)) {
|
|
89
|
+
const ex = ctx.exerciseCatalog[id];
|
|
90
|
+
if (patternSet.has(ex.movementPattern))
|
|
91
|
+
matchingIds.add(id);
|
|
92
|
+
}
|
|
93
|
+
const windowStart = ctx.now - exports.STRENGTH_WINDOW_DAYS * MS_PER_DAY;
|
|
94
|
+
const load = (0, computeNormalisedLoad_1.computeNormalisedLoad)(matchingIds, ctx.sessions, ctx.exerciseCatalog, bw, gender, windowStart, ctx.now);
|
|
95
|
+
const brightness = ratioToBrightness(load.ratio, thresholdRatio);
|
|
96
|
+
return {
|
|
97
|
+
brightness,
|
|
98
|
+
detail: {
|
|
99
|
+
currentState: bwRatioMeasure(load.ratio),
|
|
100
|
+
target: bwRatioMeasure(thresholdRatio),
|
|
101
|
+
gap: buildGap(gapKeyPrefix, load.ratio, thresholdRatio, bw),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -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 });
|
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 {};
|