Files
healthlog/CLAUDE.md
T
avandClaude Opus 5 893d63d929 канон: «почему» больше не отправляется в architecture.md
Три документа — CLAUDE.md, паспорт и openspec/config.yaml — велели писать
причину отвергнутого решения в architecture.md. По канону дом «почему» это
design.md изменения и промоут в docs/adr/, а architecture.md переезд как раз
опустошает: обоснования шли ровно туда, откуда их вычищают.

Раздел «Процесс» в CLAUDE.md пересказывал шаги пайплайна дословно — тот же
второй дом, что уже вычищен из config.yaml. Осталось три вещи, которые
действительно проектные: автономность, prior art, «поток не останавливается».

config.yaml пересказывал паспорт и инвариант безопасности — стали ссылками.

docs/review.md ссылался на healthlog-review-rubric и healthlog-task-pipeline,
удалённые вместе с проектными копиями. Первое — указание на будущее, поэтому
исправлено на проходы rubric и ops; второе оставлено историей с пометкой.

README.md называл architecture.md домом «принятых решений» и не упоминал
database.md и adr/ вовсе.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 17:31:12 +03:00

191 lines
16 KiB
Markdown
Raw 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/PLAN.md](docs/tasks/PLAN.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 через `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`); слой выводится
из выравнивания меток, а не из заголовка HAE — тот врёт.
- **Агрегация в ответе — только измеренная.** `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` — свёртка под удерживаемой блокировкой базы: занятость
обязана оставить доставку в очереди. В гейт не входит: 25 секунд на прогон
- `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`.
Причина одна на все: это ровно те отказы, которые не видны глазами и стоят
необратимо.
- **Чего в гейте намеренно нет и кто обязан это гонять:**
`task verify:archive` (минута прогона, данные есть только на этой машине) и
`task verify:busy` (25 секунд). Гоняет их **человек или оркестратор задачи**
перед любым изменением правила разбора, идентичности или слияния — а не «когда
вспомнит». Прецедент, когда молчащая краснота прожила две задачи, записан в
[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).
Читать **до** работы над разбором: скорее всего вопрос о формате уже закрыт
измерением.
## Язык
- Документация, комментарии, сообщения коммитов — **русский**.
- Код и идентификаторы — английский.