# Конвенции кода

Как мы пишем код — в отличие от `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` |

Непойманное место механизации означает, что проход по конвенциям будет
добросовестно проверять уже проверенное.
