# CLAUDE.md Памятка для работы над healthlog. Перед задачей прочитай также [docs/passport.md](docs/passport.md) (цель, сценарии, референсы), [README.md](README.md), [docs/architecture.md](docs/architecture.md), [docs/conventions/README.md](docs/conventions/README.md), [docs/security.md](docs/security.md) и [docs/tasks/ROADMAP.md](docs/tasks/ROADMAP.md). Документация ведётся по канону `av-dev-pm` (версия в `docs/.pm.json`); раскладку проверяет `av-dev-pm:canon`, содержимое ведёт `av-dev-pm:docs`. ## Что это Коллектор данных 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 (`github.com/oklog/ulid/v2`) через `internal/ident`. Module path — `git.vakhrushev.me/av/healthlog`. ## Инварианты Что нарушать нельзя. `severity` рядом с формулировкой — по ней проходы ревью присваивают вес находке, а не выводят его заново. - **Точки хранятся дословно.** `critical`, необратимо. Часовой объект держит точки ровно в том виде, в каком их прислал HAE. Начнём что-то отбрасывать внутри точки — потеряем безвозвратно. - **Хранилище — свёртка по журналу.** `critical`, необратимо. Экспорт Apple это снапшот всей истории, доставки HAE после его даты — события поверх. Состояние всегда пересобираемо: `import(экспорт) + replay(доставки по received_at)`. Поэтому сырой архив живёт до следующего проверенного экспорта (~2 ГБ за квартал), а свёртка обязана быть детерминированной. Что не восстанавливается — `stateOfMind` (его в экспорте нет) и верхние слои за периоды с удалёнными доставками; каталог обязан говорить об этом честно, а не досчитывать молча. - **Сохранили — значит приняли.** `critical`, необратимо: отказ приёма теряет доставку навсегда. Код ответа отражает доставку, а не разбор: битый JSON — 400, непонятое содержимое — 200. - **Ничего не теряем молча.** `critical`, обратимо пересборкой — но только пока архив жив. Идентичность — координаты (`метрика + слой + начало + конец`), у точки-измерения конец равен началу: под одной меткой лежит до трёх записей сна. Ключ одной формы для всех точек — отдельного класса «эпизодных метрик» нет. `source` в ключ не входит, он нестабилен. Хеш канонизированного содержимого остался детектором изменений. При столкновении выигрывает **более полная** точка, а при равной полноте — **стоящая позже в журнале** (внутри одной доставки — порядок канонических форм). Второе означает, что содержимое витрины есть функция **порядка** свёртки, и порядок этот обязан равняться журнальному. Изменение запечатанного часа — `WARN`, но данные всё равно пишутся. - **Дыры закрываются сами.** `major`, обратимо. Три прохода разной глубины (5 минут / сутки / неделя). Настройки данных у проходов теперь **разные** — намеренно, они наполняют разные слои; это безопасно ровно потому, что слой входит в ключ. - **Форма Apple не транслируется.** `major`, обратимо пересборкой. Значения отдаём как пришли, нормализовано только время (`ts_utc` + офсет исходной зоны). Единственное добавление — стабильный код рядом с переведённой строкой: HAE отдаёт «БДГ» и «Сидячий образ жизни» на языке телефона, а родной экспорт — коды HealthKit, и без словаря эти два источника не сойтись. - **Своей агрегации в хранении нет — есть слои.** `critical`, обратимо пересборкой. Метрика лежит в той подробности, в какой пришла (`sample`/`raw`/`minute`/`hour`/`day`); слой выводится из выравнивания меток, а не из заголовка HAE — тот врёт. Перечень слоёв один и лежит в [docs/database.md](docs/database.md), таблица `bucket`. - **Агрегация в ответе — только измеренная.** `critical`, обратимо: ответ не хранится, но потребитель уже принял по нему решение. Род свёртки выводится сверкой слоёв между собой (часовое = сумма минутных → накопительная, = среднее → мгновенная), а не размечается руками. Род неизвестен — свёртки нет. И никогда не суммируем нижний слой HAE: это интерполяция, а не сэмплы. - **Секреты не в логах.** `critical`, необратимо: утечка не отзывается. Токены приёма и чтения. Данные о здоровье чувствительны: тела запросов только на `DEBUG` и с обрезкой. Периметр и модель угроз — [docs/security.md](docs/security.md). ## Команды Запуск через [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 verify:archive` — сходимость на живом архиве: весь `./data/raw` через разбор, повтор обязан дать то же состояние. В гейт не входит намеренно — минута прогона и данные, которых нет ни на какой другой машине - `task verify:busy` — свёртка под удерживаемой блокировкой базы: занятость обязана оставить доставку в очереди, а отложенная доставка не должна развести живую витрину с пересборкой. В гейт не входит: около 50 секунд на прогон - `task tidy` — `go mod tidy` - `task setup` — установка golangci-lint ## Гейт - **Команда:** `task gate` (`BASE=` — база диффа; без неё берётся `git merge-base HEAD master`). Шаги: сборка, `go vet`, `golangci-lint`, `gofmt`, тесты, флаки, гонки, покрытие изменённых строк, миграции против `docs/database.md`, образцы конфига, секреты и данные о здоровье в индексе, уязвимости, раскладка документов (`docs.py check`). - **Где логи шагов:** `tmp/gate/<шаг>.log` (каталог под `.gitignore`); сводка — в терминале. - **Исходы:** 0 — зелёный; ненулевой — красный, и до его починки опиниативные проходы ревью **не запускаются**. - **Что красит безусловно:** любой файл из `./data` в индексе, любой токен в индексе, миграция без правки `docs/database.md`. Причина одна на все: это ровно те отказы, которые не видны глазами и стоят необратимо. Покрытие изменённых строк задумано тем же классом, но сегодня гейт от него **не краснеет**: `scripts/diff-coverage.py` всегда возвращает `0`, и шаг печатает `OK` при любом покрытии — разбор непокрытых строк остаётся человеку или проходу ревью. Запись 2026-08-04 в [docs/review.md](docs/review.md). - **Чего в гейте намеренно нет и кто обязан это гонять:** `task verify:archive` (минута прогона, данные есть только на этой машине) и `task verify:busy` (около 50 секунд). Гоняет их **человек или оркестратор задачи** перед любым изменением правила разбора, идентичности или слияния — а не «когда вспомнит». Прецедент, когда молчащая краснота прожила две задачи, записан в [docs/review.md](docs/review.md). ## Запреты - **Не запускать сервис против `./data`** мимо `task up` / `task run`: это рабочая база `./data/healthlog.db` и рабочий архив `./data/raw`, других копий нет ни на какой машине. - **Не удалять и не перезаписывать `./data`** — ни файл базы, ни каталог архива, ни отдельные тела. Подмена базы после пересборки — действие человека при остановленном сервисе. - **Ничего из `./data` не попадает** ни в git, ни в логи выше `DEBUG`, ни в вывод агента. - **Не ходить в rivendell** и вообще наружу: деплой и выкладка спрашиваются всегда. - `testdata` — `internal/hae/testdata`: реальные пакеты HAE с вычищенными токенами. Временное — в `./tmp` (под `.gitignore`). ## Работа - **Основная ветка:** `master`. От неё считается база диффа (`git merge-base HEAD master`), в неё вливает батч, от неё ветвятся задачи. - **Необратимое** (спрашивается у человека всегда): деплой, выкладка наружу, удаление или перезапись чего-либо в `./data`, подмена файла базы результатом пересборки. - **Общий станок:** `task verify:archive`. Покраснев, он врывается в замороженный спринт: сходимость журнала — тот инвариант, ради которого существует архив, и жить с красным прогоном нельзя. - **Ориентир по размеру спринта:** 5–8 задач. Ориентир, а не закон. - **Что такое «сделана»:** пайплайн `av-dev-pipeline:task-pipeline` пройден целиком **и** критерии приёмки задачи проверены поимённо. ## Как здесь принято работать Задачи и спринт ведёт `av-dev-pm`, работу над задачей — `av-dev-pipeline`. Порядок шагов, состав проходов ревью и правила ведения задач здесь **не пересказываются**: их дом — сами скиллы, а проектная настройка конвейера — [docs/review.md](docs/review.md). Пересказ разъедется на первой же правке скилла, и разойдётся молча. Проектного здесь три вещи: **Действуем автономно.** Умолчание — делать, а не спрашивать. Немедленно спрашиваем только про **необратимое** — список выше. Остальное, что решать не мне, уходит вопросом в файл задачи, а работа переформулируется на остаток и доводится до коммита. **Развилка или вопрос — сперва prior art.** Проект не уникален; правило и референсы — [docs/passport.md](docs/passport.md), раздел «Мы не делаем уникального». Отвергли готовое решение — причина идёт в `design.md` изменения, а оттуда промоутом в [docs/adr/](docs/adr/README.md). В `architecture.md` обоснования больше не пишем: он переопределён как обзор. **Поток не останавливается.** Телефон шлёт непрерывно и молча. Сломанный приём, оставленный работать, теряет данные необратимо: доставка, не попавшая в архив, в журнал не попадает вовсе — телефон её не перешлёт. ## Конвенции Механизируемое проверяет `task lint` по `.golangci.yml`, прозой остаётся то, что правилом не выражается — [docs/conventions/](docs/conventions/README.md). Перечень правил и перечень записей есть в обоих файлах; здесь они не дублируются. Отдельно, потому что это решает, каким тестам верить: **тесты на разбор формата HAE держим на реальных пакетах** в `testdata`. Документация формата тонкая и местами расходится с тем, что приложение реально шлёт, — источником истины служат живые данные, [docs/research/apple-health.md](docs/research/apple-health.md). Читать **до** работы над разбором: скорее всего вопрос о формате уже закрыт измерением. ## Язык - Документация, комментарии, сообщения коммитов — **русский**. - Код и идентификаторы — английский.