@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
|
@@ -15,11 +15,35 @@ export type MetricExposure = 'full' | 'collected_only';
|
|
|
15
15
|
* than shoehorning it into a number.
|
|
16
16
|
*/
|
|
17
17
|
export type MetricSamplingMinutes = number | 'per_session';
|
|
18
|
+
/**
|
|
19
|
+
* How a metric collapses to ONE value per local day before quantiles are
|
|
20
|
+
* taken across days (principle 27, "baselines are day-first").
|
|
21
|
+
*
|
|
22
|
+
* - `'avg'` — mean of the day's readings (spo2, hrv_ms, stress, hr_bpm, temp_c)
|
|
23
|
+
* - `'sum'` — total across the day (activity_steps)
|
|
24
|
+
* - `'session'` — already one value per night; no daily GROUP BY (sleep)
|
|
25
|
+
*
|
|
26
|
+
* Getting this wrong silently corrupts the "usual range": quantiling raw
|
|
27
|
+
* readings lets a single intra-day dip set p10 for the whole window, which
|
|
28
|
+
* is exactly the bug day-first compute exists to prevent.
|
|
29
|
+
*/
|
|
30
|
+
export type BaselineDailyAggregate = 'avg' | 'sum' | 'session';
|
|
18
31
|
export interface MetricMetadataEntry {
|
|
19
32
|
table: TableName;
|
|
20
33
|
column: string;
|
|
34
|
+
/**
|
|
35
|
+
* Nominal cadence. **Fallback only** — `device_config.interval_minutes`
|
|
36
|
+
* supersedes this when a row exists for the device (principle 22,
|
|
37
|
+
* hardware-agnostic cadence).
|
|
38
|
+
*/
|
|
21
39
|
sampling_minutes: MetricSamplingMinutes;
|
|
22
40
|
exposure: MetricExposure;
|
|
41
|
+
/**
|
|
42
|
+
* Daily collapse used by baseline compute. Absent for metrics that carry
|
|
43
|
+
* no baseline (BP + vascular aging are `collected_only`; calories,
|
|
44
|
+
* distance and sleep_score have no baseline consumer in v0.2.0).
|
|
45
|
+
*/
|
|
46
|
+
baselineDailyAggregate?: BaselineDailyAggregate;
|
|
23
47
|
}
|
|
24
48
|
/**
|
|
25
49
|
* Frozen metric registry, verbatim from spec §Metric metadata.
|
|
@@ -33,12 +57,14 @@ export declare const METRIC_METADATA: Readonly<{
|
|
|
33
57
|
readonly column: "hrv_ms";
|
|
34
58
|
readonly sampling_minutes: 60;
|
|
35
59
|
readonly exposure: "full";
|
|
60
|
+
readonly baselineDailyAggregate: "avg";
|
|
36
61
|
};
|
|
37
62
|
readonly stress: {
|
|
38
63
|
readonly table: "hrv";
|
|
39
64
|
readonly column: "stress";
|
|
40
65
|
readonly sampling_minutes: 60;
|
|
41
66
|
readonly exposure: "full";
|
|
67
|
+
readonly baselineDailyAggregate: "avg";
|
|
42
68
|
};
|
|
43
69
|
readonly systolic_bp: {
|
|
44
70
|
readonly table: "hrv";
|
|
@@ -63,24 +89,28 @@ export declare const METRIC_METADATA: Readonly<{
|
|
|
63
89
|
readonly column: "bpm";
|
|
64
90
|
readonly sampling_minutes: 10;
|
|
65
91
|
readonly exposure: "full";
|
|
92
|
+
readonly baselineDailyAggregate: "avg";
|
|
66
93
|
};
|
|
67
94
|
readonly spo2: {
|
|
68
95
|
readonly table: "spo2";
|
|
69
96
|
readonly column: "spo2";
|
|
70
97
|
readonly sampling_minutes: 30;
|
|
71
98
|
readonly exposure: "full";
|
|
99
|
+
readonly baselineDailyAggregate: "avg";
|
|
72
100
|
};
|
|
73
101
|
readonly temp_c: {
|
|
74
102
|
readonly table: "temperature";
|
|
75
103
|
readonly column: "temp_c";
|
|
76
104
|
readonly sampling_minutes: 30;
|
|
77
105
|
readonly exposure: "full";
|
|
106
|
+
readonly baselineDailyAggregate: "avg";
|
|
78
107
|
};
|
|
79
108
|
readonly activity_steps: {
|
|
80
109
|
readonly table: "activity";
|
|
81
110
|
readonly column: "steps";
|
|
82
111
|
readonly sampling_minutes: 1;
|
|
83
112
|
readonly exposure: "full";
|
|
113
|
+
readonly baselineDailyAggregate: "sum";
|
|
84
114
|
};
|
|
85
115
|
readonly calories: {
|
|
86
116
|
readonly table: "activity_bucket";
|
|
@@ -99,6 +129,7 @@ export declare const METRIC_METADATA: Readonly<{
|
|
|
99
129
|
readonly column: "total_minutes";
|
|
100
130
|
readonly sampling_minutes: "per_session";
|
|
101
131
|
readonly exposure: "full";
|
|
132
|
+
readonly baselineDailyAggregate: "session";
|
|
102
133
|
};
|
|
103
134
|
readonly sleep_score: {
|
|
104
135
|
readonly table: "sleep_session";
|
|
@@ -107,8 +138,8 @@ export declare const METRIC_METADATA: Readonly<{
|
|
|
107
138
|
readonly exposure: "full";
|
|
108
139
|
};
|
|
109
140
|
readonly device_battery: {
|
|
110
|
-
readonly table: "
|
|
111
|
-
readonly column: "
|
|
141
|
+
readonly table: "device_battery";
|
|
142
|
+
readonly column: "battery_pct";
|
|
112
143
|
readonly sampling_minutes: 240;
|
|
113
144
|
readonly exposure: "full";
|
|
114
145
|
};
|
|
@@ -125,4 +156,28 @@ export declare function isMetricExposed(id: MetricId): boolean;
|
|
|
125
156
|
* UI code that iterates the visible surface.
|
|
126
157
|
*/
|
|
127
158
|
export declare function getExposedMetricIds(): MetricId[];
|
|
159
|
+
/**
|
|
160
|
+
* Metric ids that carry a `user_baseline` row — those declaring a
|
|
161
|
+
* `baselineDailyAggregate`. Seven in v0.2.0, which at three windows each
|
|
162
|
+
* (7d / 30d / 90d) is the 21-baselines-per-user figure in the Sprint 5
|
|
163
|
+
* design.
|
|
164
|
+
*/
|
|
165
|
+
export declare function getBaselineMetricIds(): MetricId[];
|
|
166
|
+
/**
|
|
167
|
+
* `baselineDailyAggregate` for a metric, or `undefined` when it carries no
|
|
168
|
+
* baseline. Accessor rather than a direct property read because the
|
|
169
|
+
* `as const` registry narrows each entry to its own literal type, and the
|
|
170
|
+
* field is genuinely absent — not `undefined` — on entries without one.
|
|
171
|
+
*/
|
|
172
|
+
export declare function baselineAggregateFor(id: MetricId): BaselineDailyAggregate | undefined;
|
|
173
|
+
/** Baseline windows computed per metric (Sprint 5 §7). */
|
|
174
|
+
export declare const BASELINE_WINDOW_DAYS: readonly [7, 30, 90];
|
|
175
|
+
export type BaselineWindowDays = (typeof BASELINE_WINDOW_DAYS)[number];
|
|
176
|
+
/**
|
|
177
|
+
* Minimum distinct local DAYS before a baseline row is written
|
|
178
|
+
* (principle 27). Counting readings instead of days is the classic
|
|
179
|
+
* mis-implementation: 15 days of 30-minute SpO₂ sampling is 720 readings
|
|
180
|
+
* but still only 15 days, and must NOT populate a baseline.
|
|
181
|
+
*/
|
|
182
|
+
export declare const BASELINE_MIN_DAYS = 20;
|
|
128
183
|
//# sourceMappingURL=metric_metadata.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metric_metadata.d.ts","sourceRoot":"","sources":["../../src/core/metric_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,gBAAgB,CAAA;AAEtD;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,aAAa,CAAA;AAE1D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,gBAAgB,EAAE,qBAAqB,CAAA;IACvC,QAAQ,EAAE,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"metric_metadata.d.ts","sourceRoot":"","sources":["../../src/core/metric_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,gBAAgB,CAAA;AAEtD;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,aAAa,CAAA;AAE1D;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,sBAAsB,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,CAAA;AAE9D,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,SAAS,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,gBAAgB,EAAE,qBAAqB,CAAA;IACvC,QAAQ,EAAE,cAAc,CAAA;IACxB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,sBAAsB,CAAA;CAChD;AAED;;;;;GAKG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAeuC,CAAA;AAEnE,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,eAAe,CAAA;AAEnD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,GAAG,OAAO,CAErD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,QAAQ,EAAE,CAEhD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,QAAQ,EAAE,CAIjD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,EAAE,EAAE,QAAQ,GACX,sBAAsB,GAAG,SAAS,CAGpC;AAED,0DAA0D;AAC1D,eAAO,MAAM,oBAAoB,sBAAuB,CAAA;AACxD,MAAM,MAAM,kBAAkB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAA;AAEtE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,KAAK,CAAA"}
|
|
@@ -14,20 +14,20 @@
|
|
|
14
14
|
* change (bump metadata; add DDL column if new).
|
|
15
15
|
*/
|
|
16
16
|
export const METRIC_METADATA = Object.freeze({
|
|
17
|
-
hrv_ms: { table: 'hrv', column: 'hrv_ms', sampling_minutes: 60, exposure: 'full' },
|
|
18
|
-
stress: { table: 'hrv', column: 'stress', sampling_minutes: 60, exposure: 'full' },
|
|
17
|
+
hrv_ms: { table: 'hrv', column: 'hrv_ms', sampling_minutes: 60, exposure: 'full', baselineDailyAggregate: 'avg' },
|
|
18
|
+
stress: { table: 'hrv', column: 'stress', sampling_minutes: 60, exposure: 'full', baselineDailyAggregate: 'avg' },
|
|
19
19
|
systolic_bp: { table: 'hrv', column: 'systolic_bp', sampling_minutes: 60, exposure: 'collected_only' },
|
|
20
20
|
diastolic_bp: { table: 'hrv', column: 'diastolic_bp', sampling_minutes: 60, exposure: 'collected_only' },
|
|
21
21
|
vascular_aging: { table: 'hrv', column: 'vascular_aging', sampling_minutes: 60, exposure: 'collected_only' },
|
|
22
|
-
hr_bpm: { table: 'heart_rate', column: 'bpm', sampling_minutes: 10, exposure: 'full' },
|
|
23
|
-
spo2: { table: 'spo2', column: 'spo2', sampling_minutes: 30, exposure: 'full' },
|
|
24
|
-
temp_c: { table: 'temperature', column: 'temp_c', sampling_minutes: 30, exposure: 'full' },
|
|
25
|
-
activity_steps: { table: 'activity', column: 'steps', sampling_minutes: 1, exposure: 'full' },
|
|
22
|
+
hr_bpm: { table: 'heart_rate', column: 'bpm', sampling_minutes: 10, exposure: 'full', baselineDailyAggregate: 'avg' },
|
|
23
|
+
spo2: { table: 'spo2', column: 'spo2', sampling_minutes: 30, exposure: 'full', baselineDailyAggregate: 'avg' },
|
|
24
|
+
temp_c: { table: 'temperature', column: 'temp_c', sampling_minutes: 30, exposure: 'full', baselineDailyAggregate: 'avg' },
|
|
25
|
+
activity_steps: { table: 'activity', column: 'steps', sampling_minutes: 1, exposure: 'full', baselineDailyAggregate: 'sum' },
|
|
26
26
|
calories: { table: 'activity_bucket', column: 'calories', sampling_minutes: 10, exposure: 'full' },
|
|
27
27
|
distance_km: { table: 'activity_bucket', column: 'distance_km', sampling_minutes: 10, exposure: 'full' },
|
|
28
|
-
sleep_total_minutes: { table: 'sleep_session', column: 'total_minutes', sampling_minutes: 'per_session', exposure: 'full' },
|
|
28
|
+
sleep_total_minutes: { table: 'sleep_session', column: 'total_minutes', sampling_minutes: 'per_session', exposure: 'full', baselineDailyAggregate: 'session' },
|
|
29
29
|
sleep_score: { table: 'sleep_session', column: 'avg_confidence', sampling_minutes: 'per_session', exposure: 'full' },
|
|
30
|
-
device_battery: { table: '
|
|
30
|
+
device_battery: { table: 'device_battery', column: 'battery_pct', sampling_minutes: 240, exposure: 'full' },
|
|
31
31
|
});
|
|
32
32
|
/**
|
|
33
33
|
* Whether an app should surface this metric to end users. Metrics marked
|
|
@@ -44,4 +44,32 @@ export function isMetricExposed(id) {
|
|
|
44
44
|
export function getExposedMetricIds() {
|
|
45
45
|
return Object.keys(METRIC_METADATA).filter(isMetricExposed);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Metric ids that carry a `user_baseline` row — those declaring a
|
|
49
|
+
* `baselineDailyAggregate`. Seven in v0.2.0, which at three windows each
|
|
50
|
+
* (7d / 30d / 90d) is the 21-baselines-per-user figure in the Sprint 5
|
|
51
|
+
* design.
|
|
52
|
+
*/
|
|
53
|
+
export function getBaselineMetricIds() {
|
|
54
|
+
return Object.keys(METRIC_METADATA).filter(id => baselineAggregateFor(id) !== undefined);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* `baselineDailyAggregate` for a metric, or `undefined` when it carries no
|
|
58
|
+
* baseline. Accessor rather than a direct property read because the
|
|
59
|
+
* `as const` registry narrows each entry to its own literal type, and the
|
|
60
|
+
* field is genuinely absent — not `undefined` — on entries without one.
|
|
61
|
+
*/
|
|
62
|
+
export function baselineAggregateFor(id) {
|
|
63
|
+
const entry = METRIC_METADATA[id];
|
|
64
|
+
return entry.baselineDailyAggregate;
|
|
65
|
+
}
|
|
66
|
+
/** Baseline windows computed per metric (Sprint 5 §7). */
|
|
67
|
+
export const BASELINE_WINDOW_DAYS = [7, 30, 90];
|
|
68
|
+
/**
|
|
69
|
+
* Minimum distinct local DAYS before a baseline row is written
|
|
70
|
+
* (principle 27). Counting readings instead of days is the classic
|
|
71
|
+
* mis-implementation: 15 days of 30-minute SpO₂ sampling is 720 readings
|
|
72
|
+
* but still only 15 days, and must NOT populate a baseline.
|
|
73
|
+
*/
|
|
74
|
+
export const BASELINE_MIN_DAYS = 20;
|
|
47
75
|
//# sourceMappingURL=metric_metadata.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metric_metadata.js","sourceRoot":"","sources":["../../src/core/metric_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"metric_metadata.js","sourceRoot":"","sources":["../../src/core/metric_metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -31,6 +31,12 @@ export interface MigrationCatalogs {
|
|
|
31
31
|
/** Attached R2 iceberg catalog secret name. Undefined in local mode. */
|
|
32
32
|
remote?: string;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Iceberg namespace remote DDL is issued under — remote tables live at
|
|
36
|
+
* `<secret>.default.<table>` (spec §Attach Protocol). Local DDL stays
|
|
37
|
+
* 2-part because the local catalog's default schema resolves implicitly.
|
|
38
|
+
*/
|
|
39
|
+
export declare const REMOTE_NAMESPACE = "default";
|
|
34
40
|
export interface Migration {
|
|
35
41
|
/** Sequential integer, starting at 1. */
|
|
36
42
|
version: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/core/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjE;AAED;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,SAAS,
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/core/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtC,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,kFAAkF;IAClF,KAAK,EAAE,MAAM,CAAA;IACb,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,YAAY,CAAA;AAEzC,MAAM,WAAW,SAAS;IACxB,yCAAyC;IACzC,OAAO,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAA;IACZ,qEAAqE;IACrE,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjE;AAED;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,SAAS,SAAS,EAgHzC,CAAA;AAEF,uEAAuE;AACvE,eAAO,MAAM,eAAe,EAAE,MAA0B,CAAA;AAExD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAExE;AAWD,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;CACX;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,OAAO,EACX,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,iBAAiB,GAC1B,OAAO,CAAC,sBAAsB,CAAC,CAyCjC"}
|
package/dist/core/migrations.js
CHANGED
|
@@ -20,7 +20,13 @@
|
|
|
20
20
|
* `ensureMigrations` is idempotent and safe to call on every attach.
|
|
21
21
|
*/
|
|
22
22
|
import { AnalyticsError } from './errors';
|
|
23
|
-
import { ensureSchemas, LOCAL_SCHEMAS, SCHEMAS } from './schemas';
|
|
23
|
+
import { ensureSchemas, insightIndexDdl, LOCAL_SCHEMAS, qualifyDdl, SCHEMAS } from './schemas';
|
|
24
|
+
/**
|
|
25
|
+
* Iceberg namespace remote DDL is issued under — remote tables live at
|
|
26
|
+
* `<secret>.default.<table>` (spec §Attach Protocol). Local DDL stays
|
|
27
|
+
* 2-part because the local catalog's default schema resolves implicitly.
|
|
28
|
+
*/
|
|
29
|
+
export const REMOTE_NAMESPACE = 'default';
|
|
24
30
|
/**
|
|
25
31
|
* Ordered list of migrations. Each entry's `version` must equal its
|
|
26
32
|
* (1-based) position; `CURRENT_VERSION` is derived from the tail.
|
|
@@ -31,15 +37,97 @@ export const MIGRATIONS = Object.freeze([
|
|
|
31
37
|
name: 'baseline schemas',
|
|
32
38
|
async up(db, catalogs) {
|
|
33
39
|
// Always ensure local tables (LOCAL_SCHEMAS — no PARTITIONED BY;
|
|
34
|
-
// plain DuckDB accepts the DDL).
|
|
40
|
+
// plain DuckDB accepts the DDL). 2-part `<catalog>.<table>` resolves
|
|
41
|
+
// via the local catalog's default `main` schema.
|
|
35
42
|
await ensureSchemas(db, catalogs.local, LOCAL_SCHEMAS);
|
|
36
43
|
// Ensure remote tables when attached to R2 (SCHEMAS — with
|
|
37
|
-
// PARTITIONED BY; Iceberg requires it).
|
|
44
|
+
// PARTITIONED BY; Iceberg requires it). Since 0.5.0 dropped the
|
|
45
|
+
// `USE <catalog>.default` step, remote DDL must spell the Iceberg
|
|
46
|
+
// namespace explicitly — 2-part `zone_x.<table>` fails with
|
|
47
|
+
// "Schema with name ... not found" on an attached Iceberg catalog.
|
|
48
|
+
if (catalogs.remote) {
|
|
49
|
+
await ensureSchemas(db, `${catalogs.remote}.${REMOTE_NAMESPACE}`, SCHEMAS, { skipLocalOnly: true });
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
version: 2,
|
|
55
|
+
name: 'device_battery table',
|
|
56
|
+
// Dedicated numeric table for battery samples (fix option B2). The
|
|
57
|
+
// pre-0.6.0 `device_battery` metric pointed at `device_event.payload`
|
|
58
|
+
// (JSON VARCHAR) — rules compiled but compared strings to numbers
|
|
59
|
+
// across every event type, so they could never fire correctly.
|
|
60
|
+
// Installs that ran step 1 on ≥0.6.0 already have the table from the
|
|
61
|
+
// baseline `ensureSchemas` (IF NOT EXISTS makes this a no-op there).
|
|
62
|
+
async up(db, catalogs) {
|
|
63
|
+
await db.execute(qualifyDdl(LOCAL_SCHEMAS.device_battery, 'device_battery', catalogs.local));
|
|
38
64
|
if (catalogs.remote) {
|
|
39
|
-
await
|
|
65
|
+
await db.execute(qualifyDdl(SCHEMAS.device_battery, 'device_battery', `${catalogs.remote}.${REMOTE_NAMESPACE}`));
|
|
40
66
|
}
|
|
41
67
|
},
|
|
42
68
|
},
|
|
69
|
+
{
|
|
70
|
+
version: 3,
|
|
71
|
+
name: 'insight v2 (insight_id / metric / kind / dismissed_at)',
|
|
72
|
+
// Fix CO-2: the pre-0.7.0 `insight` table used an `id` PK and lacked
|
|
73
|
+
// the spec's `metric`, `kind`, and `dismissed_at` columns, so the
|
|
74
|
+
// registry contract (queries.ts worthALookInsight) could never match.
|
|
75
|
+
// Recreates the LOCAL table to the spec shape, preserving rows:
|
|
76
|
+
// id → insight_id, kind defaults to 'threshold', severity
|
|
77
|
+
// 'critical' → 'urgent' (spec enum is info|warn|urgent).
|
|
78
|
+
// Idempotent: skips the recreate when `metric` is already present
|
|
79
|
+
// (fresh installs get the new shape from the baseline ensureSchemas).
|
|
80
|
+
// The remote zone gets CREATE IF NOT EXISTS only — the client never
|
|
81
|
+
// destructively rewrites the attached Iceberg catalog; schema
|
|
82
|
+
// evolution of pre-existing remote tables is a server-side operation.
|
|
83
|
+
async up(db, catalogs) {
|
|
84
|
+
const cols = await db.execute(`SELECT column_name FROM information_schema.columns `
|
|
85
|
+
+ `WHERE table_catalog = $catalog AND table_name = 'insight'`, { catalog: catalogs.local });
|
|
86
|
+
const hasMetric = cols.some(c => c.column_name === 'metric');
|
|
87
|
+
if (!hasMetric) {
|
|
88
|
+
// Re-entrancy: a prior partially-failed run may have left the
|
|
89
|
+
// scratch table behind (KV only advances after full success).
|
|
90
|
+
await db.execute(`DROP TABLE IF EXISTS ${catalogs.local}.insight_v2;`);
|
|
91
|
+
const tmpDdl = LOCAL_SCHEMAS.insight.replace('CREATE TABLE insight', `CREATE TABLE ${catalogs.local}.insight_v2`);
|
|
92
|
+
await db.execute(tmpDdl);
|
|
93
|
+
// Old rows had no metric column; the package never wrote rows
|
|
94
|
+
// pre-v2 (rules only emitted events), so 'unknown' only tags
|
|
95
|
+
// app-authored legacy rows.
|
|
96
|
+
await db.execute(`INSERT INTO ${catalogs.local}.insight_v2 `
|
|
97
|
+
+ `(insight_id, ts, brand, family_id, user_id, rule_id, metric, kind, `
|
|
98
|
+
+ `severity, title, body, evidence, acknowledged_at, dismissed_at) `
|
|
99
|
+
+ `SELECT id, ts, brand, family_id, user_id, rule_id, 'unknown', 'threshold', `
|
|
100
|
+
+ `CASE WHEN severity = 'critical' THEN 'urgent' ELSE severity END, `
|
|
101
|
+
+ `title, body, evidence, acknowledged_at, NULL `
|
|
102
|
+
+ `FROM ${catalogs.local}.insight;`);
|
|
103
|
+
await db.execute(`DROP TABLE ${catalogs.local}.insight;`);
|
|
104
|
+
await db.execute(`ALTER TABLE ${catalogs.local}.insight_v2 RENAME TO insight;`);
|
|
105
|
+
}
|
|
106
|
+
await db.execute(insightIndexDdl(catalogs.local));
|
|
107
|
+
if (catalogs.remote) {
|
|
108
|
+
await db.execute(qualifyDdl(SCHEMAS.insight, 'insight', `${catalogs.remote}.${REMOTE_NAMESPACE}`));
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
version: 4,
|
|
114
|
+
name: 'user_baseline table (Sprint 5 §7)',
|
|
115
|
+
// Shared day-first baseline store for rules, tools, and screens.
|
|
116
|
+
//
|
|
117
|
+
// NOTE ON NUMBERING: the Sprint 5 spec (§33 / tasks T-03) calls this
|
|
118
|
+
// "migration v3". Versions 2 and 3 were already spent on the
|
|
119
|
+
// device_battery table and the insight v2 rebuild before Sprint 5
|
|
120
|
+
// started, so it lands at 4 instead. Same for the spec's "migration v4"
|
|
121
|
+
// (insight.dismissed_at), which is already satisfied — the column ships
|
|
122
|
+
// in the baseline DDL and step 3 backfills it, so no ALTER is needed.
|
|
123
|
+
//
|
|
124
|
+
// Local-only: baselines are derived from local+R2 union views and are
|
|
125
|
+
// cheap to recompute, so there is no value in pushing them to R2 and
|
|
126
|
+
// no correctness story for two devices racing to write the same row.
|
|
127
|
+
async up(db, catalogs) {
|
|
128
|
+
await db.execute(qualifyDdl(LOCAL_SCHEMAS.user_baseline, 'user_baseline', catalogs.local));
|
|
129
|
+
},
|
|
130
|
+
},
|
|
43
131
|
]);
|
|
44
132
|
/** Version of the latest known migration — target for every attach. */
|
|
45
133
|
export const CURRENT_VERSION = MIGRATIONS.length;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/core/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAEzC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/core/migrations.ts"],"names":[],"mappings":"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"}
|
package/dist/core/retention.d.ts
CHANGED
|
@@ -9,9 +9,18 @@
|
|
|
9
9
|
* that would preserve data wins.
|
|
10
10
|
*
|
|
11
11
|
* 2. **Sweep** the warehouse via `runRetentionSweep` — issues one DELETE
|
|
12
|
-
* per retention-managed table
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* per retention-managed table in the LOCAL catalog. The client never
|
|
13
|
+
* sweeps the remote R2 zone (principles 17/53): device-side compaction
|
|
14
|
+
* prunes local data only; server-side retention is R2 snapshot
|
|
15
|
+
* expiration, configured externally.
|
|
16
|
+
*
|
|
17
|
+
* Watermark guard (fix CO-1): tables the pusher pushes (`syncable` in
|
|
18
|
+
* `table_metadata.ts`) are bounded by their KVStore push watermark —
|
|
19
|
+
* `ts < LEAST(now() - retention, $pushWatermark)` — so rows still awaiting
|
|
20
|
+
* R2 push are never dropped. No watermark yet (nothing pushed) means the
|
|
21
|
+
* table is skipped entirely. In `'local'` mode there is no push and never
|
|
22
|
+
* will be — data is local-forever by design — so the plain retention
|
|
23
|
+
* cutoff applies without any watermark guard.
|
|
15
24
|
*
|
|
16
25
|
* Internal tables have fixed defaults independent of brand/user config:
|
|
17
26
|
* - `insight` → 90 days
|
|
@@ -22,6 +31,7 @@
|
|
|
22
31
|
*/
|
|
23
32
|
import type { HybridDuckDB } from './engine';
|
|
24
33
|
import type { TableName } from './schemas';
|
|
34
|
+
import type { AnalyticsMode } from './types';
|
|
25
35
|
export interface EffectiveRetentionInput {
|
|
26
36
|
/** Brand-level default (v0.1.0 active layer) — from `AnalyticsConfig.retention[brand].days`. */
|
|
27
37
|
brandDefault: number;
|
|
@@ -42,9 +52,28 @@ export declare function resolveEffectiveRetention(input: EffectiveRetentionInput
|
|
|
42
52
|
export declare const INSIGHT_RETENTION_DAYS = 90;
|
|
43
53
|
/** Fixed retention for the `tool_call_audit` table — spec §Retention. */
|
|
44
54
|
export declare const AUDIT_RETENTION_DAYS = 30;
|
|
55
|
+
/**
|
|
56
|
+
* Push-watermark accessor injected by the factory. Resolves the highest
|
|
57
|
+
* `ts` successfully pushed to R2 for `table` — read from the KVStore key
|
|
58
|
+
* the pusher writes (`analytics:watermark:{brand}:{familyId}:{table}:push`,
|
|
59
|
+
* see sync/watermark.ts). `null` means nothing pushed yet.
|
|
60
|
+
*/
|
|
61
|
+
export type PushWatermarkAccessor = (table: TableName) => Promise<Date | null>;
|
|
45
62
|
export interface RetentionSweepInput {
|
|
46
63
|
/** Effective retention days for sensor tables (see `resolveEffectiveRetention`). */
|
|
47
64
|
effectiveDays: number;
|
|
65
|
+
/**
|
|
66
|
+
* Attach mode. `'local'` applies plain retention cutoffs — there is no
|
|
67
|
+
* push in local mode, so nothing ever "reaches R2" and the watermark
|
|
68
|
+
* guard would block deletion forever. Defaults to `'r2'`.
|
|
69
|
+
*/
|
|
70
|
+
mode?: AnalyticsMode;
|
|
71
|
+
/**
|
|
72
|
+
* Required in r2 mode to sweep syncable tables. Missing accessor or a
|
|
73
|
+
* `null` watermark ⇒ the table is skipped (spec: no data deleted before
|
|
74
|
+
* it reaches R2).
|
|
75
|
+
*/
|
|
76
|
+
getPushWatermark?: PushWatermarkAccessor;
|
|
48
77
|
}
|
|
49
78
|
export interface RetentionSweepEntry {
|
|
50
79
|
table: TableName;
|
|
@@ -52,25 +81,25 @@ export interface RetentionSweepEntry {
|
|
|
52
81
|
}
|
|
53
82
|
export interface RetentionSweepResult {
|
|
54
83
|
swept: RetentionSweepEntry[];
|
|
84
|
+
/** Syncable tables left untouched because no push watermark exists yet. */
|
|
85
|
+
skipped: TableName[];
|
|
55
86
|
}
|
|
56
87
|
/**
|
|
57
|
-
* Delete expired rows across every retention-managed table under `catalog
|
|
58
|
-
*
|
|
88
|
+
* Delete expired rows across every retention-managed table under `catalog`
|
|
89
|
+
* (the LOCAL DuckDB catalog — never the attached R2 zone).
|
|
59
90
|
*
|
|
60
91
|
* SQL shape per table:
|
|
61
92
|
*
|
|
62
93
|
* ```sql
|
|
94
|
+
* -- pushed (syncable) tables, r2 mode:
|
|
63
95
|
* DELETE FROM {catalog}.{table}
|
|
64
|
-
* WHERE {tsCol} < LEAST(
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* );
|
|
96
|
+
* WHERE {tsCol} < LEAST(now() - INTERVAL '{days} days',
|
|
97
|
+
* CAST($pushWatermark AS TIMESTAMP));
|
|
98
|
+
* -- non-pushed tables, and every table in local mode:
|
|
99
|
+
* DELETE FROM {catalog}.{table}
|
|
100
|
+
* WHERE {tsCol} < now() - INTERVAL '{days} days';
|
|
69
101
|
* ```
|
|
70
102
|
*
|
|
71
|
-
* When no watermark exists, `MAX` returns NULL, `LEAST` becomes NULL and the
|
|
72
|
-
* comparison drops to false — safest default (no deletion on unsynced data).
|
|
73
|
-
*
|
|
74
103
|
* Failure at any table surfaces as `AnalyticsError('retention_failed', …)`;
|
|
75
104
|
* the caller decides whether to swallow-and-log or bubble.
|
|
76
105
|
*/
|
|
@@ -80,6 +109,8 @@ interface DeleteSqlInput {
|
|
|
80
109
|
table: TableName;
|
|
81
110
|
tsCol: string;
|
|
82
111
|
days: number;
|
|
112
|
+
/** Bound by `$pushWatermark` (ISO string param, cast to TIMESTAMP). */
|
|
113
|
+
watermarkBound?: boolean;
|
|
83
114
|
}
|
|
84
115
|
/**
|
|
85
116
|
* Compose the retention DELETE for one table. Extracted so tests can snapshot
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/core/retention.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"retention.d.ts","sourceRoot":"","sources":["../../src/core/retention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAI5C,MAAM,WAAW,uBAAuB;IACtC,gGAAgG;IAChG,YAAY,EAAE,MAAM,CAAA;IACpB,0EAA0E;IAC1E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAMhF;AAgDD,iEAAiE;AACjE,eAAO,MAAM,sBAAsB,KAAK,CAAA;AACxC,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,KAAK,CAAA;AAItC;;;;;GAKG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAAA;AAE9E,MAAM,WAAW,mBAAmB;IAClC,oFAAoF;IACpF,aAAa,EAAE,MAAM,CAAA;IACrB;;;;OAIG;IACH,IAAI,CAAC,EAAE,aAAa,CAAA;IACpB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,qBAAqB,CAAA;CACzC;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,SAAS,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;CACb;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,mBAAmB,EAAE,CAAA;IAC5B,2EAA2E;IAC3E,OAAO,EAAE,SAAS,EAAE,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,mBAAmB,GACzB,OAAO,CAAC,oBAAoB,CAAC,CAqC/B;AAaD,UAAU,cAAc;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,SAAS,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAO5D"}
|
package/dist/core/retention.js
CHANGED
|
@@ -9,9 +9,18 @@
|
|
|
9
9
|
* that would preserve data wins.
|
|
10
10
|
*
|
|
11
11
|
* 2. **Sweep** the warehouse via `runRetentionSweep` — issues one DELETE
|
|
12
|
-
* per retention-managed table
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* per retention-managed table in the LOCAL catalog. The client never
|
|
13
|
+
* sweeps the remote R2 zone (principles 17/53): device-side compaction
|
|
14
|
+
* prunes local data only; server-side retention is R2 snapshot
|
|
15
|
+
* expiration, configured externally.
|
|
16
|
+
*
|
|
17
|
+
* Watermark guard (fix CO-1): tables the pusher pushes (`syncable` in
|
|
18
|
+
* `table_metadata.ts`) are bounded by their KVStore push watermark —
|
|
19
|
+
* `ts < LEAST(now() - retention, $pushWatermark)` — so rows still awaiting
|
|
20
|
+
* R2 push are never dropped. No watermark yet (nothing pushed) means the
|
|
21
|
+
* table is skipped entirely. In `'local'` mode there is no push and never
|
|
22
|
+
* will be — data is local-forever by design — so the plain retention
|
|
23
|
+
* cutoff applies without any watermark guard.
|
|
15
24
|
*
|
|
16
25
|
* Internal tables have fixed defaults independent of brand/user config:
|
|
17
26
|
* - `insight` → 90 days
|
|
@@ -21,6 +30,7 @@
|
|
|
21
30
|
* are skipped.
|
|
22
31
|
*/
|
|
23
32
|
import { AnalyticsError } from './errors';
|
|
33
|
+
import { isSyncable } from './table_metadata';
|
|
24
34
|
/**
|
|
25
35
|
* Resolve effective retention days per spec §Retention precedence — returns
|
|
26
36
|
* `max(userOverride ?? 0, familySetting ?? 0, brandDefault)`.
|
|
@@ -44,10 +54,16 @@ const TABLE_RETENTION = Object.freeze({
|
|
|
44
54
|
sleep_stage: { tsColumn: 'ts', kind: 'sensor' },
|
|
45
55
|
sleep_raw: { tsColumn: 'ts', kind: 'sensor' },
|
|
46
56
|
device_event: { tsColumn: 'ts', kind: 'sensor' },
|
|
57
|
+
device_battery: { tsColumn: 'ts', kind: 'sensor' },
|
|
47
58
|
device_config: null,
|
|
48
59
|
insight: { tsColumn: 'ts', kind: 'insight' },
|
|
49
60
|
sync_watermark: null,
|
|
50
61
|
tool_call_audit: { tsColumn: 'ts', kind: 'audit' },
|
|
62
|
+
// Never swept: one row per (user, metric, window) that the baseline
|
|
63
|
+
// job UPSERTs in place. There is no growth to reclaim, and deleting
|
|
64
|
+
// by `computed_at` would drop live baselines for any metric whose
|
|
65
|
+
// recompute is stale.
|
|
66
|
+
user_baseline: null,
|
|
51
67
|
});
|
|
52
68
|
// -------------------- constants --------------------
|
|
53
69
|
/** Fixed retention for the `insight` table — spec §Retention. */
|
|
@@ -55,44 +71,56 @@ export const INSIGHT_RETENTION_DAYS = 90;
|
|
|
55
71
|
/** Fixed retention for the `tool_call_audit` table — spec §Retention. */
|
|
56
72
|
export const AUDIT_RETENTION_DAYS = 30;
|
|
57
73
|
/**
|
|
58
|
-
* Delete expired rows across every retention-managed table under `catalog
|
|
59
|
-
*
|
|
74
|
+
* Delete expired rows across every retention-managed table under `catalog`
|
|
75
|
+
* (the LOCAL DuckDB catalog — never the attached R2 zone).
|
|
60
76
|
*
|
|
61
77
|
* SQL shape per table:
|
|
62
78
|
*
|
|
63
79
|
* ```sql
|
|
80
|
+
* -- pushed (syncable) tables, r2 mode:
|
|
81
|
+
* DELETE FROM {catalog}.{table}
|
|
82
|
+
* WHERE {tsCol} < LEAST(now() - INTERVAL '{days} days',
|
|
83
|
+
* CAST($pushWatermark AS TIMESTAMP));
|
|
84
|
+
* -- non-pushed tables, and every table in local mode:
|
|
64
85
|
* DELETE FROM {catalog}.{table}
|
|
65
|
-
* WHERE {tsCol} <
|
|
66
|
-
* now() - INTERVAL '{days} days',
|
|
67
|
-
* (SELECT MAX(cursor_ts) FROM {catalog}.sync_watermark
|
|
68
|
-
* WHERE table_name = '{table}')
|
|
69
|
-
* );
|
|
86
|
+
* WHERE {tsCol} < now() - INTERVAL '{days} days';
|
|
70
87
|
* ```
|
|
71
88
|
*
|
|
72
|
-
* When no watermark exists, `MAX` returns NULL, `LEAST` becomes NULL and the
|
|
73
|
-
* comparison drops to false — safest default (no deletion on unsynced data).
|
|
74
|
-
*
|
|
75
89
|
* Failure at any table surfaces as `AnalyticsError('retention_failed', …)`;
|
|
76
90
|
* the caller decides whether to swallow-and-log or bubble.
|
|
77
91
|
*/
|
|
78
92
|
export async function runRetentionSweep(db, catalog, input) {
|
|
93
|
+
const mode = input.mode ?? 'r2';
|
|
79
94
|
const swept = [];
|
|
95
|
+
const skipped = [];
|
|
80
96
|
const tableNames = Object.keys(TABLE_RETENTION);
|
|
81
97
|
for (const table of tableNames) {
|
|
82
98
|
const cfg = TABLE_RETENTION[table];
|
|
83
99
|
if (!cfg)
|
|
84
100
|
continue;
|
|
85
101
|
const days = daysForKind(cfg.kind, input.effectiveDays);
|
|
86
|
-
const
|
|
102
|
+
const guarded = mode !== 'local' && isSyncable(table);
|
|
103
|
+
let params;
|
|
104
|
+
if (guarded) {
|
|
105
|
+
const watermark = (await input.getPushWatermark?.(table)) ?? null;
|
|
106
|
+
if (!watermark) {
|
|
107
|
+
// Nothing pushed yet — deleting would lose data that never
|
|
108
|
+
// reached R2. Leave the table alone until the pusher advances.
|
|
109
|
+
skipped.push(table);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
params = { pushWatermark: watermark.toISOString() };
|
|
113
|
+
}
|
|
114
|
+
const sql = buildDeleteSql({ catalog, table, tsCol: cfg.tsColumn, days, watermarkBound: guarded });
|
|
87
115
|
try {
|
|
88
|
-
await db.execute(sql);
|
|
116
|
+
await db.execute(sql, params);
|
|
89
117
|
swept.push({ table, days });
|
|
90
118
|
}
|
|
91
119
|
catch (cause) {
|
|
92
120
|
throw new AnalyticsError('retention_failed', `runRetentionSweep failed on table ${table}`, cause);
|
|
93
121
|
}
|
|
94
122
|
}
|
|
95
|
-
return { swept };
|
|
123
|
+
return { swept, skipped };
|
|
96
124
|
}
|
|
97
125
|
function daysForKind(kind, effective) {
|
|
98
126
|
switch (kind) {
|
|
@@ -109,11 +137,11 @@ function daysForKind(kind, effective) {
|
|
|
109
137
|
* the exact SQL issued without a live engine.
|
|
110
138
|
*/
|
|
111
139
|
export function buildDeleteSql(input) {
|
|
112
|
-
const { catalog, table, tsCol, days } = input;
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
140
|
+
const { catalog, table, tsCol, days, watermarkBound } = input;
|
|
141
|
+
const cutoff = `now() - INTERVAL '${days} days'`;
|
|
142
|
+
const bound = watermarkBound
|
|
143
|
+
? `LEAST(${cutoff}, CAST($pushWatermark AS TIMESTAMP))`
|
|
144
|
+
: cutoff;
|
|
145
|
+
return `DELETE FROM ${catalog}.${table} WHERE ${tsCol} < ${bound};`;
|
|
118
146
|
}
|
|
119
147
|
//# sourceMappingURL=retention.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/core/retention.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"retention.js","sourceRoot":"","sources":["../../src/core/retention.ts"],"names":[],"mappings":"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"}
|