добавлен словарь категориальных значений 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
@@ -0,0 +1,67 @@
-- +goose Up
-- Реестр категориальных значений: какие перечислимые строки поток приносил и
-- какой у них стабильный код HealthKit.
--
-- HAE отдаёт фазу сна как «БДГ», контекст пульса как «Сидячий образ жизни», тип
-- тренировки как «В помещении Ходьба» — строками локали телефона, а родной
-- экспорт Apple говорит кодами (`HKCategoryValueSleepAnalysisAsleepREM`).
-- Источники несопоставимы, и на этой сверке стоит устаревание нижнего слоя.
-- Словарь фаз сна выведен сопоставлением потока с экспортом за тот же период
-- (docs/research/apple-health.md, находка 43).
--
-- ПОЧЕМУ ОТДЕЛЬНАЯ ТАБЛИЦА, А НЕ ПОЛЕ РЯДОМ СО СТРОКОЙ В ТОЧКЕ. Точка хранится
-- исходными байтами; дописать в неё ключ можно только пересериализацией, а она
-- теряет литерал — ровно то, от чего эти байты и защищают. Параллельный массив
-- кодов в `bucket` завёл бы производную величину в путь слияния и хеширования:
-- правило полноты, тай-брейк и `content_hash` пришлось бы учить носить код, не
-- давая ему влиять на исход. Это правка на поверхности critical-инвариантов
-- ради нуля новых сведений — код есть функция от того, что уже лежит. И
-- пополнение словаря переписывало бы каждый объект с фазами сна; здесь меняется
-- десяток строк.
--
-- ПОЧЕМУ ЛОКАЛИ НЕТ В КЛЮЧЕ. Она приезжает заголовком `Accept-Language`, а
-- заголовков в сыром архиве не лежит: они были заголовками запроса, а не телом.
-- Доставка, восстановленная из осиротевшего тела, приходит без локали — и ключ
-- с локалью положил бы вторую строку на то же значение, то есть состояние стало
-- бы функцией от того, уцелела ли учётная строка. Ключ по трём полям — функция
-- одних тел. На каком языке приехала строка, восстанавливается по доставке
-- провенанса: `first_delivery_id` → `delivery.headers`.
--
-- ПОЧЕМУ ПРОВЕНАНС ТОЛЬКО ПЕРВОЙ ВСТРЕЧИ. Минимум по журналу идемпотентен при
-- повторной свёртке той же доставки; счётчик встреч не идемпотентен и сделал бы
-- состояние зависящим от числа прогонов. «Когда эта строка появилась впервые»
-- отвечает на вопрос о смене языка телефона — «сколько раз» не отвечает ни на
-- один заданный.
--
-- `code` — КЭШ. Он производная не от журнала, а от словаря в бинаре, и потому
-- в отпечаток витрины не входит (см. Fingerprint): включённый туда, он заставил
-- бы всякое пополнение словаря давать расхождение при побайтно совпавшем
-- журнале, а человек, принимающий по отпечатку необратимое решение о подмене
-- базы, читал бы это как дефект. Строка, переставшая приезжать, держит код
-- прежнего словаря до пересборки — осознанная цена, названная вслух.
--
-- Пустой код — законное состояние: он означает «словарь этой строки не знает».
-- Пустая строка, а не NULL: различать «кода нет» и «кода не считали» здесь
-- нечем — вывод кода идёт на каждой встрече, и третьего состояния у него не
-- бывает.
--
-- `WITHOUT ROWID`: обращение всегда по полному первичному ключу, а строк
-- единицы — на живом потоке различных значений по всем трём полям около
-- одиннадцати.
--
-- Data-миграции нет и быть не может: коды выводятся из ТЕЛ, а тела лежат в
-- архиве, а не в базе. Реестр рабочей витрины наполняется по мере свёртки новых
-- доставок и целиком — пересборкой. Отсюда первое расхождение отпечатков после
-- выкатки: оно законно, и отчёт `reindex` называет его ожидаемым классом.
CREATE TABLE category_value (
metric TEXT NOT NULL,
field TEXT NOT NULL,
value TEXT NOT NULL,
code TEXT NOT NULL,
first_seen_utc TEXT NOT NULL,
first_delivery_id TEXT NOT NULL,
PRIMARY KEY (metric, field, value)
) WITHOUT ROWID;
-- +goose Down
DROP TABLE category_value;