@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.
Files changed (64) hide show
  1. package/dist/core/migrations.d.ts.map +1 -1
  2. package/dist/core/migrations.js +35 -0
  3. package/dist/core/migrations.js.map +1 -1
  4. package/dist/core/schemas.d.ts.map +1 -1
  5. package/dist/core/schemas.js +5 -2
  6. package/dist/core/schemas.js.map +1 -1
  7. package/dist/sync/factory.d.ts +7 -0
  8. package/dist/sync/factory.d.ts.map +1 -1
  9. package/dist/sync/factory.js +31 -1
  10. package/dist/sync/factory.js.map +1 -1
  11. package/dist/sync/index.d.ts +2 -2
  12. package/dist/sync/index.d.ts.map +1 -1
  13. package/dist/sync/index.js +1 -1
  14. package/dist/sync/index.js.map +1 -1
  15. package/dist/sync/mapper/firmware.d.ts.map +1 -1
  16. package/dist/sync/mapper/firmware.js +7 -5
  17. package/dist/sync/mapper/firmware.js.map +1 -1
  18. package/dist/sync/mapper/schema.d.ts +29 -36
  19. package/dist/sync/mapper/schema.d.ts.map +1 -1
  20. package/dist/sync/mapper/schema.js +11 -12
  21. package/dist/sync/mapper/schema.js.map +1 -1
  22. package/dist/sync/mapper/sleep.d.ts +50 -43
  23. package/dist/sync/mapper/sleep.d.ts.map +1 -1
  24. package/dist/sync/mapper/sleep.js +99 -138
  25. package/dist/sync/mapper/sleep.js.map +1 -1
  26. package/dist/sync/mapper/types.d.ts +17 -18
  27. package/dist/sync/mapper/types.d.ts.map +1 -1
  28. package/dist/sync/sleep-correction/classify.d.ts +86 -0
  29. package/dist/sync/sleep-correction/classify.d.ts.map +1 -0
  30. package/dist/sync/sleep-correction/classify.js +247 -0
  31. package/dist/sync/sleep-correction/classify.js.map +1 -0
  32. package/dist/sync/sleep-correction/correct.d.ts +46 -0
  33. package/dist/sync/sleep-correction/correct.d.ts.map +1 -0
  34. package/dist/sync/sleep-correction/correct.js +60 -0
  35. package/dist/sync/sleep-correction/correct.js.map +1 -0
  36. package/dist/sync/sleep-correction/index.d.ts +8 -0
  37. package/dist/sync/sleep-correction/index.d.ts.map +1 -0
  38. package/dist/sync/sleep-correction/index.js +8 -0
  39. package/dist/sync/sleep-correction/index.js.map +1 -0
  40. package/dist/sync/sleep-correction/night.d.ts +26 -0
  41. package/dist/sync/sleep-correction/night.d.ts.map +1 -0
  42. package/dist/sync/sleep-correction/night.js +48 -0
  43. package/dist/sync/sleep-correction/night.js.map +1 -0
  44. package/dist/sync/sleep-correction/orchestrate.d.ts +50 -0
  45. package/dist/sync/sleep-correction/orchestrate.d.ts.map +1 -0
  46. package/dist/sync/sleep-correction/orchestrate.js +57 -0
  47. package/dist/sync/sleep-correction/orchestrate.js.map +1 -0
  48. package/dist/sync/sleep-correction/settle.d.ts +24 -0
  49. package/dist/sync/sleep-correction/settle.d.ts.map +1 -0
  50. package/dist/sync/sleep-correction/settle.js +59 -0
  51. package/dist/sync/sleep-correction/settle.js.map +1 -0
  52. package/dist/sync/sleep-correction/sql.d.ts +44 -0
  53. package/dist/sync/sleep-correction/sql.d.ts.map +1 -0
  54. package/dist/sync/sleep-correction/sql.js +329 -0
  55. package/dist/sync/sleep-correction/sql.js.map +1 -0
  56. package/dist/sync/sleep-correction/staging.d.ts +32 -0
  57. package/dist/sync/sleep-correction/staging.d.ts.map +1 -0
  58. package/dist/sync/sleep-correction/staging.js +51 -0
  59. package/dist/sync/sleep-correction/staging.js.map +1 -0
  60. package/dist/sync/sleep-derive.d.ts +55 -0
  61. package/dist/sync/sleep-derive.d.ts.map +1 -0
  62. package/dist/sync/sleep-derive.js +180 -0
  63. package/dist/sync/sleep-derive.js.map +1 -0
  64. 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":"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"}
@@ -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"}