Files
healthlog/docs/plan.md
T
av 37413bb551 change razbor-metrik-v-obekty заархивирован
Дельты влиты в openspec/specs (parsing, storage), задача убрана из беклога,
план отражает сделанную часть шага 3.

Не закрыт один пункт: живая доставка с телефона не разобрана — поток молчит
с 17:13, пауза началась до перезапуска сервиса.
2026-08-01 19:03:46 +03:00

107 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План
Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу.
Это **порядок и его обоснование**, а не список работ. Единицы работы живут в
[беклоге](backlog/README.md) — одна задача, один файл, свой приоритет. План
отвечает «почему в таком порядке», беклог — «что брать следующим».
## Ближайшая цель
Метрики разбираются и ложатся в часовые объекты: тела перестали быть
недифференцированной кучей. Дальше — доразобрать остаток шага 3
(тренировки и записи со своими `id`, `reindex`, словарь категориальных
значений) и разобрать блокеры, накопившиеся из ревью.
Разведка закончена: правило вывода слоя, модель идентичности и формы точки
проверены на живом потоке, выводы — в
[local-research.md](local-research.md), 47 находок.
## Шаги
- [x] **1. Каркас.** `Taskfile.yml`, `.golangci.yml`, `CLAUDE.md`, TOML-конфиг
с валидацией на старте, логгер, подкоманда `serve` с `/healthz`.
- [x] **2. Приём без разбора.** `POST /api/v1/ingest`: лимит тела, gzip,
запись тела в архив, строка в `delivery`. Разбора ещё нет.
**подключаем телефон по локальной сети**
- [~] **3. Разбор и хранилище.** Метрики — сделано (`bucket`, `internal/hae`,
`internal/canon`, `internal/fold`); остались тренировки и записи со
своими `id`, `reindex` и словарь категориальных значений. Миграции,
часовые объекты метрик
(`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.
- **Разворачивание маршрутов тренировок** в отдельную таблицу — если появится
клиент, которому мало отдачи тренировки одним пакетом.