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:
+45
-18
@@ -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` и
|
||||
|
||||
Reference in New Issue
Block a user