docs: документация приведена к канону av-dev-pm 4
- каждая запись каталога задач получила тип вместо тега kind: и префикса заголовка; секция роадмапа «Разработка» стала «Сопровождением», порядок секций канонический - поправлены протухшие факты: нереализованные маршруты Read API, MCP и `healthlog import`, словарь слоёв в инварианте, семантика гейта по покрытию диффа, периметр перестал дублировать security.md - замер слияния переведён с находки 49 на находку 54, заполнены Purpose спек storage и parsing
This commit is contained in:
+54
-32
@@ -46,12 +46,14 @@ healthlog принимает выгрузки Apple Health из приложен
|
||||
с переведённой строкой (см. «Категориальные значения»): он приписывается, а
|
||||
не подменяет.
|
||||
- **Своей агрегации в хранении нет — есть слои.** Метрика лежит в тех
|
||||
разрезах подробности, в которых пришла (`sample`/`raw`/`minute`/`hour`);
|
||||
переагрегирования при записи не происходит никогда.
|
||||
- **Агрегация в ответе — только измеренная.** Read API умеет свести метрику к
|
||||
запрошенной сетке, но род свёртки (сумма или среднее) выведен сверкой слоёв
|
||||
между собой, а не проставлен вручную. Где род неизвестен, агрегация не
|
||||
предлагается: отдаются значения как есть.
|
||||
разрезах подробности, в которых пришла (перечень слоёв —
|
||||
[database.md](database.md), таблица `bucket`); переагрегирования при записи
|
||||
не происходит никогда.
|
||||
- **Агрегация в ответе — только измеренная.** Род свёртки (сумма или среднее)
|
||||
выведен сверкой слоёв между собой, а не проставлен вручную. Где род
|
||||
неизвестен, агрегация не предлагается: отдаются значения как есть. Свёртка к
|
||||
запрошенной сетке объявлена контрактом и **ещё не реализована** — параметр
|
||||
`bucket` отвергается `400` (задача `read-api-points-bucket`).
|
||||
- **Минимум компонентов** — один процесс, SQLite, файлы. Без очередей и
|
||||
внешних зависимостей.
|
||||
|
||||
@@ -170,7 +172,7 @@ HRV); у накопительных — только `date`. Поэтому то
|
||||
```
|
||||
дыра моложе суток → закроется в течение часа
|
||||
дыра моложе недели → закроется в течение суток
|
||||
дыра старше недели → не закроется; лечится `healthlog import`
|
||||
дыра старше недели → не закроется; лечится только `healthlog import` (ещё не написан)
|
||||
```
|
||||
|
||||
Широкие проходы почти бесплатны именно из-за часовых объектов: глубокий проход
|
||||
@@ -225,7 +227,7 @@ capability**, и здесь стоит ссылка, а не пересказ т
|
||||
| `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) |
|
||||
| `ingest` | use-case приёма, общий для HTTP и будущего CLI `import` | [`ingest`](../openspec/specs/ingest/spec.md) |
|
||||
| `fold` | свёртка одной доставки в часовые объекты | [`storage`](../openspec/specs/storage/spec.md), [`uncovered-sections`](../openspec/specs/uncovered-sections/spec.md) |
|
||||
| `replay` | проигрывание журнала в витрину: состав, порядок, отчёт | [`reindex`](../openspec/specs/reindex/spec.md) |
|
||||
| `catalog` | каталог разрезов и измерение рода агрегации | [`catalog`](../openspec/specs/catalog/spec.md) |
|
||||
@@ -235,6 +237,8 @@ capability**, и здесь стоит ссылка, а не пересказ т
|
||||
|
||||
## Приём
|
||||
|
||||
<!-- канон: поведение → openspec/specs/ingest -->
|
||||
|
||||
```
|
||||
запрос → токен → лимит тела, gzip → проверка формы JSON
|
||||
→ запись тела в архив → строка в delivery → 200
|
||||
@@ -264,7 +268,8 @@ capability**, и здесь стоит ссылка, а не пересказ т
|
||||
- **200** — тело сохранено в архив. Дальше даже полный провал разбора
|
||||
(незнакомая метрика, новая форма точки) не меняет ответ: данные уже в
|
||||
безопасности, исход разбора виден в логе, в `delivery.parse_status` и в
|
||||
`/stats`, а доразобрать их можно командой `reindex`.
|
||||
`/stats` (маршрут — задача `stats-endpoint`), а доразобрать их можно командой
|
||||
`reindex`.
|
||||
|
||||
#### Очередь свёртки — таблица, а не структура в памяти
|
||||
|
||||
@@ -942,8 +947,11 @@ hour метки выровнены на час heart_rate 00:00:00
|
||||
|
||||
#### Разрешение столкновений
|
||||
|
||||
По одним координатам приезжают разные содержимые: 2 897 случаев из 444 256
|
||||
координат, 0.65% (находка 49). Выигрывает **более полная** точка, и полнота —
|
||||
По одним координатам приезжают разные содержимые: спорных координат 80 129 из
|
||||
460 995 (17,4%), и полнота отбрасывает кого-то лишь в 981 из них (1,2%) —
|
||||
остальное решает тай-брейк ([research/apple-health.md](research/apple-health.md),
|
||||
находка 54; прежняя оценка «0,65%» из находки 49 считала ключ без слоя).
|
||||
Выигрывает **более полная** точка, и полнота —
|
||||
это сравнение **множеств** ключей с непустым значением, а не их числа.
|
||||
|
||||
Число сравнимо всегда и потому отвечает там, где ответа нет: точка
|
||||
@@ -981,9 +989,9 @@ hour метки выровнены на час heart_rate 00:00:00
|
||||
отдельно (см. ниже).
|
||||
|
||||
**Несравнимые множества не сливаются, а считаются.** Объединение полей — самая
|
||||
дорогая часть правила — на живом потоке не потребовалось ни разу (0 из 2 897),
|
||||
поэтому вместо реализации стоит счётчик и `WARN` с координатами объекта. Если
|
||||
событие наступит, оно будет видно, а не додумано заранее.
|
||||
дорогая часть правила — на живом корпусе наступило дважды на 155 доставок
|
||||
(находка 54), поэтому вместо реализации стоит счётчик и `WARN` с координатами
|
||||
объекта. Событие видно, а не додумано заранее.
|
||||
|
||||
**Тай-брейк при равной полноте — пришедшая доставка.** Порядок канонических
|
||||
форм отвергнут замером: он берёт меньшее значение в 96% случаев (находка 49) и
|
||||
@@ -1445,18 +1453,26 @@ MongoDB, и так просилось из слова «перезаписыва
|
||||
|
||||
## Read API
|
||||
|
||||
<!-- канон: поведение → openspec/specs/read-api -->
|
||||
|
||||
```
|
||||
GET /api/v1/metrics каталог: имя, units, род, слои с диапазонами
|
||||
GET /api/v1/metrics/{name}?from&to&layer точки метрики за период (bucket — соседняя задача, пока 400)
|
||||
GET /api/v1/workouts?from&to заголовки тренировок
|
||||
GET /api/v1/workouts/{id} тренировка целиком, с маршрутом
|
||||
GET /api/v1/records/{kind}?from&to прочие секции
|
||||
GET /api/v1/schema схемы всего, что есть в хранилище
|
||||
GET /api/v1/metrics/{name}/schema схема и статистика одной метрики
|
||||
GET /stats последняя доставка, счётчики, тишина по потоку
|
||||
GET /healthz
|
||||
```
|
||||
|
||||
**Целевая поверхность шире реализованной.** Маршрутов ниже в роутере ещё нет,
|
||||
и запрос к ним получает `404`:
|
||||
|
||||
```
|
||||
GET /api/v1/workouts?from&to заголовки тренировок → read-api-workouts
|
||||
GET /api/v1/workouts/{id} тренировка целиком, с маршрутом → read-api-workouts
|
||||
GET /api/v1/records/{kind}?from&to прочие секции → read-api-records
|
||||
GET /api/v1/schema схемы всего, что есть в хранилище → цель self-description
|
||||
GET /api/v1/metrics/{name}/schema схема и статистика одной метрики → цель self-description
|
||||
GET /stats последняя доставка, счётчики, тишина → stats-endpoint
|
||||
```
|
||||
|
||||
Хранение пачками на контракт не влияет: `GET /metrics/{name}` собирает ответ
|
||||
из часовых объектов, попавших в диапазон, и отдаёт точки. Клиент про объекты
|
||||
не знает — это деталь хранения, а не API.
|
||||
@@ -1687,8 +1703,9 @@ Docker и go-kit; версионирование с конверсией у Kube
|
||||
|
||||
### MCP
|
||||
|
||||
Поверх Read API — адаптер MCP, чтобы агент подключался без промежуточного
|
||||
кода. Инструментов ровно два, по числу форм запроса выше, плюс каталог.
|
||||
Поверх Read API **встанет** адаптер MCP, чтобы агент подключался без
|
||||
промежуточного кода — кода адаптера сегодня нет, это задача `mcp-server` цели
|
||||
`read-api`. Инструментов ровно два, по числу форм запроса выше, плюс каталог.
|
||||
Собственной логики в адаптере нет: он переводит вызовы в те же обработчики.
|
||||
|
||||
**Транспорт — HTTP** (Streamable HTTP), не stdio: сервис живёт на VPS, и агент
|
||||
@@ -1741,18 +1758,18 @@ Docker и go-kit; версионирование с конверсией у Kube
|
||||
|
||||
## Аутентификация
|
||||
|
||||
Статический токен в заголовке `Authorization: Bearer …`; список допустимых
|
||||
токенов — в конфиге. HAE умеет слать произвольные заголовки, этого достаточно.
|
||||
|
||||
Токены **раздельные**: на запись (приём) и на чтение. Клиент, читающий
|
||||
данные, не может писать. MCP пользуется токеном чтения — отдельного контура
|
||||
у него нет, см. «MCP».
|
||||
|
||||
Наружу открыты два контура: приём (телефон) и чтение вместе с MCP (агенты и
|
||||
приложения). Оба через Caddy с TLS, оба с разными токенами.
|
||||
Периметр, модель угроз и разграничение контуров — [security.md](security.md),
|
||||
разделы «Периметр» и «Что разграничивает доступ»; сегодняшний контур отличается
|
||||
от целевого, и сказано это там. Здесь важно одно следствие для компоновки: MCP —
|
||||
эндпоинт того же процесса и того же контура чтения, отдельного контура доступа у
|
||||
него нет (см. «MCP»).
|
||||
|
||||
## Деплой
|
||||
|
||||
**Целевая** раскладка; сегодняшний контур — [security.md](security.md),
|
||||
«Периметр», статус работ — [tasks/ROADMAP.md](tasks/ROADMAP.md),
|
||||
«Сопровождение».
|
||||
|
||||
VPS **rivendell** (Timeweb), доступен всегда. Перед сервисом — **Caddy**, он
|
||||
терминирует TLS; сам сервис слушает plain HTTP. Приём открыт наружу на
|
||||
отдельном поддомене — телефон должен доставать до него из любой сети, иначе
|
||||
@@ -1764,7 +1781,12 @@ VPS **rivendell** (Timeweb), доступен всегда. Перед серв
|
||||
Тома: каталог сырого архива и файл SQLite — на постоянном хранении, конфиг
|
||||
(с токенами) — отдельно, `0600`.
|
||||
|
||||
**Откат бинаря поверх новой схемы отказывает на старте.** Версия схемы базы выше
|
||||
<!-- канон: поведение → openspec/specs/storage -->
|
||||
|
||||
**Откат бинаря поверх новой схемы отказывает на старте** — правило нормировано в
|
||||
[`storage`](../openspec/specs/storage/spec.md), требование «Открытие базы
|
||||
отказывает при схеме из будущего»; здесь только следствия для деплоя. Версия
|
||||
схемы базы выше
|
||||
версии, вшитой в бинарь, — отказ, а не повод мигрировать; в контейнере это
|
||||
выглядит циклом перезапуска, и лечится возвратом бинаря вперёд. Версию читает
|
||||
сам goose (`Provider.GetVersions`), а не собственный запрос: имя таблицы учёта и
|
||||
|
||||
Reference in New Issue
Block a user