@sunsteel/contracts 0.85.0 → 0.87.0

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/dist/index.cjs CHANGED
@@ -52,6 +52,7 @@ __export(index_exports, {
52
52
  COMEBACK_REQUIRED_ACTIVE_DAYS: () => COMEBACK_REQUIRED_ACTIVE_DAYS,
53
53
  COMEBACK_SESSION_LOOKBACK: () => COMEBACK_SESSION_LOOKBACK,
54
54
  COMEBACK_WINDOW_DAYS: () => COMEBACK_WINDOW_DAYS,
55
+ CONVERSATIONS_PAGE_SIZE: () => CONVERSATIONS_PAGE_SIZE,
55
56
  CUSTOM_EXERCISES_MAX: () => CUSTOM_EXERCISES_MAX,
56
57
  CUSTOM_EXERCISE_NAME_MAX: () => CUSTOM_EXERCISE_NAME_MAX,
57
58
  CUSTOM_EXERCISE_NOTE_MAX: () => CUSTOM_EXERCISE_NOTE_MAX,
@@ -60,6 +61,7 @@ __export(index_exports, {
60
61
  DASHBOARD_SECTION_IDS: () => DASHBOARD_SECTION_IDS,
61
62
  DEFAULT_DASHBOARD_LAYOUT: () => DEFAULT_DASHBOARD_LAYOUT,
62
63
  DEFAULT_LOCALE: () => DEFAULT_LOCALE,
64
+ DEFAULT_MESSAGE_PERMISSION: () => DEFAULT_MESSAGE_PERMISSION,
63
65
  DEFAULT_WEEK_STARTS_ON: () => DEFAULT_WEEK_STARTS_ON,
64
66
  DELOAD_DEFAULT_DAYS: () => DELOAD_DEFAULT_DAYS,
65
67
  DELOAD_DEFAULT_LOAD_REDUCTION: () => DELOAD_DEFAULT_LOAD_REDUCTION,
@@ -94,10 +96,27 @@ __export(index_exports, {
94
96
  INCHES_PER_FOOT: () => INCHES_PER_FOOT,
95
97
  LENGTH_UNITS: () => LENGTH_UNITS,
96
98
  LIMITED_SHORTFALL_KG: () => LIMITED_SHORTFALL_KG,
99
+ LP_BLOCK_STEPS: () => LP_BLOCK_STEPS,
100
+ LP_ESTIMATE_MAX_REPS_TO_FAILURE: () => LP_ESTIMATE_MAX_REPS_TO_FAILURE,
101
+ LP_ESTIMATE_MIN_SETS: () => LP_ESTIMATE_MIN_SETS,
102
+ LP_ESTIMATE_STEPS: () => LP_ESTIMATE_STEPS,
103
+ LP_NEXT_BLOCK_KINDS: () => LP_NEXT_BLOCK_KINDS,
104
+ LP_PHASES: () => LP_PHASES,
105
+ LP_PROGRESS_CAP: () => LP_PROGRESS_CAP,
106
+ LP_PROGRESS_THRESHOLD: () => LP_PROGRESS_THRESHOLD,
107
+ LP_RECOVERY: () => LP_RECOVERY,
108
+ LP_REFERENCE_MAX_KG: () => LP_REFERENCE_MAX_KG,
109
+ LP_REST_SECONDS: () => LP_REST_SECONDS,
110
+ LP_STEPS: () => LP_STEPS,
111
+ LP_WORKING_SETS: () => LP_WORKING_SETS,
97
112
  MAX_SETS_PER_EXERCISE: () => MAX_SETS_PER_EXERCISE,
98
113
  MEASURABLE_GOALS_MAX: () => MEASURABLE_GOALS_MAX,
99
114
  MEASURABLE_GOAL_DIRECTIONS: () => MEASURABLE_GOAL_DIRECTIONS,
100
115
  MEASURABLE_GOAL_TYPES: () => MEASURABLE_GOAL_TYPES,
116
+ MESSAGES_PAGE_SIZE: () => MESSAGES_PAGE_SIZE,
117
+ MESSAGES_PER_MINUTE_MAX: () => MESSAGES_PER_MINUTE_MAX,
118
+ MESSAGE_BODY_MAX: () => MESSAGE_BODY_MAX,
119
+ MESSAGE_PERMISSIONS: () => MESSAGE_PERMISSIONS,
101
120
  MINUTES_IN_DAY: () => MINUTES_IN_DAY,
102
121
  MODERATION_ACTION_KINDS: () => MODERATION_ACTION_KINDS,
103
122
  MODERATION_HISTORY_PAGE_SIZE: () => MODERATION_HISTORY_PAGE_SIZE,
@@ -105,6 +124,7 @@ __export(index_exports, {
105
124
  MODERATION_QUEUE_PAGE_SIZE: () => MODERATION_QUEUE_PAGE_SIZE,
106
125
  MOVEMENT_PATTERNS: () => MOVEMENT_PATTERNS,
107
126
  MUSCLE_GROUPS: () => MUSCLE_GROUPS,
127
+ NEW_CONVERSATIONS_PER_DAY_MAX: () => NEW_CONVERSATIONS_PER_DAY_MAX,
108
128
  NOTIFICATIONS_LIST_LIMIT: () => NOTIFICATIONS_LIST_LIMIT,
109
129
  NOTIFICATIONS_LOOKBACK_DAYS: () => NOTIFICATIONS_LOOKBACK_DAYS,
110
130
  NOTIFICATIONS_PAGE_SIZE: () => NOTIFICATIONS_PAGE_SIZE,
@@ -230,6 +250,7 @@ __export(index_exports, {
230
250
  WEIGHT_UNITS: () => WEIGHT_UNITS,
231
251
  WORKOUT_SESSION_STATUSES: () => WORKOUT_SESSION_STATUSES,
232
252
  accountExportFileName: () => accountExportFileName,
253
+ advanceLinearPeriodization: () => advanceLinearPeriodization,
233
254
  apiError: () => apiError,
234
255
  apiErrorMessage: () => apiErrorMessage,
235
256
  applyDeload: () => applyDeload,
@@ -240,6 +261,7 @@ __export(index_exports, {
240
261
  canLinkToNext: () => canLinkToNext,
241
262
  cmToFeetInches: () => cmToFeetInches,
242
263
  cmToInches: () => cmToInches,
264
+ continueLinearPeriodization: () => continueLinearPeriodization,
243
265
  countsAsWork: () => countsAsWork,
244
266
  countsForProgression: () => countsForProgression,
245
267
  customExerciseProblem: () => customExerciseProblem,
@@ -261,6 +283,7 @@ __export(index_exports, {
261
283
  isDashboardSectionId: () => isDashboardSectionId,
262
284
  isDefaultDashboardLayout: () => isDefaultDashboardLayout,
263
285
  isGoalSuggestionKey: () => isGoalSuggestionKey,
286
+ isMessagePermission: () => isMessagePermission,
264
287
  isOnboardingStepId: () => isOnboardingStepId,
265
288
  isRealtimeTopic: () => isRealtimeTopic,
266
289
  isSetKind: () => isSetKind,
@@ -268,20 +291,37 @@ __export(index_exports, {
268
291
  isWithinQuietHours: () => isWithinQuietHours,
269
292
  latestBodyWeight: () => latestBodyWeight,
270
293
  leadWorkingWeight: () => leadWorkingWeight,
294
+ linearPeriodizationProblem: () => linearPeriodizationProblem,
295
+ lpEstimate: () => lpEstimate,
296
+ lpLoadKg: () => lpLoadKg,
297
+ lpPrescription: () => lpPrescription,
298
+ lpRecommendation: () => lpRecommendation,
299
+ lpRirMidpoint: () => lpRirMidpoint,
300
+ lpSampleRir: () => lpSampleRir,
301
+ lpSetEstimateKg: () => lpSetEstimateKg,
302
+ lpStep: () => lpStep,
303
+ lpStoredSets: () => lpStoredSets,
304
+ lpWorkingSets: () => lpWorkingSets,
271
305
  matchesDeletionConfirmation: () => matchesDeletionConfirmation,
306
+ messageBodyLength: () => messageBodyLength,
272
307
  normalizeDashboardLayout: () => normalizeDashboardLayout,
273
308
  normalizeExerciseLinks: () => normalizeExerciseLinks,
274
309
  normalizeExerciseName: () => normalizeExerciseName,
310
+ normalizeMessageBody: () => normalizeMessageBody,
275
311
  normalizeSearchQuery: () => normalizeSearchQuery,
276
312
  parseRealtimeEvent: () => parseRealtimeEvent,
277
313
  primaryEquipment: () => primaryEquipment,
278
314
  requiredToFinish: () => requiredToFinish,
279
315
  resolveActiveTrainingBlock: () => resolveActiveTrainingBlock,
280
316
  resolveRoutinePlan: () => resolveRoutinePlan,
317
+ roundDownToIncrement: () => roundDownToIncrement,
318
+ roundToIncrement: () => roundToIncrement,
281
319
  routineDayLabel: () => routineDayLabel,
320
+ sameLpPosition: () => sameLpPosition,
282
321
  sessionCorrectionWindow: () => sessionCorrectionWindow,
283
322
  setKindOf: () => setKindOf,
284
323
  setLogValuesDiffer: () => setLogValuesDiffer,
324
+ startLinearPeriodization: () => startLinearPeriodization,
285
325
  summarizeBodyProgress: () => summarizeBodyProgress,
286
326
  trainingBlockCovers: () => trainingBlockCovers,
287
327
  warmUpLoad: () => warmUpLoad,
@@ -301,7 +341,9 @@ var REP_TYPES = ["FIXED", "RANGE"];
301
341
  var PROGRESSION_SCHEMES = [
302
342
  "NONE",
303
343
  "DOUBLE_PROGRESSION",
304
- "DYNAMIC_DOUBLE_PROGRESSION"
344
+ "DYNAMIC_DOUBLE_PROGRESSION",
345
+ // ROUT-17: an 8-step block by percentage of a reference max and target RIR.
346
+ "LINEAR_PERIODIZATION"
305
347
  ];
306
348
  var MUSCLE_GROUPS = [
307
349
  "PECTORAL",
@@ -884,6 +926,224 @@ function requiredToFinish(kind) {
884
926
  return k === "WORKING" || k === "DROP";
885
927
  }
886
928
 
929
+ // src/linear-periodization.ts
930
+ var LP_BLOCK_STEPS = 8;
931
+ var LP_WORKING_SETS = 3;
932
+ var LP_REST_SECONDS = 180;
933
+ var LP_REFERENCE_MAX_KG = 1e3;
934
+ var EASY = { rirMin: 2, rirMax: 3, amrap: false };
935
+ var AMRAP = { rirMin: 0, rirMax: 0, amrap: true };
936
+ var HARD = { rirMin: 1, rirMax: 2, amrap: false };
937
+ var LP_STEPS = [
938
+ { step: 1, percentage: 0.63, sets: [EASY, EASY, AMRAP] },
939
+ { step: 2, percentage: 0.66, sets: [EASY, EASY, AMRAP] },
940
+ { step: 3, percentage: 0.69, sets: [EASY, EASY, AMRAP] },
941
+ { step: 4, percentage: 0.72, sets: [EASY, EASY, AMRAP] },
942
+ { step: 5, percentage: 0.75, sets: [HARD, HARD, HARD] },
943
+ { step: 6, percentage: 0.78, sets: [HARD, HARD, HARD] },
944
+ { step: 7, percentage: 0.81, sets: [HARD, HARD, HARD] },
945
+ { step: 8, percentage: 0.85, sets: [HARD, HARD, HARD] }
946
+ ];
947
+ function lpStep(step) {
948
+ const index = Math.min(Math.max(Math.trunc(step) || 1, 1), LP_BLOCK_STEPS);
949
+ return LP_STEPS[index - 1];
950
+ }
951
+ var LP_RECOVERY = { percentage: 0.7, sets: 2, reps: 5 };
952
+ var tidy = (value) => Math.round(value * 1e6) / 1e6;
953
+ function roundToIncrement(value, increment) {
954
+ if (!(increment > 0)) return tidy(value);
955
+ const steps = value / increment;
956
+ const floor = Math.floor(steps + 1e-9);
957
+ const fraction = steps - floor;
958
+ return tidy((fraction > 0.5 + 1e-9 ? floor + 1 : floor) * increment);
959
+ }
960
+ function roundDownToIncrement(value, increment) {
961
+ if (!(increment > 0)) return tidy(value);
962
+ return tidy(Math.floor(value / increment + 1e-9) * increment);
963
+ }
964
+ function lpLoadKg(referenceMaxKg, percentage, incrementKg) {
965
+ return roundToIncrement(referenceMaxKg * percentage, incrementKg);
966
+ }
967
+ var LP_PHASES = ["BLOCK", "RECOVERY", "FINISHED"];
968
+ function startLinearPeriodization(referenceMaxKg, cycle = 1) {
969
+ return {
970
+ referenceMaxKg,
971
+ phase: "BLOCK",
972
+ step: 1,
973
+ cycle,
974
+ samples: [],
975
+ finishedAt: null,
976
+ estimatedMaxKg: null,
977
+ nextReferenceMaxKg: null
978
+ };
979
+ }
980
+ function linearPeriodizationProblem(state) {
981
+ if (!Number.isFinite(state.referenceMaxKg) || state.referenceMaxKg <= 0 || state.referenceMaxKg > LP_REFERENCE_MAX_KG) {
982
+ return "The reference max must be above 0";
983
+ }
984
+ if (!LP_PHASES.includes(state.phase)) {
985
+ return "Unknown block phase";
986
+ }
987
+ if (!Number.isInteger(state.step) || state.step < 1 || state.step > LP_BLOCK_STEPS) {
988
+ return "The block step must be between 1 and 8";
989
+ }
990
+ if (!Number.isInteger(state.cycle) || state.cycle < 1) {
991
+ return "The block number must be 1 or more";
992
+ }
993
+ if (state.phase === "RECOVERY" && !((state.nextReferenceMaxKg ?? 0) > 0)) {
994
+ return "A recovery step needs the next block's reference max";
995
+ }
996
+ return null;
997
+ }
998
+ function lpPrescription(state, incrementKg) {
999
+ if (state.phase === "FINISHED") return [];
1000
+ if (state.phase === "RECOVERY") {
1001
+ const loadKg2 = lpLoadKg(
1002
+ state.referenceMaxKg,
1003
+ LP_RECOVERY.percentage,
1004
+ incrementKg
1005
+ );
1006
+ return Array.from({ length: LP_RECOVERY.sets }, () => ({
1007
+ loadKg: loadKg2,
1008
+ target: null,
1009
+ reps: LP_RECOVERY.reps
1010
+ }));
1011
+ }
1012
+ const step = lpStep(state.step);
1013
+ const loadKg = lpLoadKg(state.referenceMaxKg, step.percentage, incrementKg);
1014
+ return step.sets.map((target) => ({ loadKg, target, reps: null }));
1015
+ }
1016
+ function lpWorkingSets(sets) {
1017
+ return sets.filter((set) => setKindOf(set) !== "WARMUP");
1018
+ }
1019
+ var LP_ESTIMATE_STEPS = [7, 8];
1020
+ var LP_ESTIMATE_MAX_REPS_TO_FAILURE = 10;
1021
+ var LP_ESTIMATE_MIN_SETS = 3;
1022
+ function lpRirMidpoint(target) {
1023
+ return (target.rirMin + target.rirMax) / 2;
1024
+ }
1025
+ function lpSampleRir(sample) {
1026
+ const target = lpStep(sample.step).sets[sample.set - 1] ?? HARD;
1027
+ return lpRirMidpoint(target);
1028
+ }
1029
+ function lpSetEstimateKg(loadKg, reps, rir) {
1030
+ return loadKg * (1 + (reps + rir) / 30);
1031
+ }
1032
+ function lpEstimate(samples, incrementKg) {
1033
+ const sets = samples.filter((sample) => LP_ESTIMATE_STEPS.includes(sample.step)).map((sample) => ({ ...sample, rir: lpSampleRir(sample) })).filter(
1034
+ (sample) => sample.reps >= 1 && sample.loadKg > 0 && sample.reps + sample.rir <= LP_ESTIMATE_MAX_REPS_TO_FAILURE
1035
+ ).map((sample) => ({
1036
+ ...sample,
1037
+ estimateKg: tidy(lpSetEstimateKg(sample.loadKg, sample.reps, sample.rir))
1038
+ })).sort((a, b) => a.step - b.step || a.set - b.set);
1039
+ if (sets.length < LP_ESTIMATE_MIN_SETS) {
1040
+ return { estimatedMaxKg: null, sets };
1041
+ }
1042
+ const values = sets.map((set) => set.estimateKg).sort((a, b) => a - b);
1043
+ const middle = Math.floor(values.length / 2);
1044
+ const median = values.length % 2 ? values[middle] : (values[middle - 1] + values[middle]) / 2;
1045
+ return { estimatedMaxKg: roundDownToIncrement(median, incrementKg), sets };
1046
+ }
1047
+ var LP_PROGRESS_THRESHOLD = 0.025;
1048
+ var LP_PROGRESS_CAP = 0.05;
1049
+ var LP_NEXT_BLOCK_KINDS = ["PROGRESS", "REPEAT", "RESET"];
1050
+ function lpRecommendation(referenceMaxKg, estimatedMaxKg, incrementKg) {
1051
+ if (!(estimatedMaxKg && estimatedMaxKg > 0) || !(referenceMaxKg > 0)) {
1052
+ return { kind: "REPEAT", nextReferenceMaxKg: referenceMaxKg, change: null };
1053
+ }
1054
+ const change = estimatedMaxKg / referenceMaxKg - 1;
1055
+ if (change >= LP_PROGRESS_THRESHOLD - 1e-9) {
1056
+ const capped = Math.min(
1057
+ estimatedMaxKg,
1058
+ referenceMaxKg * (1 + LP_PROGRESS_CAP)
1059
+ );
1060
+ const next = roundDownToIncrement(capped, incrementKg);
1061
+ return {
1062
+ kind: "PROGRESS",
1063
+ nextReferenceMaxKg: next > referenceMaxKg ? next : referenceMaxKg,
1064
+ change
1065
+ };
1066
+ }
1067
+ if (change < -LP_PROGRESS_THRESHOLD - 1e-9) {
1068
+ return {
1069
+ kind: "RESET",
1070
+ nextReferenceMaxKg: roundDownToIncrement(estimatedMaxKg, incrementKg),
1071
+ change
1072
+ };
1073
+ }
1074
+ return { kind: "REPEAT", nextReferenceMaxKg: referenceMaxKg, change };
1075
+ }
1076
+ function sameLpPosition(a, b) {
1077
+ if (!a || !b) return !a && !b;
1078
+ return a.phase === b.phase && a.step === b.step && a.cycle === b.cycle && Math.abs(a.referenceMaxKg - b.referenceMaxKg) < 1e-6;
1079
+ }
1080
+ function advanceLinearPeriodization(state, logged, context) {
1081
+ if (state.phase === "FINISHED") return null;
1082
+ const prescribed = lpPrescription(state, context.incrementKg).length;
1083
+ for (let set = 1; set <= prescribed; set += 1) {
1084
+ if (!logged.some((entry) => entry.set === set && entry.completed)) {
1085
+ return null;
1086
+ }
1087
+ }
1088
+ if (state.phase === "RECOVERY") {
1089
+ return startLinearPeriodization(
1090
+ state.nextReferenceMaxKg ?? state.referenceMaxKg,
1091
+ state.cycle + 1
1092
+ );
1093
+ }
1094
+ const samples = LP_ESTIMATE_STEPS.includes(state.step) ? [
1095
+ ...state.samples.filter((sample) => sample.step !== state.step),
1096
+ ...logged.filter((entry) => entry.completed && entry.set <= prescribed).map((entry) => ({
1097
+ sessionId: context.sessionId,
1098
+ step: state.step,
1099
+ set: entry.set,
1100
+ loadKg: entry.loadKg,
1101
+ reps: entry.reps
1102
+ }))
1103
+ ] : state.samples;
1104
+ if (state.step < LP_BLOCK_STEPS) {
1105
+ return { ...state, step: state.step + 1, samples };
1106
+ }
1107
+ return {
1108
+ ...state,
1109
+ phase: "FINISHED",
1110
+ samples,
1111
+ finishedAt: context.finishedAt,
1112
+ estimatedMaxKg: lpEstimate(samples, context.incrementKg).estimatedMaxKg,
1113
+ nextReferenceMaxKg: null
1114
+ };
1115
+ }
1116
+ function continueLinearPeriodization(state, choice) {
1117
+ if (state.phase !== "FINISHED") return null;
1118
+ if (choice.recovery) {
1119
+ return {
1120
+ ...state,
1121
+ phase: "RECOVERY",
1122
+ nextReferenceMaxKg: choice.referenceMaxKg
1123
+ };
1124
+ }
1125
+ return startLinearPeriodization(choice.referenceMaxKg, state.cycle + 1);
1126
+ }
1127
+ function lpStoredSets(state, incrementKg) {
1128
+ if (!state) {
1129
+ return Array.from({ length: LP_WORKING_SETS }, () => ({
1130
+ repType: "FIXED",
1131
+ reps: null,
1132
+ weight: null,
1133
+ rir: null,
1134
+ kind: "WORKING"
1135
+ }));
1136
+ }
1137
+ const prescription = state.phase === "FINISHED" ? lpPrescription({ ...state, phase: "BLOCK", step: LP_BLOCK_STEPS }, incrementKg) : lpPrescription(state, incrementKg);
1138
+ return prescription.map((set) => ({
1139
+ repType: "FIXED",
1140
+ reps: set.reps,
1141
+ weight: set.loadKg,
1142
+ rir: set.target ? set.target.rirMin : null,
1143
+ kind: "WORKING"
1144
+ }));
1145
+ }
1146
+
887
1147
  // src/plate-loading.ts
888
1148
  var CANONICAL_SCALE = 1e4;
889
1149
  var DENSE_CAPACITY_LIMIT = 2e5;
@@ -1516,7 +1776,9 @@ var NOTIFICATION_KINDS = [
1516
1776
  "TRAINING_PARTNER_ENCOURAGEMENT",
1517
1777
  // NOTIF-07: selected activity from a partner who currently shares it.
1518
1778
  "TRAINING_PARTNER_SESSION",
1519
- "TRAINING_PARTNER_ACHIEVEMENT"
1779
+ "TRAINING_PARTNER_ACHIEVEMENT",
1780
+ // ROUT-18: an LP exercise finished its 8-step block.
1781
+ "LINEAR_BLOCK_FINISHED"
1520
1782
  ];
1521
1783
  var NOTIFICATIONS_LOOKBACK_DAYS = 30;
1522
1784
  var NOTIFICATIONS_RETENTION_DAYS = 90;
@@ -1621,6 +1883,8 @@ var API_ERROR_MESSAGES = {
1621
1883
  REPORT_NOT_FOUND: "Report not found",
1622
1884
  REPORT_SUBJECT_NOT_FOUND: "Subject not found",
1623
1885
  REPORTED_CONTENT_GONE: "The reported content no longer exists",
1886
+ CONVERSATION_NOT_FOUND: "Conversation not found",
1887
+ MESSAGE_NOT_FOUND: "Message not found",
1624
1888
  SHARE_LINK_NOT_FOUND: "Share link not found",
1625
1889
  LINK_NOT_FOUND: "Link not found",
1626
1890
  SHARED_SESSION_NOT_FOUND: "Shared session not found",
@@ -1701,6 +1965,14 @@ var API_ERROR_MESSAGES = {
1701
1965
  SWAP_SESSION_NOT_ACTIVE: "Exercises can only be swapped in an active session",
1702
1966
  SWAP_SETS_COMPLETED: "This exercise already has completed sets. Swap it before completing any.",
1703
1967
  NOTES_DISCARDED_SESSION: "A discarded workout cannot take notes",
1968
+ // ── Linear periodization (ROUT-17 to ROUT-19) ──────────────────────────
1969
+ LINEAR_BLOCK_INVALID: "This exercise's block is not valid: {reason}",
1970
+ LINEAR_BLOCK_EXTRA_SET: "An exercise on an 8-week block takes no extra sets",
1971
+ LINEAR_BLOCK_KIND_FIXED: "The working sets of an 8-week block cannot change kind",
1972
+ LINEAR_BLOCK_LOAD_FIXED: "The load of an 8-week block is prescribed and cannot be changed",
1973
+ LINEAR_BLOCK_SWAP_ROUTINE: "An exercise on an 8-week block can be swapped for this workout only",
1974
+ LINEAR_BLOCK_NOT_LINEAR: "This exercise is not on an 8-week block",
1975
+ LINEAR_BLOCK_NOT_FINISHED: "This exercise's block has not finished, so there is nothing to choose yet",
1704
1976
  // ── Corrections (LIVE-17) ──────────────────────────────────────────────
1705
1977
  CORRECTION_NOT_COMPLETED: "Only a completed workout can be corrected",
1706
1978
  CORRECTION_NOT_LATEST: "Only your most recent workout can be corrected",
@@ -1746,6 +2018,14 @@ var API_ERROR_MESSAGES = {
1746
2018
  PARTNERS_MAX: "An account can have at most {max} training partners.",
1747
2019
  ENCOURAGEMENTS_MAX: "You can send this partner at most {max} encouragements in 24 hours.",
1748
2020
  ENCOURAGEMENT_RETRY: "Please retry the encouragement.",
2021
+ // ── Direct messages (MSG-01) ───────────────────────────────────────────
2022
+ MESSAGE_SELF: "You cannot message yourself",
2023
+ MESSAGE_LENGTH: "A message must not be empty and may be at most {max} characters.",
2024
+ MESSAGES_PER_MINUTE: "You can send at most {max} messages a minute.",
2025
+ CONVERSATIONS_PER_DAY: "You can start at most {max} new conversations a day.",
2026
+ MESSAGE_NOT_ADMITTED: "This member is not taking messages from you.",
2027
+ CONVERSATION_CLOSED: "This conversation can no longer receive messages.",
2028
+ MESSAGING_UNAVAILABLE: "Messaging is not available for this account.",
1749
2029
  // ── Account and settings ───────────────────────────────────────────────
1750
2030
  ACCOUNT_CONFLICT: "Account conflict for this email",
1751
2031
  ACCOUNT_DELETE_CONFIRMATION: "Type your username exactly as it appears to confirm.",
@@ -1798,7 +2078,7 @@ var RECENT_SEARCH_KINDS = ["MEMBER", "EXERCISE", "ROUTINE", "WORKOUT"];
1798
2078
 
1799
2079
  // src/realtime.ts
1800
2080
  var REALTIME_STREAM_PATH = "/realtime/stream";
1801
- var REALTIME_TOPICS = ["notifications"];
2081
+ var REALTIME_TOPICS = ["notifications", "conversations"];
1802
2082
  var REALTIME_HEARTBEAT_MS = 25e3;
1803
2083
  var REALTIME_MAX_STREAM_MS = 14 * 6e4;
1804
2084
  var REALTIME_MAX_STREAMS_PER_MEMBER = 5;
@@ -1823,6 +2103,28 @@ function parseRealtimeEvent(data) {
1823
2103
  }
1824
2104
  return null;
1825
2105
  }
2106
+
2107
+ // src/messages.ts
2108
+ var MESSAGE_PERMISSIONS = ["FOLLOWED", "NOBODY"];
2109
+ var DEFAULT_MESSAGE_PERMISSION = "FOLLOWED";
2110
+ var MESSAGE_BODY_MAX = 2e3;
2111
+ var MESSAGES_PER_MINUTE_MAX = 30;
2112
+ var NEW_CONVERSATIONS_PER_DAY_MAX = 20;
2113
+ var CONVERSATIONS_PAGE_SIZE = 20;
2114
+ var MESSAGES_PAGE_SIZE = 30;
2115
+ function messageBodyLength(body) {
2116
+ return Array.from(body).length;
2117
+ }
2118
+ function normalizeMessageBody(body) {
2119
+ const text = body.replace(/\r\n?/g, "\n").trim();
2120
+ if (text.length === 0 || messageBodyLength(text) > MESSAGE_BODY_MAX) {
2121
+ return null;
2122
+ }
2123
+ return text;
2124
+ }
2125
+ function isMessagePermission(value) {
2126
+ return MESSAGE_PERMISSIONS.includes(value);
2127
+ }
1826
2128
  // Annotate the CommonJS export names for ESM import in node:
1827
2129
  0 && (module.exports = {
1828
2130
  ACCOUNT_EXPORT_FORMAT,
@@ -1857,6 +2159,7 @@ function parseRealtimeEvent(data) {
1857
2159
  COMEBACK_REQUIRED_ACTIVE_DAYS,
1858
2160
  COMEBACK_SESSION_LOOKBACK,
1859
2161
  COMEBACK_WINDOW_DAYS,
2162
+ CONVERSATIONS_PAGE_SIZE,
1860
2163
  CUSTOM_EXERCISES_MAX,
1861
2164
  CUSTOM_EXERCISE_NAME_MAX,
1862
2165
  CUSTOM_EXERCISE_NOTE_MAX,
@@ -1865,6 +2168,7 @@ function parseRealtimeEvent(data) {
1865
2168
  DASHBOARD_SECTION_IDS,
1866
2169
  DEFAULT_DASHBOARD_LAYOUT,
1867
2170
  DEFAULT_LOCALE,
2171
+ DEFAULT_MESSAGE_PERMISSION,
1868
2172
  DEFAULT_WEEK_STARTS_ON,
1869
2173
  DELOAD_DEFAULT_DAYS,
1870
2174
  DELOAD_DEFAULT_LOAD_REDUCTION,
@@ -1899,10 +2203,27 @@ function parseRealtimeEvent(data) {
1899
2203
  INCHES_PER_FOOT,
1900
2204
  LENGTH_UNITS,
1901
2205
  LIMITED_SHORTFALL_KG,
2206
+ LP_BLOCK_STEPS,
2207
+ LP_ESTIMATE_MAX_REPS_TO_FAILURE,
2208
+ LP_ESTIMATE_MIN_SETS,
2209
+ LP_ESTIMATE_STEPS,
2210
+ LP_NEXT_BLOCK_KINDS,
2211
+ LP_PHASES,
2212
+ LP_PROGRESS_CAP,
2213
+ LP_PROGRESS_THRESHOLD,
2214
+ LP_RECOVERY,
2215
+ LP_REFERENCE_MAX_KG,
2216
+ LP_REST_SECONDS,
2217
+ LP_STEPS,
2218
+ LP_WORKING_SETS,
1902
2219
  MAX_SETS_PER_EXERCISE,
1903
2220
  MEASURABLE_GOALS_MAX,
1904
2221
  MEASURABLE_GOAL_DIRECTIONS,
1905
2222
  MEASURABLE_GOAL_TYPES,
2223
+ MESSAGES_PAGE_SIZE,
2224
+ MESSAGES_PER_MINUTE_MAX,
2225
+ MESSAGE_BODY_MAX,
2226
+ MESSAGE_PERMISSIONS,
1906
2227
  MINUTES_IN_DAY,
1907
2228
  MODERATION_ACTION_KINDS,
1908
2229
  MODERATION_HISTORY_PAGE_SIZE,
@@ -1910,6 +2231,7 @@ function parseRealtimeEvent(data) {
1910
2231
  MODERATION_QUEUE_PAGE_SIZE,
1911
2232
  MOVEMENT_PATTERNS,
1912
2233
  MUSCLE_GROUPS,
2234
+ NEW_CONVERSATIONS_PER_DAY_MAX,
1913
2235
  NOTIFICATIONS_LIST_LIMIT,
1914
2236
  NOTIFICATIONS_LOOKBACK_DAYS,
1915
2237
  NOTIFICATIONS_PAGE_SIZE,
@@ -2035,6 +2357,7 @@ function parseRealtimeEvent(data) {
2035
2357
  WEIGHT_UNITS,
2036
2358
  WORKOUT_SESSION_STATUSES,
2037
2359
  accountExportFileName,
2360
+ advanceLinearPeriodization,
2038
2361
  apiError,
2039
2362
  apiErrorMessage,
2040
2363
  applyDeload,
@@ -2045,6 +2368,7 @@ function parseRealtimeEvent(data) {
2045
2368
  canLinkToNext,
2046
2369
  cmToFeetInches,
2047
2370
  cmToInches,
2371
+ continueLinearPeriodization,
2048
2372
  countsAsWork,
2049
2373
  countsForProgression,
2050
2374
  customExerciseProblem,
@@ -2066,6 +2390,7 @@ function parseRealtimeEvent(data) {
2066
2390
  isDashboardSectionId,
2067
2391
  isDefaultDashboardLayout,
2068
2392
  isGoalSuggestionKey,
2393
+ isMessagePermission,
2069
2394
  isOnboardingStepId,
2070
2395
  isRealtimeTopic,
2071
2396
  isSetKind,
@@ -2073,20 +2398,37 @@ function parseRealtimeEvent(data) {
2073
2398
  isWithinQuietHours,
2074
2399
  latestBodyWeight,
2075
2400
  leadWorkingWeight,
2401
+ linearPeriodizationProblem,
2402
+ lpEstimate,
2403
+ lpLoadKg,
2404
+ lpPrescription,
2405
+ lpRecommendation,
2406
+ lpRirMidpoint,
2407
+ lpSampleRir,
2408
+ lpSetEstimateKg,
2409
+ lpStep,
2410
+ lpStoredSets,
2411
+ lpWorkingSets,
2076
2412
  matchesDeletionConfirmation,
2413
+ messageBodyLength,
2077
2414
  normalizeDashboardLayout,
2078
2415
  normalizeExerciseLinks,
2079
2416
  normalizeExerciseName,
2417
+ normalizeMessageBody,
2080
2418
  normalizeSearchQuery,
2081
2419
  parseRealtimeEvent,
2082
2420
  primaryEquipment,
2083
2421
  requiredToFinish,
2084
2422
  resolveActiveTrainingBlock,
2085
2423
  resolveRoutinePlan,
2424
+ roundDownToIncrement,
2425
+ roundToIncrement,
2086
2426
  routineDayLabel,
2427
+ sameLpPosition,
2087
2428
  sessionCorrectionWindow,
2088
2429
  setKindOf,
2089
2430
  setLogValuesDiffer,
2431
+ startLinearPeriodization,
2090
2432
  summarizeBodyProgress,
2091
2433
  trainingBlockCovers,
2092
2434
  warmUpLoad,