Files
dev-conventions/README.md
T
av 4a59c71737 заведён канон общих конвенций для личных проектов
- 13 конвенций по осям arch / lang / stack / common; репозитории берут
  оттуда копии в свой docs/conventions/ и коммитят их у себя
- conv — синхронизация копий: add / status / diff / pull / push, локальные
  регионы исключены из сравнения, поэтому расхождение не даёт шума
2026-07-25 18:18:18 +03:00

186 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Канон конвенций
Общие конвенции для личных проектов. Репозитории берут отсюда копии в свой
`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 канона отвечает на «почему база
сформулирована так».