@mongrov/analytics 0.1.0-alpha.1 → 0.1.0-alpha.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +456 -0
- package/dist/core/context.d.ts +24 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +30 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/engine.d.ts +56 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +119 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/extensions.d.ts +33 -0
- package/dist/core/extensions.d.ts.map +1 -0
- package/dist/core/extensions.js +58 -0
- package/dist/core/extensions.js.map +1 -0
- package/dist/core/factory.d.ts +22 -5
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +231 -6
- package/dist/core/factory.js.map +1 -1
- package/dist/core/hooks.d.ts +32 -13
- package/dist/core/hooks.d.ts.map +1 -1
- package/dist/core/hooks.js +137 -15
- package/dist/core/hooks.js.map +1 -1
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/logger.d.ts +25 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +33 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/machine.d.ts +174 -0
- package/dist/core/machine.d.ts.map +1 -0
- package/dist/core/machine.js +228 -0
- package/dist/core/machine.js.map +1 -0
- package/dist/core/metric_metadata.d.ts +128 -0
- package/dist/core/metric_metadata.d.ts.map +1 -0
- package/dist/core/metric_metadata.js +47 -0
- package/dist/core/metric_metadata.js.map +1 -0
- package/dist/core/migrations.d.ts +52 -0
- package/dist/core/migrations.d.ts.map +1 -0
- package/dist/core/migrations.js +66 -0
- package/dist/core/migrations.js.map +1 -0
- package/dist/core/persistence.d.ts +50 -0
- package/dist/core/persistence.d.ts.map +1 -0
- package/dist/core/persistence.js +80 -0
- package/dist/core/persistence.js.map +1 -0
- package/dist/core/retention.d.ts +90 -0
- package/dist/core/retention.d.ts.map +1 -0
- package/dist/core/retention.js +119 -0
- package/dist/core/retention.js.map +1 -0
- package/dist/core/schemas.d.ts +35 -0
- package/dist/core/schemas.d.ts.map +1 -0
- package/dist/core/schemas.js +209 -0
- package/dist/core/schemas.js.map +1 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/warehouse.d.ts +58 -0
- package/dist/core/warehouse.d.ts.map +1 -0
- package/dist/core/warehouse.js +116 -0
- package/dist/core/warehouse.js.map +1 -0
- package/dist/sync/buffer.d.ts +51 -0
- package/dist/sync/buffer.d.ts.map +1 -0
- package/dist/sync/buffer.js +188 -0
- package/dist/sync/buffer.js.map +1 -0
- package/dist/sync/context.d.ts +17 -0
- package/dist/sync/context.d.ts.map +1 -0
- package/dist/sync/context.js +23 -0
- package/dist/sync/context.js.map +1 -0
- package/dist/sync/errors.d.ts +19 -0
- package/dist/sync/errors.d.ts.map +1 -0
- package/dist/sync/errors.js +21 -0
- package/dist/sync/errors.js.map +1 -0
- package/dist/sync/events.d.ts +49 -0
- package/dist/sync/events.d.ts.map +1 -0
- package/dist/sync/events.js +46 -0
- package/dist/sync/events.js.map +1 -0
- package/dist/sync/factory.d.ts +57 -0
- package/dist/sync/factory.d.ts.map +1 -0
- package/dist/sync/factory.js +249 -0
- package/dist/sync/factory.js.map +1 -0
- package/dist/sync/fetcher.d.ts +74 -0
- package/dist/sync/fetcher.d.ts.map +1 -0
- package/dist/sync/fetcher.js +166 -0
- package/dist/sync/fetcher.js.map +1 -0
- package/dist/sync/flusher.d.ts +93 -0
- package/dist/sync/flusher.d.ts.map +1 -0
- package/dist/sync/flusher.js +234 -0
- package/dist/sync/flusher.js.map +1 -0
- package/dist/sync/hooks.d.ts +17 -0
- package/dist/sync/hooks.d.ts.map +1 -0
- package/dist/sync/hooks.js +48 -0
- package/dist/sync/hooks.js.map +1 -0
- package/dist/sync/index.d.ts +32 -1
- package/dist/sync/index.d.ts.map +1 -1
- package/dist/sync/index.js +36 -1
- package/dist/sync/index.js.map +1 -1
- package/dist/sync/manager.d.ts +48 -0
- package/dist/sync/manager.d.ts.map +1 -0
- package/dist/sync/manager.js +9 -0
- package/dist/sync/manager.js.map +1 -0
- package/dist/sync/mapper/activity.d.ts +31 -0
- package/dist/sync/mapper/activity.d.ts.map +1 -0
- package/dist/sync/mapper/activity.js +56 -0
- package/dist/sync/mapper/activity.js.map +1 -0
- package/dist/sync/mapper/battery.d.ts +12 -0
- package/dist/sync/mapper/battery.d.ts.map +1 -0
- package/dist/sync/mapper/battery.js +22 -0
- package/dist/sync/mapper/battery.js.map +1 -0
- package/dist/sync/mapper/firmware.d.ts +28 -0
- package/dist/sync/mapper/firmware.d.ts.map +1 -0
- package/dist/sync/mapper/firmware.js +43 -0
- package/dist/sync/mapper/firmware.js.map +1 -0
- package/dist/sync/mapper/heart-rate.d.ts +10 -0
- package/dist/sync/mapper/heart-rate.d.ts.map +1 -0
- package/dist/sync/mapper/heart-rate.js +19 -0
- package/dist/sync/mapper/heart-rate.js.map +1 -0
- package/dist/sync/mapper/hrv.d.ts +22 -0
- package/dist/sync/mapper/hrv.d.ts.map +1 -0
- package/dist/sync/mapper/hrv.js +56 -0
- package/dist/sync/mapper/hrv.js.map +1 -0
- package/dist/sync/mapper/ring-config.d.ts +33 -0
- package/dist/sync/mapper/ring-config.d.ts.map +1 -0
- package/dist/sync/mapper/ring-config.js +48 -0
- package/dist/sync/mapper/ring-config.js.map +1 -0
- package/dist/sync/mapper/sleep.d.ts +32 -0
- package/dist/sync/mapper/sleep.d.ts.map +1 -0
- package/dist/sync/mapper/sleep.js +103 -0
- package/dist/sync/mapper/sleep.js.map +1 -0
- package/dist/sync/mapper/spo2.d.ts +9 -0
- package/dist/sync/mapper/spo2.d.ts.map +1 -0
- package/dist/sync/mapper/spo2.js +18 -0
- package/dist/sync/mapper/spo2.js.map +1 -0
- package/dist/sync/mapper/temperature.d.ts +10 -0
- package/dist/sync/mapper/temperature.d.ts.map +1 -0
- package/dist/sync/mapper/temperature.js +19 -0
- package/dist/sync/mapper/temperature.js.map +1 -0
- package/dist/sync/mapper/time.d.ts +49 -0
- package/dist/sync/mapper/time.d.ts.map +1 -0
- package/dist/sync/mapper/time.js +107 -0
- package/dist/sync/mapper/time.js.map +1 -0
- package/dist/sync/mapper/types.d.ts +152 -0
- package/dist/sync/mapper/types.d.ts.map +1 -0
- package/dist/sync/mapper/types.js +15 -0
- package/dist/sync/mapper/types.js.map +1 -0
- package/dist/sync/overflow.d.ts +30 -0
- package/dist/sync/overflow.d.ts.map +1 -0
- package/dist/sync/overflow.js +114 -0
- package/dist/sync/overflow.js.map +1 -0
- package/dist/sync/pusher.d.ts +59 -0
- package/dist/sync/pusher.d.ts.map +1 -0
- package/dist/sync/pusher.js +115 -0
- package/dist/sync/pusher.js.map +1 -0
- package/dist/sync/scheduler.d.ts +84 -0
- package/dist/sync/scheduler.d.ts.map +1 -0
- package/dist/sync/scheduler.js +132 -0
- package/dist/sync/scheduler.js.map +1 -0
- package/dist/sync/triggers.d.ts +64 -0
- package/dist/sync/triggers.d.ts.map +1 -0
- package/dist/sync/triggers.js +113 -0
- package/dist/sync/triggers.js.map +1 -0
- package/dist/sync/types.d.ts +50 -0
- package/dist/sync/types.d.ts.map +1 -0
- package/dist/sync/types.js +8 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/watermark.d.ts +32 -0
- package/dist/sync/watermark.d.ts.map +1 -0
- package/dist/sync/watermark.js +61 -0
- package/dist/sync/watermark.js.map +1 -0
- package/package.json +12 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firmware composition mapper (T-09).
|
|
3
|
+
*
|
|
4
|
+
* Invokes every sub-mapper and produces a `MappedBatch` in one call. Empty
|
|
5
|
+
* firmware sections (undefined / empty array / absent `ring`) resolve to
|
|
6
|
+
* empty output arrays — no throw.
|
|
7
|
+
*
|
|
8
|
+
* The composition module owns the two SCD-2 outputs (device_config inserts
|
|
9
|
+
* vs. device_config closes). For flushing, callers use:
|
|
10
|
+
* - `batch.device_config` — new inserts
|
|
11
|
+
* - `batch.device_config_closes` — SCD-2 close directives
|
|
12
|
+
*
|
|
13
|
+
* See `.specifica/features/analytics-sync/spec.md` §Firmware mapper for the
|
|
14
|
+
* shape contract.
|
|
15
|
+
*/
|
|
16
|
+
import { type RingConfigClose } from './ring-config';
|
|
17
|
+
import type { DeviceConfigRow, FirmwareExport, MappedBatch, MapperContext } from './types';
|
|
18
|
+
export interface MapFirmwareOptions {
|
|
19
|
+
/** Override for deterministic tests. Defaults to `new Date()`. */
|
|
20
|
+
now?: Date;
|
|
21
|
+
/** SCD-2 prior-config map — see `mapRingConfig`. */
|
|
22
|
+
activePriorConfigs?: ReadonlyMap<string, DeviceConfigRow>;
|
|
23
|
+
}
|
|
24
|
+
export interface FirmwareMappedBatch extends MappedBatch {
|
|
25
|
+
device_config_closes: RingConfigClose[];
|
|
26
|
+
}
|
|
27
|
+
export declare function mapFirmwareExport(fw: FirmwareExport, ctx: MapperContext, opts?: MapFirmwareOptions): FirmwareMappedBatch;
|
|
28
|
+
//# sourceMappingURL=firmware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firmware.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/firmware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,OAAO,EAAiB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAA;AAInE,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,WAAW,EACX,aAAa,EACd,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,kBAAkB;IACjC,kEAAkE;IAClE,GAAG,CAAC,EAAE,IAAI,CAAA;IACV,oDAAoD;IACpD,kBAAkB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CAC1D;AAED,MAAM,WAAW,mBAAoB,SAAQ,WAAW;IACtD,oBAAoB,EAAE,eAAe,EAAE,CAAA;CACxC;AAED,wBAAgB,iBAAiB,CAC/B,EAAE,EAAE,cAAc,EAClB,GAAG,EAAE,aAAa,EAClB,IAAI,GAAE,kBAAuB,GAC5B,mBAAmB,CAuBrB"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firmware composition mapper (T-09).
|
|
3
|
+
*
|
|
4
|
+
* Invokes every sub-mapper and produces a `MappedBatch` in one call. Empty
|
|
5
|
+
* firmware sections (undefined / empty array / absent `ring`) resolve to
|
|
6
|
+
* empty output arrays — no throw.
|
|
7
|
+
*
|
|
8
|
+
* The composition module owns the two SCD-2 outputs (device_config inserts
|
|
9
|
+
* vs. device_config closes). For flushing, callers use:
|
|
10
|
+
* - `batch.device_config` — new inserts
|
|
11
|
+
* - `batch.device_config_closes` — SCD-2 close directives
|
|
12
|
+
*
|
|
13
|
+
* See `.specifica/features/analytics-sync/spec.md` §Firmware mapper for the
|
|
14
|
+
* shape contract.
|
|
15
|
+
*/
|
|
16
|
+
import { mapActivity } from './activity';
|
|
17
|
+
import { mapBattery } from './battery';
|
|
18
|
+
import { mapHeartRate } from './heart-rate';
|
|
19
|
+
import { mapHrv } from './hrv';
|
|
20
|
+
import { mapRingConfig } from './ring-config';
|
|
21
|
+
import { reconstructSleepSessions } from './sleep';
|
|
22
|
+
import { mapSpo2 } from './spo2';
|
|
23
|
+
import { mapTemperature } from './temperature';
|
|
24
|
+
export function mapFirmwareExport(fw, ctx, opts = {}) {
|
|
25
|
+
const activity = mapActivity(fw.activitydetails ?? [], ctx);
|
|
26
|
+
const sleep = reconstructSleepSessions(fw.sleep_processed ?? [], ctx);
|
|
27
|
+
const config = mapRingConfig(fw.ring ?? { automaticMonitoringData: [] }, ctx, opts);
|
|
28
|
+
return {
|
|
29
|
+
hrv: mapHrv(fw.hrv_table ?? [], ctx),
|
|
30
|
+
heart_rate: mapHeartRate(fw.heartrate ?? [], ctx),
|
|
31
|
+
spo2: mapSpo2(fw.spo2 ?? [], ctx),
|
|
32
|
+
temperature: mapTemperature(fw.temperature_table ?? [], ctx),
|
|
33
|
+
activity: activity.activity,
|
|
34
|
+
activity_bucket: activity.activity_bucket,
|
|
35
|
+
sleep_session: sleep.sleep_session,
|
|
36
|
+
sleep_stage: sleep.sleep_stage,
|
|
37
|
+
sleep_raw: sleep.sleep_raw,
|
|
38
|
+
device_event: mapBattery(fw.battery_table ?? [], ctx),
|
|
39
|
+
device_config: config.inserts,
|
|
40
|
+
device_config_closes: config.closes,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=firmware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"firmware.js","sourceRoot":"","sources":["../../../src/sync/mapper/firmware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,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,EAAwB,MAAM,eAAe,CAAA;AACnE,OAAO,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAA;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAmB9C,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,aAAa,CAC1B,EAAE,CAAC,IAAI,IAAI,EAAE,uBAAuB,EAAE,EAAE,EAAE,EAC1C,GAAG,EACH,IAAI,CACL,CAAA;IAED,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,YAAY,EAAE,UAAU,CAAC,EAAE,CAAC,aAAa,IAAI,EAAE,EAAE,GAAG,CAAC;QACrD,aAAa,EAAE,MAAM,CAAC,OAAO;QAC7B,oBAAoB,EAAE,MAAM,CAAC,MAAM;KACpC,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heart-rate mapper (T-03).
|
|
3
|
+
*
|
|
4
|
+
* Firmware column rename: `singleHR` → `bpm`. No sentinel handling: the ring
|
|
5
|
+
* firmware simply omits rows when it has no valid reading, so any row that
|
|
6
|
+
* makes it here is a valid measurement.
|
|
7
|
+
*/
|
|
8
|
+
import type { FirmwareHRRow, HeartRateRow, MapperContext } from './types';
|
|
9
|
+
export declare function mapHeartRate(rows: readonly FirmwareHRRow[], ctx: MapperContext): HeartRateRow[];
|
|
10
|
+
//# sourceMappingURL=heart-rate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heart-rate.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/heart-rate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEzE,wBAAgB,YAAY,CAC1B,IAAI,EAAE,SAAS,aAAa,EAAE,EAC9B,GAAG,EAAE,aAAa,GACjB,YAAY,EAAE,CAShB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heart-rate mapper (T-03).
|
|
3
|
+
*
|
|
4
|
+
* Firmware column rename: `singleHR` → `bpm`. No sentinel handling: the ring
|
|
5
|
+
* firmware simply omits rows when it has no valid reading, so any row that
|
|
6
|
+
* makes it here is a valid measurement.
|
|
7
|
+
*/
|
|
8
|
+
import { parseTimestamp } from './time';
|
|
9
|
+
export function mapHeartRate(rows, ctx) {
|
|
10
|
+
return rows.map(row => ({
|
|
11
|
+
ts: parseTimestamp(row.timestamp),
|
|
12
|
+
brand: ctx.brand,
|
|
13
|
+
family_id: ctx.familyId,
|
|
14
|
+
user_id: ctx.userId,
|
|
15
|
+
device_id: ctx.deviceId,
|
|
16
|
+
bpm: row.singleHR,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=heart-rate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heart-rate.js","sourceRoot":"","sources":["../../../src/sync/mapper/heart-rate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAGvC,MAAM,UAAU,YAAY,CAC1B,IAA8B,EAC9B,GAAkB;IAElB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtB,EAAE,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;QACjC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,OAAO,EAAE,GAAG,CAAC,MAAM;QACnB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,GAAG,EAAE,GAAG,CAAC,QAAQ;KAClB,CAAC,CAAC,CAAA;AACL,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HRV mapper (T-02).
|
|
3
|
+
*
|
|
4
|
+
* Firmware `hrv_table` rows carry HRV, stress, and — reused as a physical
|
|
5
|
+
* carrier — BP + vascular aging. The BP + vascular fields are `collected_only`
|
|
6
|
+
* (spec.md §Firmware fidelity preserved / analytics-core §Metric metadata) and
|
|
7
|
+
* we keep them on the `hrv` row so they aren't lost, but sentinel zeros must
|
|
8
|
+
* still be normalised to NULL for downstream queries.
|
|
9
|
+
*
|
|
10
|
+
* Sentinel rules (per `.specifica/features/analytics-sync/spec.md` §Firmware
|
|
11
|
+
* mapper):
|
|
12
|
+
* - `heartRate === 0` → NULL (0 bpm is not a valid measurement)
|
|
13
|
+
* - `systolicBP === 0` → NULL
|
|
14
|
+
* - `diastolicBP === 0` → NULL
|
|
15
|
+
* - `vascularAging === 0` → NULL
|
|
16
|
+
* - `hrv === 0` is *not* a sentinel — firmware genuinely reports 0 when RR
|
|
17
|
+
* variance is below floor; kept as-is
|
|
18
|
+
* - `stress === 0` is likewise valid
|
|
19
|
+
*/
|
|
20
|
+
import type { FirmwareHRVRow, HrvRow, MapperContext } from './types';
|
|
21
|
+
export declare function mapHrv(rows: readonly FirmwareHRVRow[], ctx: MapperContext): HrvRow[];
|
|
22
|
+
//# sourceMappingURL=hrv.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hrv.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/hrv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAEpE,wBAAgB,MAAM,CACpB,IAAI,EAAE,SAAS,cAAc,EAAE,EAC/B,GAAG,EAAE,aAAa,GACjB,MAAM,EAAE,CAiBV"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HRV mapper (T-02).
|
|
3
|
+
*
|
|
4
|
+
* Firmware `hrv_table` rows carry HRV, stress, and — reused as a physical
|
|
5
|
+
* carrier — BP + vascular aging. The BP + vascular fields are `collected_only`
|
|
6
|
+
* (spec.md §Firmware fidelity preserved / analytics-core §Metric metadata) and
|
|
7
|
+
* we keep them on the `hrv` row so they aren't lost, but sentinel zeros must
|
|
8
|
+
* still be normalised to NULL for downstream queries.
|
|
9
|
+
*
|
|
10
|
+
* Sentinel rules (per `.specifica/features/analytics-sync/spec.md` §Firmware
|
|
11
|
+
* mapper):
|
|
12
|
+
* - `heartRate === 0` → NULL (0 bpm is not a valid measurement)
|
|
13
|
+
* - `systolicBP === 0` → NULL
|
|
14
|
+
* - `diastolicBP === 0` → NULL
|
|
15
|
+
* - `vascularAging === 0` → NULL
|
|
16
|
+
* - `hrv === 0` is *not* a sentinel — firmware genuinely reports 0 when RR
|
|
17
|
+
* variance is below floor; kept as-is
|
|
18
|
+
* - `stress === 0` is likewise valid
|
|
19
|
+
*/
|
|
20
|
+
import { parseTimestamp } from './time';
|
|
21
|
+
export function mapHrv(rows, ctx) {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
out.push({
|
|
25
|
+
ts: parseTimestamp(row.timestamp),
|
|
26
|
+
brand: ctx.brand,
|
|
27
|
+
family_id: ctx.familyId,
|
|
28
|
+
user_id: ctx.userId,
|
|
29
|
+
device_id: ctx.deviceId,
|
|
30
|
+
hrv_ms: nullable(row.hrv),
|
|
31
|
+
stress: nullable(row.stress),
|
|
32
|
+
systolic_bp: sentinel(row.systolicBP),
|
|
33
|
+
diastolic_bp: sentinel(row.diastolicBP),
|
|
34
|
+
vascular_aging: sentinel(row.vascularAging),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `undefined` → NULL. Zero passes through — used for fields where 0 is a valid
|
|
41
|
+
* observed value (`hrv_ms`, `stress`).
|
|
42
|
+
*/
|
|
43
|
+
function nullable(v) {
|
|
44
|
+
return v === undefined ? null : v;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Firmware sentinel handling: 0 → NULL, undefined → NULL. Used for fields
|
|
48
|
+
* where 0 cannot represent a valid measurement (BP, vascular aging, HR carrier
|
|
49
|
+
* on the HRV row).
|
|
50
|
+
*/
|
|
51
|
+
function sentinel(v) {
|
|
52
|
+
if (v === undefined || v === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=hrv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hrv.js","sourceRoot":"","sources":["../../../src/sync/mapper/hrv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAGvC,MAAM,UAAU,MAAM,CACpB,IAA+B,EAC/B,GAAkB;IAElB,MAAM,GAAG,GAAa,EAAE,CAAA;IACxB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,GAAG,CAAC,IAAI,CAAC;YACP,EAAE,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;YACjC,KAAK,EAAE,GAAG,CAAC,KAAK;YAChB,SAAS,EAAE,GAAG,CAAC,QAAQ;YACvB,OAAO,EAAE,GAAG,CAAC,MAAM;YACnB,SAAS,EAAE,GAAG,CAAC,QAAQ;YACvB,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC;YACzB,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;YAC5B,WAAW,EAAE,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC;YACrC,YAAY,EAAE,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC;YACvC,cAAc,EAAE,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC;SAC5C,CAAC,CAAA;IACJ,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;GAGG;AACH,SAAS,QAAQ,CAAC,CAAqB;IACrC,OAAO,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;AACnC,CAAC;AAED;;;;GAIG;AACH,SAAS,QAAQ,CAAC,CAAqB;IACrC,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3C,OAAO,CAAC,CAAA;AACV,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring config mapper (T-08).
|
|
3
|
+
*
|
|
4
|
+
* Firmware `ring.automaticMonitoringData[]` describes which metrics the ring
|
|
5
|
+
* currently samples (interval + active hours). We store these as SCD-2-style
|
|
6
|
+
* rows in `device_config`, with `valid_from = now()` and `valid_to = null` on
|
|
7
|
+
* the new insert.
|
|
8
|
+
*
|
|
9
|
+
* Closing prior config (SCD-2 semantics):
|
|
10
|
+
* The mapper accepts an optional `activePriorConfigs` map keyed by metric
|
|
11
|
+
* name. For every metric that shows up in the new firmware snapshot, if a
|
|
12
|
+
* prior config was open, the mapper emits a `close` entry so the flusher
|
|
13
|
+
* can `UPDATE ... SET valid_to = now()` before the INSERT lands. Metrics
|
|
14
|
+
* present in the prior map but absent from the new firmware are left open
|
|
15
|
+
* (the ring is silent on them, not disabling them).
|
|
16
|
+
*
|
|
17
|
+
* The mapper stays pure: no engine access, no clock reads beyond the
|
|
18
|
+
* injected `now` (defaults to `new Date()` for production).
|
|
19
|
+
*/
|
|
20
|
+
import type { DeviceConfigRow, FirmwareRingConfig, MapperContext } from './types';
|
|
21
|
+
export interface RingConfigClose {
|
|
22
|
+
metric: string;
|
|
23
|
+
valid_to: Date;
|
|
24
|
+
}
|
|
25
|
+
export interface MapRingConfigResult {
|
|
26
|
+
inserts: DeviceConfigRow[];
|
|
27
|
+
closes: RingConfigClose[];
|
|
28
|
+
}
|
|
29
|
+
export declare function mapRingConfig(fw: FirmwareRingConfig, ctx: MapperContext, opts?: {
|
|
30
|
+
now?: Date;
|
|
31
|
+
activePriorConfigs?: ReadonlyMap<string, DeviceConfigRow>;
|
|
32
|
+
}): MapRingConfigResult;
|
|
33
|
+
//# sourceMappingURL=ring-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ring-config.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/ring-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EACV,eAAe,EAEf,kBAAkB,EAClB,aAAa,EACd,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,IAAI,CAAA;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,eAAe,EAAE,CAAA;IAC1B,MAAM,EAAE,eAAe,EAAE,CAAA;CAC1B;AAED,wBAAgB,aAAa,CAC3B,EAAE,EAAE,kBAAkB,EACtB,GAAG,EAAE,aAAa,EAClB,IAAI,GAAE;IACJ,GAAG,CAAC,EAAE,IAAI,CAAA;IACV,kBAAkB,CAAC,EAAE,WAAW,CAAC,MAAM,EAAE,eAAe,CAAC,CAAA;CACrD,GACL,mBAAmB,CAerB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ring config mapper (T-08).
|
|
3
|
+
*
|
|
4
|
+
* Firmware `ring.automaticMonitoringData[]` describes which metrics the ring
|
|
5
|
+
* currently samples (interval + active hours). We store these as SCD-2-style
|
|
6
|
+
* rows in `device_config`, with `valid_from = now()` and `valid_to = null` on
|
|
7
|
+
* the new insert.
|
|
8
|
+
*
|
|
9
|
+
* Closing prior config (SCD-2 semantics):
|
|
10
|
+
* The mapper accepts an optional `activePriorConfigs` map keyed by metric
|
|
11
|
+
* name. For every metric that shows up in the new firmware snapshot, if a
|
|
12
|
+
* prior config was open, the mapper emits a `close` entry so the flusher
|
|
13
|
+
* can `UPDATE ... SET valid_to = now()` before the INSERT lands. Metrics
|
|
14
|
+
* present in the prior map but absent from the new firmware are left open
|
|
15
|
+
* (the ring is silent on them, not disabling them).
|
|
16
|
+
*
|
|
17
|
+
* The mapper stays pure: no engine access, no clock reads beyond the
|
|
18
|
+
* injected `now` (defaults to `new Date()` for production).
|
|
19
|
+
*/
|
|
20
|
+
export function mapRingConfig(fw, ctx, opts = {}) {
|
|
21
|
+
const now = opts.now ?? new Date();
|
|
22
|
+
const prior = opts.activePriorConfigs ?? new Map();
|
|
23
|
+
const inserts = [];
|
|
24
|
+
const closes = [];
|
|
25
|
+
for (const window of fw.automaticMonitoringData ?? []) {
|
|
26
|
+
inserts.push(toRow(window, ctx, now));
|
|
27
|
+
if (prior.has(window.metric)) {
|
|
28
|
+
closes.push({ metric: window.metric, valid_to: now });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { inserts, closes };
|
|
32
|
+
}
|
|
33
|
+
function toRow(window, ctx, now) {
|
|
34
|
+
return {
|
|
35
|
+
ts: now,
|
|
36
|
+
brand: ctx.brand,
|
|
37
|
+
family_id: ctx.familyId,
|
|
38
|
+
user_id: ctx.userId,
|
|
39
|
+
device_id: ctx.deviceId,
|
|
40
|
+
metric: window.metric,
|
|
41
|
+
interval_minutes: window.interval_minutes,
|
|
42
|
+
start_hour: window.start_hour,
|
|
43
|
+
end_hour: window.end_hour,
|
|
44
|
+
valid_from: now,
|
|
45
|
+
valid_to: null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ring-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ring-config.js","sourceRoot":"","sources":["../../../src/sync/mapper/ring-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAmBH,MAAM,UAAU,aAAa,CAC3B,EAAsB,EACtB,GAAkB,EAClB,OAGI,EAAE;IAEN,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAA;IAClC,MAAM,KAAK,GAAG,IAAI,CAAC,kBAAkB,IAAI,IAAI,GAAG,EAA2B,CAAA;IAE3E,MAAM,OAAO,GAAsB,EAAE,CAAA;IACrC,MAAM,MAAM,GAAsB,EAAE,CAAA;IAEpC,KAAK,MAAM,MAAM,IAAI,EAAE,CAAC,uBAAuB,IAAI,EAAE,EAAE,CAAC;QACtD,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAA;QACrC,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;YAC7B,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAA;QACvD,CAAC;IACH,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AAC5B,CAAC;AAED,SAAS,KAAK,CACZ,MAAgC,EAChC,GAAkB,EAClB,GAAS;IAET,OAAO;QACL,EAAE,EAAE,GAAG;QACP,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,OAAO,EAAE,GAAG,CAAC,MAAM;QACnB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;QACzC,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,UAAU,EAAE,GAAG;QACf,QAAQ,EAAE,IAAI;KACf,CAAA;AACH,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep reconstruction mapper (T-06).
|
|
3
|
+
*
|
|
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.
|
|
8
|
+
*
|
|
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: `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.
|
|
24
|
+
*/
|
|
25
|
+
import type { FirmwareSleepRow, MapperContext, SleepRawRow, SleepSessionRow, SleepStageRow } from './types';
|
|
26
|
+
export interface ReconstructSleepResult {
|
|
27
|
+
sleep_session: SleepSessionRow[];
|
|
28
|
+
sleep_stage: SleepStageRow[];
|
|
29
|
+
sleep_raw: SleepRawRow[];
|
|
30
|
+
}
|
|
31
|
+
export declare function reconstructSleepSessions(rows: readonly FirmwareSleepRow[], ctx: MapperContext): ReconstructSleepResult;
|
|
32
|
+
//# sourceMappingURL=sleep.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sleep reconstruction mapper (T-06).
|
|
3
|
+
*
|
|
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.
|
|
8
|
+
*
|
|
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: `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.
|
|
24
|
+
*/
|
|
25
|
+
import { computeNightOf, parseTimestamp } from './time';
|
|
26
|
+
const MINUTE_MS = 60000;
|
|
27
|
+
const PRIMARY = 'primary';
|
|
28
|
+
const CONFIDENCE_FLOOR = 0.7;
|
|
29
|
+
export function reconstructSleepSessions(rows, ctx) {
|
|
30
|
+
const sleep_session = [];
|
|
31
|
+
const sleep_stage = [];
|
|
32
|
+
const sleep_raw = [];
|
|
33
|
+
// Raw pass — all input rows preserved verbatim for reprocessing.
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
sleep_raw.push({
|
|
36
|
+
ts: parseTimestamp(row.timestamp),
|
|
37
|
+
brand: ctx.brand,
|
|
38
|
+
family_id: ctx.familyId,
|
|
39
|
+
user_id: ctx.userId,
|
|
40
|
+
device_id: ctx.deviceId,
|
|
41
|
+
payload: row,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
// Group by `start`.
|
|
45
|
+
const bySession = new Map();
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
const bucket = bySession.get(row.start);
|
|
48
|
+
if (bucket)
|
|
49
|
+
bucket.push(row);
|
|
50
|
+
else
|
|
51
|
+
bySession.set(row.start, [row]);
|
|
52
|
+
}
|
|
53
|
+
for (const [startKey, blocks] of bySession) {
|
|
54
|
+
// Session must contain a primary block above the confidence floor to be
|
|
55
|
+
// counted as a real sleep session (as opposed to a nap or noise).
|
|
56
|
+
const hasQualifyingPrimary = blocks.some(b => b.block_type === PRIMARY && b.confidence >= CONFIDENCE_FLOOR);
|
|
57
|
+
if (!hasQualifyingPrimary)
|
|
58
|
+
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];
|
|
79
|
+
sleep_stage.push({
|
|
80
|
+
ts: blockInstants[i],
|
|
81
|
+
brand: ctx.brand,
|
|
82
|
+
family_id: ctx.familyId,
|
|
83
|
+
user_id: ctx.userId,
|
|
84
|
+
device_id: ctx.deviceId,
|
|
85
|
+
session_id: sessionId,
|
|
86
|
+
stage: b.block_type,
|
|
87
|
+
confidence: b.confidence,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Reference `startKey` to silence "unused" if TS ever tightens loop typing.
|
|
91
|
+
void startKey;
|
|
92
|
+
}
|
|
93
|
+
return { sleep_session, sleep_stage, sleep_raw };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
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.
|
|
99
|
+
*/
|
|
100
|
+
function makeSessionId(userId, startTs) {
|
|
101
|
+
return `${userId}:${startTs.toISOString()}`;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=sleep.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sleep.js","sourceRoot":"","sources":["../../../src/sync/mapper/sleep.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpO2 mapper (T-04).
|
|
3
|
+
*
|
|
4
|
+
* Column rename: `automaticSpo2Data` → `spo2`. No sentinel handling — firmware
|
|
5
|
+
* omits invalid reads. Values are integer percentages, 0..100.
|
|
6
|
+
*/
|
|
7
|
+
import type { FirmwareSpO2Row, MapperContext, Spo2Row } from './types';
|
|
8
|
+
export declare function mapSpo2(rows: readonly FirmwareSpO2Row[], ctx: MapperContext): Spo2Row[];
|
|
9
|
+
//# sourceMappingURL=spo2.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spo2.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/spo2.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAEtE,wBAAgB,OAAO,CACrB,IAAI,EAAE,SAAS,eAAe,EAAE,EAChC,GAAG,EAAE,aAAa,GACjB,OAAO,EAAE,CASX"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpO2 mapper (T-04).
|
|
3
|
+
*
|
|
4
|
+
* Column rename: `automaticSpo2Data` → `spo2`. No sentinel handling — firmware
|
|
5
|
+
* omits invalid reads. Values are integer percentages, 0..100.
|
|
6
|
+
*/
|
|
7
|
+
import { parseTimestamp } from './time';
|
|
8
|
+
export function mapSpo2(rows, ctx) {
|
|
9
|
+
return rows.map(row => ({
|
|
10
|
+
ts: parseTimestamp(row.timestamp),
|
|
11
|
+
brand: ctx.brand,
|
|
12
|
+
family_id: ctx.familyId,
|
|
13
|
+
user_id: ctx.userId,
|
|
14
|
+
device_id: ctx.deviceId,
|
|
15
|
+
spo2: row.automaticSpo2Data,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=spo2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spo2.js","sourceRoot":"","sources":["../../../src/sync/mapper/spo2.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAGvC,MAAM,UAAU,OAAO,CACrB,IAAgC,EAChC,GAAkB;IAElB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtB,EAAE,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;QACjC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,OAAO,EAAE,GAAG,CAAC,MAAM;QACnB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,IAAI,EAAE,GAAG,CAAC,iBAAiB;KAC5B,CAAC,CAAC,CAAA;AACL,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temperature mapper (T-04).
|
|
3
|
+
*
|
|
4
|
+
* Column rename: `temperature` → `temp_c`. Firmware emits whole-degree
|
|
5
|
+
* Celsius (limitation of the ring); we preserve as-is — downstream aggregation
|
|
6
|
+
* (e.g. daily average) is expected to smooth the discretisation.
|
|
7
|
+
*/
|
|
8
|
+
import type { FirmwareTempRow, MapperContext, TemperatureRow } from './types';
|
|
9
|
+
export declare function mapTemperature(rows: readonly FirmwareTempRow[], ctx: MapperContext): TemperatureRow[];
|
|
10
|
+
//# sourceMappingURL=temperature.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temperature.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/temperature.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AAE7E,wBAAgB,cAAc,CAC5B,IAAI,EAAE,SAAS,eAAe,EAAE,EAChC,GAAG,EAAE,aAAa,GACjB,cAAc,EAAE,CASlB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Temperature mapper (T-04).
|
|
3
|
+
*
|
|
4
|
+
* Column rename: `temperature` → `temp_c`. Firmware emits whole-degree
|
|
5
|
+
* Celsius (limitation of the ring); we preserve as-is — downstream aggregation
|
|
6
|
+
* (e.g. daily average) is expected to smooth the discretisation.
|
|
7
|
+
*/
|
|
8
|
+
import { parseTimestamp } from './time';
|
|
9
|
+
export function mapTemperature(rows, ctx) {
|
|
10
|
+
return rows.map(row => ({
|
|
11
|
+
ts: parseTimestamp(row.timestamp),
|
|
12
|
+
brand: ctx.brand,
|
|
13
|
+
family_id: ctx.familyId,
|
|
14
|
+
user_id: ctx.userId,
|
|
15
|
+
device_id: ctx.deviceId,
|
|
16
|
+
temp_c: row.temperature,
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=temperature.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"temperature.js","sourceRoot":"","sources":["../../../src/sync/mapper/temperature.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAA;AAGvC,MAAM,UAAU,cAAc,CAC5B,IAAgC,EAChC,GAAkB;IAElB,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACtB,EAAE,EAAE,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC;QACjC,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,OAAO,EAAE,GAAG,CAAC,MAAM;QACnB,SAAS,EAAE,GAAG,CAAC,QAAQ;QACvB,MAAM,EAAE,GAAG,CAAC,WAAW;KACxB,CAAC,CAAC,CAAA;AACL,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firmware timestamp + timezone utilities (T-01).
|
|
3
|
+
*
|
|
4
|
+
* Firmware exports ring readings in a `"YYYY.MM.DD HH:MM:SS"` string, wall-clock
|
|
5
|
+
* UTC. The mapper needs two operations:
|
|
6
|
+
*
|
|
7
|
+
* 1. `parseTimestamp(fwStr)` — turn the firmware string into an ISO instant.
|
|
8
|
+
* 2. `computeNightOf(ts, tz)` — assign a reading to a sleep-night bucket via
|
|
9
|
+
* the 6pm-6pm local rule (readings between 18:00 local of day D and 18:00
|
|
10
|
+
* local of D+1 belong to `night_of = D`).
|
|
11
|
+
*
|
|
12
|
+
* The `night_of` computation must be timezone-aware because the rule is defined
|
|
13
|
+
* in the user's IANA timezone (`ctx.userTimezone`), and DST transitions can
|
|
14
|
+
* shift the wall-clock boundary. We defer to `date-fns-tz` for the IANA math.
|
|
15
|
+
*
|
|
16
|
+
* See `.specifica/features/analytics-sync/spec.md` §Firmware mapper for the
|
|
17
|
+
* timestamp contract and `design.md` §sync/mapper/firmware.ts for the module
|
|
18
|
+
* shape.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Parse a firmware timestamp string into a `Date` (ISO instant, UTC).
|
|
22
|
+
*
|
|
23
|
+
* Contract: input is UTC wall-clock (`"2026.06.17 03:15:00"` = 03:15 UTC).
|
|
24
|
+
*
|
|
25
|
+
* @throws {Error} if the string does not match `"YYYY.MM.DD HH:MM:SS"`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseTimestamp(fwStr: string): Date;
|
|
28
|
+
/**
|
|
29
|
+
* Assign an instant to a sleep-night bucket in the user's timezone.
|
|
30
|
+
*
|
|
31
|
+
* Rule (spec.md §Firmware mapper):
|
|
32
|
+
* Readings whose local (in `tz`) wall-clock is in the interval
|
|
33
|
+
* `[D 18:00, D+1 18:00)`
|
|
34
|
+
* are attributed to `night_of = D`.
|
|
35
|
+
*
|
|
36
|
+
* Returned as a UTC `Date` at midnight of `D` in the user's timezone. This
|
|
37
|
+
* matches the `DATE` column shape in `sleep_session.night_of` and lets
|
|
38
|
+
* `date_trunc`/`GROUP BY` queries stay timezone-honest.
|
|
39
|
+
*
|
|
40
|
+
* DST handling:
|
|
41
|
+
* - Spring-forward gap (02:00 becomes 03:00): a reading in the vanished hour
|
|
42
|
+
* is treated as the following instant by `date-fns-tz`, so the boundary is
|
|
43
|
+
* preserved.
|
|
44
|
+
* - Fall-back double hour (01:00 occurs twice): both occurrences share the
|
|
45
|
+
* same local wall-clock and therefore the same `night_of`. This is the
|
|
46
|
+
* desired behavior — the user experiences one continuous night.
|
|
47
|
+
*/
|
|
48
|
+
export declare function computeNightOf(ts: Date, tz: string): Date;
|
|
49
|
+
//# sourceMappingURL=time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../../src/sync/mapper/time.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAaH;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAqBlD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAmCzD"}
|