fix(ingest): keep the batch sample's unit captured, not authoritative

The record-unit conversion is now opt-in per caller and only the
`export.xml` archive path opts in. There the attribute is authoritative:
Apple stamps every quantity `<Record>` with the account's own display
unit, so `km` means kilometres and the reading is meaningless without it.

`POST /api/measurements/batch` keeps its documented contract — `unit` is
captured for audit and never read. Nothing in the tree pins the native
client's unit strings for the seventeen convertible identifiers, and a
client that reads a sample in metres while stamping the person's display
unit into the field would have had every walking distance multiplied by a
thousand on ingest. Reading the field there is a wire change and needs
the client's strings pinned first.

The mapping-table audit and the shared conversion module stay. The batch
path's identity behaviour is written down where it is decided and pinned
twice: every convertible identifier round-trips a sibling unit unchanged
by default, and a walking-distance entry sent as `km` is stored as it
arrived. The OpenAPI description says what the server actually does.

Review finding H2.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 00:24:59 +02:00
1 parent e031e2211c
commit 65e9ecbde6
6 files changed
+161 -30

No files matched your search

+1 -1
View File
@@ -58,7 +58,7 @@ const batchEntrySchema = z
.min(1)
.max(60)
.describe(
"Apple's `HKUnit` string for `value`. The server converts the reading into the unit it stores the identifier in when the two differ and the factor is known (`km` to `m`, `lb` to `kg`, `degF` to `degC`, and their siblings); a unit the server cannot place leaves the reading untouched.",
"Apple's `HKUnit` string for `value`, captured for audit. The server does NOT convert from it: `value` is read in the unit HealthLog stores the identifier in (`m` for walking distance, `kg` for body mass, `kcal` for energy, `degC` for temperature). Send the sample in that unit.",
),
startDate: z.iso.datetime({ offset: true }),
endDate: z.iso.datetime({ offset: true }),