httpapi: форма провода читающих маршрутов объявлена транспортом

- доменные типы internal/catalog лишились json-тегов и MarshalJSON; типы
  metricWire/layerWire/aggregationWire и перевод catalogWire живут в транспорте,
  тело отказа тоже получило объявленный тип — байты ответа не изменились
- заведён сторож: обход графа типов ответа утверждает, что домен не доезжает до
  сериализации, плюс требование json-тега на полях транспортных структур и
  заведомо красные случаи к обоим правилам
- решение с ценой обеих сторон записано в architecture.md и ADR; шаг lint в
  гейте получил свой кеш — общий на машину красил прогон находками из чужого
  worktree
This commit is contained in:
av
2026-08-04 16:18:05 +03:00
parent bd832337df
commit a834d10415
19 changed files with 1721 additions and 53 deletions
+46 -1
View File
@@ -230,7 +230,7 @@ capability**, и здесь стоит ссылка, а не пересказ т
| `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) |
| `httpapi` | приём, read API и **форма провода** ответов чтения | [`ingest`](../openspec/specs/ingest/spec.md), [`catalog`](../openspec/specs/catalog/spec.md), [`read-api`](../openspec/specs/read-api/spec.md) |
## Приём
@@ -1594,6 +1594,51 @@ GET /healthz
семантику разбирает клиент по имени метрики. Полная нормализация означала бы,
что каждая новая метрика требует правки коллектора, а незнакомая теряется.
### Форма провода
**Форму ответа объявляет транспорт, а не домен.** Каждый читающий маршрут
`internal/httpapi` держит собственные типы с `json`-тегами и переводит в них
доменное значение присваиванием поле в поле; доменные типы (`internal/catalog`
и далее) `json`-тегов не несут и до сериализации не доезжают. То же правило
покрывает тело отказа. MCP собственной формы не объявляет — адаптер переводит
вызовы в те же обработчики.
Цена названа с обеих сторон, потому что она обратная, а не односторонняя.
- **Домен = провод** (как было у каталога): формы объявлены один раз, перевода
нет, ноль строк на маршрут. Платим тем, что публичный контракт меняется
правкой домена **молча** — переименованием поля, разъединением встроенной
структуры (плоскость `aggregation` была следствием встраивания `Basis`),
появлением внутреннего поля. Ни одна из трёх правок транспорт не трогает.
- **Раздельно** (взято): контракт меняется только правкой транспорта, то есть
действием. Платим двумя вещами. Форма объявлена дважды — типы и перевод на
каждый маршрут. И цена **обратная**: новое поле домена в ответ само не
попадёт, его обязан перечислить перевод; поле, не доехавшее до клиента, —
такой же дефект, как поле, уехавшее случайно, просто другой.
Развилку решил факт, а не вкус: провод точек обещан как
`{ts, tz_offset, units, values}`, а `store.Point` несёт
`{Start, End, OffsetSeconds, Raw}` — эти наборы не совпадают ни одним именем,
и доменный тип формой провода там быть не может. Хранилище, кстати, уже живёт
по этому правилу: формат `payload` объявлен отдельным неэкспортированным
`storedPoint`, а `encodePayload` переводит в него полем в поле.
Сторожей два, и роли у них разные. **Обход графа типов ответа** (внутренний
тест `httpapi`) утверждает, что домен до энкодера не доезжает — отсюда и
следует, что переименование поля домена байт не меняет; рядом стоит заведомо
красный случай, потому что проверка, доказывающая отсутствие, зелена и будучи
сломанной. **Байтовый литерал** на каждую различимую форму ответа — детектор
изменения формы: он краснеет в момент правки. Источником истины контракта он
не является — им станет рукописная OpenAPI-спека, и сверку с маршрутами внесёт
в гейт отдельная задача.
Разбор чужих решений (домен = провод у `wtf` и Prometheus; раздельно у Gitea,
Docker и go-kit; версионирование с конверсией у Kubernetes; отвергнутый
`apidiff`, который смены `json`-тега не видит вовсе) —
[design.md изменения](../openspec/changes/archive/2026-08-04-forma-provoda-chteniya/design.md).
Ссылка markdown-ссылкой намеренно: инлайн-код `docs.py check` не проверяет, а
путь угадывался до архивации.
### MCP
Поверх Read API — адаптер MCP, чтобы агент подключался без промежуточного