# 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. Начнём что-то отбрасывать внутри точки — потеряем безвозвратно. - **Хранилище — свёртка по журналу.** Экспорт Apple это снапшот всей истории, доставки HAE после его даты — события поверх. Состояние всегда пересобираемо: `import(экспорт) + replay(доставки по received_at)`. Поэтому сырой архив живёт до следующего проверенного экспорта (~2 ГБ за квартал), а свёртка обязана быть детерминированной. Что не восстанавливается — `stateOfMind` (его в экспорте нет) и верхние слои за периоды с удалёнными доставками; каталог обязан говорить об этом честно, а не досчитывать молча. - **Сохранили — значит приняли.** Код ответа отражает доставку, а не разбор: битый 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 up` / `task restart` / `task down` — сервис в контейнере; **основной способ запуска**, данные в `./data` переживают пересборку - `task logs` / `task ps` — что происходит с сервисом - `task gate` — детерминированный гейт ревью (build/vet/lint/test/race/ покрытие диффа/миграции/образцы конфига/секреты/данные в индексе) - `task review:context` — вход для архитектурного прохода ревью - `task run` — запуск из исходников, без контейнера - `task build` — статический бинарь linux/amd64 - `task test` / `task lint` — тесты и golangci-lint - `task tidy` — `go mod tidy` - `task setup` — установка golangci-lint ## Процесс Задачи — в [docs/backlog](docs/backlog/README.md) (один файл на задачу, индекс производен). Порядок и его обоснование — в [docs/plan.md](docs/plan.md). Работа над задачей идёт скиллом `healthlog-task-pipeline`: беклог → `opsx:explore` → `opsx:propose` → ревью спек (профиль `design`) → `opsx:apply` → ревью кода → `opsx:archive` → чистка беклога → коммит. Ревью — скилл `healthlog-review-pipeline`, проходы — агенты `healthlog-review-*`. **Действуем автономно.** Умолчание — делать, а не спрашивать. Вопрос, который решать не мне, **вынимается блокером** в секцию `блокеры` беклога (что решить, варианты с ценой каждого, что стоит без решения, рекомендация), задача переформулируется на остаток, остаток доводится до коммита. Блокеры разбираются пачками. Спрашиваем только про **необратимое**: деплой, выкладку наружу, удаление или перезапись данных в `./data`. Гейт блокирует: пока `task gate` красный, опиниативные проходы ревью не запускаются. **Поток не останавливается.** Телефон шлёт непрерывно и молча. Сломанный приём, оставленный работать, теряет данные необратимо: доставка, не попавшая в архив, в журнал не попадает вовсе — телефон её не перешлёт. Ничего из `./data` не попадает ни в git, ни в логи выше `DEBUG`, ни в вывод агента. ## Конвенции Механизируемое проверяет `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 нестабилен, поэтому хеш содержимого считается по канонической форме с рекурсивной сортировкой; мелкая группировка даёт интерполяцию, а не измерения). Файл пополняется по мере накопления доставок. ## Язык - Документация, комментарии, сообщения коммитов — **русский**. - Код и идентификаторы — английский.