## 1. Домен перестаёт быть формой провода

- [x] 1.1 `internal/catalog`: снять `json`-теги с `Basis`, `Aggregation`,
  `LayerRange`, `Metric`; комментарии типов переписать так, чтобы они называли
  их формой ответа use-case, а не формой ответа HTTP
- [x] 1.2 `internal/catalog`: удалить `Style.MarshalJSON`; `String()` оставить —
  он нужен логам и сообщениям тестов, и провод зовёт его же
- [x] 1.3 `internal/catalog/measure_test.go`: `TestStyleСловарь` переезжает с
  `MarshalJSON` на `String()`, значения словаря те же

## 2. Форма провода в транспорте

- [x] 2.1 `internal/httpapi/catalog.go`: типы `metricWire`, `layerWire`,
  `aggregationWire` с `json`-тегами; поля `aggregation` перечислены **плоско**,
  а не встраиванием — плоскость перестаёт быть следствием формы домена
- [x] 2.2 перевод ровно этой сигнатуры (одна на все документы изменения):
  `func catalogWire(metrics []catalog.Metric) catalogResponse` — чистая функция
  без `context`, без хранилища, без часов; род кладётся строкой через
  `Style.String()`; пустые коллекции — `[]`, отсутствующие метки — `null`
- [x] 2.3 `handleMetrics` собирает ответ вызовом `catalogWire`; версия снимка
  по-прежнему уходит только в `ETag`, в тело не попадает
- [x] 2.4 `internal/httpapi/httpapi.go`: тело отказа собирается объявленным
  типом вместо `map[string]string`; байты те же — `{"error":"…"}`

## 3. Проверки

- [x] 3.1 `internal/httpapi/wire_internal_test.go` (внутренний тест пакета):
  обход графа типов значения ответа — поля, срезы, массивы, ключи и значения
  карт, указатели, встроенные и неэкспортированные поля; защита от
  самоссылающегося типа; принадлежность определяется по `PkgPath` с префиксом
  module path, а не по имени пакета
- [x] 3.2 обход применён **таблицей** «маршрут → образец ответа»: каталог, тело
  отказа. Следующий маршрут добавляет строку
- [x] 3.3 **заведомо красный случай**: на фикстуре, содержащей `catalog.Metric`,
  обход обязан быть красным. Он же ловит устаревший префикс module path
- [x] 3.4 исключение задано **по типу** `json.RawMessage`, а не по признаку
  «есть свой `MarshalJSON`»; кейс с доменным типом, несущим `MarshalJSON`, —
  красный
- [x] 3.5 `TestКаталогОтдаётОжидаемыеБайты` остаётся; литерал не правится ни
  одним символом, комментарий называет его **детектором изменения формы**, а
  источником истины — рукописную OpenAPI-спеку (задача `openapi-spec`)
- [x] 3.6 `TestКаталогНеизмеренноеОкноОтдаётNull` переписывается с подстрок на
  **байты целого тела**: это ветка `*time.Time`, где ручной перевод и создаёт
  риск подставить `0001-01-01` вместо `null`
- [x] 3.7 байты пустого каталога (`{"metrics":[]}`) остаются утверждением
- [x] 3.8 форма провода утверждается **ровно в одном месте** на форму ответа:
  второго утверждения той же формы через разобранную структуру или подстроку в
  пакете нет

## 4. Документы

- [x] 4.1 `docs/architecture.md`, раздел «Read API»: подраздел «Форма провода» —
  решение, **цена обеих сторон**, ссылка на `design.md` изменения за prior art
- [x] 4.2 `docs/conventions/testing.md`: правило о механизме — байтовое
  утверждение на **каждую различимую** форму ответа; структурная проверка,
  доказывающая отсутствие, несёт заведомо красный случай
- [x] 4.3 `openspec validate --strict forma-provoda-chteniya` зелёный

## 5. Верификация

- [x] 5.1 `task gate` зелёный
- [x] 5.2 поведенческая: сервис поднят на своём порту и своей базе в `./tmp`.
  Сравниваются **два ответа одного прогона** — снятый с базовой ревизии и
  снятый с ветки против **одного и того же** файла базы при неработающем
  приёме. Числа ответа (`points`, `hours`, границы) печатаются, но в
  утверждении не участвуют: все они производны от корпуса. `ETag` сравним
  только внутри одного часа — горизонт измерения входит в метку и едет вместе
  с часами

## Критерии приёмки задачи

Из `docs/tasks/items/read-api-wire-format.md`, дословно:

- [x] решение записано в `docs/architecture.md` с названной ценой обеих сторон,
  а не только выбранной — оракул: глазами по разделу
- [x] переименование поля доменного типа либо не меняет байты ответа, либо
  меняет их намеренно, и тест утверждает это прямо, а не проверяет непустоту —
  оракул задачи назван как «тест на переименование»; **исполнимая его форма** —
  обход графа типов (3.1) плюс заведомо красный случай (3.3) плюс байтовые
  литералы (3.5–3.7). Прямого «переименуй и посмотри» в Go-тесте не бывает:
  отказ компиляции собственного пакета тест не наблюдает, а при неудавшейся
  компиляции байтов не существует вовсе. Подмена оракула названа здесь, а не
  сделана молча
- [x] маршрут каталога приведён к решению, и следующий маршрут копирует
  образец, а не выбирает заново — оракул: гейт зелёный плюс сверка
  `httpapi/catalog.go` с записанным решением

## Приёмочные критерии от ревью предложения (профиль `design`)

Рубрика прохода `review-rubric`, порождённая **до** чтения кода. Порядок —
по важности.

- [x] Р1 байты ответа не изменились, и это **измерено**: прежний байтовый
  литерал не правится ни одним символом; поведенческая сверка сравнивает два
  ответа **одного прогона**, а не ответ с записанным ранее эталоном
- [x] Р2 **оба сторожа доказали, что умеют краснеть**: обход графа типов красен
  на заведомо доменном значении, байтовое утверждение красно при переименовании
  тега в типе провода
- [x] Р3 обход полон по позициям (поле, срез, массив, ключ и значение карты,
  указатель, встроенное поле, неэкспортированное поле, анонимная вложенная
  структура), не зацикливается на рекурсивном типе, а «внутренний пакет
  проекта» определяется по `PkgPath` с префиксом module path, а не по имени
  пакета; алиас доменного типа обход **не** пропускает
- [x] Р4 пути в обход сторожа названы поимённо (`any`/интерфейс, собственный
  `MarshalJSON`, тип внешней зависимости), а исключение для дословного
  содержимого сужено **до типа** `json.RawMessage`
- [x] Р5 вырожденные значения закреплены байтами: `{"metrics":[]}`, пустые
  коллекции как `[]`, неизмеренное окно как `null`, `"style":"unknown"`
- [x] Р6 ответ детерминирован: форма провода не содержит `map`, два вызова на
  одном входе дают те же байты
- [x] Р7 перевод — **чистая функция от снимка**: без `context`, без хранилища,
  без часов; тело и валидатор `ETag` выводятся из одного снимка
- [x] Р8 ни одно утверждение не пришпилено к числу, производному от размера
  корпуса, и к ходу часов; метки времени фикстур — литералы
- [x] Р9 форма провода утверждается **ровно в одном месте**, и это место
  названо комментарием
- [x] Р10 отказные ответы принадлежат той же объявленной форме провода
- [x] Р11 образец масштабируется: помощник применяется таблицей «маршрут →
  образец ответа», следующий маршрут добавляет строку
- [x] Р12 транспорт не выносит значения точек, имена метрик и токен в лог выше
  `DEBUG` и не вкладывает доменное значение в текст ошибки

**Посылка поведенческой сверки, названная рядом с ней (Р1, Р8):** оба ответа
снимаются в одном прогоне против одного и того же файла базы при неработающем
приёме; `ETag` сравним только внутри одного часа — горизонт измерения входит в
метку и едет вместе с часами.
