# Конвенции кода Как мы пишем код — в отличие от `openspec/specs/`, который описывает, что система делает, и от [../architecture.md](../architecture.md), который описывает, как она сложена. **Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее правилом линтера или тестом-сканером, отсюда **удаляется** и переезжает в перечень «Механизировано» ниже. Причина: файл на несколько сотен строк размазывает внимание по тривиальному — и модель, и человек добросовестно проверят именование и не дойдут до формы решения. Процедура промоута — `references/promote.md` скилла `av-dev-pipeline:review-pipeline`. Обоснование «почему именно так» живёт в [../adr/](../adr/README.md); инварианты с severity — в [CLAUDE.md](../../CLAUDE.md). ## Записи - [logging.md](logging.md) — логирование: уровень по адресату, единственный логирующий чекпоинт, поля, `ext.*`, что не логируем. - [errors.md](errors.md) — ошибки: stdlib, обёртка `%w`, `errors.Is`/`As`, трансляция доменной ошибки на внешней границе, sentinel против типизированной. - [config.md](config.md) — конфигурация: TOML, секреты рендерит деплой в файл `0600`, самодокументируемый `config.example.toml`, валидация на старте. - [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT ULID через `internal/ident`, `ident.Parse` на входной границе, естественные ключи у деталей. - [web-ui.md](web-ui.md) — веб-UI на htmx: единый партиал = страница = фрагмент, ветвление по `isHTMX`, деградация без JS, ошибка на htmx-пути = 200 + фрагмент, самозавершающийся поллинг, вендоринг и кэш статики. ## Механизировано Проверяется `task gate`; прозой не дублируется и в промптах ревью не пересказывается. | Правило | Где механизировано | | --- | --- | | `msg` лога — константная категория, данные в полях, единый стиль ключ-значение | `.golangci.yml` → `sloglint` (`static-msg`, `kv-only`, `no-mixed-args`) | | В stdout напрямую не пишем (`fmt.Print*`) | `.golangci.yml` → `forbidigo` | | Конфигурация только из TOML, `os.Getenv` для конфига не используем | `.golangci.yml` → `forbidigo` | | Время только через `store.Now()` — `time.Now` запрещён вне `internal/{ident,store}` | `.golangci.yml` → `forbidigo` | | Сравнение ошибок через `errors.Is`/`As`, не `==` и не приведением типа | `.golangci.yml` → `errorlint` | | Ошибки — только stdlib (`github.com/pkg/errors`, `cockroachdb/errors` запрещены) | `.golangci.yml` → `depguard` | | Опечатки в тексте | `.golangci.yml` → `misspell` | | Транспорты не зависят друг от друга | `internal/archrules` → `TestТранспортыНеЗависятДругОтДруга` | | Ядро не зависит от транспортов | `internal/archrules` → `TestЯдроНеЗависитОтТранспортов` | | Миграции без `AUTOINCREMENT` и без серверного времени | `internal/archrules` → `TestМиграцииБезAutoincrementИСерверногоВремени` | | Ошибки не матчатся по тексту сообщения | `internal/archrules` → `TestОшибкиНеМатчатсяПоТексту` | | Покрытие изменённых строк, секреты в диффе, миграция без правки `database.md` | `scripts/gate.py`, `scripts/diff-coverage.py`, `docs.py check` | Непойманное место механизации означает, что проход по конвенциям будет добросовестно проверять уже проверенное.