обвязка: модель копий переписана под один маркер и манифест

- именованные регионы `<!-- local:имя -->` заменены на единственный
  `<!-- conv:local -->`: всё ниже него принадлежит репозиторию, всё выше
  пересобирается, поэтому имени-которое-можно-осиротить больше нет
- лок-файла и `origin_hash` в шапке нет — «что было в прошлый раз» знает git,
  копии закоммичены, автоматического обновления не существует; транспорт
  назад (`push`) убран вместе с ними
- заведены META-22 (репозиторное пишется ниже маркера) и META-23 (форк не
  носит `origin:`), META-17 переписан под маркер; буква `X` в префиксе
  зарезервирована за локальными правилами потребителей
This commit is contained in:
av
2026-07-26 12:55:05 +03:00
parent 4de6e0f896
commit c2f68e6be1
5 changed files with 213 additions and 148 deletions
+125 -90
View File
@@ -3,22 +3,21 @@
Общие конвенции для личных проектов. Репозитории берут отсюда копии в свой
`docs/conventions/`, коммитят их и живут дальше самостоятельно — как с
`ansible-roles`: канон не источник истины во время работы, а лавка, из
которой берут и в которую возвращают улучшения.
которой берут.
Сами конвенции лежат в `conventions/`, обвязка — в корне:
| Файл | Что описывает |
|---|---|
| `README.md` | устройство канона, оси, синхронизация, жизненный цикл |
| `README.md` | устройство канона, оси, сборка копий, жизненный цикл |
| [LANGUAGE.md](LANGUAGE.md) | язык записи правил: идентификаторы, модальность, «Почему» |
| [GUIDE.md](GUIDE.md) | как ведут конвенции: когда заводить, механизация, отступления |
| `prefixes.toml` | реестр префиксов правил |
| `conv` | синхронизация копий |
| `conv` | сборка копий |
Обвязка живёт только в каноне и в репозитории не оказывается — `conv`
синхронизирует лишь содержимое `conventions/`. Пока это осознанное
ограничение: копия конвенции ссылается на `LANGUAGE.md` как на внешний
документ.
Обвязка живёт только в каноне и в репозитории не оказывается — в копию едет
лишь содержимое `conventions/`. Пока это осознанное ограничение: копия
конвенции ссылается на `LANGUAGE.md` как на внешний документ.
Правило то же, что у ролей: **деплоится и читается только то, что лежит в
git репозитория**. Канон никем не подключается на лету.
@@ -28,7 +27,7 @@ git репозитория**. Канон никем не подключаетс
Конвенция формулируется независимо от конкретного приложения. Она задаёт
правило; код ему следует. Обратное направление запрещено: то, что
приложение уже делает иначе, **не является аргументом против правила** — это
отступление, и его место в локальном регионе того репозитория, а не в
отступление, и его место в локальной части копии того репозитория, а не в
переформулировке канона.
Отсюда практические следствия:
@@ -51,11 +50,12 @@ conventions/
stack/<стек>/ привязка к инструменту, хранилищу, транспорту
```
Пути **файлов** даются относительно `conventions/`: `arch/db-identifiers.md`,
а не `conventions/arch/…` — так же, как они лягут в `docs/conventions/`
репозитория. На **правила** ссылаются идентификатором без пути: `KEYS-5`.
Префикс уникален по всему канону (реестр — `prefixes.toml`), поэтому
идентификатор не зависит от того, на какой оси файл лежит сегодня.
Оси — раскладка **канона**; в репозитории копия лежит плоско, файлом на
тему. Пути файлов даются относительно `conventions/`
(`arch/db-identifiers.md`) и адресуют исходник канона, а не место в копии.
На **правила** ссылаются идентификатором без пути: `KEYS-5`. Префикс
уникален по всему канону (реестр — `prefixes.toml`), поэтому идентификатор
не зависит ни от оси, ни от того, как собран файл у потребителя.
Тест — по тому, замена чего убивает правило:
@@ -93,6 +93,12 @@ prefix: KEYS
по формуле, и не переиспользуется никогда. Правила адресуются идентификатором
`KEYS-5` — без пути к файлу. Подробности формы — `LANGUAGE.md`.
Буква `X` в начале префикса зарезервирована за репозиториями: канон её не
занимает никогда, а локальные правила потребителя берут префиксы только на
неё (`XTIM`, `XLOG`). Так столкновение локального префикса с будущим
префиксом канона невозможно по построению, и согласовывать заранее ничего не
нужно.
## Расширение
Файл в `lang/` или `stack/` может объявить в шапке ещё и базу:
@@ -106,67 +112,88 @@ extends: arch/db-identifiers.md
неверно сформулировано условие применимости (чинится в каноне), либо
репозиторий на базу просто не подписан.
`extends` — документация связи, а не механизм: `conv` о ней только
напоминает при `add` и никак не следит за тем, чтобы база лежала рядом.
`extends` — документация связи, а не механизм: за тем, чтобы база лежала
рядом, никто не следит.
## Служебная разметка
## Копия в репозитории
**Шапка копии** ставится при `conv add` и в каноне не хранится:
```yaml
---
origin: arch/time.md # откуда взято
origin_hash: a1b2c3d4 # отпечаток канона на момент синхронизации
synced: 2026-07-25
local: нет # или: чем и почему разошлись
---
```
`origin_hash` — контент-отпечаток, а не git-SHA. Он позволяет отличать
«канон обновился» от «изменено локально»; без него `status` умеет только
«differs», а такой отчёт быстро перестают читать. Прочие ключи шапки
(`prefix`, `extends`) — часть документа: они сравниваются наравне с телом.
**Локальные регионы** — куски, принадлежащие репозиторию по определению.
Из сравнения исключаются, поэтому вечного шума в `diff` не дают:
```markdown
<!-- local:механизировано -->
`AUTOINCREMENT` в новых миграциях — `internal/archrules`.
<!-- /local -->
```
Имя обязательно — перенос при `pull` идёт по именам, безымянные регионы
`conv` отвергает. Что всегда локально:
- **механизация** — канон не знает, у кого линтер уже настроен;
- **отступления** — «у нас пока не так», честно и поимённо;
- **разрешение условия** — «Здесь: INTEGER PK, id наружу не выходят»;
- **эталоны и ссылки** — имена функций, файлов, ADR конкретного репозитория;
- **список конвенций** в README репозитория.
Путь файла в каноне и имя региона — это API: переименование осиротит все
копии (`origin` строковый). Переименовывать — только вместе с обходом
потребителей.
После `pull` копию нужно перечитать глазами: содержимое региона могло
устареть относительно переписанного вокруг текста, и автоматика этого не
увидит.
## Раскладка в репозитории
Копии повторяют структуру канона:
Копия плоская: **один файл на тему**, слои осей идут внутри него секциями в
порядке `arch` → язык → стек. Пути канона в копии не воспроизводятся.
```
docs/conventions/
README.md собственный, не синхронизируется
arch/db-identifiers.md
lang/go/db-identifiers.md
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/…
```
Подписка не описана отдельным файлом — она **и есть** набор лежащих файлов,
видимый в `git ls-files`. README директории перечисляет их одной плоской
таблицей, чтобы вложенность не мешала навигации.
Так конвенция остаётся самодостаточным документом: тот, кто проверяет по ней
код — человек или агент, — читает один файл и не собирает тему из трёх мест.
**Шапка копии** ставится при сборке и в каноне не хранится:
```yaml
---
origin: time
---
```
Больше в шапке ничего нет. Отпечатка канона и даты синхронизации в ней не
хранится: обновление перезаписывает файл в рабочем дереве, и что именно
изменилось, показывает `git diff` до коммита. Второй механизм сравнения
рядом с git не нужен.
**Маркер локальной части** — единственная машинно значимая разметка внутри
файла:
```markdown
<!-- conv:local -->
MIGR-2, MIGR-4 механизированы — `internal/archrules`.
MIGR-6 не соблюдается в `show_history`, `queue`: составные ключи там
появились до конвенции, миграция данных не окупается.
```
Всё ниже маркера принадлежит репозиторию и переживает обновление; всё выше —
пересобирается из канона. Маркер один и безымянный, поэтому у него нет
имени, которое можно осиротить переименованием.
Ниже маркера живёт то, чего канон о репозитории не знает: механизация,
отступления, разрешение условий («Здесь: INTEGER PK, id наружу не выходят»),
ссылки на ADR и код, а также **собственные правила**с префиксом на `X`,
по тем же правилам формы, что и канон.
Если местных правок стало больше, чем каноничного текста, копия перестаёт
быть копией: `origin:` из шапки убирают, и дальше это обычный документ
репозитория. Файл, оставивший шапку, при следующем обновлении потеряет
всё, что выше маркера.
## Манифест
Откуда взяты копии и где брать обновления — `.conventions.toml` в корне
репозитория-потребителя:
```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: копии закоммичены, автоматического обновления не
существует, и любое изменение проходит через чтение диффа человеком.
## Контракт с агентом
@@ -175,45 +202,53 @@ docs/conventions/
`AGENTS.md` каждого потребителя должен явно говорить:
> Файлы в `docs/conventions/` с шапкой `origin:` — копии из канона
> `dev-conventions`. Репозиторное пишется только внутрь
> `<!-- local:… -->`. Правка вне регионов — либо `conv push` в канон, либо
> запись причины в `local:`.
> `dev-conventions`. Репозиторное пишется только ниже `<!-- conv:local -->`;
> всё выше маркера перезаписывается при обновлении. Своё правило — с
> префиксом на `X`.
## Команды
```bash
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 # завести в каноне новую конвенцию
conv list # какие темы есть в каноне
conv add time # добавить тему в манифест и собрать файл
conv pull # пересобрать всё, что перечислено в манифесте
```
`status` и `diff` всегда завершаются кодом 0: это отчёт, а не проверка.
Расхождение — нормальное состояние, а постоянный шум в `diff` означает не
«догони канон», а «пора разрезать файл». Симметрично: разросшийся до спора
с базой локальный регион означает «пора чинить условие применимости в
каноне».
Отчёт о том, что изменилось, отдельной командой не выдаётся: после `pull`
его показывает `git diff`, а решение — принять, поправить или откатить —
принимает человек перед коммитом.
Транспорт обратно в канон не предусмотрен. Улучшение, найденное в
репозитории, переносится в канон руками: это редкая операция, и её цена —
не аргумент против того, чтобы направление оставалось односторонним.
Запускать из корня репозитория:
```bash
~/projects/private/dev-conventions/conv status
~/projects/private/dev-conventions/conv pull
```
Обёртка в раннере репозитория (`inv conventions -- status` для ansible,
`task conventions -- status` для Go) — тонкий проброс аргументов, чтобы
Обёртка в раннере репозитория (`inv conventions -- pull` для ansible,
`task conventions -- pull` для Go) — тонкий проброс аргументов, чтобы
логика не размножалась по репозиториям в двух диалектах.
## Жизненный цикл
- **В канон.** Новая конвенция пишется в том репозитории, где заболело, и
продвигается `conv push --new`. Локальные регионы при этом опустошаются:
в канон едет только норма.
переносится в канон, когда стало ясно, что общего в ней больше, чем
местного. Локальная часть при этом не едет: в канон попадает только норма,
а префикс на `X` меняется на канонический — то есть правила получают новые
идентификаторы.
- **Из канона.** Устаревшая конвенция удаляется вместе с обходом
потребителей — тихо осиротить копии нельзя.
- **История.** Канон коммитится при каждом `push`: `origin_hash` отвечает
на «отличается ли», но только git канона отвечает на «почему база
сформулирована так».
- **История.** Канон коммитится при каждой правке: только git канона
отвечает на вопрос, почему база сформулирована так.
## Состояние
Модель выше — согласованная, а не реализованная. `conv` пока собран под
прежнюю: зеркальное дерево копий вместо плоского, именованные регионы
`<!-- local:имя -->` вместо одного маркера, `origin_hash` в шапке и команды
`status`, `diff`, `push`. Двенадцать файлов канона всё ещё несут 31 пустой
именованный регион. Ни один репозиторий-потребитель не подключён, поэтому
переход никого не ломает.