trainbud 0.5.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.
Files changed (274) hide show
  1. package/.env.example +11 -0
  2. package/CHANGELOG.md +675 -0
  3. package/LICENSE +21 -0
  4. package/QUICKSTART.md +182 -0
  5. package/README.md +322 -0
  6. package/dist/appDb.d.ts +83 -0
  7. package/dist/appDb.d.ts.map +1 -0
  8. package/dist/appDb.js +239 -0
  9. package/dist/appDb.js.map +1 -0
  10. package/dist/check.d.ts +12 -0
  11. package/dist/check.d.ts.map +1 -0
  12. package/dist/check.js +230 -0
  13. package/dist/check.js.map +1 -0
  14. package/dist/cli.d.ts +3 -0
  15. package/dist/cli.d.ts.map +1 -0
  16. package/dist/cli.js +457 -0
  17. package/dist/cli.js.map +1 -0
  18. package/dist/config.d.ts +61 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +241 -0
  21. package/dist/config.js.map +1 -0
  22. package/dist/dashboard.d.ts +8 -0
  23. package/dist/dashboard.d.ts.map +1 -0
  24. package/dist/dashboard.js +1004 -0
  25. package/dist/dashboard.js.map +1 -0
  26. package/dist/dashboardCharts.d.ts +99 -0
  27. package/dist/dashboardCharts.d.ts.map +1 -0
  28. package/dist/dashboardCharts.js +317 -0
  29. package/dist/dashboardCharts.js.map +1 -0
  30. package/dist/dashboardData.d.ts +89 -0
  31. package/dist/dashboardData.d.ts.map +1 -0
  32. package/dist/dashboardData.js +228 -0
  33. package/dist/dashboardData.js.map +1 -0
  34. package/dist/detect/baseline.d.ts +17 -0
  35. package/dist/detect/baseline.d.ts.map +1 -0
  36. package/dist/detect/baseline.js +62 -0
  37. package/dist/detect/baseline.js.map +1 -0
  38. package/dist/detect/countdown.d.ts +30 -0
  39. package/dist/detect/countdown.d.ts.map +1 -0
  40. package/dist/detect/countdown.js +79 -0
  41. package/dist/detect/countdown.js.map +1 -0
  42. package/dist/detect/detectors.d.ts +20 -0
  43. package/dist/detect/detectors.d.ts.map +1 -0
  44. package/dist/detect/detectors.js +291 -0
  45. package/dist/detect/detectors.js.map +1 -0
  46. package/dist/detect/findings.d.ts +29 -0
  47. package/dist/detect/findings.d.ts.map +1 -0
  48. package/dist/detect/findings.js +2 -0
  49. package/dist/detect/findings.js.map +1 -0
  50. package/dist/detect/forecast.d.ts +33 -0
  51. package/dist/detect/forecast.d.ts.map +1 -0
  52. package/dist/detect/forecast.js +122 -0
  53. package/dist/detect/forecast.js.map +1 -0
  54. package/dist/detect/index.d.ts +50 -0
  55. package/dist/detect/index.d.ts.map +1 -0
  56. package/dist/detect/index.js +114 -0
  57. package/dist/detect/index.js.map +1 -0
  58. package/dist/detect/sleepQuality.d.ts +21 -0
  59. package/dist/detect/sleepQuality.d.ts.map +1 -0
  60. package/dist/detect/sleepQuality.js +164 -0
  61. package/dist/detect/sleepQuality.js.map +1 -0
  62. package/dist/detect/trimp.d.ts +22 -0
  63. package/dist/detect/trimp.d.ts.map +1 -0
  64. package/dist/detect/trimp.js +77 -0
  65. package/dist/detect/trimp.js.map +1 -0
  66. package/dist/detect/week.d.ts +31 -0
  67. package/dist/detect/week.d.ts.map +1 -0
  68. package/dist/detect/week.js +184 -0
  69. package/dist/detect/week.js.map +1 -0
  70. package/dist/garmin/auth.d.ts +10 -0
  71. package/dist/garmin/auth.d.ts.map +1 -0
  72. package/dist/garmin/auth.js +81 -0
  73. package/dist/garmin/auth.js.map +1 -0
  74. package/dist/garmin/cache.d.ts +42 -0
  75. package/dist/garmin/cache.d.ts.map +1 -0
  76. package/dist/garmin/cache.js +141 -0
  77. package/dist/garmin/cache.js.map +1 -0
  78. package/dist/garmin/client.d.ts +22 -0
  79. package/dist/garmin/client.d.ts.map +1 -0
  80. package/dist/garmin/client.js +237 -0
  81. package/dist/garmin/client.js.map +1 -0
  82. package/dist/garmin/daily.d.ts +24 -0
  83. package/dist/garmin/daily.d.ts.map +1 -0
  84. package/dist/garmin/daily.js +96 -0
  85. package/dist/garmin/daily.js.map +1 -0
  86. package/dist/garmin/garminApiTypes.d.ts +70 -0
  87. package/dist/garmin/garminApiTypes.d.ts.map +1 -0
  88. package/dist/garmin/garminApiTypes.js +3 -0
  89. package/dist/garmin/garminApiTypes.js.map +1 -0
  90. package/dist/garmin/garminConnect.d.ts +17 -0
  91. package/dist/garmin/garminConnect.d.ts.map +1 -0
  92. package/dist/garmin/garminConnect.js +5 -0
  93. package/dist/garmin/garminConnect.js.map +1 -0
  94. package/dist/garmin/partial.d.ts +32 -0
  95. package/dist/garmin/partial.d.ts.map +1 -0
  96. package/dist/garmin/partial.js +38 -0
  97. package/dist/garmin/partial.js.map +1 -0
  98. package/dist/garmin/rawApi.d.ts +59 -0
  99. package/dist/garmin/rawApi.d.ts.map +1 -0
  100. package/dist/garmin/rawApi.js +113 -0
  101. package/dist/garmin/rawApi.js.map +1 -0
  102. package/dist/garmin/types.d.ts +85 -0
  103. package/dist/garmin/types.d.ts.map +1 -0
  104. package/dist/garmin/types.js +11 -0
  105. package/dist/garmin/types.js.map +1 -0
  106. package/dist/history/capture.d.ts +12 -0
  107. package/dist/history/capture.d.ts.map +1 -0
  108. package/dist/history/capture.js +63 -0
  109. package/dist/history/capture.js.map +1 -0
  110. package/dist/history/context.d.ts +51 -0
  111. package/dist/history/context.d.ts.map +1 -0
  112. package/dist/history/context.js +122 -0
  113. package/dist/history/context.js.map +1 -0
  114. package/dist/history/fallback.d.ts +72 -0
  115. package/dist/history/fallback.d.ts.map +1 -0
  116. package/dist/history/fallback.js +185 -0
  117. package/dist/history/fallback.js.map +1 -0
  118. package/dist/history/ingest.d.ts +44 -0
  119. package/dist/history/ingest.d.ts.map +1 -0
  120. package/dist/history/ingest.js +347 -0
  121. package/dist/history/ingest.js.map +1 -0
  122. package/dist/history/scheduler.d.ts +20 -0
  123. package/dist/history/scheduler.d.ts.map +1 -0
  124. package/dist/history/scheduler.js +54 -0
  125. package/dist/history/scheduler.js.map +1 -0
  126. package/dist/history/schema.d.ts +41 -0
  127. package/dist/history/schema.d.ts.map +1 -0
  128. package/dist/history/schema.js +91 -0
  129. package/dist/history/schema.js.map +1 -0
  130. package/dist/history/store.d.ts +112 -0
  131. package/dist/history/store.d.ts.map +1 -0
  132. package/dist/history/store.js +282 -0
  133. package/dist/history/store.js.map +1 -0
  134. package/dist/httpServer.d.ts +55 -0
  135. package/dist/httpServer.d.ts.map +1 -0
  136. package/dist/httpServer.js +1027 -0
  137. package/dist/httpServer.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.d.ts.map +1 -0
  140. package/dist/index.js +11 -0
  141. package/dist/index.js.map +1 -0
  142. package/dist/mcpConfig.d.ts +56 -0
  143. package/dist/mcpConfig.d.ts.map +1 -0
  144. package/dist/mcpConfig.js +106 -0
  145. package/dist/mcpConfig.js.map +1 -0
  146. package/dist/pairApi.d.ts +14 -0
  147. package/dist/pairApi.d.ts.map +1 -0
  148. package/dist/pairApi.js +38 -0
  149. package/dist/pairApi.js.map +1 -0
  150. package/dist/paths.d.ts +45 -0
  151. package/dist/paths.d.ts.map +1 -0
  152. package/dist/paths.js +105 -0
  153. package/dist/paths.js.map +1 -0
  154. package/dist/profile.d.ts +123 -0
  155. package/dist/profile.d.ts.map +1 -0
  156. package/dist/profile.js +327 -0
  157. package/dist/profile.js.map +1 -0
  158. package/dist/promptApi.d.ts +43 -0
  159. package/dist/promptApi.d.ts.map +1 -0
  160. package/dist/promptApi.js +328 -0
  161. package/dist/promptApi.js.map +1 -0
  162. package/dist/promptSuggestions.d.ts +16 -0
  163. package/dist/promptSuggestions.d.ts.map +1 -0
  164. package/dist/promptSuggestions.js +141 -0
  165. package/dist/promptSuggestions.js.map +1 -0
  166. package/dist/selfTest.d.ts +53 -0
  167. package/dist/selfTest.d.ts.map +1 -0
  168. package/dist/selfTest.js +225 -0
  169. package/dist/selfTest.js.map +1 -0
  170. package/dist/server.d.ts +10 -0
  171. package/dist/server.d.ts.map +1 -0
  172. package/dist/server.js +67 -0
  173. package/dist/server.js.map +1 -0
  174. package/dist/setup.d.ts +2 -0
  175. package/dist/setup.d.ts.map +1 -0
  176. package/dist/setup.js +210 -0
  177. package/dist/setup.js.map +1 -0
  178. package/dist/toolErrors.d.ts +2 -0
  179. package/dist/toolErrors.d.ts.map +1 -0
  180. package/dist/toolErrors.js +18 -0
  181. package/dist/toolErrors.js.map +1 -0
  182. package/dist/tools/activities.d.ts +18 -0
  183. package/dist/tools/activities.d.ts.map +1 -0
  184. package/dist/tools/activities.js +182 -0
  185. package/dist/tools/activities.js.map +1 -0
  186. package/dist/tools/bodyComposition.d.ts +10 -0
  187. package/dist/tools/bodyComposition.d.ts.map +1 -0
  188. package/dist/tools/bodyComposition.js +139 -0
  189. package/dist/tools/bodyComposition.js.map +1 -0
  190. package/dist/tools/context.d.ts +8 -0
  191. package/dist/tools/context.d.ts.map +1 -0
  192. package/dist/tools/context.js +148 -0
  193. package/dist/tools/context.js.map +1 -0
  194. package/dist/tools/findings.d.ts +7 -0
  195. package/dist/tools/findings.d.ts.map +1 -0
  196. package/dist/tools/findings.js +51 -0
  197. package/dist/tools/findings.js.map +1 -0
  198. package/dist/tools/heartRate.d.ts +10 -0
  199. package/dist/tools/heartRate.d.ts.map +1 -0
  200. package/dist/tools/heartRate.js +105 -0
  201. package/dist/tools/heartRate.js.map +1 -0
  202. package/dist/tools/index.d.ts +66 -0
  203. package/dist/tools/index.d.ts.map +1 -0
  204. package/dist/tools/index.js +101 -0
  205. package/dist/tools/index.js.map +1 -0
  206. package/dist/tools/payloads.d.ts +148 -0
  207. package/dist/tools/payloads.d.ts.map +1 -0
  208. package/dist/tools/payloads.js +2 -0
  209. package/dist/tools/payloads.js.map +1 -0
  210. package/dist/tools/recovery.d.ts +31 -0
  211. package/dist/tools/recovery.d.ts.map +1 -0
  212. package/dist/tools/recovery.js +350 -0
  213. package/dist/tools/recovery.js.map +1 -0
  214. package/dist/tools/sleep.d.ts +10 -0
  215. package/dist/tools/sleep.d.ts.map +1 -0
  216. package/dist/tools/sleep.js +124 -0
  217. package/dist/tools/sleep.js.map +1 -0
  218. package/dist/tools/stress.d.ts +11 -0
  219. package/dist/tools/stress.d.ts.map +1 -0
  220. package/dist/tools/stress.js +108 -0
  221. package/dist/tools/stress.js.map +1 -0
  222. package/dist/tools/trainingInsights.d.ts +15 -0
  223. package/dist/tools/trainingInsights.d.ts.map +1 -0
  224. package/dist/tools/trainingInsights.js +79 -0
  225. package/dist/tools/trainingInsights.js.map +1 -0
  226. package/dist/tools/types.d.ts +12 -0
  227. package/dist/tools/types.d.ts.map +1 -0
  228. package/dist/tools/types.js +2 -0
  229. package/dist/tools/types.js.map +1 -0
  230. package/dist/tools/vo2Max.d.ts +11 -0
  231. package/dist/tools/vo2Max.d.ts.map +1 -0
  232. package/dist/tools/vo2Max.js +110 -0
  233. package/dist/tools/vo2Max.js.map +1 -0
  234. package/dist/tools/week.d.ts +12 -0
  235. package/dist/tools/week.d.ts.map +1 -0
  236. package/dist/tools/week.js +74 -0
  237. package/dist/tools/week.js.map +1 -0
  238. package/dist/usage.d.ts +104 -0
  239. package/dist/usage.d.ts.map +1 -0
  240. package/dist/usage.js +247 -0
  241. package/dist/usage.js.map +1 -0
  242. package/dist/utils/batch.d.ts +2 -0
  243. package/dist/utils/batch.d.ts.map +1 -0
  244. package/dist/utils/batch.js +18 -0
  245. package/dist/utils/batch.js.map +1 -0
  246. package/dist/utils/helpers.d.ts +32 -0
  247. package/dist/utils/helpers.d.ts.map +1 -0
  248. package/dist/utils/helpers.js +188 -0
  249. package/dist/utils/helpers.js.map +1 -0
  250. package/dist/utils/jsonFile.d.ts +19 -0
  251. package/dist/utils/jsonFile.d.ts.map +1 -0
  252. package/dist/utils/jsonFile.js +24 -0
  253. package/dist/utils/jsonFile.js.map +1 -0
  254. package/dist/utils/logger.d.ts +4 -0
  255. package/dist/utils/logger.d.ts.map +1 -0
  256. package/dist/utils/logger.js +97 -0
  257. package/dist/utils/logger.js.map +1 -0
  258. package/dist/utils/secretFile.d.ts +15 -0
  259. package/dist/utils/secretFile.d.ts.map +1 -0
  260. package/dist/utils/secretFile.js +47 -0
  261. package/dist/utils/secretFile.js.map +1 -0
  262. package/dist/utils/stdio.d.ts +10 -0
  263. package/dist/utils/stdio.d.ts.map +1 -0
  264. package/dist/utils/stdio.js +70 -0
  265. package/dist/utils/stdio.js.map +1 -0
  266. package/dist/version.d.ts +2 -0
  267. package/dist/version.d.ts.map +1 -0
  268. package/dist/version.js +7 -0
  269. package/dist/version.js.map +1 -0
  270. package/dist/watchApi.d.ts +264 -0
  271. package/dist/watchApi.d.ts.map +1 -0
  272. package/dist/watchApi.js +333 -0
  273. package/dist/watchApi.js.map +1 -0
  274. package/package.json +77 -0
@@ -0,0 +1,291 @@
1
+ import { buildBaseline, meanOf, median, robustZ } from "./baseline.js";
2
+ import { dailyTrimp, estimateHrProfile } from "./trimp.js";
3
+ // SECTION: Detectors
4
+ //
5
+ // One function per signal, each pure, each returning a Finding or null. Nothing
6
+ // here calls a model: a language model asked to spot a trend will phrase a
7
+ // hallucinated one exactly as confidently as a real one, and this project's
8
+ // entire bug history is numbers being silently wrong. Code decides; the model
9
+ // only ever phrases what code decided.
10
+ const BASELINE_DAYS = 28;
11
+ const RECENT_DAYS = 3;
12
+ const SLEEP_WEEK_DAYS = 7;
13
+ /** Robust-z at which a run of days stops looking like night-to-night noise. */
14
+ const ELEVATION_Z = 2;
15
+ /**
16
+ * A statistically large move can still be physiologically meaningless. Without
17
+ * this floor, a very consistent sleeper gets flagged over 1 bpm.
18
+ */
19
+ const ELEVATION_MIN_BPM = 3;
20
+ const SLEEP_DEBT_HOURS = 3;
21
+ /**
22
+ * How far below the median a night has to fall before it counts as short at
23
+ * all. Half an hour, or the user's own spread, whichever is larger -- see the
24
+ * note in detectSleepDebt for the measurement that made this necessary.
25
+ */
26
+ const SLEEP_NOISE_FLOOR_SECONDS = 30 * 60;
27
+ const HRV_DROP_Z = -2;
28
+ /**
29
+ * The recent window is held out of the baseline. Left in, a three-day
30
+ * elevation lifts the very median it is being measured against, and the longer
31
+ * something persists the more normal it looks -- which is backwards.
32
+ *
33
+ * Split by DATE, not by position. `slice(-3)` takes the last three points, and
34
+ * three points are only three days when the store has no holes -- which it
35
+ * routinely does, because a day only gets a row when the watch was actually
36
+ * worn. Take a user who wore the watch for three days in June, stopped, and
37
+ * picked it up again in August: the last three points spanned two months and a
38
+ * resting heart rate elevated in June was reported as "3 days running" today.
39
+ * Every detector that calls this inherited the same error, and the finding it
40
+ * produced was about a run of days that never happened.
41
+ */
42
+ function split(points, recentDays, now) {
43
+ const cutoff = now.startOf("day").minus({ days: recentDays }).toISODate() ?? "";
44
+ const baselinePoints = [];
45
+ const recentPoints = [];
46
+ for (const point of points) {
47
+ if (point.date > cutoff) {
48
+ recentPoints.push(point);
49
+ }
50
+ else {
51
+ baselinePoints.push(point);
52
+ }
53
+ }
54
+ return { baselinePoints, recentPoints };
55
+ }
56
+ function round(value, places = 1) {
57
+ const factor = 10 ** places;
58
+ return Math.round(value * factor) / factor;
59
+ }
60
+ function lastDate(points, fallback) {
61
+ return points.at(-1)?.date ?? fallback;
62
+ }
63
+ /**
64
+ * How far this person's recent resting heart rate sits above their own median,
65
+ * whether or not that is far enough to be a finding.
66
+ *
67
+ * Split out because two things need it and only one of them is a threshold. The
68
+ * detector fires at a fixed bar; the colour on a watch card is graded against
69
+ * the band the user chose, which may be tighter or looser. Deriving the colour
70
+ * from "did a finding fire" would mean the card could only ever be green or
71
+ * red, and would move whenever the detector's own constants moved.
72
+ *
73
+ * Null when there is not enough history to have a baseline at all -- which is
74
+ * different from a delta of zero, and has to stay different.
75
+ */
76
+ export function restingHrDeltaBpm(input) {
77
+ const points = input.series("resting_hr", BASELINE_DAYS + RECENT_DAYS);
78
+ const { baselinePoints, recentPoints } = split(points, RECENT_DAYS, input.now);
79
+ if (recentPoints.length === 0) {
80
+ return null;
81
+ }
82
+ const baseline = buildBaseline(baselinePoints);
83
+ const recentMean = meanOf(recentPoints);
84
+ if (!baseline || recentMean === null) {
85
+ return null;
86
+ }
87
+ return round(recentMean - baseline.median);
88
+ }
89
+ export function detectRestingHrElevation(input) {
90
+ const points = input.series("resting_hr", BASELINE_DAYS + RECENT_DAYS);
91
+ const { baselinePoints, recentPoints } = split(points, RECENT_DAYS, input.now);
92
+ if (recentPoints.length < RECENT_DAYS) {
93
+ return null;
94
+ }
95
+ const baseline = buildBaseline(baselinePoints);
96
+ if (!baseline) {
97
+ return null;
98
+ }
99
+ // Every one of the recent days must clear both bars. A single normal morning
100
+ // inside the run means it is not a run.
101
+ const elevated = recentPoints.every((point) => {
102
+ const z = robustZ(point.value, baseline);
103
+ return z !== null && z >= ELEVATION_Z && point.value - baseline.median >= ELEVATION_MIN_BPM;
104
+ });
105
+ if (!elevated) {
106
+ return null;
107
+ }
108
+ const recentMean = meanOf(recentPoints);
109
+ if (recentMean === null) {
110
+ return null;
111
+ }
112
+ const deltaBpm = round(recentMean - baseline.median);
113
+ return {
114
+ kind: "rhr_elevated",
115
+ severity: deltaBpm >= 6 ? "warn" : "notice",
116
+ date: lastDate(recentPoints, input.now.toISODate() ?? ""),
117
+ headline: `Resting heart rate ${deltaBpm} bpm above your ${baseline.count}-day baseline, ${RECENT_DAYS} days running`,
118
+ detail: "A run like this usually means the last few sessions have not been absorbed yet. Easy training or a rest day is the low-risk call until it settles.",
119
+ values: {
120
+ days: RECENT_DAYS,
121
+ recentBpm: round(recentMean),
122
+ baselineBpm: round(baseline.median),
123
+ deltaBpm,
124
+ },
125
+ };
126
+ }
127
+ export function detectSleepDebt(input) {
128
+ const points = input.series("sleep_seconds", BASELINE_DAYS + SLEEP_WEEK_DAYS);
129
+ const { baselinePoints, recentPoints } = split(points, SLEEP_WEEK_DAYS, input.now);
130
+ // A week with most of its nights missing is not a week with a deficit.
131
+ if (recentPoints.length < 5) {
132
+ return null;
133
+ }
134
+ const baseline = buildBaseline(baselinePoints);
135
+ if (!baseline) {
136
+ return null;
137
+ }
138
+ // Only shortfalls count. A long Saturday does not repay a short Tuesday --
139
+ // netting them out is how a chronically short week reads as fine.
140
+ //
141
+ // But shortfalls have to be measured against a floor, not against the median.
142
+ // Half of anyone's nights fall below their own median by construction, so
143
+ // summing one-sided gaps against it accumulates "debt" out of ordinary
144
+ // variation: for a symmetric sleeper the expected shortfall is about 0.4x the
145
+ // spread per night, so a week collects ~2.8x the spread before anything has
146
+ // actually gone wrong. Measured over 2000 synthetic steady sleepers with no
147
+ // deficit whatsoever, this fired on 5.7% of weeks at half an hour of
148
+ // night-to-night variation, 27% at three quarters of an hour, and 46.7% at a
149
+ // full hour -- which is an ordinary amount of variation for an ordinary
150
+ // person. Nearly half of all "sleep debt" findings were noise, and every test
151
+ // here used a perfectly constant series, where the spread is zero and the bias
152
+ // cannot appear.
153
+ //
154
+ // The floor is the user's own spread, with a half-hour minimum so a
155
+ // metronomic sleeper is not held to the minute. A night inside normal
156
+ // variation contributes nothing; a night genuinely short still contributes
157
+ // everything below the floor.
158
+ //
159
+ // Re-measured after the change, same 2000 trials: false positives fall to
160
+ // 0.0 / 1.6 / 8.0 / 14.4 / 23.7 percent across the same spreads, while a
161
+ // sleeper genuinely an hour short every night is still caught 67.9% of the
162
+ // time, 1.5 h short 93.0%, and 2 h short 99.7%. The sensitivity that matters
163
+ // is kept; nearly all of the noise is gone.
164
+ const floor = baseline.median - Math.max(SLEEP_NOISE_FLOOR_SECONDS, baseline.mad);
165
+ const debtSeconds = recentPoints.reduce((total, point) => {
166
+ return total + Math.max(0, floor - point.value);
167
+ }, 0);
168
+ const debtHours = round(debtSeconds / 3600);
169
+ if (debtHours < SLEEP_DEBT_HOURS) {
170
+ return null;
171
+ }
172
+ return {
173
+ kind: "sleep_debt",
174
+ severity: debtHours >= 6 ? "warn" : "notice",
175
+ date: lastDate(recentPoints, input.now.toISODate() ?? ""),
176
+ headline: `${debtHours} h of sleep short of your usual ${round(baseline.median / 3600)} h over the last ${recentPoints.length} nights`,
177
+ detail: "Short weeks blunt what hard sessions give back. Worth protecting the next few nights before the next quality session.",
178
+ values: {
179
+ debtHours,
180
+ nights: recentPoints.length,
181
+ baselineHours: round(baseline.median / 3600),
182
+ },
183
+ };
184
+ }
185
+ export function detectHrvTrendBreak(input) {
186
+ const points = input.series("hrv_overnight", BASELINE_DAYS + RECENT_DAYS);
187
+ const { baselinePoints, recentPoints } = split(points, RECENT_DAYS, input.now);
188
+ if (recentPoints.length < RECENT_DAYS) {
189
+ return null;
190
+ }
191
+ const baseline = buildBaseline(baselinePoints);
192
+ if (!baseline) {
193
+ return null;
194
+ }
195
+ // The median of the recent nights, not their mean. HRV is noisy enough that
196
+ // one low reading says almost nothing, and a mean lets a single bad night
197
+ // drag the window past the threshold on its own -- which is the very outlier
198
+ // problem the baseline uses a median to avoid. With a median, two of the
199
+ // three nights have to be low before this fires.
200
+ const recentMedian = median(recentPoints.map((point) => point.value));
201
+ if (recentMedian === null) {
202
+ return null;
203
+ }
204
+ const z = robustZ(recentMedian, baseline);
205
+ if (z === null || z > HRV_DROP_Z) {
206
+ return null;
207
+ }
208
+ return buildHrvFinding(recentPoints, recentMedian, baseline, input);
209
+ }
210
+ function buildHrvFinding(recentPoints, recentMedian, baseline, input) {
211
+ const dropPercent = round(((baseline.median - recentMedian) / baseline.median) * 100);
212
+ return {
213
+ kind: "hrv_trend_break",
214
+ severity: dropPercent >= 20 ? "warn" : "notice",
215
+ date: lastDate(recentPoints, input.now.toISODate() ?? ""),
216
+ headline: `Overnight HRV ${dropPercent}% below your ${baseline.count}-day baseline across ${recentPoints.length} nights`,
217
+ detail: "A multi-night drop is the usual sign that recovery is lagging the training. Keep the next session easy and see whether it comes back.",
218
+ values: {
219
+ nights: recentPoints.length,
220
+ recentMedian: round(recentMedian),
221
+ baseline: round(baseline.median),
222
+ dropPercent,
223
+ },
224
+ };
225
+ }
226
+ // SECTION: Training load
227
+ //
228
+ // Acute (7-day TRIMP) against chronic (28-day TRIMP, expressed weekly). The
229
+ // classic injury-risk flag, and the one signal here that actually joins
230
+ // training to recovery rather than reading a wellness metric on its own.
231
+ const ACUTE_DAYS = 7;
232
+ const CHRONIC_DAYS = 28;
233
+ const RATIO_HIGH = 1.5;
234
+ const RATIO_LOW = 0.8;
235
+ /**
236
+ * A chronic average taken over a hole in the store makes every ratio look
237
+ * alarming, so most of the window has to actually be covered before a number is
238
+ * worth reporting.
239
+ */
240
+ const MIN_CHRONIC_COVERAGE_DAYS = 21;
241
+ export function detectLoadRatio(input) {
242
+ const profile = estimateHrProfile(input.series("resting_hr", CHRONIC_DAYS), input.series("max_hr", CHRONIC_DAYS));
243
+ if (!profile) {
244
+ return null;
245
+ }
246
+ const coverage = input.series("resting_hr", CHRONIC_DAYS).length;
247
+ if (coverage < MIN_CHRONIC_COVERAGE_DAYS) {
248
+ return null;
249
+ }
250
+ const byDay = dailyTrimp(input.activities(CHRONIC_DAYS), profile);
251
+ const today = input.now.startOf("day");
252
+ const sumOverDays = (days) => {
253
+ let total = 0;
254
+ for (let offset = 0; offset < days; offset += 1) {
255
+ const date = today.minus({ days: offset }).toISODate();
256
+ if (date) {
257
+ total += byDay.get(date) ?? 0;
258
+ }
259
+ }
260
+ return total;
261
+ };
262
+ const acute = sumOverDays(ACUTE_DAYS);
263
+ const chronicWeekly = sumOverDays(CHRONIC_DAYS) / (CHRONIC_DAYS / ACUTE_DAYS);
264
+ // Nothing to compare against: a first week of training is not a spike.
265
+ if (chronicWeekly <= 0) {
266
+ return null;
267
+ }
268
+ const ratio = round(acute / chronicWeekly, 2);
269
+ if (ratio <= RATIO_HIGH && ratio >= RATIO_LOW) {
270
+ return null;
271
+ }
272
+ const isHigh = ratio > RATIO_HIGH;
273
+ const provenance = "This load is TRIMP, computed here from duration and heart rate, so it will not match the training load Connect shows.";
274
+ return {
275
+ kind: isHigh ? "load_ratio_high" : "load_ratio_low",
276
+ severity: isHigh && ratio >= 2 ? "warn" : "notice",
277
+ date: today.toISODate() ?? "",
278
+ headline: isHigh
279
+ ? `This week's training load is ${ratio}x your four-week average`
280
+ : `This week's training load is down to ${ratio}x your four-week average`,
281
+ detail: isHigh
282
+ ? `Jumps this size are where injuries tend to come from. Holding the next week nearer the average is the low-risk call. ${provenance}`
283
+ : `A drop this size for more than a week or two starts costing fitness rather than building it. ${provenance}`,
284
+ values: {
285
+ ratio,
286
+ acuteLoad: round(acute),
287
+ chronicWeeklyLoad: round(chronicWeekly),
288
+ },
289
+ };
290
+ }
291
+ //# sourceMappingURL=detectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detectors.js","sourceRoot":"","sources":["../../src/detect/detectors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import type { DateTime } from "luxon";
2
+ import type { MetricKind } from "../history/schema.js";
3
+ import type { MetricPoint, StoredActivity } from "../history/store.js";
4
+ export type FindingKind = "rhr_elevated" | "sleep_debt" | "hrv_trend_break" | "load_ratio_high" | "load_ratio_low";
5
+ export type FindingSeverity = "info" | "notice" | "warn";
6
+ export interface Finding {
7
+ kind: FindingKind;
8
+ severity: FindingSeverity;
9
+ /** The day the finding is about, which is the most recent day it covers. */
10
+ date: string;
11
+ /** The measurement, in plain language. Never a cause. */
12
+ headline: string;
13
+ /** What it means for training. Never medical. */
14
+ detail: string;
15
+ /** The numbers behind the headline, for a surface that wants to render them. */
16
+ values: Record<string, number>;
17
+ }
18
+ /**
19
+ * Detectors read through this rather than reaching for the store, which is what
20
+ * keeps every one of them a pure function over arrays and their tests free of a
21
+ * database.
22
+ */
23
+ export interface DetectorInput {
24
+ now: DateTime;
25
+ /** Points for the last `days` days, oldest first. */
26
+ series: (kind: MetricKind, days: number) => MetricPoint[];
27
+ activities: (days: number) => StoredActivity[];
28
+ }
29
+ //# sourceMappingURL=findings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.d.ts","sourceRoot":"","sources":["../../src/detect/findings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AACvD,OAAO,KAAK,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAavE,MAAM,MAAM,WAAW,GACnB,cAAc,GACd,YAAY,GACZ,iBAAiB,GACjB,iBAAiB,GACjB,gBAAgB,CAAC;AAErB,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,CAAC;IAC1B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,QAAQ,CAAC;IACd,qDAAqD;IACrD,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,KAAK,WAAW,EAAE,CAAC;IAC1D,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,cAAc,EAAE,CAAC;CAChD"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=findings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.js","sourceRoot":"","sources":["../../src/detect/findings.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import type { DetectorInput } from "./findings.js";
2
+ export type LoadTrajectory = "rising" | "falling" | "steady";
3
+ export type LoadVerdict = "spike_ahead" | "detraining_ahead" | "on_track" | "unknown";
4
+ export interface LoadForecast {
5
+ /** Where the ratio is today. Null when there is not enough history to say. */
6
+ currentRatio: number | null;
7
+ /** Where it lands in seven days if this week repeats. */
8
+ projectedRatio: number | null;
9
+ trajectory: LoadTrajectory;
10
+ verdict: LoadVerdict;
11
+ /** TRIMP over the last seven days. */
12
+ acuteLoad: number;
13
+ /** TRIMP over the last 28 days, expressed as a weekly figure. */
14
+ chronicWeeklyLoad: number;
15
+ /**
16
+ * How much this week's load would have to change to land inside the safe
17
+ * band. Positive means add load, negative means shed it. Null when already
18
+ * inside the band or when there is nothing to compare against.
19
+ */
20
+ weeklyLoadAdjustment: number | null;
21
+ /** Plain sentence for a watch screen or a model prompt. Never medical. */
22
+ summary: string;
23
+ }
24
+ /**
25
+ * The projection has to move the whole 28-day window, not just add to it.
26
+ *
27
+ * A week from now the chronic window has dropped its oldest seven days and
28
+ * gained the seven we are projecting. Holding chronic fixed and only advancing
29
+ * acute overstates the ratio whenever the dropped week was heavy -- which is
30
+ * precisely the case where the user most needs the number to be right.
31
+ */
32
+ export declare function forecastLoad(input: DetectorInput): LoadForecast;
33
+ //# sourceMappingURL=forecast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forecast.d.ts","sourceRoot":"","sources":["../../src/detect/forecast.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAsBnD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAC7D,MAAM,MAAM,WAAW,GAAG,aAAa,GAAG,kBAAkB,GAAG,UAAU,GAAG,SAAS,CAAC;AAEtF,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,yDAAyD;IACzD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,UAAU,EAAE,cAAc,CAAC;IAC3B,OAAO,EAAE,WAAW,CAAC;IACrB,sCAAsC;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,0EAA0E;IAC1E,OAAO,EAAE,MAAM,CAAC;CACjB;AAOD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,aAAa,GAAG,YAAY,CA6G/D"}
@@ -0,0 +1,122 @@
1
+ import { dailyTrimp, estimateHrProfile } from "./trimp.js";
2
+ // SECTION: Training load forecast
3
+ //
4
+ // The acute:chronic ratio detector says where the ratio is. It fires after the
5
+ // jump, which is the wrong side of the event: the value of the ratio is that it
6
+ // warns you *before* the week that hurts you, and a warning that arrives once
7
+ // the week is over is a diary entry.
8
+ //
9
+ // This projects the ratio forward one week on the only assumption the data
10
+ // itself supports: that the next seven days look like the last seven. It is an
11
+ // extrapolation and it is labelled as one. Nothing here asks the user to record
12
+ // a plan, because a forecast that depends on the user diligently logging their
13
+ // intentions is a forecast that will be wrong exactly when they are busy -- and
14
+ // being busy is correlated with the training weeks that go wrong.
15
+ const ACUTE_DAYS = 7;
16
+ const CHRONIC_DAYS = 28;
17
+ const RATIO_HIGH = 1.5;
18
+ const RATIO_LOW = 0.8;
19
+ const MIN_CHRONIC_COVERAGE_DAYS = 21;
20
+ function round(value, places = 2) {
21
+ const factor = 10 ** places;
22
+ return Math.round(value * factor) / factor;
23
+ }
24
+ /**
25
+ * The projection has to move the whole 28-day window, not just add to it.
26
+ *
27
+ * A week from now the chronic window has dropped its oldest seven days and
28
+ * gained the seven we are projecting. Holding chronic fixed and only advancing
29
+ * acute overstates the ratio whenever the dropped week was heavy -- which is
30
+ * precisely the case where the user most needs the number to be right.
31
+ */
32
+ export function forecastLoad(input) {
33
+ const unknown = {
34
+ currentRatio: null,
35
+ projectedRatio: null,
36
+ trajectory: "steady",
37
+ verdict: "unknown",
38
+ acuteLoad: 0,
39
+ chronicWeeklyLoad: 0,
40
+ weeklyLoadAdjustment: null,
41
+ summary: "Not enough training history yet to project a load ratio.",
42
+ };
43
+ const profile = estimateHrProfile(input.series("resting_hr", CHRONIC_DAYS), input.series("max_hr", CHRONIC_DAYS));
44
+ if (!profile) {
45
+ return unknown;
46
+ }
47
+ if (input.series("resting_hr", CHRONIC_DAYS).length < MIN_CHRONIC_COVERAGE_DAYS) {
48
+ return unknown;
49
+ }
50
+ const byDay = dailyTrimp(input.activities(CHRONIC_DAYS + ACUTE_DAYS), profile);
51
+ const today = input.now.startOf("day");
52
+ const sumWindow = (fromDaysAgo, days) => {
53
+ let total = 0;
54
+ for (let offset = fromDaysAgo; offset < fromDaysAgo + days; offset += 1) {
55
+ const date = today.minus({ days: offset }).toISODate();
56
+ if (date) {
57
+ total += byDay.get(date) ?? 0;
58
+ }
59
+ }
60
+ return total;
61
+ };
62
+ const acute = sumWindow(0, ACUTE_DAYS);
63
+ const chronicTotal = sumWindow(0, CHRONIC_DAYS);
64
+ const chronicWeekly = chronicTotal / (CHRONIC_DAYS / ACUTE_DAYS);
65
+ if (chronicWeekly <= 0) {
66
+ return { ...unknown, acuteLoad: round(acute), chronicWeeklyLoad: 0 };
67
+ }
68
+ const currentRatio = round(acute / chronicWeekly);
69
+ // Slide the 28-day window forward seven days: drop the oldest week, add the
70
+ // week we are assuming.
71
+ const oldestWeek = sumWindow(CHRONIC_DAYS - ACUTE_DAYS, ACUTE_DAYS);
72
+ const projectedChronicTotal = chronicTotal - oldestWeek + acute;
73
+ const projectedChronicWeekly = projectedChronicTotal / (CHRONIC_DAYS / ACUTE_DAYS);
74
+ if (projectedChronicWeekly <= 0) {
75
+ return {
76
+ ...unknown,
77
+ currentRatio,
78
+ acuteLoad: round(acute),
79
+ chronicWeeklyLoad: round(chronicWeekly),
80
+ };
81
+ }
82
+ const projectedRatio = round(acute / projectedChronicWeekly);
83
+ const trajectory = projectedRatio > currentRatio + 0.05
84
+ ? "rising"
85
+ : projectedRatio < currentRatio - 0.05
86
+ ? "falling"
87
+ : "steady";
88
+ let verdict = "on_track";
89
+ if (projectedRatio > RATIO_HIGH) {
90
+ verdict = "spike_ahead";
91
+ }
92
+ else if (projectedRatio < RATIO_LOW) {
93
+ verdict = "detraining_ahead";
94
+ }
95
+ // What this week's load would have to be to sit on the edge of the band.
96
+ // Solved against the projected chronic window, which is the one the ratio
97
+ // will actually be measured against.
98
+ let weeklyLoadAdjustment = null;
99
+ if (verdict === "spike_ahead") {
100
+ weeklyLoadAdjustment = round(RATIO_HIGH * projectedChronicWeekly - acute, 0);
101
+ }
102
+ else if (verdict === "detraining_ahead") {
103
+ weeklyLoadAdjustment = round(RATIO_LOW * projectedChronicWeekly - acute, 0);
104
+ }
105
+ const provenance = "Load is TRIMP, computed here from duration and heart rate, and this is an extrapolation of the last seven days rather than a plan you have entered.";
106
+ const summary = verdict === "spike_ahead"
107
+ ? `Repeating this week puts your load ratio at ${projectedRatio}x, above the ${RATIO_HIGH}x mark where injury risk climbs. ${provenance}`
108
+ : verdict === "detraining_ahead"
109
+ ? `Repeating this week puts your load ratio at ${projectedRatio}x, below the ${RATIO_LOW}x mark where fitness starts to slip. ${provenance}`
110
+ : `Repeating this week keeps your load ratio near ${projectedRatio}x, inside the steady band. ${provenance}`;
111
+ return {
112
+ currentRatio,
113
+ projectedRatio,
114
+ trajectory,
115
+ verdict,
116
+ acuteLoad: round(acute, 0),
117
+ chronicWeeklyLoad: round(chronicWeekly, 0),
118
+ weeklyLoadAdjustment,
119
+ summary,
120
+ };
121
+ }
122
+ //# sourceMappingURL=forecast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forecast.js","sourceRoot":"","sources":["../../src/detect/forecast.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,50 @@
1
+ import { DateTime } from "luxon";
2
+ import type { DetectorInput, Finding } from "./findings.js";
3
+ export interface Coverage {
4
+ days: number;
5
+ ready: boolean;
6
+ /** Newest date with any measurement, or null for an empty store. */
7
+ throughDate: string | null;
8
+ /** Days between that and today. 0 when the record is current. */
9
+ staleDays: number;
10
+ }
11
+ export interface DetectionResult {
12
+ findings: Finding[];
13
+ coverage: Coverage;
14
+ }
15
+ /**
16
+ * Why `ready` is false, and the sentence to say about it.
17
+ *
18
+ * `ready` collapses two refusals that need opposite answers: COLD -- there is
19
+ * not enough history yet, wait -- and STALE -- there is plenty and it stops
20
+ * weeks ago, go and fetch. Each surface used to write that sentence itself, and
21
+ * only one of the four was taught the difference when `staleDays` was added.
22
+ * The three that were not printed the cold-start line at a 74-day store:
23
+ *
24
+ * Still gathering data - 74 of the 14 days needed
25
+ *
26
+ * which is arithmetic nonsense, and worse, it is what a model repeats back to
27
+ * the user as "I don't have access to your data" -- while 598 measurements sit
28
+ * on disk. One description, read by every surface, is the only shape that stops
29
+ * the next surface from inheriting the same fault.
30
+ */
31
+ export type CoverageState = "ready" | "stale" | "cold";
32
+ export interface CoverageDescription {
33
+ state: CoverageState;
34
+ /** One sentence about the record itself. */
35
+ detail: string;
36
+ /** The single next action, or null when nothing needs doing. */
37
+ fix: string | null;
38
+ }
39
+ export declare function describeFindingsCoverage(coverage: Coverage): CoverageDescription;
40
+ /** The only function here that touches the store. */
41
+ export declare function buildDetectorInput(now?: DateTime): DetectorInput;
42
+ /**
43
+ * Cold start is a first-class case, not an edge one. With no history every
44
+ * detector correctly returns null, and an empty array on its own is
45
+ * indistinguishable from a clean bill of health. `ready` is what lets a surface
46
+ * say "still gathering data" instead of implying everything is fine -- so
47
+ * surfaces read the flag, never the length of the array.
48
+ */
49
+ export declare function runDetectors(input?: DetectorInput): DetectionResult;
50
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detect/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AASjC,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAmB,MAAM,eAAe,CAAC;AAgC7E,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,oEAAoE;IACpE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,iEAAiE;IACjE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,aAAa,CAAC;IACrB,4CAA4C;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,GAAG,mBAAmB,CAwBhF;AAED,qDAAqD;AACrD,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,QAA2B,GAAG,aAAa,CAgBlF;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,GAAE,aAAoC,GAAG,eAAe,CA6CzF"}