## ADDED Requirements

### Requirement: Публичный контракт читающего маршрута меняется только правкой транспорта

Система SHALL объявлять форму ответа каждого читающего маршрута типами
транспортного слоя и MUST NOT выводить её из формы доменных типов: ни один тип
домена MUST NOT достигать сериализации ответа, а перевод домена в форму провода
MUST быть явным перечислением полей.

**Читающий маршрут** здесь — тот, что отдаёт наружу состояние витрины: каталог,
точки, тренировки, записи, статистика. `/healthz` под правило не подпадает —
его тело есть литеральный признак живости процесса, а не данные.

**MCP собственной формы провода не объявляет.** Адаптер переводит вызовы в те
же обработчики (`docs/architecture.md`, раздел «MCP»), поэтому объявленная форма
у контракта одна на оба транспорта. Второе объявление развело бы их молча.

Правило распространяется и на **тело отказа**: у читающего маршрута оно часть
того же контракта, и клиент видит его чаще успешного ответа. Тело отказа MUST
собираться объявленным типом транспорта, а не картой и не свободной строкой.

**Следствие, ради которого правило и существует:** переименование поля
доменного типа, разъединение встроенной в него структуры и появление в нём
нового поля байты ответа не меняют — до сериализации доменный тип не доезжает.
Смена публичного контракта становится правкой транспортного слоя, то есть
действием, а не побочным эффектом.

**Цена названа и взята сознательно, потому что она обратная.** Новое поле
домена не попадает в ответ само: чтобы клиент его увидел, транспорт обязан его
перечислить. Контракт перестаёт меняться случайно в обе стороны, и это дороже
ровно на объём перевода.

Из правила есть одно исключение, и оно ограничено **содержимым**, а не
конвертом: значение, которое хранилище держит дословно, уезжает клиенту сырым
JSON без разбора и переобъявления — переписывать его в тип провода значило бы
нарушить инвариант «точки хранятся дословно». Конверт вокруг такого значения —
метки времени, офсет, единицы, слой — объявляется типом провода и нормализуется,
как того требует инвариант «форма Apple не транслируется».

#### Scenario: Поле доменного типа переименовано

- **GIVEN** поле доменного типа, из которого собирается ответ, переименовано, а
  транспортный слой не тронут
- **WHEN** маршрут отвечает на прежний запрос при прежнем состоянии витрины
- **THEN** байты ответа те же

#### Scenario: Форма провода изменена намеренно

- **GIVEN** транспорт переименовал поле объявленной формы
- **WHEN** маршрут отвечает на прежний запрос при прежнем состоянии витрины
- **THEN** байты ответа изменились, и изменение целиком лежит в правке
  транспортного слоя

#### Scenario: Читающий маршрут отвечает отказом

- **WHEN** читающий маршрут отвечает кодом отказа
- **THEN** тело отказа собрано объявленным типом транспорта

#### Scenario: Словарь значения виден клиенту строкой

- **GIVEN** доменное перечисление, чьи значения клиент видит строками
- **WHEN** маршрут отвечает
- **THEN** строку в ответ кладёт транспорт, а домен собственной сериализации не
  несёт

#### Scenario: Дословное содержимое проходит насквозь

- **WHEN** в ответ идёт значение, сохранённое хранилищем дословно
- **THEN** оно уезжает сырым JSON, а конверт вокруг него объявлен типом провода

### Requirement: Пустая коллекция чтения — список, а не отсутствие

Система SHALL отдавать пустую коллекцию читающего маршрута как `[]` и MUST NOT
отдавать её как `null`; отсутствующее значение MUST отдаваться как `null` и
MUST NOT подменяться нулевым значением своего типа.

Правило общее для всех читающих маршрутов, потому что цена у него одна:
`nil`-срез сериализуется в `null`, и клиент читает «поля нет» там, где на самом
деле «элементов нет»; правдоподобная нулевая дата в ответе неотличима от
настоящей. Конкретные коллекции и поля каждого маршрута нормирует его
собственная capability — здесь только правило, чтобы следующий маршрут не
принимал его заново.

Ручной перевод домена в форму провода делает это правило не теоретическим:
присваивание поле в поле копирует `nil` и разыменовывает указатель ровно так,
как написано, и обе ошибки молчат.

#### Scenario: Коллекция ответа пуста

- **WHEN** в ответ читающего маршрута идёт коллекция без единого элемента
- **THEN** она уезжает как `[]`

#### Scenario: Значения нет

- **WHEN** поле ответа не имеет значения
- **THEN** оно уезжает как `null`, а не как нулевая метка времени, пустая
  строка или ноль
