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