- беклог и план переехали в docs/tasks (38 задач, 11 целей), слаги переименованы с транслита на английские, 85 ссылок поправлены - conventions.md разобран в docs/conventions/, local-research.md — в docs/research/, review-journal.md — в docs/review.md с разделом настройки конвейера; заведены security.md, adr/ и .pm.json - шаг docs.py check добавлен в task gate; поведение в architecture.md помечено девятью маркерами долга, database.md получил настройки с числовым значением
17 KiB
CLAUDE.md
Памятка для работы над healthlog. Перед задачей прочитай также docs/passport.md (цель, сценарии, референсы), README.md, docs/architecture.md, docs/conventions/README.md, docs/security.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.
Команды
Запуск через 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/amd64task test/task lint— тесты и golangci-linttask verify:archive— сходимость на живом архиве: весь./data/rawчерез разбор, повтор обязан дать то же состояние. В гейт не входит намеренно — минута прогона и данные, которых нет ни на какой другой машинеtask verify:busy— свёртка под удерживаемой блокировкой базы: занятость обязана оставить доставку в очереди. В гейт не входит: 25 секунд на прогонtask tidy—go mod tidytask 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.
Запреты
- Не запускать сервис против
./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пройден целиком и критерии приёмки задачи проверены поимённо.
Процесс
Задачи — в docs/tasks/BACKLOG.md (один файл на запись,
индексы производны), цели — в docs/tasks/PLAN.md. Ведёт их
скилл av-dev-pm:tasks, спринт и ритуал между спринтами — av-dev-pm:session.
Работа над задачей идёт скиллом av-dev-pipeline:task-pipeline: задача →
opsx:explore → opsx:propose → ревью спек (профиль design) → opsx:apply →
ревью кода → opsx:archive → закрытие задачи → коммит. Ревью — скилл
av-dev-pipeline:review-pipeline, проходы — агенты av-dev-pipeline:review-*,
проектная настройка конвейера — docs/review.md.
Действуем автономно. Умолчание — делать, а не спрашивать. Вопрос, который
решать не мне, выносится в раздел «Вопросы» файла задачи и помечается тегом
question; задача с открытым вопросом в спринт не берётся, а сама работа
переформулируется на остаток и доводится до коммита. Спрашиваем немедленно
только про необратимое — список выше.
Развилка или вопрос — сперва prior art. Проект не уникален: прежде чем
проектировать своё, смотрим, как это решено в референсах
паспорта и в интернете. Готовое решение либо берётся, либо
отвергается с названной причиной — и причина идёт в architecture.md.
Поток не останавливается. Телефон шлёт непрерывно и молча. Сломанный приём, оставленный работать, теряет данные необратимо: доставка, не попавшая в архив, в журнал не попадает вовсе — телефон её не перешлёт.
Конвенции
Механизируемое проверяет task lint (.golangci.yml): форма логов
(sloglint), fmt.Print* / os.Getenv / time.Now мимо единых точек
(forbidigo), сравнение ошибок (errorlint), сторонние пакеты ошибок
(depguard). Пересказывать эти правила не нужно — линтер скажет точнее.
Прозой остаётся то, что правилом не выражается:
docs/conventions/README.md — уровень лога по адресату,
единственный логирующий чекпоинт на доменной границе, трансляция ошибки на
внешней границе, самодокументируемый config.example.toml, время в БД в UTC
RFC 3339, ULID через ident.
Отдельно: тесты на разбор формата HAE держим на реальных пакетах в
testdata. Документация формата тонкая и местами расходится с тем, что
приложение реально шлёт, — источником истины служат живые данные.
Что показал реальный поток — docs/research/apple-health.md.
Читать до работы над разбором: там же лежат находки, которых нет в
документации HAE (поле source существует; порядок ключей в JSON нестабилен,
поэтому хеш содержимого считается по канонической форме с рекурсивной
сортировкой; мелкая группировка даёт интерполяцию, а не измерения). Файл
пополняется по мере накопления доставок.
Язык
- Документация, комментарии, сообщения коммитов — русский.
- Код и идентификаторы — английский.