# Паспорт проекта Куда мы идём, как выглядит работа проекта в жизни и у кого подсматривать, когда упёрлись. Самый верхний документ: [tasks/ROADMAP.md](tasks/ROADMAP.md) отвечает «что приложение умеет», [architecture.md](architecture.md) — «как устроено», паспорт — **«зачем и для кого»**. ## Цель Все мои данные Apple Health лежат в одном месте, откуда их берёт любой мой проект — так, чтобы ни один из них не писал приём, дедупликацию и хранение заново. **Потребителей три**, и в остальных документах они зовутся так: | Как называем | Что это | Что ему нужно от нас | | --- | --- | --- | | **агент-медик** | анализ здоровья через MCP | актуальная сводка, влезающая в контекст | | **трекер** | разбор тренировок | тренировка целиком, с маршрутом и рядом пульса | | **игра** | мотиватор по активности | шаги и энергия с суточной разбивкой | Список закрытый: он определяет, что считать нужным, а что — интересным. Появится четвёртый — строка добавляется сюда, а не подразумевается. Цель достигнута, когда одновременно верно: - телефон шлёт непрерывно, и поток не требует внимания неделями; - история не начинается с даты запуска сервиса — родной экспорт заливает всё с 2019 года, и дальше она только дополняется; - потребитель получает нужный разрез **одним запросом**, ничего не зная про слои, часовые объекты и особенности HAE; - любое повреждение — включая нашу же ошибку в разборе годовой давности — лечится пересборкой из журнала, а не восстановлением из бекапа; - когда поток встанет, это будет видно, а не обнаружится через месяц по пустому графику. **Мера трезвая:** проект удался, если про него не вспоминают, а данные при этом на месте и полны. ### Что целью не является - **Аналитика.** Ни норм, ни трендов, ни рекомендаций, ни дашборда. Смысл значениям придаёт тот, кто их читает; наше дело — отдать их неискажёнными. - **Полнота покрытия HealthKit ради полноты.** Разбираем то, что реально приходит в поток, и признаёмся в неразобранном, а не гонимся за списком типов из документации Apple. - **Многопользовательность.** Один человек, свои устройства, свой VPS. Никакой модели доступа сложнее двух токенов. - **Реальное время.** Поток пачечный по природе (iOS не пускает к Health при заблокированном телефоне), задержка в минуты — норма, а не дефект. ## Типовые сценарии Ситуации, ради которых всё написано. В скобках — цели [tasks/ROADMAP.md](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/](adr/README.md), > а не теряется. Формулировка «у всех так, а у нас иначе, потому что…» — это готовое обоснование решения. Формулировка «я придумал вот так» — ещё нет. ## Референсы ### Приём данных Health Auto Export | Проект | Что смотреть | Оговорка | | --- | --- | --- | | [Lybron/health-auto-export](https://github.com/Lybron/health-auto-export) | Документация формата от автора приложения — единственная, что есть | Местами расходится с тем, что приложение шлёт: см. [research/apple-health.md](research/apple-health.md) | | [HealthyApps/health-auto-export-server](https://github.com/HealthyApps/health-auto-export-server) | Как приём видят сами авторы HAE: какие поля считают опорными | Их цель — Grafana, то есть аналитика; хранения журнала нет | | [irvinlim/apple-health-ingester](https://github.com/irvinlim/apple-health-ingester) | **Ближайший по стеку**: Go, HTTP-приём HAE, конфиг, токен, разведение бэкендов | Приём синхронный, слияние по метке при записи, сырого журнала нет — ровно тот дизайн, от которого мы ушли осознанно | | [po4yka/apple-health-export-automation-backup](https://github.com/po4yka/apple-health-export-automation-backup) | Заявлены дедупликация, tombstones и dead-letter queue — смотреть, когда встанет вопрос «куда девать неразобранную доставку» | Python/FastAPI + InfluxDB; модель хранения нам не подходит | ### Родной экспорт Apple Health | Проект | Что смотреть | | --- | --- | | [dogsheep/healthkit-to-sqlite](https://github.com/dogsheep/healthkit-to-sqlite) | Прямой прототип `healthlog import`: разбор `export.xml` в SQLite, включая маршруты тренировок из GPX и обход того, что файл не влезает в память | ### Отдача агентам | Проект | Что смотреть | Оговорка | | --- | --- | --- | | [HealthyApps/health-auto-export-mcp-server](https://github.com/HealthyApps/health-auto-export-mcp-server) | Официальный MCP поверх тех же данных: набор инструментов и их именование | Правило размера ответа там **не решено** — в инструкции честно написано «помните про контекст, данные может понадобиться агрегировать». Значит для Read API брать готовое неоткуда | | [meltforce/FreeReps](https://github.com/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-адаптеры перекладывают его на человека; - **измерение рода агрегации из данных** — везде он объявляется руками; - **честность после чистки архива** — ни один из проектов не признаётся клиенту, что за старый период у него стало меньше разрезов.