- фазы сна, контекст пульса и имена тренировок попадают в реестр `category_value` (миграция 00010): строка хранится дословно, выведенный код лежит рядом отдельной записью, а не полем внутри точки - словарь и синонимы кодов живут в бинаре (`internal/healthkit`); локаль из `Accept-Language` сужает поиск, но в ключ реестра не входит — заголовков в сыром архиве нет - наблюдение входит в отпечаток витрины, выведенный код — нет: он производная от словаря, а не от журнала
5.6 KiB
Код HealthKit кладётся реестром рядом, а не полем внутри точки
- Дата: 2026-08-03
- Источник: openspec/changes/archive/2026-08-03-slovar-kategorialnyh-znachenij/design.md
Решение
Стабильный код HealthKit для локализованной строки хранится отдельной строкой
таблицы category_value с ключом (метрика, поле, значение), а не полем
value_code внутри точки, как рисовал architecture.md. Словарь и таблица
синонимов живут в бинаре (internal/healthkit), а не в базе. Наблюдение входит
в отпечаток витрины, выведенный код — нет.
Почему
Рассматривались три формы, и отвергнутые названы вместе с ценой.
Поле внутри точки — отвергнуто. Цитата источника: «Точка хранится
исходными байтами; дописать в неё ключ можно только пересериализацией, а она
теряет литерал (1.0 → 1, целые больше 2^53 сдвигаются, невалидный UTF-8 →
U+FFFD) — ровно то, от чего Point.Raw защищает. Побайтовая врезка в чужой
JSON — фокус, а не решение. Параллельный массив кодов в bucket завёл бы
производную величину в путь слияния и хеширования: правило полноты, тай-брейк и
content_hash пришлось бы учить носить код, не давая ему влиять на исход.
Правка на поверхности critical-инвариантов ради нуля новых сведений — код есть
функция от того, что уже лежит».
Код нигде не хранится, выводится на чтении — отвергнуто по одной причине:
«тогда код недостижим ничем, кроме бинаря. Владелец сегодня читает витрину
sqlite на хосте (Read API ещё нет), а вся задача затевается против того, что
„клиент угадывает словарь“. Реестр без кода сообщает только „такая строка
была“ — это половина ответа».
Словарь в базе, а не в бинаре — отвергнуто: «словарь стал бы входом,
которого нет в журнале, и import + replay перестал бы задавать состояние
однозначно. stateOfMind уже единственная дыра в журнале; вторую заводить
незачем».
Код вне отпечатка — обратная сторона того же решения: «Ключ и провенанс —
функция журнала; code — функция журнала и версии словаря в бинаре. Включи
его в отпечаток, и он перестал бы отвечать на свой единственный вопрос („дал ли
повтор журнала то же состояние“) ровно тогда, когда его задают: всякое
пополнение словаря — а оно объявлено рабочим циклом — давало бы расхождение при
побайтно совпавшем журнале, и человек, принимающий необратимое решение о
подмене базы, читал бы это как дефект».
Prior art: FHIR ConceptMap (отображение «чужая система значений → своя») и
CodeSystem с replaced-by для устаревших имён — те же два отношения,
разведённые по разным сущностям. Форма взята, реализация FHIR отвергнута ценой.
Последствия
+Инвариант «точки хранятся дословно» не тронут вовсе: точка не меняется ни байтом, обратное преобразование возможно всегда.+Пути слияния, тай-брейка иcontent_hashне знают о кодах — правки на поверхностиcritical-инвариантов не потребовалось.+Пополнение словаря меняет десяток строк реестра, а не каждый объект с фазами сна; отпечаток при этом не двигается, потому что код в него не входит.−Потребитель обязан делать соединение по(метрика, поле, значение)вместо чтения одного поля. Форма ответа Read API это скроет, когда он появится.−Код в базе отстаёт от словаря в бинаре для строк, переставших приезжать. Лечится пересборкой; на сходимость не влияет.−Ключ реестра зафиксирован миграцией00010: смена формы ключа стоит второй миграции и пересборки.