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:
@@ -24,7 +24,7 @@ Yandex SpeechKit и возвращает текст туда, откуда пр
|
|||||||
|
|
||||||
## Стек
|
## Стек
|
||||||
|
|
||||||
Go 1.25 (CGO не нужен), встроенная PocketBase — хранилище, файлы записей и
|
Go 1.26 (CGO не нужен), встроенная PocketBase — хранилище, файлы записей и
|
||||||
панель администратора, — `go-telegram-bot-api`, `aws-sdk-go-v2` для Object
|
панель администратора, — `go-telegram-bot-api`, `aws-sdk-go-v2` для Object
|
||||||
Storage, gRPC-клиент Yandex SpeechKit v3, Prometheus, `slog`. Сборка —
|
Storage, gRPC-клиент Yandex SpeechKit v3, Prometheus, `slog`. Сборка —
|
||||||
Taskfile, образ — Docker, выкладка — Ansible из `pet-project-server`.
|
Taskfile, образ — Docker, выкладка — Ansible из `pet-project-server`.
|
||||||
@@ -102,15 +102,28 @@ task gate # весь набор проверок разом
|
|||||||
`origin/master`; переопределяется `task gate BASE=<rev>`.
|
`origin/master`; переопределяется `task gate BASE=<rev>`.
|
||||||
- **Где логи шагов:** вывод команды, отдельного файла нет.
|
- **Где логи шагов:** вывод команды, отдельного файла нет.
|
||||||
- **Что означает каждый исход:** ненулевой код любого шага роняет гейт. У
|
- **Что означает каждый исход:** ненулевой код любого шага роняет гейт. У
|
||||||
`docs.py check`, `tasks.py check` и `openspec.py check` словарь кодов общий:
|
`docs.py check`, `tasks.py check`, `openspec.py check` и
|
||||||
0 сошлось, 1 дрейф, 2 ошибка употребления, 3 окружение (не корень проекта,
|
`scripts/check-go-version.sh` словарь кодов общий: 0 сошлось, 1 дрейф,
|
||||||
каталог не найден), 4 внутренний сбой.
|
2 ошибка употребления, 3 окружение (не корень проекта, каталог или файл не
|
||||||
|
найден), 4 внутренний сбой. Последний своего словаря не заводит намеренно:
|
||||||
|
четвёртый шаг с собственной семантикой сделал бы это утверждение неверным.
|
||||||
- **Что красит безусловно и почему:** отказ сборки, тестов, `go vet`,
|
- **Что красит безусловно и почему:** отказ сборки, тестов, `go vet`,
|
||||||
неотформатированный файл, находка `golangci-lint`, дрейф раскладки документов,
|
неотформатированный файл, находка `golangci-lint`, расхождение объявленных
|
||||||
|
версий Go, дрейф раскладки документов,
|
||||||
дрейф каталога задач, форма `openspec/config.yaml`. Машина проверяет всё
|
дрейф каталога задач, форма `openspec/config.yaml`. Машина проверяет всё
|
||||||
перечисленное, и это не обсуждается. Шаг, чей скрипт не найден, краснеет с
|
перечисленное, и это не обсуждается. Шаг, чей скрипт не найден, краснеет с
|
||||||
именем недостающего плагина, а не пропускается молча.
|
именем недостающего плагина, а не пропускается молча.
|
||||||
- **Чего в гейте намеренно нет и кто тогда обязан это гонять:**
|
- **Чего в гейте намеренно нет и кто тогда обязан это гонять:**
|
||||||
|
- **сборка образа** — дорога, и отказ от неё сознательный. Дешёвая замена
|
||||||
|
стоит шагом сверки версий: он сравнивает строки и ловит расхождение, из-за
|
||||||
|
которого образ перестаёт собираться, но собираемости не проверяет. Собрать
|
||||||
|
образ по-прежнему может только человек — `task image`, и на подъёме версии
|
||||||
|
это обязательно;
|
||||||
|
- `shellcheck` — shell-скрипт в гейте один, `scripts/check-go-version.sh`; три
|
||||||
|
соседних шага это Python в плагинах, и линтер оболочки к ним неприменим.
|
||||||
|
Линтер не заведён, и цена ему одна строка шага плюс одно подавление ложного
|
||||||
|
`SC1007` на идиому `CDPATH= cd`. Его не проверяет ничто, а это единственный
|
||||||
|
исполняемый файл проекта, которого не видят ни `go vet`, ни `golangci-lint`;
|
||||||
- `gitleaks` — висит на pre-commit в `lefthook.yml` и смотрит только индекс
|
- `gitleaks` — висит на pre-commit в `lefthook.yml` и смотрит только индекс
|
||||||
коммита. Полную историю никто не проверяет;
|
коммита. Полную историю никто не проверяет;
|
||||||
- согласованность документов между собой и с кодом — её судят агенты, зовёт
|
- согласованность документов между собой и с кодом — её судят агенты, зовёт
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Build stage
|
# Build stage
|
||||||
FROM docker.io/library/golang:1.25-alpine AS build-env
|
FROM docker.io/library/golang:1.26-alpine AS build-env
|
||||||
|
|
||||||
# Сборочных зависимостей нет: хранилище ходит в SQLite через modernc.org/sqlite,
|
# Сборочных зависимостей нет: хранилище ходит в SQLite через modernc.org/sqlite,
|
||||||
# и CGO больше не требуется.
|
# и CGO больше не требуется.
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
## Технологии
|
## Технологии
|
||||||
|
|
||||||
|
- **Язык**: Go 1.26, CGO не нужен
|
||||||
- **Веб-фреймворк**: gin-gonic/gin
|
- **Веб-фреймворк**: gin-gonic/gin
|
||||||
- **Telegram**: go-telegram-bot-api
|
- **Telegram**: go-telegram-bot-api
|
||||||
- **Распознавание**: Yandex SpeechKit + Yandex Object Storage (S3)
|
- **Распознавание**: Yandex SpeechKit + Yandex Object Storage (S3)
|
||||||
|
|||||||
@@ -29,10 +29,25 @@ tasks:
|
|||||||
fi
|
fi
|
||||||
- go test ./...
|
- go test ./...
|
||||||
- golangci-lint run
|
- golangci-lint run
|
||||||
|
- task: go-version
|
||||||
- task: docs
|
- task: docs
|
||||||
- task: tasks
|
- task: tasks
|
||||||
- task: openspec
|
- task: openspec
|
||||||
|
|
||||||
|
go-version:
|
||||||
|
desc: 'Одна версия Go в go.mod, Dockerfile, CLAUDE.md и README.md'
|
||||||
|
cmds:
|
||||||
|
# Скрипт лежит в самом репозитории, а не в плагине: его отсутствие значит
|
||||||
|
# сломанное дерево, а не непоставленный плагин, и переопределять путь
|
||||||
|
# нечем и незачем.
|
||||||
|
- |
|
||||||
|
py=scripts/check-go-version.sh
|
||||||
|
if [ ! -f "$py" ]; then
|
||||||
|
echo "$py не найден: дерево репозитория неполно"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sh "$py"
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
desc: 'Раскладка docs/ против канона'
|
desc: 'Раскладка docs/ против канона'
|
||||||
cmds:
|
cmds:
|
||||||
|
|||||||
@@ -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` в гейт не заведён, тестов у него
|
||||||
|
нет. Из девятнадцати сценариев нормы машина гоняет один — тот, где всё
|
||||||
|
сошлось. Остаток объявлен и уехал отдельной задачей.
|
||||||
@@ -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 | [Ссылка на файл открыта знанием записи, а защищает её отсутствие имени в журнале](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-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) | |
|
| 2026-08-11 | [Границу распознавания доменного признака держит норма, а не код](ADR-2026-08-11-domain-marker-boundary-by-norm.md) | |
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
[passport.md](passport.md) и в [tasks/ROADMAP.md](../tasks/ROADMAP.md); что из
|
[passport.md](passport.md) и в [tasks/ROADMAP.md](../tasks/ROADMAP.md); что из
|
||||||
этого ещё не решено — в разделе «Открытые вопросы».
|
этого ещё не решено — в разделе «Открытые вопросы».
|
||||||
|
|
||||||
Заведены три capability:
|
Заведены четыре capability. Три первые нормируют **поведение сервиса** для его
|
||||||
|
потребителей; четвёртая — исключение из первого абзаца: она нормирует не сервис, а
|
||||||
|
инструмент, которым его собирают, и потребитель у неё другой — тот, кто собирает.
|
||||||
|
|
||||||
- [intake](../openspec/specs/intake/spec.md) — **только приём по HTTP**: его
|
- [intake](../openspec/specs/intake/spec.md) — **только приём по HTTP**: его
|
||||||
нормируют проверки, написанные задачей `http-handler-tests-never-green`
|
нормируют проверки, написанные задачей `http-handler-tests-never-green`
|
||||||
@@ -20,6 +22,10 @@
|
|||||||
долгом; что именно не описано, перечисляет раздел `Purpose` самой спеки;
|
долгом; что именно не описано, перечисляет раздел `Purpose` самой спеки;
|
||||||
- [storage](../openspec/specs/storage/spec.md) — где живут запись, её метаданные
|
- [storage](../openspec/specs/storage/spec.md) — где живут запись, её метаданные
|
||||||
и её файл, как файл отдаётся и что видит владелец: задача `pocketbase-storage`
|
и её файл, как файл отдаётся и что видит владелец: задача `pocketbase-storage`
|
||||||
|
2026-08-12;
|
||||||
|
- [toolchain](../openspec/specs/toolchain/spec.md) — каким инструментом и какой
|
||||||
|
его версии собирается сервис: одно число версии Go во всех местах, где она
|
||||||
|
названа, и шаг гейта, который это сверяет. Задача `go-1-26-upgrade`
|
||||||
2026-08-12.
|
2026-08-12.
|
||||||
|
|
||||||
Поведение прочих узлов, включая приём из Telegram, по-прежнему живёт только в
|
Поведение прочих узлов, включая приём из Telegram, по-прежнему живёт только в
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ htmx, а здесь решено делать SPA — и перенесённы
|
|||||||
| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` |
|
| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` |
|
||||||
| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` |
|
| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` |
|
||||||
| Раскладка документов, битые ссылки, миграция без правки `database.md` | `docs.py check` |
|
| Раскладка документов, битые ссылки, миграция без правки `database.md` | `docs.py check` |
|
||||||
|
| Одно число версии Go в `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` | `Taskfile.yml` → шаг `go-version` (`scripts/check-go-version.sh`) |
|
||||||
|
|
||||||
Не названное здесь место механизации означает, что проход по конвенциям будет
|
Не названное здесь место механизации означает, что проход по конвенциям будет
|
||||||
добросовестно проверять уже проверенное.
|
добросовестно проверять уже проверенное.
|
||||||
|
|||||||
@@ -219,6 +219,12 @@ API и имя не откатываются обратной правкой по
|
|||||||
директивой `go` в `go.mod`. Строкой сравнения, без docker. Заведено урожаем
|
директивой `go` в `go.mod`. Строкой сравнения, без docker. Заведено урожаем
|
||||||
ревью.
|
ревью.
|
||||||
|
|
||||||
|
**Закрыто** задачей `go-1-26-upgrade` 2026-08-12: шаг `go-version` в `task gate`
|
||||||
|
(`scripts/check-go-version.sh`). Сверяются четыре места, а не два, — `go.mod`,
|
||||||
|
`Dockerfile`, `CLAUDE.md`, `README.md`: в этом дефекте трое из четырёх врали
|
||||||
|
согласованно, и парная сверка не увидела бы документ, разошедшийся с
|
||||||
|
согласованным кодом. Норма — capability `toolchain`.
|
||||||
|
|
||||||
## 2026-08-11 — норма требовала от сервиса недостижимого [пойман ревью]
|
## 2026-08-11 — норма требовала от сервиса недостижимого [пойман ревью]
|
||||||
|
|
||||||
- **Где:** дельта-спека `pipeline` задачи `errors-as-instead-of-typecast`, абзац
|
- **Где:** дельта-спека `pipeline` задачи `errors-as-instead-of-typecast`, абзац
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module git.vakhrushev.me/av/transcriber
|
module git.vakhrushev.me/av/transcriber
|
||||||
|
|
||||||
go 1.25.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.5.0
|
github.com/BurntSushi/toml v1.5.0
|
||||||
@@ -9,6 +9,7 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.3
|
github.com/aws/aws-sdk-go-v2/credentials v1.18.3
|
||||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.3
|
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.18.3
|
||||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.86.0
|
github.com/aws/aws-sdk-go-v2/service/s3 v1.86.0
|
||||||
|
github.com/aws/smithy-go v1.27.7
|
||||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
@@ -35,7 +36,6 @@ require (
|
|||||||
github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sso v1.27.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 // indirect
|
||||||
github.com/aws/smithy-go v1.27.7 // indirect
|
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0 h1:ywQF2N4VjqX+Psw+jLjMmUL2
|
|||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0/go.mod h1:Z+qv5Q6b7sWiclvbJyPSOT1BRVU9wfSUPaqQzZ1Xg3E=
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.32.0/go.mod h1:Z+qv5Q6b7sWiclvbJyPSOT1BRVU9wfSUPaqQzZ1Xg3E=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 h1:bRP/a9llXSSgDPk7Rqn5GD/DQCGo6uk95plBFKoXt2M=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0 h1:bRP/a9llXSSgDPk7Rqn5GD/DQCGo6uk95plBFKoXt2M=
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0/go.mod h1:tgBsFzxwl65BWkuJ/x2EUs59bD4SfYKgikvFDJi1S58=
|
github.com/aws/aws-sdk-go-v2/service/sts v1.36.0/go.mod h1:tgBsFzxwl65BWkuJ/x2EUs59bD4SfYKgikvFDJi1S58=
|
||||||
github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw=
|
|
||||||
github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
|
||||||
github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE=
|
github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE=
|
||||||
github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-12
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Версия Go названа в проекте четырежды, и сегодня четыре места расходятся:
|
||||||
|
`go.mod` требует `go 1.25.0`, `Dockerfile` собирает на `golang:1.25-alpine`,
|
||||||
|
`CLAUDE.md` обещает «Go 1.25», а `README.md` не называет версию вовсе. На машине
|
||||||
|
разработки стоит `go1.26.5`.
|
||||||
|
|
||||||
|
Число 1.25 никем не назначалось: `go mod tidy` поднял требование модуля,
|
||||||
|
следуя за PocketBase, а образ подтянули следом. Ровно этот же механизм 2026-08-12
|
||||||
|
породил дефект — требование модуля уехало на 1.25, `Dockerfile` остался на
|
||||||
|
`golang:1.24-alpine` с `GOTOOLCHAIN=local`, и образ перестал собираться. Восемь
|
||||||
|
шагов набора проверок и шесть проходов ревью показали зелёное: `go build ./...`
|
||||||
|
идёт на хостовом Go, а образ не собирает ни один шаг. Случай записан в
|
||||||
|
`docs/review.md` за 2026-08-12 и там же назван способ починки — сравнение строк
|
||||||
|
вместо сборки образа.
|
||||||
|
|
||||||
|
Ограничения, в которых работаем: набор проверок обязан оставаться дешёвым и
|
||||||
|
работать без сети и без docker; выкладку это изменение не запускает; сборку
|
||||||
|
образа в набор проверок не заводим — отказ записан.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- одно число версии Go во всех четырёх местах;
|
||||||
|
- шаг набора проверок, который краснеет на расхождении и называет оба числа;
|
||||||
|
- шаг стоит доли секунды и не зависит ни от docker, ни от сети.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- сборка образа шагом набора проверок — дорого, отказ записан в
|
||||||
|
`docs/review.md`;
|
||||||
|
- проверка того, что объявленная версия вообще существует в реестре образов, —
|
||||||
|
это требует сети;
|
||||||
|
- сверка версий прочих инструментов (`golangci-lint`, `task`, `ffmpeg`) — их
|
||||||
|
расхождение так не ломает, и заводить перечень впрок незачем;
|
||||||
|
- `shellcheck` шагом набора проверок. Замер: shell-скриптов в гейте один, три
|
||||||
|
соседних шага — Python в плагинах, линтер к ним неприменим; цена шага — одна
|
||||||
|
строка плюс подавление ложного `SC1007` на идиому `CDPATH= cd`. Отказ всё равно
|
||||||
|
осознанный, но цену называем настоящую, а не «их четыре»;
|
||||||
|
- автоматическая правка разошедшихся мест — шаг набора проверок судит, а не
|
||||||
|
чинит.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Решение 1: версия 1.26, а число выбирает человек
|
||||||
|
|
||||||
|
Берём 1.26 — она стоит на машине разработки (`go1.26.5`), образ
|
||||||
|
`golang:1.26-alpine` в реестре есть, последний релиз тоже `go1.26.5`, а
|
||||||
|
`CGO_ENABLED=0 go build ./...` на ней уже проходит. Требование PocketBase v0.39.10
|
||||||
|
(`go 1.25.0`) она выполняет.
|
||||||
|
|
||||||
|
В `go.mod` пишем `go 1.26.0`, а не `1.26.5`: требование модуля — это нижняя
|
||||||
|
граница, и привязывать её к патчу значит без нужды отсекать сборку на более
|
||||||
|
раннем патче той же минорной версии.
|
||||||
|
|
||||||
|
**Число называет человек, и нормой это не записано намеренно.** `go mod tidy`
|
||||||
|
поднимает требование модуля сам, следуя за зависимостью, и подъём, никем не
|
||||||
|
назначенный, дал сегодняшнее расхождение. Но «выбрал человек» ненаблюдаемо:
|
||||||
|
директива, поднятая инструментом, и директива, назначенная решением, выглядят
|
||||||
|
одинаково, а норма, которую нечем уронить, расходится с кодом молча. Поэтому
|
||||||
|
здесь мотив, а в спеке — то, что проверяется: сборка и тесты на объявленном
|
||||||
|
числе до мерджа.
|
||||||
|
|
||||||
|
**Отвергнуто: остаться на 1.25 и завести только сверку.** Сверка — половина
|
||||||
|
задачи, и она бы прижилась; но тогда сегодняшнее число остаётся тем, которое
|
||||||
|
никто не назначал, и первый же `go mod tidy` следующей зависимости повторит
|
||||||
|
подъём вслепую. Задача собрана из двух половин именно поэтому.
|
||||||
|
|
||||||
|
**Отвергнуто: `toolchain` в `go.mod` вместо подъёма `go`.** Директива
|
||||||
|
`toolchain` заставила бы Go скачивать нужный тулчейн сам, и расхождение с
|
||||||
|
образом перестало бы ломать сборку. Но она же превращает сборку образа в
|
||||||
|
сетевую операцию, а сборочный слой качает тулчейн при каждой сборке. Дороже и
|
||||||
|
менее предсказуемо, чем строка сравнения.
|
||||||
|
|
||||||
|
### Решение 2: новая capability `toolchain`
|
||||||
|
|
||||||
|
Дельта-спека ложится в новую capability `toolchain` — «каким инструментом и какой
|
||||||
|
его версии собирается сервис, и что об этом проверяется до выкладки».
|
||||||
|
|
||||||
|
**Отвергнуто: дописать в `pipeline`.** `pipeline` нормирует прогон воркера и
|
||||||
|
захват задачи — поведение работающего сервиса. Версия сборщика с ним не меняется
|
||||||
|
вместе, а правило гранулярности в `openspec/config.yaml` именно про это: «дробить,
|
||||||
|
когда в одной спеке смешиваются разные заботы».
|
||||||
|
|
||||||
|
**Отвергнуто: обойтись без дельта-спеки.** Изменение вводит проверяемое
|
||||||
|
требование — «расхождение роняет набор проверок», — и требование без дома
|
||||||
|
проверяется только памятью того, кто его завёл. Обещание «образ собирается» уже
|
||||||
|
один раз жило в трёх документах и во всех трёх было неверным.
|
||||||
|
|
||||||
|
**Отвергнуто имя `build`.** Первая редакция называла capability `build`, и на
|
||||||
|
разметке выяснилось, что читать её нельзя: настройка среды разработчика
|
||||||
|
запрещает чтение любого каталога с этим именем. Спека, недоступная проходам
|
||||||
|
ревью, не проверяется ни одним из них, а после архивации осталась бы слепым
|
||||||
|
пятном насовсем. Имя `toolchain` точнее и по существу: предмет здесь —
|
||||||
|
инструмент сборки и его версия, а не сборка как процесс.
|
||||||
|
|
||||||
|
Признаём натяжение: три существующие capability описывают поведение сервиса для
|
||||||
|
его потребителей, а `toolchain` описывает поведение инструмента разработки.
|
||||||
|
Потребитель у него другой — тот, кто собирает сервис. Правило `config.yaml`
|
||||||
|
говорит «поведение **или домен** системы»; инструмент сборки — домен, и именно
|
||||||
|
как домен он здесь и назван. Если capability так и останется с одним
|
||||||
|
требованием, дешевле будет переименовать её, чем расщепить
|
||||||
|
(`RENAMED Requirements`).
|
||||||
|
|
||||||
|
### Решение 3: шаг сверяет все четыре места, а не два
|
||||||
|
|
||||||
|
Минимум по критерию приёмки — `go.mod` против `Dockerfile`. Берём шире: плюс
|
||||||
|
`CLAUDE.md` и `README.md`.
|
||||||
|
|
||||||
|
Причина прямо из дефекта 2026-08-12: **три места из четырёх говорили одно и то
|
||||||
|
же, и неверными были именно они.** Пару `go.mod`↔`Dockerfile` парная сверка
|
||||||
|
тогда поймала бы — та пара как раз разошлась. Чего она не ловит, так это
|
||||||
|
документа, разошедшегося с **согласованным** кодом: сойдись тогда `go.mod` с
|
||||||
|
образом на 1.24, и памятка с README продолжали бы врать молча, а гейт оставался
|
||||||
|
бы зелёным. Сегодня проект ровно в этом состоянии наполовину: `README.md` не
|
||||||
|
называет версию вовсе, а `CLAUDE.md` проверяется только тем, что кто-то её
|
||||||
|
прочтёт.
|
||||||
|
|
||||||
|
Цена: строку о версии придётся держать в форме, которую находит машина. Это же и
|
||||||
|
польза — документ, чья строка перестала находиться, краснеет вместо того, чтобы
|
||||||
|
молча протухнуть.
|
||||||
|
|
||||||
|
**Отвергнуто: сверять только `go.mod` и `Dockerfile`.** Дешевле на три строки
|
||||||
|
скрипта и не ловит половину прошлого дефекта.
|
||||||
|
|
||||||
|
**Отвергнуто: сверять ещё и `config.dist.toml`, `docker/entrypoint.sh` и
|
||||||
|
Ansible-роль в `pet-project-server`.** Версии Go там нет; чужой репозиторий этому
|
||||||
|
набору проверок недоступен.
|
||||||
|
|
||||||
|
### Решение 4: отдельный скрипт в репозитории, а не строка в `Taskfile.yml`
|
||||||
|
|
||||||
|
Шаг живёт файлом `scripts/check-go-version.sh`, а `Taskfile.yml` его зовёт.
|
||||||
|
|
||||||
|
Сверка четырёх мест — это четыре разных способа достать число (директива
|
||||||
|
модуля, тег образа, проза памятки, проза README), сравнение и внятное сообщение
|
||||||
|
со всеми четырьмя. В `Taskfile.yml` это легло бы двадцатью строками shell внутри
|
||||||
|
YAML, где их не читает ни редактор, ни `shellcheck`, а кавычки экранируются
|
||||||
|
дважды. Соседние шаги (`docs`, `tasks`, `openspec`) уже зовут скрипты, и эта
|
||||||
|
форма для набора проверок родная.
|
||||||
|
|
||||||
|
Скрипт лежит в репозитории, а не в плагине: он про этот проект, а не про метод
|
||||||
|
работы.
|
||||||
|
|
||||||
|
Оговорка о выигрыше: `shellcheck` шагом набора проверок этим изменением **не**
|
||||||
|
заводится, и обоснование выше стоит на том, что файл хотя бы **можно** проверить
|
||||||
|
и прочитать глазами, а не на том, что его кто-то проверяет машиной. Заводить
|
||||||
|
линтер оболочки — отдельная работа, и она уезжает урожаем.
|
||||||
|
|
||||||
|
**Отвергнуто: строка shell прямо в `Taskfile.yml`.** Дешевле на один файл,
|
||||||
|
дороже при первом же изменении: правка регулярного выражения в YAML-скаляре
|
||||||
|
ошибается молча.
|
||||||
|
|
||||||
|
**Отвергнуто: написать проверку на Go отдельной командой.** Тогда она попадает в
|
||||||
|
`go build ./...` и `go vet ./...`, а вместе с ней — разбор `Dockerfile` в коде
|
||||||
|
сервиса. Проверка о проекте не должна ехать в бинарник сервиса.
|
||||||
|
|
||||||
|
### Решение 5: форма строки, которую ищет машина
|
||||||
|
|
||||||
|
| Место | Что ищем | Правило множественности |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `go.mod` | единственная строка, начинающаяся с `go ` — первые два числа | директива `toolchain` запрещена: она пятое место |
|
||||||
|
| `Dockerfile` | тег `golang:<мажор>.<минор>[.<патч>][-<база>]`, берём первые два числа | все вхождения `FROM golang:` обязаны давать одно число |
|
||||||
|
| `CLAUDE.md` | образец `Go <мажор>.<минор>` в разделе `## Стек` | ровно одно вхождение в разделе; вне раздела число не читается |
|
||||||
|
| `README.md` | образец `Go <мажор>.<минор>` в разделе `## Технологии` | ровно одно вхождение в разделе; вне раздела число не читается |
|
||||||
|
|
||||||
|
Патч и база образа из тега отбрасываются: спека объявила их свободными, и
|
||||||
|
образец обязан это допускать — иначе `golang:1.26.5-alpine` уронил бы набор
|
||||||
|
проверок на дереве, которое та же спека называет верным.
|
||||||
|
|
||||||
|
Правило множественности заведено не впрок: реализация, молча берущая первое
|
||||||
|
совпадение, судила бы по обновлённой строке и не видела протухшей соседней.
|
||||||
|
|
||||||
|
**Рамка у него — раздел, а не файл, и это правка по находке ревью.** Первая
|
||||||
|
редакция считала вхождения по всему файлу, и на памятке это давало
|
||||||
|
гарантированный ложный красный: она по устройству ведёт историю закрытых долгов
|
||||||
|
(«Два прежних долга закрыты и здесь названы»), и первая же правдивая строка о
|
||||||
|
прошлой версии уронила бы шаг — сообщением, которое толкает чинить не шаг, а
|
||||||
|
исторический документ. Тем же ловился бы любой пример команды в README. Решение
|
||||||
|
человека на чекпоинте: считать по разделу стека, за его пределами число не
|
||||||
|
читать.
|
||||||
|
|
||||||
|
Граница раздела при этом определена явно — следующий заголовок того же или более
|
||||||
|
высокого уровня, и заголовок внутри блока кода за заголовок не считается.
|
||||||
|
Неопределённая граница давала бы **ложное зелёное**: пример в чужом разделе
|
||||||
|
открывал бы раздел стека на пустом месте, и число доставалось бы оттуда, откуда
|
||||||
|
его читать не велено.
|
||||||
|
|
||||||
|
Число не нашлось — это отказ с названным местом, а не «нечего сравнивать»:
|
||||||
|
пропавшая строка иначе выглядела бы как совпадение.
|
||||||
|
|
||||||
|
### Решение 6: шаг судит по репозиторию, а не по машине
|
||||||
|
|
||||||
|
Число берётся чтением файлов. `go` шаг не зовёт вовсе — ни `go mod edit -json`,
|
||||||
|
ни `go list -m`, ни `go env`.
|
||||||
|
|
||||||
|
Способ это не самый удобный: разбор директивы через `go mod edit -json` короче и
|
||||||
|
надёжнее регулярного выражения. Он же и опасный: вызов `go` тянет за собой
|
||||||
|
`GOTOOLCHAIN`, `$PATH` и установленный тулчейн, а при непустом `GOTOOLCHAIN` `go`
|
||||||
|
вправе полезть в сеть за нужной версией — то есть требование «без сети»
|
||||||
|
перестало бы выполняться. Хуже того, исход шага стал бы зависеть от машины, а не
|
||||||
|
от коммита, — ровно та подмена, которая держала дефект 2026-08-12 невидимым:
|
||||||
|
`go build ./...` шёл на хостовом Go и потому был зелёным, пока образ не
|
||||||
|
собирался.
|
||||||
|
|
||||||
|
Отсюда же и словарь кодов выхода: 0 сошлось, 1 расхождение, 2 ошибка
|
||||||
|
употребления, 3 окружение. Свой словарь заводить нельзя — раздел «Гейт» в
|
||||||
|
`CLAUDE.md` объявляет его общим для проверочных шагов, и четвёртый шаг с
|
||||||
|
собственной семантикой сделал бы это утверждение неверным.
|
||||||
|
|
||||||
|
Оболочка — POSIX `sh`, без GNU-only флагов (`grep -P`, `sed -E` с
|
||||||
|
расширениями, `mapfile`). Пути шаг строит от корня репозитория, а не от текущего
|
||||||
|
каталога: иначе его исход зависел бы от того, откуда он запущен.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **Скрипт ищет число прозой документа, и переписанная строка сломает шаг** →
|
||||||
|
сообщение отказа называет место, где число не нашлось, поэтому чинится
|
||||||
|
однозначно и сразу. Ложное зелёное здесь невозможно по построению: не нашлось
|
||||||
|
— отказ.
|
||||||
|
- **Четыре места вместо двух — четыре места, которые надо править при подъёме
|
||||||
|
версии** → это цена решения 3, и она осознанная: молчаливо врущий документ
|
||||||
|
дороже одной лишней правки.
|
||||||
|
- **1.26 может оказаться несовместимой с зависимостью, которую мы ещё не
|
||||||
|
трогали** → проверяется до мерджа: `task image` собирает образ на объявленной
|
||||||
|
версии, а `go test ./...` идёт на хостовом go1.26.5. Обе проверки в критериях
|
||||||
|
приёмки.
|
||||||
|
- **Шаг проверяет согласованность чисел, но не то, что образ собирается** →
|
||||||
|
осознанный остаток. Собранный образ по-прежнему видит только тот, кто позвал
|
||||||
|
`task image` руками; сверка ловит класс расхождений, а не все отказы сборки.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Сервис собирается тремя разными числами версии Go сразу: модуль требует одно,
|
||||||
|
сборочный образ берёт другое, памятка обещает третье, а README не называет
|
||||||
|
никакого. Расхождение этих чисел никто не проверяет, и один раз оно уже уехало в
|
||||||
|
работу: 2026-08-12 образ не собирался вовсе, а полный набор проверок и шесть
|
||||||
|
проходов ревью показали зелёное. Поймали случайно, руками. Пока сравнения нет,
|
||||||
|
то же самое повторится на следующем подъёме версии — а замечено будет в момент
|
||||||
|
выкладки, когда чинить дороже всего.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Версия Go, на которой собирается сервис, поднимается до 1.26 и называется
|
||||||
|
**одним и тем же числом** в четырёх местах: требование модуля, сборочный
|
||||||
|
образ, памятка разработчику и README. Сегодня README не называет его вовсе —
|
||||||
|
строка заводится.
|
||||||
|
- В набор проверок добавляется шаг, который сравнивает объявленные числа между
|
||||||
|
собой и краснеет, называя все четыре места и число каждого. Шаг сравнивает
|
||||||
|
строки: он не собирает образ, не ходит в сеть, не требует docker и не
|
||||||
|
спрашивает, что за Go установлен на машине, — судит он по тому, что лежит в
|
||||||
|
репозитории.
|
||||||
|
- Собирать образ проверкой мы по-прежнему **не** будем — это дорого, и отказ
|
||||||
|
осознанный. Сравнение строк ловит тот же класс расхождений за доли секунды.
|
||||||
|
- Разница в третьем числе версии между модулем и образом остаётся законной:
|
||||||
|
сравниваются только первые два.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `toolchain`: каким инструментом и какой его версии собирается сервис, и что об
|
||||||
|
этом проверяется до выкладки. Первое требование capability — согласованность
|
||||||
|
объявленной версии инструмента сборки и её проверка набором проверок.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
Нет. Поведение сервиса для его потребителей не меняется: запись принимается,
|
||||||
|
расшифровывается и хранится ровно как прежде.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- требование версии в `go.mod`;
|
||||||
|
- сборочный слой `Dockerfile`;
|
||||||
|
- строка о версии в `CLAUDE.md` и в `README.md`;
|
||||||
|
- набор шагов `task gate` в `Taskfile.yml` и описание семантики набора проверок
|
||||||
|
в `CLAUDE.md`;
|
||||||
|
- запись журнала дефектов `docs/review.md` за 2026-08-12 получает починку,
|
||||||
|
которую обещала.
|
||||||
|
|
||||||
|
Выкладка этим изменением не запускается. Внешних зависимостей изменение не
|
||||||
|
трогает: PocketBase требует не ниже 1.25, и 1.26 это требование выполняет.
|
||||||
@@ -0,0 +1,620 @@
|
|||||||
|
# Ревью кода: go-1-26-upgrade — триаж
|
||||||
|
|
||||||
|
База диффа: `HEAD` (коммит `aa20b22`), изменение целиком в рабочем дереве. Дата
|
||||||
|
прогона: 2026-08-12.
|
||||||
|
|
||||||
|
## Сводка
|
||||||
|
|
||||||
|
**Размер, сложность, метка.** Размер — среднее: `tasks.md` 20 шагов в 4 разделах,
|
||||||
|
8 файлов; `internal/` не тронут ни строкой. Сложность — знакомое: все узлы
|
||||||
|
названы поимённо до работы, шагов формы «разобраться/выяснить» нет. **Метка
|
||||||
|
`medium`** (максимум по осям), **режим — по графу**. Триггеры `docs/review.md`
|
||||||
|
проверены все три группы, ни один пункт не совпал.
|
||||||
|
|
||||||
|
**Состояние гейта: ЗЕЛЁНЫЙ.** Подтверждено собственным прогоном триажа, а не
|
||||||
|
только отчётом прохода: `task gate BASE=HEAD` → exit 0. Все девять шагов зелёные,
|
||||||
|
включая новый `go-version`. Одно унаследованное замечание `tasks.py`
|
||||||
|
(`any-audio-source`: цель без задач и без тега `decomposed`) шаг не роняет и к
|
||||||
|
диффу отношения не имеет.
|
||||||
|
|
||||||
|
### План разметки задачи с исходом по каждой теме
|
||||||
|
|
||||||
|
| тема | дом | глубина | кто закрывает | исход |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| requirements | `openspec/specs/` + дельта `specs/toolchain/spec.md` | разбор | specs | **закрыта**, 4 находки (S1–S4) + 2 блока наблюдений |
|
||||||
|
| autotests | `CLAUDE.md`, раздел «Гейт» | — | autotests | **закрыта**, 3 находки (A1–A3) + отчёт гейта |
|
||||||
|
| conventions | `docs/conventions/` | разбор | code | **закрыта**, 3 находки (C1–C3) + 3 наблюдения ниже порога |
|
||||||
|
| architecture | `docs/architecture.md` + источник `docs/passport.md` | разбор | basics | **закрыта**, 1 находка (B1) + 4 пункта «дешевле переделать» |
|
||||||
|
| security | `docs/security.md` | разбор | basics | **закрыта**, находок нет: тема неприменима к диффу целиком |
|
||||||
|
| operations | `docs/architecture.md` §Эксплуатация + источник `docs/database.md` | разбор | basics | **закрыта**, 1 находка (B3); 4 вопроса из 6 неприменимы |
|
||||||
|
|
||||||
|
**Тем без отчёта нет.** Все шесть тем ядра вернули отчёты; своих тем сверх ядра у
|
||||||
|
проекта нет. Ни одна тема не осталась непроверенной по причине «проход не
|
||||||
|
запускался».
|
||||||
|
|
||||||
|
**Находок на входе:** 13 нумерованных (A1–A3, S1–S4, C1–C3, B1–B3) плюс 15
|
||||||
|
ненумерованных содержательных пунктов (3 «поведение вне спеки», 5 «границы
|
||||||
|
спеки», 3 наблюдения ниже порога, 4 «дешевле переделать до мерджа») = **28
|
||||||
|
позиций**. **Осталось в основных секциях: 6** — 2 блокирующие и 4 «стоит
|
||||||
|
исправить сейчас». Слито по причине 4 пары, понижено до гипотез 3, уехало в
|
||||||
|
promote 5, выброшено 6 (все названы поимённо).
|
||||||
|
|
||||||
|
### Сигнал о заниженной метке
|
||||||
|
|
||||||
|
**Сигнал подан двумя проходами независимо — `code` и `basics`.** Оба назвали одно
|
||||||
|
и то же: изменение вводит новое понятие (capability `toolchain` — первая, что
|
||||||
|
описывает не поведение сервиса, а инструмент разработки), новый каталог верхнего
|
||||||
|
уровня `scripts/` и новый шаг гейта; `design.md` сам записывает нерешённое
|
||||||
|
натяжение в размещении capability. Оба сказали, что метка `large` дала бы
|
||||||
|
отдельный проход `review-architecture`, и оба отказались решать за конвейер.
|
||||||
|
|
||||||
|
**Провенанс один, приоритет два.** Согласие двух проходов — это одна модель,
|
||||||
|
высказавшаяся дважды: `confidence` оно не повышает, приоритет повышает. Одна
|
||||||
|
строка с двумя провенансами, а не два пункта.
|
||||||
|
|
||||||
|
**Суждение триажа — факт для человека, не команда конвейеру:**
|
||||||
|
|
||||||
|
1. **По записанному правилу разметка верна.** `docs/review.md`, «Триггеры метки»:
|
||||||
|
в группе «Крупное здесь» ближайший пункт — «каркас приложения: сборка
|
||||||
|
фронтенда, раздача статики и шаг гейта разом» — требует трёх вещей сразу,
|
||||||
|
здесь только шаг гейта. В «Незнакомое здесь» не совпал ни один из шести: форма
|
||||||
|
решения (сравнение строк `sed`/`awk`, без `go` и без docker) была названа до
|
||||||
|
работы. Отрицательный тест пройден: миграции, формата файла, контракта API и
|
||||||
|
имени ключа конфига изменение не трогает. `review-scope` не ошибся против
|
||||||
|
правила, которое у него было.
|
||||||
|
2. **Ось, на которую указали проходы, в правиле отсутствует.** Ни один триггер не
|
||||||
|
говорит о заведении новой capability и о новом каталоге верхнего уровня. А
|
||||||
|
именно эта ось дала **обе блокирующие находки прогона** — обе про канон, а не
|
||||||
|
про код. Сигнал верен по существу: правило разметки не видит того, что в этом
|
||||||
|
изменении оказалось самым дорогим.
|
||||||
|
3. **Перезапуска это не требует, метку задним числом не пересматривают.** Тема
|
||||||
|
`architecture` дома не лишилась и без отчёта не осталась — её закрыл `basics`
|
||||||
|
на глубине «разбор». Честный остаток: тему смотрел проход широкого профиля, а
|
||||||
|
не специализированный, и вопрос «правильно ли выбрано имя и дом capability»
|
||||||
|
остался без независимого разбора (H-1).
|
||||||
|
4. **Что с этим делать — не здесь.** Кандидат в правило вынесен в promote (P-5).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Блокирует мердж (2 из 3)
|
||||||
|
|
||||||
|
### B-1. Требование «ровно одно вхождение» станет нормой в форме, которая про `Dockerfile` уже неверна, а на `CLAUDE.md` уронит гейт на правдивой строке
|
||||||
|
|
||||||
|
- Файл: `openspec/changes/go-1-26-upgrade/specs/toolchain/spec.md:16-18` против
|
||||||
|
`scripts/check-go-version.sh:127-131` и `:147-153`; правильные слова уже лежат в
|
||||||
|
`openspec/changes/go-1-26-upgrade/design.md:158-163`
|
||||||
|
- Severity: **major** | Confidence: **high**
|
||||||
|
- Действие: **развилка**
|
||||||
|
- Найдено проходами: `specs` (S1 и пункт «Границы спеки»), `basics` (пункт «Рамка
|
||||||
|
правила „ровно одно вхождение“»). **Слито триажем по причине:** причина одна —
|
||||||
|
требование написано пофайлово единым правилом, а четыре места устроены
|
||||||
|
по-разному. Правится одним абзацем.
|
||||||
|
- **Оракул — три прогона на копии дерева** (копии в scratchpad; рабочее дерево не
|
||||||
|
тронуто, `git status --porcelain` до и после совпадает):
|
||||||
|
1. второй сборочный слой `FROM docker.io/library/golang:1.26-alpine AS second` →
|
||||||
|
**exit 0**. Норма гласит: «Каждое место MUST называть версию ровно один раз.
|
||||||
|
Второе вхождение числа в том же месте MUST считаться отказом», и перечень
|
||||||
|
мест включает `Dockerfile`. Код нормы не исполняет и исполнять не должен:
|
||||||
|
`collect Dockerfile "$(read_dockerfile)" 0` передаёт `strict=0` намеренно.
|
||||||
|
Контроль: тот же второй слой с `golang:1.25-alpine` → exit 1, «Dockerfile
|
||||||
|
называет несколько разных версий» — то есть совпадение слоёв проверяется,
|
||||||
|
единственность нет;
|
||||||
|
2. в `CLAUDE.md` дописана правдивая строка `- прежде собирались на Go 1.25; долг
|
||||||
|
закрыт задачей go-1-26-upgrade` → **exit 1**, «CLAUDE.md называет несколько
|
||||||
|
разных версий»;
|
||||||
|
3. в `README.md` дописан блок кода с `# нужен Go 1.26` → **exit 1**, «README.md
|
||||||
|
называет версию больше одного раза».
|
||||||
|
- Последствие. **Со стороны `Dockerfile`** — молчаливое расхождение нормы и кода:
|
||||||
|
после архивации нормой станет спека, а не `design.md`. Многослойная сборка с
|
||||||
|
двумя `FROM golang:` — законная форма. Ревьюер следующего изменения увидит
|
||||||
|
`strict=0`, прочтёт MUST и «починит» скрипт, уронив гейт на рабочем
|
||||||
|
`Dockerfile`; обратный исход не лучше — норма останется ложью, на которую
|
||||||
|
сошлются. **Со стороны документов** — гарантированный ложный красный:
|
||||||
|
`CLAUDE.md` по устройству ведёт историю (раздел «Гейт» прямо говорит «Два
|
||||||
|
прежних долга закрыты и здесь названы»), и первая же правдивая запись о прошлой
|
||||||
|
версии роняет шаг. По правилу проекта «Что считается сломанным — новый красный
|
||||||
|
шаг гейта… чинится прежде любой другой работы» это остановит работу, а
|
||||||
|
сообщение «называет несколько разных версий» толкает чинить не скрипт, а
|
||||||
|
исторический документ, то есть подделывать запись. `README.md` ловится тем же на
|
||||||
|
любом блоке кода с командой установки.
|
||||||
|
- Почему до мерджа: спека замерзает архивацией, после неё правка MUST — отдельное
|
||||||
|
изменение. Сегодня это один абзац.
|
||||||
|
- **Вопрос человеку:**
|
||||||
|
- **Вариант А (дешёвый, ожидаемый).** Развести правило по местам прямо в
|
||||||
|
требовании, дословно как уже написано в `design.md:158-163`: единственности
|
||||||
|
требовать от `go.mod`, `CLAUDE.md` и `README.md`, а от `Dockerfile` —
|
||||||
|
совпадения всех вхождений `FROM golang:`. Плюс сузить рамку для документов:
|
||||||
|
правило считает не файл целиком, а помеченную строку стека (или раздел
|
||||||
|
«Стек»/«Технологии»). Цена: абзац спеки + `read_doc` начинает читать раздел, а
|
||||||
|
не файл — несколько строк скрипта. Сценарий «Одно место называет два разных
|
||||||
|
числа» остаётся верным и правки не требует.
|
||||||
|
- **Вариант Б (дешевле сейчас, дороже потом).** Развести только `Dockerfile`
|
||||||
|
(правка чисто текстовая, кода не трогает), а цену «файл целиком» для
|
||||||
|
документов принять осознанно и записать остатком в спеке: «`CLAUDE.md` не
|
||||||
|
ведёт истории версий Go; запись о прошлой версии живёт в `docs/review.md`».
|
||||||
|
Тогда красный на истории — не сюрприз, а объявленный запрет.
|
||||||
|
|
||||||
|
### B-2. Канонический перечень capability назовёт три из четырёх ровно в момент архивации, и промолчит именно о новой
|
||||||
|
|
||||||
|
- Файл: `docs/architecture.md:11-26`; `openspec/changes/go-1-26-upgrade/tasks.md`,
|
||||||
|
раздел «3. Документы» (в нём `docs/architecture.md` нет)
|
||||||
|
- Severity: **minor** | Confidence: **high**
|
||||||
|
- Действие: **инлайн**
|
||||||
|
- Найдено проходами: `specs` (S3), `basics` (B1). **Дубль по причине, слит;**
|
||||||
|
предложение взято более широкое, от `basics`.
|
||||||
|
- Оракул — поимённые положения, все перепроверены триажем:
|
||||||
|
- `openspec/config.yaml:25-26` дословно: «Состояние спек и правило „первая
|
||||||
|
задача, трогающая поведение, заводит спеку своей capability“ —
|
||||||
|
docs/architecture.md, преамбула». Дом назначен, и он один;
|
||||||
|
- `docs/architecture.md:11` дословно: «Заведены три capability:»; `proposal.md`
|
||||||
|
заводит четвёртую;
|
||||||
|
- прецедент: прошлое изменение правило этот список **тем же коммитом**, что и
|
||||||
|
реализацию — `git show 01cc31d -- docs/architecture.md` даёт `-Заведены две
|
||||||
|
capability, и каждая описана частично:` / `+Заведены три capability:`;
|
||||||
|
- `tasks.md`, раздел 3, содержит ровно два пункта — `CLAUDE.md` и
|
||||||
|
`docs/review.md`; обзора архитектуры в нём нет;
|
||||||
|
- машинного оракула нет и быть не может: `CLAUDE.md`, «Гейт» — «согласованность
|
||||||
|
документов между собой и с кодом — её судят агенты, зовёт их скилл
|
||||||
|
`av-dev-docs:healthcheck`, и звать его надо руками».
|
||||||
|
- Последствие. После архивации в `openspec/specs/` появится четвёртая capability,
|
||||||
|
о которой единственный назначенный обзор молчит. Следующий, кто возьмётся за
|
||||||
|
версию инструмента сборки, пойдёт по указанному дому, четвёртой строки не найдёт
|
||||||
|
и либо заведёт вторую спеку на ту же тему, либо припишет требование в `pipeline`
|
||||||
|
— ровно то, от чего `design.md:79-82` отказался. Отказ молчаливый: гейт этого
|
||||||
|
класса не ловит по устройству.
|
||||||
|
- Предложение (инлайн, три правки):
|
||||||
|
1. четвёртая строка перечня в `docs/architecture.md` — про `toolchain`, со
|
||||||
|
ссылкой на спеку и задачу-источник;
|
||||||
|
2. оговорка к преамбуле: сегодня она читается «поведение системы здесь не
|
||||||
|
описывается — нормативно оно живёт в `openspec/specs/`», а `toolchain`
|
||||||
|
описывает **не** поведение сервиса; без оговорки преамбула становится
|
||||||
|
неверной в момент архивации;
|
||||||
|
3. пункт в `tasks.md`, раздел «3. Документы», чтобы правка не потерялась.
|
||||||
|
|
||||||
|
**Третий слот блокирующих не занят** — кандидатов нет: всё прочее либо не
|
||||||
|
замерзает мерджем, либо не имеет оракула.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Стоит исправить сейчас (4 из 4)
|
||||||
|
|
||||||
|
### N-1. Единственный новый страж проекта не покрыт ничем: следующая правка его регулярных выражений перестанет ловить случай молча
|
||||||
|
|
||||||
|
- Файл: `scripts/check-go-version.sh` (весь); `tasks.md:96-111`
|
||||||
|
- Severity: **major** | Confidence: **high**
|
||||||
|
- Действие: **развилка** (новая работа, за границей объявленного scope)
|
||||||
|
- Найдено проходами: `autotests` (A1), `specs` (S4). **Дубль по причине, слит.**
|
||||||
|
- Оракул (перепроверено триажем):
|
||||||
|
- `find . -iname '*check-go-version*'` → ровно один файл, сам скрипт; тестов нет;
|
||||||
|
- `grep -rln 'check-go-version' --include='*_test.go' --include='*.bats'
|
||||||
|
--include='*test*.sh' .` → пусто;
|
||||||
|
- `task gate` прогоняет скрипт ровно на согласованном дереве, то есть проверяет
|
||||||
|
один сценарий дельты из четырнадцати — «Версии совпадают»;
|
||||||
|
- раздел «4. Проверка» в `tasks.md` перечисляет 11 сценариев, все `[x]`, но ни
|
||||||
|
один не зафиксирован ничем, кроме прозы: это разовый ручной прогон, а не
|
||||||
|
оракул;
|
||||||
|
- положение проекта, которое здесь нарушено, записано: `docs/review.md»,
|
||||||
|
«Типовые узлы» → «Любой узел»: «изменённое место покрыто хоть одним
|
||||||
|
**проходящим** тестом».
|
||||||
|
- Последствие. POSIX-sh с разбором четырёх разных форм через `sed`/`awk` — класс
|
||||||
|
кода, где правка одного образца ломает смежный случай беззвучно. `go vet`,
|
||||||
|
`golangci-lint` и `gofmt` shell не видят; `shellcheck` в гейт сознательно не
|
||||||
|
введён. Правка третьего аргумента `collect` или образца `read_doc` снимает
|
||||||
|
проверку молча, и заметят это на следующем подъёме версии — примерно через год,
|
||||||
|
и ровно тем способом, каким был найден дефект 2026-08-12: образ перестал
|
||||||
|
собираться, и этого не увидел никто. Класс — «молчание»: страж перестаёт
|
||||||
|
стеречь, не сообщая об этом.
|
||||||
|
- **Вопрос человеку:**
|
||||||
|
- **А. Сейчас, в этом изменении.** Тест-скрипт рядом
|
||||||
|
(`scripts/check-go-version.test.sh`) и отдельный шаг гейта: десяток
|
||||||
|
мутационных прогонов на временной копии дерева — по одному на сценарий дельты.
|
||||||
|
Цена ~100 строк shell плюс шаг Taskfile. Плюс: страж проверен ровно тем
|
||||||
|
способом, каким `docs/review.md` велит проверять оракулы.
|
||||||
|
- **Б. Задачей урожая, вместе с `shellcheck` (P-1).** Один шаг гейта, гоняющий и
|
||||||
|
линтер оболочки, и мутационные прогоны. Плюс: не раздувает изменение, scope
|
||||||
|
остаётся заявленным. Минус: между мерджем и задачей страж не проверен ничем, и
|
||||||
|
правки в этот промежуток пройдут вслепую.
|
||||||
|
- **В. Принять остаток осознанно** и записать строкой в `docs/review.md`,
|
||||||
|
«Недоступно проверке» → «Перестали проверять сознательно», с ценой. Плюс:
|
||||||
|
честно и бесплатно. Минус: следующий промах этого класса будет уже вторым.
|
||||||
|
|
||||||
|
### N-2. Новое машинное правило не попало в единственный индекс механизированного, и следующий проход конвенций будет сверять версии руками
|
||||||
|
|
||||||
|
- Файл: `docs/conventions/README.md:50-65`
|
||||||
|
- Severity: **minor** | Confidence: **medium**
|
||||||
|
- Действие: **инлайн**
|
||||||
|
- Найдено проходом: `code` (C3)
|
||||||
|
- Оракул — поимённое положение конвенций: `docs/conventions/README.md:52-53`
|
||||||
|
(«Проверяется командами из CLAUDE.md; прозой не дублируется») и `:64-65` («Не
|
||||||
|
названное здесь место механизации означает, что проход по конвенциям будет
|
||||||
|
добросовестно проверять уже проверенное»). Строка про `docs.py check` в той же
|
||||||
|
таблице — прямой прецедент внесения шагов гейта.
|
||||||
|
- Последствие. Дифф заводит новое машинное правило и не вносит его в индекс,
|
||||||
|
объявленный исчерпывающим. Следующий проход по конвенциям и следующий человек
|
||||||
|
будут считать согласованность версий непроверенной и сверять её руками. Заодно
|
||||||
|
это единственное место в `docs/conventions/`, откуда новый скрипт вообще был бы
|
||||||
|
виден: сегодня из дома конвенций он не виден никак.
|
||||||
|
- Предложение: строка в таблицу — `| Одно число версии Go в go.mod, Dockerfile,
|
||||||
|
CLAUDE.md и README.md | Taskfile.yml → шаг go-version
|
||||||
|
(scripts/check-go-version.sh) |`.
|
||||||
|
|
||||||
|
### N-3. Сломанное окружение шаг объявит расхождением версий и назовёт невиновный файл
|
||||||
|
|
||||||
|
- Файл: `scripts/check-go-version.sh:105-119`, `:147-153`
|
||||||
|
- Severity: **minor** | Confidence: **high** (проход давал `medium`; поднято
|
||||||
|
прогоном)
|
||||||
|
- Действие: **инлайн**
|
||||||
|
- Найдено проходом: `code` (C1)
|
||||||
|
- **Оракул — два прогона на копии дерева:**
|
||||||
|
1. `chmod a-r README.md && sh scripts/check-go-version.sh` →
|
||||||
|
```
|
||||||
|
sed: can't read .../README.md: Permission denied
|
||||||
|
check-go-version: README.md не называет версию Go
|
||||||
|
Объявленная версия Go по местам:
|
||||||
|
go.mod 1.26
|
||||||
|
Dockerfile 1.26
|
||||||
|
CLAUDE.md 1.26
|
||||||
|
README.md версия не названа
|
||||||
|
exit=1
|
||||||
|
```
|
||||||
|
2. прогон с `PATH`, где нет `awk` → **exit 127**, код вне словаря вовсе.
|
||||||
|
|
||||||
|
Для сравнения, штатные пути проверены и корректны: нет файла места → 3, лишний
|
||||||
|
аргумент → 2, директива `toolchain` → 1, согласованное дерево → 0.
|
||||||
|
- Последствие. Значения добываются подстановкой команд в **аргументе** (`collect
|
||||||
|
README.md "$(read_doc "$readmemd")" 1`), а POSIX теряет код возврата подстановки,
|
||||||
|
стоящей в аргументе простой команды. Любой отказ чтения — файл есть, но
|
||||||
|
нечитаем; урезанный `PATH`; сломанный апплет busybox — даёт пустой вход,
|
||||||
|
`collect` видит `total -eq 0` и печатает утверждение **о содержимом файла** там,
|
||||||
|
где сломалось окружение. По словарю, который этот же дифф и расширил, 1 значит
|
||||||
|
«дрейф», 3 — «окружение», 4 — «внутренний сбой»; кода 4 скрипт не возвращает ни
|
||||||
|
на одном пути.
|
||||||
|
- **Честная оценка веса.** Ущерб невелик: `sed` печатает свою причину строкой
|
||||||
|
выше, так что человек у терминала подсказку видит, а `task gate` различает
|
||||||
|
только ноль и не-ноль. Вероятность низкая — CI у проекта нет, гейт гоняет
|
||||||
|
владелец на своей машине. Находка остаётся потому, что шаг гейта — источник, на
|
||||||
|
который смотрят как на истину, и ложное утверждение о конкретном файле из такого
|
||||||
|
источника дороже своей вероятности.
|
||||||
|
- Предложение: добывать значения через промежуточную переменную с проверкой кода,
|
||||||
|
а не в аргументе — четыре места, по одному на источник.
|
||||||
|
|
||||||
|
### N-4. Цена отказа от `shellcheck` названа в памятке вчетверо, и вопрос от этого отложится снова
|
||||||
|
|
||||||
|
- Файл: `CLAUDE.md:122-123`; то же в `design.md:37-38`
|
||||||
|
- Severity: **minor** | Confidence: **high**
|
||||||
|
- Действие: **инлайн**
|
||||||
|
- Найдено проходом: `code` (C2); сюда же ушла находка `autotests` A3 как замер
|
||||||
|
цены.
|
||||||
|
- Оракул — замер, снятый триажем на этом прогоне:
|
||||||
|
- `find . -path ./.git -prune -o -name '*.sh' -print` → ровно два файла:
|
||||||
|
`./scripts/check-go-version.sh` и `./docker/entrypoint.sh`; в гейте из них
|
||||||
|
один. Остальные три шага гейта — `docs.py`, `tasks.py`, `openspec.py` — Python
|
||||||
|
и лежат в плагинах вне репозитория;
|
||||||
|
- `shellcheck scripts/check-go-version.sh` → **одно** замечание, SC1007 на
|
||||||
|
`root=$(CDPATH= cd -- ... )`, и оно ложное: `CDPATH= ` — идиома очистки
|
||||||
|
переменной перед `cd`. Значит заведение линтера стоит одной строки шага плюс
|
||||||
|
одной директивы подавления.
|
||||||
|
- Последствие. Строка «`shellcheck` для скриптов гейта — их четыре, и заводить им
|
||||||
|
линтер надо разом» смешивает «скриптов в гейте четыре» с «`shellcheck` применим
|
||||||
|
к четырём». Применим он к одному. Следующий прочтёт в памятке цену «надо разом,
|
||||||
|
четыре штуки» и отложит вопрос снова — при том что нелинтуемым остаётся ровно
|
||||||
|
тот файл, который проход `code` был вынужден разбирать глазами построчно, а
|
||||||
|
триаж — прогонять руками.
|
||||||
|
- Предложение: переписать пункт по факту — «shell-скрипт в гейте один; три
|
||||||
|
соседних шага — Python в плагинах. Линтер не заведён; цена ему одна строка шага
|
||||||
|
и одно подавление SC1007». Ту же правку в `design.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Гипотезы без доказательства
|
||||||
|
|
||||||
|
### H-1. Имя и дом capability `toolchain` замерзают мерджем (понижено: оракула нет)
|
||||||
|
|
||||||
|
Из прохода `basics`, «Дешевле переделать до мерджа». После архивации спека уезжает
|
||||||
|
в `openspec/specs/` насовсем; `design.md` сам признаёт, что при одном требовании
|
||||||
|
переименовать дешевле, чем расщепить. Вопрос дома шире: четыре однородных шага
|
||||||
|
набора проверок живут в двух разных домах — у трёх плагинных дома нет вовсе, у
|
||||||
|
четвёртого есть нормативная спека, и эта асимметрия становится постоянной.
|
||||||
|
|
||||||
|
**Почему понижено.** Оракула нет и построить его нечем: это суждение о
|
||||||
|
правильности имени, а не о поведении. Плюс два смягчающих факта: стадия ревью
|
||||||
|
дизайна (`specs` + rubric) прошла до кода и её замечания отработаны — вопрос уже
|
||||||
|
был на столе; `CLAUDE.md`, «Необратимое», имени capability не перечисляет.
|
||||||
|
Severity снята.
|
||||||
|
|
||||||
|
**Остаток честный:** независимого архитектурного разбора у этого вопроса не было —
|
||||||
|
на метке `medium` отдельный проход не запускается, тему закрывал `basics` широким
|
||||||
|
профилем. Это и есть содержание сигнала о заниженной метке.
|
||||||
|
|
||||||
|
### H-2. Пересборка того же коммита через месяц даст другой `ffmpeg` (понижено: замера нет, строка не из этого диффа)
|
||||||
|
|
||||||
|
Из прохода `basics` (B3). `Dockerfile:26` — рантайм-слой `alpine:latest`, а
|
||||||
|
`Taskfile.yml:93` собирает с `--pull`: два образа из одного коммита с разницей в
|
||||||
|
неделю несут разные `ffmpeg`. Регрессия конвертации после такой пересборки
|
||||||
|
выглядит как задачи в `failed` при пустом диффе репозитория, и откат на прежний
|
||||||
|
коммит её не чинит. Класс тот же, ради которого написан весь новый шаг:
|
||||||
|
объявленное и собранное расходятся, и сверять некому.
|
||||||
|
|
||||||
|
**Почему понижено.** Замера нет — ни одного числа о том, как часто и насколько
|
||||||
|
меняется `ffmpeg` в `alpine:latest`; снять на этом прогоне нечем. Строка внесена
|
||||||
|
не этим изменением, только активирована им. `Confidence: medium`.
|
||||||
|
|
||||||
|
### H-3. «Два дома у семантики шага» — проверено и не подтвердилось
|
||||||
|
|
||||||
|
Из прохода `basics`. Утверждение: семантика шага описана и в `CLAUDE.md` «Гейт», и
|
||||||
|
в спеке `toolchain`, а `openspec/config.yaml` предупреждает, что «второй дом факта
|
||||||
|
расходится с первым молча».
|
||||||
|
|
||||||
|
**Проверено триажем:** спека `spec.md:93-96` не пересказывает словарь, а
|
||||||
|
**ссылается** на него — «раздел „Гейт“ в `CLAUDE.md` объявляет словарь общим». Это
|
||||||
|
уже правильная форма: один дом факта, вторая точка — ссылка. Последствия не
|
||||||
|
построено, находкой не выводится.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Promote candidates
|
||||||
|
|
||||||
|
- **P-1. `shellcheck` шагом набора проверок.** Цена замерена на этом прогоне:
|
||||||
|
файлов `.sh` два, в гейте один, единственное сегодняшнее замечание — ложный
|
||||||
|
SC1007 на идиому `CDPATH= cd`. Шаг стоит одной строки плюс одной директивы
|
||||||
|
подавления.
|
||||||
|
- **P-2. Род узла «скрипт набора проверок» в `docs/review.md`, «Типовые узлы».**
|
||||||
|
Сегодня перечень родов покрывает только рантайм. Скрипт гейта — новый род с
|
||||||
|
собственными проверяемыми свойствами: отличает «расхождение» от «сломанного
|
||||||
|
окружения», исход есть функция коммита, покрыт мутационным прогоном. Без этого
|
||||||
|
рода свойство «изменённое место покрыто хоть одним проходящим тестом» к shell не
|
||||||
|
приложено ничем, и находка N-1 в следующий раз опять будет добываться с нуля.
|
||||||
|
- **P-3. Обёртки `Taskfile` отдают 1, когда скрипта нет, а словарь велит 3.** Из
|
||||||
|
S2/B2, слитых по причине. **Но так делают все четыре обёртки** — `docs`,
|
||||||
|
`tasks`, `openspec` и новая `go-version`: новый шаг лишь повторил существующий
|
||||||
|
рисунок, и дефектом **этого** диффа это не является. Сам скрипт при отсутствующем
|
||||||
|
месте выходит корректно — 3 (проверено). Правило, а не правка: привести все
|
||||||
|
четыре обёртки к 3 разом либо убрать «или файл не найден» из описания кода 3 и
|
||||||
|
объявить `exit 1` нормой для «скрипта шага нет».
|
||||||
|
- **P-4. Норму «версия внешнего инструмента объявлена числом и сверяется»
|
||||||
|
распространить на рантайм-базу образа.** Из H-2. Non-Goal этого изменения
|
||||||
|
записан; кандидат в отдельную задачу.
|
||||||
|
- **P-5. Триггер метки: изменение, заводящее новую capability или новый каталог
|
||||||
|
верхнего уровня.** Из сигнала о заниженной метке. Сегодня «Триггеры метки» видят
|
||||||
|
только объём и незнакомость формы решения; ось «изменение трогает канон» в них
|
||||||
|
отсутствует, а на этом прогоне именно она дала обе блокирующие находки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Границы покрытия
|
||||||
|
|
||||||
|
Секция не сокращается. Без неё формулировка «критичных проблем не обнаружено»
|
||||||
|
запрещена — и здесь она не употребляется.
|
||||||
|
|
||||||
|
### План: темы, дома, глубины
|
||||||
|
|
||||||
|
Все шесть тем ядра размечены, у всех есть дом, все закрыты — таблица в сводке.
|
||||||
|
**Тем без дома нет. Тем без отчёта нет. Своих тем сверх ядра проект не
|
||||||
|
объявляет.** Глубина «разбор» у пяти тем, у `autotests` глубина не назначалась.
|
||||||
|
|
||||||
|
### Какие проходы запускались
|
||||||
|
|
||||||
|
На метке `medium`, в режиме «по графу», запускались четыре: `autotests`, `specs`,
|
||||||
|
`code`, `basics`. Стадия ревью дизайна (`specs` + `rubric`) прошла раньше, до
|
||||||
|
кода, и её замечания отработаны — на этом прогоне она не повторялась.
|
||||||
|
|
||||||
|
### Какие проходы не запускались и почему
|
||||||
|
|
||||||
|
- **`review-architecture`** — не запускается на метке `medium` по устройству
|
||||||
|
графа; тема `architecture` отдана `basics`. Именно об этом сигнал двух проходов.
|
||||||
|
- **Проход независимой реализации** — снят из конвейера по стоимости.
|
||||||
|
- **Проход про идиоматичность языка** — упразднён.
|
||||||
|
|
||||||
|
### Что каждый запущенный проход не мог проверить в принципе
|
||||||
|
|
||||||
|
Оговорка о происхождении: **сырые выводы, поданные триажу, несут границы прозой, а
|
||||||
|
не блоком `Coverage of this pass` из контракта.** Перечень ниже восстановлен по
|
||||||
|
тому, что проходы написали, а не по их charter'ам, — и может быть неполон. Это
|
||||||
|
отдельная строка деградации.
|
||||||
|
|
||||||
|
- `autotests` — не судит содержание кода; видит зелёное/красное и наличие тестов.
|
||||||
|
Прогон гейта не собирает образ (намеренно) и не считает покрытие изменённых строк.
|
||||||
|
- `specs` — судит соответствие кода дельта-спеке и обратно; не судит качество кода
|
||||||
|
вне нормы и не проверяет, нужна ли норма вообще.
|
||||||
|
- `code` — темы `conventions` плюс технический разбор; Go-кода дифф не содержит,
|
||||||
|
поэтому `logging.md`, `errors.md`, `config.md` неприменимы поимённо, и разбор
|
||||||
|
свёлся к shell, который ни один линтер проекта не видит.
|
||||||
|
- `basics` — темы `security`, `operations`, `architecture` широким профилем; на
|
||||||
|
этой метке заменяет специализированные проходы, а не дополняет их.
|
||||||
|
- **Триаж не находит ничего нового по определению**: работает с чужими выводами и
|
||||||
|
своими прогонами-оракулами. Пропуск любого прохода — его пропуск тоже.
|
||||||
|
|
||||||
|
### Неприменимые темы и вопросы — названы, а не пропущены
|
||||||
|
|
||||||
|
- **`security`: тема неприменима к диффу целиком.** `internal/` не тронут ни
|
||||||
|
строкой. Три вопроса темы из `docs/review.md` адресованы
|
||||||
|
`internal/service/transcribe.go` и `internal/metrics` — они не менялись, вопросы
|
||||||
|
остаются открытыми и после этого прогона. Периметр сборки дом объявляет вне
|
||||||
|
модели: `docs/security.md:233-235`.
|
||||||
|
- **`operations`: применимы 2 вопроса из 6.** Отказ соседа, повтор и
|
||||||
|
одновременность, остановка на середине, наблюдаемость, рост объёма —
|
||||||
|
неприменимы: рантайм не меняется. Три вопроса темы из `docs/review.md` к диффу
|
||||||
|
неприменимы и остаются открытыми.
|
||||||
|
- **`autotests`: вопрос темы из `docs/review.md:133-134`** адресован
|
||||||
|
job-конвейеру в `internal/`, которого дифф не трогает.
|
||||||
|
- **`conventions`: вопрос темы** («новая колонка правится во всех четырёх местах»)
|
||||||
|
— **колонок изменение не трогает вовсе**; `internal/adapter/repo/pocketbase` не
|
||||||
|
изменён ни строкой.
|
||||||
|
|
||||||
|
### Что осталось целиком на человеке
|
||||||
|
|
||||||
|
**Не проверит ни один проход:**
|
||||||
|
- `operations`: поведение внешних сервисов под нагрузкой и на границах;
|
||||||
|
- `operations`: реальный профиль нагрузки;
|
||||||
|
- `security`: стойкость `ffmpeg` к вредоносному входу.
|
||||||
|
|
||||||
|
**Перестали проверять сознательно:**
|
||||||
|
- `autotests`: разбор вывода настоящего `ffprobe` — решение и цена в
|
||||||
|
`docs/adr/ADR-2026-08-11-stub-adapters-in-tests.md`.
|
||||||
|
|
||||||
|
**Своё, для этого изменения:** **собираемость образа на объявленной версии** —
|
||||||
|
требование «Объявленное число — то, на котором проект собирается» прямо оставляет
|
||||||
|
проверку человеку и запрещает вводить её в набор проверок. Косвенные свидетельства
|
||||||
|
положительные (`go version` → `go1.26.5`; гейт зелёный; образ `transcriber:dev` в
|
||||||
|
наличии), но **самой сборки триаж не запускал** — она объявлена сделанной пунктом
|
||||||
|
`tasks.md` 4.2.
|
||||||
|
|
||||||
|
### Каких документов проекта не хватило
|
||||||
|
|
||||||
|
- **`docs/review.md`, «Типовые ложноположительные» — есть и непуст (4 пункта), но
|
||||||
|
все четыре про рантайм `internal/`.** Дифф его не трогает, поэтому **проектных
|
||||||
|
ложноположительных для него не существует вовсе**: отсев вкусовщины шёл по общим
|
||||||
|
критериям устава, без проектного входа.
|
||||||
|
- **`CLAUDE.md`, «Инварианты» — раздел есть, 9 пунктов, и ни один не применим к
|
||||||
|
диффу.** Следствие названо прямо: **ни одна находка этого прогона не поднята до
|
||||||
|
`critical` по основанию «нарушен инвариант проекта» — сослаться не на что.**
|
||||||
|
Ранжирование велось по обратимости, выведенной из механики openspec, и это
|
||||||
|
**предположение триажа**, а не записанное правило проекта.
|
||||||
|
- **`CLAUDE.md`, «Необратимое» — имени capability, состава `openspec/specs/` и
|
||||||
|
раскладки `scripts/` в нём нет.** Поэтому «спека замерзает мерджем» — вывод из
|
||||||
|
механики openspec, а не проектная норма.
|
||||||
|
- **`CLAUDE.md`, «Ориентир по размеру порции: не замерялся» — дословно.** Значит
|
||||||
|
суждение «объём right-size» опирается на оценку триажа, а не на проектное число.
|
||||||
|
|
||||||
|
### Сработавшие потолки
|
||||||
|
|
||||||
|
- `basics` — **сообщил сам**: 3 находки при потолке 4, за срезом ничего.
|
||||||
|
- `code` — **сообщил частично**: «Потолок конвенций 1/4 — срез не сработал». Про
|
||||||
|
потолок технической половины не сказал ничего.
|
||||||
|
- `specs` — **не сообщил потолок вовсе.** 4 находки. **Это находка о прогоне:**
|
||||||
|
проход обязан сообщать потолок сам.
|
||||||
|
- `autotests` — **не сообщил потолок вовсе.** 3 находки. То же.
|
||||||
|
- **Триаж:** 2 из 3 блокирующих, 4 из 4 «стоит исправить сейчас». Секция «сейчас»
|
||||||
|
заполнена под завязку; ничего не выброшено молча.
|
||||||
|
|
||||||
|
### Выброшено — поимённо
|
||||||
|
|
||||||
|
1. **A2**: вопрос темы про шаг конвейера — к диффу не относится.
|
||||||
|
2. **A3**: `shellcheck` SC1007 — ложное срабатывание, действующего последствия нет;
|
||||||
|
содержание уехало замером в N-4 и P-1.
|
||||||
|
3. **Порядок шагов в `gate`**: самый дешёвый шаг стоит пятым. Вкусовщина по всем
|
||||||
|
трём условиям: поведения не меняет, стоимости следующего изменения не меняет,
|
||||||
|
записанной конвенции о порядке шагов в проекте нет.
|
||||||
|
4. **`proposal.md`: «Внешних зависимостей изменение не трогает»** против переезда
|
||||||
|
`smithy-go` из `// indirect` в прямые требования. Версия `v1.27.7` не менялась,
|
||||||
|
пакет импортируется в `internal/adapter/recognizer/yandex/s3.go:15` — штатный
|
||||||
|
результат заказанного `go mod tidy`. Прозаическая неточность без последствий.
|
||||||
|
5. **`CLAUDE.md:114-115`** «краснеет с именем недостающего плагина» — новый шаг
|
||||||
|
плагином не является. Последствия не построено; чинится вместе с P-3.
|
||||||
|
6. **`docs/review.md:194-198`**: преамбула журнала не сходится с верхней записью.
|
||||||
|
Расхождение приехало предыдущим коммитом, вне диффа.
|
||||||
|
|
||||||
|
### Четыре строки, которые не принесёт ни один проход
|
||||||
|
|
||||||
|
1. **Решения проекта не сверялись.** `docs/adr/` — процессный документ, прогон его
|
||||||
|
не открывает. Для этого изменения это особенно весомо: `design.md` ссылается на
|
||||||
|
решения, но ни один проход не открывал `docs/adr/` и не проверял, не
|
||||||
|
противоречит ли новая норма уже принятому.
|
||||||
|
2. **Записанные наблюдения проекта не использовались.** `docs/research/` — тоже
|
||||||
|
процессный. Всякое число в этом отчёте снято на этом прогоне и сопровождается
|
||||||
|
командой замера.
|
||||||
|
3. **Поимённая сверка с руководствами по стилю языка не задавалась ни одним
|
||||||
|
проходом.** Для этого изменения дыра шире обычного: основной артефакт —
|
||||||
|
POSIX-shell, у которого в проекте нет ни конвенции, ни линтера, ни руководства.
|
||||||
|
4. **Альтернативной реализации, с которой можно сдиффить решения, у конвейера
|
||||||
|
нет.** Вопрос «а можно ли было решить это принципиально иначе — например, одним
|
||||||
|
`go.mod` как источником истины и генерацией остальных трёх мест» никто не
|
||||||
|
задавал.
|
||||||
|
|
||||||
|
### Среда прогона
|
||||||
|
|
||||||
|
- **Рабочее дерево не тронуто.** Все оракулы добыты на копиях в scratchpad. `git
|
||||||
|
status --porcelain` до и после прогона совпадает символ в символ.
|
||||||
|
- **Запреты `CLAUDE.md` соблюдены:** боевой каталог данных не трогался, боевой
|
||||||
|
токен не запускался, в Yandex Cloud не ходили, выкладка не запускалась,
|
||||||
|
`testdata` не заводилась, временное — только в scratchpad.
|
||||||
|
- **Отказов доступа не было.** `deny: Read(./build)` на путях этого изменения не
|
||||||
|
сработал (capability названа `toolchain` именно поэтому).
|
||||||
|
- **Ограничение среды:** подагентам запрещено писать файлы отчётов. Отчёт возвращён
|
||||||
|
триажем текстом и записан сюда оркестратором.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Дополнение: перепроверка после отработки B-1
|
||||||
|
|
||||||
|
Записано оркестратором после того, как находки триажа были отработаны. Отчёт без
|
||||||
|
этого раздела сообщал бы о составе прогона неверно.
|
||||||
|
|
||||||
|
## Что было сделано по находкам
|
||||||
|
|
||||||
|
| Находка | Действие | Исход |
|
||||||
|
|---|---|---|
|
||||||
|
| B-1 | развилка → человеку | Выбран вариант А: правило множественности разведено по местам, рамка в документах сужена до раздела стека. Изменило требование и код |
|
||||||
|
| B-2 | инлайн | Четвёртая capability и оговорка о её природе — в преамбуле `docs/architecture.md`. Ссылка на спеку поставлена шагом синка: до архивации файла нет и `docs.py check` краснеет битой ссылкой |
|
||||||
|
| N-1 | развилка → человеку | Выбран вариант Б: задачей урожая, вместе с `shellcheck`. Между мерджем и той задачей страж не проверен ничем — названо остатком |
|
||||||
|
| N-2 | инлайн | Строка в таблице «Механизировано» `docs/conventions/README.md` |
|
||||||
|
| N-3 | инлайн | Проверка читаемости места: код 3 и сообщение о нечитаемости вместо «версия не названа» с кодом 1 |
|
||||||
|
| N-4 | инлайн | Цена отказа от `shellcheck` названа по замеру в `CLAUDE.md` и `design.md`; ложный `SC1007` подавлен в скрипте |
|
||||||
|
|
||||||
|
## Второй прогон: только проход `specs`
|
||||||
|
|
||||||
|
**Полный прогон ревью кода не повторялся.** Правка по B-1 изменила дельта-спеку и
|
||||||
|
код, поэтому перепрогнан **целенаправленно один проход** — `specs`, владеющий
|
||||||
|
темой `requirements`, чей дом и изменился. `autotests` заменён собственным
|
||||||
|
прогоном гейта оркестратором; `code` и `basics` не перезапускались.
|
||||||
|
|
||||||
|
**Чем это ограничено, прямо:** технический разбор новых функций `section`,
|
||||||
|
`has_section` и `collect_doc` независимым проходом **не выполнялся** — их читал
|
||||||
|
только `specs` в своей оптике (соответствие норме) и оркестратор. Проход `code`
|
||||||
|
видел прежнюю редакцию скрипта. Триаж второй раз не запускался, поэтому находки
|
||||||
|
ниже не проходили дедупликации и добычи оракула независимым агентом — оракулы у
|
||||||
|
них свои, прогонами.
|
||||||
|
|
||||||
|
**Разметка не повторялась,** хотя дельта-спека менялась. Причина названа: правка
|
||||||
|
сузила формулировку одного требования внутри уже размеченной capability, не
|
||||||
|
меняя ни набора capability, ни периметра узлов, ни списка тем — план тем остался
|
||||||
|
бы тем же. Это осознанное отступление от правила «дельта-спеки изменились —
|
||||||
|
повтори разметку», а не пропуск.
|
||||||
|
|
||||||
|
## Находки перепроверки — три, все minor, все отработаны
|
||||||
|
|
||||||
|
- **S1 закрыта по существу**, а не переформулировкой. Проверено обеими сторонами:
|
||||||
|
норма больше не требует единственности от сборочного образа, и код ровно это и
|
||||||
|
делает; правдивая историческая строка о прошлой версии в памятке даёт зелёное,
|
||||||
|
а второе число внутри раздела стека — красное.
|
||||||
|
- **F-1.** Таблица `design.md` продолжала велеть «ровно одно вхождение на файл» —
|
||||||
|
правило, обратное принятой норме, — и переживала бы мердж как единственное
|
||||||
|
описание того, как машина ищет число. Абзац-мотивировка вдобавок стал
|
||||||
|
фактически неверен. Переписаны оба.
|
||||||
|
- **F-2, дороже прочих.** Норма называла начало раздела и молчала о конце.
|
||||||
|
Раздел закрывался только заголовком того же уровня, а строка, похожая на
|
||||||
|
заголовок, внутри блока кода читалась как настоящий заголовок. Худший исход —
|
||||||
|
**ложное зелёное**: если строку версии из раздела убрать, а ниже по файлу
|
||||||
|
появится заголовок первого уровня и любое «Go 1.26», шаг добрал бы число из
|
||||||
|
чужого места и промолчал. Граница определена требованием и исполнена кодом;
|
||||||
|
контрольный прогон подтверждает, что ложное зелёное исчезло — шаг теперь
|
||||||
|
честно говорит «версия не названа».
|
||||||
|
- **F-3.** Отличие «сломанного окружения» от «пропавшей строки» держалось только
|
||||||
|
на коде: ни один сценарий его не требовал, автотеста нет, гейт гоняет скрипт
|
||||||
|
ровно на согласованном дереве. Записано требованием и сценарием.
|
||||||
|
|
||||||
|
## Оракулы перепроверки
|
||||||
|
|
||||||
|
Регрессионная батарея — **21 прогон**, все совпали с ожиданием: 4 мутации по
|
||||||
|
минору и 4 удаления строки версии (по одной на место), директива `toolchain`,
|
||||||
|
патч в теге, смена базы образа, два слоя одной версии, два слоя разных версий,
|
||||||
|
дубль внутри раздела, история вне раздела, пропавший раздел, `PATH` без `go`,
|
||||||
|
запуск из подкаталога, коды выхода 0/1/2/3. Отдельно проверено, что при
|
||||||
|
нечитаемом файле строка «не называет версию» не печатается ни разу.
|
||||||
|
|
||||||
|
Сверх того: `openspec validate --strict` — valid; `task gate` — exit 0;
|
||||||
|
`task image` пересобран на `golang:1.26-alpine` — exit 0; `shellcheck` на скрипте
|
||||||
|
чист.
|
||||||
|
|
||||||
|
## Что осталось открытым после отработки
|
||||||
|
|
||||||
|
- **N-1: страж не покрыт ничем.** Решением человека уехало задачей урожая. Из
|
||||||
|
девятнадцати сценариев дельты машина гоняет один — тот, где всё сошлось;
|
||||||
|
остальные восемнадцать подтверждены разовыми прогонами. До закрытия той задачи
|
||||||
|
правки скрипта идут вслепую.
|
||||||
|
- **H-2: рантайм-база образа берётся «последней доступной».** Два образа из
|
||||||
|
одного коммита с разницей в неделю несут разные `ffmpeg`. Вне границ задачи,
|
||||||
|
уезжает урожаем.
|
||||||
|
- **Сигнал о заниженной метке** остаётся фактом для человека: ось «изменение
|
||||||
|
трогает канон» в правиле выбора метки отсутствует, а на этом прогоне именно она
|
||||||
|
дала обе блокирующие находки. Кандидат в правило — P-5.
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
## 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** он завершается отказом и называет место, где число не нашлось
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
## Критерии приёмки
|
||||||
|
|
||||||
|
### От постановки
|
||||||
|
|
||||||
|
Перенесены дословно из записи задачи `tasks/items/go-1-26-upgrade.md`: закрытие
|
||||||
|
задачи удалит файл, а критерии обязаны его пережить.
|
||||||
|
|
||||||
|
- Модуль, образ и документы называют одну версию Go. Оракул — `grep` по четырём
|
||||||
|
местам: `go.mod`, `Dockerfile`, `CLAUDE.md`, `README.md`; все дают одно число.
|
||||||
|
- Сборка на объявленной версии проходит. Оракул — `task image` и
|
||||||
|
`CGO_ENABLED=0 go build ./...` на чистом дереве.
|
||||||
|
- Расхождение версий роняет гейт. Оракул — прогон `task gate` на дереве, где
|
||||||
|
версия в `Dockerfile` понижена на минор: шаг краснеет и называет оба числа.
|
||||||
|
- Совпадение гейт не роняет, а сам шаг не требует docker и работает без сети.
|
||||||
|
Оракул — `task gate` на неизменённом дереве и прогон с
|
||||||
|
`DOCKER_HOST=/dev/null`.
|
||||||
|
- Гейт зелёный целиком. Оракул — `task gate`.
|
||||||
|
|
||||||
|
### От рубрики ревью дизайна
|
||||||
|
|
||||||
|
Проход `review-rubric`, стадия ревью дизайна. Рубрика на род узла «проверочный
|
||||||
|
шаг набора проверок, читающий разнородные источники».
|
||||||
|
|
||||||
|
- **Мутационный оракул на каждый источник.** Четыре прогона: по очереди понизить
|
||||||
|
минор в `go.mod`, `Dockerfile`, `CLAUDE.md`, `README.md`. В каждом шаг краснеет
|
||||||
|
и называет именно изменённый источник. Оракул — четыре прогона, четыре красных,
|
||||||
|
четыре разных сообщения.
|
||||||
|
- **Пустая выборка — отказ, а не согласие.** Четыре прогона: по очереди убрать
|
||||||
|
строку версии из каждого источника. Каждый даёт отказ с именем этого источника.
|
||||||
|
- **Исход — функция коммита, а не машины.** Оракул — прогон с `PATH`, из которого
|
||||||
|
убран `go`: тот же код выхода и тот же вывод, что и при установленном `go`.
|
||||||
|
- **Второе вхождение числа не проходит молча.** Оракул — дописать в `CLAUDE.md`
|
||||||
|
второе «Go 1.25» и прогнать шаг: он краснеет, а не судит по первому совпадению.
|
||||||
|
- **Патч и база образа свободны.** Оракул — два прогона: `golang:1.26.5-alpine` и
|
||||||
|
`golang:1.26-bookworm` в `Dockerfile`, оба зелёные.
|
||||||
|
- **Сообщение масштабируется на четыре места.** Оракул — прогон на дереве, где
|
||||||
|
один источник разошёлся с тремя: вывод содержит четыре пары «место: число».
|
||||||
|
- **Словарь кодов выхода объявлен.** Оракул — четыре прогона: сошлось, расхождение,
|
||||||
|
лишний аргумент, файл источника убран — дают 0, 1, 2 и 3.
|
||||||
|
- **Исход не зависит от рабочего каталога.** Оракул — прогон из корня и из
|
||||||
|
подкаталога дают один код выхода.
|
||||||
|
- **Шаг только читает.** Оракул — контрольная сумма дерева до и после прогона
|
||||||
|
совпадает, два прогона подряд дают одинаковый вывод.
|
||||||
|
- **Шаг не ходит в сеть.** Оракул — прогон под `strace -f -e
|
||||||
|
trace=socket,connect,sendto,recvfrom`: ни одного сетевого вызова.
|
||||||
|
- **Граница «чего шаг не проверяет» записана.** Оракул — раздел «Гейт» в
|
||||||
|
`CLAUDE.md` содержит строку о том, что сборка образа в набор проверок
|
||||||
|
по-прежнему не входит.
|
||||||
|
- **Директива `toolchain` не проходит незамеченной.** Оракул — дописать
|
||||||
|
`toolchain go1.27.0` в `go.mod` и прогнать шаг: он краснеет.
|
||||||
|
|
||||||
|
## 1. Шаг сверки версий
|
||||||
|
|
||||||
|
- [x] 1.1 Написать `scripts/check-go-version.sh` на POSIX `sh`: достать мажор и
|
||||||
|
минор из директивы `go` в `go.mod`, из тега `FROM golang:` в `Dockerfile`, из
|
||||||
|
строки стека в `CLAUDE.md` и из строки стека в `README.md`
|
||||||
|
- [x] 1.2 Пути строить от корня репозитория, а не от текущего каталога
|
||||||
|
- [x] 1.3 Не звать `go` ни в каком виде: ни `go mod edit`, ни `go list`, ни
|
||||||
|
`go env`. Исход обязан быть функцией содержимого файлов
|
||||||
|
- [x] 1.4 Тег образа разбирать по форме `golang:<мажор>.<минор>[.<патч>][-<база>]`
|
||||||
|
— патч и база отбрасываются
|
||||||
|
- [x] 1.5 Запретить директиву `toolchain` в `go.mod` — отказ с названием причины
|
||||||
|
- [x] 1.6 Второе вхождение числа в одном источнике — отказ; для `Dockerfile` все
|
||||||
|
`FROM golang:` обязаны давать одно число
|
||||||
|
- [x] 1.7 Отказ при расхождении: сообщение печатает все четыре места и число
|
||||||
|
каждого, а не одну пару
|
||||||
|
- [x] 1.8 Отказ при ненайденном числе: место названо поимённо, «нечего
|
||||||
|
сравнивать» исходом не считается
|
||||||
|
- [x] 1.9 Коды выхода по словарю проекта: 0 сошлось, 1 расхождение, 2 ошибка
|
||||||
|
употребления, 3 окружение
|
||||||
|
- [x] 1.10 Не использовать GNU-only флаги (`grep -P`, `sed -E` с расширениями,
|
||||||
|
`mapfile`); сделать скрипт исполняемым
|
||||||
|
- [x] 1.11 Завести шаг `go-version` в `Taskfile.yml` по образцу соседних шагов,
|
||||||
|
включая внятный отказ при отсутствии скрипта
|
||||||
|
- [x] 1.12 Включить шаг в `gate`
|
||||||
|
|
||||||
|
## 2. Подъём версии до 1.26
|
||||||
|
|
||||||
|
- [x] 2.1 `go.mod`: директива `go 1.26.0`
|
||||||
|
- [x] 2.2 `Dockerfile`: сборочный слой на `golang:1.26-alpine`
|
||||||
|
- [x] 2.3 `CLAUDE.md`, раздел «Стек»: «Go 1.26» — ровно одно вхождение числа на
|
||||||
|
файл, включая абзац из шага 3.1
|
||||||
|
- [x] 2.4 `README.md`, раздел «Технологии»: завести строку версии Go в форме,
|
||||||
|
которую находит скрипт
|
||||||
|
- [x] 2.5 `go mod tidy` и проверка, что `go.sum` не разъехался и директива
|
||||||
|
`toolchain` не появилась
|
||||||
|
|
||||||
|
## 3. Документы
|
||||||
|
|
||||||
|
- [x] 3.1 `CLAUDE.md`, раздел «Гейт»: новый шаг в перечне того, что красит
|
||||||
|
безусловно; строка о том, что сборка образа в гейт по-прежнему не входит;
|
||||||
|
словарь кодов выхода распространён на четвёртый проверочный шаг
|
||||||
|
- [x] 3.2 `docs/review.md`: запись за 2026-08-12 получает строку о том, чем
|
||||||
|
дефект закрыт
|
||||||
|
- [x] 3.3 `docs/architecture.md`, преамбула: четвёртая capability в перечне и
|
||||||
|
оговорка о том, что она нормирует не поведение сервиса (находка ревью B-2).
|
||||||
|
Ссылка на спеку ставится шагом синка, после архивации: до неё файла нет и
|
||||||
|
`docs.py check` краснеет битой ссылкой
|
||||||
|
- [x] 3.4 `docs/conventions/README.md`, таблица «Механизировано»: строка про
|
||||||
|
новое машинное правило (находка ревью N-2)
|
||||||
|
|
||||||
|
## 5. Отработка находок ревью кода
|
||||||
|
|
||||||
|
Отчёт триажа — `openspec/changes/go-1-26-upgrade/review/report.md`.
|
||||||
|
|
||||||
|
- [x] 5.1 N-3: нечитаемое место даёт код 3 с честным сообщением, а не «версия не
|
||||||
|
названа» с кодом 1. Проверка читаемости стоит рядом с проверкой существования:
|
||||||
|
подстановка команд в аргументе теряет код возврата, а в `read_doc` статус
|
||||||
|
конвейера берётся от последней команды
|
||||||
|
- [x] 5.2 N-4: цена отказа от `shellcheck` названа по замеру — один shell-скрипт,
|
||||||
|
а не четыре. Правка в `CLAUDE.md` и в `design.md`
|
||||||
|
- [x] 5.3 Подавление ложного `SC1007` в скрипте, чтобы заведение линтера потом
|
||||||
|
стоило ровно одной строки шага. `shellcheck` на скрипте чист
|
||||||
|
|
||||||
|
## 6. B-1: правило множественности разведено по местам
|
||||||
|
|
||||||
|
Находка B-1 меняла требование, поэтому прошла через чекпоинт заново. Решение
|
||||||
|
человека: развести правило по местам и сузить рамку до раздела.
|
||||||
|
|
||||||
|
- [x] 6.1 Требование: единственность — от раздела стека в документах, от
|
||||||
|
сборочного образа — совпадение всех вхождений `FROM golang:`, требование модуля
|
||||||
|
единственно по устройству файла
|
||||||
|
- [x] 6.2 Пять новых сценариев: дубль в разделе, число вне раздела, два слоя с
|
||||||
|
одной версией, два слоя с разными, пропавший раздел
|
||||||
|
- [x] 6.3 Скрипт читает раздел (`## Стек` в памятке, `## Технологии` в README), а
|
||||||
|
не файл целиком
|
||||||
|
- [x] 6.4 Пропавший раздел — свой исход с именем раздела, а не «версия не названа»
|
||||||
|
- [x] 6.5 Прогоны: история о прошлой версии вне раздела — зелено; пример команды в
|
||||||
|
README вне раздела — зелено; дубль внутри раздела — красно; два слоя одной
|
||||||
|
версии — зелено; два слоя разных — красно; раздела нет — красно с его именем
|
||||||
|
- [x] 6.6 Регрессия прежней батареи: 8 мутаций по местам, `toolchain`, патч и база
|
||||||
|
тега, `PATH` без `go`, подкаталог, коды 0/1/2/3 — все как прежде
|
||||||
|
- [x] 6.7 `openspec validate --strict` и `task gate` зелёные
|
||||||
|
|
||||||
|
## 7. Отработка перепроверки спек после правки B-1
|
||||||
|
|
||||||
|
Целевой перепрогон прохода `specs`: правка коснулась ровно его темы. Прежняя
|
||||||
|
находка S1 подтверждена закрытой по существу; три новые, все minor.
|
||||||
|
|
||||||
|
- [x] 7.1 F-2, ложное зелёное: раздел кончался только заголовком того же уровня, а
|
||||||
|
заголовок внутри блока кода читался как настоящий — число доставалось из-за
|
||||||
|
границы раздела. Граница определена в требовании и исполнена в коде: следующий
|
||||||
|
заголовок того же или более высокого уровня, блок кода заголовков не даёт,
|
||||||
|
хвостовые пробелы в заголовке не значат ничего
|
||||||
|
- [x] 7.2 F-2: два сценария — заголовок раздела внутри блока кода, раздел закрыт
|
||||||
|
заголовком верхнего уровня
|
||||||
|
- [x] 7.3 F-3: отличие «сломанного окружения» от «пропавшей строки» записано
|
||||||
|
требованием и сценарием, а не только кодом
|
||||||
|
- [x] 7.4 F-1: таблица `design.md` велела правило, обратное принятой норме
|
||||||
|
(«одно вхождение на файл»), и абзац-мотивировка стал фактически неверен —
|
||||||
|
переписаны оба
|
||||||
|
- [x] 7.5 Регрессия 21 прогоном: 8 мутаций по местам, `toolchain`, патч и база
|
||||||
|
тега, два слоя одной и разных версий, дубль в разделе, история вне раздела,
|
||||||
|
пропавший раздел, `PATH` без `go`, подкаталог, коды 0/1/2/3 — все совпали с
|
||||||
|
ожиданием
|
||||||
|
- [x] 7.6 `shellcheck` на скрипте чист
|
||||||
|
|
||||||
|
## 4. Проверка
|
||||||
|
|
||||||
|
- [x] 4.1 `CGO_ENABLED=0 go build ./...` и `go test ./...` на go1.26.5
|
||||||
|
- [x] 4.2 `task image` собирает образ на `golang:1.26-alpine`
|
||||||
|
- [x] 4.3 Четыре мутации по минору — по одной на источник; в каждой шаг краснеет
|
||||||
|
и называет изменённый источник; дерево возвращается как было
|
||||||
|
- [x] 4.4 Четыре мутации удалением строки версии — по одной на источник; в каждой
|
||||||
|
отказ называет место
|
||||||
|
- [x] 4.5 Прогон с `PATH` без `go`, прогон с `DOCKER_HOST=/dev/null`, прогон под
|
||||||
|
`strace` без сетевых вызовов — исход тот же
|
||||||
|
- [x] 4.6 Прогон из подкаталога — тот же код выхода
|
||||||
|
- [x] 4.7 Прогоны на `golang:1.26.5-alpine` и `golang:1.26-bookworm` — зелёные
|
||||||
|
- [x] 4.8 Прогон с дописанным `toolchain go1.27.0` — красный
|
||||||
|
- [x] 4.9 Четыре прогона на коды выхода: 0, 1, 2, 3
|
||||||
|
- [x] 4.10 Контрольная сумма дерева до и после прогона совпадает
|
||||||
|
- [x] 4.11 `task gate` целиком зелёный
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# 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 следовать словарю прочих проверочных шагов проекта: 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** он завершается отказом и называет место, где число не нашлось
|
||||||
Executable
+243
@@ -0,0 +1,243 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Сверяет объявленную версию Go во всех местах, где она названа: требование
|
||||||
|
# модуля, сборочный образ, памятка и README. Сравниваются мажор и минор; патч и
|
||||||
|
# база сборочного образа свободны.
|
||||||
|
#
|
||||||
|
# Шаг судит по содержимому репозитория и только по нему: команда `go` не
|
||||||
|
# зовётся вовсе. Иначе исход зависел бы от установленного тулчейна и от
|
||||||
|
# GOTOOLCHAIN, а при непустом GOTOOLCHAIN `go` вправе уйти в сеть за нужной
|
||||||
|
# версией — то есть шаг перестал бы быть функцией коммита. Ровно эта подмена
|
||||||
|
# держала дефект 2026-08-12 невидимым: `go build ./...` шёл на хостовом Go и был
|
||||||
|
# зелёным, пока образ не собирался.
|
||||||
|
#
|
||||||
|
# Коды выхода — общий словарь проверочных шагов проекта (CLAUDE.md, «Гейт»):
|
||||||
|
# 0 сошлось, 1 расхождение, 2 ошибка употребления, 3 окружение.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
me=check-go-version
|
||||||
|
|
||||||
|
if [ "$#" -ne 0 ]; then
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Использование: check-go-version.sh
|
||||||
|
|
||||||
|
Сверяет объявленную версию Go в go.mod, Dockerfile, CLAUDE.md и README.md.
|
||||||
|
Аргументов не принимает. Коды выхода: 0 сошлось, 1 расхождение,
|
||||||
|
2 ошибка употребления, 3 окружение.
|
||||||
|
EOF
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Корень считается от самого скрипта, а не от текущего каталога: иначе исход
|
||||||
|
# зависел бы от того, откуда шаг запустили.
|
||||||
|
#
|
||||||
|
# `CDPATH= ` — очистка переменной перед `cd`, а не забытый пробел в присваивании:
|
||||||
|
# непустой CDPATH увёл бы `cd` в чужой каталог. shellcheck читает это как SC1007,
|
||||||
|
# и подавление стоит здесь, чтобы заведение линтера потом стоило ровно одной
|
||||||
|
# строки шага.
|
||||||
|
# shellcheck disable=SC1007
|
||||||
|
root=$(CDPATH= cd -- "$(dirname -- "$0")/.." 2>/dev/null && pwd) || {
|
||||||
|
echo "$me: не удалось определить корень репозитория" >&2
|
||||||
|
exit 3
|
||||||
|
}
|
||||||
|
|
||||||
|
# Перечень мест закрыт и лежит здесь одним списком. Пятое место, о котором никто
|
||||||
|
# не знает, — это и есть тот дефект, против которого написан шаг, поэтому
|
||||||
|
# перечень не расползается по коду.
|
||||||
|
gomod=$root/go.mod
|
||||||
|
dockerfile=$root/Dockerfile
|
||||||
|
claudemd=$root/CLAUDE.md
|
||||||
|
readmemd=$root/README.md
|
||||||
|
|
||||||
|
# Нечитаемое место проверяется здесь, а не при разборе, и вот почему. Значения
|
||||||
|
# добываются подстановкой команд в аргументе, а она теряет код возврата; в
|
||||||
|
# `read_doc` вдобавок конвейер, чей статус берётся от последней команды. Отказ
|
||||||
|
# чтения дал бы пустой вход, и шаг сказал бы «README.md не называет версию» —
|
||||||
|
# то есть отправил бы чинить документ, в котором строка на месте, а сломаны
|
||||||
|
# права. Отказ окружения обязан звучать как отказ окружения.
|
||||||
|
for f in "$gomod" "$dockerfile" "$claudemd" "$readmemd"; do
|
||||||
|
if [ ! -f "$f" ]; then
|
||||||
|
echo "$me: нет файла ${f#"$root"/}" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
if [ ! -r "$f" ]; then
|
||||||
|
echo "$me: файл ${f#"$root"/} нечитаем" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Число непустых строк на входе.
|
||||||
|
count_lines() {
|
||||||
|
awk 'NF { n = n + 1 } END { print n + 0 }'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Различные непустые значения на входе.
|
||||||
|
distinct() {
|
||||||
|
awk 'NF' | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
|
# Директива `go` в go.mod: первые два числа.
|
||||||
|
read_gomod() {
|
||||||
|
sed -n 's/^go[[:space:]]\{1,\}\([0-9][0-9]*\.[0-9][0-9]*\).*$/\1/p' "$gomod"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Все теги golang в Dockerfile: golang:<мажор>.<минор>[.<патч>][-<база>].
|
||||||
|
# Патч и база отбрасываются — спека объявила их свободными.
|
||||||
|
read_dockerfile() {
|
||||||
|
sed -n \
|
||||||
|
's/^[Ff][Rr][Oo][Mm][[:space:]].*golang:\([0-9][0-9]*\.[0-9][0-9]*\).*$/\1/p' \
|
||||||
|
"$dockerfile"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Тело раздела документа. Раздел кончается следующим заголовком того же или более
|
||||||
|
# высокого уровня; заголовок внутри блока кода заголовком не считается, иначе
|
||||||
|
# пример в чужом разделе открыл бы «раздел стека» на пустом месте. Хвостовые
|
||||||
|
# пробелы в заголовке markdown не рендерит, поэтому и здесь они не значат ничего.
|
||||||
|
section() {
|
||||||
|
awk -v want="$2" '
|
||||||
|
/^```/ { fence = !fence; if (inside) print; next }
|
||||||
|
!fence && (/^# / || /^## /) {
|
||||||
|
line = $0
|
||||||
|
sub(/[[:space:]]+$/, "", line)
|
||||||
|
inside = (line == want)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
inside { print }
|
||||||
|
' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Есть ли в документе раздел с таким заголовком. Правила те же, что у section:
|
||||||
|
# иначе «раздел есть» и «раздел читается» разошлись бы на первом же примере.
|
||||||
|
has_section() {
|
||||||
|
awk -v want="$2" '
|
||||||
|
/^```/ { fence = !fence; next }
|
||||||
|
!fence && (/^# / || /^## /) {
|
||||||
|
line = $0
|
||||||
|
sub(/[[:space:]]+$/, "", line)
|
||||||
|
if (line == want) { found = 1 }
|
||||||
|
}
|
||||||
|
END { exit(found ? 0 : 1) }
|
||||||
|
' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Все вхождения образца `Go <мажор>.<минор>` в разделе стека документа.
|
||||||
|
#
|
||||||
|
# Читается именно раздел, а не файл целиком. `CLAUDE.md` по устройству ведёт
|
||||||
|
# историю закрытых долгов, и правдивая строка о прошлой версии уронила бы шаг
|
||||||
|
# сообщением «называет версию больше одного раза» — то есть послала бы чинить
|
||||||
|
# исторический документ вместо проверки. Тем же ловился бы любой пример команды
|
||||||
|
# в README.
|
||||||
|
#
|
||||||
|
# Первый sed ставит каждое вхождение на свою строку: два числа в одной строке
|
||||||
|
# иначе слились бы в одно, и второе, протухшее, осталось бы невидимым.
|
||||||
|
read_doc() {
|
||||||
|
section "$1" "$2" | sed -e 's/Go [0-9][0-9]*\.[0-9][0-9]*/\
|
||||||
|
&\
|
||||||
|
/g' | sed -n 's/^Go \([0-9][0-9]*\.[0-9][0-9]*\)$/\1/p'
|
||||||
|
}
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
report=''
|
||||||
|
collected=''
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "$me: $1" >&2
|
||||||
|
failed=1
|
||||||
|
}
|
||||||
|
|
||||||
|
add_report() {
|
||||||
|
line=$(printf ' %-11s %s' "$1" "$2")
|
||||||
|
report="$report$line
|
||||||
|
"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Добывает число из места и кладёт его в `collected`; пусто — значит не добыто.
|
||||||
|
# Третий аргумент: 1 — место обязано называть версию ровно один раз.
|
||||||
|
#
|
||||||
|
# Функция не зовётся в подстановке команд намеренно: подоболочка потеряла бы и
|
||||||
|
# флаг отказа, и отчёт.
|
||||||
|
collect() {
|
||||||
|
place=$1
|
||||||
|
values=$2
|
||||||
|
strict=$3
|
||||||
|
|
||||||
|
collected=''
|
||||||
|
total=$(printf '%s\n' "$values" | count_lines)
|
||||||
|
uniq=$(printf '%s\n' "$values" | distinct)
|
||||||
|
uniq_total=$(printf '%s\n' "$uniq" | count_lines)
|
||||||
|
|
||||||
|
if [ "$total" -eq 0 ]; then
|
||||||
|
add_report "$place" 'версия не названа'
|
||||||
|
fail "$place не называет версию Go"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$uniq_total" -gt 1 ]; then
|
||||||
|
add_report "$place" "$(printf '%s' "$uniq" | tr '\n' '/') — разные числа"
|
||||||
|
fail "$place называет несколько разных версий"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$strict" -eq 1 ] && [ "$total" -gt 1 ]; then
|
||||||
|
add_report "$place" "$uniq — названа $total раз(а)"
|
||||||
|
fail "$place называет версию больше одного раза"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
collected=$uniq
|
||||||
|
add_report "$place" "$uniq"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Директива toolchain — пятое место, которого перечень не знает: четыре числа
|
||||||
|
# сойдутся, а собирать будет пятое.
|
||||||
|
if grep '^toolchain[[:space:]]' "$gomod" >/dev/null 2>&1; then
|
||||||
|
fail 'go.mod содержит директиву toolchain — она называет версию пятым местом'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Раздел, в котором документ называет версию. За его пределами число не читается.
|
||||||
|
claude_section='## Стек'
|
||||||
|
readme_section='## Технологии'
|
||||||
|
|
||||||
|
# Пропавший раздел — отдельный исход: он говорит «искать негде», а не «версия не
|
||||||
|
# названа», и чинится другим движением.
|
||||||
|
collect_doc() {
|
||||||
|
place=$1
|
||||||
|
path=$2
|
||||||
|
heading=$3
|
||||||
|
|
||||||
|
collected=''
|
||||||
|
if ! has_section "$path" "$heading"; then
|
||||||
|
add_report "$place" "нет раздела «$heading»"
|
||||||
|
fail "в $place нет раздела «$heading», где называется версия"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
collect "$place" "$(read_doc "$path" "$heading")" 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Документы называют версию прозой, и второе вхождение в разделе стека — не
|
||||||
|
# дубликат, а второе утверждение: обновят одно, второе протухнет молча. У
|
||||||
|
# Dockerfile иначе: каждый сборочный слой — настоящий вход сборки, и от них
|
||||||
|
# требуется совпадение, а не единственность.
|
||||||
|
collect go.mod "$(read_gomod)" 1
|
||||||
|
v_gomod=$collected
|
||||||
|
collect Dockerfile "$(read_dockerfile)" 0
|
||||||
|
v_docker=$collected
|
||||||
|
collect_doc CLAUDE.md "$claudemd" "$claude_section"
|
||||||
|
v_claude=$collected
|
||||||
|
collect_doc README.md "$readmemd" "$readme_section"
|
||||||
|
v_readme=$collected
|
||||||
|
|
||||||
|
if [ "$failed" -eq 0 ]; then
|
||||||
|
found=$(printf '%s\n%s\n%s\n%s\n' \
|
||||||
|
"$v_gomod" "$v_docker" "$v_claude" "$v_readme" | distinct | count_lines)
|
||||||
|
if [ "$found" -ne 1 ]; then
|
||||||
|
fail 'объявленные версии Go разошлись'
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$failed" -ne 0 ]; then
|
||||||
|
printf 'Объявленная версия Go по местам:\n%s' "$report" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Reference in New Issue
Block a user