@mongrov/analytics 0.6.0 → 0.8.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 (227) hide show
  1. package/README.md +23 -10
  2. package/UPGRADING.md +456 -0
  3. package/dist/core/device-events.d.ts +14 -0
  4. package/dist/core/device-events.d.ts.map +1 -0
  5. package/dist/core/device-events.js +13 -0
  6. package/dist/core/device-events.js.map +1 -0
  7. package/dist/core/errors.d.ts +1 -1
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js.map +1 -1
  10. package/dist/core/extensions.d.ts +21 -4
  11. package/dist/core/extensions.d.ts.map +1 -1
  12. package/dist/core/extensions.js +21 -4
  13. package/dist/core/extensions.js.map +1 -1
  14. package/dist/core/factory.d.ts.map +1 -1
  15. package/dist/core/factory.js +125 -23
  16. package/dist/core/factory.js.map +1 -1
  17. package/dist/core/hooks.js +1 -1
  18. package/dist/core/hooks.js.map +1 -1
  19. package/dist/core/index.d.ts +13 -1
  20. package/dist/core/index.d.ts.map +1 -1
  21. package/dist/core/index.js +18 -0
  22. package/dist/core/index.js.map +1 -1
  23. package/dist/core/insight.d.ts +44 -0
  24. package/dist/core/insight.d.ts.map +1 -0
  25. package/dist/core/insight.js +62 -0
  26. package/dist/core/insight.js.map +1 -0
  27. package/dist/core/metric_metadata.d.ts +55 -0
  28. package/dist/core/metric_metadata.d.ts.map +1 -1
  29. package/dist/core/metric_metadata.js +35 -7
  30. package/dist/core/metric_metadata.js.map +1 -1
  31. package/dist/core/migrations.d.ts.map +1 -1
  32. package/dist/core/migrations.js +103 -2
  33. package/dist/core/migrations.js.map +1 -1
  34. package/dist/core/retention.d.ts +44 -13
  35. package/dist/core/retention.d.ts.map +1 -1
  36. package/dist/core/retention.js +49 -22
  37. package/dist/core/retention.js.map +1 -1
  38. package/dist/core/sampling.d.ts +70 -0
  39. package/dist/core/sampling.d.ts.map +1 -0
  40. package/dist/core/sampling.js +107 -0
  41. package/dist/core/sampling.js.map +1 -0
  42. package/dist/core/schemas.d.ts +73 -2
  43. package/dist/core/schemas.d.ts.map +1 -1
  44. package/dist/core/schemas.js +160 -5
  45. package/dist/core/schemas.js.map +1 -1
  46. package/dist/core/table_metadata.d.ts.map +1 -1
  47. package/dist/core/table_metadata.js +3 -0
  48. package/dist/core/table_metadata.js.map +1 -1
  49. package/dist/core/types.d.ts +45 -10
  50. package/dist/core/types.d.ts.map +1 -1
  51. package/dist/core/warehouse.d.ts +28 -0
  52. package/dist/core/warehouse.d.ts.map +1 -1
  53. package/dist/core/warehouse.js +44 -0
  54. package/dist/core/warehouse.js.map +1 -1
  55. package/dist/core/watermark-cache.d.ts +75 -0
  56. package/dist/core/watermark-cache.d.ts.map +1 -0
  57. package/dist/core/watermark-cache.js +0 -0
  58. package/dist/core/watermark-cache.js.map +1 -0
  59. package/dist/rules/__fakes__/fakeEngine.d.ts +2 -0
  60. package/dist/rules/__fakes__/fakeEngine.d.ts.map +1 -1
  61. package/dist/rules/__fakes__/fakeEngine.js +4 -0
  62. package/dist/rules/__fakes__/fakeEngine.js.map +1 -1
  63. package/dist/rules/baseline-reader.d.ts +55 -0
  64. package/dist/rules/baseline-reader.d.ts.map +1 -0
  65. package/dist/rules/baseline-reader.js +111 -0
  66. package/dist/rules/baseline-reader.js.map +1 -0
  67. package/dist/rules/compiler.d.ts +36 -1
  68. package/dist/rules/compiler.d.ts.map +1 -1
  69. package/dist/rules/compiler.js +185 -39
  70. package/dist/rules/compiler.js.map +1 -1
  71. package/dist/rules/defaults/luminx.d.ts.map +1 -1
  72. package/dist/rules/defaults/luminx.js +8 -0
  73. package/dist/rules/defaults/luminx.js.map +1 -1
  74. package/dist/rules/defaults/viva.d.ts.map +1 -1
  75. package/dist/rules/defaults/viva.js +7 -0
  76. package/dist/rules/defaults/viva.js.map +1 -1
  77. package/dist/rules/defaults/yogaring.d.ts.map +1 -1
  78. package/dist/rules/defaults/yogaring.js +7 -0
  79. package/dist/rules/defaults/yogaring.js.map +1 -1
  80. package/dist/rules/defaults/ziva.d.ts.map +1 -1
  81. package/dist/rules/defaults/ziva.js +100 -24
  82. package/dist/rules/defaults/ziva.js.map +1 -1
  83. package/dist/rules/evaluator.d.ts +14 -3
  84. package/dist/rules/evaluator.d.ts.map +1 -1
  85. package/dist/rules/evaluator.js +157 -5
  86. package/dist/rules/evaluator.js.map +1 -1
  87. package/dist/rules/factory.d.ts.map +1 -1
  88. package/dist/rules/factory.js +5 -1
  89. package/dist/rules/factory.js.map +1 -1
  90. package/dist/rules/index.d.ts +2 -1
  91. package/dist/rules/index.d.ts.map +1 -1
  92. package/dist/rules/index.js +2 -1
  93. package/dist/rules/index.js.map +1 -1
  94. package/dist/rules/schema.d.ts +75 -4
  95. package/dist/rules/schema.d.ts.map +1 -1
  96. package/dist/rules/schema.js +44 -0
  97. package/dist/rules/schema.js.map +1 -1
  98. package/dist/rules/types.d.ts +16 -1
  99. package/dist/rules/types.d.ts.map +1 -1
  100. package/dist/rules/validator.d.ts.map +1 -1
  101. package/dist/rules/validator.js +89 -0
  102. package/dist/rules/validator.js.map +1 -1
  103. package/dist/sync/baseline-compute.d.ts +63 -0
  104. package/dist/sync/baseline-compute.d.ts.map +1 -0
  105. package/dist/sync/baseline-compute.js +211 -0
  106. package/dist/sync/baseline-compute.js.map +1 -0
  107. package/dist/sync/events.d.ts +19 -5
  108. package/dist/sync/events.d.ts.map +1 -1
  109. package/dist/sync/events.js +17 -0
  110. package/dist/sync/events.js.map +1 -1
  111. package/dist/sync/factory.d.ts +54 -8
  112. package/dist/sync/factory.d.ts.map +1 -1
  113. package/dist/sync/factory.js +145 -46
  114. package/dist/sync/factory.js.map +1 -1
  115. package/dist/sync/fetcher.d.ts +15 -2
  116. package/dist/sync/fetcher.d.ts.map +1 -1
  117. package/dist/sync/fetcher.js +43 -7
  118. package/dist/sync/fetcher.js.map +1 -1
  119. package/dist/sync/flusher.d.ts +52 -2
  120. package/dist/sync/flusher.d.ts.map +1 -1
  121. package/dist/sync/flusher.js +78 -9
  122. package/dist/sync/flusher.js.map +1 -1
  123. package/dist/sync/index.d.ts +3 -1
  124. package/dist/sync/index.d.ts.map +1 -1
  125. package/dist/sync/index.js +1 -0
  126. package/dist/sync/index.js.map +1 -1
  127. package/dist/sync/mapper/activity.d.ts.map +1 -1
  128. package/dist/sync/mapper/activity.js +2 -1
  129. package/dist/sync/mapper/activity.js.map +1 -1
  130. package/dist/sync/mapper/firmware.d.ts +3 -12
  131. package/dist/sync/mapper/firmware.d.ts.map +1 -1
  132. package/dist/sync/mapper/firmware.js +2 -8
  133. package/dist/sync/mapper/firmware.js.map +1 -1
  134. package/dist/sync/mapper/ring-config.d.ts +76 -28
  135. package/dist/sync/mapper/ring-config.d.ts.map +1 -1
  136. package/dist/sync/mapper/ring-config.js +118 -35
  137. package/dist/sync/mapper/ring-config.js.map +1 -1
  138. package/dist/sync/mapper/schema.d.ts +669 -0
  139. package/dist/sync/mapper/schema.d.ts.map +1 -0
  140. package/dist/sync/mapper/schema.js +160 -0
  141. package/dist/sync/mapper/schema.js.map +1 -0
  142. package/dist/sync/mapper/sleep.d.ts +58 -13
  143. package/dist/sync/mapper/sleep.d.ts.map +1 -1
  144. package/dist/sync/mapper/sleep.js +141 -44
  145. package/dist/sync/mapper/sleep.js.map +1 -1
  146. package/dist/sync/mapper/types.d.ts +118 -29
  147. package/dist/sync/mapper/types.d.ts.map +1 -1
  148. package/dist/sync/pending_closes.js +2 -2
  149. package/dist/sync/pending_closes.js.map +1 -1
  150. package/dist/sync/pusher.js +2 -2
  151. package/dist/sync/pusher.js.map +1 -1
  152. package/dist/sync/scheduler.d.ts +7 -0
  153. package/dist/sync/scheduler.d.ts.map +1 -1
  154. package/dist/sync/scheduler.js +14 -0
  155. package/dist/sync/scheduler.js.map +1 -1
  156. package/dist/tools/__fakes__/engine.d.ts +2 -0
  157. package/dist/tools/__fakes__/engine.d.ts.map +1 -1
  158. package/dist/tools/__fakes__/engine.js +10 -0
  159. package/dist/tools/__fakes__/engine.js.map +1 -1
  160. package/dist/tools/authorize.d.ts +19 -12
  161. package/dist/tools/authorize.d.ts.map +1 -1
  162. package/dist/tools/authorize.js +31 -40
  163. package/dist/tools/authorize.js.map +1 -1
  164. package/dist/tools/factory.d.ts +2 -0
  165. package/dist/tools/factory.d.ts.map +1 -1
  166. package/dist/tools/factory.js +12 -1
  167. package/dist/tools/factory.js.map +1 -1
  168. package/dist/tools/formatters/copy-guidelines.d.ts +57 -0
  169. package/dist/tools/formatters/copy-guidelines.d.ts.map +1 -0
  170. package/dist/tools/formatters/copy-guidelines.js +110 -0
  171. package/dist/tools/formatters/copy-guidelines.js.map +1 -0
  172. package/dist/tools/formatters/index.d.ts +14 -0
  173. package/dist/tools/formatters/index.d.ts.map +1 -0
  174. package/dist/tools/formatters/index.js +14 -0
  175. package/dist/tools/formatters/index.js.map +1 -0
  176. package/dist/tools/formatters/spo2.d.ts +21 -0
  177. package/dist/tools/formatters/spo2.d.ts.map +1 -0
  178. package/dist/tools/formatters/spo2.js +79 -0
  179. package/dist/tools/formatters/spo2.js.map +1 -0
  180. package/dist/tools/{formatters.d.ts → formatters/text.d.ts} +1 -1
  181. package/dist/tools/formatters/text.d.ts.map +1 -0
  182. package/dist/tools/{formatters.js → formatters/text.js} +1 -1
  183. package/dist/tools/formatters/text.js.map +1 -0
  184. package/dist/tools/impls/activity.d.ts +2 -2
  185. package/dist/tools/impls/activity.js +7 -3
  186. package/dist/tools/impls/activity.js.map +1 -1
  187. package/dist/tools/impls/anomaly.d.ts +2 -2
  188. package/dist/tools/impls/anomaly.js +8 -4
  189. package/dist/tools/impls/anomaly.js.map +1 -1
  190. package/dist/tools/impls/compare.d.ts +2 -2
  191. package/dist/tools/impls/compare.js +8 -4
  192. package/dist/tools/impls/compare.js.map +1 -1
  193. package/dist/tools/impls/hrv.d.ts +2 -2
  194. package/dist/tools/impls/hrv.js +6 -2
  195. package/dist/tools/impls/hrv.js.map +1 -1
  196. package/dist/tools/impls/insights.d.ts +4 -4
  197. package/dist/tools/impls/insights.d.ts.map +1 -1
  198. package/dist/tools/impls/insights.js +10 -3
  199. package/dist/tools/impls/insights.js.map +1 -1
  200. package/dist/tools/impls/sleep.d.ts +2 -2
  201. package/dist/tools/impls/sleep.js +6 -2
  202. package/dist/tools/impls/sleep.js.map +1 -1
  203. package/dist/tools/impls/spo2.d.ts +52 -0
  204. package/dist/tools/impls/spo2.d.ts.map +1 -0
  205. package/dist/tools/impls/spo2.js +65 -0
  206. package/dist/tools/impls/spo2.js.map +1 -0
  207. package/dist/tools/index.d.ts +1 -0
  208. package/dist/tools/index.d.ts.map +1 -1
  209. package/dist/tools/index.js +4 -0
  210. package/dist/tools/index.js.map +1 -1
  211. package/dist/tools/mcp/guard.d.ts +28 -11
  212. package/dist/tools/mcp/guard.d.ts.map +1 -1
  213. package/dist/tools/mcp/guard.js +45 -15
  214. package/dist/tools/mcp/guard.js.map +1 -1
  215. package/dist/tools/mcp/index.d.ts +1 -1
  216. package/dist/tools/mcp/index.d.ts.map +1 -1
  217. package/dist/tools/mcp/index.js +1 -1
  218. package/dist/tools/mcp/index.js.map +1 -1
  219. package/dist/tools/mcp/server.d.ts.map +1 -1
  220. package/dist/tools/mcp/server.js +4 -0
  221. package/dist/tools/mcp/server.js.map +1 -1
  222. package/dist/tools/mcp/transports/http.d.ts.map +1 -1
  223. package/dist/tools/mcp/transports/http.js +4 -0
  224. package/dist/tools/mcp/transports/http.js.map +1 -1
  225. package/package.json +8 -5
  226. package/dist/tools/formatters.d.ts.map +0 -1
  227. package/dist/tools/formatters.js.map +0 -1
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Zod contract for the firmware export payload (principle 60 — firmware
3
+ * fixture governance).
4
+ *
5
+ * Models `FirmwareExport` from `./types.ts` EXACTLY, with `.strict()` on
6
+ * every object so unknown keys fail parsing. This is the CI gate: when the
7
+ * firmware team lands a new versioned fixture, any key the mapper doesn't
8
+ * know about surfaces as a test failure — a PR-blocking event that forces a
9
+ * deliberate schema + mapper update instead of a silent drop.
10
+ *
11
+ * `FIRMWARE_EXPORT_KEY_PATHS` is the same contract expressed as a key-path
12
+ * set (`heartrate`, `heartrate[].timestamp`, ...) for the fixture key-diff
13
+ * test in `__tests__/schema.test.ts`. Keep both in lockstep with
14
+ * `FirmwareExport` — the key-diff test asserts the fixture corpus exercises
15
+ * exactly this set.
16
+ */
17
+ import { z } from 'zod';
18
+ /** Firmware timestamp: `"YYYY.MM.DD HH:MM:SS"` UTC wall-clock (see time.ts). */
19
+ const firmwareTimestamp = z.string().regex(/^\d{4}\.\d{1,2}\.\d{1,2} \d{1,2}:\d{1,2}:\d{1,2}$/, 'expected firmware "YYYY.MM.DD HH:MM:SS" timestamp');
20
+ export const firmwareHRRowSchema = z.object({
21
+ timestamp: firmwareTimestamp,
22
+ singleHR: z.number(),
23
+ }).strict();
24
+ export const firmwareHRVRowSchema = z.object({
25
+ timestamp: firmwareTimestamp,
26
+ hrv: z.number().optional(),
27
+ stress: z.number().optional(),
28
+ heartRate: z.number().optional(),
29
+ systolicBP: z.number().optional(),
30
+ diastolicBP: z.number().optional(),
31
+ vascularAging: z.number().optional(),
32
+ }).strict();
33
+ export const firmwareSpO2RowSchema = z.object({
34
+ timestamp: firmwareTimestamp,
35
+ automaticSpo2Data: z.number(),
36
+ }).strict();
37
+ export const firmwareTempRowSchema = z.object({
38
+ timestamp: firmwareTimestamp,
39
+ temperature: z.number(),
40
+ }).strict();
41
+ export const firmwareActivityRowSchema = z.object({
42
+ timestamp: firmwareTimestamp,
43
+ step: z.number(),
44
+ calories: z.number(),
45
+ distance: z.number(),
46
+ arraySteps: z.array(z.number()).length(10),
47
+ }).strict();
48
+ export const firmwareSleepRowSchema = z.object({
49
+ start: firmwareTimestamp,
50
+ end: firmwareTimestamp,
51
+ block_type: z.string(),
52
+ confidence: z.number(),
53
+ timestamp: firmwareTimestamp,
54
+ }).strict();
55
+ export const firmwareBatteryRowSchema = z.object({
56
+ timestamp: firmwareTimestamp,
57
+ battery: z.number(),
58
+ }).strict();
59
+ export const firmwareWeekdaysSchema = z.object({
60
+ sunday: z.boolean(),
61
+ monday: z.boolean(),
62
+ Tuesday: z.boolean(),
63
+ Wednesday: z.boolean(),
64
+ Thursday: z.boolean(),
65
+ Friday: z.boolean(),
66
+ Saturday: z.boolean(),
67
+ }).strict();
68
+ /**
69
+ * Mirrors `AutomaticMonitoring_J2301A` from the JStyle SDK. `dataType` is
70
+ * the firmware enum (1=heartRate, 2=spo2, 3=temperature, 4=HRV) and is
71
+ * translated away at the mapper boundary — see `mapper/ring-config.ts`.
72
+ */
73
+ export const firmwareMonitoringWindowSchema = z.object({
74
+ dataType: z.number().int(),
75
+ intervalTime: z.number().int().nonnegative(),
76
+ startTime_Hour: z.number().int(),
77
+ startTime_Minutes: z.number().int(),
78
+ endTime_Hour: z.number().int(),
79
+ endTime_Minutes: z.number().int(),
80
+ weeks: firmwareWeekdaysSchema,
81
+ mode: z.number().int().optional(),
82
+ }).strict();
83
+ export const firmwareRingConfigSchema = z.object({
84
+ automaticMonitoringData: z.array(firmwareMonitoringWindowSchema),
85
+ }).strict();
86
+ export const firmwareExportSchema = z.object({
87
+ // Fixture files carry a human-readable `$comment`; real device exports
88
+ // may omit it. Not part of `FirmwareExport` — stripped before mapping.
89
+ $comment: z.string().optional(),
90
+ heartrate: z.array(firmwareHRRowSchema),
91
+ hrv_table: z.array(firmwareHRVRowSchema),
92
+ spo2: z.array(firmwareSpO2RowSchema),
93
+ temperature_table: z.array(firmwareTempRowSchema),
94
+ activitydetails: z.array(firmwareActivityRowSchema),
95
+ sleep_processed: z.array(firmwareSleepRowSchema),
96
+ battery_table: z.array(firmwareBatteryRowSchema),
97
+ ring: firmwareRingConfigSchema,
98
+ }).strict();
99
+ const _assertParsedMatchesType = true;
100
+ void _assertParsedMatchesType;
101
+ /**
102
+ * Every key-path the schema knows about. Array elements are flattened as
103
+ * `section[].key`. Used by the fixture key-diff test (principle 60).
104
+ */
105
+ export const FIRMWARE_EXPORT_KEY_PATHS = new Set([
106
+ '$comment',
107
+ 'heartrate',
108
+ 'heartrate[].timestamp',
109
+ 'heartrate[].singleHR',
110
+ 'hrv_table',
111
+ 'hrv_table[].timestamp',
112
+ 'hrv_table[].hrv',
113
+ 'hrv_table[].stress',
114
+ 'hrv_table[].heartRate',
115
+ 'hrv_table[].systolicBP',
116
+ 'hrv_table[].diastolicBP',
117
+ 'hrv_table[].vascularAging',
118
+ 'spo2',
119
+ 'spo2[].timestamp',
120
+ 'spo2[].automaticSpo2Data',
121
+ 'temperature_table',
122
+ 'temperature_table[].timestamp',
123
+ 'temperature_table[].temperature',
124
+ 'activitydetails',
125
+ 'activitydetails[].timestamp',
126
+ 'activitydetails[].step',
127
+ 'activitydetails[].calories',
128
+ 'activitydetails[].distance',
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',
136
+ 'battery_table',
137
+ 'battery_table[].timestamp',
138
+ 'battery_table[].battery',
139
+ 'ring',
140
+ 'ring.automaticMonitoringData',
141
+ // AutomaticMonitoring_J2301A (JStyle SDK). `dataType` is the firmware
142
+ // enum — 1=heartRate, 2=spo2, 3=temperature, 4=HRV — translated away at
143
+ // the mapper boundary so it never reaches a warehouse column.
144
+ 'ring.automaticMonitoringData[].dataType',
145
+ 'ring.automaticMonitoringData[].intervalTime',
146
+ 'ring.automaticMonitoringData[].startTime_Hour',
147
+ 'ring.automaticMonitoringData[].startTime_Minutes',
148
+ 'ring.automaticMonitoringData[].endTime_Hour',
149
+ 'ring.automaticMonitoringData[].endTime_Minutes',
150
+ 'ring.automaticMonitoringData[].mode',
151
+ 'ring.automaticMonitoringData[].weeks',
152
+ 'ring.automaticMonitoringData[].weeks.sunday',
153
+ 'ring.automaticMonitoringData[].weeks.monday',
154
+ 'ring.automaticMonitoringData[].weeks.Tuesday',
155
+ 'ring.automaticMonitoringData[].weeks.Wednesday',
156
+ 'ring.automaticMonitoringData[].weeks.Thursday',
157
+ 'ring.automaticMonitoringData[].weeks.Friday',
158
+ 'ring.automaticMonitoringData[].weeks.Saturday',
159
+ ]);
160
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../../src/sync/mapper/schema.ts"],"names":[],"mappings":"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"}
@@ -2,31 +2,76 @@
2
2
  * Sleep reconstruction mapper (T-06).
3
3
  *
4
4
  * Firmware `sleep_processed` emits per-block rows tagged with a `start`
5
- * (session start), `end`, `block_type` (primary / light / deep / rem / ...),
6
- * `confidence` (0..1), and per-block `timestamp` (block instant). Multiple
7
- * blocks share the same `start` when they belong to the same sleep session.
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.
8
8
  *
9
9
  * Reconstruction rules (spec.md §Firmware mapper + tasks.md T-06):
10
10
  * - Group rows by `start`.
11
11
  * - A session is included if AT LEAST ONE block in the group has
12
12
  * `block_type === 'primary'` AND `confidence >= 0.7`. This filters
13
13
  * day-time naps and low-confidence noise.
14
- * - Session boundaries: `start_ts` = earliest block ts in the group;
15
- * `end_ts` = latest block ts + 1 minute (block ts marks start of the
16
- * 1-min block).
17
- * - `session_id` = stable hash of `user_id + start_ts.toISOString()`.
18
- * - `night_of` = 6pm-6pm rule applied to `start_ts` (see `time.ts`).
19
- * - Stages: one row per firmware block that passed the primary+confidence
20
- * filter, carrying `session_id`, `block_type`, and `confidence`.
21
- * - Raw: every input row is preserved verbatim in `sleep_raw` (payload
22
- * column) regardless of the primary/confidence filter, so downstream
23
- * reprocessing can revisit sessions with new heuristics.
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` = `nanoid(24) + '_' + fnv1a32hex(device_id | user_id |
19
+ * ts_session_start)` (principle 25, locked) collision-safe even with
20
+ * corrupted timestamps.
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.
30
+ *
31
+ * Principle 20 boundary: firmware `block_type` strings are translated to our
32
+ * integer codes here and never reach the warehouse.
24
33
  */
25
34
  import type { FirmwareSleepRow, MapperContext, SleepRawRow, SleepSessionRow, SleepStageRow } from './types';
35
+ /**
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.
47
+ */
48
+ export declare const SLEEP_STAGE_CODES: Readonly<Record<string, number>>;
49
+ /** Reverse lookup for the stage-minute accumulator + diagnostics. */
50
+ export declare const SLEEP_STAGE_NAMES: Readonly<Record<number, string>>;
26
51
  export interface ReconstructSleepResult {
27
52
  sleep_session: SleepSessionRow[];
28
53
  sleep_stage: SleepStageRow[];
29
54
  sleep_raw: SleepRawRow[];
30
55
  }
31
56
  export declare function reconstructSleepSessions(rows: readonly FirmwareSleepRow[], ctx: MapperContext): ReconstructSleepResult;
57
+ /**
58
+ * Session id components (principle 25, locked):
59
+ * `nanoid(24) + '_' + fnv1a32hex(device_id | user_id | ts_session_start)`
60
+ *
61
+ * The random prefix makes ids collision-safe even when the firmware emits
62
+ * corrupted/duplicated timestamps; the hash suffix is deterministic over the
63
+ * identifying tuple so ids remain diagnosable (same device+user+start ⇒
64
+ * same suffix). The random source is injectable via `ctx.idGenerator` to
65
+ * keep the mapper testable — the suffix is always deterministic.
66
+ */
67
+ export declare function sessionIdComponents(ctx: MapperContext, tsSessionStart: Date): {
68
+ random: string;
69
+ hash: string;
70
+ };
71
+ /**
72
+ * FNV-1a 32-bit, hex-encoded (8 lowercase hex chars). Implemented inline —
73
+ * a dependency for 10 lines of arithmetic is not worth the supply-chain
74
+ * surface, and the mapper only needs a stable non-cryptographic digest.
75
+ */
76
+ export declare function fnv1a32hex(input: string): string;
32
77
  //# sourceMappingURL=sleep.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/sleep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACd,MAAM,SAAS,CAAA;AAMhB,MAAM,WAAW,sBAAsB;IACrC,aAAa,EAAE,eAAe,EAAE,CAAA;IAChC,WAAW,EAAE,aAAa,EAAE,CAAA;IAC5B,SAAS,EAAE,WAAW,EAAE,CAAA;CACzB;AAED,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,SAAS,gBAAgB,EAAE,EACjC,GAAG,EAAE,aAAa,GACjB,sBAAsB,CAyExB"}
1
+ {"version":3,"file":"sleep.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/sleep.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAIH,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,eAAe,EACf,aAAa,EACd,MAAM,SAAS,CAAA;AAMhB;;;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,CAgGxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,aAAa,EAClB,cAAc,EAAE,IAAI,GACnB;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAMlC;AAOD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD"}
@@ -2,43 +2,98 @@
2
2
  * Sleep reconstruction mapper (T-06).
3
3
  *
4
4
  * Firmware `sleep_processed` emits per-block rows tagged with a `start`
5
- * (session start), `end`, `block_type` (primary / light / deep / rem / ...),
6
- * `confidence` (0..1), and per-block `timestamp` (block instant). Multiple
7
- * blocks share the same `start` when they belong to the same sleep session.
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.
8
8
  *
9
9
  * Reconstruction rules (spec.md §Firmware mapper + tasks.md T-06):
10
10
  * - Group rows by `start`.
11
11
  * - A session is included if AT LEAST ONE block in the group has
12
12
  * `block_type === 'primary'` AND `confidence >= 0.7`. This filters
13
13
  * day-time naps and low-confidence noise.
14
- * - Session boundaries: `start_ts` = earliest block ts in the group;
15
- * `end_ts` = latest block ts + 1 minute (block ts marks start of the
16
- * 1-min block).
17
- * - `session_id` = stable hash of `user_id + start_ts.toISOString()`.
18
- * - `night_of` = 6pm-6pm rule applied to `start_ts` (see `time.ts`).
19
- * - Stages: one row per firmware block that passed the primary+confidence
20
- * filter, carrying `session_id`, `block_type`, and `confidence`.
21
- * - Raw: every input row is preserved verbatim in `sleep_raw` (payload
22
- * column) regardless of the primary/confidence filter, so downstream
23
- * reprocessing can revisit sessions with new heuristics.
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` = `nanoid(24) + '_' + fnv1a32hex(device_id | user_id |
19
+ * ts_session_start)` (principle 25, locked) collision-safe even with
20
+ * corrupted timestamps.
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.
30
+ *
31
+ * Principle 20 boundary: firmware `block_type` strings are translated to our
32
+ * integer codes here and never reach the warehouse.
24
33
  */
34
+ import { nanoid } from 'nanoid';
25
35
  import { computeNightOf, parseTimestamp } from './time';
26
36
  const MINUTE_MS = 60000;
27
37
  const PRIMARY = 'primary';
28
38
  const CONFIDENCE_FLOOR = 0.7;
39
+ /**
40
+ * Block width in minutes when the firmware revision omits `unit_length`.
41
+ * Firmware doc §Sleep processing: blocks are 1-min wide by convention.
42
+ */
43
+ export const DEFAULT_BLOCK_MINUTES = 1;
44
+ /**
45
+ * Firmware `block_type` → `sleep_stage.stage` code (spec §Table schema:
46
+ * `stage SMALLINT NOT NULL -- 1=awake, 2=light, 3=deep, 5=rem`).
47
+ *
48
+ * `primary` is deliberately absent: it marks the session envelope, not a
49
+ * sleep stage. The enum is intentionally sparse (no 4) to match the DDL
50
+ * comment verbatim.
51
+ */
52
+ export const SLEEP_STAGE_CODES = Object.freeze({
53
+ awake: 1,
54
+ light: 2,
55
+ deep: 3,
56
+ rem: 5,
57
+ });
58
+ /** Reverse lookup for the stage-minute accumulator + diagnostics. */
59
+ export const SLEEP_STAGE_NAMES = Object.freeze({
60
+ 1: 'awake',
61
+ 2: 'light',
62
+ 3: 'deep',
63
+ 5: 'rem',
64
+ });
65
+ /** Per-block width, honouring a firmware-supplied `unit_length`. */
66
+ function blockMinutes(row) {
67
+ return typeof row.unit_length === 'number' && row.unit_length > 0
68
+ ? row.unit_length
69
+ : DEFAULT_BLOCK_MINUTES;
70
+ }
71
+ /**
72
+ * `sleep_raw.quality` is NOT NULL. Firmware revisions that carry a native
73
+ * `quality` pass it through verbatim; older revisions derive it from the
74
+ * block confidence (0..1 → 0..100) so the column is always populated.
75
+ */
76
+ function rawQuality(row) {
77
+ return typeof row.quality === 'number'
78
+ ? row.quality
79
+ : Math.round(row.confidence * 100);
80
+ }
29
81
  export function reconstructSleepSessions(rows, ctx) {
30
82
  const sleep_session = [];
31
83
  const sleep_stage = [];
32
84
  const sleep_raw = [];
33
- // Raw pass — all input rows preserved verbatim for reprocessing.
85
+ // Raw pass — every input row preserved for reprocessing, including rows
86
+ // from sessions the primary/confidence filter drops.
34
87
  for (const row of rows) {
35
88
  sleep_raw.push({
36
89
  ts: parseTimestamp(row.timestamp),
90
+ ts_session_start: parseTimestamp(row.start),
37
91
  brand: ctx.brand,
38
92
  family_id: ctx.familyId,
39
93
  user_id: ctx.userId,
40
94
  device_id: ctx.deviceId,
41
- payload: row,
95
+ quality: rawQuality(row),
96
+ unit_length: typeof row.unit_length === 'number' ? row.unit_length : null,
42
97
  });
43
98
  }
44
99
  // Group by `start`.
@@ -56,48 +111,90 @@ export function reconstructSleepSessions(rows, ctx) {
56
111
  const hasQualifyingPrimary = blocks.some(b => b.block_type === PRIMARY && b.confidence >= CONFIDENCE_FLOOR);
57
112
  if (!hasQualifyingPrimary)
58
113
  continue;
59
- const blockInstants = blocks.map(b => parseTimestamp(b.timestamp));
60
- const startTs = new Date(Math.min(...blockInstants.map(d => d.getTime())));
61
- // Sessions extend to the end of the last block (blocks are 1-min wide by
62
- // convention firmware doc §Sleep processing).
63
- const endTs = new Date(Math.max(...blockInstants.map(d => d.getTime())) + MINUTE_MS);
64
- const nightOf = computeNightOf(startTs, ctx.userTimezone);
65
- const sessionId = makeSessionId(ctx.userId, startTs);
66
- sleep_session.push({
67
- ts: startTs,
68
- brand: ctx.brand,
69
- family_id: ctx.familyId,
70
- user_id: ctx.userId,
71
- device_id: ctx.deviceId,
72
- session_id: sessionId,
73
- start_ts: startTs,
74
- end_ts: endTs,
75
- night_of: nightOf,
76
- });
77
- for (let i = 0; i < blocks.length; i++) {
78
- const b = blocks[i];
114
+ // Envelope from the firmware's own session fields. `end` is exclusive of
115
+ // nothing it is the session end instant, so total is a plain delta.
116
+ const tsStart = parseTimestamp(startKey);
117
+ const tsEnd = parseTimestamp(blocks[0].end);
118
+ const totalMinutes = Math.max(0, Math.round((tsEnd.getTime() - tsStart.getTime()) / MINUTE_MS));
119
+ const nightOf = computeNightOf(tsStart, ctx.userTimezone);
120
+ const sessionId = makeSessionId(ctx, tsStart);
121
+ // Stage rows + per-stage minute accumulation in one pass.
122
+ const stageMinutes = {
123
+ awake: 0,
124
+ light: 0,
125
+ deep: 0,
126
+ rem: 0,
127
+ };
128
+ let confidenceSum = 0;
129
+ for (const b of blocks) {
130
+ confidenceSum += b.confidence;
131
+ const code = SLEEP_STAGE_CODES[b.block_type];
132
+ if (code === undefined) {
133
+ // `primary` (envelope marker) and any unrecognised firmware block
134
+ // type. Not a stage — preserved in sleep_raw, skipped here.
135
+ continue;
136
+ }
137
+ stageMinutes[SLEEP_STAGE_NAMES[code]] += blockMinutes(b);
79
138
  sleep_stage.push({
80
- ts: blockInstants[i],
139
+ ts: parseTimestamp(b.timestamp),
81
140
  brand: ctx.brand,
82
141
  family_id: ctx.familyId,
83
142
  user_id: ctx.userId,
84
143
  device_id: ctx.deviceId,
85
144
  session_id: sessionId,
86
- stage: b.block_type,
145
+ stage: code,
87
146
  confidence: b.confidence,
88
147
  });
89
148
  }
90
- // Reference `startKey` to silence "unused" if TS ever tightens loop typing.
91
- void startKey;
149
+ sleep_session.push({
150
+ brand: ctx.brand,
151
+ family_id: ctx.familyId,
152
+ user_id: ctx.userId,
153
+ device_id: ctx.deviceId,
154
+ session_id: sessionId,
155
+ ts_start: tsStart,
156
+ ts_end: tsEnd,
157
+ total_minutes: totalMinutes,
158
+ deep_minutes: stageMinutes.deep,
159
+ rem_minutes: stageMinutes.rem,
160
+ light_minutes: stageMinutes.light,
161
+ awake_minutes: stageMinutes.awake,
162
+ avg_confidence: blocks.length > 0 ? confidenceSum / blocks.length : null,
163
+ night_of: nightOf,
164
+ });
92
165
  }
93
166
  return { sleep_session, sleep_stage, sleep_raw };
94
167
  }
95
168
  /**
96
- * Stable, non-cryptographic session id: `<userId>:<startIso>`. Deterministic
97
- * so re-imports don't produce duplicates; carries enough context to survive
98
- * cross-user joins without collision at the user's cardinality.
169
+ * Session id components (principle 25, locked):
170
+ * `nanoid(24) + '_' + fnv1a32hex(device_id | user_id | ts_session_start)`
171
+ *
172
+ * The random prefix makes ids collision-safe even when the firmware emits
173
+ * corrupted/duplicated timestamps; the hash suffix is deterministic over the
174
+ * identifying tuple so ids remain diagnosable (same device+user+start ⇒
175
+ * same suffix). The random source is injectable via `ctx.idGenerator` to
176
+ * keep the mapper testable — the suffix is always deterministic.
177
+ */
178
+ export function sessionIdComponents(ctx, tsSessionStart) {
179
+ const random = ctx.idGenerator ? ctx.idGenerator() : nanoid(24);
180
+ const hash = fnv1a32hex(`${ctx.deviceId}|${ctx.userId}|${tsSessionStart.toISOString()}`);
181
+ return { random, hash };
182
+ }
183
+ function makeSessionId(ctx, startTs) {
184
+ const { random, hash } = sessionIdComponents(ctx, startTs);
185
+ return `${random}_${hash}`;
186
+ }
187
+ /**
188
+ * FNV-1a 32-bit, hex-encoded (8 lowercase hex chars). Implemented inline —
189
+ * a dependency for 10 lines of arithmetic is not worth the supply-chain
190
+ * surface, and the mapper only needs a stable non-cryptographic digest.
99
191
  */
100
- function makeSessionId(userId, startTs) {
101
- return `${userId}:${startTs.toISOString()}`;
192
+ export function fnv1a32hex(input) {
193
+ let h = 0x811C9DC5;
194
+ for (let i = 0; i < input.length; i++) {
195
+ h ^= input.charCodeAt(i);
196
+ h = Math.imul(h, 0x01000193) >>> 0;
197
+ }
198
+ return h.toString(16).padStart(8, '0');
102
199
  }
103
200
  //# sourceMappingURL=sleep.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../../src/sync/mapper/sleep.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../../src/sync/mapper/sleep.ts"],"names":[],"mappings":"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"}