заведён канон общих конвенций для личных проектов

- 13 конвенций по осям arch / lang / stack / common; репозитории берут
  оттуда копии в свой docs/conventions/ и коммитят их у себя
- conv — синхронизация копий: add / status / diff / pull / push, локальные
  регионы исключены из сравнения, поэтому расхождение не даёт шума
This commit is contained in:
av
2026-07-25 18:18:18 +03:00
commit 4a59c71737
15 changed files with 2142 additions and 0 deletions
+135
View File
@@ -0,0 +1,135 @@
---
status: обязательная
---
# Как мы ведём конвенции
Конвенция описывает повторяющийся выбор: как называть директории, как
раскладывать данные, как оформлять ошибки. Она отвечает на вопрос «как
принято», а не «что здесь происходит». Одна конвенция — один файл.
Механической проверки у самой этой конвенции нет — осознанное исключение:
проверять «правильно ли написана конвенция» нечем, а обязательный статус
нужен, чтобы правила ниже не обсуждались заново в каждом репозитории.
## Канон и копии
Файлы в этой директории с шапкой `origin:`**копии из общего канона**
`dev-conventions`, а не собственные документы репозитория. Отсюда:
- репозиторное пишется **только внутрь локальных регионов**
`<!-- local:имя --> … <!-- /local -->`: они исключены из сравнения с
каноном, и расхождение по ним — норма, а не дрейф;
- правка вне регионов означает одно из двух: улучшение, которое надо
вернуть в канон, или сознательное расхождение, записанное в ключ `local:`
шапки;
- состояние копий показывает `conv status`, различия — `conv diff`,
обновление из канона — `conv pull`; всё через раннер репозитория.
Имя региона обязательно и стабильно: перенос содержимого при обновлении
идёт по именам.
## Отличие от соседей
- `docs/adr/`**решение**, принятое однажды и постфактум («почему выбрали
Authelia, а не Keycloak»). Запись неизменяема.
- `docs/specs/` и OpenSpec, где они есть, — **что** система делает,
наблюдаемое поведение как контракт. Конвенция — **как** написан код;
в спеки она не переносится, это не capability.
- `docs/drafts/` — оперативная хроника и черновики, «что собираюсь сделать».
- `docs/conventions/`**правило на будущее**, применяемое многократно.
Живой документ: правится, когда договорённость меняется.
## Направление: конвенция → код
Конвенция формулируется независимо от того, как устроено конкретное
приложение. Код следует конвенции, а не наоборот.
Если код расходится с правилом — это отступление, и оно записывается в
локальный регион, а не переписывает правило. Правило меняется только тогда,
когда оно **неверно по существу**: содержит фактическую ошибку, внутреннее
противоречие или условие применимости, которое не даёт ответа.
Практическое следствие: в тексте конвенции не должно быть утверждений о
текущем состоянии репозитория. «Так сделано у нас» — это регион
отступлений; норма пишется в настоящем предписывающем времени.
## Статус
Каждая конвенция объявляет статус в шапке:
- **рекомендуемая** — так стоит делать в новом коде; существующий переезжает
по мере касания, отдельной кампанией не переписывается;
- **обязательная** — нарушение считается ошибкой; по возможности проверяется
линтером или хуком, а не вниманием.
Конвенция без механической проверки держится только на внимании — это
нормально для рекомендуемой и плохо для обязательной.
## Когда заводить
Когда одно и то же решение принимается третий раз и каждый раз чуть
по-другому. Единичный выбор — не конвенция; если он ещё и был спорным, ему
место в ADR.
Путь находки: **находка → конвенция → правило линтера → удаление прозы**.
Первые два шага делаются в репозитории, где заболело; общая часть
продвигается в канон.
## Прозой — только то, что не выражается правилом
Как только свойство удаётся проверить машиной, его формулировка перестаёт
работать: файл на несколько сотен строк размазывает внимание по
тривиальному, и человек с агентом добросовестно проверят именование, не
дойдя до формы решения.
Но удаление прозы в общем каноне устроено иначе, чем в одиночном
репозитории. Механизация — состояние **конкретного** репозитория:
- **из канона формулировка не удаляется**, пока правило не механизировано
у всех потребителей: иначе те, у кого линтера нет, останутся без правила;
- **факт механизации** фиксируется в локальном регионе `механизировано`
со ссылкой на конкретное правило;
- когда механизация стала общей (правило уехало в общий конфиг линтера или
в общую роль), формулировка удаляется из канона одним `push`.
## Трудноизменяемые слои
У схемы БД, формата хранения и раскладки директорий шкала
«рекомендуемая → переезжает по мере касания» не работает: таблица не
переезжает от того, что её потрогали. Для таких конвенций:
- **область действия пишется явно** — «применяется к новым таблицам и
миграциям», а не к состоянию схемы;
- **механизируется граница изменения, а не состояние** — линтер запрещает
`AUTOINCREMENT` в новых миграциях, а не в существующей схеме: старое не
падает, новая ошибка невозможна;
- **список отступлений постоянный**, а не список задач на дочистку.
## Честный список отступлений
В локальном регионе перечисляем отступления, которые уже есть в коде, —
иначе репозиторий делает вид, что правилу следует. У рекомендуемой
конвенции пустой список отступлений почти всегда означает, что их просто не
искали.
Отступление — это «правилу не следуем здесь и вот почему». Если регион
разросся до «мы это правило вообще не применяем», значит либо у правила
неверно сформулировано условие применимости (чинить в каноне), либо
репозиторию не нужна эта конвенция (не подписываться).
## Оформление
- Имя файла — kebab-case по теме: `app-directories.md`.
- Раздел «Связано» в конце: ADR с обоснованием, спеки, код, который эту
конвенцию механизирует. Репо-специфичная часть «Связано» — в локальном
регионе, канонические ссылки — в общем тексте.
- README директории перечисляет конвенции с однострочным описанием, чтобы
список читался без открывания файлов.
- **Короткие инварианты дублируются туда, что агент читает безусловно**
(`AGENTS.md` / `CLAUDE.md`): сама по себе конвенция агенту не видна, он
дойдёт до неё, только если его туда отправили. Детали остаются здесь,
в файл-точку-входа едет одна строка на правило.
<!-- local:точки-входа -->
<!-- /local -->