mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
feat(measurements): add a COMPUTED source and first-class score types
Add a server-owned COMPUTED measurement source for rows a server engine
derives rather than a device or a person reports, plus three first-class
0-100 score types — RECOVERY_SCORE, STRESS_SCORE, STRAIN_SCORE (unit
"score", plausibility range 0..100).
COMPUTED is read-only from a client's perspective: it joins the read /
decode enum so the native client can decode the rows it queries and
charts, but every client-facing write surface rejects it exactly like
WITHINGS and IMPORT — the batch ingest allowlist stays {APPLE_HEALTH,
MANUAL} and the single-entry POST 422s a COMPUTED source.
All three score types thread through every type-exhaustiveness site: the
unit + plausibility-range maps, the PR-direction resolver (scores carry
no record direction, like the device events), the category overlay (a
new "scores" cluster), the list-view label / icon / colour maps with
locale strings across all six bundles, and the insight chart-token
allowlist + orphan-enum stripper. They are not clinical vitals, so they
stay out of the doctor-report PDF.
Only the recovery score is computed today; the stress and strain types
are defined now so the engines that compute them later need no schema
change. Migration 0106 is purely additive (ADD VALUE IF NOT EXISTS).
This commit is contained in:
1 parent
51fc48f93c
commit
33b75361f3
18 files changed
+374
-9
No files matched your search
@@ -390,6 +390,9 @@ paths:
|
||||
- LOW_HEART_RATE_EVENT
|
||||
- WALKING_STEADINESS_EVENT
|
||||
- BREATHING_DISTURBANCE_EVENT
|
||||
- RECOVERY_SCORE
|
||||
- STRESS_SCORE
|
||||
- STRAIN_SCORE
|
||||
- in: query
|
||||
name: from
|
||||
schema:
|
||||
@@ -535,6 +538,9 @@ paths:
|
||||
- LOW_HEART_RATE_EVENT
|
||||
- WALKING_STEADINESS_EVENT
|
||||
- BREATHING_DISTURBANCE_EVENT
|
||||
- RECOVERY_SCORE
|
||||
- STRESS_SCORE
|
||||
- STRAIN_SCORE
|
||||
value:
|
||||
type: number
|
||||
measuredAt:
|
||||
@@ -552,6 +558,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
glucoseContext:
|
||||
type: string
|
||||
enum:
|
||||
@@ -801,6 +808,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
externalId:
|
||||
type: string
|
||||
maxLength: 128
|
||||
@@ -1411,6 +1419,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1421,6 +1430,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1431,6 +1441,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1441,6 +1452,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1451,6 +1463,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1461,6 +1474,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1471,6 +1485,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1481,6 +1496,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1491,6 +1507,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1501,6 +1518,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1511,6 +1529,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1521,6 +1540,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1531,6 +1551,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1541,6 +1562,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
metricPriority:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1554,6 +1576,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1564,6 +1587,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1574,6 +1598,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1584,6 +1609,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1594,6 +1620,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1604,6 +1631,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1614,6 +1642,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1624,6 +1653,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1634,6 +1664,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1644,6 +1675,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1654,6 +1686,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1664,6 +1697,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1674,6 +1708,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1684,6 +1719,7 @@ paths:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
deviceTypePriority:
|
||||
type: object
|
||||
properties:
|
||||
@@ -2107,6 +2143,9 @@ paths:
|
||||
- LOW_HEART_RATE_EVENT
|
||||
- WALKING_STEADINESS_EVENT
|
||||
- BREATHING_DISTURBANCE_EVENT
|
||||
- RECOVERY_SCORE
|
||||
- STRESS_SCORE
|
||||
- STRAIN_SCORE
|
||||
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
|
||||
required: true
|
||||
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
|
||||
@@ -3788,6 +3827,9 @@ components:
|
||||
- LOW_HEART_RATE_EVENT
|
||||
- WALKING_STEADINESS_EVENT
|
||||
- BREATHING_DISTURBANCE_EVENT
|
||||
- RECOVERY_SCORE
|
||||
- STRESS_SCORE
|
||||
- STRAIN_SCORE
|
||||
value:
|
||||
type: number
|
||||
unit:
|
||||
@@ -3803,6 +3845,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
notes:
|
||||
anyOf:
|
||||
- type: string
|
||||
@@ -4090,6 +4133,9 @@ components:
|
||||
- LOW_HEART_RATE_EVENT
|
||||
- WALKING_STEADINESS_EVENT
|
||||
- BREATHING_DISTURBANCE_EVENT
|
||||
- RECOVERY_SCORE
|
||||
- STRESS_SCORE
|
||||
- STRAIN_SCORE
|
||||
value:
|
||||
type: number
|
||||
unit:
|
||||
@@ -4105,6 +4151,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
notes:
|
||||
anyOf:
|
||||
- type: string
|
||||
@@ -4322,6 +4369,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
externalId:
|
||||
anyOf:
|
||||
- type: string
|
||||
@@ -4424,6 +4472,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
externalId:
|
||||
anyOf:
|
||||
- type: string
|
||||
@@ -5685,6 +5734,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5695,6 +5745,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5705,6 +5756,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5715,6 +5767,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5725,6 +5778,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5735,6 +5789,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5745,6 +5800,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5755,6 +5811,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5765,6 +5822,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5775,6 +5833,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5785,6 +5844,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5795,6 +5855,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5805,6 +5866,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5815,6 +5877,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
metricPriority:
|
||||
type: object
|
||||
properties:
|
||||
@@ -5828,6 +5891,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5838,6 +5902,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5848,6 +5913,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5858,6 +5924,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5868,6 +5935,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5878,6 +5946,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5888,6 +5957,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5898,6 +5968,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5908,6 +5979,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5918,6 +5990,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5928,6 +6001,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5938,6 +6012,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5948,6 +6023,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -5958,6 +6034,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
additionalProperties: false
|
||||
deviceTypePriority:
|
||||
type: object
|
||||
@@ -6017,6 +6094,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6027,6 +6105,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6037,6 +6116,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6047,6 +6127,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6057,6 +6138,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6067,6 +6149,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6077,6 +6160,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6087,6 +6171,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6097,6 +6182,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6107,6 +6193,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6117,6 +6204,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6127,6 +6215,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6137,6 +6226,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6147,6 +6237,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
metricPriority:
|
||||
type: object
|
||||
properties:
|
||||
@@ -6160,6 +6251,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6170,6 +6262,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6180,6 +6273,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6190,6 +6284,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6200,6 +6295,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6210,6 +6306,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6220,6 +6317,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6230,6 +6328,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6240,6 +6339,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6250,6 +6350,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6260,6 +6361,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6270,6 +6372,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6280,6 +6383,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6290,6 +6394,7 @@ components:
|
||||
- WITHINGS
|
||||
- IMPORT
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
additionalProperties: false
|
||||
deviceTypePriority:
|
||||
type: object
|
||||
|
||||
+4
-1
@@ -437,7 +437,10 @@
|
||||
"typeHighHeartRateEvent": "Hinweis auf hohe Herzfrequenz",
|
||||
"typeLowHeartRateEvent": "Hinweis auf niedrige Herzfrequenz",
|
||||
"typeWalkingSteadinessEvent": "Hinweis zur Gehstabilität",
|
||||
"typeBreathingDisturbanceEvent": "Atemstörung"
|
||||
"typeBreathingDisturbanceEvent": "Atemstörung",
|
||||
"typeRecoveryScore": "Erholungs-Score",
|
||||
"typeStressScore": "Stress-Score",
|
||||
"typeStrainScore": "Belastungs-Score"
|
||||
},
|
||||
"mood": {
|
||||
"title": "Stimmung",
|
||||
|
||||
+4
-1
@@ -437,7 +437,10 @@
|
||||
"typeHighHeartRateEvent": "High heart rate notification",
|
||||
"typeLowHeartRateEvent": "Low heart rate notification",
|
||||
"typeWalkingSteadinessEvent": "Walking steadiness alert",
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance"
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance",
|
||||
"typeRecoveryScore": "Recovery score",
|
||||
"typeStressScore": "Stress score",
|
||||
"typeStrainScore": "Strain score"
|
||||
},
|
||||
"mood": {
|
||||
"title": "Mood",
|
||||
|
||||
+4
-1
@@ -437,7 +437,10 @@
|
||||
"typeHighHeartRateEvent": "High heart rate notification",
|
||||
"typeLowHeartRateEvent": "Low heart rate notification",
|
||||
"typeWalkingSteadinessEvent": "Walking steadiness alert",
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance"
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance",
|
||||
"typeRecoveryScore": "Recovery score",
|
||||
"typeStressScore": "Stress score",
|
||||
"typeStrainScore": "Strain score"
|
||||
},
|
||||
"mood": {
|
||||
"title": "Estado de ánimo",
|
||||
|
||||
+4
-1
@@ -437,7 +437,10 @@
|
||||
"typeHighHeartRateEvent": "High heart rate notification",
|
||||
"typeLowHeartRateEvent": "Low heart rate notification",
|
||||
"typeWalkingSteadinessEvent": "Walking steadiness alert",
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance"
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance",
|
||||
"typeRecoveryScore": "Recovery score",
|
||||
"typeStressScore": "Stress score",
|
||||
"typeStrainScore": "Strain score"
|
||||
},
|
||||
"mood": {
|
||||
"title": "Humeur",
|
||||
|
||||
+4
-1
@@ -437,7 +437,10 @@
|
||||
"typeHighHeartRateEvent": "High heart rate notification",
|
||||
"typeLowHeartRateEvent": "Low heart rate notification",
|
||||
"typeWalkingSteadinessEvent": "Walking steadiness alert",
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance"
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance",
|
||||
"typeRecoveryScore": "Recovery score",
|
||||
"typeStressScore": "Stress score",
|
||||
"typeStrainScore": "Strain score"
|
||||
},
|
||||
"mood": {
|
||||
"title": "Umore",
|
||||
|
||||
+4
-1
@@ -437,7 +437,10 @@
|
||||
"typeHighHeartRateEvent": "High heart rate notification",
|
||||
"typeLowHeartRateEvent": "Low heart rate notification",
|
||||
"typeWalkingSteadinessEvent": "Walking steadiness alert",
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance"
|
||||
"typeBreathingDisturbanceEvent": "Breathing disturbance",
|
||||
"typeRecoveryScore": "Recovery score",
|
||||
"typeStressScore": "Stress score",
|
||||
"typeStrainScore": "Strain score"
|
||||
},
|
||||
"mood": {
|
||||
"title": "Nastrój",
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
-- v1.10.0 — computed scores (WX-C): COMPUTED source + the three `*_SCORE`
|
||||
-- measurement types.
|
||||
--
|
||||
-- Two purely-additive enum extensions, no new columns and no backfill:
|
||||
--
|
||||
-- 1. A new `measurement_source` value `COMPUTED` for server-derived rows.
|
||||
-- A `COMPUTED` Measurement is minted by a nightly engine from the user's
|
||||
-- already-stored signals (the Recovery score is the first); it is NEVER
|
||||
-- ingested from a client. The client-facing batch + single-POST write
|
||||
-- surfaces reject it exactly like `WITHINGS` / `IMPORT`. It is part of
|
||||
-- the enum so the read/response shapes (and the iOS decoder) can decode
|
||||
-- the rows it surfaces.
|
||||
--
|
||||
-- 2. Three new `measurement_type` values — the server-derived wellness
|
||||
-- scores `RECOVERY_SCORE`, `STRESS_SCORE`, `STRAIN_SCORE` (0–100, unit
|
||||
-- `score`). Only RECOVERY_SCORE is computed in v1.10.0; STRESS_SCORE +
|
||||
-- STRAIN_SCORE are defined now so the later engines that compute them
|
||||
-- need no schema change. These are NOT clinical vitals — they never
|
||||
-- appear in the doctor-report PDF.
|
||||
--
|
||||
-- Idempotent guards (`IF NOT EXISTS`) make reruns safe. Forward-only.
|
||||
--
|
||||
-- Reversibility:
|
||||
-- Postgres does not support removing an enum value, so the four new
|
||||
-- members stay; with no rows carrying them (a `COMPUTED` source / a
|
||||
-- `*_SCORE` type) after the nightly job is disabled they are inert.
|
||||
|
||||
-- ── 1. measurement_source — append the COMPUTED server-owned source ────
|
||||
-- Pure additive; no existing row carries this value. The `IF NOT EXISTS`
|
||||
-- guard keeps the migration idempotent across a partially-applied environment.
|
||||
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'COMPUTED';
|
||||
|
||||
-- ── 2. measurement_type — append the three computed-score classes ──────
|
||||
-- Pure additive; no existing row carries any of these values.
|
||||
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'RECOVERY_SCORE';
|
||||
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'STRESS_SCORE';
|
||||
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'STRAIN_SCORE';
|
||||
@@ -558,6 +558,16 @@ enum MeasurementType {
|
||||
LOW_HEART_RATE_EVENT // HealthKit `HKCategoryTypeIdentifierLowHeartRateEvent`. The watch detected a sustained low heart rate below the user's configured threshold.
|
||||
WALKING_STEADINESS_EVENT // HealthKit `HKCategoryTypeIdentifierAppleWalkingSteadinessEvent`. The device flagged low / very-low walking steadiness (a falls-risk mobility signal); the severity rides in `rhythmClassification`.
|
||||
BREATHING_DISTURBANCE_EVENT // HealthKit `HKCategoryTypeIdentifierSleepApneaEvent` + `AppleSleepingBreathingDisturbances`. The device flagged an elevated breathing-disturbance / possible sleep-apnea signal during sleep. SCREENING SIGNAL ONLY — never a HealthLog diagnosis.
|
||||
// v1.10.0 — computed scores (WX-C)
|
||||
// Server-derived wellness scores (0–100, unit `score`). Each is minted by a
|
||||
// nightly engine from the user's already-stored signals and persisted as a
|
||||
// `COMPUTED`-source Measurement row — never ingested from a client. Only
|
||||
// RECOVERY_SCORE is computed in v1.10.0; STRESS_SCORE + STRAIN_SCORE are
|
||||
// defined now so the later engines that compute them need no schema change.
|
||||
// These are NOT clinical vitals — they never appear in the doctor-report PDF.
|
||||
RECOVERY_SCORE // Daily recovery / readiness proxy. Higher = more recovered. Blends RHR + HRV (SDNN) + sleep + respiratory-rate deviation from the personal baseline (Plews 2013 / Buchheit 2014 lineage). Descriptive, not a clinical or training-recovery assessment.
|
||||
STRESS_SCORE // Reserved for the later strain/stress engine. Higher = more physiological stress. No engine computes it in v1.10.0.
|
||||
STRAIN_SCORE // Reserved for the later strain/stress engine. Higher = more cardiovascular strain (workout-load lineage). No engine computes it in v1.10.0.
|
||||
|
||||
@@map("measurement_type")
|
||||
}
|
||||
@@ -606,6 +616,14 @@ enum MeasurementSource {
|
||||
WITHINGS
|
||||
IMPORT
|
||||
APPLE_HEALTH // v1.4.23 — HealthKit batch ingest. iOS DTO mirrors this exact spelling (not "HEALTHKIT").
|
||||
// v1.10.0 — computed scores (WX-C)
|
||||
// Server-derived rows. A `COMPUTED` measurement is minted by a nightly
|
||||
// engine from the user's already-stored signals (the Recovery score is the
|
||||
// first); it is NEVER ingested from a client. Server-owned / read-only:
|
||||
// the client-facing batch + single-POST write surfaces reject it exactly
|
||||
// like WITHINGS / IMPORT, but it is part of the read enum so iOS can decode
|
||||
// the rows it queries / charts.
|
||||
COMPUTED
|
||||
|
||||
@@map("measurement_source")
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ export const MEASUREMENT_TYPE_LABEL_KEYS: Record<string, string> = {
|
||||
LOW_HEART_RATE_EVENT: "measurements.typeLowHeartRateEvent",
|
||||
WALKING_STEADINESS_EVENT: "measurements.typeWalkingSteadinessEvent",
|
||||
BREATHING_DISTURBANCE_EVENT: "measurements.typeBreathingDisturbanceEvent",
|
||||
// ── v1.10.0 — computed scores (WX-C) ──
|
||||
RECOVERY_SCORE: "measurements.typeRecoveryScore",
|
||||
STRESS_SCORE: "measurements.typeStressScore",
|
||||
STRAIN_SCORE: "measurements.typeStrainScore",
|
||||
};
|
||||
|
||||
export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
|
||||
@@ -182,6 +186,11 @@ export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
|
||||
LOW_HEART_RATE_EVENT: HeartPulse,
|
||||
WALKING_STEADINESS_EVENT: Footprints,
|
||||
BREATHING_DISTURBANCE_EVENT: Wind,
|
||||
// ── v1.10.0 — computed scores (WX-C) ──
|
||||
// Gauge reads as a composite "index / score" dial for all three.
|
||||
RECOVERY_SCORE: Gauge,
|
||||
STRESS_SCORE: Gauge,
|
||||
STRAIN_SCORE: Gauge,
|
||||
};
|
||||
|
||||
export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
|
||||
@@ -259,4 +268,10 @@ export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
|
||||
LOW_HEART_RATE_EVENT: "bg-chart-3/20 text-chart-3",
|
||||
WALKING_STEADINESS_EVENT: "bg-chart-2/20 text-chart-2",
|
||||
BREATHING_DISTURBANCE_EVENT: "bg-chart-5/20 text-chart-5",
|
||||
// ── v1.10.0 — computed scores (WX-C) ──
|
||||
// chart-1 (Dracula purple) marks the server-derived composites as their
|
||||
// own visual group, distinct from the raw-signal families above.
|
||||
RECOVERY_SCORE: "bg-chart-1/20 text-chart-1",
|
||||
STRESS_SCORE: "bg-chart-1/20 text-chart-1",
|
||||
STRAIN_SCORE: "bg-chart-1/20 text-chart-1",
|
||||
};
|
||||
@@ -73,10 +73,14 @@ const EXPECTED_TYPES = [
|
||||
"LOW_HEART_RATE_EVENT",
|
||||
"WALKING_STEADINESS_EVENT",
|
||||
"BREATHING_DISTURBANCE_EVENT",
|
||||
// ── v1.10.0 — computed scores (WX-C) ──
|
||||
"RECOVERY_SCORE",
|
||||
"STRESS_SCORE",
|
||||
"STRAIN_SCORE",
|
||||
] as const;
|
||||
|
||||
describe("measurementTypeEnum coverage", () => {
|
||||
it("exposes the 50 canonical measurement types", () => {
|
||||
it("exposes the 53 canonical measurement types", () => {
|
||||
expect([...measurementTypeEnum.options].sort()).toEqual(
|
||||
[...EXPECTED_TYPES].sort(),
|
||||
);
|
||||
@@ -168,6 +172,14 @@ describe("measurementTypeEnum coverage", () => {
|
||||
"LOW_HEART_RATE_EVENT",
|
||||
"WALKING_STEADINESS_EVENT",
|
||||
"BREATHING_DISTURBANCE_EVENT",
|
||||
// v1.10.0 — computed scores (WX-C). Server-derived wellness scores
|
||||
// (Recovery / Stress / Strain) are 0–100 composites recomputed nightly,
|
||||
// not measured clinical vitals. They surface on their own Insights
|
||||
// cluster with a "descriptive, not clinical" disclaimer and never belong
|
||||
// in the clinical vitals PDF. See doctor-report-pdf-core.ts.
|
||||
"RECOVERY_SCORE",
|
||||
"STRESS_SCORE",
|
||||
"STRAIN_SCORE",
|
||||
]);
|
||||
|
||||
it("doctor-report PDF vital types cover the canonical enum minus documented exclusions", () => {
|
||||
|
||||
@@ -181,6 +181,13 @@ export const DOCTOR_REPORT_TYPE_UNIT_KEYS: Record<string, string | null> = {
|
||||
* table for clinical readability. Glucose ships separately via
|
||||
* per-context `glucoseStats`. Sleep + activity are intentionally
|
||||
* excluded from a clinical-focused report.
|
||||
*
|
||||
* v1.10.0 — computed scores (WX-C). The server-derived `*_SCORE` types
|
||||
* (RECOVERY_SCORE / STRESS_SCORE / STRAIN_SCORE) are NOT clinical vitals:
|
||||
* they are 0–100 composites recomputed nightly from the underlying signals
|
||||
* and carry a "descriptive, not a clinical assessment" disclaimer. They stay
|
||||
* out of this table by design (paired with `PDF_VITAL_EXCLUSIONS` in
|
||||
* `measurement-type-enum-coverage.test.ts`).
|
||||
*/
|
||||
export const DOCTOR_REPORT_VITAL_TYPES = [
|
||||
"WEIGHT",
|
||||
|
||||
@@ -89,6 +89,14 @@ export const ALLOWED_CHART_TOKENS = [
|
||||
"metric:STAIR_ASCENT_SPEED",
|
||||
"metric:STAIR_DESCENT_SPEED",
|
||||
"metric:BREATHING_DISTURBANCES",
|
||||
// v1.10.0 — computed scores (WX-C). Server-derived 0–100 wellness scores.
|
||||
// Unlike the categorical EVENT classes they ARE a continuous daily series,
|
||||
// so they carry a `metric:<TYPE>` token and render through the generic
|
||||
// chart renderer. Only RECOVERY_SCORE is computed in v1.10.0; the other
|
||||
// two tokens are reserved for the later engines.
|
||||
"metric:RECOVERY_SCORE",
|
||||
"metric:STRESS_SCORE",
|
||||
"metric:STRAIN_SCORE",
|
||||
] as const;
|
||||
|
||||
export type ChartToken = (typeof ALLOWED_CHART_TOKENS)[number];
|
||||
@@ -184,6 +192,11 @@ const ORPHAN_ENUMS = [
|
||||
"STAIR_ASCENT_SPEED",
|
||||
"STAIR_DESCENT_SPEED",
|
||||
"BREATHING_DISTURBANCES",
|
||||
// v1.10.0 — computed scores (WX-C), same shape. Strip the bare enum name
|
||||
// if the model drops it into prose (mirrors the MOOD_SCORE precedent).
|
||||
"RECOVERY_SCORE",
|
||||
"STRESS_SCORE",
|
||||
"STRAIN_SCORE",
|
||||
] as const;
|
||||
|
||||
// `\b` boundaries keep ordinary English prose untouched — "weight"
|
||||
|
||||
@@ -32,6 +32,13 @@ const MEASUREMENT_TYPES_WITHOUT_HK_COUNTERPART = new Set<MeasurementType>([
|
||||
"PULSE_WAVE_VELOCITY",
|
||||
"VASCULAR_AGE",
|
||||
"VISCERAL_FAT",
|
||||
// v1.10.0 — computed scores (WX-C). Server-derived wellness scores
|
||||
// (Recovery / Stress / Strain) are minted by a nightly engine from the
|
||||
// user's already-stored signals; HealthKit ships no identifier for them
|
||||
// and they are never ingested, so they have no HK mapping by design.
|
||||
"RECOVERY_SCORE",
|
||||
"STRESS_SCORE",
|
||||
"STRAIN_SCORE",
|
||||
]);
|
||||
|
||||
describe("APPLE_HEALTH_TYPE_MAP", () => {
|
||||
|
||||
@@ -42,7 +42,11 @@ export type MeasurementCategory =
|
||||
| "hearing"
|
||||
| "environment"
|
||||
| "cardiovascular"
|
||||
| "metabolic";
|
||||
| "metabolic"
|
||||
// v1.10.0 — computed scores (WX-C). Server-derived wellness scores
|
||||
// (Recovery / Stress / Strain). Their own presentation cluster — they are
|
||||
// composite indices, not a raw signal in any of the above buckets.
|
||||
| "scores";
|
||||
|
||||
/**
|
||||
* Per-MeasurementType → category mapping. The map is exhaustive over
|
||||
@@ -153,6 +157,14 @@ export const MEASUREMENT_CATEGORIES: ReadonlyMap<
|
||||
["LOW_HEART_RATE_EVENT", "cardiovascular"],
|
||||
["WALKING_STEADINESS_EVENT", "activity"],
|
||||
["BREATHING_DISTURBANCE_EVENT", "sleep"],
|
||||
|
||||
// ── v1.10.0 — computed scores (WX-C) ──
|
||||
// Server-derived wellness scores share their own presentation cluster.
|
||||
// The category is only for list grouping — scores never participate in
|
||||
// trend/rollup analytics (they ARE a nightly-computed composite).
|
||||
["RECOVERY_SCORE", "scores"],
|
||||
["STRESS_SCORE", "scores"],
|
||||
["STRAIN_SCORE", "scores"],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -144,6 +144,14 @@ export function getPRDirection(
|
||||
case "LOW_HEART_RATE_EVENT":
|
||||
case "WALKING_STEADINESS_EVENT":
|
||||
case "BREATHING_DISTURBANCE_EVENT":
|
||||
// v1.10.0 — computed scores (WX-C). The server-derived wellness scores
|
||||
// are themselves derived composites recomputed nightly from the
|
||||
// underlying signals; a "personal record" on a derived score would
|
||||
// double-count the PRs of its own inputs and read as noise. They
|
||||
// explicitly have no PR direction, like the categorical events above.
|
||||
case "RECOVERY_SCORE":
|
||||
case "STRESS_SCORE":
|
||||
case "STRAIN_SCORE":
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,4 +271,50 @@ describe("measurement validation", () => {
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// v1.10.0 — computed scores (WX-C).
|
||||
describe("computed scores", () => {
|
||||
it("canonical unit is 'score' for each computed-score type", () => {
|
||||
expect(getUnitForType("RECOVERY_SCORE")).toBe("score");
|
||||
expect(getUnitForType("STRESS_SCORE")).toBe("score");
|
||||
expect(getUnitForType("STRAIN_SCORE")).toBe("score");
|
||||
});
|
||||
|
||||
it("plausibility range is 0..100 for a computed score", () => {
|
||||
expect(validateMeasurementRange("RECOVERY_SCORE", 0)).toBeNull();
|
||||
expect(validateMeasurementRange("RECOVERY_SCORE", 100)).toBeNull();
|
||||
expect(validateMeasurementRange("RECOVERY_SCORE", -1)).not.toBeNull();
|
||||
expect(validateMeasurementRange("RECOVERY_SCORE", 101)).not.toBeNull();
|
||||
});
|
||||
|
||||
it("rejects the server-owned COMPUTED source on a client write", () => {
|
||||
const parsed = createMeasurementSchema.safeParse({
|
||||
type: "RECOVERY_SCORE",
|
||||
value: 72,
|
||||
measuredAt: "2026-06-02T12:00:00Z",
|
||||
source: "COMPUTED",
|
||||
});
|
||||
expect(parsed.success).toBe(false);
|
||||
if (!parsed.success) {
|
||||
const issue = parsed.error.issues.find((i) =>
|
||||
i.path.includes("source"),
|
||||
);
|
||||
expect(issue, "expected a source-path rejection").toBeDefined();
|
||||
expect(issue!.message).toMatch(/server-owned/i);
|
||||
}
|
||||
});
|
||||
|
||||
it("still accepts a client-writable source for a score-typed manual row", () => {
|
||||
// The COMPUTED source is the only one this refine blocks; a MANUAL
|
||||
// write of any type stays valid (the source guard is orthogonal to
|
||||
// whether a UI ever offers manual score entry).
|
||||
const parsed = createMeasurementSchema.safeParse({
|
||||
type: "RECOVERY_SCORE",
|
||||
value: 72,
|
||||
measuredAt: "2026-06-02T12:00:00Z",
|
||||
source: "MANUAL",
|
||||
});
|
||||
expect(parsed.success).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -66,6 +66,29 @@ export const measurementTypeEnum = z.enum([
|
||||
"LOW_HEART_RATE_EVENT",
|
||||
"WALKING_STEADINESS_EVENT",
|
||||
"BREATHING_DISTURBANCE_EVENT",
|
||||
// ── v1.10.0 — computed scores (WX-C) ──
|
||||
// Server-derived wellness scores (0–100, unit `score`). Minted by a
|
||||
// nightly engine from the user's already-stored signals and persisted as a
|
||||
// `COMPUTED`-source row — never ingested from a client (the batch +
|
||||
// single-POST write surfaces reject the COMPUTED source). Only
|
||||
// RECOVERY_SCORE is computed in v1.10.0; the other two are defined now so
|
||||
// the later engines that compute them need no schema change.
|
||||
"RECOVERY_SCORE",
|
||||
"STRESS_SCORE",
|
||||
"STRAIN_SCORE",
|
||||
]);
|
||||
|
||||
/**
|
||||
* v1.10.0 — computed scores (WX-C). The closed set of server-derived
|
||||
* `*_SCORE` MeasurementTypes. Each is a 0–100 wellness score persisted with
|
||||
* `source = COMPUTED` by a nightly engine — never ingested from a client.
|
||||
* Kept as a `Set` so read/write paths can branch on "is this a computed
|
||||
* score?" the same way `EVENT_MEASUREMENT_TYPES` gates the event rows.
|
||||
*/
|
||||
export const SCORE_MEASUREMENT_TYPES: ReadonlySet<string> = new Set<string>([
|
||||
"RECOVERY_SCORE",
|
||||
"STRESS_SCORE",
|
||||
"STRAIN_SCORE",
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -104,8 +127,31 @@ export const measurementSourceEnum = z.enum([
|
||||
"WITHINGS",
|
||||
"IMPORT",
|
||||
"APPLE_HEALTH",
|
||||
// v1.10.0 — computed scores (WX-C). Server-owned / read-only: a `COMPUTED`
|
||||
// row is minted by a nightly engine, never written by a client. It is part
|
||||
// of this enum so the read/response shapes (and the iOS decoder) can decode
|
||||
// the rows it surfaces; the client-facing write surfaces reject it — see
|
||||
// `WRITABLE_MEASUREMENT_SOURCES` and the batch route's `batchSourceEnum`.
|
||||
"COMPUTED",
|
||||
]);
|
||||
|
||||
/**
|
||||
* v1.10.0 — computed scores (WX-C). The subset of `MeasurementSource` a
|
||||
* client may attribute on a write. `COMPUTED` is server-owned (a nightly
|
||||
* engine mints it) and `WITHINGS` / `IMPORT` are owned by the Withings
|
||||
* webhook + CSV importer respectively, so all three are excluded — letting a
|
||||
* client forge a row attributed to them would pollute the per-source
|
||||
* canonical picker with rows the server never produced. The single-entry
|
||||
* POST validates `source` against this set; the batch route mirrors the same
|
||||
* exclusion with its own narrower `{APPLE_HEALTH, MANUAL}` allowlist.
|
||||
*/
|
||||
export const WRITABLE_MEASUREMENT_SOURCES = [
|
||||
"MANUAL",
|
||||
"WITHINGS",
|
||||
"IMPORT",
|
||||
"APPLE_HEALTH",
|
||||
] as const;
|
||||
|
||||
const unitMap: Record<string, string> = {
|
||||
WEIGHT: "kg",
|
||||
BLOOD_PRESSURE_SYS: "mmHg",
|
||||
@@ -200,6 +246,12 @@ const unitMap: Record<string, string> = {
|
||||
LOW_HEART_RATE_EVENT: "event",
|
||||
WALKING_STEADINESS_EVENT: "event",
|
||||
BREATHING_DISTURBANCE_EVENT: "event",
|
||||
// ── v1.10.0 — computed scores (WX-C) ──
|
||||
// Server-derived 0–100 wellness scores. The canonical unit is the bare
|
||||
// "score" so the value reads sensibly anywhere it surfaces.
|
||||
RECOVERY_SCORE: "score",
|
||||
STRESS_SCORE: "score",
|
||||
STRAIN_SCORE: "score",
|
||||
};
|
||||
|
||||
export function getUnitForType(type: string): string {
|
||||
@@ -343,6 +395,12 @@ const VALUE_RANGES: Record<string, { min: number; max: number }> = {
|
||||
LOW_HEART_RATE_EVENT: { min: 1, max: 1 },
|
||||
WALKING_STEADINESS_EVENT: { min: 1, max: 1 },
|
||||
BREATHING_DISTURBANCE_EVENT: { min: 1, max: 1 },
|
||||
// ── v1.10.0 — computed scores (WX-C) ──
|
||||
// Server-derived 0–100 scores. The plausibility band pins them to the
|
||||
// score range so a malformed store (a stray negative or > 100) is rejected.
|
||||
RECOVERY_SCORE: { min: 0, max: 100 },
|
||||
STRESS_SCORE: { min: 0, max: 100 },
|
||||
STRAIN_SCORE: { min: 0, max: 100 },
|
||||
};
|
||||
|
||||
export function validateMeasurementRange(
|
||||
@@ -395,7 +453,16 @@ export const createMeasurementSchema = z
|
||||
"Blood glucose measurements require a context (fasting/postprandial/random/bedtime); other types must not set one.",
|
||||
path: ["glucoseContext"],
|
||||
},
|
||||
);
|
||||
)
|
||||
// v1.10.0 — computed scores (WX-C). The `COMPUTED` source is server-owned:
|
||||
// a nightly engine mints those rows, a client never does. Reject it here
|
||||
// so the single-entry POST cannot forge a server-derived row (the batch
|
||||
// route enforces the same exclusion through its `{APPLE_HEALTH, MANUAL}`
|
||||
// allowlist).
|
||||
.refine((data) => data.source !== "COMPUTED", {
|
||||
message: "The COMPUTED source is server-owned and cannot be set by a client.",
|
||||
path: ["source"],
|
||||
});
|
||||
|
||||
export const updateMeasurementSchema = z.object({
|
||||
value: z.number().min(0).max(500000).optional(),
|
||||
|
||||
Reference in new issue
Block a user