- открытых вопросов не осталось: три решения владельца доведены до берущегося вида, по entity-without-parsed-label принято хранить с NULL-меткой после Read API - unseen-sections-check сжата до остатка — активная проверка появления секции; разбор невиденных секций из неё вынут, вслепую он не пишется - спринт под целью parsing-and-storage: categorical-value-dictionary и unseen-sections-check, обеим написаны критерии приёмки с оракулами
68 lines
5.2 KiB
Markdown
68 lines
5.2 KiB
Markdown
# Словарь категориальных значений → коды HealthKit
|
||
|
||
- **Секция:** ядро
|
||
- **Зачем:** Фазы сна и типы тренировок приходят строками русской локали — с экспортом Apple их не сверить
|
||
- **Теги:** goal:parsing-and-storage
|
||
|
||
HAE отдаёт перечислимые значения строками локали телефона: «БДГ», «Сидячий
|
||
образ жизни», «В помещении Ходьба». Родной экспорт Apple при этом говорит
|
||
кодами (`HKCategoryValueSleepAnalysisAsleepREM`) — источники несопоставимы
|
||
(находка 37).
|
||
|
||
Три следствия, и третье решающее: клиент угадывает словарь; смена языка
|
||
телефона молча расколет историю; сверить покрытие экспортом нечем — а на этой
|
||
сверке стоит устаревание нижнего слоя.
|
||
|
||
Решение (вариант «б»): строка хранится **дословно**, рядом кладётся выведенный
|
||
код. Словарь ключуется парой `(локаль, строка)`, локаль берётся из
|
||
`Accept-Language`. Незнакомая строка → пустой код, а не догадка.
|
||
|
||
**Словарь фаз сна уже выведен** сопоставлением потока с экспортом за тот же
|
||
период (находка 43) — составлять руками не нужно:
|
||
|
||
```
|
||
Основная → AsleepCore Бодрствование → Awake БДГ → AsleepREM
|
||
Глубокий → AsleepDeep В кровати → InBed Во сне → AsleepUnspecified
|
||
```
|
||
|
||
Тем же способом добираются `heart_rate.context` и типы тренировок.
|
||
|
||
Осложнение, всплывшее на истории экспортов: **коды тоже не вечны.** Одни и те
|
||
же записи сна приезжают как `…Asleep` в экспорте 2021 года и как
|
||
`…AsleepUnspecified` в экспорте 2026-го: Apple переименовала значение и
|
||
переписывает историю при выгрузке (находка 43). Значит словарь должен
|
||
переживать переименование самих кодов, иначе после обновления iOS история
|
||
расколется вторично — уже на «стабильной» стороне. Простейшее решение: хранить код как есть, а
|
||
эквивалентность старых и новых имён держать отдельной таблицей синонимов.
|
||
|
||
`stateOfMind` в словаре не нуждается — он и так шлёт коды HealthKit.
|
||
|
||
## Критерии приёмки
|
||
|
||
- фаза сна из потока («БДГ») и из экспорта Apple
|
||
(`HKCategoryValueSleepAnalysisAsleepREM`) за один период сопоставляются
|
||
напрямую — оракул: запрос на живой базе за период с известным перекрытием,
|
||
ноль несопоставимых строк
|
||
- строка сохранена **дословно**, код лежит рядом отдельным полем — оракул: тест
|
||
разбора на реальном пакете HAE из `internal/hae/testdata`
|
||
- незнакомая строка даёт пустой код, разбор не падает, а событие попадает в
|
||
счётчик — оракул: тест на выдуманной фазе сна плюс проверка счётчика
|
||
- переименование кода самой Apple (`…Asleep` → `…AsleepUnspecified`, находка 43)
|
||
не раскалывает историю — оракул: тест на паре синонимов, обе формы сходятся
|
||
в один код
|
||
- повторный прогон живого архива даёт то же состояние — оракул:
|
||
`task verify:archive`
|
||
|
||
Критерий «`/stats` показывает строки без кода» снят при переоценке 2026-08-03:
|
||
`/stats` ещё нет ([stats-endpoint](stats-endpoint.md)), и вешать приёмку на
|
||
несуществующий оракул значит либо блокировать задачу, либо принять её
|
||
непроверенной. Наблюдаемость закрывается счётчиком; показ в `/stats` — строка
|
||
задачи наблюдаемости, а не этой.
|
||
|
||
## Рамки
|
||
|
||
Схема трогается: у категориального значения появляется поле кода, плюс таблица
|
||
синонимов. Дословную строку не заменяем и не нормализуем — инвариант «точки
|
||
хранятся дословно». Пересборка обязана оставаться детерминированной, оракул
|
||
тот же `task verify:archive`.
|