остальные конвенции переведены на формальный язык

- 11 файлов разобраны на нумерованные правила: 220 правил в каноне, у
  каждого модальность и обязательный блок «Почему»
- классифицирующие места оформлены таблицами, файловый статус снят
  отовсюду, локальные регионы сохранены под прежними именами
This commit is contained in:
av
2026-07-25 19:17:32 +03:00
parent 7701a28df1
commit 31d0620f55
11 changed files with 2404 additions and 811 deletions
+201 -90
View File
@@ -2,28 +2,18 @@
Конвенция описывает повторяющийся выбор: как называть директории, как
раскладывать данные, как оформлять ошибки. Она отвечает на вопрос «как
принято», а не «что здесь происходит». Одна конвенция — один файл.
принято», а не «что здесь происходит».
Как записывается сама конвенция — правила, модальность, обоснования — в
[language.md](language.md). Здесь — про то, зачем они заводятся, где живут
и как соотносятся с соседними видами документов.
[language.md](language.md). Здесь — про то, зачем конвенции заводятся, где
живут и как соотносятся с соседними видами документов.
## Канон и копии
## Область действия
Файлы в этой директории с шапкой `origin:`**копии из общего канона**
`dev-conventions`, а не собственные документы репозитория. Отсюда:
- репозиторное пишется **только внутрь локальных регионов**
`<!-- local:имя --> … <!-- /local -->`: они исключены из сравнения с
каноном, и расхождение по ним — норма, а не дрейф;
- правка вне регионов означает одно из двух: улучшение, которое надо
вернуть в канон, или сознательное расхождение, записанное в ключ `local:`
шапки;
- состояние копий показывает `conv status`, различия — `conv diff`,
обновление из канона — `conv pull`; всё через раннер репозитория.
Имя региона обязательно и стабильно: перенос содержимого при обновлении
идёт по именам.
Правила ниже адресованы автору конвенции — тому, кто заводит новый файл или
правит существующий, в каноне и в копиях репозиториев. Обязательность живёт
на отдельном правиле, а не на файле; шкала модальных слов — в
[language.md](language.md).
## Отличие от соседей
@@ -36,101 +26,222 @@
- `docs/conventions/`**правило на будущее**, применяемое многократно.
Живой документ: правится, когда договорённость меняется.
## Направление: конвенция → код
## Канон и копии
Конвенция формулируется независимо от того, как устроено конкретное
приложение. Код следует конвенции, а не наоборот.
Файлы в `docs/conventions/` с шапкой `origin:` — копии из общего канона
`dev-conventions`, а не собственные документы репозитория. Репозиторное
живёт только внутри локальных регионов `<!-- local:имя --> … <!-- /local -->`:
они исключены из сравнения с каноном, и расхождение по ним — норма, а не
дрейф. Правка вне регионов означает одно из двух: улучшение, которое
возвращают в канон, или сознательное расхождение, записанное в ключ `local:`
шапки. Состояние копий показывает `conv status`, различия — `conv diff`,
обновление из канона — `conv pull`; всё через раннер репозитория.
Если код расходится с правилом — это отступление, и оно записывается в
локальный регион, а не переписывает правило. Правило меняется только тогда,
когда оно **неверно по существу**: содержит фактическую ошибку, внутреннее
противоречие или условие применимости, которое не даёт ответа.
Имя региона обязательно и стабильно: перенос содержимого при обновлении
идёт по именам, и переименование осиротит содержимое во всех копиях.
Практическое следствие: в тексте конвенции не должно быть утверждений о
текущем состоянии репозитория. «Так сделано у нас» — это регион
отступлений; норма пишется в настоящем предписывающем времени.
## Правила
## Насколько правило обязательно
### R1. Одна конвенция — один файл
Обязательность живёт **на правиле**, а не на файле: один документ почти
всегда смешивает жёсткие требования с советами, и общая пометка на нём
неизбежно врёт про часть содержимого. Шкала модальных слов — в
[language.md](language.md).
**ДОЛЖЕН.** Файл описывает ровно один повторяющийся выбор.
Правило без механической проверки держится только на внимании. Для
**СЛЕДУЕТ** это нормально, для **ДОЛЖЕН** — плохо: такое правило либо
механизируется, либо честно понижается.
**Почему.** Подписка — это набор лежащих в репозитории файлов, и берут файл
целиком. Файл, собравший две темы, вынуждает репозиторий взять правила,
которые ему не нужны, и получать шум в `diff` по чужой половине. Разрезать
позже дорого: путь файла — часть адреса правила, и после разреза внешние
ссылки указывают не туда.
## Когда заводить
### R2. Конвенция заводится, когда решение принимается третий раз
Когда одно и то же решение принимается третий раз и каждый раз чуть
по-другому. Единичный выбор — не конвенция; если он ещё и был спорным, ему
место в ADR.
**СЛЕДУЕТ.** Поводом служит одно и то же решение, принятое третий раз и
каждый раз чуть по-другому.
Путь находки: **находка → конвенция → правило линтера → удаление прозы**.
Первые два шага делаются в репозитории, где заболело; общая часть
продвигается в канон.
**Почему.** По одному-двум случаям не видно, что в решении повторяется, а
что было частностью места: правило, выведенное из первого случая, кодирует
частность и дальше мешает больше, чем помогает. Единичный выбор, к тому же
спорный, читателю нужен вместе с мотивом — это ADR, где мотив и есть
содержание записи.
## Прозой — только то, что не выражается правилом
### R3. Новая конвенция пишется там, где заболело
Как только свойство удаётся проверить машиной, его формулировка перестаёт
работать: файл на несколько сотен строк размазывает внимание по
тривиальному, и человек с агентом добросовестно проверят именование, не
дойдя до формы решения.
**СЛЕДУЕТ.** Первые шаги пути «находка → конвенция → правило линтера →
удаление прозы» делаются в репозитории, где случилась находка; в канон
продвигается общая часть.
Но удаление прозы в общем каноне устроено иначе, чем в одиночном
репозитории. Механизация — состояние **конкретного** репозитория:
**Почему.** Правило, написанное сразу в общем виде, не проверено ни одним
применением, и условие применимости у него придумано, а не найдено, —
платят за это все потребители сразу. Формулировка, обкатанная на одном
репозитории, приезжает в канон уже с известной границей.
- **из канона формулировка не удаляется**, пока правило не механизировано
у всех потребителей: иначе те, у кого линтера нет, останутся без правила;
- **факт механизации** фиксируется в локальном регионе `механизировано`
со ссылкой на номер правила и на конкретную проверку;
- когда механизация стала общей (правило уехало в общий конфиг линтера или
в общую роль), формулировка удаляется из канона одним `push`.
### R4. В тексте конвенции нет утверждений о состоянии репозитория
Обоснование правила («Почему») не удаляется никогда, даже когда сама норма
уехала в линтер: линтер сообщает, что нарушено, но не сообщает, зачем
правило существует, — а именно это нужно, чтобы понять, когда его пора
отменить.
**НЕ ДОЛЖЕН.** Норма пишется в настоящем предписывающем времени, без
описаний того, как сейчас устроен конкретный репозиторий.
## Трудноизменяемые слои
**Почему.** Такое утверждение устаревает молча и подменяет норму описанием:
читатель перестаёт понимать, что от него требуется, а что просто
констатировано. В копиях у остальных потребителей чужой факт вдобавок ложен
с первого дня. «Так сделано у нас» — содержимое региона отступлений, где оно
и локально, и проверяемо.
У схемы БД, формата хранения и раскладки директорий не работает привычное
«новое пишем правильно, старое переезжает по мере касания»: таблица не
переезжает от того, что её потрогали. Для таких конвенций:
### R5. Расхождение кода с правилом — отступление, а не повод переписать правило
- **область действия пишется явно** — «применяется к новым таблицам и
миграциям», а не к состоянию схемы;
- **механизируется граница изменения, а не состояние** — линтер запрещает
`AUTOINCREMENT` в новых миграциях, а не в существующей схеме: старое не
падает, новая ошибка невозможна;
- **список отступлений постоянный**, а не список задач на дочистку.
**ДОЛЖЕН.** Правило правится, только когда неверно по существу: содержит
фактическую ошибку, внутреннее противоречие или условие применимости,
которое не даёт ответа.
## Честный список отступлений
**Почему.** Правило, подогнанное под текущий код, перестаёт что-либо
требовать — оно описывает то, что и так происходит, и первое же расхождение
переписывает его снова. Направление «конвенция → код» держится ровно тем,
что факт не считается аргументом.
В локальном регионе перечисляем отступления, которые уже есть в коде, — со
ссылкой на номера правил. Иначе репозиторий делает вид, что конвенции
следует, а проверить это можно только чтением всего кода.
### R6. `ДОЛЖЕН` без механической проверки либо механизируется, либо понижается
Пустой список отступлений почти всегда означает, что их не искали.
**ДОЛЖЕН.** Правило, нарушение которого объявлено ошибкой, получает
машинную проверку или переводится в СЛЕДУЕТ.
Отступление — это «правилу не следуем здесь и вот почему». Если регион
разросся до «мы это правило вообще не применяем», значит либо у правила
неверно сформулировано условие применимости (чинить в каноне), либо
репозиторию не нужна эта конвенция (не подписываться).
**Почему.** Без проверки правило держится на внимании: нарушения копятся
молча и всплывают выборочно — на том ревью, куда дошли руки. Для СЛЕДУЕТ
это честно, а ДОЛЖЕН при этом обещает то, чего не делает, и через несколько
таких случаев обесценивает остальные ДОЛЖЕН в файле.
## Оформление
### R7. Факт механизации фиксируется в локальном регионе со ссылкой на номер
- Имя файла — kebab-case по теме: `app-directories.md`.
- Раздел «Связано» в конце: ADR с обоснованием, спеки, код, который эту
конвенцию механизирует. Репо-специфичная часть «Связано» — в локальном
регионе, канонические ссылки — в общем тексте.
- README директории перечисляет конвенции с однострочным описанием, чтобы
список читался без открывания файлов.
- **Короткие инварианты дублируются туда, что агент читает безусловно**
(`AGENTS.md` / `CLAUDE.md`): сама по себе конвенция агенту не видна, он
дойдёт до неё, только если его туда отправили. Детали остаются здесь,
в файл-точку-входа едет одна строка на правило с его номером.
**ДОЛЖЕН.** Регион `механизировано` называет номер правила и конкретную
проверку.
**Почему.** Механизация — состояние конкретного репозитория, канон о ней не
знает, а без записи следующий автор либо заведёт вторую проверку того же,
либо будет вычитывать глазами уже проверенное машиной. Без номера правила
читатель догадывается сам, к какому утверждению относится проверка, — и
догадывается по-разному.
### R8. Формулировка не удаляется из канона, пока механизирована не у всех
**НЕ ДОЛЖЕН.** Норма остаётся в каноне, пока хотя бы у одного потребителя
машинной проверки нет.
**Почему.** У кого линтера нет, тот после удаления остаётся без правила
вообще: ни проверки, ни текста. Механизация у одного потребителя ничего не
говорит о прочих, поэтому удалять по факту «у нас уже проверяется» —
значит чинить свой файл за чужой счёт.
### R9. Общая механизация разрешает удалить норму из канона
**ДОПУСКАЕТСЯ.** Правило, уехавшее в общий конфиг линтера или в общую роль,
удаляется из канона одним `push`.
**Почему.** Формулировка, дублирующая работающую у всех проверку,
размазывает внимание: файл на несколько сотен строк заставляет человека и
агента добросовестно вычитывать тривиальное именование и не доходить до
формы решения. Явное разрешение нужно, чтобы R8 не читался как запрет
удалять вообще.
### R10. Обоснование не удаляется никогда
**НЕ ДОЛЖЕН.** Блок «Почему» остаётся и после того, как норма уехала в
линтер.
**Почему.** Линтер сообщает, что нарушено, но не сообщает, зачем правило
существует. Без обоснования не видно, когда причина отпала, — проверка
продолжает работать по инерции, и возразить ей нечем, кроме как отключив.
### R11. У трудноизменяемого слоя область действия пишется явно
**ДОЛЖЕН.** Конвенция о схеме БД, формате хранения или раскладке директорий
называет, к чему применяется: к новым таблицам и миграциям, а не к
состоянию схемы.
**Почему.** Здесь не работает привычное «новое пишем правильно, старое
переезжает по мере касания»: таблица не переезжает от того, что её
потрогали. Без явной рамки правило читается как требование к текущему
состоянию, и оба исхода плохи — миграция живых данных ради опрятности либо
молчаливый вывод, что конвенция не соблюдается совсем.
### R12. Механизируется граница изменения, а не состояние
**ДОЛЖЕН.** Проверка запрещает нарушение в новых миграциях, а не в уже
существующей схеме.
**Почему.** Проверка состояния краснеет на легаси с первого дня: её
отключают или обвешивают вечным списком исключений — и она перестаёт ловить
новое, ради чего заводилась. Проверка границы оставляет старое в покое и
делает новую ошибку невозможной.
### R13. Список отступлений трудноизменяемого слоя — постоянный
**ДОЛЖЕН.** Перечисленные старые таблицы и раскладки живут как есть, а не
как задачи на дочистку.
**Почему.** Список, записанный долгом, требует либо мигрировать живые данные
без выгоды, либо год за годом объяснять невыполненный план. Второе кончается
тем, что список перестают вести, — и пропадает единственное место, где видно,
где именно правило не действует.
### R14. Отступления перечисляются поимённо, со ссылкой на номера правил
**ДОЛЖЕН.** В локальном регионе перечислены отступления, которые уже есть в
коде, с номером правила и причиной.
**Почему.** Иначе репозиторий выглядит соблюдающим конвенцию, а проверить
это можно только чтением всего кода. Со ссылками отступления счётны: видно,
сколько правил конвенции репозиторий реально не соблюдает. Пустой регион при
этом почти всегда означает не отсутствие отступлений, а то, что их не искали.
### R15. Запись в регионе отступлений разбирается по масштабу
**ДОЛЖЕН.** Судьба записи зависит от того, что в ней сказано:
| № | Что записано | Куда идёт |
|---|---|---|
| R15.1 | правилу не следуем в перечисленных местах, причина названа | остаётся отступлением |
| R15.2 | правило не применяется в репозитории целиком | чинится условие применимости — в каноне |
| R15.3 | конвенция репозиторию не нужна | копия удаляется, подписки нет |
**Почему.** Отступление описывает исключение, и по нему видно, какая часть
правила нарушена. Запись «мы это правило вообще не применяем» такой
информации не несёт и маскирует одну из двух чинимых причин: неверную рамку
правила в каноне, которую чинят один раз для всех, или лишнюю подписку,
где файл просто не нужен. Оставленная отступлением, она прячет обе.
### R16. Имя файла — kebab-case по теме
**СЛЕДУЕТ.** `app-directories.md`, а не вариации регистра и разделителя.
**Почему.** Имя файла — часть глобального адреса правила
(`stack/ansible/app-directories.md R4`) и значение ключа `origin` в каждой
копии. Один способ записи избавляет от нескольких написаний одного адреса,
а ошибка в адресе обнаруживается только тем, кто по нему пришёл и ничего не
нашёл.
### R17. Репо-специфичная часть «Связано» — в локальном регионе
**ДОЛЖЕН.** Раздел «Связано» стоит в конце файла; канонические ссылки — в
общем тексте, ссылки на ADR, код и файлы конкретного репозитория — в
локальном регионе.
**Почему.** Общий текст уезжает `push`-ем ко всем потребителям, и ссылка на
чужой файл у них битая с первого дня. Регион исключён из сравнения, поэтому
та же ссылка внутри него никого не задевает и не даёт вечного шума в `diff`.
### R18. README директории перечисляет конвенции с однострочным описанием
**СЛЕДУЕТ.** Одна плоская таблица: файл и строка о том, про что он.
**Почему.** Подписка — это набор лежащих файлов, и без описаний вопрос
«какая из них про мой случай» решается открыванием каждой. Ценой в десяток
файлов это означает, что не открывают ни одной.
### R19. Короткие инварианты дублируются в точку входа агента
**ДОЛЖЕН.** В `AGENTS.md` / `CLAUDE.md` едет одна строка на правило с его
номером; детали остаются в конвенции.
**Почему.** Сама по себе конвенция агенту не видна: он дойдёт до неё, только
если его туда отправили, — а безусловно он читает точку входа. Строка с
номером служит и напоминанием, и адресом, по которому за подробностями
идут; перенос деталей туда же вернул бы задачу поддержки двух текстов.
<!-- local:точки-входа -->
<!-- /local -->