@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.
- 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/rules/compiler.d.ts.map +1 -1
- package/dist/rules/compiler.js +37 -16
- package/dist/rules/compiler.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 -118
- 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 +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":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAEzC,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAA;AACpC,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA;AACrC,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAA;AAErC,MAAM,UAAU,gBAAgB,CAC9B,IAAqB,EACrB,MAAqB,EACrB,aAAqB;IAErB,IAAI,aAAa,GAAG,mBAAmB;QACrC,OAAO,IAAI,CAAA;IACb,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3F,IAAI,CAAC,OAAO,CAAC,MAAM;QACjB,OAAO,IAAI,CAAA;IACb,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAA;IAChC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,CAAA;IAEhC,MAAM,EAAE,GAAG,MAAM;SACd,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,GAAG,IAAI,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC;SAC3G,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;SACtC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC5B,IAAI,CAAC,EAAE,CAAC,MAAM;QACZ,OAAO,IAAI,CAAA;IAEb,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,gBAAgB,CAAA;IAC9D,MAAM,IAAI,GAAG,EAAE,CAAC,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAChC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK;YACjB,SAAQ;QACV,MAAM,KAAK,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,kBAAkB,CAAA;QAC1C,IAAI,IAAI,GAAG,KAAK;YACd,OAAO,IAAI,CAAA,CAAC,uDAAuD;QACrE,IAAI,IAAI,GAAG,IAAI,CAAA;QACf,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACvD,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,EAAE,CAAC;gBACpB,IAAI,GAAG,KAAK,CAAA;gBACZ,MAAK;YACP,CAAC;QACH,CAAC;QACD,IAAI,IAAI;YACN,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,EAAE,CAAC,CAAA;IAC3C,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,+EAA+E;AAC/E,MAAM,UAAU,gBAAgB,CAAC,IAAqB;IACpD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,UAAU,IAAI,CAAC,CAAC,MAAM,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAA;AAC/G,CAAC"}
|
|
@@ -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"}
|