Files
healthlog/docs/plan.md
T
av 2fa8287c8b план и README синхронизированы с состоянием
- шаг 3 отмечен как почти готовый: частичный разбор закрыт, остались
  тренировки, reindex и словарь категориальных значений
- ближайшая цель — reindex: после миграции доставки числятся pending, а
  подобрать их некому
- находок в разведке 50, не 46
2026-08-01 21:30:59 +03:00

12 KiB
Raw Blame History

План

Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу.

Это порядок и его обоснование, а не список работ. Единицы работы живут в беклоге — одна задача, один файл, свой приоритет. План отвечает «почему в таком порядке», беклог — «что брать следующим».

Ближайшая цель

Метрики разбираются и ложатся в часовые объекты: тела перестали быть недифференцированной кучей. Блокеры, накопившиеся из ревью, разобраны — их в беклоге ноль.

Дальше — reindex, и он сейчас срочнее остального остатка шага 3. После миграции 00005 доставки числятся pending, а подобрать их некому: код пересборки не написан. Данные целы (тела в архиве, объекты в витрине), но учёт честно говорит «этим разбором не смотрели», и так будет, пока пересборки нет. Тем же кодом закрывается половина задачи «разнести ответ и свёртку».

Потом — остаток шага 3: тренировки и записи со своими id (это половина потока: workouts и stateOfMind принимаются и хранятся, но не разбираются), словарь категориальных значений.

Разведка закончена: правило вывода слоя, модель идентичности и формы точки проверены на живом потоке, выводы — в local-research.md, 50 находок.

Шаги

  • 1. Каркас. Taskfile.yml, .golangci.yml, CLAUDE.md, TOML-конфиг с валидацией на старте, логгер, подкоманда serve с /healthz.
  • 2. Приём без разбора. POST /api/v1/ingest: лимит тела, gzip, запись тела в архив, строка в delivery. Разбора ещё нет. ← подключаем телефон по локальной сети
  • [~] 3. Разбор и хранилище. Метрики — сделано (bucket, internal/hae, internal/canon, internal/fold), вместе с частичным разбором: секции, которых разбор не покрывает, перечисляются, доставка получает статус partial и список непокрытых ключей. Остались тренировки и записи со своими 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).
  • Ретеншен сырого архива — удаление доставок старше последнего проверенного экспорта (не фиксированный срок: журнал не должен рваться). Пока архив не подчищается; включить после того, как разбор устоится. Предусловие снято: статус partial и список непокрытых секций готовы, и ретеншен обязан спрашивать статус, а не считать parsed разрешением. Вместе с ним действует правило — задача, которая начинает разбирать секцию, тем же изменением переводит partial-строки с этим ключом в pending.
  • Порог sealed — с какого возраста час считается запечатанным. Ставим по факту: сначала пишем WARN на изменение старых объектов и смотрим, какая глубина досчёта встречается в жизни (наблюдалось до 22 минут, находка 10).
  • Месячный проход по ручным секциям. Симптомы и лекарства заводятся задним числом на недели; количественным метрикам недельного прохода хватает. Заводить, когда эти секции появятся в потоке живьём.
  • Алерт «данных нет N часов». Тихо сломавшаяся автоматизация — главный эксплуатационный риск коллектора. В v1 факт виден в /stats; активное уведомление добавим после.
  • Аннотации к схемам — человеческие описания метрик поверх выведенных схем, либо рукописный каталог. Выбор зависит от того, насколько стабильным окажется формат; меняться он может только с обновлением Health Auto Export, а это отслеживается.
  • Схема тренировок — глубину вывода определим по факту, когда увидим, как приходят маршруты.
  • Отказ от heartbeatSeries. 93% объёма HRV (находка 39) ради данных, которых нет ни в одном планируемом запросе. Решать, когда станет ясна цена хранения нижнего слоя за год.
  • Выгрузка в parquet — отдельной командой, на случай тяжёлой аналитики снаружи. DuckDB читает и parquet, и файл SQLite напрямую, поэтому спешить некуда: дверь открыта без миграции.
  • NDJSON-поток для больших выборок из read API.
  • Разворачивание маршрутов тренировок в отдельную таблицу — если появится клиент, которому мало отдачи тренировки одним пакетом.