- шаг 3 отмечен как почти готовый: частичный разбор закрыт, остались тренировки, reindex и словарь категориальных значений - ближайшая цель — reindex: после миграции доставки числятся pending, а подобрать их некому - находок в разведке 50, не 46
12 KiB
План
Живой документ: шаги отмечаем по мере готовности, отложенное копится внизу.
Это порядок и его обоснование, а не список работ. Единицы работы живут в беклоге — одна задача, один файл, свой приоритет. План отвечает «почему в таком порядке», беклог — «что брать следующим».
Ближайшая цель
Метрики разбираются и ложатся в часовые объекты: тела перестали быть недифференцированной кучей. Блокеры, накопившиеся из ревью, разобраны — их в беклоге ноль.
Дальше — 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: пока импорт экспорта не написан, помечать что-либо устаревшим не на основании чего.
Отложено
Отсев идентичных тел доставок поВычеркнуто: находка 2 показала, что порядок ключей в JSON нестабилен, поэтому два пакета с одними и теми же данными почти никогда не совпадают побайтно — хеш тела не сработает. Дедупликация возможна только по канонизированному содержимому, а это и делает хеш часового объекта. Отдельная механика не нужна.sha256.Вторая автоматизация без группировки.Сделано на телефоне: метрики здоровья идут в трёх разрезах — несуммированном для несуммируемых метрик, минутном и часовом для всех.Пометка локализованных полей в схемах.Переросло в шаг 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.
- Разворачивание маршрутов тренировок в отдельную таблицу — если появится клиент, которому мало отдачи тренировки одним пакетом.