Files
healthlog/CLAUDE.md
T
av 63bffe2865 Приём отвечает 200 до свёртки, свёртку ведёт фоновый воркер
- Очередью служит сама таблица: доставка ждёт свёртки в статусе `pending`,
  канал несёт только бит «есть работа». Переполнять нечего, падение процесса
  очередь не теряет, а подбор `pending` при старте — обычный проход воркера, а
  не отдельный код. Классификация исхода общая с пересборкой журнала.
- Исход разбора начал отражать доставку, а не обстоятельства: отмена и
  занятость базы статус не меняют (иначе конкуренция за базу выводила бы
  доставку из очереди навсегда), паника свёртки больше не валит процесс, а
  учёт доставки идёт через транзакцию с повторами.
- Длинный бюджет ответа выдан маршруту приёма, а не всему серверу:
  `write_timeout` в Go покрывает и чтение тела, и общий подъём снял бы защиту с
  остальных маршрутов.
2026-08-02 11:01:42 +03:00

146 lines
12 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.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 verify:archive` — сходимость на живом архиве: весь `./data/raw` через
разбор, повтор обязан дать то же состояние. В гейт не входит намеренно —
минута прогона и данные, которых нет ни на какой другой машине
- `task verify:busy` — свёртка под удерживаемой блокировкой базы: занятость
обязана оставить доставку в очереди. В гейт не входит: 25 секунд на прогон
- `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-*`.
**Действуем автономно.** Умолчание — делать, а не спрашивать. Вопрос, который
решать не мне, **вынимается блокером** в секцию `блокеры` беклога, задача
переформулируется на остаток, остаток доводится до коммита. Блокеры разбираются
пачками; из чего состоит пункт блокера — в
[индексе беклога](docs/backlog/README.md). Спрашиваем только про
**необратимое**: деплой, выкладку наружу, удаление или перезапись данных в
`./data`.
**Развилка или блокер — сперва prior art.** Проект не уникален: прежде чем
проектировать своё, смотрим, как это решено в референсах
[паспорта](docs/passport.md) и в интернете. Готовое решение либо берётся, либо
отвергается с названной причиной — и причина идёт в `architecture.md`.
Гейт блокирует: пока `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 нестабилен,
поэтому хеш содержимого считается по канонической форме с рекурсивной
сортировкой; мелкая группировка даёт интерполяцию, а не измерения). Файл
пополняется по мере накопления доставок.
## Язык
- Документация, комментарии, сообщения коммитов — **русский**.
- Код и идентификаторы — английский.