diff --git a/.golangci.yml b/.golangci.yml index ad7da6b..8a03249 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,4 @@ -# Линтеры проекта. Перечень правил и их дома — docs/autotests.md, +# Линтеры проекта. Перечень правил и их дома — docs/conventions/go-linters.md, # «Механизировано»; здесь только настройка и «почему именно так». # # Базовый набор v2 (`default: standard`) — errcheck, govet, ineffassign, diff --git a/CLAUDE.md b/CLAUDE.md index a1a6d47..9be0c77 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -105,9 +105,10 @@ task gate # весь набор проверок разом - **Команда целиком:** `task gate`. База диффа — переменная `BASE`, по умолчанию `origin/master`; переопределяется `task gate BASE=`. -- **Какое правило чем проверяется** — [docs/autotests.md](docs/autotests.md), дом - темы `autotests`. Здесь семантика гейта, там перечень правил и место настройки - каждого; перечень здесь не повторяется. +- **Какое правило чем проверяется** — конвенция + [docs/conventions/go-linters.md](docs/conventions/go-linters.md). Здесь + семантика гейта, там перечень правил, подавлений и место настройки каждого; + перечень здесь не повторяется. - **Где логи шагов:** вывод команды, отдельного файла нет. - **Что означает каждый исход:** ненулевой код любого шага роняет гейт. У `docs.py check`, `tasks.py check`, `openspec.py check` и diff --git a/docs/conventions/README.md b/docs/conventions/README.md index 4dcff65..eecd036 100644 --- a/docs/conventions/README.md +++ b/docs/conventions/README.md @@ -6,8 +6,8 @@ **Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее правилом линтера или тестом-сканером, отсюда **удаляется** и переезжает в -перечень «Механизировано» документа [../autotests.md](../autotests.md) — дома -инструментов, которые читают код. Причина: файл на несколько сотен строк +перечень «Механизировано» записи [go-linters.md](go-linters.md) — дома правил, +которыми машина читает код. Причина: файл на несколько сотен строк размазывает внимание по тривиальному — и модель, и человек добросовестно проверят именование и не дойдут до формы решения. @@ -48,14 +48,15 @@ htmx, а здесь решено делать SPA — и перенесённы - [web-ui.md](web-ui.md) — веб-UI: Vue 3 с Vite и статикой в бинарнике, однофайловые компоненты, таблица маршрутов, состояние в экране, одна обёртка над `fetch`, показ ошибок и состояний списка. +- [go-linters.md](go-linters.md) — линтеры и механизированные проверки: лестница + механизации, два круга (pre-commit и гейт), перечень правил и подавлений, + порядок заведения нового правила. Про инструменты, а не про то, как писать + тесты. ## Что из этого проверяет машина -Перечень правил, доведённых до проверки, и место настройки каждого — в -[../autotests.md](../autotests.md). Там же сказано, что из перечисленного в +Перечень правил, доведённых до проверки, и место настройки каждого — в записи +[go-linters.md](go-linters.md). Там же сказано, что из перечисленного в прочих записях осталось прозой и потому проверяется человеком на каждом ревью заново, и там же названы остатки правил — то, что правило не ловит. Числа механизированного здесь нет намеренно: оно протухает при каждом новом правиле. - -Здесь этот перечень не повторяется. Дом у него один, и он не тут: конвенции -говорят, как писать код, а не какими инструментами его читают. diff --git a/docs/conventions/config.md b/docs/conventions/config.md index 3e724e5..f6f4c43 100644 --- a/docs/conventions/config.md +++ b/docs/conventions/config.md @@ -9,7 +9,7 @@ проверки пустых ключей внутри адаптеров. **Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml` -([../autotests.md](../autotests.md), «Механизировано»). Он держит правило «настройки +([go-linters.md](go-linters.md), «Механизировано»). Он держит правило «настройки приезжают из TOML»; `godotenv` в `main.go` по-прежнему загружает `.env`, но кладёт его в окружение процесса, а не в настройки приложения. diff --git a/docs/conventions/database.md b/docs/conventions/database.md index 32f7ec2..0964e26 100644 --- a/docs/conventions/database.md +++ b/docs/conventions/database.md @@ -12,7 +12,7 @@ [../database.md](../database.md) (`docs.py check`), чтение времени единой точкой (`forbidigo` плюс `internal/clock`) и согласованность колонок очереди (тест-сканер `internal/archrules`). Прочие пункты — прозой; адреса — -[../autotests.md](../autotests.md), «Механизировано». +[go-linters.md](go-linters.md), «Механизировано». ## Первичные ключи — ULID, не автоинкремент diff --git a/docs/conventions/errors.md b/docs/conventions/errors.md index c00c318..f0c0780 100644 --- a/docs/conventions/errors.md +++ b/docs/conventions/errors.md @@ -12,7 +12,7 @@ **Механизировано:** приведение типа и `err == ErrX` ловит `errorlint`, сторонние пакеты ошибок — `depguard`, узнавание ошибки по тексту сообщения — тест-сканер `internal/archrules`. Перечень и адреса — -[../autotests.md](../autotests.md), «Механизировано». +[go-linters.md](go-linters.md), «Механизировано». ## Базовая идиома: stdlib diff --git a/docs/autotests.md b/docs/conventions/go-linters.md similarity index 83% rename from docs/autotests.md rename to docs/conventions/go-linters.md index 50e8fd2..0521411 100644 --- a/docs/autotests.md +++ b/docs/conventions/go-linters.md @@ -1,37 +1,44 @@ -# Автопроверки +# Линтеры и механизированные проверки -Чем машина судит код этого проекта: какие свойства доведены до проверки, чем -каждое проверяется, когда оно запускается и что остаётся человеку. Документ — -дом темы ревью `autotests`: проход, которому эта тема досталась, читает его, а не -перечисляет инструменты по памяти. +Конвенция о том, **чем машина читает наш код**: какие свойства доведены до +правила, чем каждое проверяется, когда оно запускается и что осталось человеку. +Свойство, ставшее правилом, из прозы соседних записей удаляется и появляется +здесь строкой — эта запись его принимает. -Документ **переносимый**: устройство ниже — не особенность transcriber, а способ -вести автопроверки в Go-проекте, и разделы «Лестница механизации», «Два круга» и -«Как заводят новое правило» переносятся в другой проект как есть. Своё здесь — -перечень правил и подавлений; он назван так, чтобы отличать переносимое от -местного. +**Чего здесь нет: как писать тесты.** Запись говорит об инструментах и правилах — +линтерах, тестах-сканерах, шагах проверок, — а не о том, что должен утверждать +юнит-тест и какой у него оракул. Это другой предмет, и живёт он в +[../review.md](../review.md): «Типовые узлы» перечисляют свойства, которые тест +обязан проверять, и там же записано требование, чтобы проверка была **способна +упасть**. Тест-сканеры ниже попадают в эту запись не потому, что они тесты, а +потому, что они правила: у них нет ни фикстур, ни поведения — они читают +исходники. -Тема заведена 2026-08-13. Прежде перечень лежал разделом «Механизировано» в -[conventions/README.md](conventions/README.md), и это был чужой дом: конвенции -говорят, **как писать код**, а здесь речь об инструментах, которые его читают. +Пока язык у проекта один, и запись названа по нему. Появится второй — у него +будет своя запись, а лестница и два круга останутся общими. -## Границы дома +Устройство ниже **переносимо**: разделы «Лестница механизации», «Два круга» и +«Как заводят новое правило» — не особенность transcriber и переносятся в другой +Go-проект как есть. Своё здесь — перечень правил и подавлений. -Что здесь есть и чего здесь нет — чтобы факт не жил в двух местах: +## Границы: где что живёт + +Чтобы факт не жил в двух местах: - **семантика гейта** — команда целиком, база диффа, словарь кодов выхода, что красит безусловно, чего в гейте намеренно нет и кто тогда обязан это гонять — - в [CLAUDE.md](../CLAUDE.md), раздел «Гейт». Здесь это не повторяется: у гейта + в [CLAUDE.md](../../CLAUDE.md), раздел «Гейт». Здесь это не повторяется: у гейта один дом, и он у памятки, потому что её читают прежде работы; -- **как писать код** — [conventions/](conventions/README.md). Свойство, ставшее - правилом, оттуда удаляется и попадает в перечень ниже; обратный перенос - запрещён — правило, оставшееся ещё и прозой, проверяют дважды; -- **настройка конвейера ревью и журнал дефектов** — [review.md](review.md). - Оттуда берутся вопросы по темам, и перечень ниже говорит этим вопросам, чего +- **как писать код** — соседние записи этой конвенции ([README.md](README.md) — + индекс). Свойство, ставшее правилом, оттуда удаляется и попадает в перечень + ниже; обратный перенос запрещён — правило, оставшееся ещё и прозой, проверяют + дважды; +- **настройка конвейера ревью, вопросы по темам и журнал дефектов** — + [../review.md](../review.md). Перечень ниже говорит этим вопросам, чего спрашивать уже не нужно; - **поведение сервиса** — нормативные спеки `openspec/specs/`. У шага сверки версий Go поведение нормировано отдельно, спекой - [toolchain](../openspec/specs/toolchain/spec.md): это единственная проверка + [toolchain](../../openspec/specs/toolchain/spec.md): это единственная проверка проекта, у которой есть своя capability, и потому единственная, чьи сценарии проверяются построчно (`scripts/check_go_version_test.go`). @@ -77,7 +84,7 @@ | Что делает с находкой | `gofmt` правит и добавляет в коммит, прочее роняет коммит | роняет прогон | Перечень работ pre-commit и то, что остаётся только гейту, — в -[CLAUDE.md](../CLAUDE.md), раздел «Гейт». Здесь важен принцип: **pre-commit не +[CLAUDE.md](../../CLAUDE.md), раздел «Гейт». Здесь важен принцип: **pre-commit не подменяет гейт**. Он ловит дешёвое и местное, а сборка, тесты целиком, сверки документов и запрос к базе уязвимостей идут в гейте — иначе коммит стоил бы минуту, и хук отключили бы через день. @@ -87,7 +94,7 @@ ## Механизировано -Проверяется командами из [CLAUDE.md](../CLAUDE.md); прозой не дублируется и в +Проверяется командами из [CLAUDE.md](../../CLAUDE.md); прозой не дублируется и в промптах ревью не пересказывается. ### Ошибки и отказы @@ -113,7 +120,7 @@ | Правило | Где механизировано | | --- | --- | | Время читают `clock.Now` (метка, UTC) и `clock.Start` (длительность, монотонные часы) — не `time.Now` по месту | `.golangci.yml` → `forbidigo`; единая точка — `internal/clock` | -| Вывод идёт через `slog`, а не `fmt.Print*` и не встроенными `print`/`println` | `.golangci.yml` → `forbidigo`. Не ловит `fmt.Fprintln(os.Stdout, …)` — первый аргумент по имени функции не судится; остаток прозой в [conventions/logging.md](conventions/logging.md) | +| Вывод идёт через `slog`, а не `fmt.Print*` и не встроенными `print`/`println` | `.golangci.yml` → `forbidigo`. Не ловит `fmt.Fprintln(os.Stdout, …)` — первый аргумент по имени функции не судится; остаток прозой в [logging.md](logging.md) | | Конфигурация приезжает из TOML, а не из окружения | `.golangci.yml` → `forbidigo`: `os.Getenv`, `os.LookupEnv`, `os.Environ`, `os.ExpandEnv` — все четыре, иначе запрет обходится соседним именем | | Форма вызова `slog`: только пары «ключ-значение», атрибуты (`slog.String` и прочие) не употребляются вовсе; `msg` — константа | `.golangci.yml` → `sloglint` (`kv-only` запрещает атрибуты целиком, а не только смешение) | @@ -181,7 +188,7 @@ - направление «транспорт не знает адаптера»: сегодня оно нарушено осознанно — `controller/http` импортирует адаптер хранилища, потому что HTTP-поверхность и есть роутер этого хранилища. Изъятие названо в - [architecture.md](architecture.md), «Принципы», и правила на это направление + [../architecture.md](../architecture.md), «Принципы», и правила на это направление нет. Отдельно названы **правила, чей подъём отклонён**: @@ -214,7 +221,7 @@ 4. **Проверить мутацией.** Внести ровно то нарушение, против которого правило написано, и убедиться, что проверка краснеет и называет место. Правило, принятое молчанием инструмента, — это не правило: прецеденты есть, и записаны - они в [review.md](review.md) (журнал 2026-08-11 про недостижимую норму, + они в [../review.md](../review.md) (журнал 2026-08-11 про недостижимую норму, 2026-08-13 про обходимый текстовый запрет). 5. **Записать строкой здесь** и удалить прозу из конвенции, если правило её заменило. diff --git a/docs/conventions/logging.md b/docs/conventions/logging.md index 0de411c..7adeefc 100644 --- a/docs/conventions/logging.md +++ b/docs/conventions/logging.md @@ -17,7 +17,7 @@ OpenSpec. `forbidigo`; вывод в stdout через `fmt.Fprintln(os.Stdout, …)` правилом не ловится и остаётся прозой этой записи. Прозой остаются также уровень по адресату, единая логирующая точка и словарь имён полей: оракула у них нет. Адреса — -[../autotests.md](../autotests.md), «Механизировано». +[go-linters.md](go-linters.md), «Механизировано». ## Принципы diff --git a/docs/review.md b/docs/review.md index 9fd512d..fe4cd76 100644 --- a/docs/review.md +++ b/docs/review.md @@ -8,9 +8,10 @@ Разделы ниже заполнены наперёд по коду и правятся по итогам прогонов: «Типовые ложноположительные» первым прогоном уже пользовались. -Дом темы `autotests` — [autotests.md](autotests.md): что уже проверяет машина и -что из этого спрашивать больше не нужно. Вопросы ниже — то, чего машина не -проверяет. +Что уже проверяет машина и о чём поэтому спрашивать не нужно — конвенция +[conventions/go-linters.md](conventions/go-linters.md). Вопросы ниже — то, чего +машина не проверяет; свойства, которые обязан проверять тест, — в «Типовых +узлах». ### Типовые узлы @@ -148,7 +149,8 @@ состоянию обработчика — **только там, где ответ идёт мимо recorder**, через свой `http.ResponseWriter`. Обращение к живой карте recorder'а с 2026-08-12 роняет гейт правилом линтера - ([autotests.md](autotests.md), «Механизировано»), и спрашивать о нём не нужно. + ([conventions/go-linters.md](conventions/go-linters.md), «Механизировано»), и + спрашивать о нём не нужно. - `security`: не открылась ли снова поверхность, которую приносит хранилище, — собственная регистрация, вход по паролю, одноразовый код, восстановление доступа, продление сессии. Всё это приходит включённым и закрывается нами @@ -255,7 +257,7 @@ API и имя не откатываются обратной правкой по но матчинг по тексту не видит; прозой это правило записано не было, и ревью его не спрашивало - **Что меняем:** узнавание переведено на `errors.Is(err, io.EOF)`; класс закрыт - тестом-сканером (docs/autotests.md, «Ошибки и отказы») + тестом-сканером (docs/conventions/go-linters.md, «Ошибки и отказы») ## 2026-08-13 — правило гейта обходилось одной лишней строкой [пойман ревью] @@ -275,8 +277,8 @@ API и имя не откатываются обратной правкой по которого писали. Мутация была, но одна — нужна была по одной на каждую форму - **Что меняем:** правило судит по типу приёмника (`analyze-types`, `httptest.ResponseRecorder.Header` и `.HeaderMap`) и ловит все шесть форм; - проверено мутацией по каждой. Отсюда же строка в docs/autotests.md, «Лестница - механизации»: запрет по имени, обходимый лишней строкой, — это ступень + проверено мутацией по каждой. Отсюда же строка в + docs/conventions/go-linters.md, «Лестница механизации»: запрет по имени, обходимый лишней строкой, — это ступень тест-сканера, наряженная запретом ## 2026-08-12 — закрыли поверхность так, что войти не мог никто [пойман ревью] diff --git a/internal/archrules/arch_test.go b/internal/archrules/arch_test.go index acda330..396d6f0 100644 --- a/internal/archrules/arch_test.go +++ b/internal/archrules/arch_test.go @@ -4,7 +4,7 @@ // // Каждое правило здесь — бывшая строка прозы: у него есть детерминированный // оракул, поэтому ему место в наборе проверок, а не в промпте ревью. Перечень -// механизированного — docs/autotests.md. +// механизированного — docs/conventions/go-linters.md. // // Пакет тестовый целиком: рабочего кода в нём нет и быть не должно. package archrules diff --git a/lefthook.yml b/lefthook.yml index 7695ab3..682f50a 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -4,7 +4,7 @@ # Предкоммитные проверки — дешёвая часть гейта на **затронутых файлах**. Полный # набор здесь не гоняется намеренно: он идёт минуты, а pre-commit обязан быть # быстрым. Что ловит pre-commit и что остаётся только гейту — CLAUDE.md, -# раздел «Гейт»; перечень правил и их дома — docs/autotests.md. +# раздел «Гейт»; перечень правил и их дома — docs/conventions/go-linters.md. templates: av-hooks-dir: "/home/av/projects/private/git-hooks"