Files
healthlog/docs/adr/ADR-2026-08-03-kod-ryadom-so-strokoj-reestrom.md
av 3d24248075 docs: документация приведена к канону av-dev-pm 4
- каждая запись каталога задач получила тип вместо тега kind: и префикса
  заголовка; секция роадмапа «Разработка» стала «Сопровождением», порядок
  секций канонический
- поправлены протухшие факты: нереализованные маршруты Read API, MCP и
  `healthlog import`, словарь слоёв в инварианте, семантика гейта по покрытию
  диффа, периметр перестал дублировать security.md
- замер слияния переведён с находки 49 на находку 54, заполнены Purpose спек
  storage и parsing
2026-08-05 19:09:35 +03:00

5.6 KiB
Raw Permalink Blame History

Код 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.01, целые больше 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: смена формы ключа стоит второй миграции и пересборки.