@mongrov/analytics 0.6.0 → 0.7.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 (213) hide show
  1. package/README.md +23 -10
  2. package/UPGRADING.md +370 -0
  3. package/dist/core/device-events.d.ts +14 -0
  4. package/dist/core/device-events.d.ts.map +1 -0
  5. package/dist/core/device-events.js +13 -0
  6. package/dist/core/device-events.js.map +1 -0
  7. package/dist/core/errors.d.ts +1 -1
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js.map +1 -1
  10. package/dist/core/extensions.d.ts +21 -4
  11. package/dist/core/extensions.d.ts.map +1 -1
  12. package/dist/core/extensions.js +21 -4
  13. package/dist/core/extensions.js.map +1 -1
  14. package/dist/core/factory.d.ts.map +1 -1
  15. package/dist/core/factory.js +125 -23
  16. package/dist/core/factory.js.map +1 -1
  17. package/dist/core/hooks.js +1 -1
  18. package/dist/core/hooks.js.map +1 -1
  19. package/dist/core/index.d.ts +11 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/core/index.js +14 -0
  22. package/dist/core/index.js.map +1 -1
  23. package/dist/core/insight.d.ts +44 -0
  24. package/dist/core/insight.d.ts.map +1 -0
  25. package/dist/core/insight.js +62 -0
  26. package/dist/core/insight.js.map +1 -0
  27. package/dist/core/metric_metadata.d.ts +55 -0
  28. package/dist/core/metric_metadata.d.ts.map +1 -1
  29. package/dist/core/metric_metadata.js +35 -7
  30. package/dist/core/metric_metadata.js.map +1 -1
  31. package/dist/core/migrations.d.ts.map +1 -1
  32. package/dist/core/migrations.js +64 -2
  33. package/dist/core/migrations.js.map +1 -1
  34. package/dist/core/retention.d.ts +44 -13
  35. package/dist/core/retention.d.ts.map +1 -1
  36. package/dist/core/retention.js +49 -22
  37. package/dist/core/retention.js.map +1 -1
  38. package/dist/core/schemas.d.ts +73 -2
  39. package/dist/core/schemas.d.ts.map +1 -1
  40. package/dist/core/schemas.js +154 -3
  41. package/dist/core/schemas.js.map +1 -1
  42. package/dist/core/table_metadata.d.ts.map +1 -1
  43. package/dist/core/table_metadata.js +3 -0
  44. package/dist/core/table_metadata.js.map +1 -1
  45. package/dist/core/types.d.ts +45 -10
  46. package/dist/core/types.d.ts.map +1 -1
  47. package/dist/core/warehouse.d.ts +28 -0
  48. package/dist/core/warehouse.d.ts.map +1 -1
  49. package/dist/core/warehouse.js +44 -0
  50. package/dist/core/warehouse.js.map +1 -1
  51. package/dist/core/watermark-cache.d.ts +75 -0
  52. package/dist/core/watermark-cache.d.ts.map +1 -0
  53. package/dist/core/watermark-cache.js +0 -0
  54. package/dist/core/watermark-cache.js.map +1 -0
  55. package/dist/rules/__fakes__/fakeEngine.d.ts +2 -0
  56. package/dist/rules/__fakes__/fakeEngine.d.ts.map +1 -1
  57. package/dist/rules/__fakes__/fakeEngine.js +4 -0
  58. package/dist/rules/__fakes__/fakeEngine.js.map +1 -1
  59. package/dist/rules/baseline-reader.d.ts +55 -0
  60. package/dist/rules/baseline-reader.d.ts.map +1 -0
  61. package/dist/rules/baseline-reader.js +111 -0
  62. package/dist/rules/baseline-reader.js.map +1 -0
  63. package/dist/rules/compiler.d.ts +36 -1
  64. package/dist/rules/compiler.d.ts.map +1 -1
  65. package/dist/rules/compiler.js +185 -39
  66. package/dist/rules/compiler.js.map +1 -1
  67. package/dist/rules/defaults/luminx.d.ts.map +1 -1
  68. package/dist/rules/defaults/luminx.js +8 -0
  69. package/dist/rules/defaults/luminx.js.map +1 -1
  70. package/dist/rules/defaults/viva.d.ts.map +1 -1
  71. package/dist/rules/defaults/viva.js +7 -0
  72. package/dist/rules/defaults/viva.js.map +1 -1
  73. package/dist/rules/defaults/yogaring.d.ts.map +1 -1
  74. package/dist/rules/defaults/yogaring.js +7 -0
  75. package/dist/rules/defaults/yogaring.js.map +1 -1
  76. package/dist/rules/defaults/ziva.d.ts.map +1 -1
  77. package/dist/rules/defaults/ziva.js +100 -24
  78. package/dist/rules/defaults/ziva.js.map +1 -1
  79. package/dist/rules/evaluator.d.ts +14 -3
  80. package/dist/rules/evaluator.d.ts.map +1 -1
  81. package/dist/rules/evaluator.js +157 -5
  82. package/dist/rules/evaluator.js.map +1 -1
  83. package/dist/rules/factory.d.ts.map +1 -1
  84. package/dist/rules/factory.js +5 -1
  85. package/dist/rules/factory.js.map +1 -1
  86. package/dist/rules/index.d.ts +2 -1
  87. package/dist/rules/index.d.ts.map +1 -1
  88. package/dist/rules/index.js +2 -1
  89. package/dist/rules/index.js.map +1 -1
  90. package/dist/rules/schema.d.ts +75 -4
  91. package/dist/rules/schema.d.ts.map +1 -1
  92. package/dist/rules/schema.js +44 -0
  93. package/dist/rules/schema.js.map +1 -1
  94. package/dist/rules/types.d.ts +16 -1
  95. package/dist/rules/types.d.ts.map +1 -1
  96. package/dist/rules/validator.d.ts.map +1 -1
  97. package/dist/rules/validator.js +56 -0
  98. package/dist/rules/validator.js.map +1 -1
  99. package/dist/sync/baseline-compute.d.ts +63 -0
  100. package/dist/sync/baseline-compute.d.ts.map +1 -0
  101. package/dist/sync/baseline-compute.js +211 -0
  102. package/dist/sync/baseline-compute.js.map +1 -0
  103. package/dist/sync/events.d.ts +19 -5
  104. package/dist/sync/events.d.ts.map +1 -1
  105. package/dist/sync/events.js +17 -0
  106. package/dist/sync/events.js.map +1 -1
  107. package/dist/sync/factory.d.ts +25 -4
  108. package/dist/sync/factory.d.ts.map +1 -1
  109. package/dist/sync/factory.js +108 -15
  110. package/dist/sync/factory.js.map +1 -1
  111. package/dist/sync/fetcher.d.ts +15 -2
  112. package/dist/sync/fetcher.d.ts.map +1 -1
  113. package/dist/sync/fetcher.js +43 -7
  114. package/dist/sync/fetcher.js.map +1 -1
  115. package/dist/sync/flusher.d.ts +52 -2
  116. package/dist/sync/flusher.d.ts.map +1 -1
  117. package/dist/sync/flusher.js +78 -9
  118. package/dist/sync/flusher.js.map +1 -1
  119. package/dist/sync/index.d.ts +3 -1
  120. package/dist/sync/index.d.ts.map +1 -1
  121. package/dist/sync/index.js +1 -0
  122. package/dist/sync/index.js.map +1 -1
  123. package/dist/sync/mapper/activity.d.ts.map +1 -1
  124. package/dist/sync/mapper/activity.js +2 -1
  125. package/dist/sync/mapper/activity.js.map +1 -1
  126. package/dist/sync/mapper/ring-config.js +1 -1
  127. package/dist/sync/mapper/ring-config.js.map +1 -1
  128. package/dist/sync/mapper/schema.d.ts +411 -0
  129. package/dist/sync/mapper/schema.d.ts.map +1 -0
  130. package/dist/sync/mapper/schema.js +128 -0
  131. package/dist/sync/mapper/schema.js.map +1 -0
  132. package/dist/sync/mapper/sleep.d.ts +58 -13
  133. package/dist/sync/mapper/sleep.d.ts.map +1 -1
  134. package/dist/sync/mapper/sleep.js +141 -44
  135. package/dist/sync/mapper/sleep.js.map +1 -1
  136. package/dist/sync/mapper/types.d.ts +66 -10
  137. package/dist/sync/mapper/types.d.ts.map +1 -1
  138. package/dist/sync/scheduler.d.ts +7 -0
  139. package/dist/sync/scheduler.d.ts.map +1 -1
  140. package/dist/sync/scheduler.js +14 -0
  141. package/dist/sync/scheduler.js.map +1 -1
  142. package/dist/tools/__fakes__/engine.d.ts +2 -0
  143. package/dist/tools/__fakes__/engine.d.ts.map +1 -1
  144. package/dist/tools/__fakes__/engine.js +10 -0
  145. package/dist/tools/__fakes__/engine.js.map +1 -1
  146. package/dist/tools/authorize.d.ts +19 -12
  147. package/dist/tools/authorize.d.ts.map +1 -1
  148. package/dist/tools/authorize.js +31 -40
  149. package/dist/tools/authorize.js.map +1 -1
  150. package/dist/tools/factory.d.ts +2 -0
  151. package/dist/tools/factory.d.ts.map +1 -1
  152. package/dist/tools/factory.js +12 -1
  153. package/dist/tools/factory.js.map +1 -1
  154. package/dist/tools/formatters/copy-guidelines.d.ts +57 -0
  155. package/dist/tools/formatters/copy-guidelines.d.ts.map +1 -0
  156. package/dist/tools/formatters/copy-guidelines.js +110 -0
  157. package/dist/tools/formatters/copy-guidelines.js.map +1 -0
  158. package/dist/tools/formatters/index.d.ts +14 -0
  159. package/dist/tools/formatters/index.d.ts.map +1 -0
  160. package/dist/tools/formatters/index.js +14 -0
  161. package/dist/tools/formatters/index.js.map +1 -0
  162. package/dist/tools/formatters/spo2.d.ts +21 -0
  163. package/dist/tools/formatters/spo2.d.ts.map +1 -0
  164. package/dist/tools/formatters/spo2.js +79 -0
  165. package/dist/tools/formatters/spo2.js.map +1 -0
  166. package/dist/tools/{formatters.d.ts → formatters/text.d.ts} +1 -1
  167. package/dist/tools/formatters/text.d.ts.map +1 -0
  168. package/dist/tools/{formatters.js → formatters/text.js} +1 -1
  169. package/dist/tools/formatters/text.js.map +1 -0
  170. package/dist/tools/impls/activity.d.ts +2 -2
  171. package/dist/tools/impls/activity.js +7 -3
  172. package/dist/tools/impls/activity.js.map +1 -1
  173. package/dist/tools/impls/anomaly.d.ts +2 -2
  174. package/dist/tools/impls/anomaly.js +8 -4
  175. package/dist/tools/impls/anomaly.js.map +1 -1
  176. package/dist/tools/impls/compare.d.ts +2 -2
  177. package/dist/tools/impls/compare.js +8 -4
  178. package/dist/tools/impls/compare.js.map +1 -1
  179. package/dist/tools/impls/hrv.d.ts +2 -2
  180. package/dist/tools/impls/hrv.js +6 -2
  181. package/dist/tools/impls/hrv.js.map +1 -1
  182. package/dist/tools/impls/insights.d.ts +4 -4
  183. package/dist/tools/impls/insights.d.ts.map +1 -1
  184. package/dist/tools/impls/insights.js +10 -3
  185. package/dist/tools/impls/insights.js.map +1 -1
  186. package/dist/tools/impls/sleep.d.ts +2 -2
  187. package/dist/tools/impls/sleep.js +6 -2
  188. package/dist/tools/impls/sleep.js.map +1 -1
  189. package/dist/tools/impls/spo2.d.ts +52 -0
  190. package/dist/tools/impls/spo2.d.ts.map +1 -0
  191. package/dist/tools/impls/spo2.js +65 -0
  192. package/dist/tools/impls/spo2.js.map +1 -0
  193. package/dist/tools/index.d.ts +1 -0
  194. package/dist/tools/index.d.ts.map +1 -1
  195. package/dist/tools/index.js +4 -0
  196. package/dist/tools/index.js.map +1 -1
  197. package/dist/tools/mcp/guard.d.ts +28 -11
  198. package/dist/tools/mcp/guard.d.ts.map +1 -1
  199. package/dist/tools/mcp/guard.js +45 -15
  200. package/dist/tools/mcp/guard.js.map +1 -1
  201. package/dist/tools/mcp/index.d.ts +1 -1
  202. package/dist/tools/mcp/index.d.ts.map +1 -1
  203. package/dist/tools/mcp/index.js +1 -1
  204. package/dist/tools/mcp/index.js.map +1 -1
  205. package/dist/tools/mcp/server.d.ts.map +1 -1
  206. package/dist/tools/mcp/server.js +4 -0
  207. package/dist/tools/mcp/server.js.map +1 -1
  208. package/dist/tools/mcp/transports/http.d.ts.map +1 -1
  209. package/dist/tools/mcp/transports/http.js +4 -0
  210. package/dist/tools/mcp/transports/http.js.map +1 -1
  211. package/package.json +8 -5
  212. package/dist/tools/formatters.d.ts.map +0 -1
  213. package/dist/tools/formatters.js.map +0 -1
@@ -19,6 +19,19 @@ export declare const COMPARES: readonly ["less_than", "greater_than", "equals",
19
19
  export type Compare = (typeof COMPARES)[number];
20
20
  export declare const SEVERITIES: readonly ["info", "warn", "critical"];
21
21
  export type Severity = (typeof SEVERITIES)[number];
22
+ /**
23
+ * Physiological context the rule restricts its samples to (Sprint 5 §4).
24
+ *
25
+ * - `any` — every sample in the window
26
+ * - `asleep` — samples falling inside a `v_sleep_session` interval
27
+ * - `resting` — samples in a minute with zero steps
28
+ *
29
+ * Context is a JOIN, not a post-filter: "SpO₂ during sleep" means the
30
+ * aggregate is computed over sleep samples only, not computed over
31
+ * everything and then labelled.
32
+ */
33
+ export declare const CONTEXTS: readonly ["any", "asleep", "resting"];
34
+ export type RuleContext = (typeof CONTEXTS)[number];
22
35
  export declare const TargetSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
23
36
  type: z.ZodLiteral<"absolute">;
24
37
  value: z.ZodNumber;
@@ -64,6 +77,23 @@ export declare const TargetSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<
64
77
  type: "range";
65
78
  min: number;
66
79
  max: number;
80
+ }>, z.ZodObject<{
81
+ type: z.ZodLiteral<"user_setting">;
82
+ /** KVStore key suffix, e.g. `user:spo2SafeLevel`. See the namespace registry. */
83
+ key: z.ZodString;
84
+ /** Used when the user has never set the value. */
85
+ defaultValue: z.ZodNumber;
86
+ compare: z.ZodDefault<z.ZodEnum<["as_configured"]>>;
87
+ }, "strip", z.ZodTypeAny, {
88
+ type: "user_setting";
89
+ key: string;
90
+ defaultValue: number;
91
+ compare: "as_configured";
92
+ }, {
93
+ type: "user_setting";
94
+ key: string;
95
+ defaultValue: number;
96
+ compare?: "as_configured" | undefined;
67
97
  }>]>;
68
98
  export type Target = z.infer<typeof TargetSchema>;
69
99
  export declare const ThrottleSchema: z.ZodDefault<z.ZodObject<{
@@ -86,6 +116,16 @@ export declare const RuleSchema: z.ZodObject<{
86
116
  window: z.ZodEnum<["1h", "6h", "24h", "3d", "7d", "30d"]>;
87
117
  aggregation: z.ZodDefault<z.ZodEnum<["avg", "min", "max", "sum", "last", "count"]>>;
88
118
  compare: z.ZodEnum<["less_than", "greater_than", "equals", "not_equals", "between"]>;
119
+ /** Restrict samples to a physiological context. Default `any`. */
120
+ context: z.ZodDefault<z.ZodEnum<["any", "asleep", "resting"]>>;
121
+ /**
122
+ * Require `n` ADJACENT breaching samples before the rule fires.
123
+ *
124
+ * Omitted or `1` means "any breach in the window", which the aggregate
125
+ * path already expresses (e.g. `aggregation: 'min'` + `less_than`).
126
+ * `>= 2` switches the compiler to run-length detection.
127
+ */
128
+ consecutive: z.ZodOptional<z.ZodNumber>;
89
129
  target: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
90
130
  type: z.ZodLiteral<"absolute">;
91
131
  value: z.ZodNumber;
@@ -131,6 +171,23 @@ export declare const RuleSchema: z.ZodObject<{
131
171
  type: "range";
132
172
  min: number;
133
173
  max: number;
174
+ }>, z.ZodObject<{
175
+ type: z.ZodLiteral<"user_setting">;
176
+ /** KVStore key suffix, e.g. `user:spo2SafeLevel`. See the namespace registry. */
177
+ key: z.ZodString;
178
+ /** Used when the user has never set the value. */
179
+ defaultValue: z.ZodNumber;
180
+ compare: z.ZodDefault<z.ZodEnum<["as_configured"]>>;
181
+ }, "strip", z.ZodTypeAny, {
182
+ type: "user_setting";
183
+ key: string;
184
+ defaultValue: number;
185
+ compare: "as_configured";
186
+ }, {
187
+ type: "user_setting";
188
+ key: string;
189
+ defaultValue: number;
190
+ compare?: "as_configured" | undefined;
134
191
  }>]>;
135
192
  severity: z.ZodEnum<["info", "warn", "critical"]>;
136
193
  throttle: z.ZodDefault<z.ZodObject<{
@@ -167,12 +224,18 @@ export declare const RuleSchema: z.ZodObject<{
167
224
  type: "range";
168
225
  min: number;
169
226
  max: number;
227
+ } | {
228
+ type: "user_setting";
229
+ key: string;
230
+ defaultValue: number;
231
+ compare: "as_configured";
170
232
  };
233
+ context: "any" | "asleep" | "resting";
171
234
  name: string;
172
235
  metric: "spo2" | "device_battery" | "hrv_ms" | "stress" | "systolic_bp" | "diastolic_bp" | "vascular_aging" | "hr_bpm" | "temp_c" | "activity_steps" | "calories" | "distance_km" | "sleep_total_minutes" | "sleep_score";
173
- window: "1h" | "6h" | "24h" | "3d" | "7d" | "30d";
174
- aggregation: "avg" | "min" | "max" | "sum" | "last" | "count";
175
236
  compare: "less_than" | "greater_than" | "equals" | "not_equals" | "between";
237
+ window: "1h" | "6h" | "24h" | "3d" | "7d" | "30d";
238
+ aggregation: "avg" | "sum" | "min" | "max" | "last" | "count";
176
239
  severity: "info" | "warn" | "critical";
177
240
  throttle: {
178
241
  minGapMinutes: number;
@@ -180,6 +243,7 @@ export declare const RuleSchema: z.ZodObject<{
180
243
  };
181
244
  description?: string | undefined;
182
245
  brand?: string | undefined;
246
+ consecutive?: number | undefined;
183
247
  rawSql?: string | undefined;
184
248
  rawSqlParams?: string[] | undefined;
185
249
  exposureOverride?: boolean | undefined;
@@ -200,15 +264,22 @@ export declare const RuleSchema: z.ZodObject<{
200
264
  type: "range";
201
265
  min: number;
202
266
  max: number;
267
+ } | {
268
+ type: "user_setting";
269
+ key: string;
270
+ defaultValue: number;
271
+ compare?: "as_configured" | undefined;
203
272
  };
204
273
  name: string;
205
274
  metric: "spo2" | "device_battery" | "hrv_ms" | "stress" | "systolic_bp" | "diastolic_bp" | "vascular_aging" | "hr_bpm" | "temp_c" | "activity_steps" | "calories" | "distance_km" | "sleep_total_minutes" | "sleep_score";
206
- window: "1h" | "6h" | "24h" | "3d" | "7d" | "30d";
207
275
  compare: "less_than" | "greater_than" | "equals" | "not_equals" | "between";
276
+ window: "1h" | "6h" | "24h" | "3d" | "7d" | "30d";
208
277
  severity: "info" | "warn" | "critical";
209
278
  description?: string | undefined;
279
+ context?: "any" | "asleep" | "resting" | undefined;
210
280
  brand?: string | undefined;
211
- aggregation?: "avg" | "min" | "max" | "sum" | "last" | "count" | undefined;
281
+ aggregation?: "avg" | "sum" | "min" | "max" | "last" | "count" | undefined;
282
+ consecutive?: number | undefined;
212
283
  throttle?: {
213
284
  minGapMinutes?: number | undefined;
214
285
  maxPerDay?: number | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/rules/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,+DAA+D;AAC/D,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAID,eAAO,MAAM,OAAO,iDAAkD,CAAA;AACtE,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7C,eAAO,MAAM,YAAY,wDAAyD,CAAA;AAClF,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvD,eAAO,MAAM,QAAQ,2EAMX,CAAA;AACV,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/C,eAAO,MAAM,UAAU,uCAAwC,CAAA;AAC/D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAyBlD,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAKvB,CAAA;AACF,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAEjD,eAAO,MAAM,cAAc;;;;;;;;;GAKb,CAAA;AAEd,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAA;AAErD,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAcrB;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEH,CAAA;AAEF,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAA"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/rules/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB,+DAA+D;AAC/D,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAID,eAAO,MAAM,OAAO,iDAAkD,CAAA;AACtE,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAA;AAE7C,eAAO,MAAM,YAAY,wDAAyD,CAAA;AAClF,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAA;AAEvD,eAAO,MAAM,QAAQ,2EAMX,CAAA;AACV,MAAM,MAAM,OAAO,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAA;AAE/C,eAAO,MAAM,UAAU,uCAAwC,CAAA;AAC/D,MAAM,MAAM,QAAQ,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,CAAC,CAAA;AAElD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,uCAAwC,CAAA;AAC7D,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAA;AA+CnD,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAPvB,iFAAiF;;IAEjF,kDAAkD;;;;;;;;;;;;;IAWlD,CAAA;AACF,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AAEjD,eAAO,MAAM,cAAc;;;;;;;;;GAKb,CAAA;AAEd,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAA;AAErD,eAAO,MAAM,UAAU;;;;;;;;;IASrB,kEAAkE;;IAElE;;;;;;OAMG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA1CH,iFAAiF;;QAEjF,kDAAkD;;;;;;;;;;;;;;;;;;;;;;;;;;;IA+ClD;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEH,CAAA;AAEF,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAA"}
@@ -26,6 +26,18 @@ export const COMPARES = [
26
26
  'between',
27
27
  ];
28
28
  export const SEVERITIES = ['info', 'warn', 'critical'];
29
+ /**
30
+ * Physiological context the rule restricts its samples to (Sprint 5 §4).
31
+ *
32
+ * - `any` — every sample in the window
33
+ * - `asleep` — samples falling inside a `v_sleep_session` interval
34
+ * - `resting` — samples in a minute with zero steps
35
+ *
36
+ * Context is a JOIN, not a post-filter: "SpO₂ during sleep" means the
37
+ * aggregate is computed over sleep samples only, not computed over
38
+ * everything and then labelled.
39
+ */
40
+ export const CONTEXTS = ['any', 'asleep', 'resting'];
29
41
  const TargetAbsolute = z.object({
30
42
  type: z.literal('absolute'),
31
43
  value: z.number(),
@@ -45,11 +57,33 @@ const TargetRange = z.object({
45
57
  min: z.number(),
46
58
  max: z.number(),
47
59
  });
60
+ /**
61
+ * v0.2.0 (Sprint 5 §4, Ziva #1) — threshold read from KVStore at eval time
62
+ * rather than baked into the rule.
63
+ *
64
+ * This is what makes the ⚙ sheet's promise real: the user drags a safe
65
+ * level, the mutation writes `user:spo2SafeLevel`, and the SAME shipped
66
+ * rule re-evaluates against the new number on the next batch. No recompile,
67
+ * no per-user rule rows.
68
+ *
69
+ * `compare` is a single-member enum rather than omitted: the comparison is
70
+ * fixed by the rule's own `compare` field, and a second knob here would let
71
+ * an author write a rule whose two comparison directions disagree.
72
+ */
73
+ const TargetUserSetting = z.object({
74
+ type: z.literal('user_setting'),
75
+ /** KVStore key suffix, e.g. `user:spo2SafeLevel`. See the namespace registry. */
76
+ key: z.string().min(1),
77
+ /** Used when the user has never set the value. */
78
+ defaultValue: z.number(),
79
+ compare: z.enum(['as_configured']).default('as_configured'),
80
+ });
48
81
  export const TargetSchema = z.discriminatedUnion('type', [
49
82
  TargetAbsolute,
50
83
  TargetBaselinePercent,
51
84
  TargetBaselineStddev,
52
85
  TargetRange,
86
+ TargetUserSetting,
53
87
  ]);
54
88
  export const ThrottleSchema = z
55
89
  .object({
@@ -66,6 +100,16 @@ export const RuleSchema = z.object({
66
100
  window: z.enum(WINDOWS),
67
101
  aggregation: z.enum(AGGREGATIONS).default('avg'),
68
102
  compare: z.enum(COMPARES),
103
+ /** Restrict samples to a physiological context. Default `any`. */
104
+ context: z.enum(CONTEXTS).default('any'),
105
+ /**
106
+ * Require `n` ADJACENT breaching samples before the rule fires.
107
+ *
108
+ * Omitted or `1` means "any breach in the window", which the aggregate
109
+ * path already expresses (e.g. `aggregation: 'min'` + `less_than`).
110
+ * `>= 2` switches the compiler to run-length detection.
111
+ */
112
+ consecutive: z.number().int().min(1).optional(),
69
113
  target: TargetSchema,
70
114
  severity: z.enum(SEVERITIES),
71
115
  throttle: ThrottleSchema,
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/rules/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/rules/schema.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@
5
5
  * consumers don't need to pull the Zod schema module unless they
6
6
  * validate rules themselves.
7
7
  */
8
- import type { AnalyticsEngine, FamilyMembersProvider, KVStore, Unsubscribe } from '../core/types';
8
+ import type { AnalyticsEngine, EventBus, FamilyMembersProvider, KVStore, Unsubscribe } from '../core/types';
9
9
  import type { MetricId } from '../core/metric_metadata';
10
10
  import type { TableName } from '../core/schemas';
11
11
  import type { Rule } from './schema';
@@ -35,6 +35,14 @@ export interface CompiledRule {
35
35
  /** Statically-known params. Runtime binds `$userId`/`$brand`/`$familyId` on top. */
36
36
  params: Record<string, string | number>;
37
37
  description: string;
38
+ /**
39
+ * KVStore key the evaluator reads to bind `$userSettingValue`
40
+ * (Sprint 5 T-19/T-23). Present only for `target.type: 'user_setting'`.
41
+ * Full key is `analytics:{userId}:{userSettingKey}`.
42
+ */
43
+ userSettingKey?: string;
44
+ /** Fallback threshold when the user has never set the value. */
45
+ userSettingDefault?: number;
38
46
  }
39
47
  /** Structured violation delivered to `on('violation', handler)` subscribers. */
40
48
  export interface RuleViolation {
@@ -66,6 +74,13 @@ export interface RulesEngineConfig {
66
74
  brand: string;
67
75
  /** Family id for `onSchedule` — v0.1.0 single-user apps pass the userId. */
68
76
  familyId: string;
77
+ /**
78
+ * Optional app event bus (structural type from core — no runtime dep on
79
+ * @mongrov/data-access). When set, the evaluator emits
80
+ * `threshold:violation` + `insight:insert` after persisting a violation
81
+ * to the `insight` table (principle 35 fire-and-forget).
82
+ */
83
+ eventBus?: EventBus;
69
84
  logger?: RulesLogger;
70
85
  clock?: Clock;
71
86
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/rules/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/rules/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/rules/validator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,EAAE,KAAK,IAAI,EAAuB,KAAK,MAAM,EAAW,MAAM,UAAU,CAAA;AA4B/E;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,EAAE,CAS5D;AAgBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAwBnE;AAED,8EAA8E;AAC9E,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAOpE"}
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/rules/validator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAmB,KAAK,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAC1C,OAAO,EAAE,KAAK,IAAI,EAAuB,KAAK,MAAM,EAAW,MAAM,UAAU,CAAA;AA4B/E;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,QAAQ,GAAG,MAAM,EAAE,CAS5D;AAgBD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CA2BnE;AAiED,8EAA8E;AAC9E,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAOpE"}
@@ -65,6 +65,8 @@ function samplingLabel(metric) {
65
65
  * When rawSql is present with `exposureOverride: true`, emits a warn via `logger`.
66
66
  */
67
67
  export function validateRule(rule, logger) {
68
+ validateConsecutive(rule);
69
+ validateContext(rule);
68
70
  const allowed = allowedWindowsFor(rule.metric);
69
71
  if (!allowed.includes(rule.window)) {
70
72
  throw new RuleValidationError(`Rule ${rule.id}: metric ${rule.metric} sampled ${samplingLabel(rule.metric)}. `
@@ -84,6 +86,60 @@ export function validateRule(rule, logger) {
84
86
  }
85
87
  }
86
88
  }
89
+ /**
90
+ * T-21 — `consecutive x effective_sampling_minutes` must fit the window.
91
+ *
92
+ * A rule asking for 3 consecutive HRV samples (60-min cadence) inside a 1h
93
+ * window is unsatisfiable: the window holds at most one sample, so the
94
+ * rule can never fire and the author gets silence instead of an error.
95
+ *
96
+ * `effective` sampling comes from `device_config.interval_minutes` at eval
97
+ * time when a row exists (principle 22); at register time only the
98
+ * `metric_metadata` fallback is known, so this check uses the fallback and
99
+ * is deliberately permissive — a denser real device only makes a rule MORE
100
+ * satisfiable, never less.
101
+ */
102
+ function validateConsecutive(rule) {
103
+ const n = rule.consecutive;
104
+ if (n === undefined || n <= 1)
105
+ return;
106
+ // Baseline targets resolve per-window, not per-sample; the compiler has
107
+ // no correct query shape for the combination.
108
+ if (rule.target.type === 'baseline_percent' || rule.target.type === 'baseline_stddev') {
109
+ throw new RuleValidationError(`Rule ${rule.id}: consecutive is not supported with target.type `
110
+ + `'${rule.target.type}' — baseline targets resolve per-window, not `
111
+ + `per-sample. Use absolute or user_setting, or drop consecutive.`);
112
+ }
113
+ const cadence = METRIC_METADATA[rule.metric].sampling_minutes;
114
+ if (cadence === 'per_session') {
115
+ // One value per night; "consecutive samples" means consecutive nights,
116
+ // which the window already bounds.
117
+ return;
118
+ }
119
+ const needed = n * cadence;
120
+ const available = WINDOW_MINUTES[rule.window];
121
+ if (needed > available) {
122
+ throw new RuleValidationError(`Rule ${rule.id}: consecutive ${n} x ${cadence}min sampling needs `
123
+ + `${needed}min but window ${rule.window} is only ${available}min. `
124
+ + `The rule could never fire. Widen the window or lower consecutive.`);
125
+ }
126
+ }
127
+ /**
128
+ * T-21 — context compatibility.
129
+ *
130
+ * `context: 'asleep'` JOINs `v_sleep_session` on the metric's `ts`, so a
131
+ * metric whose own time axis IS the sleep session (sleep_total_minutes)
132
+ * would self-join meaninglessly.
133
+ */
134
+ function validateContext(rule) {
135
+ if (rule.context === 'any')
136
+ return;
137
+ const table = METRIC_METADATA[rule.metric].table;
138
+ if (table === 'sleep_session') {
139
+ throw new RuleValidationError(`Rule ${rule.id}: context '${rule.context}' is redundant for metric `
140
+ + `${rule.metric} — it is already a per-session measure. Use context 'any'.`);
141
+ }
142
+ }
87
143
  /** Scan a rawSql body for whole-word references to collected-only columns. */
88
144
  export function referencedCollectedOnlyColumns(sql) {
89
145
  const hits = new Set();
@@ -1 +1 @@
1
- {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/rules/validator.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,eAAe,EAAiB,MAAM,yBAAyB,CAAA;AAExE,OAAO,EAAa,mBAAmB,EAAe,OAAO,EAAE,MAAM,UAAU,CAAA;AAE/E,mCAAmC;AACnC,MAAM,cAAc,GAA2B;IAC7C,IAAI,EAAE,EAAE;IACR,IAAI,EAAE,CAAC,GAAG,EAAE;IACZ,KAAK,EAAE,EAAE,GAAG,EAAE;IACd,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE;IACjB,IAAI,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE;IACjB,KAAK,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE;CACpB,CAAA;AAED;;;;GAIG;AACH,MAAM,0BAA0B,GAAwC;IACtE,MAAM,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;IAClC,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;IACxC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;IAC9C,IAAI,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;IACtC,MAAM,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;IAClC,cAAc,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;IACtD,mBAAmB,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC;IACjC,cAAc,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC;CACpC,CAAA;AAED;;;;;GAKG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAgB;IAChD,MAAM,QAAQ,GAAG,0BAA0B,CAAC,MAAM,CAAC,CAAA;IACnD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC7B,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,gBAAgB,CAAA;IACxD,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,CAAA;IAC5B,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,GAAG,CAAC,CAAA;IAC7B,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC,CAAA;AAC5D,CAAC;AAED,uDAAuD;AACvD,MAAM,sBAAsB,GAAG,IAAI,GAAG,CACpC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC;KAC3B,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,QAAQ,KAAK,gBAAgB,CAAC;KACpD,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAC9B,CAAA;AAED,SAAS,aAAa,CAAC,MAAgB;IACrC,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC,gBAAgB,CAAA;IACxD,OAAO,OAAO,KAAK,aAAa;QAC9B,CAAC,CAAC,wBAAwB;QAC1B,CAAC,CAAC,SAAS,OAAO,KAAK,CAAA;AAC3B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,IAAU,EAAE,MAAoB;IAC3D,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC9C,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,mBAAmB,CAC3B,QAAQ,IAAI,CAAC,EAAE,YAAY,IAAI,CAAC,MAAM,YAAY,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI;cAC9E,UAAU,IAAI,CAAC,MAAM,+BAA+B,OAAO,CAAC,CAAC,CAAC,GAAG,CACpE,CAAA;IACH,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,MAAM,UAAU,GAAG,8BAA8B,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC9D,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACpD,MAAM,IAAI,mBAAmB,CAC3B,QAAQ,IAAI,CAAC,EAAE,iDAAiD,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;kBACxF,4CAA4C,CAC/C,CAAA;QACH,CAAC;QACD,IAAI,IAAI,CAAC,gBAAgB,IAAI,MAAM,EAAE,CAAC;YACpC,MAAM,CAAC,IAAI,CAAC,iDAAiD,EAAE;gBAC7D,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,OAAO,EAAE,UAAU;aACpB,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,8BAA8B,CAAC,GAAW;IACxD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,KAAK,MAAM,GAAG,IAAI,sBAAsB,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,IAAI,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,CAAA;QAC1C,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACtC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACzB,CAAC"}
1
+ {"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/rules/validator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Day-first baseline compute (Sprint 5 §7 / T-13 / T-14, principle 27).
3
+ *
4
+ * Populates `user_baseline`, the shared "usual range" that rules, AI tools,
5
+ * and screens all read. One row per (brand, family, user, metric, window).
6
+ *
7
+ * ## Why day-first
8
+ *
9
+ * The naive implementation quantiles raw readings. That is wrong, and
10
+ * wrong in a way that looks plausible: SpO₂ sampled every 30 minutes dips
11
+ * during deep sleep on most nights, so a raw p10 across 30 days reports the
12
+ * *dip* (~87%) as the bottom of the user's usual range. It isn't — it's a
13
+ * normal nightly event that happens every day. The user's day-to-day
14
+ * variation is much narrower.
15
+ *
16
+ * So: collapse each local day to ONE value, then quantile across days. A
17
+ * fixture of `95, 95, 87, 95, 95` repeated for 30 days gives p10 ≈ 92.6
18
+ * day-first versus 87 raw — and 92.6 is the honest answer.
19
+ *
20
+ * This is also why `sample_count` counts DAYS. Fifteen days of 30-minute
21
+ * sampling is 720 readings but still only 15 days of evidence, and must not
22
+ * populate a baseline.
23
+ */
24
+ import { type BaselineWindowDays, type MetricId } from '../core/metric_metadata';
25
+ import type { AnalyticsEngine, EventBus } from '../core/types';
26
+ import type { SchedulerLogger } from './scheduler';
27
+ export interface BaselineComputeContext {
28
+ brand: string;
29
+ familyId: string;
30
+ userId: string;
31
+ /** IANA zone. Day boundaries are local, so this decides the buckets. */
32
+ userTimezone: string;
33
+ }
34
+ export interface BaselineComputeConfig {
35
+ analytics: AnalyticsEngine;
36
+ eventBus?: EventBus;
37
+ logger?: SchedulerLogger;
38
+ /** Override for tests. Defaults to every metric declaring an aggregate. */
39
+ metrics?: readonly MetricId[];
40
+ /** Override for tests. Defaults to 7 / 30 / 90. */
41
+ windows?: readonly BaselineWindowDays[];
42
+ }
43
+ export interface BaselineComputeResult {
44
+ computed: number;
45
+ skipped: number;
46
+ failed: number;
47
+ }
48
+ /**
49
+ * Build the day-first aggregate SQL for one (metric, window).
50
+ *
51
+ * Exported for snapshot tests — the shape of this query IS the correctness
52
+ * property, so it is worth asserting directly rather than only through
53
+ * results.
54
+ */
55
+ export declare function buildBaselineSql(metric: MetricId, windowDays: BaselineWindowDays): string;
56
+ export interface BaselineComputer {
57
+ /** Compute one (metric, window). Returns true when a row was written. */
58
+ computeOne(metric: MetricId, windowDays: BaselineWindowDays, ctx: BaselineComputeContext): Promise<boolean>;
59
+ /** Compute every configured metric x window for one user. */
60
+ computeAll(ctx: BaselineComputeContext): Promise<BaselineComputeResult>;
61
+ }
62
+ export declare function createBaselineComputer(config: BaselineComputeConfig): BaselineComputer;
63
+ //# sourceMappingURL=baseline-compute.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline-compute.d.ts","sourceRoot":"","sources":["../../src/sync/baseline-compute.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAIL,KAAK,kBAAkB,EAGvB,KAAK,QAAQ,EACd,MAAM,yBAAyB,CAAA;AAChC,OAAO,KAAK,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAC9D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAclD,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,YAAY,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,eAAe,CAAA;IAC1B,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,MAAM,CAAC,EAAE,eAAe,CAAA;IACxB,2EAA2E;IAC3E,OAAO,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAA;IAC7B,mDAAmD;IACnD,OAAO,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAA;CACxC;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,QAAQ,EAChB,UAAU,EAAE,kBAAkB,GAC7B,MAAM,CAoDR;AA4CD,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,UAAU,CACR,MAAM,EAAE,QAAQ,EAChB,UAAU,EAAE,kBAAkB,EAC9B,GAAG,EAAE,sBAAsB,GAC1B,OAAO,CAAC,OAAO,CAAC,CAAA;IACnB,6DAA6D;IAC7D,UAAU,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;CACxE;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,qBAAqB,GAC5B,gBAAgB,CAmGlB"}
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Day-first baseline compute (Sprint 5 §7 / T-13 / T-14, principle 27).
3
+ *
4
+ * Populates `user_baseline`, the shared "usual range" that rules, AI tools,
5
+ * and screens all read. One row per (brand, family, user, metric, window).
6
+ *
7
+ * ## Why day-first
8
+ *
9
+ * The naive implementation quantiles raw readings. That is wrong, and
10
+ * wrong in a way that looks plausible: SpO₂ sampled every 30 minutes dips
11
+ * during deep sleep on most nights, so a raw p10 across 30 days reports the
12
+ * *dip* (~87%) as the bottom of the user's usual range. It isn't — it's a
13
+ * normal nightly event that happens every day. The user's day-to-day
14
+ * variation is much narrower.
15
+ *
16
+ * So: collapse each local day to ONE value, then quantile across days. A
17
+ * fixture of `95, 95, 87, 95, 95` repeated for 30 days gives p10 ≈ 92.6
18
+ * day-first versus 87 raw — and 92.6 is the honest answer.
19
+ *
20
+ * This is also why `sample_count` counts DAYS. Fifteen days of 30-minute
21
+ * sampling is 720 readings but still only 15 days of evidence, and must not
22
+ * populate a baseline.
23
+ */
24
+ import { baselineAggregateFor, BASELINE_MIN_DAYS, BASELINE_WINDOW_DAYS, getBaselineMetricIds, METRIC_METADATA, } from '../core/metric_metadata';
25
+ /**
26
+ * Build the day-first aggregate SQL for one (metric, window).
27
+ *
28
+ * Exported for snapshot tests — the shape of this query IS the correctness
29
+ * property, so it is worth asserting directly rather than only through
30
+ * results.
31
+ */
32
+ export function buildBaselineSql(metric, windowDays) {
33
+ const meta = METRIC_METADATA[metric];
34
+ const aggregate = baselineAggregateFor(metric);
35
+ const column = meta.column;
36
+ const view = `v_${meta.table}`;
37
+ // DuckDB cannot bind a parameter inside an INTERVAL literal, but it can
38
+ // multiply a static unit interval by a bound integer. Same trick the
39
+ // rules compiler uses.
40
+ const windowBind = `(INTERVAL 1 DAY) * $windowDays`;
41
+ let dailySelect;
42
+ let tsColumn;
43
+ if (aggregate === 'session') {
44
+ // Sleep is already attributed to a local night by the mapper's 6pm-6pm
45
+ // rule, so `night_of` is a better day key than re-deriving one from a
46
+ // timestamp — it is DST-correct by construction.
47
+ //
48
+ // NOTE: this groups, where the Sprint 5 design sketch skipped the
49
+ // GROUP BY for session metrics. Skipping it makes `count(*)` count
50
+ // SESSIONS, so a user with two sessions a night would satisfy the
51
+ // 20-day minimum after 10 nights — contradicting T-14. Summing per
52
+ // night also gives the right daily value: total sleep that night.
53
+ dailySelect = `night_of AS day, sum(${column}) AS daily_value`;
54
+ tsColumn = 'ts_start';
55
+ return `
56
+ WITH daily_values AS (
57
+ SELECT ${dailySelect}
58
+ FROM ${view}
59
+ WHERE user_id = $userId AND brand = $brand AND family_id = $familyId
60
+ AND ${tsColumn} > now() - ${windowBind}
61
+ GROUP BY night_of
62
+ )
63
+ ${quantileSelect()}
64
+ `.trim();
65
+ }
66
+ const fn = aggregate === 'sum' ? 'sum' : 'avg';
67
+ tsColumn = 'ts';
68
+ dailySelect
69
+ = `date_trunc('day', timezone($tz, ${tsColumn})) AS day, ${fn}(${column}) AS daily_value`;
70
+ return `
71
+ WITH daily_values AS (
72
+ SELECT ${dailySelect}
73
+ FROM ${view}
74
+ WHERE user_id = $userId AND brand = $brand AND family_id = $familyId
75
+ AND ${tsColumn} > now() - ${windowBind}
76
+ GROUP BY day
77
+ )
78
+ ${quantileSelect()}
79
+ `.trim();
80
+ }
81
+ /**
82
+ * The quantile pass, identical across aggregate types.
83
+ *
84
+ * `HAVING count(*) >= n` rather than `HAVING sample_count >= n`: the alias
85
+ * is not guaranteed visible in HAVING across DuckDB versions, and getting
86
+ * this wrong would silently write under-evidenced baselines.
87
+ */
88
+ function quantileSelect() {
89
+ return `SELECT
90
+ quantile_cont(daily_value, 0.05) AS p05,
91
+ quantile_cont(daily_value, 0.10) AS p10,
92
+ quantile_cont(daily_value, 0.50) AS p50,
93
+ quantile_cont(daily_value, 0.90) AS p90,
94
+ quantile_cont(daily_value, 0.95) AS p95,
95
+ avg(daily_value) AS mean,
96
+ stddev_samp(daily_value) AS stddev,
97
+ count(*) AS sample_count
98
+ FROM daily_values
99
+ HAVING count(*) >= ${BASELINE_MIN_DAYS}`;
100
+ }
101
+ /** UPSERT — recompute replaces the previous row for the same key. */
102
+ const UPSERT_SQL = `
103
+ INSERT INTO user_baseline (
104
+ brand, family_id, user_id, metric, window_days,
105
+ p05, p10, p50, p90, p95, mean, stddev, sample_count, computed_at
106
+ ) VALUES (
107
+ $brand, $familyId, $userId, $metric, $windowDays,
108
+ $p05, $p10, $p50, $p90, $p95, $mean, $stddev, $sampleCount, now()
109
+ )
110
+ ON CONFLICT (brand, family_id, user_id, metric, window_days) DO UPDATE SET
111
+ p05 = excluded.p05,
112
+ p10 = excluded.p10,
113
+ p50 = excluded.p50,
114
+ p90 = excluded.p90,
115
+ p95 = excluded.p95,
116
+ mean = excluded.mean,
117
+ stddev = excluded.stddev,
118
+ sample_count = excluded.sample_count,
119
+ computed_at = excluded.computed_at
120
+ `.trim();
121
+ export function createBaselineComputer(config) {
122
+ const { analytics, eventBus, logger } = config;
123
+ const metrics = config.metrics ?? getBaselineMetricIds();
124
+ const windows = config.windows ?? BASELINE_WINDOW_DAYS;
125
+ async function computeOne(metric, windowDays, ctx) {
126
+ const sql = buildBaselineSql(metric, windowDays);
127
+ const rows = await analytics.execute(sql, {
128
+ userId: ctx.userId,
129
+ brand: ctx.brand,
130
+ familyId: ctx.familyId,
131
+ tz: ctx.userTimezone,
132
+ windowDays,
133
+ });
134
+ // HAVING filtered it out — fewer than BASELINE_MIN_DAYS distinct days.
135
+ // Not an error: a new user simply has no baseline yet, and consumers
136
+ // render "still learning" rather than a wrong range.
137
+ const row = rows[0];
138
+ if (!row || row.sample_count === null || row.sample_count < BASELINE_MIN_DAYS) {
139
+ logger?.debug('baseline: insufficient days, row not written', {
140
+ metric,
141
+ windowDays,
142
+ userId: ctx.userId,
143
+ sampleCount: row?.sample_count ?? 0,
144
+ required: BASELINE_MIN_DAYS,
145
+ });
146
+ return false;
147
+ }
148
+ await analytics.execute(UPSERT_SQL, {
149
+ brand: ctx.brand,
150
+ familyId: ctx.familyId,
151
+ userId: ctx.userId,
152
+ metric,
153
+ windowDays,
154
+ p05: row.p05,
155
+ p10: row.p10,
156
+ p50: row.p50,
157
+ p90: row.p90,
158
+ p95: row.p95,
159
+ mean: row.mean,
160
+ stddev: row.stddev,
161
+ sampleCount: row.sample_count,
162
+ });
163
+ // Registry queries (`spo2.compareBaseline`, `spo2.baselineMaturity`)
164
+ // invalidate on this.
165
+ eventBus?.emit('user_baseline:updated', {
166
+ userId: ctx.userId,
167
+ metric,
168
+ windowDays,
169
+ sampleCount: row.sample_count,
170
+ computedAt: new Date().toISOString(),
171
+ });
172
+ return true;
173
+ }
174
+ return {
175
+ computeOne,
176
+ async computeAll(ctx) {
177
+ let computed = 0;
178
+ let skipped = 0;
179
+ let failed = 0;
180
+ for (const metric of metrics) {
181
+ for (const windowDays of windows) {
182
+ try {
183
+ const written = await computeOne(metric, windowDays, ctx);
184
+ if (written)
185
+ computed += 1;
186
+ else
187
+ skipped += 1;
188
+ }
189
+ catch (err) {
190
+ // One metric's failure must not abort the other twenty.
191
+ failed += 1;
192
+ logger?.warn('baseline: compute failed', {
193
+ metric,
194
+ windowDays,
195
+ userId: ctx.userId,
196
+ err: err instanceof Error ? err.message : String(err),
197
+ });
198
+ }
199
+ }
200
+ }
201
+ logger?.debug('baseline: compute cycle complete', {
202
+ userId: ctx.userId,
203
+ computed,
204
+ skipped,
205
+ failed,
206
+ });
207
+ return { computed, skipped, failed };
208
+ },
209
+ };
210
+ }
211
+ //# sourceMappingURL=baseline-compute.js.map