- internal/fold — свёртка по идентификатору доставки, тело из архива: тот же код, каким пойдёт пересборка витрины - приём зовёт свёртку на context.WithoutCancel с собственным дедлайном; исход разбора на код ответа не влияет - слой доставки хранится в delivery.derived_layer и наследуется от ПРЕДШЕСТВУЮЩЕЙ доставки автоматизации: без границы по времени свёртка переставала быть функцией от префикса журнала (1737 объектов против 1742) - task verify:archive — сходимость на живом архиве, 99 доставок из 99
11 KiB
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. Начнём что-то отбрасывать внутри точки — потеряем безвозвратно.
- Хранилище — свёртка по журналу. Экспорт 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 (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 tidy—go mod tidytask setup— установка golangci-lint
Процесс
Задачи — в docs/backlog (один файл на задачу, индекс производен). Порядок и его обоснование — в docs/plan.md.
Работа над задачей идёт скиллом healthlog-task-pipeline: беклог → opsx:explore →
opsx:propose → ревью спек (профиль design) → opsx:apply → ревью кода →
opsx:archive → чистка беклога → коммит. Ревью — скилл healthlog-review-pipeline,
проходы — агенты healthlog-review-*.
Действуем автономно. Умолчание — делать, а не спрашивать. Вопрос, который
решать не мне, вынимается блокером в секцию блокеры беклога (что решить,
варианты с ценой каждого, что стоит без решения, рекомендация), задача
переформулируется на остаток, остаток доводится до коммита. Блокеры
разбираются пачками. Спрашиваем только про необратимое: деплой, выкладку
наружу, удаление или перезапись данных в ./data.
Гейт блокирует: пока task gate красный, опиниативные проходы ревью не
запускаются.
Поток не останавливается. Телефон шлёт непрерывно и молча. Сломанный приём,
оставленный работать, теряет данные необратимо: доставка, не попавшая в
архив, в журнал не попадает вовсе — телефон её не перешлёт.
Ничего из ./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 нестабилен,
поэтому хеш содержимого считается по канонической форме с рекурсивной
сортировкой; мелкая группировка даёт интерполяцию, а не измерения). Файл
пополняется по мере накопления доставок.
Язык
- Документация, комментарии, сообщения коммитов — русский.
- Код и идентификаторы — английский.