- документ самоуправления объявляется ключом governance, а не угадывается по «он один и без ключей оси»: конвенция, потерявшая topic, была от него неотличима и тихо теряла все проверки об отъезде к потребителю - проверка путей канона больше не ловит README.md и READING.md — эти два имени значат что-то и на стороне потребителя - lang.Recognize требует совпадения и слов, и номера версии; директории компонентов сверяются на вложенность, а не только на равенство - у обеих проверок появился --json, а convy sync называет ссылки на темы, которых компонент не взял
319 lines
18 KiB
Markdown
319 lines
18 KiB
Markdown
# 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. «Сейчас» берётся у слоя хранилища
|
||
...
|
||
<!-- 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
|
||
```
|
||
|
||
Язык копии определяется по строке о версии, которую она несёт: манифест рядом
|
||
с ней не лежит, и больше сказать некому.
|
||
|
||
## Ступени и словарь
|
||
|
||
Слова, которыми записаны модальность и метки, — свойство версии языка и
|
||
естественного языка набора, а не самого набора. Инструмент знает их сам, и
|
||
`.conventions-suite.toml` их не дублирует: хватает `[language] version` и
|
||
`lang`.
|
||
|
||
Поэтому ступень называется категорией, а не словом:
|
||
|
||
```
|
||
--modality requirement | prohibition | recommendation | not-recommended | permission
|
||
```
|
||
|
||
`prohibition` в русском наборе превращается в `**НЕ ДОЛЖЕН.**`, в английском —
|
||
в `**MUST NOT.**`. Вызывающему не нужно знать, на каком языке записан набор.
|
||
|
||
По той же причине `convy` умеет написать строку о версии языка, и созданный им
|
||
файл проходит `suite check` без единой правки.
|
||
|
||
## Чего инструмент не делает
|
||
|
||
- не сливает трёхсторонне и не разрешает конфликты: правка выше маркера
|
||
локальной части теряется, и это заявленное поведение;
|
||
- не ведёт лок-файл: копии закоммичены, ответ на «что было в прошлый раз» даёт
|
||
git;
|
||
- не хранит список подписчиков: подписка — свойство проекта;
|
||
- не переносит правки из проекта в набор: операция ручная и редкая;
|
||
- не перенумеровывает правила: номер — идентификатор, а не позиция;
|
||
- не кэширует источник: клон делается заново и удаляется;
|
||
- не знает нескольких наборов сразу: `source` в проекте один;
|
||
- не хранит комментарии в манифестах: они данные, а объяснения — в соседних
|
||
файлах.
|