feat(healthkit): thread the device-event verdict through batch ingest

Accept the optional HKCategoryValue codepoint on each batch entry and
persist the resolved rhythm_classification on the stored row. The wire
field is additive and ignored for non-event identifiers, so every
existing caller is byte-for-byte unchanged. Regenerate the OpenAPI
contract.
This commit is contained in:
Marc-André Bombeck committed 2026-06-02 22:07:45 +02:00
1 parent b06b1cada4
commit a45325af52
3 files changed
+52

No files matched your search

+33
View File
@@ -383,6 +383,11 @@ paths:
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
- in: query
name: from
schema:
@@ -523,6 +528,11 @@ paths:
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
value:
type: number
measuredAt:
@@ -2090,6 +2100,11 @@ paths:
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
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."
@@ -2544,6 +2559,14 @@ components:
type: integer
minimum: 0
maximum: 20
categoryValue:
description: v1.10.0 — `HKCategoryValue` codepoint for an EVENT-class category sample (irregular-rhythm, high/low-HR,
walking-steadiness, breathing-disturbance). Carries the device's own classification verdict / severity,
which the server resolves to a stored `rhythmClassification`. HealthLog stores ONLY the device's result — it
never re-classifies. Ignored for non-event identifiers.
type: integer
minimum: 0
maximum: 20
externalId:
type: string
minLength: 1
@@ -3758,6 +3781,11 @@ components:
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
value:
type: number
unit:
@@ -4055,6 +4083,11 @@ components:
- STAIR_ASCENT_SPEED
- STAIR_DESCENT_SPEED
- BREATHING_DISTURBANCES
- IRREGULAR_RHYTHM_NOTIFICATION
- HIGH_HEART_RATE_EVENT
- LOW_HEART_RATE_EVENT
- WALKING_STEADINESS_EVENT
- BREATHING_DISTURBANCE_EVENT
value:
type: number
unit:
+10
View File
@@ -90,6 +90,12 @@ const batchEntrySchema = z.object({
startDate: z.iso.datetime({ offset: true }),
endDate: z.iso.datetime({ offset: true }),
sleepStage: z.number().int().min(0).max(20).optional(),
// v1.10.0 — categorical events (WX-B). The integer `HKCategoryValue`
// codepoint for an EVENT-class category sample (irregular-rhythm,
// high/low-HR, walking-steadiness, breathing-disturbance). Carries the
// device's own verdict / severity; resolved to a `rhythmClassification`
// by `mapAppleHealthEntry`. Ignored for every non-event identifier.
categoryValue: z.number().int().min(0).max(20).optional(),
externalId: z.string().min(1).max(120),
externalSourceVersion: z.string().min(1).max(120).optional(),
// v1.8.6 W6 — optional per-entry source tag. Defaults to
@@ -215,6 +221,7 @@ async function postBatch(request: NextRequest): Promise<Response> {
startDate: entry.startDate,
endDate: entry.endDate,
sleepStage: entry.sleepStage,
categoryValue: entry.categoryValue,
});
if (!mapped) {
@@ -254,6 +261,9 @@ async function postBatch(request: NextRequest): Promise<Response> {
externalId: entry.externalId,
externalSourceVersion: entry.externalSourceVersion ?? null,
sleepStage: mapped.sleepStage ?? null,
// v1.10.0 — categorical events (WX-B). The device's own verdict /
// severity for an EVENT row; null for every continuous reading.
rhythmClassification: mapped.rhythmClassification ?? null,
// v1.4.25 W8c — pass the iOS-supplied device-type through to
// the row. Stays null for pre-W8c iOS builds; the canonical
// picker treats null as `unknown` so legacy ingest keeps
+9
View File
@@ -378,6 +378,15 @@ const batchEntrySchema = z
.describe(
"HKCategoryValueSleepAnalysis codepoint; only for sleep samples.",
),
categoryValue: z
.number()
.int()
.min(0)
.max(20)
.optional()
.describe(
"v1.10.0 — `HKCategoryValue` codepoint for an EVENT-class category sample (irregular-rhythm, high/low-HR, walking-steadiness, breathing-disturbance). Carries the device's own classification verdict / severity, which the server resolves to a stored `rhythmClassification`. HealthLog stores ONLY the device's result — it never re-classifies. Ignored for non-event identifiers.",
),
externalId: z
.string()
.min(1)