@mongrov/analytics 0.23.2 → 0.24.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/dist/core/migrations.d.ts.map +1 -1
- package/dist/core/migrations.js +35 -0
- package/dist/core/migrations.js.map +1 -1
- package/dist/core/schemas.d.ts.map +1 -1
- package/dist/core/schemas.js +5 -2
- package/dist/core/schemas.js.map +1 -1
- package/dist/sync/factory.d.ts +7 -0
- package/dist/sync/factory.d.ts.map +1 -1
- package/dist/sync/factory.js +31 -1
- package/dist/sync/factory.js.map +1 -1
- package/dist/sync/index.d.ts +2 -2
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +1 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/mapper/firmware.d.ts.map +1 -1
- package/dist/sync/mapper/firmware.js +7 -5
- package/dist/sync/mapper/firmware.js.map +1 -1
- package/dist/sync/mapper/schema.d.ts +29 -36
- package/dist/sync/mapper/schema.d.ts.map +1 -1
- package/dist/sync/mapper/schema.js +11 -12
- package/dist/sync/mapper/schema.js.map +1 -1
- package/dist/sync/mapper/sleep.d.ts +50 -43
- package/dist/sync/mapper/sleep.d.ts.map +1 -1
- package/dist/sync/mapper/sleep.js +99 -138
- package/dist/sync/mapper/sleep.js.map +1 -1
- package/dist/sync/mapper/types.d.ts +17 -18
- package/dist/sync/mapper/types.d.ts.map +1 -1
- package/dist/sync/sleep-correction/classify.d.ts +86 -0
- package/dist/sync/sleep-correction/classify.d.ts.map +1 -0
- package/dist/sync/sleep-correction/classify.js +247 -0
- package/dist/sync/sleep-correction/classify.js.map +1 -0
- package/dist/sync/sleep-correction/correct.d.ts +46 -0
- package/dist/sync/sleep-correction/correct.d.ts.map +1 -0
- package/dist/sync/sleep-correction/correct.js +60 -0
- package/dist/sync/sleep-correction/correct.js.map +1 -0
- package/dist/sync/sleep-correction/index.d.ts +8 -0
- package/dist/sync/sleep-correction/index.d.ts.map +1 -0
- package/dist/sync/sleep-correction/index.js +8 -0
- package/dist/sync/sleep-correction/index.js.map +1 -0
- package/dist/sync/sleep-correction/night.d.ts +26 -0
- package/dist/sync/sleep-correction/night.d.ts.map +1 -0
- package/dist/sync/sleep-correction/night.js +48 -0
- package/dist/sync/sleep-correction/night.js.map +1 -0
- package/dist/sync/sleep-correction/orchestrate.d.ts +50 -0
- package/dist/sync/sleep-correction/orchestrate.d.ts.map +1 -0
- package/dist/sync/sleep-correction/orchestrate.js +57 -0
- package/dist/sync/sleep-correction/orchestrate.js.map +1 -0
- package/dist/sync/sleep-correction/settle.d.ts +24 -0
- package/dist/sync/sleep-correction/settle.d.ts.map +1 -0
- package/dist/sync/sleep-correction/settle.js +59 -0
- package/dist/sync/sleep-correction/settle.js.map +1 -0
- package/dist/sync/sleep-correction/sql.d.ts +44 -0
- package/dist/sync/sleep-correction/sql.d.ts.map +1 -0
- package/dist/sync/sleep-correction/sql.js +329 -0
- package/dist/sync/sleep-correction/sql.js.map +1 -0
- package/dist/sync/sleep-correction/staging.d.ts +32 -0
- package/dist/sync/sleep-correction/staging.d.ts.map +1 -0
- package/dist/sync/sleep-correction/staging.js +51 -0
- package/dist/sync/sleep-correction/staging.js.map +1 -0
- package/dist/sync/sleep-derive.d.ts +55 -0
- package/dist/sync/sleep-derive.d.ts.map +1 -0
- package/dist/sync/sleep-derive.js +180 -0
- package/dist/sync/sleep-derive.js.map +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrate.d.ts","sourceRoot":"","sources":["../../../src/sync/sleep-correction/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAM3C,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAA;IAChB,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,cAAc,CAAA;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,MAAM,EAAE,IAAI,GAAG,mBAAmB,GAAG,gBAAgB,GAAG,QAAQ,CAAA;IAChE,IAAI,CAAC,EAAE,aAAa,EAAE,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,aAAa,EAAE,MAAM,CAAA;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,YAAY,EAAE,CAAA;CACvB;AAED,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA2B7G;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAOpF"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correct every night a sync touched — v3.1's post-sync loop, per night.
|
|
3
|
+
*
|
|
4
|
+
* As v3.1: the HR tier and sleeping-HR percentiles are computed once per run,
|
|
5
|
+
* then each night gets its window, Phase 2 + 3, and the port's two scalars.
|
|
6
|
+
* Unlike v3.1, each night's window uses the offset its own zone had at its
|
|
7
|
+
* 18:00 (see `night.ts`), not one fixed device offset.
|
|
8
|
+
*
|
|
9
|
+
* A night that throws is reported `failed` and the rest continue — the spec's
|
|
10
|
+
* containment rule: a failed night keeps whatever rows it already had; it
|
|
11
|
+
* never blocks the batch. Writing the results (night-scoped replace) is the
|
|
12
|
+
* caller's job (sleep-correction tasks §3).
|
|
13
|
+
*/
|
|
14
|
+
import { correctNight, detectHrTier, globalBaselines } from './correct';
|
|
15
|
+
import { nightWindow, offsetAtInstantMs } from './night';
|
|
16
|
+
import { computeSettleMin, recoveredMinutes } from './settle';
|
|
17
|
+
export async function correctSleepNights(db, input) {
|
|
18
|
+
const hrIntervalMin = await detectHrTier(db, input.relations);
|
|
19
|
+
const { p75, p90 } = await globalBaselines(db, input.cutoffEpoch, input.relations);
|
|
20
|
+
const nights = [];
|
|
21
|
+
for (const night of input.nights) {
|
|
22
|
+
const w = nightWindow(night, input.timeZone);
|
|
23
|
+
try {
|
|
24
|
+
const res = await correctNight(db, { ...w, hrIntervalMin, p75, p90, relations: input.relations });
|
|
25
|
+
if (res.status !== 'ok') {
|
|
26
|
+
nights.push({ night, ...w, status: res.status });
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
nights.push({
|
|
30
|
+
night,
|
|
31
|
+
...w,
|
|
32
|
+
status: 'ok',
|
|
33
|
+
rows: res.rows,
|
|
34
|
+
settleMin: computeSettleMin(res.rows, res.vitals, hrIntervalMin),
|
|
35
|
+
recoveredMin: recoveredMinutes(res.rows),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
nights.push({ night, ...w, status: 'failed', error: e instanceof Error ? e.message : String(e) });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { hrIntervalMin, p75, p90, nights };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The nights (6pm → 6pm local) that a set of instants falls in — which nights
|
|
46
|
+
* a sync batch touched. Same attribution as v3.1's harness `nightOf`: shift to
|
|
47
|
+
* local time, add 6 h so 18:00 lands on midnight, take the date.
|
|
48
|
+
*/
|
|
49
|
+
export function nightsForEpochs(epochs, timeZone) {
|
|
50
|
+
const out = new Set();
|
|
51
|
+
for (const e of epochs) {
|
|
52
|
+
const offsetMs = offsetAtInstantMs(timeZone, e * 1000);
|
|
53
|
+
out.add(new Date(e * 1000 + offsetMs + 6 * 3600000).toISOString().slice(0, 10));
|
|
54
|
+
}
|
|
55
|
+
return [...out].sort();
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=orchestrate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"orchestrate.js","sourceRoot":"","sources":["../../../src/sync/sleep-correction/orchestrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAMH,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AACxD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AA+B7D,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,EAAa,EAAE,KAAwB;IAC9E,MAAM,aAAa,GAAG,MAAM,YAAY,CAAC,EAAE,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;IAC7D,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,eAAe,CAAC,EAAE,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;IAElF,MAAM,MAAM,GAAmB,EAAE,CAAA;IACjC,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QACjC,MAAM,CAAC,GAAG,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAA;QAC5C,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAA;YACjG,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC;gBACxB,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;gBAChD,SAAQ;YACV,CAAC;YACD,MAAM,CAAC,IAAI,CAAC;gBACV,KAAK;gBACL,GAAG,CAAC;gBACJ,MAAM,EAAE,IAAI;gBACZ,IAAI,EAAE,GAAG,CAAC,IAAI;gBACd,SAAS,EAAE,gBAAgB,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC;gBAChE,YAAY,EAAE,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC;aACzC,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,CAAC,EAAE,CAAC;YACT,MAAM,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QACnG,CAAC;IACH,CAAC;IACD,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAA;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAwB,EAAE,QAAgB;IACxE,MAAM,GAAG,GAAG,IAAI,GAAG,EAAU,CAAA;IAC7B,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC,CAAA;QACtD,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,IAAI,GAAG,QAAQ,GAAG,CAAC,GAAG,OAAS,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAA;IACnF,CAAC;IACD,OAAO,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,CAAA;AACxB,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `settle_min` and `recovered_min` — the two per-night scalars the port adds
|
|
3
|
+
* to v3.1 (sleep-correction spec; registry §2.6; ruling R-C).
|
|
4
|
+
*
|
|
5
|
+
* settle_min: minutes from bed (the first primary minute) until the first HR
|
|
6
|
+
* sample at or below `night_min × 1.05` after which HR stays at or below that
|
|
7
|
+
* level for 15 minutes. `night_min` is the lowest HR inside the primary block.
|
|
8
|
+
* Null when it never settles, when there is no primary block, and — R-C — when
|
|
9
|
+
* the HR tier is coarser than 10 min: a 30-min cadence cannot support a
|
|
10
|
+
* minutes-resolution claim.
|
|
11
|
+
*
|
|
12
|
+
* Two readings of "stays for 15 minutes" the registry leaves open, decided
|
|
13
|
+
* here conservatively: every sample in [t, t + 15 min] must be at or below the
|
|
14
|
+
* level, and the data must actually reach t + 15 min — a run cut off by the end
|
|
15
|
+
* of the block is unconfirmed, not settled.
|
|
16
|
+
*/
|
|
17
|
+
import type { ClassifiedRow, VitalSample } from './classify';
|
|
18
|
+
export declare const SETTLE_HR_FACTOR = 1.05;
|
|
19
|
+
export declare const SETTLE_SUSTAIN_SEC = 900;
|
|
20
|
+
export declare const SETTLE_MAX_TIER_MIN = 10;
|
|
21
|
+
export declare function computeSettleMin(rows: ClassifiedRow[], vitals: VitalSample[], hrIntervalMin: number): number | null;
|
|
22
|
+
/** Primary minutes that came from HR recovery (envelope/gap), not firmware. */
|
|
23
|
+
export declare function recoveredMinutes(rows: ClassifiedRow[]): number;
|
|
24
|
+
//# sourceMappingURL=settle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settle.d.ts","sourceRoot":"","sources":["../../../src/sync/sleep-correction/settle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAI5D,eAAO,MAAM,gBAAgB,OAAO,CAAA;AACpC,eAAO,MAAM,kBAAkB,MAAM,CAAA;AACrC,eAAO,MAAM,mBAAmB,KAAK,CAAA;AAErC,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,aAAa,EAAE,EACrB,MAAM,EAAE,WAAW,EAAE,EACrB,aAAa,EAAE,MAAM,GACpB,MAAM,GAAG,IAAI,CAmCf;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,EAAE,GAAG,MAAM,CAE9D"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `settle_min` and `recovered_min` — the two per-night scalars the port adds
|
|
3
|
+
* to v3.1 (sleep-correction spec; registry §2.6; ruling R-C).
|
|
4
|
+
*
|
|
5
|
+
* settle_min: minutes from bed (the first primary minute) until the first HR
|
|
6
|
+
* sample at or below `night_min × 1.05` after which HR stays at or below that
|
|
7
|
+
* level for 15 minutes. `night_min` is the lowest HR inside the primary block.
|
|
8
|
+
* Null when it never settles, when there is no primary block, and — R-C — when
|
|
9
|
+
* the HR tier is coarser than 10 min: a 30-min cadence cannot support a
|
|
10
|
+
* minutes-resolution claim.
|
|
11
|
+
*
|
|
12
|
+
* Two readings of "stays for 15 minutes" the registry leaves open, decided
|
|
13
|
+
* here conservatively: every sample in [t, t + 15 min] must be at or below the
|
|
14
|
+
* level, and the data must actually reach t + 15 min — a run cut off by the end
|
|
15
|
+
* of the block is unconfirmed, not settled.
|
|
16
|
+
*/
|
|
17
|
+
import { parseDateStr } from './classify';
|
|
18
|
+
export const SETTLE_HR_FACTOR = 1.05;
|
|
19
|
+
export const SETTLE_SUSTAIN_SEC = 900;
|
|
20
|
+
export const SETTLE_MAX_TIER_MIN = 10;
|
|
21
|
+
export function computeSettleMin(rows, vitals, hrIntervalMin) {
|
|
22
|
+
if (hrIntervalMin > SETTLE_MAX_TIER_MIN)
|
|
23
|
+
return null;
|
|
24
|
+
const primary = rows.filter(r => r.block_type === 'primary').map(r => parseDateStr(r.date));
|
|
25
|
+
if (!primary.length)
|
|
26
|
+
return null;
|
|
27
|
+
const bed = Math.min(...primary);
|
|
28
|
+
const end = Math.max(...primary);
|
|
29
|
+
const hr = vitals
|
|
30
|
+
.filter(v => v.kind === 'hr' && Number.isFinite(v.value) && v.value > 0 && v.epoch >= bed && v.epoch <= end)
|
|
31
|
+
.map(v => ({ e: v.epoch, v: v.value }))
|
|
32
|
+
.sort((a, b) => a.e - b.e);
|
|
33
|
+
if (!hr.length)
|
|
34
|
+
return null;
|
|
35
|
+
const level = Math.min(...hr.map(h => h.v)) * SETTLE_HR_FACTOR;
|
|
36
|
+
const last = hr[hr.length - 1].e;
|
|
37
|
+
for (let i = 0; i < hr.length; i++) {
|
|
38
|
+
if (hr[i].v > level)
|
|
39
|
+
continue;
|
|
40
|
+
const until = hr[i].e + SETTLE_SUSTAIN_SEC;
|
|
41
|
+
if (last < until)
|
|
42
|
+
return null; // not enough data left to confirm any later run either
|
|
43
|
+
let held = true;
|
|
44
|
+
for (let j = i; j < hr.length && hr[j].e <= until; j++) {
|
|
45
|
+
if (hr[j].v > level) {
|
|
46
|
+
held = false;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (held)
|
|
51
|
+
return Math.round((hr[i].e - bed) / 60);
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
/** Primary minutes that came from HR recovery (envelope/gap), not firmware. */
|
|
56
|
+
export function recoveredMinutes(rows) {
|
|
57
|
+
return rows.filter(r => r.block_type === 'primary' && (r.source === 'envelope' || r.source === 'gap')).length;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=settle.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settle.js","sourceRoot":"","sources":["../../../src/sync/sleep-correction/settle.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep correction — Phase 2 SQL, ported from DataFusion to DuckDB.
|
|
3
|
+
*
|
|
4
|
+
* Source: ziva_app `sleepCorrectionLayer.ts` v3.1. The query SHAPE is kept as
|
|
5
|
+
* v3.1 wrote it — same CTEs, same self-joins, same UNION ALL offset generator
|
|
6
|
+
* — so a diff against the original reads as a list of dialect changes only:
|
|
7
|
+
*
|
|
8
|
+
* - `TO_CHAR(TO_TIMESTAMP(d), fmt)` → `strftime(make_timestamp(d * 1000000), fmt)`.
|
|
9
|
+
* make_timestamp is naive UTC; `to_timestamp()` would be TIMESTAMPTZ and
|
|
10
|
+
* render in the session zone, breaking Phase 3's UTC parse.
|
|
11
|
+
* - `APPROX_PERCENTILE_CONT` → `quantile_cont` (exact). Expect ~1-unit
|
|
12
|
+
* drift on the tier/p75/p90 scalars; never retune constants to hide it.
|
|
13
|
+
* - The unused nested-window `session_start` column is dropped (DuckDB
|
|
14
|
+
* rejects nested windows; the final `start` recomputes it anyway).
|
|
15
|
+
* - Confidence literals are CAST to DOUBLE — DuckDB types `0.90` as DECIMAL,
|
|
16
|
+
* DataFusion as Float64.
|
|
17
|
+
*
|
|
18
|
+
* Numbers are inlined, as v3.1 does: every value is computed by this module
|
|
19
|
+
* (epochs, tier, percentiles), never user input, and inlining sidesteps
|
|
20
|
+
* react-native-duckdb's untyped-param bind (zivaone_app#70). `num()` refuses
|
|
21
|
+
* anything non-finite.
|
|
22
|
+
*
|
|
23
|
+
* Relations are the v3.1 raw firmware shapes: sleep(date, quality, start,
|
|
24
|
+
* "unitLength"), heartrate(date, "singleHR"), activity(date, step), epochs in
|
|
25
|
+
* seconds. Production supplies staging views over the warehouse; the parity
|
|
26
|
+
* harness supplies tables loaded from captures.
|
|
27
|
+
*/
|
|
28
|
+
export interface SleepRelations {
|
|
29
|
+
sleep: string;
|
|
30
|
+
heartrate: string;
|
|
31
|
+
activity: string;
|
|
32
|
+
}
|
|
33
|
+
export declare const DEFAULT_RELATIONS: SleepRelations;
|
|
34
|
+
/** Once per sync. `hr_interval_minutes` is 5, 10 or 30 (v3.1 CASE, NULL ⇒ 30). */
|
|
35
|
+
export declare function detectHrTierSql(r?: SleepRelations): string;
|
|
36
|
+
/** Once per sync. Sleeping-HR p90 / p75 since `cutoffEpoch`; 120 when empty. */
|
|
37
|
+
export declare function globalBaselinesSql(cutoffEpoch: number, r?: SleepRelations): string;
|
|
38
|
+
/** Per night, before Phase 2: zero firmware sleep ⇒ skip the night entirely. */
|
|
39
|
+
export declare function hasSleepDataSql(windowStart: number, windowEnd: number, r?: SleepRelations): string;
|
|
40
|
+
/** Phase 2 — envelope extension, gap recovery and session stitching. */
|
|
41
|
+
export declare function correctNightSql(windowStart: number, windowEnd: number, hrIntervalMin: number, globalP75: number, globalP90: number, r?: SleepRelations): string;
|
|
42
|
+
/** Phase 3 input — HR + step samples for the Step 5.6 END refinement. */
|
|
43
|
+
export declare function morningVitalsSql(windowStart: number, windowEnd: number, r?: SleepRelations): string;
|
|
44
|
+
//# sourceMappingURL=sql.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.d.ts","sourceRoot":"","sources":["../../../src/sync/sleep-correction/sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAmBH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED,eAAO,MAAM,iBAAiB,EAAE,cAI/B,CAAA;AAgBD,kFAAkF;AAClF,wBAAgB,eAAe,CAAC,CAAC,GAAE,cAAkC,GAAG,MAAM,CAyB7E;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,EAAE,CAAC,GAAE,cAAkC,GAAG,MAAM,CAarG;AAED,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,GAAE,cAAkC,GAAG,MAAM,CAOrH;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAC7B,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,CAAC,GAAE,cAAkC,GACpC,MAAM,CA2NR;AAED,yEAAyE;AACzE,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,GAAE,cAAkC,GAAG,MAAM,CAWtH"}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep correction — Phase 2 SQL, ported from DataFusion to DuckDB.
|
|
3
|
+
*
|
|
4
|
+
* Source: ziva_app `sleepCorrectionLayer.ts` v3.1. The query SHAPE is kept as
|
|
5
|
+
* v3.1 wrote it — same CTEs, same self-joins, same UNION ALL offset generator
|
|
6
|
+
* — so a diff against the original reads as a list of dialect changes only:
|
|
7
|
+
*
|
|
8
|
+
* - `TO_CHAR(TO_TIMESTAMP(d), fmt)` → `strftime(make_timestamp(d * 1000000), fmt)`.
|
|
9
|
+
* make_timestamp is naive UTC; `to_timestamp()` would be TIMESTAMPTZ and
|
|
10
|
+
* render in the session zone, breaking Phase 3's UTC parse.
|
|
11
|
+
* - `APPROX_PERCENTILE_CONT` → `quantile_cont` (exact). Expect ~1-unit
|
|
12
|
+
* drift on the tier/p75/p90 scalars; never retune constants to hide it.
|
|
13
|
+
* - The unused nested-window `session_start` column is dropped (DuckDB
|
|
14
|
+
* rejects nested windows; the final `start` recomputes it anyway).
|
|
15
|
+
* - Confidence literals are CAST to DOUBLE — DuckDB types `0.90` as DECIMAL,
|
|
16
|
+
* DataFusion as Float64.
|
|
17
|
+
*
|
|
18
|
+
* Numbers are inlined, as v3.1 does: every value is computed by this module
|
|
19
|
+
* (epochs, tier, percentiles), never user input, and inlining sidesteps
|
|
20
|
+
* react-native-duckdb's untyped-param bind (zivaone_app#70). `num()` refuses
|
|
21
|
+
* anything non-finite.
|
|
22
|
+
*
|
|
23
|
+
* Relations are the v3.1 raw firmware shapes: sleep(date, quality, start,
|
|
24
|
+
* "unitLength"), heartrate(date, "singleHR"), activity(date, step), epochs in
|
|
25
|
+
* seconds. Production supplies staging views over the warehouse; the parity
|
|
26
|
+
* harness supplies tables loaded from captures.
|
|
27
|
+
*/
|
|
28
|
+
import { CONF_ENVELOPE, CONF_FIRMWARE, CONF_GAP, EXTEND_AFTER, FETCH_AFTER, FETCH_BEFORE, HR_MAX, HR_MIN, LONG_GAP_HR_FRAC, LONG_GAP_MAX, LONG_GAP_MIN, STITCH_GAP_LONG, STITCH_GAP_MED, STITCH_GAP_SHORT, } from './classify';
|
|
29
|
+
export const DEFAULT_RELATIONS = {
|
|
30
|
+
sleep: 'stg_sleep',
|
|
31
|
+
heartrate: 'stg_heartrate',
|
|
32
|
+
activity: 'stg_activity',
|
|
33
|
+
};
|
|
34
|
+
const IDENT_RE = /^[a-z_][\w.]*$/i;
|
|
35
|
+
function rel(name) {
|
|
36
|
+
if (!IDENT_RE.test(name))
|
|
37
|
+
throw new Error(`sleep-correction: invalid relation name ${JSON.stringify(name)}`);
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
function num(v) {
|
|
41
|
+
if (!Number.isFinite(v))
|
|
42
|
+
throw new Error(`sleep-correction: non-finite SQL value ${String(v)}`);
|
|
43
|
+
return String(v);
|
|
44
|
+
}
|
|
45
|
+
/** Once per sync. `hr_interval_minutes` is 5, 10 or 30 (v3.1 CASE, NULL ⇒ 30). */
|
|
46
|
+
export function detectHrTierSql(r = DEFAULT_RELATIONS) {
|
|
47
|
+
return `
|
|
48
|
+
WITH hr_numbered AS (
|
|
49
|
+
SELECT date,
|
|
50
|
+
ROW_NUMBER() OVER (ORDER BY date) AS rn
|
|
51
|
+
FROM ${rel(r.heartrate)}
|
|
52
|
+
WHERE "singleHR" BETWEEN ${HR_MIN} AND ${HR_MAX}
|
|
53
|
+
),
|
|
54
|
+
hr_intervals AS (
|
|
55
|
+
SELECT cur.date - COALESCE(prv.date, cur.date) AS gap_sec
|
|
56
|
+
FROM hr_numbered cur
|
|
57
|
+
LEFT JOIN hr_numbered prv ON prv.rn = cur.rn - 1
|
|
58
|
+
WHERE cur.date - COALESCE(prv.date, cur.date) BETWEEN 61 AND 3599
|
|
59
|
+
),
|
|
60
|
+
median_gap AS (
|
|
61
|
+
SELECT quantile_cont(gap_sec, 0.5) AS med
|
|
62
|
+
FROM hr_intervals
|
|
63
|
+
)
|
|
64
|
+
SELECT
|
|
65
|
+
med AS median_interval_seconds,
|
|
66
|
+
CASE WHEN med <= 360 THEN 5
|
|
67
|
+
WHEN med <= 900 THEN 10
|
|
68
|
+
ELSE 30
|
|
69
|
+
END AS hr_interval_minutes
|
|
70
|
+
FROM median_gap;`;
|
|
71
|
+
}
|
|
72
|
+
/** Once per sync. Sleeping-HR p90 / p75 since `cutoffEpoch`; 120 when empty. */
|
|
73
|
+
export function globalBaselinesSql(cutoffEpoch, r = DEFAULT_RELATIONS) {
|
|
74
|
+
return `
|
|
75
|
+
WITH hr_sleep AS (
|
|
76
|
+
SELECT h."singleHR" AS hr
|
|
77
|
+
FROM ${rel(r.heartrate)} h
|
|
78
|
+
INNER JOIN ${rel(r.sleep)} s ON ABS(h.date - s.date) <= 30
|
|
79
|
+
WHERE h."singleHR" BETWEEN ${HR_MIN} AND ${HR_MAX}
|
|
80
|
+
AND CAST(s.quality AS INT) IN (1, 2, 3)
|
|
81
|
+
AND h.date >= ${num(cutoffEpoch)}
|
|
82
|
+
)
|
|
83
|
+
SELECT
|
|
84
|
+
COALESCE((SELECT quantile_cont(hr, 0.90) FROM hr_sleep), ${HR_MAX}) AS p90_global,
|
|
85
|
+
COALESCE((SELECT quantile_cont(hr, 0.75) FROM hr_sleep), ${HR_MAX}) AS p75_global;`;
|
|
86
|
+
}
|
|
87
|
+
/** Per night, before Phase 2: zero firmware sleep ⇒ skip the night entirely. */
|
|
88
|
+
export function hasSleepDataSql(windowStart, windowEnd, r = DEFAULT_RELATIONS) {
|
|
89
|
+
return `
|
|
90
|
+
SELECT COUNT(*) AS has_data
|
|
91
|
+
FROM ${rel(r.sleep)}
|
|
92
|
+
WHERE date >= ${num(windowStart)}
|
|
93
|
+
AND date < ${num(windowEnd)}
|
|
94
|
+
AND CAST(quality AS INT) IN (1, 2, 3, 5);`;
|
|
95
|
+
}
|
|
96
|
+
/** Phase 2 — envelope extension, gap recovery and session stitching. */
|
|
97
|
+
export function correctNightSql(windowStart, windowEnd, hrIntervalMin, globalP75, globalP90, r = DEFAULT_RELATIONS) {
|
|
98
|
+
const fetchStart = windowStart - FETCH_BEFORE;
|
|
99
|
+
const fetchEnd = windowEnd + FETCH_AFTER;
|
|
100
|
+
const offsets = Array.from({ length: hrIntervalMin }, (_, i) => `SELECT ${i} AS v`).join(' UNION ALL ');
|
|
101
|
+
const ws = num(windowStart);
|
|
102
|
+
const we = num(windowEnd);
|
|
103
|
+
return `
|
|
104
|
+
WITH sleep_raw AS (
|
|
105
|
+
SELECT
|
|
106
|
+
date,
|
|
107
|
+
CAST(quality AS INT) AS quality,
|
|
108
|
+
start AS fw_start,
|
|
109
|
+
CAST("unitLength" AS INT) AS unit_length
|
|
110
|
+
FROM ${rel(r.sleep)}
|
|
111
|
+
WHERE date >= ${ws}
|
|
112
|
+
AND date < ${we}
|
|
113
|
+
AND CAST(quality AS INT) IN (1, 2, 3, 5)
|
|
114
|
+
),
|
|
115
|
+
|
|
116
|
+
fw_bounds AS (
|
|
117
|
+
SELECT
|
|
118
|
+
COALESCE(MIN(date), ${ws}) AS first_fw,
|
|
119
|
+
COALESCE(MAX(date), ${we}) AS last_fw
|
|
120
|
+
FROM sleep_raw
|
|
121
|
+
WHERE quality IN (1, 2, 3, 5)
|
|
122
|
+
),
|
|
123
|
+
|
|
124
|
+
night_hr AS (
|
|
125
|
+
SELECT date, "singleHR" AS hr
|
|
126
|
+
FROM ${rel(r.heartrate)}
|
|
127
|
+
WHERE date >= ${num(fetchStart)}
|
|
128
|
+
AND date <= ${num(fetchEnd)}
|
|
129
|
+
AND "singleHR" BETWEEN ${HR_MIN} AND ${HR_MAX}
|
|
130
|
+
),
|
|
131
|
+
|
|
132
|
+
effective AS (
|
|
133
|
+
SELECT
|
|
134
|
+
fw_bounds.first_fw AS eff_start,
|
|
135
|
+
fw_bounds.last_fw + ${EXTEND_AFTER} AS eff_end
|
|
136
|
+
FROM fw_bounds
|
|
137
|
+
),
|
|
138
|
+
|
|
139
|
+
fw_trimmed AS (
|
|
140
|
+
SELECT s.date, s.quality, s.fw_start, s.unit_length,
|
|
141
|
+
CAST(${CONF_FIRMWARE} AS DOUBLE) AS confidence
|
|
142
|
+
FROM sleep_raw s
|
|
143
|
+
CROSS JOIN effective e
|
|
144
|
+
WHERE s.date >= e.eff_start
|
|
145
|
+
AND s.quality IN (1, 2, 3, 5)
|
|
146
|
+
),
|
|
147
|
+
|
|
148
|
+
fw_has_rows AS (
|
|
149
|
+
SELECT CASE WHEN COUNT(*) > 0 THEN 1 ELSE 0 END AS has_rows
|
|
150
|
+
FROM sleep_raw
|
|
151
|
+
WHERE quality IN (1, 2, 3, 5)
|
|
152
|
+
),
|
|
153
|
+
|
|
154
|
+
envelope_hr AS (
|
|
155
|
+
SELECT h.date AS hr_epoch
|
|
156
|
+
FROM night_hr h
|
|
157
|
+
CROSS JOIN effective e
|
|
158
|
+
CROSS JOIN fw_has_rows fhr
|
|
159
|
+
CROSS JOIN fw_bounds fb
|
|
160
|
+
WHERE fhr.has_rows = 1
|
|
161
|
+
AND h.date >= fb.first_fw
|
|
162
|
+
AND h.date <= e.eff_end
|
|
163
|
+
AND h.hr <= ${num(globalP90)}
|
|
164
|
+
AND NOT EXISTS (
|
|
165
|
+
SELECT 1 FROM sleep_raw fs
|
|
166
|
+
WHERE ABS(fs.date - h.date) <= 30
|
|
167
|
+
AND fs.quality IN (1, 2, 3, 5)
|
|
168
|
+
)
|
|
169
|
+
),
|
|
170
|
+
|
|
171
|
+
offsets AS (${offsets}),
|
|
172
|
+
|
|
173
|
+
envelope_expanded AS (
|
|
174
|
+
SELECT DISTINCT
|
|
175
|
+
eh.hr_epoch + (o.v * 60) AS date,
|
|
176
|
+
2 AS quality,
|
|
177
|
+
NULL AS fw_start,
|
|
178
|
+
1 AS unit_length,
|
|
179
|
+
CAST(${CONF_ENVELOPE} AS DOUBLE) AS confidence
|
|
180
|
+
FROM envelope_hr eh
|
|
181
|
+
CROSS JOIN offsets o
|
|
182
|
+
CROSS JOIN effective e
|
|
183
|
+
WHERE eh.hr_epoch + (o.v * 60) >= e.eff_start
|
|
184
|
+
AND eh.hr_epoch + (o.v * 60) <= e.eff_end
|
|
185
|
+
),
|
|
186
|
+
|
|
187
|
+
fw_sessions AS (
|
|
188
|
+
SELECT fw_start,
|
|
189
|
+
MIN(date) AS sess_start,
|
|
190
|
+
MAX(date) AS sess_end,
|
|
191
|
+
COUNT(*) AS sess_len
|
|
192
|
+
FROM sleep_raw
|
|
193
|
+
WHERE quality IN (1, 2, 3, 5)
|
|
194
|
+
GROUP BY fw_start
|
|
195
|
+
),
|
|
196
|
+
|
|
197
|
+
fw_sess_numbered AS (
|
|
198
|
+
SELECT *,
|
|
199
|
+
ROW_NUMBER() OVER (ORDER BY sess_start) AS rn
|
|
200
|
+
FROM fw_sessions
|
|
201
|
+
),
|
|
202
|
+
|
|
203
|
+
fw_gaps AS (
|
|
204
|
+
SELECT
|
|
205
|
+
cur.sess_end AS gap_start,
|
|
206
|
+
nxt.sess_start AS gap_end,
|
|
207
|
+
nxt.sess_start - cur.sess_end AS gap_sec
|
|
208
|
+
FROM fw_sess_numbered cur
|
|
209
|
+
LEFT JOIN fw_sess_numbered nxt ON nxt.rn = cur.rn + 1
|
|
210
|
+
WHERE nxt.sess_start IS NOT NULL
|
|
211
|
+
AND nxt.sess_start - cur.sess_end BETWEEN ${LONG_GAP_MIN} AND ${LONG_GAP_MAX}
|
|
212
|
+
),
|
|
213
|
+
|
|
214
|
+
gap_hr_check AS (
|
|
215
|
+
SELECT
|
|
216
|
+
fg.gap_start, fg.gap_end,
|
|
217
|
+
CASE
|
|
218
|
+
WHEN (SELECT has_rows FROM fw_has_rows) = 0 THEN 0
|
|
219
|
+
WHEN COUNT(h.date) >= 3
|
|
220
|
+
AND CAST(SUM(CASE WHEN h.hr <= ${num(globalP75)} THEN 1 ELSE 0 END) AS DOUBLE)
|
|
221
|
+
/ CAST(COUNT(h.date) AS DOUBLE) >= ${LONG_GAP_HR_FRAC}
|
|
222
|
+
THEN 1 ELSE 0
|
|
223
|
+
END AS qualifies
|
|
224
|
+
FROM fw_gaps fg
|
|
225
|
+
LEFT JOIN night_hr h
|
|
226
|
+
ON h.date > fg.gap_start
|
|
227
|
+
AND h.date < fg.gap_end
|
|
228
|
+
GROUP BY fg.gap_start, fg.gap_end
|
|
229
|
+
),
|
|
230
|
+
|
|
231
|
+
gap_recovered AS (
|
|
232
|
+
SELECT DISTINCT
|
|
233
|
+
h.date + (o.v * 60) AS date,
|
|
234
|
+
2 AS quality,
|
|
235
|
+
NULL AS fw_start,
|
|
236
|
+
1 AS unit_length,
|
|
237
|
+
CAST(${CONF_GAP} AS DOUBLE) AS confidence
|
|
238
|
+
FROM gap_hr_check gc
|
|
239
|
+
INNER JOIN night_hr h
|
|
240
|
+
ON h.date > gc.gap_start
|
|
241
|
+
AND h.date < gc.gap_end
|
|
242
|
+
CROSS JOIN offsets o
|
|
243
|
+
WHERE gc.qualifies = 1
|
|
244
|
+
AND h.date + (o.v * 60) > gc.gap_start
|
|
245
|
+
AND h.date + (o.v * 60) < gc.gap_end
|
|
246
|
+
),
|
|
247
|
+
|
|
248
|
+
combined_raw AS (
|
|
249
|
+
SELECT date, quality, fw_start, unit_length, confidence, 'firmware' AS source
|
|
250
|
+
FROM fw_trimmed
|
|
251
|
+
UNION ALL
|
|
252
|
+
SELECT date, quality, fw_start, unit_length, confidence, 'envelope' AS source
|
|
253
|
+
FROM envelope_expanded
|
|
254
|
+
UNION ALL
|
|
255
|
+
SELECT date, quality, fw_start, unit_length, confidence, 'gap' AS source
|
|
256
|
+
FROM gap_recovered
|
|
257
|
+
),
|
|
258
|
+
|
|
259
|
+
deduped AS (
|
|
260
|
+
SELECT date, quality, fw_start, unit_length, confidence, source
|
|
261
|
+
FROM (
|
|
262
|
+
SELECT *,
|
|
263
|
+
ROW_NUMBER() OVER (
|
|
264
|
+
PARTITION BY date
|
|
265
|
+
ORDER BY CASE source WHEN 'firmware' THEN 0 ELSE 1 END
|
|
266
|
+
) AS rn
|
|
267
|
+
FROM combined_raw
|
|
268
|
+
)
|
|
269
|
+
WHERE rn = 1
|
|
270
|
+
),
|
|
271
|
+
|
|
272
|
+
deduped_numbered AS (
|
|
273
|
+
SELECT *,
|
|
274
|
+
ROW_NUMBER() OVER (ORDER BY date) AS rn
|
|
275
|
+
FROM deduped
|
|
276
|
+
),
|
|
277
|
+
|
|
278
|
+
with_gaps AS (
|
|
279
|
+
SELECT
|
|
280
|
+
cur.date, cur.quality, cur.fw_start, cur.unit_length,
|
|
281
|
+
cur.confidence, cur.source, cur.rn,
|
|
282
|
+
cur.date - COALESCE(prv.date, cur.date) AS gap_to_prev,
|
|
283
|
+
COALESCE(prv.unit_length, 0) AS prev_unit_length,
|
|
284
|
+
COALESCE(prv.date, cur.date) AS prev_date
|
|
285
|
+
FROM deduped_numbered cur
|
|
286
|
+
LEFT JOIN deduped_numbered prv ON prv.rn = cur.rn - 1
|
|
287
|
+
),
|
|
288
|
+
|
|
289
|
+
stitched AS (
|
|
290
|
+
SELECT
|
|
291
|
+
date, quality, source, confidence,
|
|
292
|
+
SUM(CASE
|
|
293
|
+
WHEN gap_to_prev <= ${STITCH_GAP_SHORT} THEN 0
|
|
294
|
+
WHEN prev_unit_length = 120
|
|
295
|
+
AND gap_to_prev <= ${STITCH_GAP_MED} THEN 0
|
|
296
|
+
WHEN gap_to_prev <= ${STITCH_GAP_MED} THEN 0
|
|
297
|
+
WHEN gap_to_prev <= ${STITCH_GAP_LONG} THEN 0
|
|
298
|
+
ELSE 1
|
|
299
|
+
END) OVER (ORDER BY rn ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS session_id
|
|
300
|
+
FROM with_gaps
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
SELECT
|
|
304
|
+
strftime(make_timestamp(date * 1000000), '%Y.%m.%d %H:%M:%S') AS date,
|
|
305
|
+
quality,
|
|
306
|
+
strftime(make_timestamp(MIN(date) OVER (
|
|
307
|
+
PARTITION BY session_id
|
|
308
|
+
) * 1000000), '%Y.%m.%d %H:%M:%S') AS start,
|
|
309
|
+
1 AS "unitLength",
|
|
310
|
+
source,
|
|
311
|
+
confidence,
|
|
312
|
+
session_id
|
|
313
|
+
FROM stitched
|
|
314
|
+
ORDER BY date;`;
|
|
315
|
+
}
|
|
316
|
+
/** Phase 3 input — HR + step samples for the Step 5.6 END refinement. */
|
|
317
|
+
export function morningVitalsSql(windowStart, windowEnd, r = DEFAULT_RELATIONS) {
|
|
318
|
+
return `
|
|
319
|
+
SELECT 'hr' AS kind, date AS epoch, "singleHR" AS value
|
|
320
|
+
FROM ${rel(r.heartrate)}
|
|
321
|
+
WHERE date >= ${num(windowStart)} AND date < ${num(windowEnd + FETCH_AFTER)}
|
|
322
|
+
AND "singleHR" BETWEEN ${HR_MIN} AND ${HR_MAX}
|
|
323
|
+
UNION ALL
|
|
324
|
+
SELECT 'step' AS kind, date AS epoch, step AS value
|
|
325
|
+
FROM ${rel(r.activity)}
|
|
326
|
+
WHERE date >= ${num(windowStart)} AND date < ${num(windowEnd + FETCH_AFTER)}
|
|
327
|
+
ORDER BY epoch;`;
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=sql.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql.js","sourceRoot":"","sources":["../../../src/sync/sleep-correction/sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EACL,aAAa,EACb,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,MAAM,EACN,MAAM,EACN,gBAAgB,EAChB,YAAY,EACZ,YAAY,EACZ,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,YAAY,CAAA;AAQnB,MAAM,CAAC,MAAM,iBAAiB,GAAmB;IAC/C,KAAK,EAAE,WAAW;IAClB,SAAS,EAAE,eAAe;IAC1B,QAAQ,EAAE,cAAc;CACzB,CAAA;AAED,MAAM,QAAQ,GAAG,iBAAiB,CAAA;AAElC,SAAS,GAAG,CAAC,IAAY;IACvB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC;QACtB,MAAM,IAAI,KAAK,CAAC,2CAA2C,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACpF,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,GAAG,CAAC,CAAS;IACpB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,0CAA0C,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IACxE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAA;AAClB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,eAAe,CAAC,IAAoB,iBAAiB;IACnE,OAAO;;;;WAIE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;+BACI,MAAM,QAAQ,MAAM;;;;;;;;;;;;;;;;;;iBAkBlC,CAAA;AACjB,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,kBAAkB,CAAC,WAAmB,EAAE,IAAoB,iBAAiB;IAC3F,OAAO;;;WAGE,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;iBACV,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;iCACI,MAAM,QAAQ,MAAM;;sBAE/B,GAAG,CAAC,WAAW,CAAC;;;+DAGyB,MAAM;+DACN,MAAM,kBAAkB,CAAA;AACvF,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAAC,WAAmB,EAAE,SAAiB,EAAE,IAAoB,iBAAiB;IAC3G,OAAO;;OAEF,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;gBACH,GAAG,CAAC,WAAW,CAAC;gBAChB,GAAG,CAAC,SAAS,CAAC;4CACc,CAAA;AAC5C,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,eAAe,CAC7B,WAAmB,EACnB,SAAiB,EACjB,aAAqB,EACrB,SAAiB,EACjB,SAAiB,EACjB,IAAoB,iBAAiB;IAErC,MAAM,UAAU,GAAG,WAAW,GAAG,YAAY,CAAA;IAC7C,MAAM,QAAQ,GAAG,SAAS,GAAG,WAAW,CAAA;IACxC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IACvG,MAAM,EAAE,GAAG,GAAG,CAAC,WAAW,CAAC,CAAA;IAC3B,MAAM,EAAE,GAAG,GAAG,CAAC,SAAS,CAAC,CAAA;IAEzB,OAAO;;;;;;;WAOE,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;oBACH,EAAE;oBACF,EAAE;;;;;;8BAMQ,EAAE;8BACF,EAAE;;;;;;;WAOrB,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;oBACP,GAAG,CAAC,UAAU,CAAC;oBACf,GAAG,CAAC,QAAQ,CAAC;+BACF,MAAM,QAAQ,MAAM;;;;;;8BAMrB,YAAY;;;;;;kBAMxB,aAAa;;;;;;;;;;;;;;;;;;;;;;sBAsBT,GAAG,CAAC,SAAS,CAAC;;;;;;;;cAQtB,OAAO;;;;;;;;eAQN,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kDAgCsB,YAAY,QAAQ,YAAY;;;;;;;;;8CASpC,GAAG,CAAC,SAAS,CAAC;oDACR,gBAAgB;;;;;;;;;;;;;;;;eAgBrD,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAwDW,gBAAgB;;kCAEhB,cAAc;kCACd,cAAc;kCACd,eAAe;;;;;;;;;;;;;;;;;eAiBlC,CAAA;AACf,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,gBAAgB,CAAC,WAAmB,EAAE,SAAiB,EAAE,IAAoB,iBAAiB;IAC5G,OAAO;;OAEF,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;gBACP,GAAG,CAAC,WAAW,CAAC,eAAe,GAAG,CAAC,SAAS,GAAG,WAAW,CAAC;2BAChD,MAAM,QAAQ,MAAM;;;OAGxC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;gBACN,GAAG,CAAC,WAAW,CAAC,eAAe,GAAG,CAAC,SAAS,GAAG,WAAW,CAAC;gBAC3D,CAAA;AAChB,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staging views: the warehouse, re-shaped into v3.1's raw firmware relations.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline's SQL reads sleep(date, quality, start, "unitLength"),
|
|
5
|
+
* heartrate(date, "singleHR") and activity(date, step) with epoch-second
|
|
6
|
+
* dates — the shapes v3.1 was validated on. Rather than rewrite the SQL for
|
|
7
|
+
* the warehouse columns, these TEMP views rename back at the boundary, so the
|
|
8
|
+
* ported queries stay as close to v3.1 as the parity suite proved them.
|
|
9
|
+
*
|
|
10
|
+
* - sleep comes from the LOCAL `sleep_raw` (not viewed: collected-only, and
|
|
11
|
+
* the nights a batch touches are local). Catalog-qualified, because after
|
|
12
|
+
* an R2 attach an unqualified name resolves to the remote catalog.
|
|
13
|
+
* - HR and steps come from `v_heart_rate` / `v_activity`, so a 30-day
|
|
14
|
+
* baseline lookback sees remote history too.
|
|
15
|
+
* - One user and one device: the correction is per ring.
|
|
16
|
+
*
|
|
17
|
+
* `sleep_raw.quality` must hold the firmware's own codes (1 deep / 2 light /
|
|
18
|
+
* 3 rem / 5 awake). That is true only once the producer sends raw rows
|
|
19
|
+
* (sleep-correction tasks §3); until then these views are exercised by tests.
|
|
20
|
+
*/
|
|
21
|
+
import type { SleepRelations } from './sql';
|
|
22
|
+
export interface StagingScope {
|
|
23
|
+
brand: string;
|
|
24
|
+
familyId: string;
|
|
25
|
+
userId: string;
|
|
26
|
+
deviceId: string;
|
|
27
|
+
/** Local DuckDB catalog holding `sleep_raw`. */
|
|
28
|
+
localCatalog?: string;
|
|
29
|
+
}
|
|
30
|
+
/** `CREATE OR REPLACE TEMP VIEW` statements for the three staging relations. */
|
|
31
|
+
export declare function stagingViewsSql(scope: StagingScope, r?: SleepRelations): string[];
|
|
32
|
+
//# sourceMappingURL=staging.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staging.d.ts","sourceRoot":"","sources":["../../../src/sync/sleep-correction/staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAI3C,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,CAAA;IAChB,gDAAgD;IAChD,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AASD,gFAAgF;AAChF,wBAAgB,eAAe,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,GAAE,cAAkC,GAAG,MAAM,EAAE,CAsBpG"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Staging views: the warehouse, re-shaped into v3.1's raw firmware relations.
|
|
3
|
+
*
|
|
4
|
+
* The pipeline's SQL reads sleep(date, quality, start, "unitLength"),
|
|
5
|
+
* heartrate(date, "singleHR") and activity(date, step) with epoch-second
|
|
6
|
+
* dates — the shapes v3.1 was validated on. Rather than rewrite the SQL for
|
|
7
|
+
* the warehouse columns, these TEMP views rename back at the boundary, so the
|
|
8
|
+
* ported queries stay as close to v3.1 as the parity suite proved them.
|
|
9
|
+
*
|
|
10
|
+
* - sleep comes from the LOCAL `sleep_raw` (not viewed: collected-only, and
|
|
11
|
+
* the nights a batch touches are local). Catalog-qualified, because after
|
|
12
|
+
* an R2 attach an unqualified name resolves to the remote catalog.
|
|
13
|
+
* - HR and steps come from `v_heart_rate` / `v_activity`, so a 30-day
|
|
14
|
+
* baseline lookback sees remote history too.
|
|
15
|
+
* - One user and one device: the correction is per ring.
|
|
16
|
+
*
|
|
17
|
+
* `sleep_raw.quality` must hold the firmware's own codes (1 deep / 2 light /
|
|
18
|
+
* 3 rem / 5 awake). That is true only once the producer sends raw rows
|
|
19
|
+
* (sleep-correction tasks §3); until then these views are exercised by tests.
|
|
20
|
+
*/
|
|
21
|
+
import { DEFAULT_RELATIONS } from './sql';
|
|
22
|
+
const CATALOG_RE = /^[a-z_]\w*$/i;
|
|
23
|
+
const QUOTE_RE = /'/g;
|
|
24
|
+
function lit(s) {
|
|
25
|
+
return `'${s.replace(QUOTE_RE, '\'\'')}'`;
|
|
26
|
+
}
|
|
27
|
+
/** `CREATE OR REPLACE TEMP VIEW` statements for the three staging relations. */
|
|
28
|
+
export function stagingViewsSql(scope, r = DEFAULT_RELATIONS) {
|
|
29
|
+
const catalog = scope.localCatalog ?? 'memory';
|
|
30
|
+
if (!CATALOG_RE.test(catalog))
|
|
31
|
+
throw new Error(`sleep-correction: invalid catalog ${JSON.stringify(catalog)}`);
|
|
32
|
+
const tenant = `user_id = ${lit(scope.userId)} AND device_id = ${lit(scope.deviceId)}`;
|
|
33
|
+
return [
|
|
34
|
+
`CREATE OR REPLACE TEMP VIEW ${r.sleep} AS
|
|
35
|
+
SELECT CAST(epoch(ts) AS BIGINT) AS date,
|
|
36
|
+
quality,
|
|
37
|
+
CAST(epoch(ts_session_start) AS BIGINT) AS start,
|
|
38
|
+
unit_length AS "unitLength"
|
|
39
|
+
FROM ${catalog}.main.sleep_raw
|
|
40
|
+
WHERE brand = ${lit(scope.brand)} AND family_id = ${lit(scope.familyId)} AND ${tenant}`,
|
|
41
|
+
`CREATE OR REPLACE TEMP VIEW ${r.heartrate} AS
|
|
42
|
+
SELECT CAST(epoch(ts) AS BIGINT) AS date, bpm AS "singleHR"
|
|
43
|
+
FROM v_heart_rate
|
|
44
|
+
WHERE ${tenant}`,
|
|
45
|
+
`CREATE OR REPLACE TEMP VIEW ${r.activity} AS
|
|
46
|
+
SELECT CAST(epoch(ts) AS BIGINT) AS date, steps AS step
|
|
47
|
+
FROM v_activity
|
|
48
|
+
WHERE ${tenant}`,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=staging.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"staging.js","sourceRoot":"","sources":["../../../src/sync/sleep-correction/staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAA;AAWzC,MAAM,UAAU,GAAG,cAAc,CAAA;AACjC,MAAM,QAAQ,GAAG,IAAI,CAAA;AAErB,SAAS,GAAG,CAAC,CAAS;IACpB,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,MAAM,CAAC,GAAG,CAAA;AAC3C,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,eAAe,CAAC,KAAmB,EAAE,IAAoB,iBAAiB;IACxF,MAAM,OAAO,GAAG,KAAK,CAAC,YAAY,IAAI,QAAQ,CAAA;IAC9C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,qCAAqC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IACjF,MAAM,MAAM,GAAG,aAAa,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,oBAAoB,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAA;IACtF,OAAO;QACL,+BAA+B,CAAC,CAAC,KAAK;;;;;OAKnC,OAAO;gBACE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,oBAAoB,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,MAAM,EAAE;QACnF,+BAA+B,CAAC,CAAC,SAAS;;;QAGtC,MAAM,EAAE;QACZ,+BAA+B,CAAC,CAAC,QAAQ;;;QAGrC,MAAM,EAAE;KACb,CAAA;AACH,CAAC"}
|