@mongrov/analytics 0.5.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 +124 -18
- 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/machine.d.ts.map +1 -1
- package/dist/core/machine.js +9 -1
- package/dist/core/machine.js.map +1 -1
- package/dist/core/metric_metadata.d.ts +57 -2
- package/dist/core/metric_metadata.d.ts.map +1 -1
- package/dist/core/metric_metadata.js +36 -8
- package/dist/core/metric_metadata.js.map +1 -1
- package/dist/core/migrations.d.ts +6 -0
- package/dist/core/migrations.d.ts.map +1 -1
- package/dist/core/migrations.js +92 -4
- 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 +50 -22
- package/dist/core/retention.js.map +1 -1
- package/dist/core/schemas.d.ts +74 -3
- package/dist/core/schemas.d.ts.map +1 -1
- package/dist/core/schemas.js +168 -5
- package/dist/core/schemas.js.map +1 -1
- package/dist/core/table_metadata.d.ts.map +1 -1
- package/dist/core/table_metadata.js +4 -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 +78 -7
- 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 +109 -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 +59 -11
- 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 +4 -2
- 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/battery.d.ts +14 -7
- package/dist/sync/mapper/battery.d.ts.map +1 -1
- package/dist/sync/mapper/battery.js +13 -7
- package/dist/sync/mapper/battery.js.map +1 -1
- package/dist/sync/mapper/firmware.d.ts.map +1 -1
- package/dist/sync/mapper/firmware.js +4 -1
- package/dist/sync/mapper/firmware.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 +75 -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/compiler.js
CHANGED
|
@@ -17,6 +17,21 @@
|
|
|
17
17
|
* observed_value NUMERIC, threshold_value NUMERIC
|
|
18
18
|
* so the evaluator can build `RuleViolation` without dispatching per
|
|
19
19
|
* target type.
|
|
20
|
+
*
|
|
21
|
+
* ## v0.2.0 additions (Sprint 5 §4)
|
|
22
|
+
*
|
|
23
|
+
* - **`context`** (T-17) — an INNER JOIN, not a post-filter. "SpO₂ during
|
|
24
|
+
* sleep" must aggregate over sleep samples only; computing over
|
|
25
|
+
* everything and labelling the result would report a number nobody asked
|
|
26
|
+
* for.
|
|
27
|
+
* - **`consecutive: n`** (T-18) — run-length detection via gaps-and-islands.
|
|
28
|
+
* `n <= 1` stays on the aggregate path, because "any single breach" is
|
|
29
|
+
* already expressible as `aggregation: 'min'` + `less_than`.
|
|
30
|
+
* - **`target.type: 'user_setting'`** (T-19) — emits a `$userSettingValue`
|
|
31
|
+
* placeholder the evaluator binds from KVStore at eval time. The compiled
|
|
32
|
+
* SQL is threshold-agnostic and therefore cacheable across users.
|
|
33
|
+
* - **Views** (T-20) — all generated SQL reads `v_{table}`, never
|
|
34
|
+
* `local.{table}` or `r2.default.{table}` (principle 19).
|
|
20
35
|
*/
|
|
21
36
|
import { METRIC_METADATA } from '../core/metric_metadata';
|
|
22
37
|
import { RuleValidationError, } from './schema';
|
|
@@ -24,6 +39,14 @@ import { RuleValidationError, } from './schema';
|
|
|
24
39
|
export function sanitizeIdent(raw) {
|
|
25
40
|
return raw.replace(/[^A-Za-z0-9_]/g, '_');
|
|
26
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Union-view name for a table (T-20, principle 19). Reading the view is
|
|
44
|
+
* what makes a rule see rows flushed locally but not yet pushed to R2 —
|
|
45
|
+
* i.e. this morning's sync, which is exactly the data a rule fires on.
|
|
46
|
+
*/
|
|
47
|
+
export function viewFor(table) {
|
|
48
|
+
return `v_${sanitizeIdent(table)}`;
|
|
49
|
+
}
|
|
27
50
|
/** Timestamp column per table — `sleep_session` uses `ts_start`. */
|
|
28
51
|
function tsColumn(table) {
|
|
29
52
|
return table === 'sleep_session' ? 'ts_start' : 'ts';
|
|
@@ -63,6 +86,37 @@ function compareClause(compare, lhs, rhs) {
|
|
|
63
86
|
case 'between': throw new Error('between handled per-target');
|
|
64
87
|
}
|
|
65
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* T-17 — INNER JOIN restricting samples to a physiological context.
|
|
91
|
+
*
|
|
92
|
+
* Aliased `m` for the metric relation throughout. Both joins carry the
|
|
93
|
+
* full tenant triple, not just `user_id`: a join on user alone would match
|
|
94
|
+
* another brand's sleep sessions for the same person on a multi-brand
|
|
95
|
+
* install.
|
|
96
|
+
*/
|
|
97
|
+
export function emitContextJoin(context) {
|
|
98
|
+
switch (context) {
|
|
99
|
+
case 'any':
|
|
100
|
+
return '';
|
|
101
|
+
case 'asleep':
|
|
102
|
+
return `
|
|
103
|
+
INNER JOIN ${viewFor('sleep_session')} s
|
|
104
|
+
ON s.user_id = m.user_id
|
|
105
|
+
AND s.brand = m.brand
|
|
106
|
+
AND s.family_id = m.family_id
|
|
107
|
+
AND m.ts BETWEEN s.ts_start AND s.ts_end`;
|
|
108
|
+
case 'resting':
|
|
109
|
+
// Activity rows are 1-minute after unnest, so a sample is "resting"
|
|
110
|
+
// when its own minute recorded zero steps.
|
|
111
|
+
return `
|
|
112
|
+
INNER JOIN ${viewFor('activity')} a
|
|
113
|
+
ON a.user_id = m.user_id
|
|
114
|
+
AND a.brand = m.brand
|
|
115
|
+
AND a.family_id = m.family_id
|
|
116
|
+
AND a.ts = date_trunc('minute', m.ts)
|
|
117
|
+
AND a.steps = 0`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
66
120
|
const ALLOWED_RAW_PLACEHOLDERS = new Set(['userId', 'brand', 'familyId']);
|
|
67
121
|
/** Extract every `$name` placeholder from a SQL string. */
|
|
68
122
|
function placeholdersOf(sql) {
|
|
@@ -76,40 +130,50 @@ function placeholdersOf(sql) {
|
|
|
76
130
|
}
|
|
77
131
|
return out;
|
|
78
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Placeholder the evaluator binds from KVStore for `user_setting` targets
|
|
135
|
+
* (T-19/T-23). Exported so the evaluator and its tests agree on the name.
|
|
136
|
+
*/
|
|
137
|
+
export const USER_SETTING_PARAM = 'userSettingValue';
|
|
79
138
|
/** Compile a validated rule to a `CompiledRule`. */
|
|
80
139
|
export function compileRule(rule) {
|
|
81
140
|
const meta = METRIC_METADATA[rule.metric];
|
|
82
141
|
const rawTable = meta.table;
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const column = sanitizeIdent(rawColumn);
|
|
142
|
+
const column = sanitizeIdent(meta.column);
|
|
143
|
+
const view = viewFor(rawTable);
|
|
86
144
|
if (rule.rawSql) {
|
|
87
145
|
return compileRawSql(rule, rule.rawSql);
|
|
88
146
|
}
|
|
89
147
|
const ts = tsColumn(rawTable);
|
|
90
148
|
const interval = windowInterval(rule.window);
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
149
|
+
const join = emitContextJoin(rule.context);
|
|
150
|
+
const description = `${rule.metric} ${rule.aggregation} over ${rule.window} ${rule.compare} `
|
|
151
|
+
+ `${describeTarget(rule.target)}`
|
|
152
|
+
+ (rule.context === 'any' ? '' : ` [${rule.context}]`)
|
|
153
|
+
+ (rule.consecutive && rule.consecutive > 1 ? ` x${rule.consecutive} consecutive` : '');
|
|
154
|
+
const args = {
|
|
155
|
+
view,
|
|
156
|
+
join,
|
|
98
157
|
ts,
|
|
99
158
|
interval,
|
|
100
159
|
column,
|
|
101
|
-
agg,
|
|
102
|
-
filter,
|
|
160
|
+
agg: aggExpr(rule.aggregation, `m.${column}`, `m.${ts}`),
|
|
103
161
|
target: rule.target,
|
|
104
162
|
compare: rule.compare,
|
|
105
|
-
|
|
106
|
-
|
|
163
|
+
consecutive: rule.consecutive,
|
|
164
|
+
};
|
|
165
|
+
const { sql, params } = rule.consecutive && rule.consecutive > 1
|
|
166
|
+
? buildConsecutive(args)
|
|
167
|
+
: buildForTarget(args);
|
|
107
168
|
return {
|
|
108
169
|
ruleId: rule.id,
|
|
109
170
|
metric: rule.metric,
|
|
110
171
|
sql,
|
|
111
172
|
params,
|
|
112
173
|
description,
|
|
174
|
+
/** Evaluator binds `$userSettingValue` from this key when present. */
|
|
175
|
+
userSettingKey: rule.target.type === 'user_setting' ? rule.target.key : undefined,
|
|
176
|
+
userSettingDefault: rule.target.type === 'user_setting' ? rule.target.defaultValue : undefined,
|
|
113
177
|
};
|
|
114
178
|
}
|
|
115
179
|
function describeTarget(target) {
|
|
@@ -118,18 +182,36 @@ function describeTarget(target) {
|
|
|
118
182
|
case 'baseline_percent': return `${target.percent}% of ${target.windowDays}d baseline`;
|
|
119
183
|
case 'baseline_stddev': return `${target.stddevs} stddev over ${target.windowDays}d baseline`;
|
|
120
184
|
case 'range': return `[${target.min}, ${target.max}]`;
|
|
185
|
+
case 'user_setting': return `user setting ${target.key} (default ${target.defaultValue})`;
|
|
121
186
|
}
|
|
122
187
|
}
|
|
188
|
+
/** Tenant + window predicate, shared by both paths. */
|
|
189
|
+
function whereClause(ts, interval) {
|
|
190
|
+
return (`WHERE m.user_id = $userId AND m.brand = $brand AND m.family_id = $familyId\n`
|
|
191
|
+
+ ` AND m.${ts} > NOW() - ${interval}`);
|
|
192
|
+
}
|
|
123
193
|
function buildForTarget(args) {
|
|
124
|
-
const {
|
|
194
|
+
const { view, join, ts, interval, column, agg, target, compare } = args;
|
|
195
|
+
const where = whereClause(ts, interval);
|
|
196
|
+
const from = `FROM ${view} m${join}`;
|
|
125
197
|
if (target.type === 'absolute') {
|
|
126
198
|
const params = { threshold_absolute: target.value };
|
|
127
199
|
const sql = `SELECT ${agg} AS observed_value, $threshold_absolute AS threshold_value
|
|
128
|
-
|
|
129
|
-
${
|
|
200
|
+
${from}
|
|
201
|
+
${where}
|
|
130
202
|
HAVING ${compareClause(compare, 'observed_value', '$threshold_absolute')};`;
|
|
131
203
|
return { sql, params };
|
|
132
204
|
}
|
|
205
|
+
if (target.type === 'user_setting') {
|
|
206
|
+
// T-19: threshold is a bound param resolved at eval time, so the same
|
|
207
|
+
// compiled SQL serves every user in the family and survives the user
|
|
208
|
+
// changing their setting — no cache invalidation needed.
|
|
209
|
+
const sql = `SELECT ${agg} AS observed_value, $${USER_SETTING_PARAM} AS threshold_value
|
|
210
|
+
${from}
|
|
211
|
+
${where}
|
|
212
|
+
HAVING ${compareClause(compare, 'observed_value', `$${USER_SETTING_PARAM}`)};`;
|
|
213
|
+
return { sql, params: {} };
|
|
214
|
+
}
|
|
133
215
|
if (target.type === 'range') {
|
|
134
216
|
const params = { range_min: target.min, range_max: target.max };
|
|
135
217
|
// For range, `between` violates on values OUTSIDE the range; other
|
|
@@ -139,8 +221,8 @@ HAVING ${compareClause(compare, 'observed_value', '$threshold_absolute')};`;
|
|
|
139
221
|
? `NOT (observed_value BETWEEN $range_min AND $range_max)`
|
|
140
222
|
: compareClause(compare, 'observed_value', '$range_min');
|
|
141
223
|
const sql = `SELECT ${agg} AS observed_value, $range_min AS threshold_value
|
|
142
|
-
|
|
143
|
-
${
|
|
224
|
+
${from}
|
|
225
|
+
${where}
|
|
144
226
|
HAVING ${outside};`;
|
|
145
227
|
return { sql, params };
|
|
146
228
|
}
|
|
@@ -148,42 +230,106 @@ HAVING ${outside};`;
|
|
|
148
230
|
// DuckDB can't parametrize inside an INTERVAL literal, but it can
|
|
149
231
|
// multiply a static unit interval by a bound integer.
|
|
150
232
|
const baselineDaysBind = `(INTERVAL 1 DAY) * $baselineDays`;
|
|
233
|
+
const baselineFrom = `FROM ${view} m${join}`;
|
|
234
|
+
const baselineWhere = `WHERE m.user_id = $userId AND m.brand = $brand AND m.family_id = $familyId\n`
|
|
235
|
+
+ ` AND m.${ts} > NOW() - ${baselineDaysBind}`;
|
|
151
236
|
if (target.type === 'baseline_percent') {
|
|
152
|
-
const params = {
|
|
153
|
-
baselineDays: target.windowDays,
|
|
154
|
-
pct: target.percent,
|
|
155
|
-
};
|
|
237
|
+
const params = { baselineDays: target.windowDays, pct: target.percent };
|
|
156
238
|
const sql = `WITH baseline AS (
|
|
157
|
-
SELECT AVG(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
AND ${ts} > NOW() - ${baselineDaysBind}
|
|
239
|
+
SELECT AVG(m.${column}) AS mean
|
|
240
|
+
${baselineFrom}
|
|
241
|
+
${baselineWhere}
|
|
161
242
|
)
|
|
162
243
|
SELECT ${agg} AS observed_value,
|
|
163
244
|
(SELECT mean FROM baseline) * ($pct / 100.0) AS threshold_value
|
|
164
|
-
|
|
165
|
-
${
|
|
245
|
+
${from}
|
|
246
|
+
${where}
|
|
166
247
|
HAVING ${compareClause(compare, 'observed_value', 'threshold_value')};`;
|
|
167
248
|
return { sql, params };
|
|
168
249
|
}
|
|
169
250
|
// baseline_stddev
|
|
170
|
-
const params = {
|
|
171
|
-
baselineDays: target.windowDays,
|
|
172
|
-
stddevs: target.stddevs,
|
|
173
|
-
};
|
|
251
|
+
const params = { baselineDays: target.windowDays, stddevs: target.stddevs };
|
|
174
252
|
const sql = `WITH baseline AS (
|
|
175
|
-
SELECT AVG(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
AND ${ts} > NOW() - ${baselineDaysBind}
|
|
253
|
+
SELECT AVG(m.${column}) AS mean, stddev_pop(m.${column}) AS sd
|
|
254
|
+
${baselineFrom}
|
|
255
|
+
${baselineWhere}
|
|
179
256
|
)
|
|
180
257
|
SELECT ${agg} AS observed_value,
|
|
181
258
|
(SELECT mean + $stddevs * sd FROM baseline) AS threshold_value
|
|
182
|
-
|
|
183
|
-
${
|
|
259
|
+
${from}
|
|
260
|
+
${where}
|
|
184
261
|
HAVING ${compareClause(compare, 'observed_value', 'threshold_value')};`;
|
|
185
262
|
return { sql, params };
|
|
186
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* T-18 — `consecutive: n` via gaps-and-islands.
|
|
266
|
+
*
|
|
267
|
+
* A run of adjacent breaching samples is identified by the classic
|
|
268
|
+
* difference of two row numbers: one over all samples in time order, one
|
|
269
|
+
* partitioned by the breach flag. Within a maximal run of same-flag rows
|
|
270
|
+
* that difference is constant, so it serves as a group key.
|
|
271
|
+
*
|
|
272
|
+
* The rule fires when ANY breaching run reaches length `n`. `observed_value`
|
|
273
|
+
* is the run's extreme in the direction of the comparison — the worst
|
|
274
|
+
* reading in the run, which is what a user-facing card should quote.
|
|
275
|
+
*
|
|
276
|
+
* Only supported for the threshold-style targets. Baseline targets would
|
|
277
|
+
* need the baseline resolved per-sample rather than per-window, which is a
|
|
278
|
+
* different query shape and has no consumer in v0.2.0 — the validator
|
|
279
|
+
* rejects the combination rather than silently emitting something subtly
|
|
280
|
+
* wrong.
|
|
281
|
+
*/
|
|
282
|
+
function buildConsecutive(args) {
|
|
283
|
+
const { view, join, ts, interval, column, target, compare, consecutive } = args;
|
|
284
|
+
const where = whereClause(ts, interval);
|
|
285
|
+
const from = `FROM ${view} m${join}`;
|
|
286
|
+
let thresholdExpr;
|
|
287
|
+
let params = {};
|
|
288
|
+
if (target.type === 'absolute') {
|
|
289
|
+
thresholdExpr = '$threshold_absolute';
|
|
290
|
+
params = { threshold_absolute: target.value };
|
|
291
|
+
}
|
|
292
|
+
else if (target.type === 'user_setting') {
|
|
293
|
+
thresholdExpr = `$${USER_SETTING_PARAM}`;
|
|
294
|
+
}
|
|
295
|
+
else if (target.type === 'range') {
|
|
296
|
+
thresholdExpr = '$range_min';
|
|
297
|
+
params = { range_min: target.min, range_max: target.max };
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
throw new RuleValidationError(`consecutive is not supported with target.type '${target.type}' — `
|
|
301
|
+
+ `baseline targets resolve per-window, not per-sample. Use an absolute `
|
|
302
|
+
+ `or user_setting target, or drop consecutive.`);
|
|
303
|
+
}
|
|
304
|
+
// Worst reading in the run, in the direction of the comparison.
|
|
305
|
+
const extreme = compare === 'greater_than' ? 'MAX' : 'MIN';
|
|
306
|
+
const breach = compare === 'between'
|
|
307
|
+
? `NOT (m.${column} BETWEEN $range_min AND $range_max)`
|
|
308
|
+
: compareClause(compare, `m.${column}`, thresholdExpr);
|
|
309
|
+
const sql = `WITH samples AS (
|
|
310
|
+
SELECT m.${ts} AS ts,
|
|
311
|
+
m.${column} AS value,
|
|
312
|
+
(${breach}) AS breached,
|
|
313
|
+
${thresholdExpr} AS threshold_value
|
|
314
|
+
${from}
|
|
315
|
+
${where}
|
|
316
|
+
),
|
|
317
|
+
runs AS (
|
|
318
|
+
SELECT ts, value, breached, threshold_value,
|
|
319
|
+
ROW_NUMBER() OVER (ORDER BY ts)
|
|
320
|
+
- ROW_NUMBER() OVER (PARTITION BY breached ORDER BY ts) AS run_key
|
|
321
|
+
FROM samples
|
|
322
|
+
)
|
|
323
|
+
SELECT ${extreme}(value) AS observed_value,
|
|
324
|
+
ANY_VALUE(threshold_value) AS threshold_value
|
|
325
|
+
FROM runs
|
|
326
|
+
WHERE breached
|
|
327
|
+
GROUP BY run_key
|
|
328
|
+
HAVING COUNT(*) >= $consecutive
|
|
329
|
+
ORDER BY observed_value ${compare === 'greater_than' ? 'DESC' : 'ASC'}
|
|
330
|
+
LIMIT 1;`;
|
|
331
|
+
return { sql, params: { ...params, consecutive: consecutive } };
|
|
332
|
+
}
|
|
187
333
|
/**
|
|
188
334
|
* Pass-through path with allow-list check. Placeholders must be one of
|
|
189
335
|
* {userId, brand, familyId} or declared in `rule.rawSqlParams`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compiler.js","sourceRoot":"","sources":["../../src/rules/compiler.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"compiler.js","sourceRoot":"","sources":["../../src/rules/compiler.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"luminx.d.ts","sourceRoot":"","sources":["../../../src/rules/defaults/luminx.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"luminx.d.ts","sourceRoot":"","sources":["../../../src/rules/defaults/luminx.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAqErC,eAAO,MAAM,cAAc,EAAE,IAAI,EAA2C,CAAA"}
|
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { parseCatalog } from './loader';
|
|
5
5
|
const TOML = `
|
|
6
|
+
# LuminX brand default rules.
|
|
7
|
+
#
|
|
8
|
+
# LuminX rules are asset-side: battery low, prolonged disconnect, and
|
|
9
|
+
# firmware/status. All three run against the \`device_event\` table, whose
|
|
10
|
+
# metric proxy \`device_battery\` gates evaluation on batch.
|
|
11
|
+
#
|
|
12
|
+
# See \`luminx.ts\` — this file is the source of truth.
|
|
13
|
+
|
|
6
14
|
[[rule]]
|
|
7
15
|
id = "luminx.battery-low"
|
|
8
16
|
brand = "luminx"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"luminx.js","sourceRoot":"","sources":["../../../src/rules/defaults/luminx.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"luminx.js","sourceRoot":"","sources":["../../../src/rules/defaults/luminx.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiEZ,CAAA;AAED,MAAM,CAAC,MAAM,cAAc,GAAW,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viva.d.ts","sourceRoot":"","sources":["../../../src/rules/defaults/viva.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"viva.d.ts","sourceRoot":"","sources":["../../../src/rules/defaults/viva.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAiDrC,eAAO,MAAM,YAAY,EAAE,IAAI,EAAyC,CAAA"}
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { parseCatalog } from './loader';
|
|
5
5
|
const TOML = `
|
|
6
|
+
# Viva brand default rules (placeholder set for v0.1.0).
|
|
7
|
+
#
|
|
8
|
+
# Viva is asset-focused; rules mirror the LuminX asset-threshold pattern
|
|
9
|
+
# for battery + connectivity. Refined once product surfaces land.
|
|
10
|
+
#
|
|
11
|
+
# See \`viva.ts\` — this file is the source of truth.
|
|
12
|
+
|
|
6
13
|
[[rule]]
|
|
7
14
|
id = "viva.battery-low"
|
|
8
15
|
brand = "viva"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"viva.js","sourceRoot":"","sources":["../../../src/rules/defaults/viva.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"viva.js","sourceRoot":"","sources":["../../../src/rules/defaults/viva.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CZ,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAW,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yogaring.d.ts","sourceRoot":"","sources":["../../../src/rules/defaults/yogaring.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"yogaring.d.ts","sourceRoot":"","sources":["../../../src/rules/defaults/yogaring.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAiDrC,eAAO,MAAM,gBAAgB,EAAE,IAAI,EAA6C,CAAA"}
|
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { parseCatalog } from './loader';
|
|
5
5
|
const TOML = `
|
|
6
|
+
# YogaRing brand default rules (placeholder set for v0.1.0).
|
|
7
|
+
#
|
|
8
|
+
# YogaRing focuses on session-adjacent metrics; v0.1.0 ships a stress
|
|
9
|
+
# nudge + a low-activity nudge derived from the shared metric surface.
|
|
10
|
+
#
|
|
11
|
+
# See \`yogaring.ts\` — this file is the source of truth.
|
|
12
|
+
|
|
6
13
|
[[rule]]
|
|
7
14
|
id = "yogaring.stress-elevated"
|
|
8
15
|
brand = "yogaring"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"yogaring.js","sourceRoot":"","sources":["../../../src/rules/defaults/yogaring.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"yogaring.js","sourceRoot":"","sources":["../../../src/rules/defaults/yogaring.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6CZ,CAAA;AAED,MAAM,CAAC,MAAM,gBAAgB,GAAW,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ziva.d.ts","sourceRoot":"","sources":["../../../src/rules/defaults/ziva.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"ziva.d.ts","sourceRoot":"","sources":["../../../src/rules/defaults/ziva.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AA4JrC,eAAO,MAAM,YAAY,EAAE,IAAI,EAAyC,CAAA"}
|
|
@@ -7,11 +7,26 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { parseCatalog } from './loader';
|
|
9
9
|
const TOML = `
|
|
10
|
+
# Ziva brand default rules.
|
|
11
|
+
#
|
|
12
|
+
# Copied verbatim into \`ziva.ts\` as an inline template string. Do NOT edit
|
|
13
|
+
# the wrapper file directly — treat this .toml as the source of truth.
|
|
14
|
+
#
|
|
15
|
+
# Rules cover the core Ziva health surface: HRV drop, sleep deprivation,
|
|
16
|
+
# elevated stress, and low weekly activity. Verbatim from
|
|
17
|
+
# \`.specifica/features/analytics-rules/spec.md\` §Brand defaults.
|
|
18
|
+
#
|
|
19
|
+
# Two of these deliberately use relative targets rather than absolutes:
|
|
20
|
+
# \`baseline_percent\` (HRV) and \`baseline_stddev\` (stress). A fixed HRV
|
|
21
|
+
# threshold is meaningless across users — 30 ms is alarming for one person
|
|
22
|
+
# and unremarkable for another — so the shipped catalog exercises the
|
|
23
|
+
# compiler's baseline paths, not just the absolute path.
|
|
24
|
+
|
|
10
25
|
[[rule]]
|
|
11
26
|
id = "ziva.hrv-drop-30"
|
|
12
27
|
brand = "ziva"
|
|
13
|
-
name = "HRV drop
|
|
14
|
-
description = "
|
|
28
|
+
name = "Sudden HRV drop"
|
|
29
|
+
description = "HRV dropped 30% below 7-day baseline"
|
|
15
30
|
metric = "hrv_ms"
|
|
16
31
|
window = "24h"
|
|
17
32
|
aggregation = "avg"
|
|
@@ -19,65 +34,126 @@ compare = "less_than"
|
|
|
19
34
|
severity = "warn"
|
|
20
35
|
|
|
21
36
|
[rule.target]
|
|
22
|
-
type = "
|
|
23
|
-
|
|
37
|
+
type = "baseline_percent"
|
|
38
|
+
windowDays = 7
|
|
39
|
+
percent = 70
|
|
24
40
|
|
|
25
41
|
[rule.throttle]
|
|
26
|
-
minGapMinutes =
|
|
27
|
-
maxPerDay =
|
|
42
|
+
minGapMinutes = 720
|
|
43
|
+
maxPerDay = 1
|
|
28
44
|
|
|
29
45
|
[[rule]]
|
|
30
|
-
id = "ziva.sleep-deprivation-
|
|
46
|
+
id = "ziva.sleep-deprivation-3"
|
|
31
47
|
brand = "ziva"
|
|
32
|
-
name = "
|
|
33
|
-
description = "Average nightly sleep across 3 nights below
|
|
48
|
+
name = "Three nights short sleep"
|
|
49
|
+
description = "Average nightly sleep across 3 nights below 300 minutes."
|
|
34
50
|
metric = "sleep_total_minutes"
|
|
35
51
|
window = "3d"
|
|
36
52
|
aggregation = "avg"
|
|
37
53
|
compare = "less_than"
|
|
38
|
-
severity = "
|
|
54
|
+
severity = "info"
|
|
39
55
|
|
|
40
56
|
[rule.target]
|
|
41
57
|
type = "absolute"
|
|
42
|
-
value =
|
|
58
|
+
value = 300
|
|
43
59
|
|
|
44
60
|
[rule.throttle]
|
|
45
|
-
minGapMinutes =
|
|
61
|
+
minGapMinutes = 1440
|
|
46
62
|
maxPerDay = 1
|
|
47
63
|
|
|
48
64
|
[[rule]]
|
|
49
|
-
id = "ziva.stress-elevated"
|
|
65
|
+
id = "ziva.stress-elevated-day"
|
|
50
66
|
brand = "ziva"
|
|
51
|
-
name = "Elevated stress"
|
|
52
|
-
description = "
|
|
67
|
+
name = "Elevated stress today"
|
|
68
|
+
description = "Daily average stress rose 1.5 standard deviations above the 14-day baseline."
|
|
53
69
|
metric = "stress"
|
|
54
|
-
window = "
|
|
70
|
+
window = "24h"
|
|
55
71
|
aggregation = "avg"
|
|
56
72
|
compare = "greater_than"
|
|
57
73
|
severity = "info"
|
|
58
74
|
|
|
75
|
+
[rule.target]
|
|
76
|
+
type = "baseline_stddev"
|
|
77
|
+
windowDays = 14
|
|
78
|
+
stddevs = 1.5
|
|
79
|
+
|
|
80
|
+
[rule.throttle]
|
|
81
|
+
minGapMinutes = 1440
|
|
82
|
+
maxPerDay = 1
|
|
83
|
+
|
|
84
|
+
[[rule]]
|
|
85
|
+
id = "ziva.low-activity-week"
|
|
86
|
+
brand = "ziva"
|
|
87
|
+
name = "Low activity all week"
|
|
88
|
+
description = "Total steps across the last 7 days below 20,000."
|
|
89
|
+
metric = "activity_steps"
|
|
90
|
+
window = "7d"
|
|
91
|
+
aggregation = "sum"
|
|
92
|
+
compare = "less_than"
|
|
93
|
+
severity = "info"
|
|
94
|
+
|
|
59
95
|
[rule.target]
|
|
60
96
|
type = "absolute"
|
|
61
|
-
value =
|
|
97
|
+
value = 20000
|
|
62
98
|
|
|
63
99
|
[rule.throttle]
|
|
64
|
-
minGapMinutes =
|
|
100
|
+
minGapMinutes = 10080
|
|
101
|
+
maxPerDay = 1
|
|
102
|
+
|
|
103
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
104
|
+
# SpO₂ — two rules that coexist deliberately (Sprint 5 §4, Ziva #1).
|
|
105
|
+
#
|
|
106
|
+
# A is the LOUD one: the alert the user configured in the ⚙ sheet, firing
|
|
107
|
+
# on a single reading below their own safe level, day or night.
|
|
108
|
+
# B is a SECONDARY pattern insight: a sustained nightly dip, which is a
|
|
109
|
+
# different observation and belongs in the feed rather than as an alert.
|
|
110
|
+
# They fire independently and hold independent throttle state.
|
|
111
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
[[rule]]
|
|
114
|
+
id = "ziva.spo2-safe-level"
|
|
115
|
+
brand = "ziva"
|
|
116
|
+
name = "SpO₂ below your safe level"
|
|
117
|
+
description = "Alerts on any single reading below your configured safe level"
|
|
118
|
+
metric = "spo2"
|
|
119
|
+
# 24h, NOT 1h. Rules evaluate at batch:complete, and a morning sync
|
|
120
|
+
# delivers ~8 hours of overnight readings in one batch. A wall-clock 1h
|
|
121
|
+
# window would silently miss anything older than an hour before eval —
|
|
122
|
+
# including the exact overnight crossings the "day or night" promise
|
|
123
|
+
# exists for. 24h covers the batch delivery envelope; the throttle below
|
|
124
|
+
# prevents duplicate fires from re-scanned data.
|
|
125
|
+
window = "24h"
|
|
126
|
+
aggregation = "min"
|
|
127
|
+
compare = "less_than"
|
|
128
|
+
context = "any"
|
|
129
|
+
consecutive = 1
|
|
130
|
+
severity = "warn"
|
|
131
|
+
|
|
132
|
+
[rule.target]
|
|
133
|
+
type = "user_setting"
|
|
134
|
+
key = "user:spo2SafeLevel"
|
|
135
|
+
defaultValue = 90
|
|
136
|
+
|
|
137
|
+
[rule.throttle]
|
|
138
|
+
minGapMinutes = 60
|
|
65
139
|
maxPerDay = 3
|
|
66
140
|
|
|
67
141
|
[[rule]]
|
|
68
|
-
id = "ziva.
|
|
142
|
+
id = "ziva.spo2-desaturation-asleep"
|
|
69
143
|
brand = "ziva"
|
|
70
|
-
name = "
|
|
71
|
-
description = "
|
|
72
|
-
metric = "
|
|
144
|
+
name = "Nightly low SpO₂ pattern"
|
|
145
|
+
description = "Pattern of low SpO₂ during sleep — 3 consecutive samples below 88"
|
|
146
|
+
metric = "spo2"
|
|
73
147
|
window = "24h"
|
|
74
|
-
aggregation = "
|
|
148
|
+
aggregation = "min"
|
|
75
149
|
compare = "less_than"
|
|
150
|
+
context = "asleep"
|
|
151
|
+
consecutive = 3
|
|
76
152
|
severity = "info"
|
|
77
153
|
|
|
78
154
|
[rule.target]
|
|
79
155
|
type = "absolute"
|
|
80
|
-
value =
|
|
156
|
+
value = 88
|
|
81
157
|
|
|
82
158
|
[rule.throttle]
|
|
83
159
|
minGapMinutes = 720
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ziva.js","sourceRoot":"","sources":["../../../src/rules/defaults/ziva.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"ziva.js","sourceRoot":"","sources":["../../../src/rules/defaults/ziva.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAGvC,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwJZ,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAW,YAAY,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA"}
|