- периметр: passport.md и security.md больше не утверждают, что HTTP API открыт без аутентификации, а review.md не числит эту находку типовой ложноположительной — приём, опрос и файл закрыты сессией с 2026-08-12; - logging.md писал, что расширение попадает в журнал полем пути: описано изъятие инварианта приватности — собственное поле, имени и пути нет; - узел ревью переименован в repo/pocketbase, поведение конвейера из обзора уехало ссылкой в спеку pipeline, Purpose спеки storage написан вместо заглушки, в ADR о переезде дописано уточнение о действующей раскладке.
242 lines
16 KiB
Markdown
242 lines
16 KiB
Markdown
# 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** он завершается отказом и называет место, где число не нашлось
|