@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,86 @@
1
+ /**
2
+ * Sleep correction — Phase 3 and the night window (sleep-correction port).
3
+ *
4
+ * VERBATIM port of ziva_app `packages/ux/helpers/sleepCorrectionLayer.ts` v3.1
5
+ * (`fix/sleepProcessed`, unchanged db0c58a43 → a611cecca): `computeNightEpochs`,
6
+ * `parseDateStr`, `classifyBlocks` and every constant. Only the formatting
7
+ * follows this repo's lint; the logic, order of operations and constants are
8
+ * the validated ones (END within-30 74% → 88% on the QA study set) and must
9
+ * not be retuned during the port. The parity harness is the check.
10
+ *
11
+ * Timestamps are epoch seconds. Row `date`/`start` strings are the Phase 2
12
+ * output shape, "YYYY.MM.DD HH:MM:SS" in UTC.
13
+ */
14
+ export declare const HR_MIN = 40;
15
+ export declare const HR_MAX = 120;
16
+ export declare const EXTEND_AFTER = 7200;
17
+ export declare const LONG_GAP_MIN = 600;
18
+ export declare const LONG_GAP_MAX = 28800;
19
+ export declare const LONG_GAP_HR_FRAC = 0.4;
20
+ export declare const STITCH_GAP_SHORT = 120;
21
+ export declare const STITCH_GAP_MED = 300;
22
+ export declare const STITCH_GAP_LONG = 1200;
23
+ export declare const BLOCK_SEP = 1800;
24
+ export declare const BOUT_MAX_LEN = 1;
25
+ export declare const MORNING_TRIM_FLOOR_MIN: number;
26
+ export declare const MORNING_TRIM_CEIL_MIN: number;
27
+ export declare const FETCH_BEFORE = 21600;
28
+ export declare const FETCH_AFTER = 1800;
29
+ export declare const CONF_FIRMWARE = 0.9;
30
+ export declare const CONF_ENVELOPE = 0.5;
31
+ export declare const CONF_GAP = 0.4;
32
+ export declare const CONF_BOUT = 0.7;
33
+ export declare const END_BASE_PCTILE = 0.2;
34
+ export declare const END_WAKE_MARGIN_BPM = 12;
35
+ export declare const END_WAKE_MARGIN_FRAC = 0.18;
36
+ export declare const END_QUIET_STEPS = 80;
37
+ export declare const END_WAKE_STEPS = 150;
38
+ export declare const END_MIN_TRIM = 1800;
39
+ export declare const END_MIN_SLEEP = 7200;
40
+ export declare const END_WAKE_SUSTAIN = 1200;
41
+ export declare const END_GAP_TOL = 900;
42
+ /**
43
+ * 'YYYY-MM-DD' night → epoch window [18:00 previous local day, 18:00 local).
44
+ *
45
+ * `tzOffsetHours` is the offset in force for THIS night. v3.1 fed it one fixed
46
+ * device offset; the port's caller derives it per night from the user's IANA
47
+ * zone (spec: per-night IANA offset), so DST nights land correctly.
48
+ */
49
+ export declare function computeNightEpochs(nightDate: string, tzOffsetHours?: number): {
50
+ windowStart: number;
51
+ windowEnd: number;
52
+ };
53
+ /** "YYYY.MM.DD HH:mm:ss" (UTC) → epoch seconds. v3.1 verbatim. */
54
+ export declare function parseDateStr(d: string): number;
55
+ /** A Phase 2 output row. */
56
+ export interface Phase2Row {
57
+ date: string;
58
+ quality: number;
59
+ start: string;
60
+ unitLength: number;
61
+ source: string;
62
+ confidence: number;
63
+ session_id: number;
64
+ }
65
+ /** A Phase 3 output row — v3.1's `sleep_processed` row shape. */
66
+ export interface ClassifiedRow {
67
+ date: string;
68
+ quality: number;
69
+ start: string;
70
+ unitLength: number;
71
+ source: string;
72
+ confidence: number;
73
+ block_type: string;
74
+ }
75
+ export interface VitalSample {
76
+ kind: string;
77
+ epoch: number;
78
+ value: number;
79
+ }
80
+ /**
81
+ * Bout consolidation + block classification (primary / secondary /
82
+ * microsleep), the synthetic head/tail trims and the Step 5.6 HR + movement
83
+ * END refinement. v3.1 verbatim — see the file header.
84
+ */
85
+ export declare function classifyBlocks(rows: Phase2Row[], tzOffsetHours?: number, vitals?: VitalSample[]): ClassifiedRow[];
86
+ //# sourceMappingURL=classify.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.ts","sourceRoot":"","sources":["../../../src/sync/sleep-correction/classify.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,MAAM,KAAK,CAAA;AACxB,eAAO,MAAM,MAAM,MAAM,CAAA;AACzB,eAAO,MAAM,YAAY,OAAO,CAAA;AAChC,eAAO,MAAM,YAAY,MAAM,CAAA;AAC/B,eAAO,MAAM,YAAY,QAAQ,CAAA;AACjC,eAAO,MAAM,gBAAgB,MAAO,CAAA;AACpC,eAAO,MAAM,gBAAgB,MAAM,CAAA;AACnC,eAAO,MAAM,cAAc,MAAM,CAAA;AACjC,eAAO,MAAM,eAAe,OAAO,CAAA;AACnC,eAAO,MAAM,SAAS,OAAO,CAAA;AAC7B,eAAO,MAAM,YAAY,IAAI,CAAA;AAG7B,eAAO,MAAM,sBAAsB,QAAS,CAAA;AAC5C,eAAO,MAAM,qBAAqB,QAAU,CAAA;AAC5C,eAAO,MAAM,YAAY,QAAQ,CAAA;AACjC,eAAO,MAAM,WAAW,OAAO,CAAA;AAC/B,eAAO,MAAM,aAAa,MAAO,CAAA;AACjC,eAAO,MAAM,aAAa,MAAO,CAAA;AACjC,eAAO,MAAM,QAAQ,MAAO,CAAA;AAC5B,eAAO,MAAM,SAAS,MAAO,CAAA;AAG7B,eAAO,MAAM,eAAe,MAAO,CAAA;AACnC,eAAO,MAAM,mBAAmB,KAAK,CAAA;AACrC,eAAO,MAAM,oBAAoB,OAAO,CAAA;AACxC,eAAO,MAAM,eAAe,KAAK,CAAA;AACjC,eAAO,MAAM,cAAc,MAAM,CAAA;AACjC,eAAO,MAAM,YAAY,OAAO,CAAA;AAChC,eAAO,MAAM,aAAa,OAAO,CAAA;AACjC,eAAO,MAAM,gBAAgB,OAAO,CAAA;AACpC,eAAO,MAAM,WAAW,MAAM,CAAA;AAE9B;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,aAAa,GAAE,MAAU,GACxB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAS5C;AAID,kEAAkE;AAClE,wBAAgB,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAM9C;AAED,4BAA4B;AAC5B,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;IAClB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,EAAE,EACjB,aAAa,GAAE,MAAU,EACzB,MAAM,CAAC,EAAE,WAAW,EAAE,GACrB,aAAa,EAAE,CA8LjB"}
@@ -0,0 +1,247 @@
1
+ /**
2
+ * Sleep correction — Phase 3 and the night window (sleep-correction port).
3
+ *
4
+ * VERBATIM port of ziva_app `packages/ux/helpers/sleepCorrectionLayer.ts` v3.1
5
+ * (`fix/sleepProcessed`, unchanged db0c58a43 → a611cecca): `computeNightEpochs`,
6
+ * `parseDateStr`, `classifyBlocks` and every constant. Only the formatting
7
+ * follows this repo's lint; the logic, order of operations and constants are
8
+ * the validated ones (END within-30 74% → 88% on the QA study set) and must
9
+ * not be retuned during the port. The parity harness is the check.
10
+ *
11
+ * Timestamps are epoch seconds. Row `date`/`start` strings are the Phase 2
12
+ * output shape, "YYYY.MM.DD HH:MM:SS" in UTC.
13
+ */
14
+ export const HR_MIN = 40;
15
+ export const HR_MAX = 120;
16
+ export const EXTEND_AFTER = 7200; // max seconds to extend past firmware end
17
+ export const LONG_GAP_MIN = 600;
18
+ export const LONG_GAP_MAX = 28800;
19
+ export const LONG_GAP_HR_FRAC = 0.40;
20
+ export const STITCH_GAP_SHORT = 120;
21
+ export const STITCH_GAP_MED = 300;
22
+ export const STITCH_GAP_LONG = 1200;
23
+ export const BLOCK_SEP = 1800;
24
+ export const BOUT_MAX_LEN = 1;
25
+ // Morning over-extension guard (Step 5.5): only trim when the primary END lands
26
+ // in this late-morning band.
27
+ export const MORNING_TRIM_FLOOR_MIN = 7 * 60; // 07:00 local
28
+ export const MORNING_TRIM_CEIL_MIN = 14 * 60; // 14:00 local
29
+ export const FETCH_BEFORE = 21600; // 6h before window, so envelope HR context spans pre-sleep
30
+ export const FETCH_AFTER = 1800;
31
+ export const CONF_FIRMWARE = 0.90;
32
+ export const CONF_ENVELOPE = 0.50;
33
+ export const CONF_GAP = 0.40;
34
+ export const CONF_BOUT = 0.70;
35
+ // HR + movement END refinement (Step 5.6).
36
+ export const END_BASE_PCTILE = 0.20; // personal sleep-HR baseline = 20th pct of in-block HR
37
+ export const END_WAKE_MARGIN_BPM = 12; // wake threshold = baseline + max(this, frac*baseline)
38
+ export const END_WAKE_MARGIN_FRAC = 0.18;
39
+ export const END_QUIET_STEPS = 80; // steps within ±10min for a sample to still count "asleep"
40
+ export const END_WAKE_STEPS = 150; // steps after the cut required to confirm wake (movement)
41
+ export const END_MIN_TRIM = 1800; // 30min — only correct clear over-extensions
42
+ export const END_MIN_SLEEP = 7200; // 2h — never collapse the primary block below this
43
+ export const END_WAKE_SUSTAIN = 1200; // 20min — the awake run after the cut must persist
44
+ export const END_GAP_TOL = 900; // 15min — tolerance for step/HR sample proximity
45
+ /**
46
+ * 'YYYY-MM-DD' night → epoch window [18:00 previous local day, 18:00 local).
47
+ *
48
+ * `tzOffsetHours` is the offset in force for THIS night. v3.1 fed it one fixed
49
+ * device offset; the port's caller derives it per night from the user's IANA
50
+ * zone (spec: per-night IANA offset), so DST nights land correctly.
51
+ */
52
+ export function computeNightEpochs(nightDate, tzOffsetHours = 0) {
53
+ const [year, month, day] = nightDate.split('-').map(Number);
54
+ const nightMidnightUtc = Date.UTC(year, month - 1, day) / 1000;
55
+ const priorMidnightUtc = nightMidnightUtc - 86400;
56
+ const tzOffsetSec = Math.round(tzOffsetHours * 3600);
57
+ return {
58
+ windowStart: priorMidnightUtc + 18 * 3600 - tzOffsetSec,
59
+ windowEnd: nightMidnightUtc + 18 * 3600 - tzOffsetSec,
60
+ };
61
+ }
62
+ const DOT_RE = /\./g;
63
+ /** "YYYY.MM.DD HH:mm:ss" (UTC) → epoch seconds. v3.1 verbatim. */
64
+ export function parseDateStr(d) {
65
+ if (!d)
66
+ return 0;
67
+ // "2026.04.18 16:52:01" → "2026-04-18T16:52:01Z"
68
+ const iso = `${d.replace(DOT_RE, (_m, offset) => offset < 10 ? '-' : '.').replace(' ', 'T')}Z`;
69
+ return Math.floor(new Date(iso).getTime() / 1000);
70
+ }
71
+ /**
72
+ * Bout consolidation + block classification (primary / secondary /
73
+ * microsleep), the synthetic head/tail trims and the Step 5.6 HR + movement
74
+ * END refinement. v3.1 verbatim — see the file header.
75
+ */
76
+ export function classifyBlocks(rows, tzOffsetHours = 0, vitals) {
77
+ if (!rows || rows.length === 0)
78
+ return [];
79
+ const tzOffsetSec = Math.round(tzOffsetHours * 3600);
80
+ const localHour = (epoch) => new Date((epoch + tzOffsetSec) * 1000).getUTCHours();
81
+ // Step 1: Assign bout IDs (consecutive same-quality runs)
82
+ const withBouts = rows.map((row, i) => {
83
+ const prev = rows[i - 1];
84
+ const isBoutStart = !prev
85
+ || row.quality !== prev.quality
86
+ || parseDateStr(row.date) - parseDateStr(prev.date) > 65
87
+ || row.quality === 5
88
+ || (prev && prev.quality === 5);
89
+ return { ...row, is_bout_start: isBoutStart ? 1 : 0 };
90
+ });
91
+ let boutId = 0;
92
+ const withBoutIds = withBouts.map((row) => {
93
+ if (row.is_bout_start)
94
+ boutId++;
95
+ return { ...row, bout_id: boutId };
96
+ });
97
+ // Step 2: Consolidate short deep bouts → light (never REM)
98
+ const boutLengths = {};
99
+ const boutQualities = {};
100
+ for (const row of withBoutIds) {
101
+ boutLengths[row.bout_id] = (boutLengths[row.bout_id] || 0) + 1;
102
+ boutQualities[row.bout_id] = row.quality;
103
+ }
104
+ const consolidated = withBoutIds.map((row) => {
105
+ const len = boutLengths[row.bout_id];
106
+ const qual = boutQualities[row.bout_id];
107
+ const prevBoutQual = boutQualities[row.bout_id - 1];
108
+ const nextBoutQual = boutQualities[row.bout_id + 1];
109
+ // Only consolidate deep sleep (1) micro-fragments — never REM (3).
110
+ const isMergeable = qual === 1
111
+ && len <= BOUT_MAX_LEN
112
+ && (prevBoutQual === 2 || nextBoutQual === 2);
113
+ return {
114
+ ...row,
115
+ quality: isMergeable ? 2 : row.quality,
116
+ source: isMergeable ? 'bout_consolidated' : row.source,
117
+ confidence: isMergeable ? CONF_BOUT : row.confidence,
118
+ };
119
+ });
120
+ // Step 3: Assign block IDs (gaps > BLOCK_SEP = new block)
121
+ let blockId = 0;
122
+ const withBlocks = consolidated.map((row, i) => {
123
+ const prev = consolidated[i - 1];
124
+ if (!prev || parseDateStr(row.date) - parseDateStr(prev.date) > BLOCK_SEP)
125
+ blockId++;
126
+ return { ...row, block_id: blockId };
127
+ });
128
+ // Step 4: Find primary block (most sleep minutes during NIGHT hours)
129
+ const blockSleepMins = {};
130
+ const blockNightMins = {};
131
+ const blockFwMins = {};
132
+ const sessionSizes = {};
133
+ const sessionFwMins = {};
134
+ for (const row of withBlocks) {
135
+ if ([1, 2, 3].includes(row.quality)) {
136
+ blockSleepMins[row.block_id] = (blockSleepMins[row.block_id] || 0) + 1;
137
+ // Core-night window (20:00–10:00 local) only, so a spurious daytime
138
+ // firmware block can't be picked as the primary sleep.
139
+ const h = localHour(parseDateStr(row.date));
140
+ if (h >= 20 || h < 10)
141
+ blockNightMins[row.block_id] = (blockNightMins[row.block_id] || 0) + 1;
142
+ }
143
+ if (row.source === 'firmware') {
144
+ blockFwMins[row.block_id] = (blockFwMins[row.block_id] || 0) + 1;
145
+ sessionFwMins[row.session_id] = (sessionFwMins[row.session_id] || 0) + 1;
146
+ }
147
+ sessionSizes[row.session_id] = (sessionSizes[row.session_id] || 0) + 1;
148
+ }
149
+ // Prefer the block with the most night-hour sleep; fall back to total sleep
150
+ // minutes when no block has any (e.g. shift workers).
151
+ const primaryScore = Object.keys(blockNightMins).length ? blockNightMins : blockSleepMins;
152
+ const primaryBlockId = Object.entries(primaryScore)
153
+ .sort(([idA, a], [idB, b]) => b - a || Number(idA) - Number(idB))[0]?.[0];
154
+ const isFwBacked = (s) => s === 'firmware' || s === 'bout_consolidated';
155
+ const primaryIndices = () => {
156
+ const primIdx = [];
157
+ withBlocks.forEach((row, i) => {
158
+ if (String(row.block_id) === primaryBlockId)
159
+ primIdx.push(i);
160
+ });
161
+ return primIdx;
162
+ };
163
+ // Step 5.5: Trailing-synthetic-tail trim (morning over-extension guard).
164
+ const trimmedIdx = new Set();
165
+ if (primaryBlockId != null && (blockFwMins[Number(primaryBlockId)] || 0) > 0) {
166
+ const primIdx = primaryIndices();
167
+ const lastIdx = primIdx[primIdx.length - 1];
168
+ const endLocalMin = ((((parseDateStr(withBlocks[lastIdx].date) + tzOffsetSec) % 86400) + 86400) % 86400) / 60;
169
+ if (endLocalMin >= MORNING_TRIM_FLOOR_MIN && endLocalMin < MORNING_TRIM_CEIL_MIN) {
170
+ for (let p = primIdx.length - 1; p >= 0; p--) {
171
+ if (isFwBacked(withBlocks[primIdx[p]].source))
172
+ break;
173
+ trimmedIdx.add(primIdx[p]);
174
+ }
175
+ }
176
+ }
177
+ // Step 5.5b: Leading-synthetic-head trim (evening onset guard).
178
+ if (primaryBlockId != null && (blockFwMins[Number(primaryBlockId)] || 0) > 0) {
179
+ const primIdx = primaryIndices();
180
+ for (let p = 0; p < primIdx.length; p++) {
181
+ if (isFwBacked(withBlocks[primIdx[p]].source))
182
+ break;
183
+ trimmedIdx.add(primIdx[p]);
184
+ }
185
+ }
186
+ // Step 5.6: HR + movement END refinement.
187
+ if (vitals && vitals.length && primaryBlockId != null) {
188
+ const hr = vitals
189
+ .filter(v => v.kind === 'hr' && Number.isFinite(v.value) && v.value > 0)
190
+ .map(v => ({ e: Number(v.epoch), v: Number(v.value) }))
191
+ .sort((a, b) => a.e - b.e);
192
+ const steps = vitals
193
+ .filter(v => v.kind === 'step' && Number.isFinite(v.value))
194
+ .map(v => ({ e: Number(v.epoch), v: Number(v.value) }))
195
+ .sort((a, b) => a.e - b.e);
196
+ const stepsIn = (a, b) => {
197
+ let s = 0;
198
+ for (const x of steps) {
199
+ if (x.e > a && x.e <= b)
200
+ s += x.v;
201
+ }
202
+ return s;
203
+ };
204
+ const primIdx = primaryIndices();
205
+ const procStart = parseDateStr(withBlocks[primIdx[0]].date);
206
+ const procEnd = parseDateStr(withBlocks[primIdx[primIdx.length - 1]].date);
207
+ const inBlock = hr.filter(h => h.e >= procStart && h.e <= procEnd).map(h => h.v).sort((a, b) => a - b);
208
+ if (inBlock.length >= 4) {
209
+ const base = inBlock[Math.min(inBlock.length - 1, Math.floor(END_BASE_PCTILE * inBlock.length))];
210
+ const thr = base + Math.max(END_WAKE_MARGIN_BPM, END_WAKE_MARGIN_FRAC * base);
211
+ // "Asleep" only if HR is at sleep level AND steps are quiet around it.
212
+ const asleepAt = (h) => h.v <= thr && stepsIn(h.e - END_GAP_TOL + 300, h.e + END_GAP_TOL - 300) <= END_QUIET_STEPS;
213
+ const tail = hr.filter(h => h.e >= procStart && h.e <= procEnd + END_GAP_TOL);
214
+ let wakeAt = procEnd;
215
+ for (let i = tail.length - 1; i >= 0; i--) {
216
+ if (asleepAt(tail[i])) {
217
+ wakeAt = tail[i].e;
218
+ break;
219
+ }
220
+ }
221
+ const runLen = procEnd - wakeAt;
222
+ const stepsAfter = stepsIn(wakeAt, procEnd + END_GAP_TOL);
223
+ if (wakeAt < procEnd
224
+ && runLen >= END_WAKE_SUSTAIN
225
+ && stepsAfter >= END_WAKE_STEPS
226
+ && runLen >= END_MIN_TRIM
227
+ && (wakeAt - procStart) >= END_MIN_SLEEP) {
228
+ for (const i of primIdx) {
229
+ if (parseDateStr(withBlocks[i].date) > wakeAt)
230
+ trimmedIdx.add(i);
231
+ }
232
+ }
233
+ }
234
+ }
235
+ // Step 5: Assign block_type
236
+ return withBlocks.map(({ is_bout_start: _b, bout_id: _bo, block_id, session_id, ...row }, i) => ({
237
+ ...row,
238
+ block_type: sessionSizes[session_id] < 30 && !sessionFwMins[session_id]
239
+ ? 'microsleep'
240
+ : (blockFwMins[block_id] || 0) === 0 && (blockSleepMins[block_id] || 0) < 30
241
+ ? 'microsleep'
242
+ : String(block_id) === primaryBlockId
243
+ ? (trimmedIdx.has(i) ? 'secondary' : 'primary')
244
+ : 'secondary',
245
+ }));
246
+ }
247
+ //# sourceMappingURL=classify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.js","sourceRoot":"","sources":["../../../src/sync/sleep-correction/classify.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Sleep correction — orchestration (v3.1's AppContextProvider loop, per night).
3
+ *
4
+ * Engine-agnostic: anything with `execute(sql)` returning rows. The relation
5
+ * names select where the raw firmware shapes come from — staging views over
6
+ * the warehouse in production, capture tables in the parity harness.
7
+ *
8
+ * NOT wired into sync yet (sleep-correction tasks §3): nothing calls this on
9
+ * a device, so the mapper path is unchanged until that lands.
10
+ */
11
+ import type { ClassifiedRow, VitalSample } from './classify';
12
+ import type { SleepRelations } from './sql';
13
+ export interface SqlRunner {
14
+ execute: (sql: string) => Promise<unknown[]>;
15
+ }
16
+ /** v3.1 fallbacks when a scalar cannot be computed (AppContextProvider). */
17
+ export declare const TIER_FALLBACK = 5;
18
+ export declare const P90_FALLBACK = 120;
19
+ export declare const P75_FALLBACK = 80;
20
+ export declare function detectHrTier(db: SqlRunner, r?: SleepRelations): Promise<number>;
21
+ export declare function globalBaselines(db: SqlRunner, cutoffEpoch: number, r?: SleepRelations): Promise<{
22
+ p75: number;
23
+ p90: number;
24
+ }>;
25
+ export interface CorrectNightInput {
26
+ windowStart: number;
27
+ windowEnd: number;
28
+ hrIntervalMin: number;
29
+ p75: number;
30
+ p90: number;
31
+ /** Offset in force for this night (the caller derives it from the IANA zone). */
32
+ tzOffsetHours: number;
33
+ relations?: SleepRelations;
34
+ }
35
+ export type CorrectNightResult = {
36
+ status: 'no_firmware_sleep';
37
+ } | {
38
+ status: 'no_phase2_rows';
39
+ } | {
40
+ status: 'ok';
41
+ rows: ClassifiedRow[];
42
+ vitals: VitalSample[];
43
+ };
44
+ /** One night: guard → Phase 2 (SQL) → morning vitals → Phase 3 (JS). */
45
+ export declare function correctNight(db: SqlRunner, input: CorrectNightInput): Promise<CorrectNightResult>;
46
+ //# sourceMappingURL=correct.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correct.d.ts","sourceRoot":"","sources":["../../../src/sync/sleep-correction/correct.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAa,WAAW,EAAE,MAAM,YAAY,CAAA;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,OAAO,CAAA;AAW3C,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;CAC7C;AAcD,4EAA4E;AAC5E,eAAO,MAAM,aAAa,IAAI,CAAA;AAC9B,eAAO,MAAM,YAAY,MAAM,CAAA;AAC/B,eAAO,MAAM,YAAY,KAAK,CAAA;AAE9B,wBAAsB,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,CAAC,GAAE,cAAkC,GAAG,OAAO,CAAC,MAAM,CAAC,CAGxG;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,SAAS,EACb,WAAW,EAAE,MAAM,EACnB,CAAC,GAAE,cAAkC,GACpC,OAAO,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAMvC;AAED,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,GAAG,EAAE,MAAM,CAAA;IACX,GAAG,EAAE,MAAM,CAAA;IACX,iFAAiF;IACjF,aAAa,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,cAAc,CAAA;CAC3B;AAED,MAAM,MAAM,kBAAkB,GACxB;IAAE,MAAM,EAAE,mBAAmB,CAAA;CAAE,GAC/B;IAAE,MAAM,EAAE,gBAAgB,CAAA;CAAE,GAC5B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,aAAa,EAAE,CAAC;IAAC,MAAM,EAAE,WAAW,EAAE,CAAA;CAAE,CAAA;AAEpE,wEAAwE;AACxE,wBAAsB,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA4BvG"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Sleep correction — orchestration (v3.1's AppContextProvider loop, per night).
3
+ *
4
+ * Engine-agnostic: anything with `execute(sql)` returning rows. The relation
5
+ * names select where the raw firmware shapes come from — staging views over
6
+ * the warehouse in production, capture tables in the parity harness.
7
+ *
8
+ * NOT wired into sync yet (sleep-correction tasks §3): nothing calls this on
9
+ * a device, so the mapper path is unchanged until that lands.
10
+ */
11
+ import { classifyBlocks } from './classify';
12
+ import { correctNightSql, DEFAULT_RELATIONS, detectHrTierSql, globalBaselinesSql, hasSleepDataSql, morningVitalsSql, } from './sql';
13
+ /** DuckDB hands BIGINT/HUGEINT back as bigint; every consumer here wants number. */
14
+ function toNum(v) {
15
+ if (v == null)
16
+ return null;
17
+ if (typeof v === 'bigint')
18
+ return Number(v);
19
+ const n = Number(v);
20
+ return Number.isFinite(n) ? n : null;
21
+ }
22
+ /** v3.1 fallbacks when a scalar cannot be computed (AppContextProvider). */
23
+ export const TIER_FALLBACK = 5;
24
+ export const P90_FALLBACK = 120;
25
+ export const P75_FALLBACK = 80;
26
+ export async function detectHrTier(db, r = DEFAULT_RELATIONS) {
27
+ const [row] = (await db.execute(detectHrTierSql(r)));
28
+ return toNum(row?.hr_interval_minutes) || TIER_FALLBACK;
29
+ }
30
+ export async function globalBaselines(db, cutoffEpoch, r = DEFAULT_RELATIONS) {
31
+ const [row] = (await db.execute(globalBaselinesSql(cutoffEpoch, r)));
32
+ return {
33
+ p90: toNum(row?.p90_global) ?? P90_FALLBACK,
34
+ p75: toNum(row?.p75_global) ?? P75_FALLBACK,
35
+ };
36
+ }
37
+ /** One night: guard → Phase 2 (SQL) → morning vitals → Phase 3 (JS). */
38
+ export async function correctNight(db, input) {
39
+ const r = input.relations ?? DEFAULT_RELATIONS;
40
+ const { windowStart, windowEnd } = input;
41
+ const [guard] = (await db.execute(hasSleepDataSql(windowStart, windowEnd, r)));
42
+ if (!toNum(guard?.has_data))
43
+ return { status: 'no_firmware_sleep' };
44
+ const phase2 = (await db.execute(correctNightSql(windowStart, windowEnd, input.hrIntervalMin, input.p75, input.p90, r)));
45
+ if (!phase2.length)
46
+ return { status: 'no_phase2_rows' };
47
+ const rows = phase2.map(p => ({
48
+ date: String(p.date),
49
+ quality: toNum(p.quality) ?? 0,
50
+ start: String(p.start),
51
+ unitLength: 1,
52
+ source: String(p.source),
53
+ confidence: toNum(p.confidence) ?? 0,
54
+ session_id: toNum(p.session_id) ?? 0,
55
+ }));
56
+ const vitals = (await db.execute(morningVitalsSql(windowStart, windowEnd, r)))
57
+ .map(v => ({ kind: String(v.kind), epoch: toNum(v.epoch) ?? 0, value: toNum(v.value) ?? Number.NaN }));
58
+ return { status: 'ok', rows: classifyBlocks(rows, input.tzOffsetHours, vitals), vitals };
59
+ }
60
+ //# sourceMappingURL=correct.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"correct.js","sourceRoot":"","sources":["../../../src/sync/sleep-correction/correct.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export { type ClassifiedRow, classifyBlocks, computeNightEpochs, parseDateStr, type Phase2Row, type VitalSample, } from './classify';
2
+ export { correctNight, type CorrectNightInput, type CorrectNightResult, detectHrTier, globalBaselines, type SqlRunner, } from './correct';
3
+ export { nightOffsetHours, nightWindow } from './night';
4
+ export { type CorrectSleepInput, correctSleepNights, type CorrectSleepResult, type NightOutcome, nightsForEpochs, } from './orchestrate';
5
+ export { computeSettleMin, recoveredMinutes } from './settle';
6
+ export { correctNightSql, DEFAULT_RELATIONS, detectHrTierSql, globalBaselinesSql, hasSleepDataSql, morningVitalsSql, type SleepRelations, } from './sql';
7
+ export { type StagingScope, stagingViewsSql } from './staging';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/sync/sleep-correction/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,KAAK,SAAS,EACd,KAAK,WAAW,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EACZ,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,YAAY,EACZ,eAAe,EACf,KAAK,SAAS,GACf,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EACL,KAAK,iBAAiB,EACtB,kBAAkB,EAClB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,eAAe,GAChB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,KAAK,cAAc,GACpB,MAAM,OAAO,CAAA;AACd,OAAO,EAAE,KAAK,YAAY,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1,8 @@
1
+ export { classifyBlocks, computeNightEpochs, parseDateStr, } from './classify';
2
+ export { correctNight, detectHrTier, globalBaselines, } from './correct';
3
+ export { nightOffsetHours, nightWindow } from './night';
4
+ export { correctSleepNights, nightsForEpochs, } from './orchestrate';
5
+ export { computeSettleMin, recoveredMinutes } from './settle';
6
+ export { correctNightSql, DEFAULT_RELATIONS, detectHrTierSql, globalBaselinesSql, hasSleepDataSql, morningVitalsSql, } from './sql';
7
+ export { stagingViewsSql } from './staging';
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/sync/sleep-correction/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EACd,kBAAkB,EAClB,YAAY,GAGb,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,YAAY,EAGZ,YAAY,EACZ,eAAe,GAEhB,MAAM,WAAW,CAAA;AAClB,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACvD,OAAO,EAEL,kBAAkB,EAGlB,eAAe,GAChB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAC7D,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GAEjB,MAAM,OAAO,CAAA;AACd,OAAO,EAAqB,eAAe,EAAE,MAAM,WAAW,CAAA"}
@@ -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