From 6c7f006e75ee57271600631021f976bd21827a46 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Thu, 13 Aug 2026 07:35:58 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D1=8B=20=D0=B3=D0=B5=D0=B9=D1=82=D0=B0?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B5=D1=85=D0=B0=D0=BB=D0=B8=20?= =?UTF-8?q?=D0=B2=20=D1=81=D0=B2=D0=BE=D0=B9=20=D0=B4=D0=BE=D0=BC=20?= =?UTF-8?q?=E2=80=94=20docs/autotests.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - перечень «Механизировано» и то, что осталось прозой, снято из конвенций: они про то, как писать код, а не про инструменты, которые его читают - новый документ — дом темы ревью autotests, с границами: семантика гейта остаётся в CLAUDE.md, журнал дефектов и вопросы по темам — в review.md - вопрос ревью о суждении по готовому ответу сужен до того, что машина не проверяет: до ответа мимо recorder --- CLAUDE.md | 3 ++ docs/autotests.md | 64 ++++++++++++++++++++++++++++++++++++ docs/conventions/README.md | 35 +++++--------------- docs/conventions/database.md | 2 +- docs/conventions/logging.md | 2 +- docs/review.md | 10 ++++-- 6 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 docs/autotests.md diff --git a/CLAUDE.md b/CLAUDE.md index 3bc8bf6..b45a0da 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,6 +105,9 @@ task gate # весь набор проверок разом - **Команда целиком:** `task gate`. База диффа — переменная `BASE`, по умолчанию `origin/master`; переопределяется `task gate BASE=`. +- **Какое правило чем проверяется** — [docs/autotests.md](docs/autotests.md), дом + темы `autotests`. Здесь семантика гейта, там перечень правил и место настройки + каждого; перечень здесь не повторяется. - **Где логи шагов:** вывод команды, отдельного файла нет. - **Что означает каждый исход:** ненулевой код любого шага роняет гейт. У `docs.py check`, `tasks.py check`, `openspec.py check` и diff --git a/docs/autotests.md b/docs/autotests.md new file mode 100644 index 0000000..ef6b87b --- /dev/null +++ b/docs/autotests.md @@ -0,0 +1,64 @@ +# Автопроверки + +Чем машина судит код: перечень свойств, доведённых до проверки, и место, где +каждое настроено. Документ — дом темы ревью `autotests`: проход, которому эта +тема досталась, читает его, а не перечисляет инструменты по памяти. + +Тема заведена 2026-08-13. Прежде перечень лежал разделом «Механизировано» в +[conventions/README.md](conventions/README.md), и это был чужой дом: конвенции +говорят, **как писать код**, а здесь речь об инструментах, которые его читают. + +## Границы дома + +Что здесь есть и чего здесь нет — чтобы факт не жил в двух местах: + +- **семантика гейта** — команда целиком, база диффа, словарь кодов выхода, что + красит безусловно, чего в гейте намеренно нет и кто тогда обязан это гонять — + в [CLAUDE.md](../CLAUDE.md), раздел «Гейт». Здесь это не повторяется: у гейта + один дом, и он у памятки, потому что её читают прежде работы; +- **как писать код** — [conventions/](conventions/README.md). Свойство, ставшее + правилом, оттуда удаляется и попадает в перечень ниже; обратный перенос + запрещён — правило, оставшееся ещё и прозой, проверяют дважды; +- **настройка конвейера ревью и журнал дефектов** — [review.md](review.md). + Оттуда берутся вопросы по темам, и перечень ниже говорит этим вопросам, чего + спрашивать уже не нужно; +- **поведение сервиса** — нормативные спеки `openspec/specs/`. У шага сверки + версий Go поведение нормировано отдельно, спекой + [toolchain](../openspec/specs/toolchain/spec.md): это единственная проверка + проекта, у которой есть своя capability. + +Домов настройки четыре: `.golangci.yml` — линтеры и форматтер, `lefthook.yml` — +проверки на pre-commit, `Taskfile.yml` — шаги гейта и их обёртки, `scripts/` — +единственный собственный скрипт проверки. Скрипты `docs.py`, `tasks.py` и +`openspec.py` живут вне репозитория, в плагинах, и Taskfile знает их путями. + +## Механизировано + +Проверяется командами из [CLAUDE.md](../CLAUDE.md); прозой не дублируется и в +промптах ревью не пересказывается. + +| Правило | Где механизировано | +| --- | --- | +| Сравнение ошибок через `errors.Is` и `errors.As`, не `==` и не приведением типа | `.golangci.yml` → `errorlint` | +| Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck`, включая присваивание в `_` (`check-blank`). Отказ, который решено не проверять, объявляют в `exclude-functions` поимённо — там сегодня `defer Close` и `send` | +| Проверка судит ответ по готовому ответу (`Result()`), а не по живой карте заголовков обработчика | `.golangci.yml` → `forbidigo` с `analyze-types`, находки только в `*_test.go`. Судит по типу приёмника (`httptest.ResponseRecorder`), поэтому ловит любую форму: цепочкой, через переменную, по индексу карты, обходом, полем `HeaderMap`. Остаётся ревью проверка, идущая мимо recorder — через свой `http.ResponseWriter` | +| Форматирование исходников | `.golangci.yml` → `gofmt` | +| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` | +| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` | +| Достижимая из кода уязвимость в зависимостях | `Taskfile.yml` → шаг `vulns` (`govulncheck ./...`) | +| Раскладка документов, битые ссылки, изменённый шаг схемы без правки `database.md` | `docs.py check`; каталог шагов задаёт ключ `migrations` в `docs/.docs.json` | +| Одно число версии Go в `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` | `Taskfile.yml` → шаг `go-version` (`scripts/check-go-version.sh`) | + +Не названное здесь место механизации означает, что проход по конвенциям будет +добросовестно проверять уже проверенное. + +## Что остаётся прозой + +**Из перечисленного в записях конвенций правилом выражено одно** — сравнение +ошибок через `errors.Is` и `errors.As` (`errorlint`, строка таблицы выше). Прозой +остаётся всё прочее: ни константный `msg` лога (`sloglint`), ни запрет +`fmt.Print*` и `os.Getenv` (`forbidigo` заведён, но правило у него одно — о том, +чем судят ответ в проверках; этих двух запретов в нём нет), ни запрет сторонних +пакетов ошибок (`depguard`), ни архитектурные тесты-сканеры. Это следующий шаг +переноса в правило: свойство, оставшееся прозой, проверяет человек на каждом +ревью заново. diff --git a/docs/conventions/README.md b/docs/conventions/README.md index 71a352e..63fdbf1 100644 --- a/docs/conventions/README.md +++ b/docs/conventions/README.md @@ -6,7 +6,8 @@ **Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее правилом линтера или тестом-сканером, отсюда **удаляется** и переезжает в -перечень «Механизировано» ниже. Причина: файл на несколько сотен строк +перечень «Механизировано» документа [../autotests.md](../autotests.md) — дома +инструментов, которые читают код. Причина: файл на несколько сотен строк размазывает внимание по тривиальному — и модель, и человек добросовестно проверят именование и не дойдут до формы решения. @@ -47,30 +48,12 @@ htmx, а здесь решено делать SPA — и перенесённы однофайловые компоненты, таблица маршрутов, состояние в экране, одна обёртка над `fetch`, показ ошибок и состояний списка. -## Механизировано +## Что из этого проверяет машина -Проверяется командами из [CLAUDE.md](../../CLAUDE.md); прозой не дублируется и в -промптах ревью не пересказывается. +Перечень правил, доведённых до проверки, и место настройки каждого — в +[../autotests.md](../autotests.md). Там же сказано, что из перечисленного в +записях осталось прозой и потому проверяется человеком на каждом ревью заново: +сегодня правилом выражено ровно одно свойство из всех записей. -| Правило | Где механизировано | -| --- | --- | -| Сравнение ошибок через `errors.Is` и `errors.As`, не `==` и не приведением типа | `.golangci.yml` → `errorlint` | -| Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck`, включая присваивание в `_` (`check-blank`). Отказ, который решено не проверять, объявляют в `exclude-functions` поимённо — там сегодня `defer Close` и `send` | -| Проверка судит ответ по готовому ответу (`Result()`), а не по живой карте заголовков обработчика | `.golangci.yml` → `forbidigo` с `analyze-types`, находки только в `*_test.go`. Судит по типу приёмника (`httptest.ResponseRecorder`), поэтому ловит любую форму: цепочкой, через переменную, по индексу карты, обходом, полем `HeaderMap`. Остаётся ревью проверка, идущая мимо recorder — через свой `http.ResponseWriter` | -| Форматирование исходников | `.golangci.yml` → `gofmt` | -| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` | -| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` | -| Достижимая из кода уязвимость в зависимостях | `Taskfile.yml` → шаг `vulns` (`govulncheck ./...`) | -| Раскладка документов, битые ссылки, изменённый шаг схемы без правки `database.md` | `docs.py check`; каталог шагов задаёт ключ `migrations` в `docs/.docs.json` | -| Одно число версии Go в `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` | `Taskfile.yml` → шаг `go-version` (`scripts/check-go-version.sh`) | - -Не названное здесь место механизации означает, что проход по конвенциям будет -добросовестно проверять уже проверенное. - -**Из перечисленного в записях правилом выражено одно** — сравнение ошибок через -`errors.Is` и `errors.As` (`errorlint`, строка таблицы выше). Прозой остаётся всё -прочее: ни константный `msg` лога (`sloglint`), ни запрет `fmt.Print*` и -`os.Getenv` (`forbidigo` заведён, но одним правилом — о том, чем судят ответ в -проверках; этих двух запретов в нём нет), ни запрет сторонних пакетов ошибок -(`depguard`), ни архитектурные тесты-сканеры. Это следующий шаг переноса в правило: свойство, -оставшееся прозой, проверяет человек на каждом ревью заново. +Здесь этот перечень не повторяется. Дом у него один, и он не тут: конвенции +говорят, как писать код, а не какими инструментами его читают. diff --git a/docs/conventions/database.md b/docs/conventions/database.md index 8e18c78..ecd097c 100644 --- a/docs/conventions/database.md +++ b/docs/conventions/database.md @@ -10,7 +10,7 @@ **Механизировано:** одно — сверка изменённого шага схемы с [../database.md](../database.md), шаг гейта `docs.py check` -([README.md](README.md), «Механизировано»). Под прочие пункты ни правила +([../autotests.md](../autotests.md), «Механизировано»). Под прочие пункты ни правила линтера, ни теста-сканера в transcriber нет. ## Первичные ключи — ULID, не автоинкремент diff --git a/docs/conventions/logging.md b/docs/conventions/logging.md index 991eeda..b820ae8 100644 --- a/docs/conventions/logging.md +++ b/docs/conventions/logging.md @@ -13,7 +13,7 @@ OpenSpec. **Механизировано:** ничего из перечисленного ниже. `forbidigo` в `.golangci.yml` включён, но правило у него одно и о другом — чем судят ответ в проверках -([README.md](README.md), «Механизировано»); `sloglint` не заведён, и ни один +([../autotests.md](../autotests.md), «Механизировано»); `sloglint` не заведён, и ни один пункт этой записи правилом не выражен. ## Принципы diff --git a/docs/review.md b/docs/review.md index fab748c..a3be27f 100644 --- a/docs/review.md +++ b/docs/review.md @@ -8,6 +8,10 @@ Разделы ниже заполнены наперёд по коду и правятся по итогам прогонов: «Типовые ложноположительные» первым прогоном уже пользовались. +Дом темы `autotests` — [autotests.md](autotests.md): что уже проверяет машина и +что из этого спрашивать больше не нужно. Вопросы ниже — то, чего машина не +проверяет. + ### Типовые узлы Рода узлов проекта и проверяемые свойства к каждому. @@ -141,8 +145,10 @@ - `autotests`: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня тестов два файла, и оба мимо конвейера. - `autotests`: судит ли проверка ответа по готовому ответу, а не по изменяемому - состоянию обработчика — класс всплывал трижды, последний раз 2026-08-12 на - уборке носителя состояния входа. + состоянию обработчика — **только там, где ответ идёт мимо recorder**, через + свой `http.ResponseWriter`. Обращение к живой карте recorder'а с + 2026-08-12 роняет гейт правилом линтера + ([autotests.md](autotests.md), «Механизировано»), и спрашивать о нём не нужно. - `security`: не открылась ли снова поверхность, которую приносит хранилище, — собственная регистрация, вход по паролю, одноразовый код, восстановление доступа, продление сессии. Всё это приходит включённым и закрывается нами