# convy CLI для управления конвенциями разработки: ведёт набор конвенций и собирает копии в проектах. Модель, которую инструмент реализует, описана не здесь, а в репозитории набора (`dev-conventions`): `README.md` — устройство набора и копий, `LANGUAGE.md` — форма правила, `GUIDE.md` — правила ведения набора. При расхождении истина там. ## Термины | Уровень | Что это | |---|---| | язык | как записывается правило: слова, версия, `READING.md` | | набор | репозиторий с конвенциями, манифестом `.conventions-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 . ``` Готовых бинарей пока нет: `go install` хватает. ## Команды ``` В наборе: 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 проверить форму того, что здесь ``` Контекст определяется по манифесту рядом: `.conventions-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` — команда набрана неверно или не в том контексте. Обе проверки принимают `--json` — те же находки в том же порядке, для вызывающего, который не человек: ```json {"findings":[{"severity":"error","family":"spread","path":"docs/conventions/time.md", "line":22,"message":"..."}],"errors":1,"warnings":0} ``` Манифест набора называет ключом `governance` документ, которым набор ведёт себя сам, — тот, что написан языком конвенций, но не принадлежит теме и потому никуда не едет. Без этого ключа конвенция, потерявшая `topic`, была бы от него неотличима. ## Подключение в проект ``` $ 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`: ```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 "Время" .conventions-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` сверяет манифест: подписка на снятую или несуществующую тему, тема дважды, два языка в одном компоненте, тема без подходящего слоя, общая директория у двух компонентов. Находки называются разом, и ничего не пишется. Копия плоская, файл на тему. Первый слой — сам документ; каждый следующий становится его разделом, и заголовки внутри опускаются на уровень: слой реализует и сужает базу, а не стоит рядом с ней. Строка о версии языка остаётся одна. ```markdown --- origin: time --- # Время ... ### TIME-1. Единый формат — RFC 3339, UTC ... ## Время: реализация на Go ... #### GTIM-1. «Сейчас» берётся у слоя хранилища ... ``` Всё ниже `` принадлежит репозиторию и переживает `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 ``` Язык копии определяется по строке о версии, которую она несёт: манифест рядом с ней не лежит, и больше сказать некому. ## Ступени и словарь Слова, которыми записаны модальность и метки, — свойство версии языка и естественного языка набора, а не самого набора. Инструмент знает их сам, и `.conventions-suite.toml` их не дублирует: хватает `[language] version` и `lang`. Поэтому ступень называется категорией, а не словом: ``` --modality requirement | prohibition | recommendation | not-recommended | permission ``` `prohibition` в русском наборе превращается в `**НЕ ДОЛЖЕН.**`, в английском — в `**MUST NOT.**`. Вызывающему не нужно знать, на каком языке записан набор. По той же причине `convy` умеет написать строку о версии языка, и созданный им файл проходит `suite check` без единой правки. ## Чего инструмент не делает - не сливает трёхсторонне и не разрешает конфликты: правка выше маркера локальной части теряется, и это заявленное поведение; - не ведёт лок-файл: копии закоммичены, ответ на «что было в прошлый раз» даёт git; - не хранит список подписчиков: подписка — свойство проекта; - не переносит правки из проекта в набор: операция ручная и редкая; - не перенумеровывает правила: номер — идентификатор, а не позиция; - не кэширует источник: клон делается заново и удаляется; - не знает нескольких наборов сразу: `source` в проекте один; - не хранит комментарии в манифестах: они данные, а объяснения — в соседних файлах.