- периметр: passport.md и security.md больше не утверждают, что HTTP API открыт без аутентификации, а review.md не числит эту находку типовой ложноположительной — приём, опрос и файл закрыты сессией с 2026-08-12; - logging.md писал, что расширение попадает в журнал полем пути: описано изъятие инварианта приватности — собственное поле, имени и пути нет; - узел ревью переименован в repo/pocketbase, поведение конвейера из обзора уехало ссылкой в спеку pipeline, Purpose спеки storage написан вместо заглушки, в ADR о переезде дописано уточнение о действующей раскладке.
16 KiB
toolchain Specification
Purpose
Каким инструментом и какой его версии собирается сервис, и что об этом
проверяется до выкладки. Заведена задачей go-1-26-upgrade 2026-08-12 по
дефекту, записанному в docs/review.md за то же число: сборочный образ разошёлся
с требованием модуля, образ перестал собираться, а восемь шагов гейта и шесть
проходов ревью показали зелёное.
Capability нормирует не поведение сервиса для его потребителей, а поведение
инструмента разработки; потребитель у неё другой — тот, кто собирает сервис. Это
осознанное исключение, и оно названо в преамбуле docs/architecture.md.
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 следовать общему словарю проверочных шагов проекта; словарь
объявляет раздел «Гейт» в CLAUDE.md, и здесь он не повторяется. Своего словаря шаг
MUST не заводить: четвёртый шаг с собственной семантикой сделал бы это
утверждение неверным.
Место, где числа не нашлось вовсе, 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 он завершается отказом и называет место, где число не нашлось