@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.
Files changed (79) hide show
  1. package/README.md +148 -148
  2. package/dist/__mocks__/exercises.mock.js +1 -0
  3. package/dist/constants/index.d.ts +1 -0
  4. package/dist/constants/index.js +1 -0
  5. package/dist/constants/quickStartIntents.d.ts +19 -0
  6. package/dist/constants/quickStartIntents.js +39 -0
  7. package/dist/types/TApiAiExerciseAnalysis.d.ts +2 -1
  8. package/dist/types/TApiClientConstellation.d.ts +33 -0
  9. package/dist/types/TApiClientConstellation.js +13 -0
  10. package/dist/types/TApiExercise.d.ts +5 -3
  11. package/dist/types/index.d.ts +1 -0
  12. package/dist/utils/constellation/computeNormalisedLoad.d.ts +48 -0
  13. package/dist/utils/constellation/computeNormalisedLoad.js +150 -0
  14. package/dist/utils/constellation/evaluateConstellation.d.ts +27 -0
  15. package/dist/utils/constellation/evaluateConstellation.js +135 -0
  16. package/dist/utils/constellation/index.d.ts +17 -0
  17. package/dist/utils/constellation/index.js +26 -0
  18. package/dist/utils/constellation/levelThresholds.d.ts +99 -0
  19. package/dist/utils/constellation/levelThresholds.js +123 -0
  20. package/dist/utils/constellation/starFoundation.d.ts +25 -0
  21. package/dist/utils/constellation/starFoundation.js +54 -0
  22. package/dist/utils/constellation/stars/consistency.d.ts +29 -0
  23. package/dist/utils/constellation/stars/consistency.js +142 -0
  24. package/dist/utils/constellation/stars/lowerBody.d.ts +17 -0
  25. package/dist/utils/constellation/stars/lowerBody.js +30 -0
  26. package/dist/utils/constellation/stars/pull.d.ts +11 -0
  27. package/dist/utils/constellation/stars/pull.js +24 -0
  28. package/dist/utils/constellation/stars/push.d.ts +11 -0
  29. package/dist/utils/constellation/stars/push.js +24 -0
  30. package/dist/utils/constellation/stars/quality.d.ts +19 -0
  31. package/dist/utils/constellation/stars/quality.js +98 -0
  32. package/dist/utils/constellation/stars/recovery.d.ts +29 -0
  33. package/dist/utils/constellation/stars/recovery.js +169 -0
  34. package/dist/utils/constellation/strengthStarHelpers.d.ts +41 -0
  35. package/dist/utils/constellation/strengthStarHelpers.js +104 -0
  36. package/dist/utils/constellation/types.d.ts +124 -0
  37. package/dist/utils/constellation/types.js +18 -0
  38. package/dist/utils/index.d.ts +5 -3
  39. package/dist/utils/index.js +1 -0
  40. package/dist/utils/scoringWorkout/calculateQualityScore.d.ts +59 -36
  41. package/dist/utils/scoringWorkout/calculateQualityScore.js +234 -233
  42. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.d.ts +8 -5
  43. package/dist/utils/scoringWorkout/computeMuscleFatigueMap.js +72 -88
  44. package/dist/utils/scoringWorkout/constants.d.ts +20 -6
  45. package/dist/utils/scoringWorkout/constants.js +23 -9
  46. package/dist/utils/scoringWorkout/helpers.d.ts +7 -0
  47. package/dist/utils/scoringWorkout/helpers.js +24 -18
  48. package/dist/utils/scoringWorkout/index.d.ts +12 -8
  49. package/dist/utils/scoringWorkout/index.js +23 -15
  50. package/dist/utils/scoringWorkout/parseRecords.js +4 -3
  51. package/dist/utils/scoringWorkout/scoringWorkout.integration.test.js +210 -172
  52. package/dist/utils/scoringWorkout/types.d.ts +34 -14
  53. package/package.json +31 -31
  54. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.d.ts +0 -30
  55. package/dist/utils/exerciseRecord/__mocks__/exercises.mock.js +0 -138
  56. package/dist/utils/scaleProPlan.util.d.ts +0 -9
  57. package/dist/utils/scaleProPlan.util.js +0 -139
  58. package/dist/utils/scoring/calculateCalories.d.ts +0 -67
  59. package/dist/utils/scoring/calculateCalories.js +0 -345
  60. package/dist/utils/scoring/calculateMuscleFatiue.d.ts +0 -67
  61. package/dist/utils/scoring/calculateMuscleFatiue.js +0 -310
  62. package/dist/utils/scoring/calculateQualityScore.d.ts +0 -71
  63. package/dist/utils/scoring/calculateQualityScore.js +0 -334
  64. package/dist/utils/scoring/calculateTotalVolume.d.ts +0 -15
  65. package/dist/utils/scoring/calculateTotalVolume.js +0 -73
  66. package/dist/utils/scoring/constants.d.ts +0 -211
  67. package/dist/utils/scoring/constants.js +0 -247
  68. package/dist/utils/scoring/helpers.d.ts +0 -119
  69. package/dist/utils/scoring/helpers.js +0 -229
  70. package/dist/utils/scoring/index.d.ts +0 -28
  71. package/dist/utils/scoring/index.js +0 -47
  72. package/dist/utils/scoring/parseRecords.d.ts +0 -98
  73. package/dist/utils/scoring/parseRecords.js +0 -284
  74. package/dist/utils/scoring/types.d.ts +0 -86
  75. package/dist/utils/scoring/types.js +0 -11
  76. package/dist/utils/scoring.utils.d.ts +0 -14
  77. package/dist/utils/scoring.utils.js +0 -243
  78. /package/dist/utils/scoringWorkout/{calculateMuscleFatiue.d.ts → calculateMuscleFatigue.d.ts} +0 -0
  79. /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 });
@@ -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 {};