- LANGUAGE.md: раздел «Идентификаторы» переписан под префиксы, в список машинных проверок добавлена сверка с реестром - GUIDE.md перенумерован под префикс META, «номер правила» заменён на «идентификатор»
220 lines
14 KiB
Markdown
220 lines
14 KiB
Markdown
# Канон конвенций
|
||
|
||
Общие конвенции для личных проектов. Репозитории берут отсюда копии в свой
|
||
`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`,
|
||
а не `conventions/arch/…` — так же, как они лягут в `docs/conventions/`
|
||
репозитория. На **правила** ссылаются идентификатором без пути: `KEYS-5`.
|
||
Префикс уникален по всему канону (реестр — `conventions/prefixes.toml`),
|
||
поэтому идентификатор не зависит от того, на какой оси файл лежит сегодня.
|
||
|
||
Тест — по тому, замена чего убивает правило:
|
||
|
||
> Умирает при смене **языка** → `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/` (типы колонок). Это не принцип, а незавершённая
|
||
работа.
|
||
|
||
## Префиксы
|
||
|
||
Каждый файл канона объявляет в шапке свой префикс правил:
|
||
|
||
```yaml
|
||
prefix: KEYS
|
||
```
|
||
|
||
Четыре заглавные латинские буквы, уникальные по всему канону; реестр —
|
||
[`conventions/prefixes.toml`](conventions/prefixes.toml). Префикс выбирается
|
||
под файл, а не выводится по формуле, и не переиспользуется никогда. Правила
|
||
адресуются идентификатором `KEYS-5` — без пути к файлу. Подробности формы —
|
||
`LANGUAGE.md`.
|
||
|
||
## Расширение
|
||
|
||
Файл в `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», а такой отчёт быстро перестают читать. Прочие ключи шапки
|
||
(`prefix`, `extends`) — часть документа: они сравниваются наравне с телом.
|
||
|
||
**Локальные регионы** — куски, принадлежащие репозиторию по определению.
|
||
Из сравнения исключаются, поэтому вечного шума в `diff` не дают:
|
||
|
||
```markdown
|
||
<!-- local:механизировано -->
|
||
`AUTOINCREMENT` в новых миграциях — `internal/archrules`.
|
||
<!-- /local -->
|
||
```
|
||
|
||
Имя обязательно — перенос при `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`. Репозиторное пишется только внутрь
|
||
> `<!-- local:… -->`. Правка вне регионов — либо `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 канона отвечает на «почему база
|
||
сформулирована так».
|