- экспорт Apple это снапшот всей истории, доставки после его даты — события поверх; состояние пересобирается как import(экспорт) + replay(доставки) - отсюда ретеншен архива меняется с произвольных 14 дней на «до следующего проверенного экспорта» (~2 ГБ за квартал, измерено), а свёртка обязана быть детерминированной — воспроизведение строго по received_at - названы границы модели: stateOfMind в экспорт не попадает вовсе, а верхние слои за периоды с удалёнными доставками не воскресают и досчитываться не должны — каталог обязан говорить это честно
103 lines
9.5 KiB
Markdown
103 lines
9.5 KiB
Markdown
# План
|
||
|
||
Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу.
|
||
|
||
Это **порядок и его обоснование**, а не список работ. Единицы работы живут в
|
||
[беклоге](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.
|
||
- **Разворачивание маршрутов тренировок** в отдельную таблицу — если появится
|
||
клиент, которому мало отдачи тренировки одним пакетом.
|