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
@@ -0,0 +1,75 @@
## Why
Публичный контракт единственного читающего маршрута сегодня **выведен из формы
доменных типов**: `internal/catalog` сам несёт `json`-теги, а
`internal/httpapi/catalog.go` владеет только оболочкой `{"metrics": …}`.
Переименование поля в домене, разъединение встроенного `Basis` или добавление
внутреннего поля меняют байты ответа, не касаясь транспорта; удерживает это один
литерал в тесте `TestКаталогОтдаётОжидаемыеБайты`, и о том, что он и есть
контракт, в коде не сказано нигде.
Пока маршрут один и потребителей у него нет, цена нулевая. Со следующей задачи
цели образец копируют точки, тренировки, записи и MCP — четыре маршрута и второй
транспорт. Решение принимается **до** копирования: потом это будет не решение, а
археология. Момент выбран ещё и потому, что менять контракт каталога сейчас
бесплатно — снаружи его не читает никто.
## What Changes
- **Форма провода объявляется транспортом, а не выводится из домена.** Каждый
читающий маршрут `internal/httpapi` объявляет собственные типы ответа с
`json`-тегами; доменные типы `internal/catalog` тегов лишаются, перевод —
явная функция транспорта.
- **Доменные типы каталога перестают быть публичным контрактом.** `Metric`,
`LayerRange`, `Aggregation`, `Basis` остаются формой ответа **use-case**, а не
формой ответа HTTP. `Style.MarshalJSON` убирается: сериализацию рода кладёт
транспорт. Значения словаря (`cumulative`/`instant`/`unknown`) при этом
остаются в домене — провод зовёт `Style.String()`, чтобы не завести второй
словарь; граница и её цена названы в `design.md`.
- **Тело отказа тоже получает объявленный тип.** Сегодня `writeError` собирает
его из `map[string]string`; у читающего маршрута тело отказа — часть того же
контракта, и оно не должно оставаться единственным местом, куда не смотрит ни
один сторож. Байты те же.
- **Байты ответа каталога не меняются.** Правка структурная; контракт из
требования «Форма ответа каталога» остаётся тем же до символа, и это
утверждается тестом, а не намерением.
- **Заводится проверка, прямо утверждающая разделение:** ни один тип домена не
достигает сериализации ответа — обход графа типов значения ответа, с заведомо
красным случаем рядом. Отсюда и следует, что переименование поля домена до
провода не доезжает.
- **Решение и цена обеих сторон записываются** в `docs/architecture.md`
(раздел Read API); правило о **механизме** проверки — в
`docs/conventions/testing.md`, где у правил такого рода дом.
- Схема не трогается, миграции нет, данные только читаются.
## Capabilities
### New Capabilities
- `read-api`: общие правила читающих маршрутов, поверх которых встают точки,
тренировки, записи и MCP. Два правила: **публичный контракт меняется только
правкой транспорта** (форма провода объявляется явно и не выводится из формы
доменных типов) и **пустая коллекция — список, а не отсутствие**.
### Modified Capabilities
Нет. Поведение каталога не меняется: байты ответа, заголовки и коды остаются
прежними, требование «Форма ответа каталога» из `catalog` продолжает описывать
их без единой правки. Это и есть проверяемое утверждение изменения.
## Impact
- `internal/catalog/catalog.go` — снятие `json`-тегов с `Basis`, `Aggregation`,
`LayerRange`, `Metric`; удаление `Style.MarshalJSON`.
- `internal/catalog/measure_test.go``TestStyleСловарь` переезжает с
`MarshalJSON` на `String()`.
- `internal/httpapi/catalog.go` — типы провода каталога и перевод из домена.
- `internal/httpapi/httpapi.go` — объявленный тип тела отказа вместо карты.
- `internal/httpapi/catalog_test.go` — байтовые утверждения формы (измеренное
окно, неизмеренное окно, пустая витрина).
- `internal/httpapi/wire_internal_test.go` — обход графа типов ответа и заведомо
красный случай к нему.
- `docs/architecture.md` — раздел Read API: решение с ценой обеих сторон.
- `docs/conventions/testing.md` — правило о механизме: байты на каждую
различимую форму ответа, заведомо красный случай для структурной проверки.
- Схема, витрина, архив, конфиг — не трогаются.