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

6.4 KiB
Raw Blame History

Why

Публичный контракт единственного читающего маршрута сегодня выведен из формы доменных типов: internal/catalog сам несёт json-теги, а internal/httpapi/catalog.go владеет только оболочкой {"metrics": …}. Переименование поля в домене, разъединение встроенного Basis или добавление внутреннего поля меняют байты ответа, не касаясь транспорта; удерживает это один литерал в тесте TestКаталогОтдаётОжидаемыеБайты, и о том, что он и есть контракт, в коде не сказано нигде.

Пока маршрут один и потребителей у него нет, цена нулевая. Со следующей задачи цели образец копируют точки, тренировки, записи и MCP — четыре маршрута и второй транспорт. Решение принимается до копирования: потом это будет не решение, а археология. Момент выбран ещё и потому, что менять контракт каталога сейчас бесплатно — снаружи его не читает никто.

What Changes

  • Форма провода объявляется транспортом, а не выводится из домена. Каждый читающий маршрут internal/httpapi объявляет собственные типы ответа с json-тегами; доменные типы internal/catalog тегов лишаются, перевод — явная функция транспорта.
  • Доменные типы каталога перестают быть публичным контрактом. Metric, LayerRange, Aggregation, Basis остаются формой ответа use-case, а не формой ответа HTTP. Style.MarshalJSON убирается: сериализацию рода кладёт транспорт. Значения словаря (cumulative/instant/unknown) при этом остаются в домене — провод зовёт Style.String(), чтобы не завести второй словарь; граница и её цена названы в design.md.
  • Тело отказа тоже получает объявленный тип. Сегодня writeError собирает его из map[string]string; у читающего маршрута тело отказа — часть того же контракта, и оно не должно оставаться единственным местом, куда не смотрит ни один сторож. Байты те же.
  • Байты ответа каталога не меняются. Правка структурная; контракт из требования «Форма ответа каталога» остаётся тем же до символа, и это утверждается тестом, а не намерением.
  • Заводится проверка, прямо утверждающая разделение: ни один тип домена не достигает сериализации ответа — обход графа типов значения ответа, с заведомо красным случаем рядом. Отсюда и следует, что переименование поля домена до провода не доезжает.
  • Решение и цена обеих сторон записываются в docs/architecture.md (раздел Read API); правило о механизме проверки — в docs/conventions/testing.md, где у правил такого рода дом.
  • Схема не трогается, миграции нет, данные только читаются.

Capabilities

New Capabilities

  • read-api: общие правила читающих маршрутов, поверх которых встают точки, тренировки, записи и MCP. Два правила: публичный контракт меняется только правкой транспорта (форма провода объявляется явно и не выводится из формы доменных типов) и пустая коллекция — список, а не отсутствие.

Modified Capabilities

Нет. Поведение каталога не меняется: байты ответа, заголовки и коды остаются прежними, требование «Форма ответа каталога» из catalog продолжает описывать их без единой правки. Это и есть проверяемое утверждение изменения.

Impact

  • internal/catalog/catalog.go — снятие json-тегов с Basis, Aggregation, LayerRange, Metric; удаление Style.MarshalJSON.
  • internal/catalog/measure_test.goTestStyleСловарь переезжает с MarshalJSON на String().
  • internal/httpapi/catalog.go — типы провода каталога и перевод из домена.
  • internal/httpapi/httpapi.go — объявленный тип тела отказа вместо карты.
  • internal/httpapi/catalog_test.go — байтовые утверждения формы (измеренное окно, неизмеренное окно, пустая витрина).
  • internal/httpapi/wire_internal_test.go — обход графа типов ответа и заведомо красный случай к нему.
  • docs/architecture.md — раздел Read API: решение с ценой обеих сторон.
  • docs/conventions/testing.md — правило о механизме: байты на каждую различимую форму ответа, заведомо красный случай для структурной проверки.
  • Схема, витрина, архив, конфиг — не трогаются.