# План Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу. Это **порядок и его обоснование**, а не список работ. Единицы работы живут в [беклоге](backlog/README.md) — одна задача, один файл, свой приоритет. План отвечает «почему в таком порядке», беклог — «что брать следующим». ## Ближайшая цель Приём работает, поток настоящих пакетов копится в сыром архиве. Дальше — разбор и хранилище (шаг 3): всё, что нужно, чтобы данные перестали быть недифференцированной кучей тел запросов. Разведка закончена: правило вывода слоя, модель идентичности и формы точки проверены на живом потоке, выводы — в [local-research.md](local-research.md), 46 находок. ## Шаги - [x] **1. Каркас.** `Taskfile.yml`, `.golangci.yml`, `CLAUDE.md`, TOML-конфиг с валидацией на старте, логгер, подкоманда `serve` с `/healthz`. - [x] **2. Приём без разбора.** `POST /api/v1/ingest`: лимит тела, gzip, запись тела в архив, строка в `delivery`. Разбора ещё нет. ← **подключаем телефон по локальной сети** - [ ] **3. Разбор и хранилище.** Миграции, часовые объекты метрик (`bucket`, ключ `метрика + слой + час`) + `workout`/`record` по своим `id`. Вывод слоя из выравнивания меток; развод `sleep_analysis` на два имени (находка 38). Канонизация с округлением чисел и хеш содержимого как детектор изменений. Слияние точек: при столкновении выигрывает **более полная** точка, а не последняя (находка 41). Три формата времени: локальное со смещением, RFC 3339 Z, Unix-эпоха внутри `heartbeatSeries` (находка 39). Словарь `(локаль, строка) → код HealthKit` для переведённых значений (находка 37). `healthlog reindex`. - [ ] **4. Каталог и род агрегации.** Род (`cumulative`/`instant`/`unknown`) **измеряется** сверкой слоёв между собой, а не размечается руками (находка 40). Каталог метрик со слоями, диапазонами и родом. - [ ] **5. Read API.** Точки метрики из часовых объектов, выбор слоя, необязательная свёртка по сетке. Огрубление, когда сетка не задана; ошибка со списком доступных сеток, когда задана явно. Тренировки, записи. - [ ] **6. Самоописание.** Выведенные из данных схемы содержимого со статистикой + статичная схема контракта API. - [ ] **7. MCP.** Эндпоинт того же процесса, транспорт Streamable HTTP, токен чтения общий с Read API. Три инструмента: каталог, значения за период, значения с разбивкой. - [ ] **8. `healthlog import`.** Родной экспорт Apple Health: разбор `экспорт.xml` в слой `sample`, маршруты GPX, ЭКГ из CSV. Заливка полной истории кусками по годам. - [ ] **9. Устаревание нижнего слоя.** Пометка данных HAE старше проверенного экспорта. Проверка покрытия — непрерывность по дням и сходимость сумм с часовым слоем. Пометка ≠ удаление: удаление включаем только после того, как восстановление из экспорта отработает на живых данных хотя бы раз. - [ ] **10. Наблюдаемость.** `/stats`: последняя доставка, счётчики, тишина по потоку, список строк без кода в словаре. - [ ] **11. Деплой.** `Dockerfile`, сборка образа локально, доставка на rivendell, конфиг Caddy, поддомены приёма и чтения, токены. Порядок неслучаен. Шаг 4 стоит перед Read API, потому что без измеренного рода свёртка на шаге 5 неотличима от угадывания. Шаг 8 стоит перед 9: пока импорт экспорта не написан, помечать что-либо устаревшим не на основании чего. ## Отложено - ~~Отсев идентичных тел доставок по `sha256`.~~ **Вычеркнуто:** находка 2 показала, что порядок ключей в JSON нестабилен, поэтому два пакета с одними и теми же данными почти никогда не совпадают побайтно — хеш тела не сработает. Дедупликация возможна только по канонизированному содержимому, а это и делает хеш часового объекта. Отдельная механика не нужна. - ~~Вторая автоматизация без группировки.~~ **Сделано на телефоне:** метрики здоровья идут в трёх разрезах — несуммированном для несуммируемых метрик, минутном и часовом для всех. - ~~Пометка локализованных полей в схемах.~~ **Переросло в шаг 3:** одной пометки мало, нужен словарь кодов, иначе не сойтись с родным экспортом (находка 37). - **Ретеншен сырого архива** — удаление доставок старше последнего проверенного экспорта (не фиксированный срок: журнал не должен рваться). Пока архив не подчищается; включить после того, как разбор устоится. - **Порог `sealed`** — с какого возраста час считается запечатанным. Ставим по факту: сначала пишем `WARN` на изменение старых объектов и смотрим, какая глубина досчёта встречается в жизни (наблюдалось до 22 минут, находка 10). - **Месячный проход по ручным секциям.** Симптомы и лекарства заводятся задним числом на недели; количественным метрикам недельного прохода хватает. Заводить, когда эти секции появятся в потоке живьём. - **Алерт «данных нет N часов».** Тихо сломавшаяся автоматизация — главный эксплуатационный риск коллектора. В v1 факт виден в `/stats`; активное уведомление добавим после. - **Аннотации к схемам** — человеческие описания метрик поверх выведенных схем, либо рукописный каталог. Выбор зависит от того, насколько стабильным окажется формат; меняться он может только с обновлением Health Auto Export, а это отслеживается. - **Схема тренировок** — глубину вывода определим по факту, когда увидим, как приходят маршруты. - **Отказ от `heartbeatSeries`.** 93% объёма HRV (находка 39) ради данных, которых нет ни в одном планируемом запросе. Решать, когда станет ясна цена хранения нижнего слоя за год. - **Выгрузка в parquet** — отдельной командой, на случай тяжёлой аналитики снаружи. DuckDB читает и parquet, и файл SQLite напрямую, поэтому спешить некуда: дверь открыта без миграции. - **NDJSON-поток** для больших выборок из read API. - **Разворачивание маршрутов тренировок** в отдельную таблицу — если появится клиент, которому мало отдачи тренировки одним пакетом.