- экспорт Apple это снапшот всей истории, доставки после его даты — события поверх; состояние пересобирается как import(экспорт) + replay(доставки) - отсюда ретеншен архива меняется с произвольных 14 дней на «до следующего проверенного экспорта» (~2 ГБ за квартал, измерено), а свёртка обязана быть детерминированной — воспроизведение строго по received_at - названы границы модели: stateOfMind в экспорт не попадает вовсе, а верхние слои за периоды с удалёнными доставками не воскресают и досчитываться не должны — каталог обязан говорить это честно
9.5 KiB
План
Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу.
Это порядок и его обоснование, а не список работ. Единицы работы живут в беклоге — одна задача, один файл, свой приоритет. План отвечает «почему в таком порядке», беклог — «что брать следующим».
Ближайшая цель
Приём работает, поток настоящих пакетов копится в сыром архиве. Дальше — разбор и хранилище (шаг 3): всё, что нужно, чтобы данные перестали быть недифференцированной кучей тел запросов.
Разведка закончена: правило вывода слоя, модель идентичности и формы точки проверены на живом потоке, выводы — в local-research.md, 46 находок.
Шаги
- 1. Каркас.
Taskfile.yml,.golangci.yml,CLAUDE.md, TOML-конфиг с валидацией на старте, логгер, подкомандаserveс/healthz. - 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: пока импорт экспорта не написан, помечать что-либо устаревшим не на основании чего.
Отложено
Отсев идентичных тел доставок поВычеркнуто: находка 2 показала, что порядок ключей в JSON нестабилен, поэтому два пакета с одними и теми же данными почти никогда не совпадают побайтно — хеш тела не сработает. Дедупликация возможна только по канонизированному содержимому, а это и делает хеш часового объекта. Отдельная механика не нужна.sha256.Вторая автоматизация без группировки.Сделано на телефоне: метрики здоровья идут в трёх разрезах — несуммированном для несуммируемых метрик, минутном и часовом для всех.Пометка локализованных полей в схемах.Переросло в шаг 3: одной пометки мало, нужен словарь кодов, иначе не сойтись с родным экспортом (находка 37).- Ретеншен сырого архива — удаление доставок старше последнего проверенного экспорта (не фиксированный срок: журнал не должен рваться). Пока архив не подчищается; включить после того, как разбор устоится.
- Порог
sealed— с какого возраста час считается запечатанным. Ставим по факту: сначала пишемWARNна изменение старых объектов и смотрим, какая глубина досчёта встречается в жизни (наблюдалось до 22 минут, находка 10). - Месячный проход по ручным секциям. Симптомы и лекарства заводятся задним числом на недели; количественным метрикам недельного прохода хватает. Заводить, когда эти секции появятся в потоке живьём.
- Алерт «данных нет N часов». Тихо сломавшаяся автоматизация — главный
эксплуатационный риск коллектора. В v1 факт виден в
/stats; активное уведомление добавим после. - Аннотации к схемам — человеческие описания метрик поверх выведенных схем, либо рукописный каталог. Выбор зависит от того, насколько стабильным окажется формат; меняться он может только с обновлением Health Auto Export, а это отслеживается.
- Схема тренировок — глубину вывода определим по факту, когда увидим, как приходят маршруты.
- Отказ от
heartbeatSeries. 93% объёма HRV (находка 39) ради данных, которых нет ни в одном планируемом запросе. Решать, когда станет ясна цена хранения нижнего слоя за год. - Выгрузка в parquet — отдельной командой, на случай тяжёлой аналитики снаружи. DuckDB читает и parquet, и файл SQLite напрямую, поэтому спешить некуда: дверь открыта без миграции.
- NDJSON-поток для больших выборок из read API.
- Разворачивание маршрутов тренировок в отдельную таблицу — если появится клиент, которому мало отдачи тренировки одним пакетом.