docs: документация переведена на канон av-dev-pm

- беклог и план переехали в 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 получил настройки с числовым значением
This commit is contained in:
av
2026-08-03 17:14:53 +03:00
parent de7b15d48c
commit d79189be18
94 changed files with 1234 additions and 566 deletions
+45 -18
View File
@@ -1,5 +1,11 @@
# Архитектура
Обзор: как сложено и где что работает. **Поведение системы здесь не
описывается** — его нормативный дом [`openspec/specs/`](../openspec/specs).
Разделы, помеченные `<!-- канон: поведение → … -->`, ещё не разнесены:
это долг переезда на канон 2026-08-03, он закрывается порциями по ходу
задач и гейт от него не краснеет.
## Назначение
healthlog принимает выгрузки Apple Health из приложения Health Auto Export
@@ -50,6 +56,8 @@ healthlog принимает выгрузки Apple Health из приложен
## Формат Health Auto Export
<!-- канон: поведение → openspec/specs/parsing -->
Документация формата скудная: [help.healthyapps.dev](https://help.healthyapps.dev/en/health-auto-export/automations/rest-api/)
и [wiki Lybron/health-auto-export](https://github.com/Lybron/health-auto-export/wiki/API-Export---JSON-Format).
Ниже — то, на что мы опираемся; всё остальное уточняем по реальным пакетам.
@@ -84,7 +92,7 @@ healthlog принимает выгрузки Apple Health из приложен
Вопреки документации, в точке **есть поле `source`** — какие устройства
вложились в значение (составное, через `|`). Что ещё документация описывает
неверно и как поток выглядит на самом деле — [local-research.md](local-research.md).
неверно и как поток выглядит на самом деле — [research/apple-health.md](research/apple-health.md).
Даты приходят строкой с офсетом: `2026-07-31 12:00:00 +0300` — не RFC 3339.
@@ -95,7 +103,7 @@ healthlog принимает выгрузки Apple Health из приложен
данные» выключен, группировка при этом недоступна). Причина — суммированные
значения досчитываются задним числом: минутное ведро уезжает неполным и в
следующей доставке приезжает полным
([local-research.md](local-research.md), находка 10). На несуммированных
([research/apple-health.md](research/apple-health.md), находка 10). На несуммированных
данных расхождений не наблюдалось (находка 3), поэтому идентичность по
содержимому работает без оговорок. Заодно сохраняются детали, которые
группировка съедает: эпизоды сна и межударные интервалы (находки 6, 19).
@@ -205,19 +213,22 @@ HRV); у накопительных — только `date`. Поэтому то
## Компоненты
| Пакет | Ответственность |
| ---------- | ------------------------------------------------------ |
| `config` | загрузка и валидация TOML-конфига |
| `logging` | сборка slog-логгера |
| `ident` | генерация и разбор ULID |
| `archive` | сырой архив: запись тела, чтение для reindex, ретеншен |
| `hae` | разбор формата HAE, канонизация, хеш содержимого |
| `ingest` | use-case приёма, общий для HTTP и CLI `import` |
| `fold` | свёртка одной доставки в часовые объекты |
| `replay` | проигрывание журнала в витрину: состав, порядок, отчёт |
| `catalog` | каталог разрезов и измерение рода агрегации |
| `store` | SQLite: доставки, часовые объекты, тренировки, записи |
| `httpapi` | приём и read API |
Пакет — это реализация; **что система делает, нормативно сказано в
capability**, и здесь стоит ссылка, а не пересказ требований.
| Пакет | Ответственность | Capability |
| ---------- | ------------------------------------------------------ | ---------- |
| `config` | загрузка и валидация TOML-конфига | — |
| `logging` | сборка slog-логгера | — |
| `ident` | генерация и разбор ULID | — |
| `archive` | сырой архив: запись тела, чтение для reindex, ретеншен | [`storage`](../openspec/specs/storage/spec.md) |
| `hae` | разбор формата HAE, канонизация, хеш содержимого | [`parsing`](../openspec/specs/parsing/spec.md) |
| `ingest` | use-case приёма, общий для HTTP и CLI `import` | [`ingest`](../openspec/specs/ingest/spec.md) |
| `fold` | свёртка одной доставки в часовые объекты | [`storage`](../openspec/specs/storage/spec.md) |
| `replay` | проигрывание журнала в витрину: состав, порядок, отчёт | [`reindex`](../openspec/specs/reindex/spec.md) |
| `catalog` | каталог разрезов и измерение рода агрегации | [`catalog`](../openspec/specs/catalog/spec.md) |
| `store` | SQLite: доставки, часовые объекты, тренировки, записи | [`storage`](../openspec/specs/storage/spec.md) |
| `httpapi` | приём и read API | [`ingest`](../openspec/specs/ingest/spec.md), [`catalog`](../openspec/specs/catalog/spec.md) |
## Приём
@@ -309,7 +320,7 @@ HRV); у накопительных — только `date`. Поэтому то
предшественницы, слоя не выведет и уйдёт в `failed`: её точки доедут только
пересборкой. Окно узкое, и изменение его сужает, а не открывает, — но закрытие
предела требует удерживать порядок на самом приёме, и это отдельный вопрос
(беклог, блокеры).
(задача `journal-order-on-ingest`).
Остановка формулируется **инвариантом**: приём прекращается раньше воркера, и
после остановки не существует доставки, которая числится разобранной, а записана
@@ -378,6 +389,8 @@ HRV); у накопительных — только `date`. Поэтому то
### Сырой архив и восстановление состояния
<!-- канон: поведение → openspec/specs/reindex -->
`raw/ГГГГ/ММ/ДД/<ulid>.json.gz` — тело запроса как пришло, не редактируется.
Два источника вместе образуют **полный журнал событий**, а хранилище —
@@ -521,6 +534,8 @@ HAE. Значит для него доставки не хвост журнал
### Версия витрины и обслуживание журнала
<!-- канон: поведение → openspec/specs/reindex -->
Два механизма живут рядом и держатся друг за друга: один говорит читателю «в
базу никто не коммитил», второй разбирает журнал, в который эти коммиты легли.
@@ -647,6 +662,8 @@ Litestream) не взят по названной причине: он двиг
### Устаревание нижнего слоя
<!-- канон: поведение → openspec/specs/storage -->
Родной экспорт Apple Health точнее HAE (находка 34) и делается раз в 2–3
месяца. Данные HAE в нижнем слое старше последнего экспорта избыточны: тот же
период лежит в слое `sample` подробнее и честнее.
@@ -683,6 +700,8 @@ Litestream) не взят по названной причине: он двиг
### Часовые объекты метрик
<!-- канон: поведение → openspec/specs/storage -->
Точки метрик хранятся не по одной, а **пачками: один объект = одна метрика за
один час UTC**.
@@ -719,6 +738,8 @@ record(kind, id, ts_utc, tz_offset, payload BLOB, content_hash,
### Слои гранулярности
<!-- канон: поведение → openspec/specs/storage -->
Одна и та же метрика может приходить с разной подробностью: несуммированной,
минутной, часовой. Мы **не сводим их к одной** и не агрегируем сами — храним
разрезами и говорим клиенту, какие разрезы есть.
@@ -779,7 +800,7 @@ hour метки выровнены на час heart_rate 00:00:00
Слово «предшествующей» в третьем пункте несёт вес: слой обязан быть функцией от
**префикса журнала**. Наследование от последней доставки вообще делает свёртку
зависящей от истории, и пересборка даёт не то состояние, что живой приём —
поймано прогоном архива, 1737 объектов против 1742 (docs/review-journal.md).
поймано прогоном архива, 1737 объектов против 1742 (docs/review.md).
Классифицировать доставку целиком нельзя: при перенастройке автоматизации
приезжают **смешанные доставки**, где часть метрик уже минутная, а часть ещё
@@ -810,7 +831,7 @@ hour метки выровнены на час heart_rate 00:00:00
причина держать сырой архив. Точнее она именно этим, а не тем, что видит более
длинный ряд: слой обязан оставаться функцией **префикса** журнала, и наследование
«от последней доставки вообще» уже ловили дефектом (1737 объектов против 1742,
`docs/review-journal.md`).
`docs/review.md`).
Следствие: **пересечение наборов метрик между автоматизациями перестаёт быть
проблемой**. Минутный и несуммированный `heart_rate` наполняют разные слои и
@@ -928,6 +949,8 @@ hour метки выровнены на час heart_rate 00:00:00
### Измерение рода агрегации
<!-- канон: поведение → openspec/specs/catalog -->
Род метрики — `cumulative`, `instant` или `unknown` — выводится сверкой
минутного слоя с часовым. Правило целиком:
@@ -1062,6 +1085,8 @@ Assistant требует ручного удаления статистики).
### Категориальные значения
<!-- канон: поведение → openspec/specs/parsing -->
HAE отдаёт перечислимые значения строками из локали телефона, а не кодами:
фаза сна приезжает как «БДГ», контекст пульса — как «Сидячий образ жизни», тип
тренировки — как «В помещении Ходьба» (машинная калька с `Indoor Walk`). При
@@ -1095,6 +1120,8 @@ value_code "HKCategoryValueSleepAnalysisAsleepREM" ← выведено по с
### Тренировки и прочие секции
<!-- канон: поведение → openspec/specs/parsing -->
Тренировка адресуется своим `id` из HealthKit, запись — парой `род + id`.
`record` держит секции с собственными идентификаторами; разбором покрыт пока
только `stateOfMind`, а `ecg`, `symptoms`, `cycleTracking`, `medications` и