# Как мы ведём конвенции Конвенция описывает повторяющийся выбор: как называть директории, как раскладывать данные, как оформлять ошибки. Она отвечает на вопрос «как принято», а не «что здесь происходит». Одна конвенция — один файл. Как записывается сама конвенция — правила, модальность, обоснования — в [language.md](language.md). Здесь — про то, зачем они заводятся, где живут и как соотносятся с соседними видами документов. ## Канон и копии Файлы в этой директории с шапкой `origin:` — **копии из общего канона** `dev-conventions`, а не собственные документы репозитория. Отсюда: - репозиторное пишется **только внутрь локальных регионов** ``: они исключены из сравнения с каноном, и расхождение по ним — норма, а не дрейф; - правка вне регионов означает одно из двух: улучшение, которое надо вернуть в канон, или сознательное расхождение, записанное в ключ `local:` шапки; - состояние копий показывает `conv status`, различия — `conv diff`, обновление из канона — `conv pull`; всё через раннер репозитория. Имя региона обязательно и стабильно: перенос содержимого при обновлении идёт по именам. ## Отличие от соседей - `docs/adr/` — **решение**, принятое однажды и постфактум («почему выбрали Authelia, а не Keycloak»). Запись неизменяема. - `docs/specs/` и OpenSpec, где они есть, — **что** система делает, наблюдаемое поведение как контракт. Конвенция — **как** написан код; в спеки она не переносится, это не capability. - `docs/drafts/` — оперативная хроника и черновики, «что собираюсь сделать». - `docs/conventions/` — **правило на будущее**, применяемое многократно. Живой документ: правится, когда договорённость меняется. ## Направление: конвенция → код Конвенция формулируется независимо от того, как устроено конкретное приложение. Код следует конвенции, а не наоборот. Если код расходится с правилом — это отступление, и оно записывается в локальный регион, а не переписывает правило. Правило меняется только тогда, когда оно **неверно по существу**: содержит фактическую ошибку, внутреннее противоречие или условие применимости, которое не даёт ответа. Практическое следствие: в тексте конвенции не должно быть утверждений о текущем состоянии репозитория. «Так сделано у нас» — это регион отступлений; норма пишется в настоящем предписывающем времени. ## Насколько правило обязательно Обязательность живёт **на правиле**, а не на файле: один документ почти всегда смешивает жёсткие требования с советами, и общая пометка на нём неизбежно врёт про часть содержимого. Шкала модальных слов — в [language.md](language.md). Правило без механической проверки держится только на внимании. Для **СЛЕДУЕТ** это нормально, для **ДОЛЖЕН** — плохо: такое правило либо механизируется, либо честно понижается. ## Когда заводить Когда одно и то же решение принимается третий раз и каждый раз чуть по-другому. Единичный выбор — не конвенция; если он ещё и был спорным, ему место в ADR. Путь находки: **находка → конвенция → правило линтера → удаление прозы**. Первые два шага делаются в репозитории, где заболело; общая часть продвигается в канон. ## Прозой — только то, что не выражается правилом Как только свойство удаётся проверить машиной, его формулировка перестаёт работать: файл на несколько сотен строк размазывает внимание по тривиальному, и человек с агентом добросовестно проверят именование, не дойдя до формы решения. Но удаление прозы в общем каноне устроено иначе, чем в одиночном репозитории. Механизация — состояние **конкретного** репозитория: - **из канона формулировка не удаляется**, пока правило не механизировано у всех потребителей: иначе те, у кого линтера нет, останутся без правила; - **факт механизации** фиксируется в локальном регионе `механизировано` — со ссылкой на номер правила и на конкретную проверку; - когда механизация стала общей (правило уехало в общий конфиг линтера или в общую роль), формулировка удаляется из канона одним `push`. Обоснование правила («Почему») не удаляется никогда, даже когда сама норма уехала в линтер: линтер сообщает, что нарушено, но не сообщает, зачем правило существует, — а именно это нужно, чтобы понять, когда его пора отменить. ## Трудноизменяемые слои У схемы БД, формата хранения и раскладки директорий не работает привычное «новое пишем правильно, старое переезжает по мере касания»: таблица не переезжает от того, что её потрогали. Для таких конвенций: - **область действия пишется явно** — «применяется к новым таблицам и миграциям», а не к состоянию схемы; - **механизируется граница изменения, а не состояние** — линтер запрещает `AUTOINCREMENT` в новых миграциях, а не в существующей схеме: старое не падает, новая ошибка невозможна; - **список отступлений постоянный**, а не список задач на дочистку. ## Честный список отступлений В локальном регионе перечисляем отступления, которые уже есть в коде, — со ссылкой на номера правил. Иначе репозиторий делает вид, что конвенции следует, а проверить это можно только чтением всего кода. Пустой список отступлений почти всегда означает, что их не искали. Отступление — это «правилу не следуем здесь и вот почему». Если регион разросся до «мы это правило вообще не применяем», значит либо у правила неверно сформулировано условие применимости (чинить в каноне), либо репозиторию не нужна эта конвенция (не подписываться). ## Оформление - Имя файла — kebab-case по теме: `app-directories.md`. - Раздел «Связано» в конце: ADR с обоснованием, спеки, код, который эту конвенцию механизирует. Репо-специфичная часть «Связано» — в локальном регионе, канонические ссылки — в общем тексте. - README директории перечисляет конвенции с однострочным описанием, чтобы список читался без открывания файлов. - **Короткие инварианты дублируются туда, что агент читает безусловно** (`AGENTS.md` / `CLAUDE.md`): сама по себе конвенция агенту не видна, он дойдёт до неё, только если его туда отправили. Детали остаются здесь, в файл-точку-входа едет одна строка на правило с его номером.