Files
av a834d10415 httpapi: форма провода читающих маршрутов объявлена транспортом
- доменные типы internal/catalog лишились json-тегов и MarshalJSON; типы
  metricWire/layerWire/aggregationWire и перевод catalogWire живут в транспорте,
  тело отказа тоже получило объявленный тип — байты ответа не изменились
- заведён сторож: обход графа типов ответа утверждает, что домен не доезжает до
  сериализации, плюс требование json-тега на полях транспортных структур и
  заведомо красные случаи к обоим правилам
- решение с ценой обеих сторон записано в architecture.md и ADR; шаг lint в
  гейте получил свой кеш — общий на машину красил прогон находками из чужого
  worktree
2026-08-04 16:18:05 +03:00

127 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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` сравним только внутри одного часа — горизонт измерения входит в
метку и едет вместе с часами.