Files
transcriber/openspec/specs/toolchain/spec.md
T
av b46be019fc docs: канон приведён к сегодняшнему состоянию после сверки
- периметр: passport.md и security.md больше не утверждают, что HTTP API
  открыт без аутентификации, а review.md не числит эту находку типовой
  ложноположительной — приём, опрос и файл закрыты сессией с 2026-08-12;
- logging.md писал, что расширение попадает в журнал полем пути: описано
  изъятие инварианта приватности — собственное поле, имени и пути нет;
- узел ревью переименован в repo/pocketbase, поведение конвейера из обзора
  уехало ссылкой в спеку pipeline, Purpose спеки storage написан вместо
  заглушки, в ADR о переезде дописано уточнение о действующей раскладке.
2026-08-12 21:13:06 +03:00

16 KiB
Raw Blame History

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 он завершается отказом и называет место, где число не нашлось