tasks: закрыт разбор и хранилище, начат спринт по чтению данных клиентами

- цель parsing-and-storage закрыта по своему критерию; незакрываемый остаток
  (новые формы от источника, ручные секции задним числом) переехал в тему
  parsing-completeness
- цель mcp поглощена целью read-api, переименованной в «Чтение данных
  клиентами»: адаптер — последний шаг того же направления, а не своё
- read-api-points разложена на конверт с точками, свёртку по сетке и
  тренировки с записями; спринт 2026-08-04 набран пятью задачами
This commit is contained in:
av
2026-08-04 14:01:38 +03:00
parent 637eb38bce
commit 79331ac670
17 changed files with 276 additions and 144 deletions
@@ -0,0 +1,56 @@
# Конверт ответа и точки за период
- **Секция:** ядро
- **Зачем:** Точки лежат в витрине и наружу не отдаются: ни один потребитель не может спросить «вес за год» иначе как sqlite3 на хосте
- **Теги:** goal:read-api
Потребитель получает значения метрики за период одним запросом
`?from&to`, и из ответа видно, что именно ему отдали: слой, род свёртки и
границу окна, в котором род измерен.
Первая из трёх частей, на которые разложена `read-api-points`. Здесь решается
**конверт** — форма, которую унаследуют все остальные маршруты чтения и оба
транспорта.
**Форма провода решается здесь, один раз.** Сегодня типы `internal/catalog`
сами несут json-теги, а транспорт владеет только обёрткой: переименование поля
в домене меняет публичный контракт без касания `httpapi`, и держит это один
байтовый тест непустого ответа. Либо объявить в `architecture.md`, что типы
чтения и есть форма провода для всех транспортов (HTTP и MCP отдают её байт в
байт), либо завести DTO в транспорте. Выбрать надо **до** того, как образец
скопируют соседние задачи.
**Машинерия условного запроса готова — её берут, а не пишут заново.**
`store.VersionedRead` держит правило «версией, снятой после чтения, не
подписывать»; `httpapi` умеет `If-None-Match` и `304`. Новое здесь одно: метка
обязана нести **область действия** — у точек ответ есть функция параметров
запроса, поэтому `etag(scope, version)` требует их канонизированной формы, иначе
`304` ответит на другой набор данных. Детали — `docs/architecture.md`,
«Условный запрос».
**Клиент обязан видеть границы окна измерения.** Род метрики измерен по 48 самым
свежим **общим** часам, а не по последним 48 часам календаря: выключенная
минутная автоматизация HAE останавливает пополнение множества общих часов, и
окно замирает, продолжая объявлять род. Единственный след — `last_hour` в
ответе; конверт обязан его нести.
## Критерии приёмки
- «вес за год» отвечается одним запросом без доступа к файлу базы — оракул:
запрос к поднятому сервису на живом архиве
- в ответе всегда видны `layer`, `aggregation` и `last_hour` — оракул: тест на
форме ответа
- повторный запрос с `If-None-Match` даёт `304`, а тот же запрос с другими
параметрами — `200` с другим телом — оракул: тест на паре запросов с разной
областью действия метки
- форма провода объявлена в `docs/architecture.md` — доменные типы или DTO, —
и соседние маршруты ссылаются на это решение, а не повторяют выбор — оракул:
глазами по разделу
## Рамки
Схема не трогается, данные только читаются, сервис перезапускается. Против
`./data` — только `task up` / `task run`.
Связано: `docs/architecture.md` → «Read API», «Условный запрос», «Измерение рода
агрегации».