Files
healthlog/CLAUDE.md
T
av d33f37249c docs: документация переведена на канон av-dev-pm 3
- роадмап отвечает «что умеет и чего не умеет»: PLAN.md → ROADMAP.md, четыре
  канонические секции, достигнутые звенья строками в «Готово», цели
  переформулированы возможностями приложения
- задачи: род работы и «Затрагивает» набору спринта, 34 заголовка в форму
  действия, «Завершение» целей перечнями со ссылкой из каждой задачи
- вычитка проходами task-form и doc-wording, починены протухшие факты в README,
  паспорте и review.md
2026-08-04 20:48:30 +03:00

16 KiB
Raw Blame History

CLAUDE.md

Памятка для работы над healthlog. Перед задачей прочитай также docs/passport.md (цель, сценарии, референсы), README.md, docs/architecture.md, docs/conventions/README.md, docs/security.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 через 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.

Команды

Запуск через 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 verify:archive — сходимость на живом архиве: весь ./data/raw через разбор, повтор обязан дать то же состояние. В гейт не входит намеренно — минута прогона и данные, которых нет ни на какой другой машине
  • task verify:busy — свёртка под удерживаемой блокировкой базы: занятость обязана оставить доставку в очереди, а отложенная доставка не должна развести живую витрину с пересборкой. В гейт не входит: около 50 секунд на прогон
  • task tidygo 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 (около 50 секунд). Гоняет их человек или оркестратор задачи перед любым изменением правила разбора, идентичности или слияния — а не «когда вспомнит». Прецедент, когда молчащая краснота прожила две задачи, записан в docs/review.md.

Запреты

  • Не запускать сервис против ./data мимо task up / task run: это рабочая база ./data/healthlog.db и рабочий архив ./data/raw, других копий нет ни на какой машине.
  • Не удалять и не перезаписывать ./data — ни файл базы, ни каталог архива, ни отдельные тела. Подмена базы после пересборки — действие человека при остановленном сервисе.
  • Ничего из ./data не попадает ни в git, ни в логи выше DEBUG, ни в вывод агента.
  • Не ходить в rivendell и вообще наружу: деплой и выкладка спрашиваются всегда.
  • testdatainternal/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. Пересказ разъедется на первой же правке скилла, и разойдётся молча.

Проектного здесь три вещи:

Действуем автономно. Умолчание — делать, а не спрашивать. Немедленно спрашиваем только про необратимое — список выше. Остальное, что решать не мне, уходит вопросом в файл задачи, а работа переформулируется на остаток и доводится до коммита.

Развилка или вопрос — сперва prior art. Проект не уникален; правило и референсы — docs/passport.md, раздел «Мы не делаем уникального». Отвергли готовое решение — причина идёт в design.md изменения, а оттуда промоутом в docs/adr/. В architecture.md обоснования больше не пишем: он переопределён как обзор.

Поток не останавливается. Телефон шлёт непрерывно и молча. Сломанный приём, оставленный работать, теряет данные необратимо: доставка, не попавшая в архив, в журнал не попадает вовсе — телефон её не перешлёт.

Конвенции

Механизируемое проверяет task lint по .golangci.yml, прозой остаётся то, что правилом не выражается — docs/conventions/. Перечень правил и перечень записей есть в обоих файлах; здесь они не дублируются.

Отдельно, потому что это решает, каким тестам верить: тесты на разбор формата HAE держим на реальных пакетах в testdata. Документация формата тонкая и местами расходится с тем, что приложение реально шлёт, — источником истины служат живые данные, docs/research/apple-health.md. Читать до работы над разбором: скорее всего вопрос о формате уже закрыт измерением.

Язык

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