@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.
- package/README.md +23 -10
- package/UPGRADING.md +370 -0
- package/dist/core/device-events.d.ts +14 -0
- package/dist/core/device-events.d.ts.map +1 -0
- package/dist/core/device-events.js +13 -0
- package/dist/core/device-events.js.map +1 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/extensions.d.ts +21 -4
- package/dist/core/extensions.d.ts.map +1 -1
- package/dist/core/extensions.js +21 -4
- package/dist/core/extensions.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +125 -23
- package/dist/core/factory.js.map +1 -1
- package/dist/core/hooks.js +1 -1
- package/dist/core/hooks.js.map +1 -1
- package/dist/core/index.d.ts +11 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +14 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/insight.d.ts +44 -0
- package/dist/core/insight.d.ts.map +1 -0
- package/dist/core/insight.js +62 -0
- package/dist/core/insight.js.map +1 -0
- package/dist/core/metric_metadata.d.ts +55 -0
- package/dist/core/metric_metadata.d.ts.map +1 -1
- package/dist/core/metric_metadata.js +35 -7
- package/dist/core/metric_metadata.js.map +1 -1
- package/dist/core/migrations.d.ts.map +1 -1
- package/dist/core/migrations.js +64 -2
- package/dist/core/migrations.js.map +1 -1
- package/dist/core/retention.d.ts +44 -13
- package/dist/core/retention.d.ts.map +1 -1
- package/dist/core/retention.js +49 -22
- package/dist/core/retention.js.map +1 -1
- package/dist/core/schemas.d.ts +73 -2
- package/dist/core/schemas.d.ts.map +1 -1
- package/dist/core/schemas.js +154 -3
- package/dist/core/schemas.js.map +1 -1
- package/dist/core/table_metadata.d.ts.map +1 -1
- package/dist/core/table_metadata.js +3 -0
- package/dist/core/table_metadata.js.map +1 -1
- package/dist/core/types.d.ts +45 -10
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/warehouse.d.ts +28 -0
- package/dist/core/warehouse.d.ts.map +1 -1
- package/dist/core/warehouse.js +44 -0
- package/dist/core/warehouse.js.map +1 -1
- package/dist/core/watermark-cache.d.ts +75 -0
- package/dist/core/watermark-cache.d.ts.map +1 -0
- package/dist/core/watermark-cache.js +0 -0
- package/dist/core/watermark-cache.js.map +1 -0
- package/dist/rules/__fakes__/fakeEngine.d.ts +2 -0
- package/dist/rules/__fakes__/fakeEngine.d.ts.map +1 -1
- package/dist/rules/__fakes__/fakeEngine.js +4 -0
- package/dist/rules/__fakes__/fakeEngine.js.map +1 -1
- package/dist/rules/baseline-reader.d.ts +55 -0
- package/dist/rules/baseline-reader.d.ts.map +1 -0
- package/dist/rules/baseline-reader.js +111 -0
- package/dist/rules/baseline-reader.js.map +1 -0
- package/dist/rules/compiler.d.ts +36 -1
- package/dist/rules/compiler.d.ts.map +1 -1
- package/dist/rules/compiler.js +185 -39
- package/dist/rules/compiler.js.map +1 -1
- package/dist/rules/defaults/luminx.d.ts.map +1 -1
- package/dist/rules/defaults/luminx.js +8 -0
- package/dist/rules/defaults/luminx.js.map +1 -1
- package/dist/rules/defaults/viva.d.ts.map +1 -1
- package/dist/rules/defaults/viva.js +7 -0
- package/dist/rules/defaults/viva.js.map +1 -1
- package/dist/rules/defaults/yogaring.d.ts.map +1 -1
- package/dist/rules/defaults/yogaring.js +7 -0
- package/dist/rules/defaults/yogaring.js.map +1 -1
- package/dist/rules/defaults/ziva.d.ts.map +1 -1
- package/dist/rules/defaults/ziva.js +100 -24
- package/dist/rules/defaults/ziva.js.map +1 -1
- package/dist/rules/evaluator.d.ts +14 -3
- package/dist/rules/evaluator.d.ts.map +1 -1
- package/dist/rules/evaluator.js +157 -5
- package/dist/rules/evaluator.js.map +1 -1
- package/dist/rules/factory.d.ts.map +1 -1
- package/dist/rules/factory.js +5 -1
- package/dist/rules/factory.js.map +1 -1
- package/dist/rules/index.d.ts +2 -1
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +2 -1
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/schema.d.ts +75 -4
- package/dist/rules/schema.d.ts.map +1 -1
- package/dist/rules/schema.js +44 -0
- package/dist/rules/schema.js.map +1 -1
- package/dist/rules/types.d.ts +16 -1
- package/dist/rules/types.d.ts.map +1 -1
- package/dist/rules/validator.d.ts.map +1 -1
- package/dist/rules/validator.js +56 -0
- package/dist/rules/validator.js.map +1 -1
- package/dist/sync/baseline-compute.d.ts +63 -0
- package/dist/sync/baseline-compute.d.ts.map +1 -0
- package/dist/sync/baseline-compute.js +211 -0
- package/dist/sync/baseline-compute.js.map +1 -0
- package/dist/sync/events.d.ts +19 -5
- package/dist/sync/events.d.ts.map +1 -1
- package/dist/sync/events.js +17 -0
- package/dist/sync/events.js.map +1 -1
- package/dist/sync/factory.d.ts +25 -4
- package/dist/sync/factory.d.ts.map +1 -1
- package/dist/sync/factory.js +108 -15
- package/dist/sync/factory.js.map +1 -1
- package/dist/sync/fetcher.d.ts +15 -2
- package/dist/sync/fetcher.d.ts.map +1 -1
- package/dist/sync/fetcher.js +43 -7
- package/dist/sync/fetcher.js.map +1 -1
- package/dist/sync/flusher.d.ts +52 -2
- package/dist/sync/flusher.d.ts.map +1 -1
- package/dist/sync/flusher.js +78 -9
- package/dist/sync/flusher.js.map +1 -1
- package/dist/sync/index.d.ts +3 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -0
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/mapper/activity.d.ts.map +1 -1
- package/dist/sync/mapper/activity.js +2 -1
- package/dist/sync/mapper/activity.js.map +1 -1
- package/dist/sync/mapper/ring-config.js +1 -1
- package/dist/sync/mapper/ring-config.js.map +1 -1
- package/dist/sync/mapper/schema.d.ts +411 -0
- package/dist/sync/mapper/schema.d.ts.map +1 -0
- package/dist/sync/mapper/schema.js +128 -0
- package/dist/sync/mapper/schema.js.map +1 -0
- package/dist/sync/mapper/sleep.d.ts +58 -13
- package/dist/sync/mapper/sleep.d.ts.map +1 -1
- package/dist/sync/mapper/sleep.js +141 -44
- package/dist/sync/mapper/sleep.js.map +1 -1
- package/dist/sync/mapper/types.d.ts +66 -10
- package/dist/sync/mapper/types.d.ts.map +1 -1
- package/dist/sync/scheduler.d.ts +7 -0
- package/dist/sync/scheduler.d.ts.map +1 -1
- package/dist/sync/scheduler.js +14 -0
- package/dist/sync/scheduler.js.map +1 -1
- package/dist/tools/__fakes__/engine.d.ts +2 -0
- package/dist/tools/__fakes__/engine.d.ts.map +1 -1
- package/dist/tools/__fakes__/engine.js +10 -0
- package/dist/tools/__fakes__/engine.js.map +1 -1
- package/dist/tools/authorize.d.ts +19 -12
- package/dist/tools/authorize.d.ts.map +1 -1
- package/dist/tools/authorize.js +31 -40
- package/dist/tools/authorize.js.map +1 -1
- package/dist/tools/factory.d.ts +2 -0
- package/dist/tools/factory.d.ts.map +1 -1
- package/dist/tools/factory.js +12 -1
- package/dist/tools/factory.js.map +1 -1
- package/dist/tools/formatters/copy-guidelines.d.ts +57 -0
- package/dist/tools/formatters/copy-guidelines.d.ts.map +1 -0
- package/dist/tools/formatters/copy-guidelines.js +110 -0
- package/dist/tools/formatters/copy-guidelines.js.map +1 -0
- package/dist/tools/formatters/index.d.ts +14 -0
- package/dist/tools/formatters/index.d.ts.map +1 -0
- package/dist/tools/formatters/index.js +14 -0
- package/dist/tools/formatters/index.js.map +1 -0
- package/dist/tools/formatters/spo2.d.ts +21 -0
- package/dist/tools/formatters/spo2.d.ts.map +1 -0
- package/dist/tools/formatters/spo2.js +79 -0
- package/dist/tools/formatters/spo2.js.map +1 -0
- package/dist/tools/{formatters.d.ts → formatters/text.d.ts} +1 -1
- package/dist/tools/formatters/text.d.ts.map +1 -0
- package/dist/tools/{formatters.js → formatters/text.js} +1 -1
- package/dist/tools/formatters/text.js.map +1 -0
- package/dist/tools/impls/activity.d.ts +2 -2
- package/dist/tools/impls/activity.js +7 -3
- package/dist/tools/impls/activity.js.map +1 -1
- package/dist/tools/impls/anomaly.d.ts +2 -2
- package/dist/tools/impls/anomaly.js +8 -4
- package/dist/tools/impls/anomaly.js.map +1 -1
- package/dist/tools/impls/compare.d.ts +2 -2
- package/dist/tools/impls/compare.js +8 -4
- package/dist/tools/impls/compare.js.map +1 -1
- package/dist/tools/impls/hrv.d.ts +2 -2
- package/dist/tools/impls/hrv.js +6 -2
- package/dist/tools/impls/hrv.js.map +1 -1
- package/dist/tools/impls/insights.d.ts +4 -4
- package/dist/tools/impls/insights.d.ts.map +1 -1
- package/dist/tools/impls/insights.js +10 -3
- package/dist/tools/impls/insights.js.map +1 -1
- package/dist/tools/impls/sleep.d.ts +2 -2
- package/dist/tools/impls/sleep.js +6 -2
- package/dist/tools/impls/sleep.js.map +1 -1
- package/dist/tools/impls/spo2.d.ts +52 -0
- package/dist/tools/impls/spo2.d.ts.map +1 -0
- package/dist/tools/impls/spo2.js +65 -0
- package/dist/tools/impls/spo2.js.map +1 -0
- package/dist/tools/index.d.ts +1 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/mcp/guard.d.ts +28 -11
- package/dist/tools/mcp/guard.d.ts.map +1 -1
- package/dist/tools/mcp/guard.js +45 -15
- package/dist/tools/mcp/guard.js.map +1 -1
- package/dist/tools/mcp/index.d.ts +1 -1
- package/dist/tools/mcp/index.d.ts.map +1 -1
- package/dist/tools/mcp/index.js +1 -1
- package/dist/tools/mcp/index.js.map +1 -1
- package/dist/tools/mcp/server.d.ts.map +1 -1
- package/dist/tools/mcp/server.js +4 -0
- package/dist/tools/mcp/server.js.map +1 -1
- package/dist/tools/mcp/transports/http.d.ts.map +1 -1
- package/dist/tools/mcp/transports/http.js +4 -0
- package/dist/tools/mcp/transports/http.js.map +1 -1
- package/package.json +8 -5
- package/dist/tools/formatters.d.ts.map +0 -1
- package/dist/tools/formatters.js.map +0 -1
package/dist/rules/schema.d.ts
CHANGED
|
@@ -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" | "
|
|
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;
|
|
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"}
|
package/dist/rules/schema.js
CHANGED
|
@@ -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,
|
package/dist/rules/schema.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/rules/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAAE,mBAAmB,EAAiB,MAAM,yBAAyB,CAAA;AAE5E,+DAA+D;AAC/D,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAC5C,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;IACnC,CAAC;CACF;AAED,MAAM,gBAAgB,GAAG,mBAAmB,EAA+B,CAAA;AAE3E,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,CAAU,CAAA;AAGtE,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAU,CAAA;AAGlF,MAAM,CAAC,MAAM,QAAQ,GAAG;IACtB,WAAW;IACX,cAAc;IACd,QAAQ;IACR,YAAY;IACZ,SAAS;CACD,CAAA;AAGV,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAU,CAAA;AAG/D,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9B,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;IAC3B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;CAClB,CAAC,CAAA;AAEF,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,kBAAkB,CAAC;IACnC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACvC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC/B,CAAC,CAAA;AAEF,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IACpC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC;IAClC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IACvC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;CACpB,CAAC,CAAA;AAEF,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3B,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;IACxB,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;IACf,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE;CAChB,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,kBAAkB,CAAC,MAAM,EAAE;IACvD,cAAc;IACd,qBAAqB;IACrB,oBAAoB;IACpB,WAAW;
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/rules/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/rules/types.d.ts
CHANGED
|
@@ -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":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,qBAAqB,EACrB,OAAO,EACP,WAAW,EACZ,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,UAAU,CAAA;AAEpC,YAAY,EAAE,WAAW,EAAE,CAAA;AAE3B,2DAA2D;AAC3D,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAA;AAEvD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,eAAe,EAAE,MAAM,EAAE,CAAA;IACzB,cAAc,EAAE,SAAS,EAAE,CAAA;CAC5B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,QAAQ,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,oFAAoF;IACpF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAA;IACvC,WAAW,EAAE,MAAM,CAAA;
|
|
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,
|
|
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"}
|
package/dist/rules/validator.js
CHANGED
|
@@ -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":"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"}
|
|
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
|