av 62c5645dd4 common: язык дополнен состоянием МЕХАНИЗИРОВАНО
- правило, чья норма уехала в линтер, сохраняет номер и «Почему», а место
  нормы занимает отметка — иначе получался объект без модальности
- записано, что факт «механизировано у всех» устанавливается вручную:
  канон списка подписчиков не знает по построению
2026-07-25 19:17:33 +03:00

Канон конвенций

Общие конвенции для личных проектов. Репозитории берут отсюда копии в свой 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/ может объявить в шапке:

extends: arch/db-identifiers.md

Расширение только реализует и сужает базу, но не отменяет её. Если слою нужно противоречить базе — это сигнал одного из двух: либо у базы неверно сформулировано условие применимости (чинится в каноне), либо репозиторий на базу просто не подписан.

extends — документация связи, а не механизм: conv о ней только напоминает при add и никак не следит за тем, чтобы база лежала рядом.

Служебная разметка

Шапка копии ставится при conv add и в каноне не хранится:

---
origin: arch/time.md      # откуда взято
origin_hash: a1b2c3d4     # отпечаток канона на момент синхронизации
synced: 2026-07-25
local: нет                # или: чем и почему разошлись
---

origin_hash — контент-отпечаток, а не git-SHA. Он позволяет отличать «канон обновился» от «изменено локально»; без него status умеет только «differs», а такой отчёт быстро перестают читать. Прочие ключи шапки (status, extends) — часть документа: они сравниваются наравне с телом.

Локальные регионы — куски, принадлежащие репозиторию по определению. Из сравнения исключаются, поэтому вечного шума в diff не дают:

<!-- 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:.

Команды

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 означает не «догони канон», а «пора разрезать файл». Симметрично: разросшийся до спора с базой локальный регион означает «пора чинить условие применимости в каноне».

Запускать из корня репозитория:

~/projects/private/dev-conventions/conv status

Обёртка в раннере репозитория (inv conventions -- status для ansible, task conventions -- status для Go) — тонкий проброс аргументов, чтобы логика не размножалась по репозиториям в двух диалектах.

Жизненный цикл

  • В канон. Новая конвенция пишется в том репозитории, где заболело, и продвигается conv push --new. Локальные регионы при этом опустошаются: в канон едет только норма.
  • Из канона. Устаревшая конвенция удаляется вместе с обходом потребителей — тихо осиротить копии нельзя.
  • История. Канон коммитится при каждом push: origin_hash отвечает на «отличается ли», но только git канона отвечает на «почему база сформулирована так».
S
Description
No description provided
Readme
865 KiB
Languages
Markdown 100%