- агрегация появилась в ответе на запрос: род метрики измеряется сверкой слоёв, нижний слой HAE не суммируется никогда - переведённые строки хранятся дословно с приписанным кодом HealthKit; снято правило «настройки данных у всех проходов одинаковы» — слой в ключе - добавлены устаревание нижнего слоя по проверенному экспорту и MCP поверх read API; план вырос до 11 шагов
93 lines
7.1 KiB
Markdown
93 lines
7.1 KiB
Markdown
# CLAUDE.md
|
||
|
||
Памятка для работы над healthlog. Перед задачей прочитай также
|
||
[README.md](README.md), [docs/architecture.md](docs/architecture.md),
|
||
[docs/conventions.md](docs/conventions.md) и [docs/plan.md](docs/plan.md).
|
||
|
||
## Что это
|
||
|
||
Коллектор данных Apple Health. Принимает выгрузки из Health Auto Export и
|
||
родного экспорта Apple, хранит их и отдаёт другим моим проектам — через HTTP
|
||
API и через MCP. Это **хранилище, а не аналитика**: принять, дедуплицировать,
|
||
сохранить, отдать. Не переименовывать поля Apple, не интерпретировать
|
||
значения. Агрегат считается только в ответе на запрос и только там, где род
|
||
метрики измерен.
|
||
|
||
## Стек
|
||
|
||
Go, один статический бинарь (`CGO_ENABLED=0`). SQLite (`modernc.org/sqlite`,
|
||
чистый Go), `chi`, `sqlx`, `goose` (миграции), `pelletier/go-toml/v2`,
|
||
`log/slog`, ULID через `internal/ident`.
|
||
|
||
Module path — `git.vakhrushev.me/av/healthlog`.
|
||
|
||
## Инварианты
|
||
|
||
- **Точки хранятся дословно.** Часовой объект держит точки ровно в том виде,
|
||
в каком их прислал HAE. На этом инварианте держится всё остальное: сырой
|
||
архив живёт лишь 14 дней, дальше истина — сами объекты. Начнём что-то
|
||
отбрасывать внутри точки — срок хранения архива станет сроком жизни данных.
|
||
- **Сохранили — значит приняли.** Код ответа отражает доставку, а не разбор:
|
||
битый JSON — 400, непонятое содержимое — 200.
|
||
- **Ничего не теряем молча.** Идентичность — координаты
|
||
(`метрика + слой + метка`); `source` в ключ не входит, он нестабилен. Хеш
|
||
канонизированного содержимого остался детектором изменений. При
|
||
столкновении выигрывает **более полная** точка, а не последняя. Изменение
|
||
запечатанного часа — `WARN`, но данные всё равно пишутся.
|
||
- **Дыры закрываются сами.** Три прохода разной глубины (5 минут / сутки /
|
||
неделя). Настройки данных у проходов теперь **разные** — намеренно, они
|
||
наполняют разные слои; это безопасно ровно потому, что слой входит в ключ.
|
||
- **Форма Apple не транслируется.** Значения отдаём как пришли, нормализовано
|
||
только время (`ts_utc` + офсет исходной зоны). Единственное добавление —
|
||
стабильный код рядом с переведённой строкой: HAE отдаёт «БДГ» и «Сидячий
|
||
образ жизни» на языке телефона, а родной экспорт — коды HealthKit, и без
|
||
словаря эти два источника не сойтись.
|
||
- **Своей агрегации в хранении нет — есть слои.** Метрика лежит в той
|
||
подробности, в какой пришла (`sample`/`raw`/`minute`/`hour`); слой выводится
|
||
из выравнивания меток, а не из заголовка HAE — тот врёт.
|
||
- **Агрегация в ответе — только измеренная.** Род свёртки выводится сверкой
|
||
слоёв между собой (часовое = сумма минутных → накопительная, = среднее →
|
||
мгновенная), а не размечается руками. Род неизвестен — свёртки нет. И
|
||
никогда не суммируем нижний слой HAE: это интерполяция, а не сэмплы.
|
||
- **Секреты не в логах** — токены приёма и чтения. Данные о здоровье
|
||
чувствительны: тела запросов только на `DEBUG` и с обрезкой.
|
||
|
||
## Команды
|
||
|
||
Запуск через [Task](https://taskfile.dev) (`task --list` — полный список):
|
||
|
||
- `task run` — локальный запуск (`--config ./config.toml`)
|
||
- `task build` — статический бинарь linux/amd64
|
||
- `task test` / `task lint` — тесты и golangci-lint
|
||
- `task tidy` — `go mod tidy`
|
||
- `task setup` — установка golangci-lint
|
||
|
||
## Конвенции
|
||
|
||
Механизируемое проверяет `task lint` (`.golangci.yml`): форма логов
|
||
(`sloglint`), `fmt.Print*` / `os.Getenv` / `time.Now` мимо единых точек
|
||
(`forbidigo`), сравнение ошибок (`errorlint`), сторонние пакеты ошибок
|
||
(`depguard`). Пересказывать эти правила не нужно — линтер скажет точнее.
|
||
|
||
Прозой остаётся то, что правилом не выражается:
|
||
[docs/conventions.md](docs/conventions.md) — уровень лога по адресату,
|
||
единственный логирующий чекпоинт на доменной границе, трансляция ошибки на
|
||
внешней границе, самодокументируемый `config.example.toml`, время в БД в UTC
|
||
RFC 3339, ULID через `ident`.
|
||
|
||
Отдельно: **тесты на разбор формата HAE держим на реальных пакетах** в
|
||
`testdata`. Документация формата тонкая и местами расходится с тем, что
|
||
приложение реально шлёт, — источником истины служат живые данные.
|
||
|
||
Что показал реальный поток — [docs/local-research.md](docs/local-research.md).
|
||
Читать **до** работы над разбором: там же лежат находки, которых нет в
|
||
документации HAE (поле `source` существует; порядок ключей в JSON нестабилен,
|
||
поэтому хеш содержимого считается по канонической форме с рекурсивной
|
||
сортировкой; мелкая группировка даёт интерполяцию, а не измерения). Файл
|
||
пополняется по мере накопления доставок.
|
||
|
||
## Язык
|
||
|
||
- Документация, комментарии, сообщения коммитов — **русский**.
|
||
- Код и идентификаторы — английский.
|