## 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` сравним только внутри одного часа — горизонт измерения входит в метку и едет вместе с часами.