Files
av 3d24248075 docs: документация приведена к канону av-dev-pm 4
- каждая запись каталога задач получила тип вместо тега kind: и префикса
  заголовка; секция роадмапа «Разработка» стала «Сопровождением», порядок
  секций канонический
- поправлены протухшие факты: нереализованные маршруты Read API, MCP и
  `healthlog import`, словарь слоёв в инварианте, семантика гейта по покрытию
  диффа, периметр перестал дублировать security.md
- замер слияния переведён с находки 49 на находку 54, заполнены Purpose спек
  storage и parsing
2026-08-05 19:09:35 +03:00

200 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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=<rev>` — база диффа; без неё берётся
`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).
Читать **до** работы над разбором: скорее всего вопрос о формате уже закрыт
измерением.
## Язык
- Документация, комментарии, сообщения коммитов — **русский**.
- Код и идентификаторы — английский.