- «Почему» не пересказывает норму словами обязательства: у оригинала есть идентификатор, у копии нет, и расходятся они при первой правке оригинала, а отступление от копии адресовать нечем - модальность рекомендательная по META-6: греп находит слово, а не нарушение — «за попыткой следует повтор» и «становится обязанностью вызывающего» описывают ход событий; утверждения о невозможности («нельзя») правилом не затрагиваются, это ISO-евская возможность в прозе - SLOG-12 починен: факт о том, что `slog` не разделяет CRITICAL и FATAL, уехал из нормы в «Почему», а норма теперь говорит то же, что заголовок
360 lines
29 KiB
Markdown
360 lines
29 KiB
Markdown
---
|
||
prefix: META
|
||
---
|
||
|
||
# Как мы ведём конвенции
|
||
|
||
Конвенция описывает повторяющийся выбор: как называть директории, как
|
||
раскладывать данные, как оформлять ошибки. Она отвечает на вопрос «как
|
||
принято», а не «что здесь происходит».
|
||
|
||
Как записывается сама конвенция — правила, модальность, обоснования — в
|
||
[LANGUAGE.md](LANGUAGE.md). Здесь — про то, зачем конвенции заводятся, где
|
||
живут и как соотносятся с соседними видами документов.
|
||
|
||
## Область действия
|
||
|
||
Правила ниже адресованы автору конвенции — тому, кто заводит новый файл или
|
||
правит существующий, в каноне и в копиях репозиториев. Обязательность живёт
|
||
на отдельном правиле, а не на файле; шкала модальных слов — в
|
||
[LANGUAGE.md](LANGUAGE.md).
|
||
|
||
## Отличие от соседей
|
||
|
||
- `docs/adr/` — **решение**, принятое однажды и постфактум («почему выбрали
|
||
Authelia, а не Keycloak»). Запись неизменяема.
|
||
- `docs/specs/` и OpenSpec, где они есть, — **что** система делает,
|
||
наблюдаемое поведение как контракт. Конвенция — **как** написан код;
|
||
в спеки она не переносится, это не capability.
|
||
- `docs/drafts/` — оперативная хроника и черновики, «что собираюсь сделать».
|
||
- `docs/conventions/` — **правило на будущее**, применяемое многократно.
|
||
Живой документ: правится, когда договорённость меняется.
|
||
|
||
## Оформление
|
||
|
||
Имя файла — kebab-case по теме: `app-directories.md`. Правилом это не
|
||
записано, и это случай META-25: регуляркой имя проверяется тривиально, но
|
||
обоснование сводится к «чтобы имя файла в реестре префиксов писалось одним
|
||
способом» — вреда от нарушения нет, значит и высшей модальности нет, а на
|
||
СЛЕДУЕТ такое правило не окупает строчку. Номер META-16, под которым оно
|
||
существовало, оставлен свободным и не переиспользуется.
|
||
|
||
## Канон и копии
|
||
|
||
Файлы в `docs/conventions/` с шапкой `origin:` — копии из общего канона
|
||
`dev-conventions`, а не собственные документы репозитория. Копия собирается
|
||
из канона целиком, поэтому репозиторное живёт ниже маркера локальной части
|
||
в конце файла: обновление сохраняет всё, что ниже маркера, и переписывает
|
||
всё, что выше. Откуда взяты копии и где брать обновления — в манифесте
|
||
`.conventions.toml` в корне репозитория.
|
||
|
||
Отдельного механизма отчёта о расхождении нет: обновление перезаписывает
|
||
файлы в рабочем дереве, а что именно изменилось, показывает `git diff` до
|
||
коммита. Поэтому в шапке копии хранится только `origin:` — отпечатков
|
||
канона и дат синхронизации в ней нет, историю держит git.
|
||
|
||
Правка выше маркера означает одно из двух: улучшение, которое переносят в
|
||
канон, или документ, переставший быть копией, — тогда `origin:` из шапки
|
||
убирают.
|
||
|
||
## Правила
|
||
|
||
### META-1. Одна конвенция — один файл
|
||
|
||
**ДОЛЖЕН.** Файл описывает ровно один повторяющийся выбор.
|
||
|
||
**Почему.** Подписка перечисляется по темам, и тему берут целиком. Файл,
|
||
собравший две темы, вынуждает репозиторий взять правила, которые ему не
|
||
нужны, и вычитывать чужую половину при каждом обновлении. Разрезать позже
|
||
дорого: перенос правила в другой файл — это новый префикс и новая
|
||
нумерация, поэтому после разреза все внешние ссылки обходят руками.
|
||
|
||
### META-2. Конвенция заводится, когда решение принимается третий раз
|
||
|
||
**СЛЕДУЕТ.** Поводом служит одно и то же решение, принятое третий раз и
|
||
каждый раз чуть по-другому.
|
||
|
||
**Почему.** По одному-двум случаям не видно, что в решении повторяется, а
|
||
что было частностью места: правило, выведенное из первого случая, кодирует
|
||
частность и дальше мешает больше, чем помогает. Единичный выбор, к тому же
|
||
спорный, читателю нужен вместе с мотивом — это ADR, где мотив и есть
|
||
содержание записи.
|
||
|
||
### META-3. Новая конвенция пишется там, где заболело
|
||
|
||
**СЛЕДУЕТ.** Первые шаги пути «находка → конвенция → правило линтера →
|
||
удаление прозы» делаются в репозитории, где случилась находка; в канон
|
||
продвигается общая часть.
|
||
|
||
**Почему.** Правило, написанное сразу в общем виде, не проверено ни одним
|
||
применением, и условие применимости у него придумано, а не найдено, —
|
||
платят за это все потребители сразу. Формулировка, обкатанная на одном
|
||
репозитории, приезжает в канон уже с известной границей.
|
||
|
||
### META-4. В тексте конвенции нет утверждений о состоянии репозитория
|
||
|
||
**НЕ ДОЛЖЕН.** Норма пишется в настоящем предписывающем времени, без
|
||
описаний того, как сейчас устроен конкретный репозиторий.
|
||
|
||
**Почему.** Такое утверждение устаревает молча и подменяет норму описанием:
|
||
читатель перестаёт понимать, что от него требуется, а что просто
|
||
констатировано. В копиях у остальных потребителей чужой факт вдобавок ложен
|
||
с первого дня. «Так сделано у нас» — содержимое региона отступлений, где оно
|
||
и локально, и проверяемо.
|
||
|
||
### META-20. Норма самодостаточна, наружу смотрит только обоснование
|
||
|
||
**ДОЛЖЕН.** Норму правила можно исполнить, имея один этот файл. Ссылка на
|
||
правило чужой темы допустима в «Почему», в «Связано» и в разграничении
|
||
области действия — но не в самой норме. Если норме нужен концепт соседней
|
||
темы, он коротко повторяется здесь, а сосед называется в «Почему» как
|
||
источник решения.
|
||
|
||
**Почему.** Репозиторий подписывается на произвольное подмножество
|
||
конвенций, и графа зависимостей у него нет по построению. Норма, которую
|
||
нельзя исполнить без отсутствующего файла, делает такое подмножество
|
||
невалидным молча: читатель видит связный текст и не замечает, что часть
|
||
нормы не определена. Обоснование, потерявшее адресата, деградирует честно —
|
||
пропадает перекрёстная проверка, смысл остаётся. Цена повтора — риск
|
||
разойтись с источником; она платится сознательно и видна, в отличие от
|
||
скрытой зависимости.
|
||
|
||
### META-21. Ссылка ведёт на тему или на правило, но не на путь в каноне
|
||
|
||
**ДОЛЖЕН.** На соседнюю конвенцию ссылаются именем темы (конвенция
|
||
`logging`), на конкретное правило — идентификатором (`SLOG-27`); путь файла
|
||
канона в тексте конвенции не употребляется.
|
||
|
||
**Почему.** В репозитории конвенция лежит собранной: слои одной темы — это
|
||
секции одного файла, и пути `lang/go/logging.md` там не существует. Ссылка
|
||
на путь канона умирает при сборке, причём молча — текст остаётся связным.
|
||
Имя темы и идентификатор правила переживают и сборку, и переезд файла между
|
||
осями. Слой своей темы поэтому называют идентификатором его правила, а не
|
||
словами «базовый слой»: слова не проверяются и не ведут к утверждению.
|
||
|
||
### META-24. Слой ссылается на идентификаторы своего базового слоя
|
||
|
||
**ДОПУСКАЕТСЯ.** Правило языкового или стекового слоя называет идентификатор
|
||
правила арх-слоя своей темы прямо в норме.
|
||
|
||
**Почему.** Подписываются темой, а не слоем: собранный файл начинается с
|
||
арх-слоя независимо от того, какие язык и стек выбраны, — базовое правило в
|
||
копии всегда рядом, и ссылка на него никуда не ведёт. Повтор его концепта
|
||
здесь заводил бы второй источник правды внутри одного документа: META-20
|
||
требует повторять концепт там, где соседнего файла может не быть, а базовый
|
||
слой отсутствовать не может. Остальные слои темы попадают в копию по
|
||
манифесту, и такой гарантии у них нет — отсюда узость разрешения. Записано
|
||
оно явно, потому что META-20 читают строже, чем он есть, и без этой строки
|
||
базу дублируют без нужды.
|
||
|
||
### META-5. Расхождение кода с правилом — отступление, а не повод переписать правило
|
||
|
||
**ДОЛЖЕН.** Правило правится, только когда неверно по существу: содержит
|
||
фактическую ошибку, внутреннее противоречие или условие применимости,
|
||
которое не даёт ответа.
|
||
|
||
**Почему.** Правило, подогнанное под текущий код, перестаёт что-либо
|
||
требовать — оно описывает то, что и так происходит, и первое же расхождение
|
||
переписывает его снова. Направление «конвенция → код» держится ровно тем,
|
||
что факт не считается аргументом.
|
||
|
||
### META-6. `ДОЛЖЕН` без механической проверки либо механизируется, либо понижается
|
||
|
||
**ДОЛЖЕН.** Правило, нарушение которого объявлено ошибкой, получает
|
||
машинную проверку или переводится в СЛЕДУЕТ.
|
||
|
||
**Почему.** Без проверки правило держится на внимании: нарушения копятся
|
||
молча и всплывают выборочно — на том ревью, куда дошли руки. Для СЛЕДУЕТ
|
||
это честно, а ДОЛЖЕН при этом обещает то, чего не делает, и через несколько
|
||
таких случаев обесценивает остальные ДОЛЖЕН в файле.
|
||
|
||
Отсюда следствие: правило, машинная проверка которого невозможна в принципе
|
||
(вкус формулировки, выбор границы, суждение о ситуации), не может быть
|
||
ДОЛЖЕН — его модальность СЛЕДУЕТ по построению, а не по слабости.
|
||
|
||
### META-25. Высшая модальность выбирается, только когда назван вред
|
||
|
||
**СЛЕДУЕТ.** Правило получает ДОЛЖЕН или НЕ ДОЛЖЕН, если в «Почему» сказано,
|
||
что́ ломается при нарушении.
|
||
|
||
**Почему.** Машинная проверка — условие необходимое (META-6), но не
|
||
достаточное: проверяемых мелочей больше, чем важных вещей, и без второго
|
||
условия единственным фильтром остаётся удобство проверки. Шкала наполняется
|
||
опрятностью, читатель перестаёт отличать «уронит прод» от «неаккуратно» — и
|
||
обесцениваются все ДОЛЖЕН в файле, ровно то, от чего META-6 защищает с
|
||
другой стороны. Само это правило машиной не проверяется: «вред назван»
|
||
устанавливается чтением, поэтому его собственная модальность по META-6 —
|
||
СЛЕДУЕТ.
|
||
|
||
### META-7. Факт механизации фиксируется в копии со ссылкой на правило
|
||
|
||
**ДОЛЖЕН.** Запись о механизации называет идентификатор правила и
|
||
конкретную проверку.
|
||
|
||
**Почему.** Механизация — состояние конкретного репозитория, канон о ней не
|
||
знает, а без записи следующий автор либо заведёт вторую проверку того же,
|
||
либо будет вычитывать глазами уже проверенное машиной. Без идентификатора
|
||
читатель догадывается сам, к какому утверждению относится проверка, — и
|
||
догадывается по-разному.
|
||
|
||
### META-8. Формулировка не удаляется из канона, пока механизирована не у всех
|
||
|
||
**НЕ ДОЛЖЕН.** Норма остаётся в каноне, пока хотя бы у одного потребителя
|
||
машинной проверки нет.
|
||
|
||
**Почему.** У кого линтера нет, тот после удаления остаётся без правила
|
||
вообще: ни проверки, ни текста. Механизация у одного потребителя ничего не
|
||
говорит о прочих, поэтому удалять по факту «у нас уже проверяется» —
|
||
значит чинить свой файл за чужой счёт.
|
||
|
||
Списка подписчиков канон по построению не знает, поэтому факт «механизировано
|
||
у всех» устанавливается обходом репозиториев вручную — это часть работы по
|
||
удалению нормы, а не то, что можно проверить машиной (см. `LANGUAGE.md`,
|
||
состояние МЕХАНИЗИРОВАНО).
|
||
|
||
### META-9. Общая механизация разрешает удалить норму из канона
|
||
|
||
**ДОПУСКАЕТСЯ.** Правило, уехавшее в общий конфиг линтера или в общую роль,
|
||
удаляется из канона.
|
||
|
||
**Почему.** Формулировка, дублирующая работающую у всех проверку,
|
||
размазывает внимание: файл на несколько сотен строк заставляет человека и
|
||
агента добросовестно вычитывать тривиальное именование и не доходить до
|
||
формы решения. Явное разрешение нужно, чтобы META-8 не читался как запрет
|
||
удалять вообще.
|
||
|
||
### META-10. Обоснование не удаляется никогда
|
||
|
||
**НЕ ДОЛЖЕН.** Блок «Почему» остаётся и после того, как норма уехала в
|
||
линтер.
|
||
|
||
**Почему.** Линтер сообщает, что нарушено, но не сообщает, зачем правило
|
||
существует. Без обоснования не видно, когда причина отпала, — проверка
|
||
продолжает работать по инерции, и возразить ей нечем, кроме как отключив.
|
||
|
||
### META-26. Обоснование объясняет, а не требует
|
||
|
||
**НЕ СЛЕДУЕТ.** Абзац «Почему» не повторяет норму словами обязательства — на
|
||
ту норму, на которую он опирается, ссылаются идентификатором.
|
||
|
||
**Почему.** Обоснование, сформулированное как требование, заводит вторую
|
||
копию нормы: у оригинала есть идентификатор, у копии нет, и расходятся они
|
||
при первой же правке оригинала. Читатель не отличает объяснение от
|
||
досказанной нормы, а отступление от копии не адресуемо — ссылаться на абзац
|
||
нечем.
|
||
|
||
Утверждения о невозможности («нельзя», «не выйдет») сюда не относятся: они
|
||
описывают, чего не бывает, а не то, что запрещено, и проза — их законное
|
||
место. Модальность здесь рекомендательная не по слабости, а по META-6:
|
||
машинная проверка даёт кандидатов, а не вердикт, потому что слова
|
||
омонимичны — «за попыткой следует повтор» и «присвоение становится
|
||
обязанностью каждого вызывающего» описывают ход событий. Вердикт остаётся за
|
||
чтением, грепу достаётся роль подсказки.
|
||
|
||
### META-11. У трудноизменяемого слоя область действия пишется явно
|
||
|
||
**ДОЛЖЕН.** Конвенция о схеме БД, формате хранения или раскладке директорий
|
||
называет, к чему применяется: к новым таблицам и миграциям, а не к
|
||
состоянию схемы.
|
||
|
||
**Почему.** Здесь не работает привычное «новое пишем правильно, старое
|
||
переезжает по мере касания»: таблица не переезжает от того, что её
|
||
потрогали. Без явной рамки правило читается как требование к текущему
|
||
состоянию, и оба исхода плохи — миграция живых данных ради опрятности либо
|
||
молчаливый вывод, что конвенция не соблюдается совсем.
|
||
|
||
### META-12. Механизируется граница изменения, а не состояние
|
||
|
||
**ДОЛЖЕН.** Проверка запрещает нарушение в новых миграциях, а не в уже
|
||
существующей схеме.
|
||
|
||
**Почему.** Проверка состояния краснеет на легаси с первого дня: её
|
||
отключают или обвешивают вечным списком исключений — и она перестаёт ловить
|
||
новое, ради чего заводилась. Проверка границы оставляет старое в покое и
|
||
делает новую ошибку невозможной.
|
||
|
||
### META-13. Список отступлений трудноизменяемого слоя — постоянный
|
||
|
||
**ДОЛЖЕН.** Перечисленные старые таблицы и раскладки живут как есть, а не
|
||
как задачи на дочистку.
|
||
|
||
**Почему.** Список, записанный долгом, требует либо мигрировать живые данные
|
||
без выгоды, либо год за годом объяснять невыполненный план. Второе кончается
|
||
тем, что список перестают вести, — и пропадает единственное место, где видно,
|
||
где именно правило не действует.
|
||
|
||
### META-14. Отступления перечисляются поимённо, со ссылкой на правила
|
||
|
||
**ДОЛЖЕН.** В локальной части копии перечислены отступления, которые уже
|
||
есть в коде, с идентификатором правила и причиной.
|
||
|
||
**Почему.** Иначе репозиторий выглядит соблюдающим конвенцию, а проверить
|
||
это можно только чтением всего кода. Со ссылками отступления счётны: видно,
|
||
сколько правил конвенции репозиторий реально не соблюдает. Пустой список при
|
||
этом почти всегда означает не отсутствие отступлений, а то, что их не искали.
|
||
|
||
### META-15. Запись об отступлении разбирается по масштабу
|
||
|
||
**ДОЛЖЕН.** Судьба записи зависит от того, что в ней сказано:
|
||
|
||
| № | Что записано | Куда идёт |
|
||
|---|---|---|
|
||
| META-15.1 | правилу не следуем в перечисленных местах, причина названа | остаётся отступлением |
|
||
| META-15.2 | правило не применяется в репозитории целиком | чинится условие применимости — в каноне |
|
||
| META-15.3 | конвенция репозиторию не нужна | копия удаляется, подписки нет |
|
||
|
||
**Почему.** Отступление описывает исключение, и по нему видно, какая часть
|
||
правила нарушена. Запись «мы это правило вообще не применяем» такой
|
||
информации не несёт и маскирует одну из двух чинимых причин: неверную рамку
|
||
правила в каноне, которую чинят один раз для всех, или лишнюю подписку,
|
||
где файл просто не нужен. Оставленная отступлением, она прячет обе.
|
||
|
||
### META-17. Ссылки на код репозитория стоят в локальной части, а не в «Связано»
|
||
|
||
**ДОЛЖЕН.** Раздел «Связано» канона содержит только ссылки, верные у всех
|
||
потребителей; ссылки на ADR, код и файлы конкретного репозитория — в
|
||
локальной части копии.
|
||
|
||
**Почему.** Текст канона приезжает ко всем потребителям, и ссылка на чужой
|
||
файл у них битая с первого дня. Ниже маркера та же ссылка никого не
|
||
задевает и переживает обновление, потому что обновление её не трогает.
|
||
|
||
### META-22. Репозиторное в копии пишется ниже маркера локальной части
|
||
|
||
**ДОЛЖЕН.** Правки репозитория вносятся ниже маркера, а не в текст,
|
||
пришедший из канона.
|
||
|
||
**Почему.** Обновление перезаписывает всё, что выше маркера, поэтому правка
|
||
там живёт до первого `pull`. Заметить пропажу можно, только вычитав
|
||
`git diff` целиком — а он в этот момент и без того полон изменений канона,
|
||
и своя строка теряется среди чужих.
|
||
|
||
### META-23. Документ, переставший быть копией, не носит `origin:`
|
||
|
||
**НЕ ДОЛЖЕН.** Файл, который развели с каноном намеренно, шапку `origin:`
|
||
не сохраняет.
|
||
|
||
**Почему.** По `origin:` решается, какие файлы пересобирать из канона. Форк,
|
||
оставивший шапку, при первом же обновлении теряет ровно то, ради чего его
|
||
заводили. Происхождение такого документа остаётся в истории коммита, где оно
|
||
никого не вводит в заблуждение.
|
||
|
||
### META-18. README директории перечисляет конвенции с однострочным описанием
|
||
|
||
**СЛЕДУЕТ.** Одна плоская таблица: файл и строка о том, про что он.
|
||
|
||
**Почему.** Подписка — это набор лежащих файлов, и без описаний вопрос
|
||
«какая из них про мой случай» решается открыванием каждой. Ценой в десяток
|
||
файлов это означает, что не открывают ни одной.
|
||
|
||
### META-19. Короткие инварианты дублируются в точку входа агента
|
||
|
||
**ДОЛЖЕН.** В `AGENTS.md` / `CLAUDE.md` едет одна строка на правило с его
|
||
идентификатором; детали остаются в конвенции.
|
||
|
||
**Почему.** Сама по себе конвенция агенту не видна: он дойдёт до неё, только
|
||
если его туда отправили, — а безусловно он читает точку входа. Строка с
|
||
идентификатором служит и напоминанием, и адресом, по которому за
|
||
подробностями идут; перенос деталей туда же вернул бы задачу поддержки двух
|
||
текстов.
|