- роадмап отвечает «что умеет и чего не умеет»: PLAN.md → ROADMAP.md, четыре канонические секции, достигнутые звенья строками в «Готово», цели переформулированы возможностями приложения - задачи: род работы и «Затрагивает» набору спринта, 34 заголовка в форму действия, «Завершение» целей перечнями со ссылкой из каждой задачи - вычитка проходами task-form и doc-wording, починены протухшие факты в README, паспорте и review.md
18 KiB
Паспорт проекта
Куда мы идём, как выглядит работа проекта в жизни и у кого подсматривать, когда упёрлись. Самый верхний документ: tasks/ROADMAP.md отвечает «что приложение умеет», architecture.md — «как устроено», паспорт — «зачем и для кого».
Цель
Все мои данные Apple Health лежат в одном месте, откуда их берёт любой мой проект — так, чтобы ни один из них не писал приём, дедупликацию и хранение заново.
Потребителей три, и в остальных документах они зовутся так:
| Как называем | Что это | Что ему нужно от нас |
|---|---|---|
| агент-медик | анализ здоровья через MCP | актуальная сводка, влезающая в контекст |
| трекер | разбор тренировок | тренировка целиком, с маршрутом и рядом пульса |
| игра | мотиватор по активности | шаги и энергия с суточной разбивкой |
Список закрытый: он определяет, что считать нужным, а что — интересным. Появится четвёртый — строка добавляется сюда, а не подразумевается.
Цель достигнута, когда одновременно верно:
- телефон шлёт непрерывно, и поток не требует внимания неделями;
- история не начинается с даты запуска сервиса — родной экспорт заливает всё с 2019 года, и дальше она только дополняется;
- потребитель получает нужный разрез одним запросом, ничего не зная про слои, часовые объекты и особенности HAE;
- любое повреждение — включая нашу же ошибку в разборе годовой давности — лечится пересборкой из журнала, а не восстановлением из бекапа;
- когда поток встанет, это будет видно, а не обнаружится через месяц по пустому графику.
Мера трезвая: проект удался, если про него не вспоминают, а данные при этом на месте и полны.
Что целью не является
- Аналитика. Ни норм, ни трендов, ни рекомендаций, ни дашборда. Смысл значениям придаёт тот, кто их читает; наше дело — отдать их неискажёнными.
- Полнота покрытия HealthKit ради полноты. Разбираем то, что реально приходит в поток, и признаёмся в неразобранном, а не гонимся за списком типов из документации Apple.
- Многопользовательность. Один человек, свои устройства, свой VPS. Никакой модели доступа сложнее двух токенов.
- Реальное время. Поток пачечный по природе (iOS не пускает к Health при заблокированном телефоне), задержка в минуты — норма, а не дефект.
Типовые сценарии
Ситуации, ради которых всё написано. В скобках — цели tasks/ROADMAP.md, которыми сценарий закрывается: достигнутые названы слагом из «Готово», открытые — заголовком цели. Названы, а не пронумерованы, потому что роадмап живой и нумерация в нём сдвинется на первой же вставке.
1. Молчаливый приём (ingest, parsing-and-storage — сделаны). Телефон каждые 5 минут шлёт доставку;
сервис кладёт тело в архив, отвечает 200, разбирает метрики в часовые
объекты. Никто ничего не спрашивает и не смотрит.
Успех: сутки работы не порождают ни одной строки лога уровня WARN и ни
одного действия человека.
2. Дыра закрывается сама (parsing-and-storage — сделано). Телефон был заблокирован ночью,
автоматизация не отработала, часть дня отсутствует. Средний проход (сутки) и
глубокий (неделя) переприсылают окно целиком, точки доезжают.
Успех: дыра моложе недели закрывается без вмешательства; никто о ней даже не
узнаёт.
3. Квартальный экспорт (История из родного экспорта Apple лежит в
хранилище; Нижний слой чистится после проверенного экспорта).
Изредка владелец выгружает
родной экспорт Apple Health и скармливает его healthlog import. Нижний слой
за прошлое становится честным (настоящие сэмплы вместо посекундной развёртки
HAE), а сырой архив получает право быть подчищенным до даты снапшота.
Успех: экспорт разобран, покрытие периода проверено, объём архива вернулся к
норме, ничего не потеряно.
4. Агент спрашивает про здоровье (catalog — сделан; Клиенты читают данные
через HTTP и MCP). Агент-медик по MCP
спрашивает каталог («что у тебя вообще есть»), затем «шаги по дням за месяц»
или «пульс за вчера». Получает свёрнутый ряд с честным указанием слоя, сетки и
рода агрегации.
Успех: ответ влезает в контекст агента, число не завышено вдвое, и агенту не
пришлось знать про слои, чтобы спросить правильно.
5. Приложение берёт тренировки (Клиенты читают данные через HTTP и MCP). Разборщик тренировок запрашивает заголовки за период, потом одну тренировку целиком — с маршрутом и рядом пульса. Успех: тренировка отдана одним пакетом в том виде, в каком её прислал Apple, без нашей интерпретации того, что в ней главное.
6. Разбор поменялся (reindex — сделан). Мы начали разбирать секцию, которую
раньше пропускали, или нашли ошибку в старом разборе. Запускается пересборка
по сырому архиву: import(экспорт) + replay(доставки по received_at).
Успех: состояние пересобрано детерминированно, повтор даёт то же самое,
доставки со снятым статусом partial подобраны.
7. Владелец проверяет, жив ли поток (Приложение сообщает о своём состоянии). Раз в сколько-то дней —
взгляд в /stats: когда была последняя доставка, сколько точек, есть ли
тишина, какие строки не легли в словарь кодов.
Успех: один экран отвечает «всё идёт» или «встало тогда-то», без залезания
в SQLite.
8. Приехало незнакомое (parsing-and-storage — сделано; Новая форма от
источника не теряется молча). HAE обновился и прислал новую метрику,
новую форму точки или новую секцию. Тело сохраняется, ответ — 200, разбор
честно помечает доставку partial и перечисляет непокрытое.
Успех: данные в архиве и восстановимы, факт виден в логе и /stats, а
доразобрать их можно потом — задним числом и без потерь.
Мы не делаем уникального
Ни одна задача этого проекта не нова. Приём данных с телефона, дедупликация по координатам, несколько разрешений одного ряда, накопительное против мгновенного, свёртка состояния по журналу событий, ретеншен сырых тел — всё это решалось десятки раз, и чаще всего лучше, чем выйдет с первого раза у нас. Своё здесь ровно одно: удобство под себя — свои источники, свои потребители, свой объём.
Отсюда правило работы:
Развилка или вопрос — сперва prior art. Прежде чем проектировать своё, посмотреть, как это сделано в проектах ниже и в интернете. Готовое решение либо берётся, либо отвергается с названной причиной — и тогда причина идёт в
design.mdизменения, а оттуда промоутом в adr/, а не теряется.
Формулировка «у всех так, а у нас иначе, потому что…» — это готовое обоснование решения. Формулировка «я придумал вот так» — ещё нет.
Референсы
Приём данных Health Auto Export
| Проект | Что смотреть | Оговорка |
|---|---|---|
| Lybron/health-auto-export | Документация формата от автора приложения — единственная, что есть | Местами расходится с тем, что приложение шлёт: см. research/apple-health.md |
| HealthyApps/health-auto-export-server | Как приём видят сами авторы HAE: какие поля считают опорными | Их цель — Grafana, то есть аналитика; хранения журнала нет |
| irvinlim/apple-health-ingester | Ближайший по стеку: Go, HTTP-приём HAE, конфиг, токен, разведение бэкендов | Приём синхронный, слияние по метке при записи, сырого журнала нет — ровно тот дизайн, от которого мы ушли осознанно |
| po4yka/apple-health-export-automation-backup | Заявлены дедупликация, tombstones и dead-letter queue — смотреть, когда встанет вопрос «куда девать неразобранную доставку» | Python/FastAPI + InfluxDB; модель хранения нам не подходит |
Родной экспорт Apple Health
| Проект | Что смотреть |
|---|---|
| dogsheep/healthkit-to-sqlite | Прямой прототип healthlog import: разбор export.xml в SQLite, включая маршруты тренировок из GPX и обход того, что файл не влезает в память |
Отдача агентам
| Проект | Что смотреть | Оговорка |
|---|---|---|
| HealthyApps/health-auto-export-mcp-server | Официальный MCP поверх тех же данных: набор инструментов и их именование | Правило размера ответа там не решено — в инструкции честно написано «помните про контекст, данные может понадобиться агрегировать». Значит для Read API брать готовое неоткуда |
| meltforce/FreeReps | Self-hosted сервер здоровья с MCP-интерфейсом — второй взгляд на тот же контракт | Тянет за собой дашборд и визуализацию, то есть нашу границу «хранилище, а не аналитика» не держит |
Слои, свёртка и род агрегации
Здесь чужого опыта больше всего, и он старше нашей задачи на двадцать лет.
- HealthKit сам — деление на
HKQuantityTypeCumulativeиHKQuantityTypeDiscrete, а вHKStatisticsQuery—.cumulativeSumпротив.discreteAverage. Первоисточник того самого рода, который мы измеряем. У Apple он объявлен типом; у нас типа нет, потому что HAE его не шлёт. - Home Assistant, recorder и long-term statistics —
state_class(measurement/total/total_increasing) и то, что для одних метрик хранитсяmean/min/max, а для другихsum. Плюс их правило пересчёта при смене класса. Разница с нами: там род объявляет интеграция, у нас его некому объявить, поэтому измеряем сверкой слоёв. - Graphite —
storage-schemas.conf(несколько разрешений одного ряда в одном файле) иstorage-aggregation.conf(aggregationMethod: average / sum / last / max, плюсxFilesFactor— доля точек, ниже которой свёртка не делается). Это буквально «слои + род», только заданы конфигом.xFilesFactor— готовый ответ на вопрос, который у нас ещё не задан: что делать со свёрткой неполного ведра. - Prometheus — counter против gauge и правило «счётчик не суммируют, а
берут
rate». Полезен как формулировка ошибки, которую мы боимся: сложить не тот разрез и получить завышение.
Идентичность, слияние, журнал
- Литература по CRDT (state-based merge, LWW-Register, semilattice) — наше «выигрывает более полная точка» это merge в полурешётке, а отвергнутое «выигрывает последняя» — LWW. Оттуда же требование коммутативности и идемпотентности, и оттуда же понятно, почему нетранзитивное отношение победы ломает воспроизводимость.
- Event sourcing и log compaction (Kafka) — «состояние = свёртка журнала»,
снапшот плюс хвост событий, требование детерминированности реплея. Наша
формула
import(экспорт) + replay(доставки)— ровно этот шаблон. - FHIR Observation —
componentдля составных измерений (давление),effectiveDateTimeпротивeffectivePeriod. Прямой аналог решения «конец равен началу у точки-измерения». Семантику FHIR при этом не берём: мы храним дословно, а не переводим Apple в чужую онтологию.
Чего в референсах нет
Стоит знать заранее, что по трём вопросам готового ответа не нашлось и проектировать придётся самим:
- предел размера ответа агенту — все известные MCP-адаптеры перекладывают его на человека;
- измерение рода агрегации из данных — везде он объявляется руками;
- честность после чистки архива — ни один из проектов не признаётся клиенту, что за старый период у него стало меньше разрезов.