Files
healthlog/docs/passport.md
T
avandClaude Opus 5 893d63d929 канон: «почему» больше не отправляется в architecture.md
Три документа — CLAUDE.md, паспорт и openspec/config.yaml — велели писать
причину отвергнутого решения в architecture.md. По канону дом «почему» это
design.md изменения и промоут в docs/adr/, а architecture.md переезд как раз
опустошает: обоснования шли ровно туда, откуда их вычищают.

Раздел «Процесс» в CLAUDE.md пересказывал шаги пайплайна дословно — тот же
второй дом, что уже вычищен из config.yaml. Осталось три вещи, которые
действительно проектные: автономность, prior art, «поток не останавливается».

config.yaml пересказывал паспорт и инвариант безопасности — стали ссылками.

docs/review.md ссылался на healthlog-review-rubric и healthlog-task-pipeline,
удалённые вместе с проектными копиями. Первое — указание на будущее, поэтому
исправлено на проходы rubric и ops; второе оставлено историей с пометкой.

README.md называл architecture.md домом «принятых решений» и не упоминал
database.md и adr/ вовсе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 17:31:12 +03:00

201 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Паспорт проекта
Куда мы идём, как выглядит работа проекта в жизни и у кого подсматривать,
когда упёрлись. Самый верхний документ: [tasks/PLAN.md](tasks/PLAN.md) отвечает «в каком
порядке», [architecture.md](architecture.md) — «как устроено», паспорт —
**«зачем и для кого»**.
## Цель
Все мои данные Apple Health лежат в одном месте, откуда их берёт любой мой
проект — так, чтобы ни один из них не писал приём, дедупликацию и хранение
заново.
**Потребителей три**, и в остальных документах они зовутся так:
| Как называем | Что это | Что ему нужно от нас |
| --- | --- | --- |
| **агент-медик** | анализ здоровья через MCP | актуальная сводка, влезающая в контекст |
| **трекер** | разбор тренировок | тренировка целиком, с маршрутом и рядом пульса |
| **игра** | мотиватор по активности | шаги и энергия с суточной разбивкой |
Список закрытый: он определяет, что считать нужным, а что — интересным. Появится
четвёртый — строка добавляется сюда, а не подразумевается.
Цель достигнута, когда одновременно верно:
- телефон шлёт непрерывно, и поток не требует внимания неделями;
- история не начинается с даты запуска сервиса — родной экспорт заливает всё
с 2019 года, и дальше она только дополняется;
- потребитель получает нужный разрез **одним запросом**, ничего не зная про
слои, часовые объекты и особенности HAE;
- любое повреждение — включая нашу же ошибку в разборе годовой давности —
лечится пересборкой из журнала, а не восстановлением из бекапа;
- когда поток встанет, это будет видно, а не обнаружится через месяц по
пустому графику.
**Мера трезвая:** проект удался, если про него не вспоминают, а данные при
этом на месте и полны.
### Что целью не является
- **Аналитика.** Ни норм, ни трендов, ни рекомендаций, ни дашборда. Смысл
значениям придаёт тот, кто их читает; наше дело — отдать их неискажёнными.
- **Полнота покрытия HealthKit ради полноты.** Разбираем то, что реально
приходит в поток, и признаёмся в неразобранном, а не гонимся за списком
типов из документации Apple.
- **Многопользовательность.** Один человек, свои устройства, свой VPS.
Никакой модели доступа сложнее двух токенов.
- **Реальное время.** Поток пачечный по природе (iOS не пускает к Health при
заблокированном телефоне), задержка в минуты — норма, а не дефект.
## Типовые сценарии
Ситуации, ради которых всё написано. В скобках — шаги [tasks/PLAN.md](tasks/PLAN.md),
которыми сценарий закрывается; названы, а не пронумерованы, потому что план
живой и нумерация в нём поедет.
**1. Молчаливый приём** (приём, разбор и хранилище). Телефон каждые 5 минут шлёт доставку;
сервис кладёт тело в архив, отвечает `200`, разбирает метрики в часовые
объекты. Никто ничего не спрашивает и не смотрит.
*Успех:* сутки работы не порождают ни одной строки лога уровня `WARN` и ни
одного действия человека.
**2. Дыра закрывается сама** (разбор и хранилище). Телефон был заблокирован ночью,
автоматизация не отработала, часть дня отсутствует. Средний проход (сутки) и
глубокий (неделя) переприсылают окно целиком, точки доезжают.
*Успех:* дыра моложе недели закрывается без вмешательства; никто о ней даже не
узнаёт.
**3. Квартальный экспорт** (`healthlog import`, устаревание нижнего слоя).
Изредка владелец выгружает
родной экспорт Apple Health и скармливает его `healthlog import`. Нижний слой
за прошлое становится честным (настоящие сэмплы вместо посекундной развёртки
HAE), а сырой архив получает право быть подчищенным до даты снапшота.
*Успех:* экспорт разобран, покрытие периода проверено, объём архива вернулся к
норме, ничего не потеряно.
**4. Агент спрашивает про здоровье** (каталог и род агрегации, Read API, MCP). Агент-медик по MCP
спрашивает каталог («что у тебя вообще есть»), затем «шаги по дням за месяц»
или «пульс за вчера». Получает свёрнутый ряд с честным указанием слоя, сетки и
рода агрегации.
*Успех:* ответ влезает в контекст агента, число не завышено вдвое, и агенту не
пришлось знать про слои, чтобы спросить правильно.
**5. Приложение берёт тренировки** (Read API). Разборщик тренировок запрашивает
заголовки за период, потом одну тренировку целиком — с маршрутом и рядом
пульса.
*Успех:* тренировка отдана одним пакетом в том виде, в каком её прислал Apple,
без нашей интерпретации того, что в ней главное.
**6. Разбор поменялся** (`healthlog reindex`). Мы начали разбирать секцию, которую
раньше пропускали, или нашли ошибку в старом разборе. Запускается пересборка
по сырому архиву: `import(экспорт) + replay(доставки по received_at)`.
*Успех:* состояние пересобрано детерминированно, повтор даёт то же самое,
доставки со снятым статусом `partial` подобраны.
**7. Владелец проверяет, жив ли поток** (наблюдаемость). Раз в сколько-то дней —
взгляд в `/stats`: когда была последняя доставка, сколько точек, есть ли
тишина, какие строки не легли в словарь кодов.
*Успех:* один экран отвечает «всё идёт» или «встало тогда-то», без залезания
в SQLite.
**8. Приехало незнакомое** (разбор и хранилище). 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-адаптеры перекладывают
его на человека;
- **измерение рода агрегации из данных** — везде он объявляется руками;
- **честность после чистки архива** — ни один из проектов не признаётся
клиенту, что за старый период у него стало меньше разрезов.