- шаг go-version в task gate сверяет объявленную версию в go.mod, Dockerfile, CLAUDE.md и README.md; судит по репозиторию, go не зовёт, docker и сети не требует - заведена capability toolchain: до сих пор спеки нормировали только поведение сервиса, теперь и инструмент сборки. Причина и цена — в двух ADR - закрыт дефект 2026-08-12: образ на golang:1.24-alpine разошёлся с go.mod и перестал собираться, а восемь шагов гейта и шесть проходов ревью были зелёными
15 KiB
ADDED Requirements
Requirement: Версия инструмента сборки объявлена одним числом
Проект SHALL объявлять версию Go, на которой собирается сервис, одинаково во
всех местах, где она названа. Мест ровно четыре, и перечень закрыт: требование
модуля в go.mod, сборочный образ в Dockerfile, строка стека в CLAUDE.md,
строка стека в README.md.
Сравниваются мажор и минор. Третье число у сборочного образа MUST оставаться
свободным, как и база образа: образ обновляется своим темпом, и требовать от
него совпадения по патчу значило бы краснеть на каждом его обновлении. Тег
читается по форме golang:<мажор>.<минор>[.<патч>][-<база>], и берутся из него
первые два числа.
Правило множественности у мест разное, потому что места устроены по-разному.
Документы — CLAUDE.md и README.md — MUST называть версию ровно один раз,
и считается это не по файлу, а по разделу стека: ## Стек в памятке,
## Технологии в README. Второе вхождение числа в этом разделе MUST
считаться отказом: обновят одно, второе протухнет молча. За пределами раздела
число не читается вовсе — иначе памятка, которая по устройству ведёт историю
закрытых долгов, роняла бы проверку на первой же правдивой строке о прошлой
версии, а сообщение толкало бы чинить не проверку, а исторический документ.
Сборочный образ единственности не требует: каждый слой — настоящий вход
сборки, и многослойная сборка законна. От всех вхождений FROM golang: MUST
требоваться совпадение мажора и минора, а не единственность.
Требование модуля называется директивой go и по устройству файла
единственно.
Граница раздела MUST быть определена, а не подразумеваться: раздел кончается следующим заголовком того же или более высокого уровня, заголовок третьего уровня и ниже остаётся внутри раздела, а строка, похожая на заголовок, но лежащая внутри блока кода, заголовком MUST не считаться. Без этого пример в чужом разделе открывал бы раздел стека на пустом месте, и число доставалось бы оттуда, откуда норма его читать не велит.
go.mod MUST не содержать директиву toolchain. Она называет версию пятым
местом, которого перечень не знает: при toolchain go1.27.0 четыре объявленных
числа сойдутся, а собирать будет пятое — то есть вернётся тот самый класс
расхождения, ради которого требование и заведено.
Scenario: Все четыре места названы одинаково
- GIVEN дерево проекта, где
go.mod,Dockerfile,CLAUDE.mdиREADME.mdназывают версию Go - WHEN их читают подряд
- THEN мажор и минор совпадают во всех четырёх
Scenario: Патч сборочного образа отличается законно
- GIVEN
go.modтребует1.26.0, а образ собирается наgolang:1.26.5-alpine - WHEN версии сравнивают
- THEN расхождением это не считается
Scenario: База сборочного образа сменилась
- GIVEN образ переехал с
golang:1.26-alpineнаgolang:1.26-bookworm - WHEN версии сравнивают
- THEN расхождением это не считается
Scenario: Раздел стека называет версию дважды
- GIVEN раздел стека в
CLAUDE.mdназывает версию два раза - WHEN версии сравнивают
- THEN это расхождение, даже если оба числа одинаковы
Scenario: Число за пределами раздела стека не читается
- GIVEN
CLAUDE.mdвне раздела стека упоминает прошлую версию Go — например записью о закрытом долге - WHEN версии сравнивают
- THEN расхождением это не считается
Scenario: Сборочный образ собран в два слоя
- GIVEN
Dockerfileсодержит дваFROM golang:с одним мажором и минором - WHEN версии сравнивают
- THEN расхождением это не считается
Scenario: Слои сборочного образа разошлись между собой
- GIVEN
Dockerfileсодержит дваFROM golang:с разными минорами - WHEN версии сравнивают
- THEN это расхождение
Scenario: Заголовок раздела встретился внутри блока кода
- GIVEN документ в чужом разделе показывает пример, внутри которого есть строка, совпадающая с заголовком раздела стека, а ниже названо другое число
- WHEN версии сравнивают
- THEN число из примера не читается, и расхождением это не считается
Scenario: Раздел стека закрыт заголовком верхнего уровня
- GIVEN после раздела стека идёт заголовок первого уровня, а ниже названа прошлая версия
- WHEN версии сравнивают
- THEN это число не читается, и расхождением не считается
Scenario: Раздела стека нет вовсе
- GIVEN в документе нет раздела, где называется версия
- WHEN запускают шаг сверки
- THEN он завершается отказом и называет недостающий раздел
Scenario: Модуль объявляет версию пятым местом
- GIVEN
go.modсодержит директивуtoolchain - WHEN версии сравнивают
- THEN это расхождение
Requirement: Объявленное число — то, на котором проект собирается
Объявленная версия SHALL быть той, на которой сервис действительно собирается и проходит тесты. Согласованность четырёх строк между собой этого не доказывает: четыре одинаковых числа несуществующей версии требованию о согласованности удовлетворяют, а собрать на них нельзя.
Проверка эта MUST оставаться за человеком и MUST не входить в набор проверок: она требует сборки образа, а сборка образа набором проверок не делается намеренно — дорого. Подъём версии MUST не уезжать в основную ветку, пока сборка образа и тесты на объявленном числе не прогнаны.
Scenario: Версию подняли
- GIVEN объявленную версию Go подняли во всех четырёх местах
- WHEN изменение готовят к мерджу
- THEN до мерджа на этой версии прогнаны сборка образа и тесты
Requirement: Расхождение версий роняет набор проверок
Набор проверок task gate SHALL включать шаг, который сравнивает объявленные
версии между собой и MUST завершаться отказом, когда они разошлись. Сообщение
отказа MUST называть все четыре места и прочитанное в каждом число — не одну
разошедшуюся пару: в дефекте 2026-08-12 три места из четырёх говорили одно и то
же и неверными были именно они, а по сообщению о паре человек чинит не то место.
Шаг MUST судить по содержимому файлов репозитория и MUST не спрашивать
установленный инструмент — ни go version, ни go env, ни GOTOOLCHAIN. Исход
его MUST быть функцией коммита, а не машины: шаг, чей ответ зависит от того, что
стоит на хосте, воспроизводит ровно ту подмену, которая держала дефект
2026-08-12 невидимым — там go build ./... шёл на хостовом Go, а объявленное
число не проверял никто.
Шаг MUST работать сравнением строк — без сборки образа, без docker и без сети — и MUST не зависеть от рабочего каталога, из которого запущен. Шаг MUST только читать: файлов он не правит и разошедшихся мест не чинит.
Коды выхода MUST следовать словарю прочих проверочных шагов проекта: 0 сошлось,
1 расхождение, 2 ошибка употребления, 3 окружение. Своего словаря шаг MUST не
заводить: раздел «Гейт» в CLAUDE.md объявляет словарь общим, и четвёртый шаг с
собственной семантикой сделал бы это утверждение неверным.
Место, где числа не нашлось вовсе, MUST считаться отказом с именем этого места. «Нечего сравнивать» исходом MUST не быть: пропавшая строка иначе выглядела бы как совпадение.
Отказ чтения места MUST не выглядеть как отсутствие числа. Место, которое существует, но не читается, — это отказ окружения, и сообщение MUST говорить о нечитаемости, а не о ненайденной версии: иначе шаг отправляет чинить документ, в котором строка на месте, а сломаны права.
Scenario: Разошёлся сборочный образ
- GIVEN
Dockerfileназывает версию, отличную от прочих трёх мест - WHEN запускают
task gate - THEN шаг сверки завершается отказом
- AND сообщение называет все четыре места и число каждого
- AND весь набор проверок краснеет
Scenario: Разошлось требование модуля
- GIVEN
go.modназывает версию, отличную от прочих трёх мест - WHEN запускают шаг сверки
- THEN он завершается отказом и называет
go.modсреди разошедшихся
Scenario: Разошлась памятка
- GIVEN
CLAUDE.mdназывает версию, отличную от прочих трёх мест - WHEN запускают шаг сверки
- THEN он завершается отказом и называет
CLAUDE.mdсреди разошедшихся
Scenario: Разошёлся README
- GIVEN
README.mdназывает версию, отличную от прочих трёх мест - WHEN запускают шаг сверки
- THEN он завершается отказом и называет
README.mdсреди разошедшихся
Scenario: Версии совпадают
- GIVEN все четыре места называют одно число
- WHEN запускают
task gate - THEN шаг сверки проходит с кодом 0
- AND остальные шаги набора идут как прежде
Scenario: Инструмента сборки нет на машине
- GIVEN в
PATHнетgoвовсе - WHEN запускают шаг сверки
- THEN исход и сообщение те же, что и при установленном
go
Scenario: Ни docker, ни сети нет
- GIVEN docker недоступен и сети нет
- WHEN запускают шаг сверки
- THEN он отрабатывает и даёт тот же исход, что и при доступном docker
Scenario: Шаг запущен не из корня проекта
- GIVEN шаг запускают из подкаталога дерева
- WHEN он ищет свои четыре места
- THEN исход тот же, что и при запуске из корня
Scenario: Место существует, но не читается
- GIVEN файл одного из мест на диске есть, но прав на чтение нет
- WHEN запускают шаг сверки
- THEN он завершается кодом окружения и говорит о нечитаемости места
- AND сообщения «версия не названа» не печатает
Scenario: Версия не названа там, где должна быть
- GIVEN одно из четырёх мест перестало называть версию Go
- WHEN запускают шаг сверки
- THEN он завершается отказом и называет место, где число не нашлось