Files
healthlog/CLAUDE.md
T
av 59ec613184 заведён беклог проекта
- 20 задач: 8 высоких, 9 средних, 3 низких; две заведены идеями — сутки при
  смене часового пояса и пересекающиеся источники одной метрики
- план сведён к порядку и его обоснованию, единицы работы переехали в беклог
2026-08-01 14:11:42 +03:00

8.8 KiB
Raw Blame History

CLAUDE.md

Памятка для работы над healthlog. Перед задачей прочитай также README.md, docs/architecture.md, docs/conventions.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. На этом инварианте держится всё остальное: сырой архив живёт лишь 14 дней, дальше истина — сами объекты. Начнём что-то отбрасывать внутри точки — срок хранения архива станет сроком жизни данных.
  • Сохранили — значит приняли. Код ответа отражает доставку, а не разбор: битый JSON — 400, непонятое содержимое — 200.
  • Ничего не теряем молча. Идентичность — координаты (метрика + слой + метка); source в ключ не входит, он нестабилен. Хеш канонизированного содержимого остался детектором изменений. При столкновении выигрывает более полная точка, а не последняя. Изменение запечатанного часа — WARN, но данные всё равно пишутся.
  • Дыры закрываются сами. Три прохода разной глубины (5 минут / сутки / неделя). Настройки данных у проходов теперь разные — намеренно, они наполняют разные слои; это безопасно ровно потому, что слой входит в ключ.
  • Форма Apple не транслируется. Значения отдаём как пришли, нормализовано только время (ts_utc + офсет исходной зоны). Единственное добавление — стабильный код рядом с переведённой строкой: HAE отдаёт «БДГ» и «Сидячий образ жизни» на языке телефона, а родной экспорт — коды HealthKit, и без словаря эти два источника не сойтись.
  • Своей агрегации в хранении нет — есть слои. Метрика лежит в той подробности, в какой пришла (sample/raw/minute/hour); слой выводится из выравнивания меток, а не из заголовка HAE — тот врёт.
  • Агрегация в ответе — только измеренная. Род свёртки выводится сверкой слоёв между собой (часовое = сумма минутных → накопительная, = среднее → мгновенная), а не размечается руками. Род неизвестен — свёртки нет. И никогда не суммируем нижний слой HAE: это интерполяция, а не сэмплы.
  • Секреты не в логах — токены приёма и чтения. Данные о здоровье чувствительны: тела запросов только на DEBUG и с обрезкой.

Команды

Запуск через Task (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 tidygo mod tidy
  • task setup — установка golangci-lint

Процесс

Задачи — в docs/backlog (один файл на задачу, индекс производен). Порядок и его обоснование — в docs/plan.md.

Работа над задачей идёт скиллом task-pipeline: беклог → opsx:exploreopsx:propose → ревью спек (профиль design) → opsx:apply → ревью кода → opsx:archive → чистка беклога → коммит. Ревью — скилл review-pipeline, проходы — агенты healthlog-review-*.

Гейт блокирует: пока task gate красный, опиниативные проходы ревью не запускаются.

Поток не останавливается. Телефон шлёт непрерывно и молча. Сломанный приём, оставленный работать, теряет данные необратимо — сырой архив живёт 14 дней. Ничего из ./data не попадает ни в git, ни в логи выше DEBUG, ни в вывод агента.

Конвенции

Механизируемое проверяет task lint (.golangci.yml): форма логов (sloglint), fmt.Print* / os.Getenv / time.Now мимо единых точек (forbidigo), сравнение ошибок (errorlint), сторонние пакеты ошибок (depguard). Пересказывать эти правила не нужно — линтер скажет точнее.

Прозой остаётся то, что правилом не выражается: docs/conventions.md — уровень лога по адресату, единственный логирующий чекпоинт на доменной границе, трансляция ошибки на внешней границе, самодокументируемый config.example.toml, время в БД в UTC RFC 3339, ULID через ident.

Отдельно: тесты на разбор формата HAE держим на реальных пакетах в testdata. Документация формата тонкая и местами расходится с тем, что приложение реально шлёт, — источником истины служат живые данные.

Что показал реальный поток — docs/local-research.md. Читать до работы над разбором: там же лежат находки, которых нет в документации HAE (поле source существует; порядок ключей в JSON нестабилен, поэтому хеш содержимого считается по канонической форме с рекурсивной сортировкой; мелкая группировка даёт интерполяцию, а не измерения). Файл пополняется по мере накопления доставок.

Язык

  • Документация, комментарии, сообщения коммитов — русский.
  • Код и идентификаторы — английский.