av 92bd1f463d манифесты стали данными, заведён convy sync
- убраны комментарии из suite.toml и .conventions.toml: файл, который
  машина переписывает, комментарий через круг не проносит; объяснения
  ушли в README рядом, который suite init теперь заводит
- удалена текстовая правка манифеста целиком — 520 строк ручного
  лексера TOML вместе со всем классом ошибок порчи данных
- запись идёт из структур энкодером; ключ, которого инструмент не
  знает, запись останавливает, а не теряется молча
- convy sync сверяет манифест и подводит под него раскладку файлов:
  чего не хватает — собирает, что осиротело — удаляет, копию с
  локальной частью не трогает никогда
2026-07-28 09:45:10 +03:00

convy

CLI для управления конвенциями разработки: ведёт набор конвенций и собирает копии в проектах.

Модель, которую инструмент реализует, описана не здесь, а в репозитории набора (dev-conventions): README.md — устройство набора и копий, LANGUAGE.md — форма правила, GUIDE.md — правила ведения набора, TOOL.md — решения об этом инструменте. При расхождении истина там.

Термины

Уровень Что это
язык как записывается правило: слова, версия, READING.md
набор репозиторий с конвенциями, манифестом suite.toml и обвязкой
проект репозиторий-потребитель с манифестом .conventions.toml
тема набор правил об одном фокусе разработки; единица подписки
слой один файл темы: базовый, языковой или стековый
компонент адресат сборки в проекте: один язык, один стек, один вид приложения
префикс четыре заглавные латинские буквы, адрес правила: GTIM-3

Тему и ось слоя объявляет шапка файла, а не путь: topic:, lang:, stack:. Слой без ключей оси — базовый, он попадает в копию всегда.

Три уровня и ссылки между ними

Уровни стоят стопкой, и каждый нижний называет верхний ссылкой:

язык    →  слова, версия, короткое описание для читателя
набор   →  ссылается на язык: [language] version, lang, source
проект  →  ссылается на набор: source в .conventions.toml

Каждый уровень — набор файлов, а где эти файлы лежат, решает не модель, а ссылка. Реализовано два транспорта:

Ссылка Что это
../dev-conventions, /srv/conventions директория на диске, как она лежит
https://git.example.org/av/conventions.git git-репозиторий, клонируется
file:///srv/conventions#v2 тот же репозиторий, но в закоммиченном виде

Относительный путь считается от манифеста, который ссылку несёт. Хвост #ветка, #тег или #коммит закрепляет ревизию и осмыслен только у git. ssh:// и git@host:path работают тем же клонированием, но проверены хуже.

Клон делается заново на каждый вызов и удаляется. Кэш сэкономил бы второй клон и вернул бы вопрос, что в нём протухло, — а на «что было в прошлый раз» отвечает git в репозитории-потребителе.

Ключ [language] source в наборе пока обычно пуст: описание языка живёт в самом наборе. Когда спецификация уедет в свой репозиторий, тот же ключ её назовёт, и больше ничего не изменится.

Установка

Внешняя зависимость одна (BurntSushi/toml), сборка обычная:

go install git.vakhrushev.me/av/convy@latest

или из клона репозитория:

go build -o convy .

Способ раздачи готовых бинарей пока не выбран — вопрос открыт в TOOL.md.

Команды

В наборе:
  convy suite init    завести набор: директория и манифест
  convy suite add     завести конвенцию: файл, тема и префикс
  convy suite rule    дописать правило: следующий номер, блоки по порядку
  convy suite retire  снять правило, конвенцию или тему — без переиспользования
  convy suite list    что в наборе и что возьмёт компонент
  convy suite check   целостность набора: префиксы, темы, оси, ссылки, форма

В проекте:
  convy init          подключить конвенции: источник и первый компонент
  convy add <тема>    подписаться и собрать
  convy pull          пересобрать подписанное, текст и всё
  convy sync          привести файлы в соответствие манифесту
  convy list          что подключено и что ещё есть в наборе
  convy check         проверить форму того, что здесь

Контекст определяется по манифесту рядом: suite.toml — набор, .conventions.toml — проект. Наугад не делается ничего: команда не того уровня отказывает и подсказывает нужную.

Два режима

Команды, которые что-то меняют, работают в двух режимах, и режим читается по командной строке.

Без аргументов — диалог. Каждое поле спрашивается с подсказкой о том, что туда кладут и почему. Это для человека.

$ convy suite add

The focus the rules are about: time, config, db-schema. A topic is the unit of
subscription — a consumer takes it whole. The name never changes and is never reused.
Topic: logging

It goes into the manifest and builds the table of conventions in a consumer's README.
One line about the topic: логирование: уровни, структура записи
...

С флагами — автоматика. Всё передаётся сразу, вопросов не задаётся, все недостающие поля называются разом. Это для агентов и скриптов.

$ convy suite add --topic logging --about "логирование: уровни" \
    --prefix SLOG --title "Логирование"

Без терминала пустой вызов отказывает, а не виснет в ожидании ответа, которого некому дать.

Проверка набора

$ convy suite check
conventions/lang/go/logging.md
  error:5  extends points at "arch/time.md" with topic "time", while the file
           carries topic "logging": the layers of one topic declare one name [spread]

suite: 13 files, 8 topics, language version 1 (ru)
errors: 1, warnings: 0

Строка находки — уровень:строка что не так [семейство]. Семейств четыре:

  • manifest — префиксы, темы, объявленные файлы, незарегистрированные файлы;
  • form — форма правила: нумерация, блоки, модальные слова, словарь. Проверяется в любом файле, который язык употребляет;
  • spread — то, что относится к отъезду к потребителю: тема в шапке, ось, extends, пути канона, самодостаточность нормы. Только в конвенциях;
  • links — разрешение ссылок вида PREFIX-N.

Третья часть списка проверок языка — взаимоисключительность строк таблицы, покрытие области действия, отвечает ли обоснование на «что сломается» — разбором текста не даётся и остаётся работой читателя.

Коды возврата: 0 — чисто, 1 — есть ошибки, 2 — команда набрана неверно или не в том контексте.

Подключение в проект

$ convy init --source ../dev-conventions --component backend \
    --dir backend/docs/conventions --lang go --stack sqlite
$ convy add time
$ convy add logging --for backend

init проверяет источник до того, как писать манифест: набор, до которого никто не доберётся, проходит любую проверку и никому не помогает. Получается .conventions.toml:

source = "../dev-conventions"

[components.backend]
dir = "backend/docs/conventions"
lang = ["go"]
stack = ["sqlite"]
topics = ["logging", "time"]

Компонент пишется всегда, даже когда он один; при нескольких команда без --for не угадывает, а перечисляет имена. stack — список: sqlite и postgres действуют вместе, это разные таблицы одного сервиса. Два языка не действуют вместе никогда — ради этого компонент и заведён.

Манифесты — данные, а не текст

Оба манифеста инструмент и читает, и переписывает целиком. Поэтому комментариев в них нет: файл, который машина переписывает, комментарий через круг не проносит, а вид, что проносит, стоит этого комментария в день, когда никто не смотрит. Объяснения живут в соседних файлах, которых ни одна команда не касается: convy suite init заводит рядом README.md и пишет их туда.

Ключ, которого инструмент не знает, при записи потерялся бы. Поэтому он не пишет вовсе:

$ convy suite add --topic time --about "время" --prefix TIME --title "Время"
suite.toml holds 1 key the tool does not know (language.descriptoin); a write
goes out of what the tool understands, so the key would be dropped — fix the
spelling first

Манифест — источник истины

.conventions.toml правится руками так же законно, как командой. Дальше раскладку под него подводит sync:

$ convy sync --dry-run
backend → docs/conventions
  + docs/conventions/errors.md   subscribed, and no file
  - docs/conventions/logging.md  nothing subscribes to "logging"

2 files would change; run without --dry-run to do it

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

Копия с локальной частью не удаляется никогда: ниже маркера лежит единственное, чего нет больше нигде. Такая копия называется в отчёте, и sync завершается с ошибкой, пока её не убрали руками или не подписались снова.

Перед тем как что-то трогать, sync сверяет манифест: подписка на снятую или несуществующую тему, тема дважды, два языка в одном компоненте, тема без подходящего слоя, общая директория у двух компонентов. Находки называются разом, и ничего не пишется.

Копия плоская, файл на тему. Первый слой — сам документ; каждый следующий становится его разделом, и заголовки внутри опускаются на уровень: слой реализует и сужает базу, а не стоит рядом с ней. Строка о версии языка остаётся одна.

---
origin: time
---

# Время
...
### TIME-1. Единый формат — RFC 3339, UTC
...
## Время: реализация на Go
...
#### GTIM-1. «Сейчас» берётся у слоя хранилища
...
<!-- conv:local -->

Всё ниже <!-- conv:local --> принадлежит репозиторию и переживает pull; всё выше перезаписывается. Рядом с копиями кладётся READING.md — он приезжает с уровня языка. README.md в той же директории принадлежит проекту и не трогается, а файл, у которого убрали origin:, перестаёт быть копией: pull его не перезапишет и скажет почему.

Проверка проекта

convy check до набора не дотягивается и сети не требует: форма правила одна и та же, локальные правила проекта на X-префиксах записаны по ней же. Проверяются шапка origin:, маркер локальной части, нумерация по каждому префиксу, блоки правила, словарь — и то, чего в наборе не бывает:

$ convy check
docs/conventions/time.md
  error:22  rule XTIM-1 is a rule of the repository standing above the
            marker: a reassembly would wipe it [spread]

project: 2 files in 1 component
errors: 1, warnings: 0

Язык копии определяется по строке о версии, которую она несёт: манифест рядом с ней не лежит, и больше сказать некому.

Ступени и словарь

Слова, которыми записаны модальность и метки, — свойство версии языка и естественного языка набора, а не самого набора. Инструмент знает их сам, и suite.toml их не дублирует: достаточно [language] version и lang.

Поэтому ступень называется категорией, а не словом:

--modality requirement | prohibition | recommendation | not-recommended | permission

prohibition в русском наборе превращается в **НЕ ДОЛЖЕН.**, в английском — в **MUST NOT.**. Вызывающему не нужно знать, на каком языке записан набор.

По той же причине convy умеет написать строку о версии языка, и созданный им файл проходит suite check без единой правки.

Чего инструмент не делает

  • не сливает трёхсторонне и не разрешает конфликты: правка выше маркера локальной части теряется, и это заявленное поведение;
  • не ведёт лок-файл: копии закоммичены, ответ на «что было в прошлый раз» даёт git;
  • не хранит список подписчиков: подписка — свойство проекта;
  • не переносит правки из проекта в набор: операция ручная и редкая;
  • не перенумеровывает правила: номер — идентификатор, а не позиция;
  • не кэширует источник: клон делается заново и удаляется;
  • не знает нескольких наборов сразу: source в проекте один;
  • не хранит комментарии в манифестах: они данные, а объяснения — в соседних файлах.
S
Description
No description provided
Readme
309 KiB
Languages
Go 100%