@mongrov/analytics 0.23.1 → 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 (67) 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/rules/compiler.d.ts.map +1 -1
  8. package/dist/rules/compiler.js +37 -16
  9. package/dist/rules/compiler.js.map +1 -1
  10. package/dist/sync/factory.d.ts +7 -0
  11. package/dist/sync/factory.d.ts.map +1 -1
  12. package/dist/sync/factory.js +31 -1
  13. package/dist/sync/factory.js.map +1 -1
  14. package/dist/sync/index.d.ts +2 -2
  15. package/dist/sync/index.d.ts.map +1 -1
  16. package/dist/sync/index.js +1 -1
  17. package/dist/sync/index.js.map +1 -1
  18. package/dist/sync/mapper/firmware.d.ts.map +1 -1
  19. package/dist/sync/mapper/firmware.js +7 -5
  20. package/dist/sync/mapper/firmware.js.map +1 -1
  21. package/dist/sync/mapper/schema.d.ts +29 -36
  22. package/dist/sync/mapper/schema.d.ts.map +1 -1
  23. package/dist/sync/mapper/schema.js +11 -12
  24. package/dist/sync/mapper/schema.js.map +1 -1
  25. package/dist/sync/mapper/sleep.d.ts +50 -43
  26. package/dist/sync/mapper/sleep.d.ts.map +1 -1
  27. package/dist/sync/mapper/sleep.js +99 -118
  28. package/dist/sync/mapper/sleep.js.map +1 -1
  29. package/dist/sync/mapper/types.d.ts +17 -18
  30. package/dist/sync/mapper/types.d.ts.map +1 -1
  31. package/dist/sync/sleep-correction/classify.d.ts +86 -0
  32. package/dist/sync/sleep-correction/classify.d.ts.map +1 -0
  33. package/dist/sync/sleep-correction/classify.js +247 -0
  34. package/dist/sync/sleep-correction/classify.js.map +1 -0
  35. package/dist/sync/sleep-correction/correct.d.ts +46 -0
  36. package/dist/sync/sleep-correction/correct.d.ts.map +1 -0
  37. package/dist/sync/sleep-correction/correct.js +60 -0
  38. package/dist/sync/sleep-correction/correct.js.map +1 -0
  39. package/dist/sync/sleep-correction/index.d.ts +8 -0
  40. package/dist/sync/sleep-correction/index.d.ts.map +1 -0
  41. package/dist/sync/sleep-correction/index.js +8 -0
  42. package/dist/sync/sleep-correction/index.js.map +1 -0
  43. package/dist/sync/sleep-correction/night.d.ts +26 -0
  44. package/dist/sync/sleep-correction/night.d.ts.map +1 -0
  45. package/dist/sync/sleep-correction/night.js +48 -0
  46. package/dist/sync/sleep-correction/night.js.map +1 -0
  47. package/dist/sync/sleep-correction/orchestrate.d.ts +50 -0
  48. package/dist/sync/sleep-correction/orchestrate.d.ts.map +1 -0
  49. package/dist/sync/sleep-correction/orchestrate.js +57 -0
  50. package/dist/sync/sleep-correction/orchestrate.js.map +1 -0
  51. package/dist/sync/sleep-correction/settle.d.ts +24 -0
  52. package/dist/sync/sleep-correction/settle.d.ts.map +1 -0
  53. package/dist/sync/sleep-correction/settle.js +59 -0
  54. package/dist/sync/sleep-correction/settle.js.map +1 -0
  55. package/dist/sync/sleep-correction/sql.d.ts +44 -0
  56. package/dist/sync/sleep-correction/sql.d.ts.map +1 -0
  57. package/dist/sync/sleep-correction/sql.js +329 -0
  58. package/dist/sync/sleep-correction/sql.js.map +1 -0
  59. package/dist/sync/sleep-correction/staging.d.ts +32 -0
  60. package/dist/sync/sleep-correction/staging.d.ts.map +1 -0
  61. package/dist/sync/sleep-correction/staging.js +51 -0
  62. package/dist/sync/sleep-correction/staging.js.map +1 -0
  63. package/dist/sync/sleep-derive.d.ts +55 -0
  64. package/dist/sync/sleep-derive.d.ts.map +1 -0
  65. package/dist/sync/sleep-derive.js +180 -0
  66. package/dist/sync/sleep-derive.js.map +1 -0
  67. package/package.json +4 -4
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Per-night offsets from the user's IANA zone (principle 24).
3
+ *
4
+ * v3.1 passed one fixed device offset (`getTzOffsetHours()`) for every night,
5
+ * so on the nights either side of a DST change the 18:00→18:00 window sat an
6
+ * hour off. The port asks the zone what offset is in force at the START of
7
+ * each night's window — 18:00 local on the previous day — and hands that to
8
+ * the v3.1 arithmetic unchanged.
9
+ *
10
+ * Offsets are read for INSTANTS via `formatInTimeZone(…, 'xxx')`. Not
11
+ * date-fns-tz's `getTimezoneOffset(zone, date)`: that reads the Date's fields
12
+ * as a wall-clock time in the zone, so `2026-03-08T02:00Z` (Mar 7 18:00 PST)
13
+ * comes back as −7 — 02:00 is the spring-forward wall time. Measured; the
14
+ * spring-forward test pins it.
15
+ */
16
+ /** UTC offset (ms) in force at the instant `epochMs` in `timeZone`. */
17
+ export declare function offsetAtInstantMs(timeZone: string, epochMs: number): number;
18
+ /** UTC offset (hours) in force at 18:00 local on the day before `nightIso`. */
19
+ export declare function nightOffsetHours(nightIso: string, timeZone: string): number;
20
+ /** The night's epoch window plus the offset it was computed with. */
21
+ export declare function nightWindow(nightIso: string, timeZone: string): {
22
+ windowStart: number;
23
+ windowEnd: number;
24
+ tzOffsetHours: number;
25
+ };
26
+ //# sourceMappingURL=night.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"night.d.ts","sourceRoot":"","sources":["../../../src/sync/sleep-correction/night.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,uEAAuE;AACvE,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAa3E;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAO3E;AAED,qEAAqE;AACrE,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAGnE"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Per-night offsets from the user's IANA zone (principle 24).
3
+ *
4
+ * v3.1 passed one fixed device offset (`getTzOffsetHours()`) for every night,
5
+ * so on the nights either side of a DST change the 18:00→18:00 window sat an
6
+ * hour off. The port asks the zone what offset is in force at the START of
7
+ * each night's window — 18:00 local on the previous day — and hands that to
8
+ * the v3.1 arithmetic unchanged.
9
+ *
10
+ * Offsets are read for INSTANTS via `formatInTimeZone(…, 'xxx')`. Not
11
+ * date-fns-tz's `getTimezoneOffset(zone, date)`: that reads the Date's fields
12
+ * as a wall-clock time in the zone, so `2026-03-08T02:00Z` (Mar 7 18:00 PST)
13
+ * comes back as −7 — 02:00 is the spring-forward wall time. Measured; the
14
+ * spring-forward test pins it.
15
+ */
16
+ import { formatInTimeZone } from 'date-fns-tz';
17
+ import { computeNightEpochs } from './classify';
18
+ const OFFSET_RE = /^([+-])(\d{2}):(\d{2})$/;
19
+ /** UTC offset (ms) in force at the instant `epochMs` in `timeZone`. */
20
+ export function offsetAtInstantMs(timeZone, epochMs) {
21
+ let text;
22
+ try {
23
+ text = formatInTimeZone(new Date(epochMs), timeZone, 'xxx');
24
+ }
25
+ catch {
26
+ throw new Error(`sleep-correction: unknown time zone ${JSON.stringify(timeZone)}`);
27
+ }
28
+ const m = OFFSET_RE.exec(text);
29
+ if (!m)
30
+ throw new Error(`sleep-correction: unreadable offset ${JSON.stringify(text)} for ${timeZone}`);
31
+ const ms = (Number(m[2]) * 60 + Number(m[3])) * 60000;
32
+ return m[1] === '-' ? -ms : ms;
33
+ }
34
+ /** UTC offset (hours) in force at 18:00 local on the day before `nightIso`. */
35
+ export function nightOffsetHours(nightIso, timeZone) {
36
+ const [y, m, d] = nightIso.split('-').map(Number);
37
+ // Local 18:00 as if it were UTC; one correction by the offset near that
38
+ // instant lands on the true local 18:00, and the offset there is the answer.
39
+ const wall = Date.UTC(y, m - 1, d - 1, 18);
40
+ const guess = offsetAtInstantMs(timeZone, wall);
41
+ return offsetAtInstantMs(timeZone, wall - guess) / 3600000;
42
+ }
43
+ /** The night's epoch window plus the offset it was computed with. */
44
+ export function nightWindow(nightIso, timeZone) {
45
+ const tzOffsetHours = nightOffsetHours(nightIso, timeZone);
46
+ return { ...computeNightEpochs(nightIso, tzOffsetHours), tzOffsetHours };
47
+ }
48
+ //# sourceMappingURL=night.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"night.js","sourceRoot":"","sources":["../../../src/sync/sleep-correction/night.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAE9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AAE/C,MAAM,SAAS,GAAG,yBAAyB,CAAA;AAE3C,uEAAuE;AACvE,MAAM,UAAU,iBAAiB,CAAC,QAAgB,EAAE,OAAe;IACjE,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,gBAAgB,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAA;IAC7D,CAAC;IACD,MAAM,CAAC;QACL,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAA;IACpF,CAAC;IACD,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,CAAC;QACJ,MAAM,IAAI,KAAK,CAAC,uCAAuC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,QAAQ,EAAE,CAAC,CAAA;IAChG,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAM,CAAA;IACtD,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;AAChC,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,QAAgB,EAAE,QAAgB;IACjE,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACjD,wEAAwE;IACxE,6EAA6E;IAC7E,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;IAC1C,MAAM,KAAK,GAAG,iBAAiB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IAC/C,OAAO,iBAAiB,CAAC,QAAQ,EAAE,IAAI,GAAG,KAAK,CAAC,GAAG,OAAS,CAAA;AAC9D,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,WAAW,CACzB,QAAgB,EAChB,QAAgB;IAEhB,MAAM,aAAa,GAAG,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAC1D,OAAO,EAAE,GAAG,kBAAkB,CAAC,QAAQ,EAAE,aAAa,CAAC,EAAE,aAAa,EAAE,CAAA;AAC1E,CAAC"}
@@ -0,0 +1,50 @@
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 type { ClassifiedRow } from './classify';
15
+ import type { SqlRunner } from './correct';
16
+ import type { SleepRelations } from './sql';
17
+ export interface CorrectSleepInput {
18
+ /** 'YYYY-MM-DD' nights (6pm → 6pm), e.g. from `nightsForEpochs`. */
19
+ nights: string[];
20
+ /** The user's IANA zone. */
21
+ timeZone: string;
22
+ /** Baseline lookback start (epoch seconds) — production: now − 30 d. */
23
+ cutoffEpoch: number;
24
+ relations?: SleepRelations;
25
+ }
26
+ export interface NightOutcome {
27
+ night: string;
28
+ windowStart: number;
29
+ windowEnd: number;
30
+ tzOffsetHours: number;
31
+ status: 'ok' | 'no_firmware_sleep' | 'no_phase2_rows' | 'failed';
32
+ rows?: ClassifiedRow[];
33
+ settleMin?: number | null;
34
+ recoveredMin?: number;
35
+ error?: string;
36
+ }
37
+ export interface CorrectSleepResult {
38
+ hrIntervalMin: number;
39
+ p75: number;
40
+ p90: number;
41
+ nights: NightOutcome[];
42
+ }
43
+ export declare function correctSleepNights(db: SqlRunner, input: CorrectSleepInput): Promise<CorrectSleepResult>;
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 declare function nightsForEpochs(epochs: Iterable<number>, timeZone: string): string[];
50
+ //# sourceMappingURL=orchestrate.d.ts.map
@@ -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"}