добавлен словарь категориальных значений HAE → коды HealthKit

- фазы сна, контекст пульса и имена тренировок попадают в реестр
  `category_value` (миграция 00010): строка хранится дословно, выведенный код
  лежит рядом отдельной записью, а не полем внутри точки
- словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из
  `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в
  сыром архиве нет
- наблюдение входит в отпечаток витрины, выведенный код — нет: он производная
  от словаря, а не от журнала
This commit is contained in:
av
2026-08-04 07:32:19 +03:00
parent eb3fca77ee
commit 1b649ba3d5
40 changed files with 4317 additions and 42 deletions
+31 -7
View File
@@ -1103,21 +1103,45 @@ HAE отдаёт перечислимые значения строками из
Он объявлен источником истины, и на нём держится ретеншен нижнего слоя —
но сверить покрытие по этим полям было бы нечем.
Поэтому строка **хранится дословно, а рядом кладётся выведенный код**:
Поэтому строка **хранится дословно, а рядом кладётся выведенный код**
отдельной строкой реестра `category_value`, а не полем внутри точки:
```
value "БДГ" ← как прислал HAE
value_code "HKCategoryValueSleepAnalysisAsleepREM" ← выведено по словарю
category_value sleep_analysis / value / "БДГ" → HKCategoryValueSleepAnalysisAsleepREM
точка {"date": …, "value": "БДГ", …} ← не тронута
```
Словарь ключуется парой `(локаль, строка)`; локаль берётся из
`Accept-Language`, который мы уже сохраняем (находка 32). Для незнакомой
строки код пустой — пустота честнее догадки, и она же видна в `/stats` как
список того, что пора добавить в словарь.
Рядом, а не внутри, по трём причинам: точка хранится исходными байтами и
дописать в неё ключ можно только пересериализацией; параллельный массив кодов в
`bucket` завёл бы производную величину в путь слияния и хеширования; пополнение
словаря переписывало бы каждый объект с фазами сна. Обоснование целиком — в
[журнале решений](adr/README.md).
Ключ реестра — `(метрика, поле, значение)`. Словарь при этом ключуется парой
`(локаль, строка)`, локаль берётся из `Accept-Language` (находка 32) и **в ключ
реестра не входит**: заголовков в сыром архиве нет, и ключ с локалью сделал бы
состояние функцией от того, уцелела ли учётная строка. Локаль сужает поиск; её
отсутствие вывода не отменяет, если строка однозначна по всем локалям.
Словарь и таблица синонимов кодов живут в бинаре (`internal/healthkit`), а не в
базе: словарь, наполняемый руками, стал бы входом, которого нет в журнале, и
`import + replay` перестал бы задавать состояние однозначно. Синонимы нужны
потому, что коды тоже не вечны: Apple переименовала `…Asleep` в
`…AsleepUnspecified` и переписывает историю при выгрузке (находка 43).
Для незнакомой строки код пустой — пустота честнее догадки, и перечень таких
строк в реестре есть заявка на пополнение словаря. Счётчик неизвестных строк
уходит в лог свёртки числом; сами строки — данные о здоровье и в лог не
попадают.
Дословность инварианта не нарушена: код **приписывается**, а не подменяет
строку. Обратное преобразование всегда возможно.
Реестр — единица хранения витрины и входит в отпечаток **наблюдением**, но не
выведенным кодом: код производен от словаря в бинаре, а не от журнала, и в
отпечатке он превратил бы всякое пополнение словаря в расхождение при совпавшем
журнале.
### Тренировки и прочие секции
<!-- канон: поведение → openspec/specs/parsing -->