добавлен словарь категориальных значений 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
+52
View File
@@ -46,6 +46,17 @@ SQLite (`modernc.org/sqlite`, чистый Go), миграции — goose, фа
│ created_at TEXT │ └──────────────────────────┘
│ updated_at TEXT │
└──────────────────────────┘
┌────────────────────────────┐
│ category_value │
│ ───────────────────────── │
│ metric TEXT ┐ │
│ field TEXT ├PK │
│ value TEXT ┘ │
│ code TEXT │
│ first_seen_utc TEXT │
│ first_delivery_id TEXT │
└────────────────────────────┘
```
Связь `bucket.first_delivery_id → delivery.id` **внешним ключом не объявлена**
@@ -156,6 +167,47 @@ ROWID` строка целиком, вместе со сжатым `payload`, ж
журнала, а не свёрнутая последней. Подробности и обоснование — в
`architecture.md`, раздел «Тренировки и прочие секции».
## `category_value` — реестр категориальных значений
Какие перечислимые строки поток приносил и какой у них стабильный код
HealthKit. HAE отдаёт фазу сна как «БДГ», контекст пульса как «Сидячий образ
жизни», тип тренировки как «В помещении Ходьба» — строками локали телефона, а
родной экспорт Apple говорит кодами; без словаря источники не сходятся
(находка 37). Словарь фаз сна выведен сопоставлением потока с экспортом за тот
же период (находка 43).
| Колонка | Смысл |
|---|---|
| `metric` | имя метрики или секции, **то же**, которым адресуется единица хранения (`sleep_analysis_summary` после разделения схем, `workouts` у тренировок). Второе имя для того же понятия развело бы наблюдение и объект по разным ключам |
| `field` | имя поля внутри точки или сущности дословно как у HAE: `value`, `context`, `name` |
| `value` | строка **дословно**, как прислал HAE. Код приписывается рядом, а не подменяет её: инвариант «точки хранятся дословно» это и означает |
| `code` | канонический код HealthKit. Пустая строка — законное состояние: «словарь этой строки не знает», и перечень таких строк есть заявка на пополнение словаря. **Это кэш**: код производен от словаря в бинаре, а не от журнала, и потому в отпечаток витрины не входит. Строка, переставшая приезжать, держит код прежнего словаря до пересборки |
| `first_seen_utc`, `first_delivery_id` | провенанс **первой** встречи, минимум по журналу `(received_at, id)`. Минимум идемпотентен при повторной свёртке той же доставки; счётчик встреч не идемпотентен и потому не заводится вовсе. Отвечает на вопрос «когда сменился язык телефона», а язык доставки восстанавливается по `delivery.headers` |
Ключ — тройка без локали, и это решение, а не упущение. Локаль приезжает
заголовком `Accept-Language`, а заголовков в сыром архиве нет: они были
заголовками запроса, а не телом. Доставка, восстановленная из осиротевшего
тела, приходит без локали — ключ с локалью положил бы вторую строку на то же
значение, то есть состояние стало бы функцией от того, уцелела ли учётная
строка. Локаль при выводе кода сужает поиск по словарю; её отсутствие вывода не
отменяет, если строка однозначна.
Таблица `WITHOUT ROWID`: обращение всегда по полному первичному ключу, а строк
единицы — на живом потоке различных значений по всем трём полям около
одиннадцати. Индексов нет: чтение идёт целиком, в порядке ключа.
Границы разбора не дают доставке положить больше 64 различных значений и
значение длиннее 128 байт (измерено: ~11 значений, самое длинное 36 байт).
Слишком длинное **отбрасывается со счётчиком, а не обрезается** — обрезанная
строка неотличима от настоящей и стала бы самостоятельным ключом; сама точка
при этом хранится целиком.
Data-миграции у таблицы нет и быть не может: коды выводятся из тел, а тела
лежат в архиве. Реестр рабочей витрины наполняется по мере свёртки новых
доставок и целиком — пересборкой. Отсюда первое расхождение отпечатков после
выкатки: оно законно, и отчёт `reindex` называет его ожидаемым классом
«появилась единица хранения».
## Представление данных
- **Точки часового объекта лежат сжатым BLOB** (`gzip`) в колонке `payload`.