- доменные типы internal/catalog лишились json-тегов и MarshalJSON; типы metricWire/layerWire/aggregationWire и перевод catalogWire живут в транспорте, тело отказа тоже получило объявленный тип — байты ответа не изменились - заведён сторож: обход графа типов ответа утверждает, что домен не доезжает до сериализации, плюс требование json-тега на полях транспортных структур и заведомо красные случаи к обоим правилам - решение с ценой обеих сторон записано в architecture.md и ADR; шаг lint в гейте получил свой кеш — общий на машину красил прогон находками из чужого worktree
11 KiB
11 KiB
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 сравним только внутри одного часа — горизонт измерения входит в
метку и едет вместе с часами.