# Канон конвенций Общие конвенции для личных проектов. Репозитории берут отсюда копии в свой `docs/conventions/`, коммитят их и живут дальше самостоятельно — как с `ansible-roles`: канон не источник истины во время работы, а лавка, из которой берут и в которую возвращают улучшения. Сами конвенции лежат в `conventions/`, обвязка — в корне: | Файл | Что описывает | |---|---| | `README.md` | устройство канона, оси, синхронизация, жизненный цикл | | [LANGUAGE.md](LANGUAGE.md) | язык записи правил: номера, модальность, «Почему» | | [GUIDE.md](GUIDE.md) | как ведут конвенции: когда заводить, механизация, отступления | | `conv` | синхронизация копий | Обвязка живёт только в каноне и в репозитории не оказывается — `conv` синхронизирует лишь содержимое `conventions/`. Пока это осознанное ограничение: копия конвенции ссылается на `LANGUAGE.md` как на внешний документ. Правило то же, что у ролей: **деплоится и читается только то, что лежит в git репозитория**. Канон никем не подключается на лету. ## Направление — конвенция → код Конвенция формулируется независимо от конкретного приложения. Она задаёт правило; код ему следует. Обратное направление запрещено: то, что приложение уже делает иначе, **не является аргументом против правила** — это отступление, и его место в локальном регионе того репозитория, а не в переформулировке канона. Отсюда практические следствия: - в каноне нет утверждений о том, как что-то устроено в конкретном репозитории («у нас так в девяти плейбуках из тридцати трёх») — только нормы и условия их применимости; - в каноне нет списка, кто на что подписан: подписка — свойство репозитория, а не конвенции; - расхождение канона с кодом чинится либо кодом, либо честной записью отступления, либо — если правило оказалось неверным — правкой правила по существу, а не подгонкой под факт. ## Оси ``` conventions/ arch/ решения, переживающие смену языка и инструментов lang/<язык>/ как решение реализуется и механизируется в языке stack/<стек>/ привязка к инструменту, хранилищу, транспорту ``` Пути правил даются относительно `conventions/`: адрес `arch/db-identifiers.md R5`, а не `conventions/arch/…` — так же, как они лягут в `docs/conventions/` репозитория. Тест — по тому, замена чего убивает правило: > Умирает при смене **языка** → `lang/`. Умирает при смене **инструмента, > хранилища или транспорта** → `stack/`. Не умирает ни от того, ни от > другого → `arch/`. `PK — ULID, генерирует приложение` не умирает ни от чего — это лежит в данных → `arch/`. `internal/ident`, `ident.Parse` на границах умирают со сменой языка → `lang/go/`. `enum как TEXT без CHECK` переживёт Go → Python, но не переживёт уход от SQLite → это `stack/sqlite/`. Ось определяется **природой правила**, а не тем, сколько сегодня потребителей. Конвенция независима от приложений по построению, поэтому арх-слой выделяется тогда, когда правило действительно не зависит от языка, а не когда появился второй язык. **Известный долг.** По этому тесту `lang/go/errors.md`, `lang/go/logging.md` и `lang/go/db-schema.md` содержат невыделенные слои: у первых двух — архитектурное ядро (уровень как адресат, что не логируем; трансляция ошибки на внешней границе, приватный канал против публичного), у третьего — целый пласт `stack/sqlite/` (типы колонок). Это не принцип, а незавершённая работа. ## Расширение Файл в `lang/` или `stack/` может объявить в шапке: ```yaml extends: arch/db-identifiers.md ``` Расширение **только реализует и сужает** базу, но не отменяет её. Если слою нужно противоречить базе — это сигнал одного из двух: либо у базы неверно сформулировано условие применимости (чинится в каноне), либо репозиторий на базу просто не подписан. `extends` — документация связи, а не механизм: `conv` о ней только напоминает при `add` и никак не следит за тем, чтобы база лежала рядом. ## Служебная разметка **Шапка копии** ставится при `conv add` и в каноне не хранится: ```yaml --- origin: arch/time.md # откуда взято origin_hash: a1b2c3d4 # отпечаток канона на момент синхронизации synced: 2026-07-25 local: нет # или: чем и почему разошлись --- ``` `origin_hash` — контент-отпечаток, а не git-SHA. Он позволяет отличать «канон обновился» от «изменено локально»; без него `status` умеет только «differs», а такой отчёт быстро перестают читать. Прочие ключи шапки (`status`, `extends`) — часть документа: они сравниваются наравне с телом. **Локальные регионы** — куски, принадлежащие репозиторию по определению. Из сравнения исключаются, поэтому вечного шума в `diff` не дают: ```markdown `AUTOINCREMENT` в новых миграциях — `internal/archrules`. ``` Имя обязательно — перенос при `pull` идёт по именам, безымянные регионы `conv` отвергает. Что всегда локально: - **механизация** — канон не знает, у кого линтер уже настроен; - **отступления** — «у нас пока не так», честно и поимённо; - **разрешение условия** — «Здесь: INTEGER PK, id наружу не выходят»; - **эталоны и ссылки** — имена функций, файлов, ADR конкретного репозитория; - **список конвенций** в README репозитория. Путь файла в каноне и имя региона — это API: переименование осиротит все копии (`origin` строковый). Переименовывать — только вместе с обходом потребителей. После `pull` копию нужно перечитать глазами: содержимое региона могло устареть относительно переписанного вокруг текста, и автоматика этого не увидит. ## Раскладка в репозитории Копии повторяют структуру канона: ``` docs/conventions/ README.md собственный, не синхронизируется arch/db-identifiers.md lang/go/db-identifiers.md ``` Подписка не описана отдельным файлом — она **и есть** набор лежащих файлов, видимый в `git ls-files`. README директории перечисляет их одной плоской таблицей, чтобы вложенность не мешала навигации. ## Контракт с агентом Копии — обычные файлы, и правка их агентом никак не отличима от правки любого другого документа. Это главный канал тихого дрейфа, поэтому `AGENTS.md` каждого потребителя должен явно говорить: > Файлы в `docs/conventions/` с шапкой `origin:` — копии из канона > `dev-conventions`. Репозиторное пишется только внутрь > ``. Правка вне регионов — либо `conv push` в канон, либо > запись причины в `local:`. ## Команды ```bash conv list # что есть в каноне conv add arch/time.md # взять к себе (можно несколько за раз) conv status # ok / изменено локально / канон обновился / разошлись conv diff [arch/time.md] # чем копия отличается, без учёта локальных регионов conv pull arch/time.md # забрать обновление канона (регионы переносятся) conv push arch/time.md # вернуть локальное улучшение в канон conv push --new lang/go/x.md # завести в каноне новую конвенцию ``` `status` и `diff` всегда завершаются кодом 0: это отчёт, а не проверка. Расхождение — нормальное состояние, а постоянный шум в `diff` означает не «догони канон», а «пора разрезать файл». Симметрично: разросшийся до спора с базой локальный регион означает «пора чинить условие применимости в каноне». Запускать из корня репозитория: ```bash ~/projects/private/dev-conventions/conv status ``` Обёртка в раннере репозитория (`inv conventions -- status` для ansible, `task conventions -- status` для Go) — тонкий проброс аргументов, чтобы логика не размножалась по репозиториям в двух диалектах. ## Жизненный цикл - **В канон.** Новая конвенция пишется в том репозитории, где заболело, и продвигается `conv push --new`. Локальные регионы при этом опустошаются: в канон едет только норма. - **Из канона.** Устаревшая конвенция удаляется вместе с обходом потребителей — тихо осиротить копии нельзя. - **История.** Канон коммитится при каждом `push`: `origin_hash` отвечает на «отличается ли», но только git канона отвечает на «почему база сформулирована так».