## 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` — правило о механизме: байты на каждую
  различимую форму ответа, заведомо красный случай для структурной проверки.
- Схема, витрина, архив, конфиг — не трогаются.
