docs: добавлен паспорт проекта — цель, сценарии, референсы
- восемь типовых сценариев работы, у каждого проверяемое «успех» и ссылка на шаг плана - референсы по задачам проекта, с оговорками где чужое решение не годится, и три вопроса без готового ответа - правило «развилка или блокер — сперва prior art» продублировано в CLAUDE.md
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# CLAUDE.md
|
||||
|
||||
Памятка для работы над healthlog. Перед задачей прочитай также
|
||||
[docs/passport.md](docs/passport.md) (цель, сценарии, референсы),
|
||||
[README.md](README.md), [docs/architecture.md](docs/architecture.md),
|
||||
[docs/conventions.md](docs/conventions.md) и [docs/plan.md](docs/plan.md).
|
||||
|
||||
@@ -94,7 +95,12 @@ Module path — `git.vakhrushev.me/av/healthlog`.
|
||||
решать не мне, **вынимается блокером** в секцию `блокеры` беклога (что решить,
|
||||
варианты с ценой каждого, что стоит без решения, рекомендация), задача
|
||||
переформулируется на остаток, остаток доводится до коммита. Блокеры
|
||||
разбираются пачками. Спрашиваем только про **необратимое**: деплой, выкладку
|
||||
разбираются пачками.
|
||||
|
||||
**Развилка или блокер — сперва prior art.** Проект не уникален: прежде чем
|
||||
проектировать своё, смотрим, как это решено в референсах
|
||||
[паспорта](docs/passport.md) и в интернете. Готовое решение либо берётся, либо
|
||||
отвергается с названной причиной — и причина идёт в `architecture.md`. Спрашиваем только про **необратимое**: деплой, выкладку
|
||||
наружу, удаление или перезапись данных в `./data`.
|
||||
|
||||
Гейт блокирует: пока `task gate` красный, опиниативные проходы ревью не
|
||||
|
||||
@@ -107,6 +107,9 @@ curl -X POST localhost:8080/api/v1/ingest -d '{"data":{"metrics":[]}}'
|
||||
|
||||
## Документация
|
||||
|
||||
- [docs/passport.md](docs/passport.md) — цель проекта, типовые сценарии
|
||||
работы, референсы: чужие проекты, у которых смотрим решения, прежде чем
|
||||
придумывать своё
|
||||
- [docs/architecture.md](docs/architecture.md) — устройство, схема данных, API, принятые решения
|
||||
- [docs/conventions.md](docs/conventions.md) — как пишем код
|
||||
- [docs/plan.md](docs/plan.md) — шаги и отложенное
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# Паспорт проекта
|
||||
|
||||
Куда мы идём, как выглядит работа проекта в жизни и у кого подсматривать,
|
||||
когда упёрлись. Самый верхний документ: [plan.md](plan.md) отвечает «в каком
|
||||
порядке», [architecture.md](architecture.md) — «как устроено», паспорт —
|
||||
**«зачем и для кого»**.
|
||||
|
||||
## Цель
|
||||
|
||||
Все мои данные Apple Health лежат в одном месте, откуда их берёт любой мой
|
||||
проект — так, чтобы ни один из них не писал приём, дедупликацию и хранение
|
||||
заново.
|
||||
|
||||
Цель достигнута, когда одновременно верно:
|
||||
|
||||
- телефон шлёт непрерывно, и поток не требует внимания неделями;
|
||||
- история не начинается с даты запуска сервиса — родной экспорт заливает всё
|
||||
с 2019 года, и дальше она только дополняется;
|
||||
- потребитель получает нужный разрез **одним запросом**, ничего не зная про
|
||||
слои, часовые объекты и особенности HAE;
|
||||
- любое повреждение — включая нашу же ошибку в разборе годовой давности —
|
||||
лечится пересборкой из журнала, а не восстановлением из бекапа;
|
||||
- когда поток встанет, это будет видно, а не обнаружится через месяц по
|
||||
пустому графику.
|
||||
|
||||
**Мера трезвая:** проект удался, если про него не вспоминают, а данные при
|
||||
этом на месте и полны.
|
||||
|
||||
### Что целью не является
|
||||
|
||||
- **Аналитика.** Ни норм, ни трендов, ни рекомендаций, ни дашборда. Смысл
|
||||
значениям придаёт тот, кто их читает; наше дело — отдать их неискажёнными.
|
||||
- **Полнота покрытия HealthKit ради полноты.** Разбираем то, что реально
|
||||
приходит в поток, и признаёмся в неразобранном, а не гонимся за списком
|
||||
типов из документации Apple.
|
||||
- **Многопользовательность.** Один человек, свои устройства, свой VPS.
|
||||
Никакой модели доступа сложнее двух токенов.
|
||||
- **Реальное время.** Поток пачечный по природе (iOS не пускает к Health при
|
||||
заблокированном телефоне), задержка в минуты — норма, а не дефект.
|
||||
|
||||
## Типовые сценарии
|
||||
|
||||
Восемь ситуаций, ради которых всё написано. Номера шагов — по
|
||||
[plan.md](plan.md).
|
||||
|
||||
**1. Молчаливый приём** (шаги 2–3). Телефон каждые 5 минут шлёт доставку;
|
||||
сервис кладёт тело в архив, отвечает `200`, разбирает метрики в часовые
|
||||
объекты. Никто ничего не спрашивает и не смотрит.
|
||||
*Успех:* сутки работы не порождают ни одной строки лога уровня `WARN` и ни
|
||||
одного действия человека.
|
||||
|
||||
**2. Дыра закрывается сама** (шаг 3). Телефон был заблокирован ночью,
|
||||
автоматизация не отработала, часть дня отсутствует. Средний проход (сутки) и
|
||||
глубокий (неделя) переприсылают окно целиком, точки доезжают.
|
||||
*Успех:* дыра моложе недели закрывается без вмешательства; никто о ней даже не
|
||||
узнаёт.
|
||||
|
||||
**3. Квартальный экспорт** (шаги 8–9). Раз в 2–3 месяца владелец выгружает
|
||||
родной экспорт Apple Health и скармливает его `healthlog import`. Нижний слой
|
||||
за прошлое становится честным (настоящие сэмплы вместо посекундной развёртки
|
||||
HAE), а сырой архив получает право быть подчищенным до даты снапшота.
|
||||
*Успех:* экспорт разобран, покрытие периода проверено, объём архива вернулся к
|
||||
норме, ничего не потеряно.
|
||||
|
||||
**4. Агент спрашивает про здоровье** (шаги 4, 5, 7). Агент-медик по MCP
|
||||
спрашивает каталог («что у тебя вообще есть»), затем «шаги по дням за месяц»
|
||||
или «пульс за вчера». Получает свёрнутый ряд с честным указанием слоя, сетки и
|
||||
рода агрегации.
|
||||
*Успех:* ответ влезает в контекст агента, число не завышено вдвое, и агенту не
|
||||
пришлось знать про слои, чтобы спросить правильно.
|
||||
|
||||
**5. Приложение берёт тренировки** (шаг 5). Разборщик тренировок запрашивает
|
||||
заголовки за период, потом одну тренировку целиком — с маршрутом и рядом
|
||||
пульса.
|
||||
*Успех:* тренировка отдана одним пакетом в том виде, в каком её прислал Apple,
|
||||
без нашей интерпретации того, что в ней главное.
|
||||
|
||||
**6. Разбор поменялся** (шаг 3, `reindex`). Мы начали разбирать секцию, которую
|
||||
раньше пропускали, или нашли ошибку в старом разборе. Запускается пересборка
|
||||
по сырому архиву: `import(экспорт) + replay(доставки по received_at)`.
|
||||
*Успех:* состояние пересобрано детерминированно, повтор даёт то же самое,
|
||||
доставки со снятым статусом `partial` подобраны.
|
||||
|
||||
**7. Владелец проверяет, жив ли поток** (шаг 10). Раз в сколько-то дней —
|
||||
взгляд в `/stats`: когда была последняя доставка, сколько точек, есть ли
|
||||
тишина, какие строки не легли в словарь кодов.
|
||||
*Успех:* один экран отвечает «всё идёт» или «встало тогда-то», без залезания
|
||||
в SQLite.
|
||||
|
||||
**8. Приехало незнакомое** (шаг 3). HAE обновился и прислал новую метрику,
|
||||
новую форму точки или новую секцию. Тело сохраняется, ответ — `200`, разбор
|
||||
честно помечает доставку `partial` и перечисляет непокрытое.
|
||||
*Успех:* данные в архиве и восстановимы, факт виден в логе и `/stats`, а
|
||||
доразобрать их можно потом — задним числом и без потерь.
|
||||
|
||||
## Мы не делаем уникального
|
||||
|
||||
Ни одна задача этого проекта не нова. Приём данных с телефона, дедупликация по
|
||||
координатам, несколько разрешений одного ряда, накопительное против
|
||||
мгновенного, свёртка состояния по журналу событий, ретеншен сырых тел — всё
|
||||
это решалось десятки раз, и чаще всего лучше, чем выйдет с первого раза у нас.
|
||||
Своё здесь ровно одно: **удобство под себя** — свои источники, свои
|
||||
потребители, свой объём.
|
||||
|
||||
Отсюда правило работы:
|
||||
|
||||
> **Развилка или блокер — сперва prior art.** Прежде чем проектировать своё,
|
||||
> посмотреть, как это сделано в проектах ниже и в интернете. Готовое решение
|
||||
> либо берётся, либо отвергается **с названной причиной** — и тогда причина
|
||||
> идёт в [architecture.md](architecture.md), а не теряется.
|
||||
|
||||
Формулировка «у всех так, а у нас иначе, потому что…» — это готовое
|
||||
обоснование решения. Формулировка «я придумал вот так» — ещё нет.
|
||||
|
||||
## Референсы
|
||||
|
||||
### Приём данных Health Auto Export
|
||||
|
||||
| Проект | Что смотреть | Оговорка |
|
||||
| --- | --- | --- |
|
||||
| [Lybron/health-auto-export](https://github.com/Lybron/health-auto-export) | Документация формата от автора приложения — единственная, что есть | Местами расходится с тем, что приложение шлёт: см. [local-research.md](local-research.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` (шаг 8): разбор `export.xml` в SQLite, включая маршруты тренировок из GPX и обход того, что файл не влезает в память |
|
||||
|
||||
### Отдача агентам
|
||||
|
||||
| Проект | Что смотреть | Оговорка |
|
||||
| --- | --- | --- |
|
||||
| [HealthyApps/health-auto-export-mcp-server](https://github.com/HealthyApps/health-auto-export-mcp-server) | Официальный MCP поверх тех же данных: набор инструментов и их именование (шаг 7) | Правило размера ответа там **не решено** — в инструкции честно написано «помните про контекст, данные может понадобиться агрегировать». Значит на шаге 5 брать готовое неоткуда |
|
||||
| [meltforce/FreeReps](https://github.com/meltforce/FreeReps) | Self-hosted сервер здоровья с MCP-интерфейсом — второй взгляд на тот же контракт | Тянет за собой дашборд и визуализацию, то есть нашу границу «хранилище, а не аналитика» не держит |
|
||||
|
||||
### Слои, свёртка и род агрегации (шаги 4–5)
|
||||
|
||||
Здесь чужого опыта больше всего, и он старше нашей задачи на двадцать лет.
|
||||
|
||||
- **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-адаптеры перекладывают
|
||||
его на человека;
|
||||
- **измерение рода агрегации из данных** — везде он объявляется руками;
|
||||
- **честность после чистки архива** — ни один из проектов не признаётся
|
||||
клиенту, что за старый период у него стало меньше разрезов.
|
||||
Reference in New Issue
Block a user