av d5118336cb язык: примеры переведены на вымышленные X-правила
- живые идентификаторы KEYS-5, SLOG-8.1, SLOG-27, MIGR-2/4/6 в примерах
  заменены на XKEY, XMIG, XLOG: номера канона означали не то, что в примере,
  и расходились дальше при каждой перенумерации
- сказано явно, что описание языка ни на один набор конвенций не опирается;
  ссылка на обоснования канона в разделе про ПОЧЕМУ заменена на внешние
  практики
2026-07-26 15:15:30 +03:00

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

Общие конвенции для личных проектов. Репозитории берут отсюда копии в свой docs/conventions/, коммитят их и живут дальше самостоятельно — как с ansible-roles: канон не источник истины во время работы, а лавка, из которой берут.

Сами конвенции лежат в conventions/, обвязка — в корне:

Файл Что описывает
README.md устройство канона, оси, сборка копий, жизненный цикл
LANGUAGE.md язык записи правил: идентификаторы, модальность, обоснование
GUIDE.md как ведут конвенции: когда заводить, механизация, отступления
prefixes.toml реестр префиксов правил
conv сборка копий

Обвязка живёт только в каноне и в репозитории не оказывается — в копию едет лишь содержимое conventions/. Самодостаточность копии это не нарушает: конвенция называет язык записи одной строкой с номером версии и не ссылается на путь (LANGUAGE.md, раздел «Ссылка на язык из конвенции»).

Правило то же, что у ролей: деплоится и читается только то, что лежит в git репозитория. Канон никем не подключается на лету.

Направление — конвенция → код

Конвенция формулируется независимо от конкретного приложения. Она задаёт правило; код ему следует. Обратное направление запрещено: то, что приложение уже делает иначе, не является аргументом против правила — это отступление, и его место в локальной части копии того репозитория, а не в переформулировке канона.

Отсюда практические следствия:

  • в каноне нет утверждений о том, как что-то устроено в конкретном репозитории («у нас так в девяти плейбуках из тридцати трёх») — только нормы и условия их применимости;
  • в каноне нет списка, кто на что подписан: подписка — свойство репозитория, а не конвенции;
  • расхождение канона с кодом чинится либо кодом, либо честной записью отступления, либо — если правило оказалось неверным — правкой правила по существу, а не подгонкой под факт.

Оси

conventions/
  arch/            решения, переживающие смену языка и инструментов
  lang/<язык>/     как решение реализуется и механизируется в языке
  stack/<стек>/    привязка к инструменту, хранилищу, транспорту

Оси — раскладка канона; в репозитории копия лежит плоско, файлом на тему. Пути файлов даются относительно conventions/ (arch/db-identifiers.md) и адресуют исходник канона, а не место в копии. На правила ссылаются идентификатором без пути: KEYS-5. Префикс уникален по всему канону (реестр — 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/ (типы колонок). Это не принцип, а незавершённая работа.

Префиксы

Каждый файл канона объявляет в шапке свой префикс правил:

prefix: KEYS

Четыре заглавные латинские буквы, уникальные по всему канону; реестр — prefixes.toml. Префикс выбирается под файл, а не выводится по формуле, и не переиспользуется никогда. Правила адресуются идентификатором KEYS-5 — без пути к файлу. Подробности формы — LANGUAGE.md.

Буква X в начале префикса зарезервирована за репозиториями: канон её не занимает никогда, а локальные правила потребителя берут префиксы только на неё (XTIM, XLOG). Так столкновение локального префикса с будущим префиксом канона невозможно по построению, и согласовывать заранее ничего не нужно.

Расширение

Файл в lang/ или stack/ может объявить в шапке ещё и базу:

extends: arch/db-identifiers.md

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

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

Копия в репозитории

Копия плоская: один файл на тему, слои осей идут внутри него секциями в порядке arch → язык → стек. Пути канона в копии не воспроизводятся.

docs/conventions/
  README.md            собственный, не собирается
  time.md              arch/time.md + lang/go/time.md
  db-identifiers.md    arch/db-identifiers.md + lang/go/db-identifiers.md
  app-directories.md   arch/… + stack/ansible/…

Так конвенция остаётся самодостаточным документом: тот, кто проверяет по ней код — человек или агент, — читает один файл и не собирает тему из трёх мест.

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

---
origin: time
---

Больше в шапке ничего нет. Отпечатка канона и даты синхронизации в ней не хранится: обновление перезаписывает файл в рабочем дереве, и что именно изменилось, показывает git diff до коммита. Второй механизм сравнения рядом с git не нужен.

Маркер локальной части — единственная машинно значимая разметка внутри файла:

<!-- conv:local -->

MIGR-2, MIGR-4 механизированы — `internal/archrules`.
MIGR-6 не соблюдается в `show_history`, `queue`: составные ключи там
появились до конвенции, миграция данных не окупается.

Всё ниже маркера принадлежит репозиторию и переживает обновление; всё выше — пересобирается из канона. Маркер один и безымянный, поэтому у него нет имени, которое можно осиротить переименованием.

Ниже маркера живёт то, чего канон о репозитории не знает: механизация, отступления, разрешение условий («Здесь: INTEGER PK, id наружу не выходят»), ссылки на ADR и код, а также собственные правилас префиксом на X, по тем же правилам формы, что и канон.

Если местных правок стало больше, чем каноничного текста, копия перестаёт быть копией: origin: из шапки убирают, и дальше это обычный документ репозитория. Файл, оставивший шапку, при следующем обновлении потеряет всё, что выше маркера.

Манифест

Откуда взяты копии и где брать обновления — .conventions.toml в корне репозитория-потребителя:

source = "ssh://git@git.vakhrushev.me:2222/av/dev-conventions.git"

lang   = ["go"]
stack  = ["sqlite", "htmx"]

topics = ["time", "config", "db-identifiers"]

lang и stack выбирают строку разреженной матрицы: файл темы собирает только те слои, которые репозиторию подходят. topics — подписка; списка подписчиков у канона по-прежнему нет, список тем есть только у потребителя.

Как именно инструмент добирается до канона — путь на диске, git, HTTP — дело инструмента, а не модели. Манифест отвечает на два вопроса: откуда это взято и где искать обновления.

Отдельного лок-файла нет. Он отвечал бы на «что было в прошлый раз», а на это отвечает git: копии закоммичены, автоматического обновления не существует, и любое изменение проходит через чтение диффа человеком.

Контракт с агентом

Копии — обычные файлы, и правка их агентом никак не отличима от правки любого другого документа. Это главный канал тихого дрейфа, поэтому AGENTS.md каждого потребителя должен явно говорить:

Файлы в docs/conventions/ с шапкой origin: — копии из канона dev-conventions. Репозиторное пишется только ниже <!-- conv:local -->; всё выше маркера перезаписывается при обновлении. Своё правило — с префиксом на X.

Команды

conv list                    # какие темы есть в каноне
conv add time                # добавить тему в манифест и собрать файл
conv pull                    # пересобрать всё, что перечислено в манифесте

Отчёт о том, что изменилось, отдельной командой не выдаётся: после pull его показывает git diff, а решение — принять, поправить или откатить — принимает человек перед коммитом.

Транспорт обратно в канон не предусмотрен. Улучшение, найденное в репозитории, переносится в канон руками: это редкая операция, и её цена — не аргумент против того, чтобы направление оставалось односторонним.

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

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

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

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

  • В канон. Новая конвенция пишется в том репозитории, где заболело, и переносится в канон, когда стало ясно, что общего в ней больше, чем местного. Локальная часть при этом не едет: в канон попадает только норма, а префикс на X меняется на канонический — то есть правила получают новые идентификаторы.
  • Из канона. Устаревшая конвенция удаляется вместе с обходом потребителей — тихо осиротить копии нельзя.
  • История. Канон коммитится при каждой правке: только git канона отвечает на вопрос, почему база сформулирована так.

Состояние

Модель выше — согласованная, а не реализованная. conv пока собран под прежнюю: зеркальное дерево копий вместо плоского, именованные регионы <!-- local:имя --> вместо одного маркера, origin_hash в шапке и команды status, diff, push. Сами конвенции уже приведены к новой модели — именованных регионов в каноне нет. Ни один репозиторий-потребитель не подключён, поэтому переход никого не ломает.

S
Description
No description provided
Readme
865 KiB
Languages
Markdown 100%