- правило = номер + модальность (ДОЛЖЕН / СЛЕДУЕТ / ДОПУСКАЕТСЯ) плюс обязательный блок «Почему»; из OpenSpec взяты идентификаторы правил, но не SHALL и не GIVEN/WHEN/THEN - файловый статус отменён: обязательность живёт на правиле, а в шапке ещё не переведённых конвенций ключ остаётся меткой «это проза»
137 lines
10 KiB
Markdown
137 lines
10 KiB
Markdown
# Как мы ведём конвенции
|
||
|
||
Конвенция описывает повторяющийся выбор: как называть директории, как
|
||
раскладывать данные, как оформлять ошибки. Она отвечает на вопрос «как
|
||
принято», а не «что здесь происходит». Одна конвенция — один файл.
|
||
|
||
Как записывается сама конвенция — правила, модальность, обоснования — в
|
||
[language.md](language.md). Здесь — про то, зачем они заводятся, где живут
|
||
и как соотносятся с соседними видами документов.
|
||
|
||
## Канон и копии
|
||
|
||
Файлы в этой директории с шапкой `origin:` — **копии из общего канона**
|
||
`dev-conventions`, а не собственные документы репозитория. Отсюда:
|
||
|
||
- репозиторное пишется **только внутрь локальных регионов**
|
||
`<!-- local:имя --> … <!-- /local -->`: они исключены из сравнения с
|
||
каноном, и расхождение по ним — норма, а не дрейф;
|
||
- правка вне регионов означает одно из двух: улучшение, которое надо
|
||
вернуть в канон, или сознательное расхождение, записанное в ключ `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`): сама по себе конвенция агенту не видна, он
|
||
дойдёт до неё, только если его туда отправили. Детали остаются здесь,
|
||
в файл-точку-входа едет одна строка на правило с его номером.
|
||
|
||
<!-- local:точки-входа -->
|
||
<!-- /local -->
|