From 26256cdb064a26f6c687682c2e1150d2062e4a22 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Thu, 13 Aug 2026 16:36:22 +0300 Subject: [PATCH] =?UTF-8?q?openspec:=20=D1=83=D0=BF=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=B4=D0=BD=D0=B5=D0=BD=D0=B0=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B0?= =?UTF-8?q?=20toolchain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Инструментарий проекта спеками не нормируется: capability toolchain удалена, норму шага сверки версий Go держат его проверки в scripts. - Перечень capability в архитектуре и ревью сокращён до четырёх, решение ADR-2026-08-12-spec-norms-build-toolchain помечено устаревшим. --- ...R-2026-08-12-spec-norms-build-toolchain.md | 8 +- docs/adr/README.md | 2 +- docs/architecture.md | 14 +- docs/conventions/go-linters.md | 14 +- docs/review.md | 8 +- openspec/specs/toolchain/spec.md | 241 ------------------ scripts/check_go_version_test.go | 11 +- 7 files changed, 31 insertions(+), 267 deletions(-) delete mode 100644 openspec/specs/toolchain/spec.md diff --git a/docs/adr/ADR-2026-08-12-spec-norms-build-toolchain.md b/docs/adr/ADR-2026-08-12-spec-norms-build-toolchain.md index 0d36caf..d5ca6b2 100644 --- a/docs/adr/ADR-2026-08-12-spec-norms-build-toolchain.md +++ b/docs/adr/ADR-2026-08-12-spec-norms-build-toolchain.md @@ -2,6 +2,9 @@ - **Дата:** 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 +- **Статус:** устарело — 2026-08-13 владелец решил обратное: инструментарию в + спеках не место. Capability `toolchain` упразднена, замены у неё нет, норму + шага держат его проверки в `scripts/check_go_version_test.go` ## Решение @@ -10,8 +13,9 @@ собирают. Потребитель у неё другой: тот, кто собирает. Требование о согласованности объявленной версии Go живёт нормой в -[openspec/specs/toolchain/spec.md](../../openspec/specs/toolchain/spec.md), а не -прозой в памятке. +`openspec/specs/toolchain/spec.md`, а не прозой в памятке. *Уточнено 2026-08-13: +файла по этому адресу больше нет, ссылка снята — capability упразднена, см. +статус записи.* ## Почему diff --git a/docs/adr/README.md b/docs/adr/README.md index 9c8af88..0300b22 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -39,7 +39,7 @@ | 2026-08-12 | [Сессия живёт семь суток и не продлевает саму себя](ADR-2026-08-12-session-without-refresh.md) | | | 2026-08-12 | [Кого пускать в сервис, решает правило провайдера, а не сервис](ADR-2026-08-12-access-delegated-to-provider.md) | | | 2026-08-12 | [Код провайдера меняется на сессию вызовом собственного адреса хранилища внутри процесса](ADR-2026-08-12-oidc-exchange-via-own-route.md) | | -| 2026-08-12 | [Спекой нормируется и инструмент сборки, а не только поведение сервиса](ADR-2026-08-12-spec-norms-build-toolchain.md) | | +| 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) | заменено на [ADR-2026-08-12-protected-file-behind-session](ADR-2026-08-12-protected-file-behind-session.md) | | 2026-08-12 | [Каталог данных задаётся одним ключом `[storage] data_dir`](ADR-2026-08-12-single-data-dir-config-key.md) | | diff --git a/docs/architecture.md b/docs/architecture.md index 9b6fe55..d90e7fe 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,9 +8,11 @@ [passport.md](passport.md) и в [tasks/BACKLOG.md](../tasks/BACKLOG.md); что из этого ещё не решено — в разделе «Открытые вопросы». -Заведены пять capability. Четыре первые нормируют **поведение сервиса** для его -потребителей; пятая — исключение из первого абзаца: она нормирует не сервис, а -инструмент, которым его собирают, и потребитель у неё другой — тот, кто собирает. +Заведены четыре capability, и все нормируют **поведение сервиса** для его +потребителей. Инструмент, которым сервис собирают, спеками не нормируется вовсе: +у набора проверок и сборки другой потребитель — тот, кто собирает, — и решением +от 2026-08-13 его нормы живут в самих шагах, их проверках и +[conventions/go-linters.md](conventions/go-linters.md). - [intake](../openspec/specs/intake/spec.md) — **только приём по HTTP**: приём и опрос за сессией, имя отправителя не доходит ни до хранилища, ни до журнала, @@ -30,11 +32,7 @@ его дальше: вход через внешнего провайдера OIDC, чем предъявляется сессия, что её прекращает и какие адреса остаются открытыми. Задача `oidc-login` 2026-08-12. Разграничения записей по владельцу здесь нет: всякий вошедший - видит всё, что видел прежде аноним; -- [toolchain](../openspec/specs/toolchain/spec.md) — каким инструментом и какой - его версии собирается сервис: одно число версии Go во всех местах, где она - названа, и шаг гейта, который это сверяет. Задача `go-1-26-upgrade` - 2026-08-12. + видит всё, что видел прежде аноним. Поведение прочих узлов, включая приём из Telegram, по-прежнему живёт только в коде. Задача, которая его трогает, дописывает спеку своей capability. diff --git a/docs/conventions/go-linters.md b/docs/conventions/go-linters.md index 320a95c..07176a7 100644 --- a/docs/conventions/go-linters.md +++ b/docs/conventions/go-linters.md @@ -36,12 +36,12 @@ Go-проект как есть. Своё здесь — перечень пра - **настройка конвейера ревью, вопросы по темам и журнал дефектов** — [../review.md](../review.md). Перечень ниже говорит этим вопросам, чего спрашивать уже не нужно; -- **поведение сервиса** — нормативные спеки `openspec/specs/`. У шага сверки - версий Go поведение нормировано отдельно, спекой - [toolchain](../../openspec/specs/toolchain/spec.md): это единственная проверка - проекта, у которой есть своя capability, и потому единственная, чьи сценарии - проверяются построчно (`scripts/check_go_version_test.go`). Второй самодельный - шаг — `migrations` — нормы не имеет: он проверен мутацией на трёх исходах +- **поведение сервиса** — нормативные спеки `openspec/specs/`. Шаги набора + проверок туда не входят: инструментарий спеками не нормируется, и спека + `toolchain`, заведённая под шаг сверки версий Go, упразднена 2026-08-13. Норму + этого шага держат его собственные проверки — двадцать сценариев в + `scripts/check_go_version_test.go`, и другого дома у неё нет. Второй самодельный + шаг — `migrations` — не проверен и ими: он прогнан мутацией на трёх исходах (переписанный шаг, пустой каталог, чистое дерево), но регрессионных проверок у него нет, и дрейф его собственного шаблона имени никто не поймает. Владелец решил 2026-08-13 оставить это как есть: проверка над проверкой даёт много @@ -147,7 +147,7 @@ Go-проект как есть. Своё здесь — перечень пра | Правило | Где механизировано | | --- | --- | | Проверка судит ответ по готовому ответу (`Result()`), а не по живой карте заголовков обработчика | `.golangci.yml` → `forbidigo` с `analyze-types`, находки только в `*_test.go`. Судит по типу приёмника (`httptest.ResponseRecorder`), поэтому ловит любую форму: цепочкой, через переменную, по индексу карты, обходом, полем `HeaderMap`. Остаётся ревью проверка, идущая мимо recorder — через свой `http.ResponseWriter` | -| Каждый сценарий нормы шага сверки версий проверен мутацией, а не памятью | `scripts/check_go_version_test.go` — 20 сценариев спеки `toolchain` плюс два свойства самого шага: исход не зависит от установленного `go`, и шаг не зовёт ни `go`, ни `docker`, ни сеть | +| Каждый сценарий нормы шага сверки версий проверен мутацией, а не памятью | `scripts/check_go_version_test.go` — 20 сценариев шага плюс два его свойства: исход не зависит от установленного `go`, и шаг не зовёт ни `go`, ни `docker`, ни сеть | | Форма утверждения в проверках: «ожидалось» и «получено» не перепутаны местами, отказ судится `NoError`, а не `Nil`, `require` не зовут из горутины | `.golangci.yml` → `testifylint` | | Одновременный доступ проверен детектором, а не чтением кода | `Taskfile.yml` → шаг `tests` (`go test -race ./...`). Общее у воркеров — счётчики метрик, логгер и клиент бота; захват задачи в гонку не входит, он по построению её не даёт (одно состояние на воркер) — см. «Типовые ложноположительные» в [../review.md](../review.md). Что делает шаг без компилятора C и каким кодом краснеет — [CLAUDE.md](../../CLAUDE.md), «Гейт» | | Строчное подавление называет линтер и причину, а протухшее краснеет | `.golangci.yml` → `nolintlint` (`require-explanation`, `require-specific`, `allow-unused: false`) | diff --git a/docs/review.md b/docs/review.md index 0f7e11a..2d5b5b4 100644 --- a/docs/review.md +++ b/docs/review.md @@ -145,8 +145,8 @@ `createTranscribeJob` — сегодня через него идут оба входа ([architecture.md](architecture.md), «Единые точки проекта»). - `architecture`: не поехало ли поведение в `architecture.md` вместо спеки — - заведены пять capability (`intake`, `pipeline`, `storage`, `access`, - `toolchain`), и первые две описаны частично. Поведение прочих узлов, включая + заведены четыре capability (`intake`, `pipeline`, `storage`, `access`), и + первые две описаны частично. Поведение прочих узлов, включая приём из Telegram, живёт в обзоре под маркерами долга, а соблазн дописать туда ещё — самый большой. - `conventions`: новая колонка правится во всех четырёх местах репозитория @@ -472,7 +472,9 @@ API и имя не откатываются обратной правкой по (`scripts/check-go-version.sh`). Сверяются четыре места, а не два, — `go.mod`, `Dockerfile`, `CLAUDE.md`, `README.md`: в этом дефекте трое из четырёх врали согласованно, и парная сверка не увидела бы документ, разошедшийся с -согласованным кодом. Норма — capability `toolchain`. +согласованным кодом. Норму шага держат его собственные проверки +(`scripts/check_go_version_test.go`): спека `toolchain`, бывшая его домом, +упразднена 2026-08-13 — инструментарий спеками не нормируется. ## 2026-08-11 — норма требовала от сервиса недостижимого [пойман ревью] diff --git a/openspec/specs/toolchain/spec.md b/openspec/specs/toolchain/spec.md deleted file mode 100644 index 325b7e1..0000000 --- a/openspec/specs/toolchain/spec.md +++ /dev/null @@ -1,241 +0,0 @@ -# toolchain Specification - -## Purpose - -Каким инструментом и какой его версии собирается сервис, и что об этом -проверяется до выкладки. Заведена задачей `go-1-26-upgrade` 2026-08-12 по -дефекту, записанному в `docs/review.md` за то же число: сборочный образ разошёлся -с требованием модуля, образ перестал собираться, а восемь шагов гейта и шесть -проходов ревью показали зелёное. - -Capability нормирует **не поведение сервиса** для его потребителей, а поведение -инструмента разработки; потребитель у неё другой — тот, кто собирает сервис. Это -осознанное исключение, и оно названо в преамбуле `docs/architecture.md`. - -## Requirements -### Requirement: Версия инструмента сборки объявлена одним числом - -Проект SHALL объявлять версию Go, на которой собирается сервис, одинаково во -всех местах, где она названа. Мест ровно четыре, и перечень закрыт: требование -модуля в `go.mod`, сборочный образ в `Dockerfile`, строка стека в `CLAUDE.md`, -строка стека в `README.md`. - -Сравниваются мажор и минор. Третье число у сборочного образа MUST оставаться -свободным, как и база образа: образ обновляется своим темпом, и требовать от -него совпадения по патчу значило бы краснеть на каждом его обновлении. Тег -читается по форме `golang:<мажор>.<минор>[.<патч>][-<база>]`, и берутся из него -первые два числа. - -Правило множественности у мест разное, потому что места устроены по-разному. - -**Документы** — `CLAUDE.md` и `README.md` — MUST называть версию ровно один раз, -и считается это **не по файлу, а по разделу стека**: `## Стек` в памятке, -`## Технологии` в README. Второе вхождение числа **в этом разделе** MUST -считаться отказом: обновят одно, второе протухнет молча. За пределами раздела -число не читается вовсе — иначе памятка, которая по устройству ведёт историю -закрытых долгов, роняла бы проверку на первой же правдивой строке о прошлой -версии, а сообщение толкало бы чинить не проверку, а исторический документ. - -**Сборочный образ** единственности не требует: каждый слой — настоящий вход -сборки, и многослойная сборка законна. От всех вхождений `FROM golang:` MUST -требоваться совпадение мажора и минора, а не единственность. - -**Требование модуля** называется директивой `go` и по устройству файла -единственно. - -Граница раздела MUST быть определена, а не подразумеваться: раздел кончается -следующим заголовком того же или более высокого уровня, заголовок третьего уровня -и ниже остаётся внутри раздела, а строка, похожая на заголовок, но лежащая внутри -блока кода, заголовком MUST не считаться. Без этого пример в чужом разделе -открывал бы раздел стека на пустом месте, и число доставалось бы оттуда, откуда -норма его читать не велит. - -`go.mod` MUST не содержать директиву `toolchain`. Она называет версию **пятым** -местом, которого перечень не знает: при `toolchain go1.27.0` четыре объявленных -числа сойдутся, а собирать будет пятое — то есть вернётся тот самый класс -расхождения, ради которого требование и заведено. - -#### Scenario: Все четыре места названы одинаково - -- **GIVEN** дерево проекта, где `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` - называют версию Go -- **WHEN** их читают подряд -- **THEN** мажор и минор совпадают во всех четырёх - -#### Scenario: Патч сборочного образа отличается законно - -- **GIVEN** `go.mod` требует `1.26.0`, а образ собирается на `golang:1.26.5-alpine` -- **WHEN** версии сравнивают -- **THEN** расхождением это не считается - -#### Scenario: База сборочного образа сменилась - -- **GIVEN** образ переехал с `golang:1.26-alpine` на `golang:1.26-bookworm` -- **WHEN** версии сравнивают -- **THEN** расхождением это не считается - -#### Scenario: Раздел стека называет версию дважды - -- **GIVEN** раздел стека в `CLAUDE.md` называет версию два раза -- **WHEN** версии сравнивают -- **THEN** это расхождение, даже если оба числа одинаковы - -#### Scenario: Число за пределами раздела стека не читается - -- **GIVEN** `CLAUDE.md` вне раздела стека упоминает прошлую версию Go — например - записью о закрытом долге -- **WHEN** версии сравнивают -- **THEN** расхождением это не считается - -#### Scenario: Сборочный образ собран в два слоя - -- **GIVEN** `Dockerfile` содержит два `FROM golang:` с одним мажором и минором -- **WHEN** версии сравнивают -- **THEN** расхождением это не считается - -#### Scenario: Слои сборочного образа разошлись между собой - -- **GIVEN** `Dockerfile` содержит два `FROM golang:` с разными минорами -- **WHEN** версии сравнивают -- **THEN** это расхождение - -#### Scenario: Заголовок раздела встретился внутри блока кода - -- **GIVEN** документ в чужом разделе показывает пример, внутри которого есть - строка, совпадающая с заголовком раздела стека, а ниже названо другое число -- **WHEN** версии сравнивают -- **THEN** число из примера не читается, и расхождением это не считается - -#### Scenario: Раздел стека закрыт заголовком верхнего уровня - -- **GIVEN** после раздела стека идёт заголовок первого уровня, а ниже названа - прошлая версия -- **WHEN** версии сравнивают -- **THEN** это число не читается, и расхождением не считается - -#### Scenario: Раздела стека нет вовсе - -- **GIVEN** в документе нет раздела, где называется версия -- **WHEN** запускают шаг сверки -- **THEN** он завершается отказом и называет недостающий раздел - -#### Scenario: Модуль объявляет версию пятым местом - -- **GIVEN** `go.mod` содержит директиву `toolchain` -- **WHEN** версии сравнивают -- **THEN** это расхождение - -### Requirement: Объявленное число — то, на котором проект собирается - -Объявленная версия SHALL быть той, на которой сервис действительно собирается и -проходит тесты. Согласованность четырёх строк между собой этого не доказывает: -четыре одинаковых числа несуществующей версии требованию о согласованности -удовлетворяют, а собрать на них нельзя. - -Проверка эта MUST оставаться за человеком и MUST не входить в набор проверок: -она требует сборки образа, а сборка образа набором проверок не делается -намеренно — дорого. Подъём версии MUST не уезжать в основную ветку, пока сборка -образа и тесты на объявленном числе не прогнаны. - -#### Scenario: Версию подняли - -- **GIVEN** объявленную версию Go подняли во всех четырёх местах -- **WHEN** изменение готовят к мерджу -- **THEN** до мерджа на этой версии прогнаны сборка образа и тесты - -### Requirement: Расхождение версий роняет набор проверок - -Набор проверок `task gate` SHALL включать шаг, который сравнивает объявленные -версии между собой и MUST завершаться отказом, когда они разошлись. Сообщение -отказа MUST называть **все четыре места и прочитанное в каждом число** — не одну -разошедшуюся пару: в дефекте 2026-08-12 три места из четырёх говорили одно и то -же и неверными были именно они, а по сообщению о паре человек чинит не то место. - -Шаг MUST судить по содержимому файлов репозитория и MUST не спрашивать -установленный инструмент — ни `go version`, ни `go env`, ни `GOTOOLCHAIN`. Исход -его MUST быть функцией коммита, а не машины: шаг, чей ответ зависит от того, что -стоит на хосте, воспроизводит ровно ту подмену, которая держала дефект -2026-08-12 невидимым — там `go build ./...` шёл на хостовом Go, а объявленное -число не проверял никто. - -Шаг MUST работать сравнением строк — без сборки образа, без docker и без сети — -и MUST не зависеть от рабочего каталога, из которого запущен. Шаг MUST только -читать: файлов он не правит и разошедшихся мест не чинит. - -Коды выхода MUST следовать общему словарю проверочных шагов проекта; словарь -объявляет раздел «Гейт» в `CLAUDE.md`, и здесь он не повторяется. Своего словаря шаг -MUST не заводить: четвёртый шаг с собственной семантикой сделал бы это -утверждение неверным. - -Место, где числа не нашлось вовсе, MUST считаться отказом с именем этого места. -«Нечего сравнивать» исходом MUST не быть: пропавшая строка иначе выглядела бы -как совпадение. - -Отказ чтения места MUST не выглядеть как отсутствие числа. Место, которое -существует, но не читается, — это отказ окружения, и сообщение MUST говорить о -нечитаемости, а не о ненайденной версии: иначе шаг отправляет чинить документ, в -котором строка на месте, а сломаны права. - -#### Scenario: Разошёлся сборочный образ - -- **GIVEN** `Dockerfile` называет версию, отличную от прочих трёх мест -- **WHEN** запускают `task gate` -- **THEN** шаг сверки завершается отказом -- **AND** сообщение называет все четыре места и число каждого -- **AND** весь набор проверок краснеет - -#### Scenario: Разошлось требование модуля - -- **GIVEN** `go.mod` называет версию, отличную от прочих трёх мест -- **WHEN** запускают шаг сверки -- **THEN** он завершается отказом и называет `go.mod` среди разошедшихся - -#### Scenario: Разошлась памятка - -- **GIVEN** `CLAUDE.md` называет версию, отличную от прочих трёх мест -- **WHEN** запускают шаг сверки -- **THEN** он завершается отказом и называет `CLAUDE.md` среди разошедшихся - -#### Scenario: Разошёлся README - -- **GIVEN** `README.md` называет версию, отличную от прочих трёх мест -- **WHEN** запускают шаг сверки -- **THEN** он завершается отказом и называет `README.md` среди разошедшихся - -#### Scenario: Версии совпадают - -- **GIVEN** все четыре места называют одно число -- **WHEN** запускают `task gate` -- **THEN** шаг сверки проходит с кодом 0 -- **AND** остальные шаги набора идут как прежде - -#### Scenario: Инструмента сборки нет на машине - -- **GIVEN** в `PATH` нет `go` вовсе -- **WHEN** запускают шаг сверки -- **THEN** исход и сообщение те же, что и при установленном `go` - -#### Scenario: Ни docker, ни сети нет - -- **GIVEN** docker недоступен и сети нет -- **WHEN** запускают шаг сверки -- **THEN** он отрабатывает и даёт тот же исход, что и при доступном docker - -#### Scenario: Шаг запущен не из корня проекта - -- **GIVEN** шаг запускают из подкаталога дерева -- **WHEN** он ищет свои четыре места -- **THEN** исход тот же, что и при запуске из корня - -#### Scenario: Место существует, но не читается - -- **GIVEN** файл одного из мест на диске есть, но прав на чтение нет -- **WHEN** запускают шаг сверки -- **THEN** он завершается кодом окружения и говорит о нечитаемости места -- **AND** сообщения «версия не названа» не печатает - -#### Scenario: Версия не названа там, где должна быть - -- **GIVEN** одно из четырёх мест перестало называть версию Go -- **WHEN** запускают шаг сверки -- **THEN** он завершается отказом и называет место, где число не нашлось diff --git a/scripts/check_go_version_test.go b/scripts/check_go_version_test.go index a046b8e..c4ffad8 100644 --- a/scripts/check_go_version_test.go +++ b/scripts/check_go_version_test.go @@ -1,11 +1,12 @@ // Package scripts — проверки скриптов репозитория. Рабочего кода на Go в нём // нет: пакет существует ради того, чтобы `go test ./...` гонял и shell. // -// Норма шага сверки версий — openspec/specs/toolchain/spec.md. Каждый её -// сценарий проверяется здесь мутацией: дерево-образец собирается во временном -// каталоге, портится ровно одним способом, и от скрипта требуется объявленный -// исход. Прежде сценарии подтверждались разовыми ручными прогонами — после -// первой правки образца они перестали бы выполняться молча. +// Норма шага сверки версий живёт здесь и в комментариях самого скрипта: спека +// toolchain, бывшая её домом, упразднена 2026-08-13 — инструментарий спеками не +// нормируется. Каждый сценарий проверяется мутацией: дерево-образец собирается +// во временном каталоге, портится ровно одним способом, и от скрипта требуется +// объявленный исход. Прежде сценарии подтверждались разовыми ручными прогонами — +// после первой правки образца они перестали бы выполняться молча. package scripts import (