заведён канон общих конвенций для личных проектов
- 13 конвенций по осям arch / lang / stack / common; репозитории берут оттуда копии в свой docs/conventions/ и коммитят их у себя - conv — синхронизация копий: add / status / diff / pull / push, локальные регионы исключены из сравнения, поэтому расхождение не даёт шума
This commit is contained in:
@@ -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 канона отвечает на «почему база
|
||||
сформулирована так».
|
||||
Reference in New Issue
Block a user