Files
healthlog/openspec/changes/archive/2026-08-04-forma-provoda-chteniya/tasks.md
T
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

11 KiB
Raw Blame History

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

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

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

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

3. Проверки

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

4. Документы

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

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

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

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

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

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

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

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

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

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