@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
package/README.md CHANGED
@@ -6,16 +6,29 @@ Per-family (or per-tenant) warehouses attached as DuckDB catalogs. Zero-copy
6
6
  reads against R2 Iceberg tables. Headless core with optional React hooks and
7
7
  UI subpaths.
8
8
 
9
- Status: **0.3.0** — Phases 1 7 landed on the core engine plus the full
10
- `@mongrov/analytics/sync` subpath (mapper buffer flusher watermark →
11
- pusher fetcher scheduler factory + hooks). T-18 / T-28 integration
12
- suites green against the MinIO + Iceberg REST docker stack (10 cases + 3
13
- round trips). 0.3.0 additionally lands SCD-2 `device_config` support:
14
- `RingConfigTranslator`, `PendingClosesStore`, `R2Pusher.pushCloses`, and
15
- a rewritten sink that closes prior configs both locally and against the
16
- remote Iceberg zone. **Breaking**: `DeviceConfigRow` shape changed
17
- (`metric` `data_type`, `start_hour`/`end_hour` `start_time`/`end_time`,
18
- added `weeks`) — see the `ringConfigTranslator` section below.
9
+ Status: **0.7.0** — Sprint 5 hardening. Core engine, `/sync`, `/rules`,
10
+ `/tools` and `/tools/mcp` subpaths all landed; 727 tests green, plus the
11
+ T-18 / T-28 integration suites against the MinIO + Iceberg REST docker
12
+ stack.
13
+
14
+ 0.7.0 lands: `icu` extension, `v_{table}` union views (local-unpushed +
15
+ remote), `user_baseline` with day-first quantiles, `batch:complete` batch
16
+ lifecycle, rules `context` / `consecutive` / `user_setting` targets, the
17
+ `getSpO2` tool, and copy guardrails on every formatter.
18
+
19
+ **Breaking** — see [UPGRADING.md](./UPGRADING.md) for rewrites. Headlines:
20
+ mapped sleep row shapes now match the DDL (they did not before, and sleep
21
+ flushes were writing NULLs into NOT NULL columns); `AnalyticsEngine` gained
22
+ `getFamilyMembers()` and `dismissInsight()`; rule and tool SQL now reads
23
+ `v_{table}` views, so an attach is required; rules evaluate once per batch
24
+ rather than per table.
25
+
26
+ > Historical note (0.3.0): SCD-2 `device_config` support inverted the
27
+ > ring-config mapping direction — `DeviceConfigRow` moved from `metric` to
28
+ > `data_type`, with a consumer-supplied `RingConfigTranslator` producing the
29
+ > firmware enum from our metric names. Sprint 5 §2 intended to reverse this
30
+ > (`metric VARCHAR`), but the change is **blocked**: the spec's firmware
31
+ > codes do not match the real JStyle codes. See UPGRADING §12.
19
32
 
20
33
  ## Features
21
34
 
package/UPGRADING.md ADDED
@@ -0,0 +1,456 @@
1
+ # Upgrading
2
+
3
+ ## 0.7.0 → 0.8.0 — `device_config` firmware contract (Sprint 5 §2, T-02/T-09/T-10)
4
+
5
+ **Breaking, and it changes both a table and a mapper input type.**
6
+
7
+ ### `device_config.data_type` → `device_config.metric`
8
+
9
+ Principle 21 says firmware enums must not reach the schema. The table now
10
+ stores our metric id:
11
+
12
+ ```diff
13
+ - data_type INTEGER NOT NULL,
14
+ - PRIMARY KEY (device_id, data_type, valid_from)
15
+ + metric VARCHAR NOT NULL,
16
+ + PRIMARY KEY (device_id, metric, valid_from)
17
+ ```
18
+
19
+ Migration **v5** converts existing rows using the JStyle automatic-monitoring
20
+ enum. Unknown codes are preserved as `unknown_<n>` rather than dropped, so a
21
+ firmware revision that added one stays diagnosable. Local catalog only — the
22
+ client never destructively rewrites the attached Iceberg zone.
23
+
24
+ **Update your `columnOrder`** for `device_config`: `data_type` → `metric`.
25
+
26
+ ### `FirmwareMonitoringWindow` now matches the vendor struct
27
+
28
+ The previous type matched no vendor type — it had already discarded
29
+ `dataType`, which is why consumers had to supply a `RingConfigTranslator`
30
+ inventing the enum back from our own metric names.
31
+
32
+ ```diff
33
+ -{ metric: string, interval_minutes: number, start_hour: number, end_hour: number }
34
+ +{ dataType: number, intervalTime: number,
35
+ + startTime_Hour: number, startTime_Minutes: number,
36
+ + endTime_Hour: number, endTime_Minutes: number,
37
+ + weeks: { sunday: boolean, monday: boolean, Tuesday: boolean, ... },
38
+ + mode?: number }
39
+ ```
40
+
41
+ Verbatim from `AutomaticMonitoring_J2301A` (JStyle J2301A SDK). `dataType`
42
+ is **1 = heartRate, 2 = spo2, 3 = temperature, 4 = HRV**.
43
+
44
+ Note this is a **1:1 map**. The Sprint 5 spec described `dataType = 2` as
45
+ "spo2 + temperature (shared schedule)" needing a fan-out to two rows; the
46
+ SDK does not agree — temperature has its own code 3, so there is nothing to
47
+ fan out.
48
+
49
+ ### New: `createSamplingResolver` (principle 22)
50
+
51
+ Chart rendering, gap detection and rule windows should derive cadence from
52
+ `device_config.interval_minutes`, not from constants — so a firmware
53
+ upgrade that changes sampling rate propagates without an app release.
54
+
55
+ ```ts
56
+ import { createSamplingResolver } from '@mongrov/analytics'
57
+
58
+ const sampling = createSamplingResolver({ analytics })
59
+ const { minutes, source } = await sampling.resolve('spo2', deviceId)
60
+ // source: 'device_config' when the ring reported a schedule,
61
+ // 'metric_metadata' before the first sync
62
+ ```
63
+
64
+ Memoized per `(deviceId, metric)` with a 10-minute TTL; call
65
+ `sampling.invalidate(deviceId)` after a sync that wrote a new config row.
66
+ Omitting `deviceId` returns the metadata fallback with no query, which is
67
+ what register-time rule validation uses.
68
+
69
+ `metric_metadata.sampling_minutes` stays as the fallback — it is what we
70
+ know before a device has ever reported.
71
+
72
+ ### `ringConfigTranslator` is deprecated and ignored
73
+
74
+ The mapper reads the vendor struct directly and owns the translation.
75
+
76
+ ```diff
77
+ createSyncManager({
78
+ …,
79
+ - ringConfigTranslator: myTranslator, // now ignored; delete it
80
+ })
81
+ ```
82
+
83
+ It is no longer required when `tables` includes `device_config` — the Zod
84
+ refinement enforcing that is gone. The type is still exported for one
85
+ release so existing call sites compile.
86
+
87
+ ---
88
+
89
+ ## 0.6.0 → 0.7.0 (Sprint 5 coordinated release)
90
+
91
+ This is a **coordinated release**. Upgrade all four together — the packages
92
+ share contracts that changed in lockstep:
93
+
94
+ | Package | From | To |
95
+ |---|---|---|
96
+ | `@mongrov/analytics` | 0.6.0 | **0.7.0** |
97
+ | `@mongrov/data-access` | 0.1.0 | **0.2.0** |
98
+ | `@mongrov/types` | 0.4.0 | **0.5.0** |
99
+ | `@mongrov/device` | 0.1.1 | **0.2.0** |
100
+
101
+ > The Sprint 5 spec named these 0.2.0 / 0.2.0 / 0.4.0 / 0.2.0. Those targets
102
+ > were unreachable: analytics had already shipped 0.2.0→0.6.0 across five
103
+ > post-Sprint-4 releases, and types had reached 0.4.0 without ever landing
104
+ > the Sprint 3 §18 content. Versions are next-minor-from-actual.
105
+
106
+ ---
107
+
108
+ ## 1. Breaking: mapped sleep row shapes
109
+
110
+ **Who is affected:** anyone who consumes `MappedBatch` directly, supplies
111
+ `columnOrder` for sleep tables, or has snapshot tests over mapper output.
112
+
113
+ The sleep mapper had drifted from the DDL it writes to and was emitting rows
114
+ the `sleep_session` table could not accept. If you were flushing sleep data,
115
+ it was failing (or writing NULLs into NOT NULL columns) — this release fixes
116
+ that, and the row types changed as a result.
117
+
118
+ ```diff
119
+ interface SleepSessionRow {
120
+ session_id: string
121
+ - ts: Date // no such column; DDL partitions on day(ts_start)
122
+ - start_ts: Date
123
+ - end_ts: Date
124
+ + ts_start: Date
125
+ + ts_end: Date
126
+ + total_minutes: number // NOT NULL — was never produced
127
+ + deep_minutes: number | null
128
+ + rem_minutes: number | null
129
+ + light_minutes: number | null
130
+ + awake_minutes: number | null
131
+ + avg_confidence: number | null
132
+ night_of: Date
133
+ }
134
+
135
+ interface SleepStageRow {
136
+ - stage: string // firmware block_type leaked into an INTEGER column
137
+ + stage: number // 1=awake, 2=light, 3=deep, 5=rem
138
+ - confidence: number
139
+ + confidence: number | null
140
+ }
141
+
142
+ interface SleepRawRow {
143
+ - payload: FirmwareSleepRow // no such column
144
+ + ts_session_start: Date
145
+ + quality: number // NOT NULL; derived from confidence when
146
+ + unit_length: number | null // firmware omits it
147
+ }
148
+ ```
149
+
150
+ Two smaller removals in the same pass — both were keys with no column:
151
+
152
+ ```diff
153
+ -interface ActivityBucketRow { steps: number; … } // steps live on `activity`
154
+ -interface DeviceConfigRow { ts: Date; … } // axis is valid_from/valid_to
155
+ ```
156
+
157
+ **Migrating `columnOrder`:** if you hand-wrote sleep entries, regenerate them
158
+ from the DDL. Column names now match exactly, so
159
+ `columnOrder[table].map(c => row[c])` works without translation.
160
+
161
+ **Behaviour changes worth knowing:**
162
+
163
+ - Session boundaries come from the firmware's own `start`/`end` fields, not
164
+ min/max of block instants. Deriving from blocks under-reported any session
165
+ whose first or last minutes went unclassified.
166
+ - `primary` blocks produce **no** `sleep_stage` row. The DDL enum has no code
167
+ for them and they read as the session envelope rather than a stage. All
168
+ blocks are still preserved in `sleep_raw`. *Open question with the firmware
169
+ team — see `analytics-sync/tasks.md` T-06.*
170
+
171
+ A new contract test (`mapper/__tests__/ddl-contract.test.ts`) parses
172
+ `core/schemas.ts` and asserts every mapped row's keys are real columns and
173
+ every NOT NULL column is populated. If you extend the mapper, it will tell
174
+ you when you drift.
175
+
176
+ ---
177
+
178
+ ## 2. Breaking: `AnalyticsEngine` gained two methods
179
+
180
+ **Who is affected:** anyone implementing `AnalyticsEngine` — test fakes,
181
+ custom adapters.
182
+
183
+ ```diff
184
+ interface AnalyticsEngine {
185
+
186
+ + getFamilyMembers(): Promise<string[]>
187
+ + dismissInsight(args: { insightId: string; userId: string }): Promise<void>
188
+ }
189
+ ```
190
+
191
+ `getFamilyMembers()` closes a principle-39 gap: the rules evaluator and the
192
+ AI-tools authorize hook each held their own wire to family membership. Now
193
+ both go through the engine, which delegates to the `familyMembersProvider`
194
+ you configured at `createAnalytics` and caches for 60s.
195
+
196
+ Minimal fake:
197
+
198
+ ```ts
199
+ const engine: AnalyticsEngine = {
200
+ …,
201
+ async getFamilyMembers() { return ['alice', 'bob'] },
202
+ async dismissInsight() {},
203
+ }
204
+ ```
205
+
206
+ ---
207
+
208
+ ## 3. Breaking: authorize hooks no longer fall back to SQL
209
+
210
+ `familyScopeAuthorize` / `orgScopeAuthorize` previously fell back to
211
+ `SELECT 1 FROM family_member …` when no provider was passed. **No DDL in this
212
+ package ever created that table**, so the fallback could only throw and fail
213
+ closed — silently denying every cross-member query on any install that did
214
+ not pass a provider explicitly.
215
+
216
+ ```diff
217
+ -familyScopeAuthorize(analytics) // used a phantom table
218
+ +familyScopeAuthorize(analytics) // uses engine roster
219
+ familyScopeAuthorize(analytics, { familyMembersProvider }) // still wins
220
+ ```
221
+
222
+ If you relied on the SQL path, wire `familyMembersProvider` at
223
+ `createAnalytics` instead. Membership lives in the RxDB Family doc, not
224
+ DuckDB.
225
+
226
+ ---
227
+
228
+ ## 4. Breaking: generated SQL reads `v_{table}` views
229
+
230
+ Rule SQL and tool SQL now target the union views instead of raw catalogs.
231
+ This is what lets a rule fire on data flushed locally but not yet pushed to
232
+ R2 — i.e. this morning's sync.
233
+
234
+ **Consequence:** the views must exist. They are created on `attach()` and
235
+ dropped on `detach()`, so anything executing rule or tool SQL against a
236
+ non-attached engine now fails where it previously read stale remote rows.
237
+
238
+ View bodies inline `brand` and `family_id`, because DuckDB views cannot bind
239
+ parameters. That makes per-attach recreation a **correctness** requirement,
240
+ not housekeeping — a brand switch reusing old views would serve the previous
241
+ tenant's rows. Handled automatically; do not cache view DDL yourself.
242
+
243
+ `insight` is deliberately *not* viewed — it is local-authoritative.
244
+
245
+ ---
246
+
247
+ ## 5. Behaviour change: rules evaluate on `batch:complete`
248
+
249
+ Rules previously evaluated once per table, on each `{table}:insert`. They now
250
+ evaluate once per batch. **Default is the new behaviour.**
251
+
252
+ This fixes a real race: a `context: 'asleep'` rule JOINs `v_sleep_session`,
253
+ and firing it the moment `spo2` flushed let it evaluate against a night whose
254
+ sleep rows were still buffered.
255
+
256
+ ```ts
257
+ createSyncManager({
258
+ …,
259
+ strictBatchOrdering: false, // opt out — restores per-table triggering
260
+ })
261
+ ```
262
+
263
+ Only opt out if you need lower evaluation latency **and** ship no
264
+ context-JOIN or `consecutive` rules.
265
+
266
+ Per-table `{table}:insert` events still fire — they drive cache
267
+ invalidation, which wants to refresh as early as possible. `batch:complete`
268
+ is additional, not a replacement.
269
+
270
+ ---
271
+
272
+ ## 6. New config: `userTimezoneProvider`
273
+
274
+ Day-first baselines bucket by **local** day, so they need each user's IANA
275
+ zone. `User.timezone` lives in auth/RxDB where this package cannot reach it.
276
+
277
+ ```ts
278
+ createSyncManager({
279
+ …,
280
+ userTimezoneProvider: async (userId) => (await getUser(userId))?.timezone,
281
+ })
282
+ ```
283
+
284
+ Unwired, it falls back to the device zone — wrong for a travelling user or
285
+ one whose profile zone differs from their device. Baselines still compute;
286
+ they just bucket by the wrong midnight.
287
+
288
+ ---
289
+
290
+ ## 7. Rules schema additions
291
+
292
+ Additive — existing rules keep parsing. New fields:
293
+
294
+ ```toml
295
+ context = "asleep" # 'any' (default) | 'asleep' | 'resting'
296
+ consecutive = 3 # require N adjacent breaching samples
297
+
298
+ [rule.target]
299
+ type = "user_setting" # threshold read from KVStore at eval time
300
+ key = "user:spo2SafeLevel"
301
+ defaultValue = 90
302
+ ```
303
+
304
+ `user_setting` is what makes a user-configurable threshold work without
305
+ per-user rule rows: the compiled SQL is threshold-agnostic, and the evaluator
306
+ binds `analytics:{userId}:{key}` at eval time, falling back to `defaultValue`.
307
+
308
+ Two constraints the validator now enforces:
309
+
310
+ - `consecutive` is **rejected with baseline targets**. Baselines resolve
311
+ per-window, not per-sample; there is no correct query shape, so it throws
312
+ rather than emitting something subtly wrong.
313
+ - `consecutive × sampling_minutes` must fit the window, or the rule could
314
+ never fire.
315
+
316
+ **Ziva catalog rule ids changed.** Throttle state is keyed by rule id, so
317
+ existing throttle counters for the renamed rules are orphaned (they expire
318
+ naturally):
319
+
320
+ | Was | Now |
321
+ |---|---|
322
+ | `ziva.sleep-deprivation-3d` | `ziva.sleep-deprivation-3` |
323
+ | `ziva.stress-elevated` | `ziva.stress-elevated-day` |
324
+ | `ziva.activity-low-24h` | `ziva.low-activity-week` |
325
+
326
+ `ziva.hrv-drop-30` and `ziva.stress-elevated-day` also switched from
327
+ `absolute` to `baseline_percent` / `baseline_stddev` targets — a fixed HRV
328
+ threshold is meaningless across users.
329
+
330
+ Also note `registry.register()` is **async**. An un-awaited call silently
331
+ no-ops:
332
+
333
+ ```diff
334
+ -rules.register(zivaDefaults)
335
+ +await rules.register(zivaDefaults)
336
+ ```
337
+
338
+ ---
339
+
340
+ ## 8. Copy guardrails — contract for tool authors
341
+
342
+ Every tool formatter must call `assertNoBanTerms(text, formatterName)` on its
343
+ return path. Tool output lands in an LLM's context, and the model repeats
344
+ whatever register it finds there.
345
+
346
+ ```ts
347
+ import { assertNoBanTerms } from '@mongrov/analytics/tools'
348
+
349
+ function finalize(text: string, rowCount: number): ToolResult {
350
+ assertNoBanTerms(text, 'myTool')
351
+ return { text, rowCount, bytes: formatBytes(text) }
352
+ }
353
+ ```
354
+
355
+ It throws `FormatterCopyError` on any banned medical term. Matching is
356
+ leading-boundary, so `desaturation` also catches `desaturations` — over-
357
+ blocking is the right bias here. `applyPreferredLanguage(text)` rewrites the
358
+ common phrasings, but it is a convenience, not a substitute: the guard still
359
+ runs afterwards.
360
+
361
+ This applies to text you did not author. An insight row titled *"Nocturnal
362
+ desaturation detected"* interpolated into `getInsights` output fails the tool
363
+ call — which is the point.
364
+
365
+ ---
366
+
367
+ ## 9. `@mongrov/data-access` 0.2.0
368
+
369
+ `RequestContext` shape:
370
+
371
+ ```diff
372
+ interface RequestContext {
373
+ - requesterUserId: string
374
+ + userId: string // canonical — queries bind it as $userId
375
+ brand: string
376
+ familyId: string
377
+ + timezone: string // bound as $tz by every day-grouped query
378
+ now: () => Date
379
+ }
380
+ ```
381
+
382
+ Both are required. Update your `DataAccessProvider` context factory.
383
+
384
+ The remaining 0.2.0 items (`asyncFetch`, `fetching` hook state,
385
+ JOIN-invalidation validator, `transform` field, `MutationContext.analytics`)
386
+ were already implemented before this release; 0.2.0 records the version.
387
+
388
+ ---
389
+
390
+ ## 10. `@mongrov/types` 0.5.0
391
+
392
+ Additive. Lands the Sprint 3 §18 layer every Sprint 4 feature was written
393
+ against but which never shipped: `Brand`, `Family`, `User`,
394
+ `AnalyticsDevice`, `SensorSink`, `FirmwareExport`, `MapperContext`,
395
+ `EventBus`, and the seven missing `AnalyticsQuerySchemas`.
396
+
397
+ **`AnalyticsDevice`, not `Device`.** `types/device.ts` already exports a
398
+ `Device` describing the BLE transport's view (adapterId, connection state,
399
+ RSSI). Both are legitimate and neither is a superset — one is the durable
400
+ tenancy record sensor rows key against, the other is the live connection
401
+ handle. The existing `Device` is unchanged.
402
+
403
+ `DEVICE_EVENT_TYPES` and its payload schemas now live here canonically
404
+ (`@mongrov/types/device-events`); `@mongrov/analytics` re-exports them. This
405
+ is what lets `@mongrov/device` emit events the analytics engine will store
406
+ without depending on the engine.
407
+
408
+ **Packaging change: dual CJS/ESM emit.** `dist/` is now `dist/esm` +
409
+ `dist/cjs`, each with its own `type` marker, and every export declares
410
+ `import` / `require` / `types` conditions. Previously the package emitted
411
+ ESM only and declared no `require` condition — which nothing noticed,
412
+ because every cross-package import was type-only and therefore erased at
413
+ compile. The first *value* import from a subpath (`device-events`,
414
+ `analytics-queries`) failed to resolve under CommonJS.
415
+
416
+ If you deep-imported `@mongrov/types/dist/...` — don't; use the subpath
417
+ exports. `main` / `module` / `types` now point into the split trees.
418
+
419
+ ---
420
+
421
+ ## 11. Migrations
422
+
423
+ One new migration runs automatically on attach:
424
+
425
+ | Version | What |
426
+ |---|---|
427
+ | v4 | `user_baseline` table (local catalog only) |
428
+
429
+ **Numbering note.** The Sprint 5 spec called this "migration v3" and
430
+ specified a "v4" adding `insight.dismissed_at`. Versions 2 and 3 were already
431
+ spent (`device_battery`, `insight` v2 rebuild), and `dismissed_at` already
432
+ ships in the baseline DDL with migration v3 backfilling it — so the ALTER
433
+ would have been a no-op against an existing column.
434
+
435
+ `user_baseline` and `sync_watermark` are **local-only** — never created in
436
+ the attached R2 zone. Baselines are derived data, cheap to recompute, and
437
+ pushing them would invite two devices in one family to race on the same
438
+ composite PK.
439
+
440
+ ---
441
+
442
+ ## 12. Known gaps in this release
443
+
444
+ Documented so you do not go looking:
445
+
446
+ - **`device_config` still uses `data_type SMALLINT`.** Sprint 5 §2's change
447
+ to `metric VARCHAR` is blocked: the spec's firmware codes (1/2/4) do not
448
+ match the real JStyle codes (`HR:0, SLEEP:1, SPO2:3, HRV:8, ACTIVITY:9,
449
+ TEMPERATURE:14`), and SpO₂/temperature are separate schedules — so the
450
+ "fan-out `data_type=2` into two rows" the task describes may not correspond
451
+ to real hardware behaviour. Unresolved with the firmware team.
452
+ - **`@mongrov/device` sync events are emitter-only.** `createSyncEventEmitter`
453
+ and the `DeviceEventSink` port ship and are usable stand-alone, but the sync
454
+ machine that would call them is D5 work and does not exist yet.
455
+ - **`transform` is declared but not resolved** by the data-access dispatcher.
456
+ Queries declaring it will not produce derived output fields yet.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `device_event.event_type` enum + per-type payload schemas (Sprint 5 §6).
3
+ *
4
+ * **Re-export only.** The canonical definition lives in
5
+ * `@mongrov/types/device-events` so that `@mongrov/device` — which must
6
+ * never depend on the analytics engine — can emit events this engine will
7
+ * store, against the same contract.
8
+ *
9
+ * Kept as a module here so analytics consumers get one import surface, and
10
+ * so the `core/index.ts` export list stays stable.
11
+ */
12
+ export { decodeDeviceEventPayload, DEVICE_EVENT_PAYLOAD_SCHEMAS, DEVICE_EVENT_TYPES, encodeDeviceEventPayload, isDeviceEventType, SyncTrigger, } from '@mongrov/types/device-events';
13
+ export type { DeviceEventPayload, DeviceEventType, SyncTriggerValue, } from '@mongrov/types/device-events';
14
+ //# sourceMappingURL=device-events.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-events.d.ts","sourceRoot":"","sources":["../../src/core/device-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,wBAAwB,EACxB,4BAA4B,EAC5B,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,WAAW,GACZ,MAAM,8BAA8B,CAAA;AAErC,YAAY,EACV,kBAAkB,EAClB,eAAe,EACf,gBAAgB,GACjB,MAAM,8BAA8B,CAAA"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * `device_event.event_type` enum + per-type payload schemas (Sprint 5 §6).
3
+ *
4
+ * **Re-export only.** The canonical definition lives in
5
+ * `@mongrov/types/device-events` so that `@mongrov/device` — which must
6
+ * never depend on the analytics engine — can emit events this engine will
7
+ * store, against the same contract.
8
+ *
9
+ * Kept as a module here so analytics consumers get one import surface, and
10
+ * so the `core/index.ts` export list stays stable.
11
+ */
12
+ export { decodeDeviceEventPayload, DEVICE_EVENT_PAYLOAD_SCHEMAS, DEVICE_EVENT_TYPES, encodeDeviceEventPayload, isDeviceEventType, SyncTrigger, } from '@mongrov/types/device-events';
13
+ //# sourceMappingURL=device-events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-events.js","sourceRoot":"","sources":["../../src/core/device-events.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,wBAAwB,EACxB,4BAA4B,EAC5B,kBAAkB,EAClB,wBAAwB,EACxB,iBAAiB,EACjB,WAAW,GACZ,MAAM,8BAA8B,CAAA"}
@@ -5,7 +5,7 @@
5
5
  * ships the class and code union so stubs can throw typed errors from
6
6
  * day one; downstream phases add more codes without breaking imports.
7
7
  */
8
- export type AnalyticsErrorCode = 'engine_open_failed' | 'extension_load_failed' | 'attach_failed' | 'detach_failed' | 'token_vendor_failed' | 'migration_failed' | 'retention_failed' | 'query_failed' | 'not_attached' | 'not_ready' | 'not_implemented';
8
+ export type AnalyticsErrorCode = 'engine_open_failed' | 'extension_load_failed' | 'attach_failed' | 'detach_failed' | 'token_vendor_failed' | 'migration_failed' | 'retention_failed' | 'family_members_failed' | 'query_failed' | 'not_attached' | 'not_ready' | 'not_implemented';
9
9
  export declare class AnalyticsError extends Error {
10
10
  readonly code: AnalyticsErrorCode;
11
11
  readonly cause?: unknown;
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,kBAAkB,GAC1B,oBAAoB,GACpB,uBAAuB,GACvB,eAAe,GACf,eAAe,GACf,qBAAqB,GACrB,kBAAkB,GAClB,kBAAkB,GAClB,cAAc,GACd,cAAc,GACd,WAAW,GACX,iBAAiB,CAAA;AAErB,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,kBAAkB,CAAA;IACxC,SAAgB,KAAK,CAAC,EAAE,OAAO,CAAA;gBAEnB,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAMvE;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,cAAc;gBACzC,MAAM,EAAE,MAAM;CAI3B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,kBAAkB,GAC1B,oBAAoB,GACpB,uBAAuB,GACvB,eAAe,GACf,eAAe,GACf,qBAAqB,GACrB,kBAAkB,GAClB,kBAAkB,GAClB,uBAAuB,GACvB,cAAc,GACd,cAAc,GACd,WAAW,GACX,iBAAiB,CAAA;AAErB,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,IAAI,EAAE,kBAAkB,CAAA;IACxC,SAAgB,KAAK,CAAC,EAAE,OAAO,CAAA;gBAEnB,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAMvE;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,cAAc;gBACzC,MAAM,EAAE,MAAM;CAI3B"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAIvC,YAAY,IAAwB,EAAE,OAAe,EAAE,KAAe;QACpE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,cAAc;IACrD,YAAY,MAAc;QACxB,KAAK,CAAC,iBAAiB,EAAE,GAAG,MAAM,qBAAqB,CAAC,CAAA;QACxD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;IACnC,CAAC;CACF"}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAgBH,MAAM,OAAO,cAAe,SAAQ,KAAK;IAIvC,YAAY,IAAwB,EAAE,OAAe,EAAE,KAAe;QACpE,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAA;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;QAChB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;IACpB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,mBAAoB,SAAQ,cAAc;IACrD,YAAY,MAAc;QACxB,KAAK,CAAC,iBAAiB,EAAE,GAAG,MAAM,qBAAqB,CAAC,CAAA;QACxD,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAA;IACnC,CAAC;CACF"}
@@ -18,10 +18,27 @@
18
18
  */
19
19
  import type { HybridDuckDB } from './engine';
20
20
  import type { AnalyticsMode } from './types';
21
- /** Extensions required for R2 Iceberg attach, in load order. */
22
- export declare const REQUIRED_EXTENSIONS: readonly ["httpfs", "iceberg", "parquet"];
23
- /** Extensions required for local-only mode (no remote sync, no iceberg). */
24
- export declare const LOCAL_EXTENSIONS: readonly ["parquet"];
21
+ /**
22
+ * Extensions required for R2 Iceberg attach, in load order.
23
+ *
24
+ * `icu` sits between `httpfs` and `iceberg` per Sprint 5 T-01. It is not
25
+ * optional: every registry query and every baseline computation groups by
26
+ * local day via `timezone($tz, ts)` / `date_trunc('day', timezone(...))`,
27
+ * and DuckDB's `timezone()` with a named IANA zone resolves only when
28
+ * `icu` is loaded. Without it those queries fail at parse time rather than
29
+ * silently returning UTC-grouped rows.
30
+ */
31
+ export declare const REQUIRED_EXTENSIONS: readonly ["httpfs", "icu", "iceberg", "parquet"];
32
+ /**
33
+ * Extensions required for local-only mode (no remote sync, no iceberg).
34
+ *
35
+ * `icu` is here too, and deliberately so: local mode skips `httpfs` +
36
+ * `iceberg` to avoid the AWSSDK/vcpkg build burden, but it does NOT skip
37
+ * timezone-aware queries — a local-only install still renders day charts
38
+ * and computes day-first baselines. Omitting `icu` here would make those
39
+ * paths work on R2 installs and fail on local ones.
40
+ */
41
+ export declare const LOCAL_EXTENSIONS: readonly ["icu", "parquet"];
25
42
  export type RequiredExtension = (typeof REQUIRED_EXTENSIONS)[number];
26
43
  /**
27
44
  * Issue `INSTALL <ext>; LOAD <ext>;` for each mode-appropriate extension
@@ -1 +1 @@
1
- {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/core/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C,gEAAgE;AAChE,eAAO,MAAM,mBAAmB,2CAA4C,CAAA;AAE5E,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,sBAAuB,CAAA;AAEpD,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAQpE;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,YAAY,EAChB,IAAI,GAAE,aAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,WAAW,CAAC,MAAM,CAAC,CAEzE"}
1
+ {"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/core/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE5C;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,kDAAmD,CAAA;AAEnF;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,6BAA8B,CAAA;AAE3D,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAA;AAQpE;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,EAAE,EAAE,YAAY,EAChB,IAAI,GAAE,aAAoB,GACzB,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,YAAY,GAAG,WAAW,CAAC,MAAM,CAAC,CAEzE"}
@@ -17,10 +17,27 @@
17
17
  * calls, and DuckDB itself no-ops re-loads, so calling twice is safe.
18
18
  */
19
19
  import { AnalyticsError } from './errors';
20
- /** Extensions required for R2 Iceberg attach, in load order. */
21
- export const REQUIRED_EXTENSIONS = ['httpfs', 'iceberg', 'parquet'];
22
- /** Extensions required for local-only mode (no remote sync, no iceberg). */
23
- export const LOCAL_EXTENSIONS = ['parquet'];
20
+ /**
21
+ * Extensions required for R2 Iceberg attach, in load order.
22
+ *
23
+ * `icu` sits between `httpfs` and `iceberg` per Sprint 5 T-01. It is not
24
+ * optional: every registry query and every baseline computation groups by
25
+ * local day via `timezone($tz, ts)` / `date_trunc('day', timezone(...))`,
26
+ * and DuckDB's `timezone()` with a named IANA zone resolves only when
27
+ * `icu` is loaded. Without it those queries fail at parse time rather than
28
+ * silently returning UTC-grouped rows.
29
+ */
30
+ export const REQUIRED_EXTENSIONS = ['httpfs', 'icu', 'iceberg', 'parquet'];
31
+ /**
32
+ * Extensions required for local-only mode (no remote sync, no iceberg).
33
+ *
34
+ * `icu` is here too, and deliberately so: local mode skips `httpfs` +
35
+ * `iceberg` to avoid the AWSSDK/vcpkg build burden, but it does NOT skip
36
+ * timezone-aware queries — a local-only install still renders day charts
37
+ * and computes day-first baselines. Omitting `icu` here would make those
38
+ * paths work on R2 installs and fail on local ones.
39
+ */
40
+ export const LOCAL_EXTENSIONS = ['icu', 'parquet'];
24
41
  /**
25
42
  * Tracks which extensions this process has already booted. Keyed by
26
43
  * `HybridDuckDB` instance to keep isolation across engines in tests.
@@ -1 +1 @@
1
- {"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../src/core/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAIzC,gEAAgE;AAChE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAU,CAAA;AAE5E,4EAA4E;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,SAAS,CAAU,CAAA;AAIpD;;;GAGG;AACH,MAAM,UAAU,GAAG,IAAI,OAAO,EAA6B,CAAA;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,EAAgB,EAChB,OAAsB,IAAI;IAE1B,IAAI,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,IAAI,GAAG,EAAE,CAAA;QAClB,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;IAC5B,CAAC;IAED,MAAM,UAAU,GACZ,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,mBAAmB,CAAA;IAE7D,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,SAAQ;QACV,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,GAAG,GAAG,CAAC,CAAA;YACnC,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAA;QAClC,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,IAAI,cAAc,CACtB,uBAAuB,EACvB,oCAAoC,GAAG,EAAE,EACzC,KAAK,CACN,CAAA;QACH,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACjB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAgB;IAClD,OAAO,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG,EAAE,CAAA;AACxC,CAAC"}
1
+ {"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../src/core/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAIzC;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,CAAU,CAAA;AAEnF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,KAAK,EAAE,SAAS,CAAU,CAAA;AAI3D;;;GAGG;AACH,MAAM,UAAU,GAAG,IAAI,OAAO,EAA6B,CAAA;AAE3D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,EAAgB,EAChB,OAAsB,IAAI;IAE1B,IAAI,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,GAAG,IAAI,GAAG,EAAE,CAAA;QAClB,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;IAC5B,CAAC;IAED,MAAM,UAAU,GACZ,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAC,CAAC,mBAAmB,CAAA;IAE7D,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACpB,SAAQ;QACV,CAAC;QACD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,OAAO,CAAC,WAAW,GAAG,GAAG,CAAC,CAAA;YACnC,MAAM,EAAE,CAAC,OAAO,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAA;QAClC,CAAC;QACD,OAAO,KAAK,EAAE,CAAC;YACb,MAAM,IAAI,cAAc,CACtB,uBAAuB,EACvB,oCAAoC,GAAG,EAAE,EACzC,KAAK,CACN,CAAA;QACH,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACjB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,EAAgB;IAClD,OAAO,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,IAAI,GAAG,EAAE,CAAA;AACxC,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/core/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,UAAU,CAAA;AAe7D,OAAO,KAAK,EAEV,eAAe,EACf,eAAe,EAKhB,MAAM,SAAS,CAAA;AAgChB;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;AAwED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,QAAQ,GAAE,uBAA4B,GACrC,eAAe,CAkRjB"}
1
+ {"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/core/factory.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,UAAU,CAAA;AAgB7D,OAAO,KAAK,EAEV,eAAe,EACf,eAAe,EAKhB,MAAM,SAAS,CAAA;AAkChB;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,6EAA6E;IAC7E,aAAa,CAAC,EAAE,aAAa,CAAA;CAC9B;AAwED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,eAAe,EACvB,QAAQ,GAAE,uBAA4B,GACrC,eAAe,CAoYjB"}