@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
@@ -18,12 +18,11 @@ import { mapBattery } from './battery';
18
18
  import { mapHeartRate } from './heart-rate';
19
19
  import { mapHrv } from './hrv';
20
20
  import { mapRingConfig } from './ring-config';
21
- import { reconstructSleepSessions } from './sleep';
21
+ import { mapSleepRaw } from './sleep';
22
22
  import { mapSpo2 } from './spo2';
23
23
  import { mapTemperature } from './temperature';
24
24
  export function mapFirmwareExport(fw, ctx, opts = {}) {
25
25
  const activity = mapActivity(fw.activitydetails ?? [], ctx);
26
- const sleep = reconstructSleepSessions(fw.sleep_processed ?? [], ctx);
27
26
  const config = mapRingConfigIfPossible(fw, ctx, opts);
28
27
  return {
29
28
  hrv: mapHrv(fw.hrv_table ?? [], ctx),
@@ -32,9 +31,12 @@ export function mapFirmwareExport(fw, ctx, opts = {}) {
32
31
  temperature: mapTemperature(fw.temperature_table ?? [], ctx),
33
32
  activity: activity.activity,
34
33
  activity_bucket: activity.activity_bucket,
35
- sleep_session: sleep.sleep_session,
36
- sleep_stage: sleep.sleep_stage,
37
- sleep_raw: sleep.sleep_raw,
34
+ // Raw firmware minutes only. Sessions and stages are DERIVED — the sync
35
+ // factory runs the sleep-correction pipeline over `sleep_raw` after the
36
+ // batch flushes and writes them night by night (sleep-correction §3).
37
+ sleep_session: [],
38
+ sleep_stage: [],
39
+ sleep_raw: mapSleepRaw(fw.sleep ?? [], ctx),
38
40
  // Battery moved off the generic event stream in 0.6.0 (fix B2);
39
41
  // `device_event` stays in the batch shape for future event types.
40
42
  device_event: [],
@@ -1 +1 @@
1
- {"version":3,"file":"firmware.js","sourceRoot":"","sources":["../../../src/sync/mapper/firmware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAe9C,MAAM,UAAU,iBAAiB,CAC/B,EAAkB,EAClB,GAAkB,EAClB,OAA2B,EAAE;IAE7B,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAE,CAAC,eAAe,IAAI,EAAE,EAAE,GAAG,CAAC,CAAA;IAC3D,MAAM,KAAK,GAAG,wBAAwB,CAAC,EAAE,CAAC,eAAe,IAAI,EAAE,EAAE,GAAG,CAAC,CAAA;IACrE,MAAM,MAAM,GAAG,uBAAuB,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;IAErD,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,SAAS,IAAI,EAAE,EAAE,GAAG,CAAC;QACpC,UAAU,EAAE,YAAY,CAAC,EAAE,CAAC,SAAS,IAAI,EAAE,EAAE,GAAG,CAAC;QACjD,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,GAAG,CAAC;QACjC,WAAW,EAAE,cAAc,CAAC,EAAE,CAAC,iBAAiB,IAAI,EAAE,EAAE,GAAG,CAAC;QAC5D,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,eAAe,EAAE,QAAQ,CAAC,eAAe;QACzC,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,gEAAgE;QAChE,kEAAkE;QAClE,YAAY,EAAE,EAAE;QAChB,cAAc,EAAE,UAAU,CAAC,EAAE,CAAC,aAAa,IAAI,EAAE,EAAE,GAAG,CAAC;QACvD,aAAa,EAAE,MAAM,CAAC,OAAO;QAC7B,oBAAoB,EAAE,MAAM,CAAC,MAAM;KACpC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,uBAAuB,CAC9B,EAAkB,EAClB,GAAkB,EAClB,IAAwB;IAExB,sEAAsE;IACtE,+DAA+D;IAC/D,OAAO,aAAa,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,uBAAuB,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE;QACpE,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;KAC5C,CAAC,CAAA;AACJ,CAAC"}
1
+ {"version":3,"file":"firmware.js","sourceRoot":"","sources":["../../../src/sync/mapper/firmware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AASH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACtC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAe9C,MAAM,UAAU,iBAAiB,CAC/B,EAAkB,EAClB,GAAkB,EAClB,OAA2B,EAAE;IAE7B,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAE,CAAC,eAAe,IAAI,EAAE,EAAE,GAAG,CAAC,CAAA;IAC3D,MAAM,MAAM,GAAG,uBAAuB,CAAC,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;IAErD,OAAO;QACL,GAAG,EAAE,MAAM,CAAC,EAAE,CAAC,SAAS,IAAI,EAAE,EAAE,GAAG,CAAC;QACpC,UAAU,EAAE,YAAY,CAAC,EAAE,CAAC,SAAS,IAAI,EAAE,EAAE,GAAG,CAAC;QACjD,IAAI,EAAE,OAAO,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,GAAG,CAAC;QACjC,WAAW,EAAE,cAAc,CAAC,EAAE,CAAC,iBAAiB,IAAI,EAAE,EAAE,GAAG,CAAC;QAC5D,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,eAAe,EAAE,QAAQ,CAAC,eAAe;QACzC,wEAAwE;QACxE,wEAAwE;QACxE,sEAAsE;QACtE,aAAa,EAAE,EAAE;QACjB,WAAW,EAAE,EAAE;QACf,SAAS,EAAE,WAAW,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,EAAE,GAAG,CAAC;QAC3C,gEAAgE;QAChE,kEAAkE;QAClE,YAAY,EAAE,EAAE;QAChB,cAAc,EAAE,UAAU,CAAC,EAAE,CAAC,aAAa,IAAI,EAAE,EAAE,GAAG,CAAC;QACvD,aAAa,EAAE,MAAM,CAAC,OAAO;QAC7B,oBAAoB,EAAE,MAAM,CAAC,MAAM;KACpC,CAAA;AACH,CAAC;AAED;;;;;;GAMG;AACH,SAAS,uBAAuB,CAC9B,EAAkB,EAClB,GAAkB,EAClB,IAAwB;IAExB,sEAAsE;IACtE,+DAA+D;IAC/D,OAAO,aAAa,CAAC,EAAE,CAAC,IAAI,IAAI,EAAE,uBAAuB,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE;QACpE,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,kBAAkB,EAAE,IAAI,CAAC,kBAAkB;KAC5C,CAAC,CAAA;AACJ,CAAC"}
@@ -78,35 +78,33 @@ export declare const firmwareActivityRowSchema: z.ZodObject<{
78
78
  arraySteps: z.ZodArray<z.ZodNumber, "many">;
79
79
  }, "strict", z.ZodTypeAny, {
80
80
  calories: number;
81
- timestamp: string;
82
81
  step: number;
82
+ timestamp: string;
83
83
  distance: number;
84
84
  arraySteps: number[];
85
85
  }, {
86
86
  calories: number;
87
- timestamp: string;
88
87
  step: number;
88
+ timestamp: string;
89
89
  distance: number;
90
90
  arraySteps: number[];
91
91
  }>;
92
- export declare const firmwareSleepRowSchema: z.ZodObject<{
93
- start: z.ZodString;
94
- end: z.ZodString;
95
- block_type: z.ZodString;
96
- confidence: z.ZodNumber;
92
+ /** Raw firmware sleep sample (types 0.10.0); `quality` is the firmware code. */
93
+ export declare const firmwareSleepRawRowSchema: z.ZodObject<{
97
94
  timestamp: z.ZodString;
95
+ quality: z.ZodNumber;
96
+ start: z.ZodString;
97
+ unitLength: z.ZodNumber;
98
98
  }, "strict", z.ZodTypeAny, {
99
99
  start: string;
100
+ quality: number;
101
+ unitLength: number;
100
102
  timestamp: string;
101
- end: string;
102
- block_type: string;
103
- confidence: number;
104
103
  }, {
105
104
  start: string;
105
+ quality: number;
106
+ unitLength: number;
106
107
  timestamp: string;
107
- end: string;
108
- block_type: string;
109
- confidence: number;
110
108
  }>;
111
109
  export declare const firmwareBatteryRowSchema: z.ZodObject<{
112
110
  timestamp: z.ZodString;
@@ -389,35 +387,32 @@ export declare const firmwareExportSchema: z.ZodObject<{
389
387
  arraySteps: z.ZodArray<z.ZodNumber, "many">;
390
388
  }, "strict", z.ZodTypeAny, {
391
389
  calories: number;
392
- timestamp: string;
393
390
  step: number;
391
+ timestamp: string;
394
392
  distance: number;
395
393
  arraySteps: number[];
396
394
  }, {
397
395
  calories: number;
398
- timestamp: string;
399
396
  step: number;
397
+ timestamp: string;
400
398
  distance: number;
401
399
  arraySteps: number[];
402
400
  }>, "many">;
403
- sleep_processed: z.ZodArray<z.ZodObject<{
404
- start: z.ZodString;
405
- end: z.ZodString;
406
- block_type: z.ZodString;
407
- confidence: z.ZodNumber;
401
+ sleep: z.ZodArray<z.ZodObject<{
408
402
  timestamp: z.ZodString;
403
+ quality: z.ZodNumber;
404
+ start: z.ZodString;
405
+ unitLength: z.ZodNumber;
409
406
  }, "strict", z.ZodTypeAny, {
410
407
  start: string;
408
+ quality: number;
409
+ unitLength: number;
411
410
  timestamp: string;
412
- end: string;
413
- block_type: string;
414
- confidence: number;
415
411
  }, {
416
412
  start: string;
413
+ quality: number;
414
+ unitLength: number;
417
415
  timestamp: string;
418
- end: string;
419
- block_type: string;
420
- confidence: number;
421
416
  }>, "many">;
422
417
  battery_table: z.ZodArray<z.ZodObject<{
423
418
  timestamp: z.ZodString;
@@ -561,17 +556,16 @@ export declare const firmwareExportSchema: z.ZodObject<{
561
556
  }[];
562
557
  activitydetails: {
563
558
  calories: number;
564
- timestamp: string;
565
559
  step: number;
560
+ timestamp: string;
566
561
  distance: number;
567
562
  arraySteps: number[];
568
563
  }[];
569
- sleep_processed: {
564
+ sleep: {
570
565
  start: string;
566
+ quality: number;
567
+ unitLength: number;
571
568
  timestamp: string;
572
- end: string;
573
- block_type: string;
574
- confidence: number;
575
569
  }[];
576
570
  battery_table: {
577
571
  timestamp: string;
@@ -622,17 +616,16 @@ export declare const firmwareExportSchema: z.ZodObject<{
622
616
  }[];
623
617
  activitydetails: {
624
618
  calories: number;
625
- timestamp: string;
626
619
  step: number;
620
+ timestamp: string;
627
621
  distance: number;
628
622
  arraySteps: number[];
629
623
  }[];
630
- sleep_processed: {
624
+ sleep: {
631
625
  start: string;
626
+ quality: number;
627
+ unitLength: number;
632
628
  timestamp: string;
633
- end: string;
634
- block_type: string;
635
- confidence: number;
636
629
  }[];
637
630
  battery_table: {
638
631
  timestamp: string;
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAQvB,eAAO,MAAM,mBAAmB;;;;;;;;;EAGrB,CAAA;AAEX,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;EAQtB,CAAA;AAEX,eAAO,MAAM,qBAAqB;;;;;;;;;EAGvB,CAAA;AAEX,eAAO,MAAM,qBAAqB;;;;;;;;;EAGvB,CAAA;AAEX,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;EAM3B,CAAA;AAEX,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;EAMxB,CAAA;AAEX,eAAO,MAAM,wBAAwB;;;;;;;;;EAG1B,CAAA;AAEX,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;EAQxB,CAAA;AAEX;;;;GAIG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAShC,CAAA;AAEX,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAE1B,CAAA;AAEX,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYtB,CAAA;AAEX,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AASvE;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAsDxD,CAAA"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAQvB,eAAO,MAAM,mBAAmB;;;;;;;;;EAGrB,CAAA;AAEX,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;EAQtB,CAAA;AAEX,eAAO,MAAM,qBAAqB;;;;;;;;;EAGvB,CAAA;AAEX,eAAO,MAAM,qBAAqB;;;;;;;;;EAGvB,CAAA;AAEX,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;EAM3B,CAAA;AAEX,gFAAgF;AAChF,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;EAK3B,CAAA;AAEX,eAAO,MAAM,wBAAwB;;;;;;;;;EAG1B,CAAA;AAEX,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;EAQxB,CAAA;AAEX;;;;GAIG;AACH,eAAO,MAAM,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAShC,CAAA;AAEX,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAE1B,CAAA;AAEX,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAYtB,CAAA;AAEX,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AASvE;;;GAGG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAqDxD,CAAA"}
@@ -45,12 +45,12 @@ export const firmwareActivityRowSchema = z.object({
45
45
  distance: z.number(),
46
46
  arraySteps: z.array(z.number()).length(10),
47
47
  }).strict();
48
- export const firmwareSleepRowSchema = z.object({
49
- start: firmwareTimestamp,
50
- end: firmwareTimestamp,
51
- block_type: z.string(),
52
- confidence: z.number(),
48
+ /** Raw firmware sleep sample (types 0.10.0); `quality` is the firmware code. */
49
+ export const firmwareSleepRawRowSchema = z.object({
53
50
  timestamp: firmwareTimestamp,
51
+ quality: z.number().int(),
52
+ start: firmwareTimestamp,
53
+ unitLength: z.number().int().positive(),
54
54
  }).strict();
55
55
  export const firmwareBatteryRowSchema = z.object({
56
56
  timestamp: firmwareTimestamp,
@@ -92,7 +92,7 @@ export const firmwareExportSchema = z.object({
92
92
  spo2: z.array(firmwareSpO2RowSchema),
93
93
  temperature_table: z.array(firmwareTempRowSchema),
94
94
  activitydetails: z.array(firmwareActivityRowSchema),
95
- sleep_processed: z.array(firmwareSleepRowSchema),
95
+ sleep: z.array(firmwareSleepRawRowSchema),
96
96
  battery_table: z.array(firmwareBatteryRowSchema),
97
97
  ring: firmwareRingConfigSchema,
98
98
  }).strict();
@@ -127,12 +127,11 @@ export const FIRMWARE_EXPORT_KEY_PATHS = new Set([
127
127
  'activitydetails[].calories',
128
128
  'activitydetails[].distance',
129
129
  'activitydetails[].arraySteps',
130
- 'sleep_processed',
131
- 'sleep_processed[].start',
132
- 'sleep_processed[].end',
133
- 'sleep_processed[].block_type',
134
- 'sleep_processed[].confidence',
135
- 'sleep_processed[].timestamp',
130
+ 'sleep',
131
+ 'sleep[].timestamp',
132
+ 'sleep[].quality',
133
+ 'sleep[].start',
134
+ 'sleep[].unitLength',
136
135
  'battery_table',
137
136
  'battery_table[].timestamp',
138
137
  'battery_table[].battery',
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/sync/mapper/schema.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/sync/mapper/schema.ts"],"names":[],"mappings":"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"}
@@ -1,59 +1,66 @@
1
1
  /**
2
- * Sleep reconstruction mapper (T-06).
2
+ * Sleep mapping (sleep-correction §3).
3
3
  *
4
- * Firmware `sleep_processed` emits per-block rows tagged with a `start`
5
- * (session start), `end` (session end), `block_type` (primary / light / deep /
6
- * rem / awake), `confidence` (0..1), and per-block `timestamp` (block instant).
7
- * Multiple blocks share the same `start` when they belong to the same session.
4
+ * Two halves that run at different times:
8
5
  *
9
- * Reconstruction rules (spec.md §Firmware mapper + tasks.md T-06):
10
- * - Group rows by `start`.
11
- * - A session is included if AT LEAST ONE block in the group has
12
- * `block_type === 'primary'` AND `confidence >= 0.7`. This filters
13
- * day-time naps and low-confidence noise.
14
- * - Session boundaries come from the firmware's own envelope fields:
15
- * `ts_start = parse(start)`, `ts_end = parse(end)`. `total_minutes` is
16
- * their difference. Deriving the envelope from block instants would
17
- * under-report any session whose first/last minutes went unclassified.
18
- * - `session_id` = `fnv1a32hex(device_id | user_id | ts_session_start |
19
- * ts_session_end)` (principle 25, amended 2026-08-14) — fully
20
- * deterministic, so the same night mapped twice yields the same id.
21
- * - `night_of` = 6pm-6pm rule applied to `ts_start` (see `time.ts`).
22
- * - Stages: one row per classified block, carrying `session_id` and the
23
- * DDL's integer `stage` code. `primary` is the session envelope marker,
24
- * not a stage, so it produces no `sleep_stage` row — the DDL's stage
25
- * enum (1=awake, 2=light, 3=deep, 5=rem) has no code for it. Unknown
26
- * block types are likewise skipped; nothing is lost, because...
27
- * - Raw: every input row is preserved in `sleep_raw` regardless of the
28
- * primary/confidence filter, flattened onto the DDL's columns, so
29
- * downstream reprocessing can revisit sessions with new heuristics.
6
+ * - `mapSleepRaw`, at ingest: every raw firmware sample → `sleep_raw`,
7
+ * verbatim. `quality` stays the FIRMWARE code (1 deep / 2 light / 3 rem /
8
+ * 5 awake, other codes kept). Nothing is classified here `sleep_raw` is
9
+ * the reprocessing source the correction pipeline reads.
10
+ * - `sessionsFromCorrected`, after the batch flushes: one night's Phase 3
11
+ * output from `../sleep-correction` `sleep_session` + `sleep_stage`.
30
12
  *
31
- * Principle 20 boundary: firmware `block_type` strings are translated to our
32
- * integer codes here and never reach the warehouse.
13
+ * Until 0.24.0 the mapper built sessions straight from app-classified blocks
14
+ * (`sleep_processed`) an admission filter on `primary`, a per-block `end`
15
+ * read as the session end, and a guessed stage map. All three were wrong on
16
+ * device (zivaone_app#77, and the stage swap found 2026-09-10). Sessions now
17
+ * come only from the validated correction pipeline.
18
+ *
19
+ * Principle 20: the firmware `quality` code is translated to the DDL's
20
+ * `stage` code here, and nowhere else. The two enums share integers and mean
21
+ * different things — `FIRMWARE_QUALITY_TO_STAGE` is the one crossing point.
33
22
  */
34
- import type { FirmwareSleepRow, MapperContext, SleepRawRow, SleepSessionRow, SleepStageRow } from './types';
23
+ import type { ClassifiedRow } from '../sleep-correction/classify';
24
+ import type { FirmwareSleepRawRow, MapperContext, SleepRawRow, SleepSessionRow, SleepStageRow } from './types';
35
25
  /**
36
- * Block width in minutes when the firmware revision omits `unit_length`.
37
- * Firmware doc §Sleep processing: blocks are 1-min wide by convention.
38
- */
39
- export declare const DEFAULT_BLOCK_MINUTES = 1;
40
- /**
41
- * Firmware `block_type` → `sleep_stage.stage` code (spec §Table schema:
42
- * `stage SMALLINT NOT NULL -- 1=awake, 2=light, 3=deep, 5=rem`).
43
- *
44
- * `primary` is deliberately absent: it marks the session envelope, not a
45
- * sleep stage. The enum is intentionally sparse (no 4) to match the DDL
46
- * comment verbatim.
26
+ * `sleep_stage.stage` codes (DDL: `1=awake, 2=light, 3=deep, 5=rem`). Sparse
27
+ * on purpose (no 4) to match the DDL comment verbatim.
47
28
  */
48
29
  export declare const SLEEP_STAGE_CODES: Readonly<Record<string, number>>;
49
30
  /** Reverse lookup for the stage-minute accumulator + diagnostics. */
50
31
  export declare const SLEEP_STAGE_NAMES: Readonly<Record<number, string>>;
51
- export interface ReconstructSleepResult {
32
+ /**
33
+ * Firmware `quality` → DDL `stage`. The firmware says 1 deep / 2 light /
34
+ * 3 rem / 5 awake (ziva_app v3.1; the vendor header is silent); the DDL says
35
+ * 1 awake / 2 light / 3 deep / 5 rem. Copying a predicate across the two
36
+ * computes deep from awake with no error — this map is the only translation.
37
+ */
38
+ export declare const FIRMWARE_QUALITY_TO_STAGE: Readonly<Record<number, number>>;
39
+ /** Raw firmware samples → `sleep_raw`, verbatim. */
40
+ export declare function mapSleepRaw(rows: readonly FirmwareSleepRawRow[], ctx: MapperContext): SleepRawRow[];
41
+ export interface SessionsFromCorrectedResult {
52
42
  sleep_session: SleepSessionRow[];
53
43
  sleep_stage: SleepStageRow[];
54
- sleep_raw: SleepRawRow[];
55
44
  }
56
- export declare function reconstructSleepSessions(rows: readonly FirmwareSleepRow[], ctx: MapperContext): ReconstructSleepResult;
45
+ /**
46
+ * One night's corrected rows → sessions + stages.
47
+ *
48
+ * - Only `block_type === 'primary'` rows count (R-A: every primary minute,
49
+ * whatever its confidence — confidence is provenance, never a gate).
50
+ * - Sessions are Phase 2's stitched sessions, identified by `start`. A night
51
+ * is normally one; a gap between 20 and 30 minutes inside the primary
52
+ * block leaves two.
53
+ * - `ts_end` is the last primary minute plus its width; `total_minutes` is
54
+ * the primary minutes actually present (awake minutes included).
55
+ * - `settle_min` belongs to the night, measured from bed, so it goes on the
56
+ * session that starts at bed — the first. `recovered_min` is per session.
57
+ * - `session_id` is principle 25's deterministic hash; the caller replaces
58
+ * the whole night before writing (principle 66 as amended), because a
59
+ * corrected END moves as later data arrives and the id moves with it.
60
+ */
61
+ export declare function sessionsFromCorrected(rows: readonly ClassifiedRow[], ctx: MapperContext, night?: {
62
+ settleMin?: number | null;
63
+ }): SessionsFromCorrectedResult;
57
64
  /**
58
65
  * Session id (principle 25, as amended 2026-08-14):
59
66
  * `fnv1a32hex(device_id | user_id | ts_session_start | ts_session_end)`
@@ -1 +1 @@
1
- {"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/sleep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACd,MAAM,SAAS,CAAA;AAOhB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,IAAI,CAAA;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK7D,CAAA;AAEF,qEAAqE;AACrE,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK7D,CAAA;AAEF,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,eAAe,EAAE,CAAA;IAChC,WAAW,EAAE,aAAa,EAAE,CAAA;IAC5B,SAAS,EAAE,WAAW,EAAE,CAAA;CACzB;AAoBD,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,gBAAgB,EAAE,EACjC,GAAG,EAAE,aAAa,GACjB,sBAAsB,CAkGxB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,aAAa,EAClB,cAAc,EAAE,IAAI,EACpB,YAAY,EAAE,IAAI,GACjB,MAAM,CAKR;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD"}
1
+ {"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/sleep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAA;AACjE,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACd,MAAM,SAAS,CAAA;AAIhB;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK7D,CAAA;AAEF,qEAAqE;AACrE,eAAO,MAAM,iBAAiB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK7D,CAAA;AAEF;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKrE,CAAA;AAEF,oDAAoD;AACpD,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,mBAAmB,EAAE,EAAE,GAAG,EAAE,aAAa,GAAG,WAAW,EAAE,CAWnG;AAED,MAAM,WAAW,2BAA2B;IAC1C,aAAa,EAAE,eAAe,EAAE,CAAA;IAChC,WAAW,EAAE,aAAa,EAAE,CAAA;CAC7B;AAKD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,SAAS,aAAa,EAAE,EAC9B,GAAG,EAAE,aAAa,EAClB,KAAK,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GACxC,2BAA2B,CAkE7B;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAC3B,GAAG,EAAE,aAAa,EAClB,cAAc,EAAE,IAAI,EACpB,YAAY,EAAE,IAAI,GACjB,MAAM,CAKR;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD"}
@@ -1,52 +1,30 @@
1
1
  /**
2
- * Sleep reconstruction mapper (T-06).
2
+ * Sleep mapping (sleep-correction §3).
3
3
  *
4
- * Firmware `sleep_processed` emits per-block rows tagged with a `start`
5
- * (session start), `end` (session end), `block_type` (primary / light / deep /
6
- * rem / awake), `confidence` (0..1), and per-block `timestamp` (block instant).
7
- * Multiple blocks share the same `start` when they belong to the same session.
4
+ * Two halves that run at different times:
8
5
  *
9
- * Reconstruction rules (spec.md §Firmware mapper + tasks.md T-06):
10
- * - Group rows by `start`.
11
- * - A session is included if AT LEAST ONE block in the group has
12
- * `block_type === 'primary'` AND `confidence >= 0.7`. This filters
13
- * day-time naps and low-confidence noise.
14
- * - Session boundaries come from the firmware's own envelope fields:
15
- * `ts_start = parse(start)`, `ts_end = parse(end)`. `total_minutes` is
16
- * their difference. Deriving the envelope from block instants would
17
- * under-report any session whose first/last minutes went unclassified.
18
- * - `session_id` = `fnv1a32hex(device_id | user_id | ts_session_start |
19
- * ts_session_end)` (principle 25, amended 2026-08-14) — fully
20
- * deterministic, so the same night mapped twice yields the same id.
21
- * - `night_of` = 6pm-6pm rule applied to `ts_start` (see `time.ts`).
22
- * - Stages: one row per classified block, carrying `session_id` and the
23
- * DDL's integer `stage` code. `primary` is the session envelope marker,
24
- * not a stage, so it produces no `sleep_stage` row — the DDL's stage
25
- * enum (1=awake, 2=light, 3=deep, 5=rem) has no code for it. Unknown
26
- * block types are likewise skipped; nothing is lost, because...
27
- * - Raw: every input row is preserved in `sleep_raw` regardless of the
28
- * primary/confidence filter, flattened onto the DDL's columns, so
29
- * downstream reprocessing can revisit sessions with new heuristics.
6
+ * - `mapSleepRaw`, at ingest: every raw firmware sample → `sleep_raw`,
7
+ * verbatim. `quality` stays the FIRMWARE code (1 deep / 2 light / 3 rem /
8
+ * 5 awake, other codes kept). Nothing is classified here `sleep_raw` is
9
+ * the reprocessing source the correction pipeline reads.
10
+ * - `sessionsFromCorrected`, after the batch flushes: one night's Phase 3
11
+ * output from `../sleep-correction` `sleep_session` + `sleep_stage`.
30
12
  *
31
- * Principle 20 boundary: firmware `block_type` strings are translated to our
32
- * integer codes here and never reach the warehouse.
13
+ * Until 0.24.0 the mapper built sessions straight from app-classified blocks
14
+ * (`sleep_processed`) an admission filter on `primary`, a per-block `end`
15
+ * read as the session end, and a guessed stage map. All three were wrong on
16
+ * device (zivaone_app#77, and the stage swap found 2026-09-10). Sessions now
17
+ * come only from the validated correction pipeline.
18
+ *
19
+ * Principle 20: the firmware `quality` code is translated to the DDL's
20
+ * `stage` code here, and nowhere else. The two enums share integers and mean
21
+ * different things — `FIRMWARE_QUALITY_TO_STAGE` is the one crossing point.
33
22
  */
23
+ import { parseDateStr } from '../sleep-correction/classify';
34
24
  import { computeNightOf, parseTimestamp } from './time';
35
- const MINUTE_MS = 60000;
36
- const PRIMARY = 'primary';
37
- const CONFIDENCE_FLOOR = 0.7;
38
- /**
39
- * Block width in minutes when the firmware revision omits `unit_length`.
40
- * Firmware doc §Sleep processing: blocks are 1-min wide by convention.
41
- */
42
- export const DEFAULT_BLOCK_MINUTES = 1;
43
25
  /**
44
- * Firmware `block_type` → `sleep_stage.stage` code (spec §Table schema:
45
- * `stage SMALLINT NOT NULL -- 1=awake, 2=light, 3=deep, 5=rem`).
46
- *
47
- * `primary` is deliberately absent: it marks the session envelope, not a
48
- * sleep stage. The enum is intentionally sparse (no 4) to match the DDL
49
- * comment verbatim.
26
+ * `sleep_stage.stage` codes (DDL: `1=awake, 2=light, 3=deep, 5=rem`). Sparse
27
+ * on purpose (no 4) to match the DDL comment verbatim.
50
28
  */
51
29
  export const SLEEP_STAGE_CODES = Object.freeze({
52
30
  awake: 1,
@@ -61,88 +39,89 @@ export const SLEEP_STAGE_NAMES = Object.freeze({
61
39
  3: 'deep',
62
40
  5: 'rem',
63
41
  });
64
- /** Per-block width, honouring a firmware-supplied `unit_length`. */
65
- function blockMinutes(row) {
66
- return typeof row.unit_length === 'number' && row.unit_length > 0
67
- ? row.unit_length
68
- : DEFAULT_BLOCK_MINUTES;
69
- }
70
42
  /**
71
- * `sleep_raw.quality` is NOT NULL. Firmware revisions that carry a native
72
- * `quality` pass it through verbatim; older revisions derive it from the
73
- * block confidence (0..1 0..100) so the column is always populated.
43
+ * Firmware `quality` DDL `stage`. The firmware says 1 deep / 2 light /
44
+ * 3 rem / 5 awake (ziva_app v3.1; the vendor header is silent); the DDL says
45
+ * 1 awake / 2 light / 3 deep / 5 rem. Copying a predicate across the two
46
+ * computes deep from awake with no error — this map is the only translation.
74
47
  */
75
- function rawQuality(row) {
76
- return typeof row.quality === 'number'
77
- ? row.quality
78
- : Math.round(row.confidence * 100);
48
+ export const FIRMWARE_QUALITY_TO_STAGE = Object.freeze({
49
+ 1: SLEEP_STAGE_CODES.deep,
50
+ 2: SLEEP_STAGE_CODES.light,
51
+ 3: SLEEP_STAGE_CODES.rem,
52
+ 5: SLEEP_STAGE_CODES.awake,
53
+ });
54
+ /** Raw firmware samples → `sleep_raw`, verbatim. */
55
+ export function mapSleepRaw(rows, ctx) {
56
+ return rows.map(r => ({
57
+ ts: parseTimestamp(r.timestamp),
58
+ ts_session_start: parseTimestamp(r.start),
59
+ brand: ctx.brand,
60
+ family_id: ctx.familyId,
61
+ user_id: ctx.userId,
62
+ device_id: ctx.deviceId,
63
+ quality: r.quality,
64
+ unit_length: typeof r.unitLength === 'number' ? r.unitLength : null,
65
+ }));
79
66
  }
80
- export function reconstructSleepSessions(rows, ctx) {
81
- const sleep_session = [];
82
- const sleep_stage = [];
83
- const sleep_raw = [];
84
- // Raw pass — every input row preserved for reprocessing, including rows
85
- // from sessions the primary/confidence filter drops.
86
- for (const row of rows) {
87
- sleep_raw.push({
88
- ts: parseTimestamp(row.timestamp),
89
- ts_session_start: parseTimestamp(row.start),
90
- brand: ctx.brand,
91
- family_id: ctx.familyId,
92
- user_id: ctx.userId,
93
- device_id: ctx.deviceId,
94
- quality: rawQuality(row),
95
- unit_length: typeof row.unit_length === 'number' ? row.unit_length : null,
96
- });
97
- }
98
- // Group by `start`.
67
+ /** Width of one Phase 2/3 row. v3.1 emits every row at `unitLength` 1. */
68
+ const ROW_SECONDS = 60;
69
+ /**
70
+ * One night's corrected rows → sessions + stages.
71
+ *
72
+ * - Only `block_type === 'primary'` rows count (R-A: every primary minute,
73
+ * whatever its confidence — confidence is provenance, never a gate).
74
+ * - Sessions are Phase 2's stitched sessions, identified by `start`. A night
75
+ * is normally one; a gap between 20 and 30 minutes inside the primary
76
+ * block leaves two.
77
+ * - `ts_end` is the last primary minute plus its width; `total_minutes` is
78
+ * the primary minutes actually present (awake minutes included).
79
+ * - `settle_min` belongs to the night, measured from bed, so it goes on the
80
+ * session that starts at bed — the first. `recovered_min` is per session.
81
+ * - `session_id` is principle 25's deterministic hash; the caller replaces
82
+ * the whole night before writing (principle 66 as amended), because a
83
+ * corrected END moves as later data arrives and the id moves with it.
84
+ */
85
+ export function sessionsFromCorrected(rows, ctx, night = {}) {
99
86
  const bySession = new Map();
100
- for (const row of rows) {
101
- const bucket = bySession.get(row.start);
87
+ for (const r of rows) {
88
+ if (r.block_type !== 'primary')
89
+ continue;
90
+ const bucket = bySession.get(r.start);
102
91
  if (bucket)
103
- bucket.push(row);
92
+ bucket.push(r);
104
93
  else
105
- bySession.set(row.start, [row]);
94
+ bySession.set(r.start, [r]);
106
95
  }
107
- for (const [startKey, blocks] of bySession) {
108
- // Session must contain a primary block above the confidence floor to be
109
- // counted as a real sleep session (as opposed to a nap or noise).
110
- const hasQualifyingPrimary = blocks.some(b => b.block_type === PRIMARY && b.confidence >= CONFIDENCE_FLOOR);
111
- if (!hasQualifyingPrimary)
112
- continue;
113
- // Envelope from the firmware's own session fields. `end` is exclusive of
114
- // nothing — it is the session end instant, so total is a plain delta.
115
- const tsStart = parseTimestamp(startKey);
116
- const tsEnd = parseTimestamp(blocks[0].end);
117
- const totalMinutes = Math.max(0, Math.round((tsEnd.getTime() - tsStart.getTime()) / MINUTE_MS));
118
- const nightOf = computeNightOf(tsStart, ctx.userTimezone);
96
+ const groups = Array.from(bySession.values(), g => g.map(r => ({ r, e: parseDateStr(r.date) })).sort((a, b) => a.e - b.e))
97
+ .sort((a, b) => a[0].e - b[0].e);
98
+ const sleep_session = [];
99
+ const sleep_stage = [];
100
+ groups.forEach((group, index) => {
101
+ const tsStart = new Date(group[0].e * 1000);
102
+ const tsEnd = new Date((group[group.length - 1].e + ROW_SECONDS) * 1000);
119
103
  const sessionId = makeSessionId(ctx, tsStart, tsEnd);
120
- // Stage rows + per-stage minute accumulation in one pass.
121
- const stageMinutes = {
122
- awake: 0,
123
- light: 0,
124
- deep: 0,
125
- rem: 0,
126
- };
104
+ const minutes = { awake: 0, light: 0, deep: 0, rem: 0 };
127
105
  let confidenceSum = 0;
128
- for (const b of blocks) {
129
- confidenceSum += b.confidence;
130
- const code = SLEEP_STAGE_CODES[b.block_type];
131
- if (code === undefined) {
132
- // `primary` (envelope marker) and any unrecognised firmware block
133
- // type. Not a stage preserved in sleep_raw, skipped here.
134
- continue;
135
- }
136
- stageMinutes[SLEEP_STAGE_NAMES[code]] += blockMinutes(b);
106
+ let recovered = 0;
107
+ for (const { r, e } of group) {
108
+ confidenceSum += r.confidence;
109
+ if (r.source === 'envelope' || r.source === 'gap')
110
+ recovered++;
111
+ const stage = FIRMWARE_QUALITY_TO_STAGE[r.quality];
112
+ if (stage === undefined)
113
+ continue; // a code the pipeline passed but the DDL has no stage for
114
+ minutes[SLEEP_STAGE_NAMES[stage]]++;
137
115
  sleep_stage.push({
138
- ts: parseTimestamp(b.timestamp),
116
+ ts: new Date(e * 1000),
139
117
  brand: ctx.brand,
140
118
  family_id: ctx.familyId,
141
119
  user_id: ctx.userId,
142
120
  device_id: ctx.deviceId,
143
121
  session_id: sessionId,
144
- stage: code,
145
- confidence: b.confidence,
122
+ stage,
123
+ confidence: r.confidence,
124
+ source: r.source,
146
125
  });
147
126
  }
148
127
  sleep_session.push({
@@ -153,16 +132,18 @@ export function reconstructSleepSessions(rows, ctx) {
153
132
  session_id: sessionId,
154
133
  ts_start: tsStart,
155
134
  ts_end: tsEnd,
156
- total_minutes: totalMinutes,
157
- deep_minutes: stageMinutes.deep,
158
- rem_minutes: stageMinutes.rem,
159
- light_minutes: stageMinutes.light,
160
- awake_minutes: stageMinutes.awake,
161
- avg_confidence: blocks.length > 0 ? confidenceSum / blocks.length : null,
162
- night_of: nightOf,
135
+ total_minutes: group.length,
136
+ deep_minutes: minutes.deep,
137
+ rem_minutes: minutes.rem,
138
+ light_minutes: minutes.light,
139
+ awake_minutes: minutes.awake,
140
+ avg_confidence: confidenceSum / group.length,
141
+ night_of: computeNightOf(tsStart, ctx.userTimezone),
142
+ settle_min: index === 0 ? (night.settleMin ?? null) : null,
143
+ recovered_min: recovered,
163
144
  });
164
- }
165
- return { sleep_session, sleep_stage, sleep_raw };
145
+ });
146
+ return { sleep_session, sleep_stage };
166
147
  }
167
148
  /**
168
149
  * Session id (principle 25, as amended 2026-08-14):