# Конвенции кода Как мы пишем код — в отличие от `openspec/specs/`, который описывает, что система делает, и от [../architecture.md](../architecture.md), который описывает, как она сложена. **Прозой остаётся только то, что не выражается правилом.** Свойство, ставшее правилом линтера или тестом-сканером, отсюда **удаляется** и переезжает в перечень «Механизировано» ниже. Причина: файл на несколько сотен строк размазывает внимание по тривиальному — и модель, и человек добросовестно проверят именование и не дойдут до формы решения. Обоснование «почему именно так» живёт в [../adr/](../adr/README.md); инварианты с severity — в [CLAUDE.md](../../CLAUDE.md). ## Откуда взяты и что с расхождениями Все пять записей перенесены из проекта jellybit — тот же Go, тот же автор, те же задачи. Код transcriber написан раньше и **части правил не следует**: ключи — UUID вместо ULID, время берётся `time.Now()` по месту, лог пишется на каждом шаге и дублируется воркером, доменные ошибки проверяются приведением типа. Каждое такое место названо в своей записи строкой «*Расхождение:*». Читается оно как **долг, а не как нарушение**: правила действуют на новый код, переписывание существующего — отдельная работа. Проходу ревью строка «Расхождение» говорит, что находка на этом месте уже известна и новой не считается. ## Записи - [logging.md](logging.md) — логирование: уровень по адресату, единая логирующая точка на доменной границе, словарь полей, `ext.*`, что не логируем. - [errors.md](errors.md) — ошибки: stdlib, обёртка `%w`, `errors.Is` и `errors.As`, трансляция доменной ошибки на внешней границе, sentinel против типизированной. - [config.md](config.md) — конфигурация: TOML, секреты рендерит выкладка в файл `0600`, самодокументируемый `config.dist.toml`, проверка на старте. - [database.md](database.md) — БД и идентификаторы: время в UTC RFC 3339, TEXT ULID, разбор на входной границе, естественные ключи у деталей. - [web-ui.md](web-ui.md) — веб-UI на htmx: партиал равен странице равен фрагменту, ветвление по `HX-Request`, деградация без JS, ошибка на htmx-пути как 200 плюс фрагмент, самозавершающийся опрос, вендоринг статики. **Записана наперёд: веб-UI ещё нет.** ## Механизировано Проверяется командами из [CLAUDE.md](../../CLAUDE.md); прозой не дублируется и в промптах ревью не пересказывается. | Правило | Где механизировано | | --- | --- | | Сравнение ошибок через `errors.Is` и `errors.As`, не `==` и не приведением типа | `.golangci.yml` → `errorlint` | | Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck` (кроме `defer Close` и `send`) | | Форматирование исходников | `.golangci.yml` → `gofmt` | | Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` | | Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` | | Раскладка документов, битые ссылки, миграция без правки `database.md` | `docs.py check` | Не названное здесь место механизации означает, что проход по конвенциям будет добросовестно проверять уже проверенное. **Из перечисленного в записях правилом выражено одно** — сравнение ошибок через `errors.Is` и `errors.As` (`errorlint`, строка таблицы выше). Прозой остаётся всё прочее: ни константный `msg` лога (`sloglint`), ни запрет `fmt.Print*` и `os.Getenv` (`forbidigo`), ни запрет сторонних пакетов ошибок (`depguard`), ни архитектурные тесты-сканеры. Это следующий шаг переноса в правило: свойство, оставшееся прозой, проверяет человек на каждом ревью заново.