# Словарь категориальных значений → коды 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`.