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

- 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
+185
View File
@@ -0,0 +1,185 @@
# Канон конвенций
Общие конвенции для личных проектов. Репозитории берут отсюда копии в свой
`docs/conventions/`, коммитят их и живут дальше самостоятельно — как с
`ansible-roles`: канон не источник истины во время работы, а лавка, из
которой берут и в которую возвращают улучшения.
Правило то же, что у ролей: **деплоится и читается только то, что лежит в
git репозитория**. Канон никем не подключается на лету.
## Направление — конвенция → код
Конвенция формулируется независимо от конкретного приложения. Она задаёт
правило; код ему следует. Обратное направление запрещено: то, что
приложение уже делает иначе, **не является аргументом против правила** — это
отступление, и его место в локальном регионе того репозитория, а не в
переформулировке канона.
Отсюда практические следствия:
- в каноне нет утверждений о том, как что-то устроено в конкретном
репозитории («у нас так в девяти плейбуках из тридцати трёх») — только
нормы и условия их применимости;
- в каноне нет списка, кто на что подписан: подписка — свойство
репозитория, а не конвенции;
- расхождение канона с кодом чинится либо кодом, либо честной записью
отступления, либо — если правило оказалось неверным — правкой правила по
существу, а не подгонкой под факт.
## Оси
```
common/ как вести сами конвенции
arch/ решения, переживающие смену языка и инструментов
lang/<язык>/ как решение реализуется и механизируется в языке
stack/<стек>/ привязка к инструменту, хранилищу, транспорту
```
Тест — по тому, замена чего убивает правило:
> Умирает при смене **языка** → `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
<!-- 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 канона отвечает на «почему база
сформулирована так».