Go обновлён до 1.26, а расхождение версий теперь роняет гейт

- шаг 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 и
  перестал собираться, а восемь шагов гейта и шесть проходов ревью были зелёными
This commit is contained in:
av
2026-08-12 10:57:50 +03:00
parent aa20b229f9
commit 09228f23d8
20 changed files with 1963 additions and 11 deletions
@@ -0,0 +1,62 @@
# ADR-2026-08-12. Спекой нормируется и инструмент сборки, а не только поведение сервиса
- **Дата:** 2026-08-12
- **Источник:** [openspec/changes/archive/2026-08-12-go-1-26-upgrade/design.md](../../openspec/changes/archive/2026-08-12-go-1-26-upgrade/design.md), раздел `Decisions`, Решение 2
## Решение
Заведена capability `toolchain` — четвёртая, и первая, которая описывает **не
поведение сервиса** для его потребителей, а поведение инструмента, которым сервис
собирают. Потребитель у неё другой: тот, кто собирает.
Требование о согласованности объявленной версии Go живёт нормой в
[openspec/specs/toolchain/spec.md](../../openspec/specs/toolchain/spec.md), а не
прозой в памятке.
## Почему
Три существующие capability — `intake`, `pipeline`, `storage` — все про то, что
сервис делает для своих потребителей, а преамбула `architecture.md` прямо
говорила «поведение системы здесь не описывается — нормативно оно живёт в
`openspec/specs/`». Согласованность версий сборки под это определение не
подходит, и натяжение признано прямо в источнике:
> Признаём натяжение: три существующие capability описывают поведение сервиса для
> его потребителей, а `toolchain` описывает поведение инструмента разработки.
> Потребитель у него другой — тот, кто собирает сервис. Правило `config.yaml`
> говорит «поведение **или домен** системы»; инструмент сборки — домен, и именно
> как домен он здесь и назван.
Очевидные пути отвергнуты оба:
> **Отвергнуто: дописать в `pipeline`.** `pipeline` нормирует прогон воркера и
> захват задачи — поведение работающего сервиса. Версия сборщика с ним не
> меняется вместе.
>
> **Отвергнуто: обойтись без дельта-спеки.** Изменение вводит проверяемое
> требование — «расхождение роняет набор проверок», — и требование без дома
> проверяется только памятью того, кто его завёл. Обещание «образ собирается» уже
> один раз жило в трёх документах и во всех трёх было неверным.
Последнее и есть довод, перевесивший чистоту определения: дефект 2026-08-12
случился именно потому, что утверждение о версии сборки жило только прозой, в
трёх местах сразу, и никто не отвечал за его истинность.
## Последствия
- `+` у правила о версиях есть нормативный дом со сценариями, и по нему видно, что
проверено, а что оставлено человеку. Три требования, двадцать два сценария.
- `+` следующая задача про инструмент сборки знает, куда дописывать, и не заводит
вторую спеку о том же.
- `` определение capability в проекте стало шире, чем «поведение сервиса», и
граница теперь проходит по слову «домен». Следующее пограничное решение будет
ссылаться на этот прецедент — в том числе тогда, когда ссылаться не стоило бы.
- `` асимметрия: четыре однородных шага гейта живут в двух разных домах. У трёх
плагинных (`docs.py`, `tasks.py`, `openspec.py`) нормативного дома нет вовсе,
только строка в памятке; у четвёртого есть спека. Либо дома появятся у
остальных, либо асимметрия останется навсегда.
- `` имя `toolchain` выбрано в том числе из-за настройки среды разработчика:
первая редакция звалась `build`, и глобальный запрет чтения каталогов с таким
именем сделал спеку нечитаемой для проходов ревью. Имя, выбранное под
ограничение инструмента, а не под предмет, — слабое основание, и при следующем
пересмотре его стоит перепроверить.
@@ -0,0 +1,60 @@
# ADR-2026-08-12. Объявленную версию Go шаг гейта читает из репозитория, а не спрашивает у инструмента
- **Дата:** 2026-08-12
- **Источник:** [openspec/changes/archive/2026-08-12-go-1-26-upgrade/design.md](../../openspec/changes/archive/2026-08-12-go-1-26-upgrade/design.md), раздел `Decisions`, Решения 1 и 6
## Решение
Шаг сверки версий добывает числа чтением файлов и **не зовёт `go` ни в каком
виде** — ни `go mod edit -json`, ни `go list -m`, ни `go env`. Директива
`toolchain` в `go.mod` при этом запрещена: её наличие роняет шаг.
Дословно из источника:
> Способ это не самый удобный: разбор директивы через `go mod edit -json` короче
> и надёжнее регулярного выражения. Он же и опасный: вызов `go` тянет за собой
> `GOTOOLCHAIN`, `$PATH` и установленный тулчейн, а при непустом `GOTOOLCHAIN`
> `go` вправе полезть в сеть за нужной версией — то есть требование «без сети»
> перестало бы выполняться. Хуже того, исход шага стал бы зависеть от машины, а
> не от коммита.
## Почему
Причина не в аккуратности, а в том, что **ровно этой подменой и держался дефект,
ради которого шаг заведён**. 2026-08-12 требование модуля уехало на 1.25,
сборочный образ остался на 1.24, образ перестал собираться — и восемь шагов гейта
с шестью проходами ревью показали зелёное, потому что `go build ./...` шёл на
хостовом Go. Проверка судила по тому, что стоит на машине, вместо того что
записано в коммите. Шаг, зовущий `go`, воспроизвёл бы ту же подмену внутри себя:
зелёный там, где стоит нужная версия, и другой ответ на другой машине.
Отсюда же запрет `toolchain`. Директива — штатный механизм Go и очевидное
решение задачи расхождения: она заставила бы Go скачать нужную версию самому, и
сверять стало бы нечего. Отвергнута намеренно:
> Директива `toolchain` заставила бы Go скачивать нужный тулчейн сам, и
> расхождение с образом перестало бы ломать сборку. Но она же превращает сборку
> образа в сетевую операцию, а сборочный слой качает тулчейн при каждой сборке.
> Дороже и менее предсказуемо, чем строка сравнения.
Вдобавок она вводит **пятое место**, называющее версию, — то, которого закрытый
перечень из четырёх мест не знает: при `toolchain go1.27.0` четыре объявленных
числа сойдутся, а собирать будет пятое.
## Последствия
- `+` исход шага есть функция коммита. Проверено прогоном: с `PATH`, где нет
`go`, шаг даёт тот же код выхода и тот же вывод.
- `+` требование «без сети» выполняется по построению, а не обещанием: под
`strace` шаг не делает ни одного сетевого вызова.
- `+` пятое место закрыто: `toolchain` в `go.mod` роняет шаг с названной
причиной.
- `` разбор держится на регулярных выражениях `sed`/`awk` вместо готового
разбора, который дал бы сам `go`. Это дороже в сопровождении и хрупче: правка
образца ломает смежный случай беззвучно.
- `` запрет `toolchain` придётся снять или пересмотреть, если зависимость
однажды потребует версию выше той, что стоит у нас. Тогда эта запись
пересматривается, а не обходится.
- `` проверять сам скрипт нечем: `shellcheck` в гейт не заведён, тестов у него
нет. Из девятнадцати сценариев нормы машина гоняет один — тот, где всё
сошлось. Остаток объявлен и уехал отдельной задачей.
+2
View File
@@ -32,6 +32,8 @@
| Дата | Запись | Статус |
| --- | --- | --- |
| 2026-08-12 | [Спекой нормируется и инструмент сборки, а не только поведение сервиса](ADR-2026-08-12-spec-norms-build-toolchain.md) | |
| 2026-08-12 | [Объявленную версию Go шаг гейта читает из репозитория, а не спрашивает у инструмента](ADR-2026-08-12-version-read-from-repo-not-from-tool.md) | |
| 2026-08-12 | [Ссылка на файл открыта знанием записи, а защищает её отсутствие имени в журнале](ADR-2026-08-12-file-link-open-but-not-logged.md) | |
| 2026-08-12 | [Каталог данных задаётся одним ключом `[storage] data_dir`](ADR-2026-08-12-single-data-dir-config-key.md) | |
| 2026-08-11 | [Границу распознавания доменного признака держит норма, а не код](ADR-2026-08-11-domain-marker-boundary-by-norm.md) | |