# 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 нестабилен, поэтому хеш содержимого считается по канонической форме с рекурсивной сортировкой; мелкая группировка даёт интерполяцию, а не измерения). Файл пополняется по мере накопления доставок. ## Язык - Документация, комментарии, сообщения коммитов — **русский**. - Код и идентификаторы — английский.