Compare commits

..
22 Commits
Author SHA1 Message Date
av 271603122d манифест приведён к машинному виду, объявлен governance
- добавлен ключ governance: без него конвенция, потерявшая topic, была
  неотличима от GUIDE.md и тихо теряла проверки об отъезде к потребителю
- комментарии из манифеста убраны — их всё равно съела бы первая же
  команда; то, чего не было в README.md, дописано туда
2026-07-28 10:16:39 +03:00
av 34d53d667d удалены TOOL.md и conv, инструкции переведены на convy
- TOOL.md был стартовой точкой разработки инструмента и свою задачу
  выполнил: решения о нём теперь живут в его собственном репозитории,
  открытые вопросы перенесены туда же
- питоновский conv собран под прежнюю модель копий (зеркальное дерево,
  именованные регионы, origin_hash) и удалён вместе с ней
- команды в README.md переписаны на convy, включая suite-сторону и sync
2026-07-28 10:05:22 +03:00
av 0d335ff58d манифест набора переименован в .conventions-suite.toml
- оба манифеста теперь данные инструмента: он их читает и переписывает
  целиком, комментариев они не держат — точка в начале ставит их рядом
  со служебными файлами, а не среди содержимого репозитория
- прежнее основание из TOOL.md («в наборе файл правится при каждой новой
  теме, и прятать его незачем») отпало: правит его convy
- ссылки в README.md, CLAUDE.md, TOOL.md и TODO.md обновлены
2026-07-28 09:54:52 +03:00
av 9a3a89358f logging: убран extends на чужую тему
- шапка `lang/go/logging.md` объявляла базой `arch/time.md` — копипаста
  из соседнего `lang/go/time.md`, единственного файла с этой базой
- у темы `logging` арх-слоя нет вовсе, расширять было нечего; ключ
  вернётся сам, когда невыделенное ядро уедет в `arch/logging.md`
- `convy suite check` на наборе проходит чисто
2026-07-28 09:48:56 +03:00
av 7fb60828db guide: граница со спекой переписана на тест наблюдаемости вердикта
- «что против как» на пограничных правилах не работает: capability
  проверяется снаружи работающей системы, конвенция — только в исходном
  тексте, и отсюда расходятся направление, распространение и шкала
- добавлен признак для спорного случая: обязательство перед внешним
  потребителем — в спеку, зависимость автора следующего патча — в конвенцию
2026-07-27 08:57:35 +03:00
av 9c86d9f2de компоненты как адресат сборки и плоский набор
- компонент — область репозитория, где выбранные слои действуют
  одновременно; сборка идёт по разу на компонент, у каждого своя директория
  копий, подписка и локальная часть, секции [components.<имя>] в манифесте
- плоский набор описан как низкий конец модели, а не отдельный режим: тема с
  одним слоем собирается копированием, ключи оси и lang/stack не пишутся
- в TODO заведён вопрос о реестре значений осей и судьбе extends:
2026-07-26 22:01:39 +03:00
av 787d0bb5ea guide: имя темы и объявленная ось — META-37, META-38
- META-37: тема называется решением и адресатом, а не ролью части проекта;
  логи сервера и браузера — logging и client-logging, а не суффиксная пара
- META-38: ось слоя объявляется ключами lang/stack в шапке, а не выводится
  из пути — переезд файла между директориями иначе молча менял состав копии
  у каждого потребителя; шапки двенадцати конвенций приведены к правилу
- в список проверок добавлены объявление оси, единственность базового слоя
  и совпадение объявленного с директорией
2026-07-26 22:01:38 +03:00
av 11fc9e1fee guide: заведены критерии границы темы — META-33…META-36
- тема определяется решением, а не веществом (META-33) и нужна потребителю
  целиком (META-34); слой сужает базу, но не отменяет её (META-35), иначе
  это другая тема, а вид приложения называется в области действия (META-36)
- добавлен раздел «Как проверить границу темы»: пять вопросов со ссылками
  на правила, включая META-20; те же строки в CLAUDE.md, а в LANGUAGE.md
  оговорка, что граница темы языку не принадлежит
- в TODO заведён прогон восьми тем по критериям с разбором подозреваемых:
  шесть правил time выносят вердикты чужих тем, logging мешает три страта
2026-07-26 21:25:24 +03:00
av b516bfb02c manifest.toml переименован в suite.toml
- родовое «манифест» заменено именем уровня: файл в наборе описывает сам
  набор, файл в проекте — подключённые конвенции, и каждый назван по тому,
  что описывает
- по имени рядом лежащего манифеста определяется контекст: suite.toml —
  набор, .conventions.toml — проект; в TOOL.md решение зафиксировано, из
  открытых вопросов убрано
2026-07-26 17:06:29 +03:00
av e8fdc98557 заведён TOOL.md — стартовая точка для convy
- имя, термины suite/project, раскладка команд: проектные наверху, ведение
  набора под подкомандой suite, check остаётся общим
- собрано в одном месте то, что инструмент делает и чего не делает: сборка
  копии, три группы проверок, независимость от конкретного набора, отказ от
  слияния, лока и обратного транспорта
- два тулинговых вопроса вынесены из TODO в раздел «Открытые вопросы»;
  вопросы про инструмент там больше не живут
2026-07-26 17:00:00 +03:00
av c96566d4b4 todo: заведён разбор шести сниппетов в блоках нормы
- GCFG-7, GCFG-9, SLOG-20, GTIM-8, HTMX-7, HTMX-24 несут код внутри нормы,
  то есть требуют ровно такой код; с появлением блока ПРИМЕРЫ часть из них
  туда и переезжает
- записана цена ошибки в обе стороны: деталь кода как требование против
  нормы, потерявшей обязательность в иллюстративном блоке
2026-07-26 16:14:26 +03:00
av 59a1c23f55 язык: заведён блок ПРИМЕРЫ
- пятая, необязательная часть правила: код парой «плохо → хорошо» после
  обоснования; метка добавлена в словарь (EXAMPLES) и в строку о версии
  языка во всех тринадцати файлах
- сказано, чем примеры не являются: требований в блоке нет, дословным
  сниппетом он не служит, при расхождении с нормой правят пример
- READING.md обновлён по META-30, в машинные проверки добавлен порядок
  блоков, в читательские — что примеры норму не расширяют
2026-07-26 16:09:58 +03:00
av 1d19e0357b язык: усиления источников названы своими
- три клетки «что взято» правились по факту: DMN даёт политику совпадения,
  но не требует полноты; в 29148 обоснование — рекомендуемый атрибут; вывод
  про обязательность шаблона в EARS не сформулирован
- заведён раздел «Где источник усилен»: полнота таблиц, обязательность
  обоснования и вывод из EARS предъявлены как наши решения с доводами
- поправлены два места, где та же натяжка повторялась прозой: «Таблицы
  решений» и «Обоснование обязательно»
2026-07-26 16:03:47 +03:00
av 682fa075bb снятое правило остаётся заглушкой, нумерация сплошная
- META-31 и META-32: номера идут без пропусков, ссылка обязана разрешаться;
  обе проверки стали механическими — данных со стороны языка им хватает
- заведена метка СНЯТО: заголовок и номер снятого правила сохраняются, норму
  с обоснованием заменяет блок с датой и причиной, отдельный реестр снятых
  номеров не нужен
- META-9, META-16 и META-26 переписаны из таблицы «Освободившиеся номера» в
  заглушки; три висячие ссылки, тянувшиеся с утра, закрылись
2026-07-26 15:59:22 +03:00
av 170c06c1da язык: короткое описание для читателя копии едет в репозиторий
- заведён READING.md: словарь со значениями, форма правила и её граница,
  ссылки, локальная часть — без разделов о ведении набора и без META-ссылок,
  примеры на X-префиксах
- сборщик кладёт его в docs/conventions/ и перезаписывает целиком; в манифест
  набора добавлена секция [language] с версией и двумя документами
- META-30: правка словаря или состава частей правила доходит до READING.md,
  иначе потребитель толкует ДОПУСКАЕТСЯ по прежней версии
2026-07-26 15:51:54 +03:00
av 67d51db212 механизация больше не разрешает удалять норму
- META-9 снят: удаление оставляло подписчика, пришедшего после, без нормы и
  без проверки, а «механизировано у всех» канону не проверить — списка
  подписчиков у него нет по построению
- META-8 переписан в запрет: норма остаётся в правиле, чем бы её ни
  проверяли; линтер сообщает, что нарушено, но не что требуется (META-6)
- МЕХАНИЗИРОВАНО объявлена свойством репозитория: в тексте конвенции отметки
  нет, её место — запись о механизации ниже маркера (META-7)
2026-07-26 15:41:52 +03:00
av fe61ecd6c5 guide: язык употребляется, значит и проверяется как в конвенции
- проверки разведены по роли слова: то, что язык употребляет (конвенции и
  GUIDE.md), проверяется; то, что цитирует (LANGUAGE.md, README.md), — нет
- список машинных проверок разбит на форму правила и распространение:
  вторая группа (тема в шапке, пути канона, чужие префиксы, локальные X)
  касается только того, что едет к потребителю
- в GUIDE.md добавлена строка о версии языка и убрано заглавное СЛЕДУЕТ из
  вводной прозы «Оформления» — единственное нарушение, которое исключение
  прятало
2026-07-26 15:34:49 +03:00
av c1cb240540 темы: определение, объявление в шапке и манифест набора
- тема — набор правил об одном фокусе разработки, имя латиницей (нижний
  kebab-case рекомендуется, годится любой идентификатор, пригодный для имени
  файла); определение в LANGUAGE.md и README.md
- заведены META-28 и META-29: тема объявляется в шапке (`topic:`), стоит в
  манифесте набора и не переиспользуется; `topic:` добавлен во все 12 файлов
- prefixes.toml и topics.toml слиты в manifest.toml — манифест набора против
  манифеста подключения `.conventions.toml`, разделы topics/prefixes с live
  и retired
2026-07-26 15:27:55 +03:00
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
av df8c58671f язык: объявлена граница правила
- область правила — от его заголовка до следующего заголовка любого уровня;
  метка открывает блок, хвост после ПОЧЕМУ — продолжение обоснования, а
  таблица после модальной метки — часть нормы
- проверка «заглавных модальных слов вне правил нет» стала реализуемой:
  прозой считается то, что лежит вне областей правил
- нормы, сидевшие в хвостах, подняты в блок нормы: заведены SLOG-25.4 и
  GERR-26.3, у GTIM-12 «базовый слой» заменён на TIME-12
2026-07-26 15:10:59 +03:00
av 4943bf1dd2 второе условие ДОЛЖЕН ослаблено до воспроизводимости вердикта
- META-6 переписан: ступень требует не машинной проверки, а того, чтобы
  двое проверяющих по тексту правила выносили один вердикт; проверяющий по
  умолчанию — читатель, человек или агент
- заведён META-27: машинная проверка желательна везде, где пишется, но
  ступени не задаёт — иначе канон стоит в СЛЕДУЕТ до появления скриптов;
  обоснование META-25 пересобрано на новом условии
- LANGUAGE.md и CLAUDE.md приведены к той же формулировке, вопрос 1 из
  TODO закрыт, остальные перенумерованы
2026-07-26 15:01:51 +03:00
av 7b2869d3a4 todo: заведена секция «Язык и подход» по итогам ревью
- девять находок ревью описания языка записаны вопросами 1–9: противоречие в
  условиях ДОЛЖЕН, неопределённая граница правила, примеры на живых
  идентификаторах, «тема» без реестра, ложное исключение GUIDE из проверок,
  МЕХАНИЗИРОВАНО без защиты нового подписчика, семантика слов вне копии,
  проверки без источника данных, натяжки в опоре на стандарты
- вопросы поделены на две секции: язык с подходом идёт первым, канон с
  тулингом вторым; «Мелкое» слито в вопрос 8, куда относилось по смыслу
- нумерация сплошная 1–15, внутренние ссылки переписаны под неё; в вопрос 12
  добавлена зависимость чекера от вопросов 2 и 8
2026-07-26 14:49:41 +03:00
21 changed files with 1267 additions and 938 deletions
+34
View File
@@ -0,0 +1,34 @@
governance = "GUIDE.md"
[language]
version = 1
lang = "ru"
description = "LANGUAGE.md"
reading = "READING.md"
[topics]
[topics.live]
app-directories = "категории директорий приложения и что в каждой лежит"
config = "конфигурация: файл, валидация, секреты"
db-identifiers = "идентификаторы сущностей: вид ключа, генерация, границы"
db-schema = "схема БД и миграции: типы колонок, форма изменения"
errors = "ошибки: обёртки, границы трансляции, паники"
logging = "логирование: уровни, структура записи, что не логируем"
time = "время: хранение, зоны, форматы, календарные границы"
web-ui = "веб-UI: партиалы, свопы, поллинг"
[prefixes]
[prefixes.live]
ANSD = "conventions/stack/ansible/app-directories.md"
CONF = "conventions/arch/config.md"
DIRS = "conventions/arch/app-directories.md"
GCFG = "conventions/lang/go/config.md"
GERR = "conventions/lang/go/errors.md"
GKEY = "conventions/lang/go/db-identifiers.md"
GTIM = "conventions/lang/go/time.md"
HTMX = "conventions/stack/htmx/web-ui.md"
KEYS = "conventions/arch/db-identifiers.md"
META = "GUIDE.md"
MIGR = "conventions/lang/go/db-schema.md"
SLOG = "conventions/lang/go/logging.md"
TIME = "conventions/arch/time.md"
+103 -35
View File
@@ -9,8 +9,9 @@ code in this repository.
Канон конвенций разработки для личных проектов. Сами конвенции лежат в Канон конвенций разработки для личных проектов. Сами конвенции лежат в
`conventions/{arch,lang/<язык>,stack/<стек>}/`; обвязка канона (`README.md`, `conventions/{arch,lang/<язык>,stack/<стек>}/`; обвязка канона (`README.md`,
`LANGUAGE.md`, `GUIDE.md`, `prefixes.toml`, `conv`) живёт в корне и в `LANGUAGE.md`, `GUIDE.md`, `READING.md`, `.conventions-suite.toml`) живёт в
репозитории-потребители не едет. корне. К потребителю из неё едет только `READING.md` — короткое описание языка
для читателя копий.
Ниже — короткие инварианты с идентификаторами; детали и обоснования в Ниже — короткие инварианты с идентификаторами; детали и обоснования в
`LANGUAGE.md` (форма записи) и `GUIDE.md` (процесс, префикс META). `LANGUAGE.md` (форма записи) и `GUIDE.md` (процесс, префикс META).
@@ -20,7 +21,15 @@ code in this repository.
- Четыре обязательные части: `### <ПРЕФИКС>-<N>. Заголовок`, абзац - Четыре обязательные части: `### <ПРЕФИКС>-<N>. Заголовок`, абзац
`**МОДАЛЬНОСТЬ.** норма`, абзац `**ПОЧЕМУ.** …`. Правило без обоснования не `**МОДАЛЬНОСТЬ.** норма`, абзац `**ПОЧЕМУ.** …`. Правило без обоснования не
принимается. принимается.
- `**ПРИМЕРЫ.**` — необязательный пятый блок после обоснования: код парой
«плохо → хорошо». Иллюстрация нормы, а не спецификация — требований в блоке
нет, дословным сниппетом он не является, при расхождении действует норма.
- Норма — одна фраза; если в неё не влезает, это два правила. - Норма — одна фраза; если в неё не влезает, это два правила.
- Область правила — от его заголовка до следующего заголовка любого уровня;
метка открывает блок, блок длится до следующей метки или до конца области.
Абзацы после `**ПОЧЕМУ.**` — продолжение обоснования: требований в них не
живёт, требование ставят в блок нормы. Таблица и список после модальной
метки — часть нормы.
- Шкала инвариантна, словарь — параметр языка набора. Канон русский, значит - Шкала инвариантна, словарь — параметр языка набора. Канон русский, значит
слова: **ДОЛЖЕН**, **НЕ ДОЛЖЕН**, **СЛЕДУЕТ**, **НЕ СЛЕДУЕТ**, слова: **ДОЛЖЕН**, **НЕ ДОЛЖЕН**, **СЛЕДУЕТ**, **НЕ СЛЕДУЕТ**,
**ДОПУСКАЕТСЯ**. Словарь один на канон, синонимов на ступень нет. **ДОПУСКАЕТСЯ**. Словарь один на канон, синонимов на ступень нет.
@@ -28,14 +37,20 @@ code in this repository.
- Нормативно только заглавное написание (правило RFC 8174): строчное - Нормативно только заглавное написание (правило RFC 8174): строчное
«должен» в прозе нормой не является. «должен» в прозе нормой не является.
- ДОЛЖЕН требует двух условий сразу: назван вред от нарушения (META-25) и - ДОЛЖЕН требует двух условий сразу: назван вред от нарушения (META-25) и
норма проверяема машиной (META-6). Проверяемость сама по себе до ДОЛЖЕН не вердикт о нарушении воспроизводим (META-6). Воспроизводимость сама по себе
повышает — иначе шкала наполняется проверяемыми мелочами. до ДОЛЖЕН не повышает — иначе шкала наполняется проверяемыми мелочами.
- ДОПУСКАЕТСЯ адресовано рецензенту: помеченный им выбор на ревью не - ДОПУСКАЕТСЯ адресовано рецензенту: помеченный им выбор на ревью не
обсуждается. обсуждается.
- **МЕХАНИЗИРОВАНО** — не модальность, а способ проверки: отметка стоит - **МЕХАНИЗИРОВАНО** — не модальность, а способ проверки, и свойство
рядом с модальным словом (`**ДОЛЖЕН. МЕХАНИЗИРОВАНО.**`), а не вместо него. репозитория, а не канона: в тексте конвенции отметки нет, она стоит при
- Метки правила — **ПОЧЕМУ** и **МЕХАНИЗИРОВАНО** — тоже словарь набора и записи о механизации в локальной части копии (META-7).
перечислены в строке о версии языка наравне с модальными словами. - META-8: норма не удаляется из канона никогда, чем бы её ни проверяли.
Механизация её не заменяет и не сокращает.
- Метки правила — **ПОЧЕМУ**, **ПРИМЕРЫ**, **МЕХАНИЗИРОВАНО** и **СНЯТО**
тоже словарь набора и перечислены в строке о версии языка наравне с
модальными словами.
- META-30: правка словаря или состава частей правила доходит до `READING.md`
документа, который едет к потребителю. Словари двух описаний совпадают.
- Заглавные модальные слова не употребляются вне правил: ни в «Область - Заглавные модальные слова не употребляются вне правил: ни в «Область
действия», ни в «Связано», ни в локальной части копии, ни во вводной прозе. действия», ни в «Связано», ни в локальной части копии, ни во вводной прозе.
Исключение — строка о версии языка, которая их перечисляет. Исключение — строка о версии языка, которая их перечисляет.
@@ -55,18 +70,32 @@ code in this repository.
действия. действия.
- Модальность принадлежит правилу, а не файлу: `status:` в шапке отменён. - Модальность принадлежит правилу, а не файлу: `status:` в шапке отменён.
## Идентификаторы и префиксы ## Идентификаторы: тема и префикс
- Тема — набор правил об одном фокусе разработки и единица подписки. Имя —
латиницей, рекомендуется нижний kebab-case, годится любой идентификатор,
пригодный для имени файла.
- META-28: тема объявлена в шапке (`topic: time`) и стоит в манифесте набора
(`.conventions-suite.toml`, секция `[topics.live]`). Слои одной темы несут
одно имя — по нему собираются в один файл, как бы ни назывались их файлы;
имя файла повторяет тему из удобства.
- META-29: имя темы не переиспользуется, снятое уходит в `[topics.retired]`
с причиной и датой. Оно живёт в `origin:` копий и в подписках манифестов.
- Формат `<ПРЕФИКС>-<номер>`, нумерация сквозная внутри файла. Порядок правил - Формат `<ПРЕФИКС>-<номер>`, нумерация сквозная внутри файла. Порядок правил
в файле — по читаемости: номер это идентификатор, а не позиция. в файле — по читаемости: номер это идентификатор, а не позиция.
- Идентификаторы не переиспользуются. Удалённое правило оставляет дыру, новое - Идентификаторы не переиспользуются: новое правило берёт номер, следующий за
берёт следующий свободный номер, а не первый освободившийся. наибольшим.
- META-31: нумерация в файле сплошная. Снятое правило не исчезает, а остаётся
заглушкой: заголовок с номером плюс блок `**СНЯТО <дата>.**` с причиной
вместо нормы и ПОЧЕМУ. Реестра снятых номеров нет — файл сам себе реестр.
- META-32: ссылок на несуществующие правила нет; неразрешённый идентификатор —
всегда ошибка, а не «правило, наверное, сняли».
- Новый файл конвенции — новый префикс: четыре заглавные латинские буквы, - Новый файл конвенции — новый префикс: четыре заглавные латинские буквы,
уникальные по всему канону, выбираются под файл, а не выводятся по формуле. уникальные по всему канону, выбираются под файл, а не выводятся по формуле.
Объявляется в шапке (`prefix: KEYS`) и регистрируется в `prefixes.toml`, Объявляется в шапке (`prefix: KEYS`) и регистрируется в манифесте набора,
секция `[live]`, путём от корня репозитория. секция `[prefixes.live]`, путём от корня репозитория.
- Удаление или разделение файла: префикс уходит в `[retired]` с причиной и - Удаление или разделение файла: префикс уходит в `[prefixes.retired]` с
датой, а не освобождается. причиной и датой, а не освобождается.
- Префиксы на букву `X` канон не занимает: они зарезервированы за локальными - Префиксы на букву `X` канон не занимает: они зарезервированы за локальными
правилами репозиториев-потребителей. правилами репозиториев-потребителей.
- Перенос правила в другой файл — смысловое изменение: новый префикс и новый - Перенос правила в другой файл — смысловое изменение: новый префикс и новый
@@ -93,34 +122,69 @@ code in this repository.
- META-5: расхождение кода с правилом — отступление, а не повод переписать - META-5: расхождение кода с правилом — отступление, а не повод переписать
правило. Направление всегда конвенция → код; факт «в приложении уже иначе» правило. Направление всегда конвенция → код; факт «в приложении уже иначе»
не является аргументом. не является аргументом.
- META-6: ДОЛЖЕН без механической проверки либо механизируется, либо - META-6: ДОЛЖЕН требует воспроизводимого вердикта — двое проверяющих по
понижается в СЛЕДУЕТ. Правило, непроверяемое машиной в принципе (вкус тексту правила отвечают одинаково. Правило, вердикт которого зависит от
формулировки, суждение о ситуации), — СЛЕДУЕТ по построению. суждения (вкус формулировки, уместность в конкретном месте), — СЛЕДУЕТ по
построению. META-27: машинная проверка желательна, но ступени не задаёт;
проверяющий по умолчанию — читатель правила, человек или агент.
- META-10: блок ПОЧЕМУ не удаляется никогда, в том числе после того, как - META-10: блок ПОЧЕМУ не удаляется никогда, в том числе после того, как
норма уехала в линтер. правило стало проверяться линтером.
- META-1: один файл — ровно один повторяющийся выбор. META-2: конвенция - META-1: один файл — ровно один повторяющийся выбор. META-2: конвенция
заводится, когда решение принимается третий раз. заводится, когда решение принимается третий раз.
- Репозиторного в каноне нет вовсе: механизация, отступления и ссылки на код - Репозиторного в каноне нет вовсе: механизация, отступления и ссылки на код
живут в копии ниже маркера `<!-- conv:local -->` (META-22), который ставит живут в копии ниже маркера `<!-- conv:local -->` (META-22), который ставит
сборщик. Заводить пустые местные разделы в каноне не нужно. сборщик. Заводить пустые местные разделы в каноне не нужно.
## Граница темы
Пять вопросов, по которым тему проверяют на «не хапнули ли лишнего»; сначала
разрез темы, ось — потом.
- META-33: правило стоит в теме, чей вопрос оно решает. Тема — решение, а не
вещество: «время» проходит через несколько решений сразу, и правило о
колонках БД принадлежит схеме, а не времени.
- META-37: имя темы называет решение и адресата, а не роль части проекта:
`logging` и `client-logging`, но не `logging-backend`/`logging-frontend`.
- META-34: тема нужна потребителю целиком — подписка берёт её без остатка.
Если два правдоподобных потребителя хотят непересекающиеся части, между
ними и проходит граница.
- META-35: слой сужает базу, но не отменяет её. Приходится отменять — это не
слой, а другая тема; общим осталось слово, а не решение.
- META-36: вид приложения (веб-сервис, CLI, плейбуки, библиотека) называется
в области действия, если норма от него зависит. Осью он не является.
- META-20: норма исполнима без соседних тем.
## Выбор оси ## Выбор оси
Умирает при смене языка → `lang/<язык>/`. Умирает при смене инструмента, Умирает при смене языка → `lang/<язык>/`. Умирает при смене инструмента,
хранилища или транспорта → `stack/<стек>/`. Не умирает ни от того, ни от хранилища или транспорта → `stack/<стек>/`. Не умирает ни от того, ни от
другого → `arch/`. Ось определяется природой правила, а не числом сегодняшних другого → `arch/`. Ось определяется природой правила, а не числом сегодняшних
потребителей. `extends: arch/<файл>.md` в шапке — документация связи, а не потребителей. `extends: arch/<файл>.md` в шапке — документация связи, а не
механизм; слой только реализует и сужает базу, но не отменяет её. механизм; слой только реализует и сужает базу, но не отменяет её (META-35).
META-38: ось объявлена в шапке ключами `lang:` и `stack:`, а не выведена из
пути; без обоих ключей файл — базовый слой темы. Директория повторяет
объявленное для человека. Осей может не быть вовсе: набор, где у темы один
слой, — низкий конец той же модели, а не особый режим.
## Компоненты
Компонент — область репозитория, где все выбранные слои действуют
одновременно (`sqlite` и `postgres` — да, go и javascript — никогда). Уровней
три: набор → проект → компонент. Сборка прогоняется по разу на компонент, у
каждого своя директория копий, своя подписка и своя локальная часть; в
`.conventions.toml` они записаны секциями `[components.<имя>]` с ключами
`dir`, `lang`, `stack`, `topics`. Компонент пишется всегда, даже когда он
один. Директории компонентов различны — этим копии и разводятся.
## Оформление файла ## Оформление файла
Шапка `prefix:` (плюс `extends:`) → `# Тема` → вводная проза → отдельным Шапка `topic:` и `prefix:` (плюс `extends:`) → `# Тема` → вводная проза →
абзацем строка о версии языка (её точный текст — в `LANGUAGE.md`, раздел отдельным абзацем строка о версии языка (её точный текст — в `LANGUAGE.md`,
«Ссылка на язык из конвенции») → `## Область действия` (обязателен для раздел «Ссылка на язык из конвенции») → `## Область действия` (обязателен для
трудноизменяемых слоёв — META-11) → правила → `## Связано`, если трудноизменяемых слоёв — META-11) → правила → `## Связано`, если канонические
канонические ссылки есть (META-17; пустого раздела не заводят). Имя файла ссылки есть (META-17; пустого раздела не заводят). Имя файла повторяет имя
kebab-case по теме. Проза темы. Проза переносится по ~76 колонок; таблицы и блоки кода не переносятся.
переносится по ~76 колонок; таблицы и блоки кода не переносятся.
## Ревью формы ## Ревью формы
@@ -128,6 +192,12 @@ kebab-case по теме. Проза
проверять машиной». Ни одна из проверок не реализована, поэтому при ревью их проверять машиной». Ни одна из проверок не реализована, поэтому при ревью их
выполняют чтением. выполняют чтением.
Проверяется всё, что язык **употребляет**: конвенции и `GUIDE.md` (он несёт
правила META и строку о версии языка). `LANGUAGE.md` и `README.md` язык
цитируют — ключевые слова в них предмет описания, а не норма. Проверки
распространения (тема в шапке, самодостаточность нормы, отсутствие путей
канона) касаются только конвенций: обвязка к потребителю не едет.
## Коммиты ## Коммиты
Русский, строчная буква, без точки в конце, прошедшее время или страдательный Русский, строчная буква, без точки в конце, прошедшее время или страдательный
@@ -138,14 +208,12 @@ kebab-case по теме. Проза
## Состояние репозитория ## Состояние репозитория
- Тестов, линтеров и CI нет. `conv` — python3 CLI на одной stdlib; запускают - Тестов, линтеров и CI здесь нет: репозиторий — данные, а не код. Проверяет
его из корня репозитория-потребителя (`~/projects/private/dev-conventions/` их `convy suite check`, живущий в своём репозитории и ставящийся бинарём.
плюс команда). - Модель копий, описанная в `README.md`, реализована в `convy`. Прежний
- Модель копий, описанная в `README.md`, согласована, но не реализована: питоновский `conv` удалён вместе со своей моделью (зеркальное дерево,
`conv` собран под прежнюю (зеркальное дерево, именованные регионы, именованные регионы, `origin_hash`). При расхождении обвязки с инструментом
`origin_hash`, команды `status`/`diff`/`push`). Сами конвенции к новой истина — README, а не код.
модели приведены — регионов в каноне нет. При правке обвязки истина —
README, а не код `conv`.
- Ни один репозиторий-потребитель ещё не подключён: копий с шапкой `origin:` - Ни один репозиторий-потребитель ещё не подключён: копий с шапкой `origin:`
в природе нет. в природе нет.
- `TODO.md` — площадка для обсуждения на будущее, а не принятые решения; при - `TODO.md` — площадка для обсуждения на будущее, а не принятые решения; при
+283 -61
View File
@@ -12,6 +12,13 @@ prefix: META
[LANGUAGE.md](LANGUAGE.md). Здесь — про то, зачем конвенции заводятся, где [LANGUAGE.md](LANGUAGE.md). Здесь — про то, зачем конвенции заводятся, где
живут и как соотносятся с соседними видами документов. живут и как соотносятся с соседними видами документов.
Правила ниже записаны тем же языком, что и сами конвенции, и проверяются так
же.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
## Область действия ## Область действия
Правила ниже адресованы автору конвенции — тому, кто заводит новый файл или Правила ниже адресованы автору конвенции — тому, кто заводит новый файл или
@@ -23,20 +30,38 @@ prefix: META
- `docs/adr/`**решение**, принятое однажды и постфактум («почему выбрали - `docs/adr/`**решение**, принятое однажды и постфактум («почему выбрали
Authelia, а не Keycloak»). Запись неизменяема. Authelia, а не Keycloak»). Запись неизменяема.
- `docs/specs/` и OpenSpec, где они есть, — **что** система делает, - `docs/specs/` и OpenSpec, где они есть, — контракт наблюдаемого поведения.
наблюдаемое поведение как контракт. Конвенция — **как** написан код; Конвенция в спеки не переносится: это не capability.
в спеки она не переносится, это не capability.
- `docs/drafts/` — оперативная хроника и черновики, «что собираюсь сделать». - `docs/drafts/` — оперативная хроника и черновики, «что собираюсь сделать».
- `docs/conventions/`**правило на будущее**, применяемое многократно. - `docs/conventions/`**правило на будущее**, применяемое многократно.
Живой документ: правится, когда договорённость меняется. Живой документ: правится, когда договорённость меняется.
Со спекой конвенцию путают чаще прочего, а «что против как» на границе не
работает. Разводит их то, **где наблюдается вердикт**. У capability он виден
снаружи работающей системы: подали вход, получили выход, совпало или нет. У
конвенции — только в исходном тексте: снаружи не различить, обёрнута ошибка
или проглочена и по какому признаку выбран уровень записи.
Отсюда расходится остальное. Спека едет за системой — изменилось поведение,
меняется контракт; конвенция ведёт код, и факт «в приложении уже иначе»
аргументом не считается (META-5), а утверждений о состоянии репозитория в ней
нет вовсе (META-4). Спека принадлежит одной системе; конвенция ездит копиями
и потому знает про темы, слои и локальную часть. Capability бинарна —
реализована или нет; у конвенции есть ступени и постоянный список отступлений
(META-13). Спеку пишут до кода, конвенцию — на третий раз (META-2).
Пограничное правило разбирается признаком внешнего потребителя. Формат логов,
который собирает чужой агрегатор, — обязательство перед кем-то снаружи, и
место ему в спеке. Если от правила зависит только автор следующего патча —
это конвенция.
## Оформление ## Оформление
Имя файла — kebab-case по теме: `app-directories.md`. Правилом это не Имя файла повторяет имя темы: `app-directories.md`. Правилом это не
записано, и это случай META-25: регуляркой имя проверяется тривиально, но записано, и это случай META-25: регуляркой имя проверяется тривиально, но
обоснование сводится к «чтобы имя файла в реестре префиксов писалось одним вреда от нарушения нет — тема объявлена в шапке (META-28), и сборка идёт по
способом» — вреда от нарушения нет, значит и высшей модальности нет, а на объявленному имени, а не по имени файла. Значит, и высшей модальности нет, а
СЛЕДУЕТ такое правило не окупает строчку. ступенью ниже такое правило не окупает строчку.
## Канон и копии ## Канон и копии
@@ -56,6 +81,23 @@ prefix: META
канон, или документ, переставший быть копией, — тогда `origin:` из шапки канон, или документ, переставший быть копией, — тогда `origin:` из шапки
убирают. убирают.
## Как проверить границу темы
Готовая тема проходится по шести вопросам; на каждый отвечает своё правило:
- на какой вопрос отвечает правило — и тот ли это вопрос, что у темы
(META-33);
- нужна ли тема правдоподобному потребителю целиком (META-34);
- слой сужает базу или отменяет её (META-35);
- зависит ли норма от вида приложения и назван ли он (META-36);
- названа ли тема решением и адресатом, а не ролью части проекта (META-37);
- исполнима ли норма, если соседних тем в репозитории нет (META-20).
Расхождение на любом из них означает, что граница проходит не там, где
нарисована: тема собрана вокруг вещества, склеила два решения или молча
предполагает вид приложения. Чинится это разрезом темы или областью
действия, а не смягчением нормы.
## Правила ## Правила
### META-1. Одна конвенция — один файл ### META-1. Одна конвенция — один файл
@@ -68,6 +110,165 @@ prefix: META
дорого: перенос правила в другой файл — это новый префикс и новая дорого: перенос правила в другой файл — это новый префикс и новая
нумерация, поэтому после разреза все внешние ссылки обходят руками. нумерация, поэтому после разреза все внешние ссылки обходят руками.
### META-33. Правило стоит в теме, чей вопрос оно решает
**ДОЛЖЕН.** Тема правила определяется вердиктом, который правило выносит, а
не веществом, о котором оно говорит.
**ПОЧЕМУ.** Одно и то же вещество — время, идентификатор, конфигурация —
проходит через несколько решений сразу, и тема, собранная вокруг вещества,
склеивает чужие решения: «в каком виде хранить в базе», «что писать в лог»,
«что отдавать наружу» попадают в один файл на том основании, что все три
говорят о моментах. Подписка после этого промахивается в обе стороны:
репозиторий без базы получает правила о колонках, а репозиторий с базой, не
подписанный на время, правил о своих колонках не получает — хотя они про его
схему. Отличить одно от другого дёшево: вопрос темы выписывается одной
фразой, и норма читается как ответ на него; ответ на чужой вопрос означает,
что правило лежит не в своей теме.
### META-34. Тема нужна потребителю целиком
**СЛЕДУЕТ.** Тема нарезается так, чтобы правдоподобному потребителю
требовалась вся она, а не часть.
**ПОЧЕМУ.** Взять половину темы нечем: подписка перечисляется темами, и
сборщик кладёт файл целиком. Потребитель, которому нужна треть правил,
платит за остальные две трети вычиткой при каждом обновлении и пачкой
отступлений — а пачка отступлений неотличима от небрежности и обесценивает
список, по которому считают реальное соблюдение (META-14). Линия разреза
видна заранее: если два правдоподобных потребителя хотят непересекающиеся
части одной темы, между этими частями и проходит граница. Ступень ниже
высшей потому, что «правдоподобный потребитель» — суждение: двое разойдутся
в том, бывает ли такой репозиторий вообще.
### META-35. Слой сужает базу, но не отменяет её
**НЕ ДОЛЖЕН.** Правило языкового или стекового слоя не требует
противоположного норме арх-слоя своей темы и не снимает её требование.
**ПОЧЕМУ.** Слои темы приезжают в копию одним файлом, секция за секцией, и
исполняются подряд: база и отменяющее её уточнение стоят рядом без указания,
какое из них главнее, — читатель выбирает сам, и вердикт перестаёт быть
воспроизводимым (META-6). Отсюда же тест на границу: если ради нового случая
базу приходится отменять, это не слой, а другая тема — общим у них осталось
слово, а не решение. Сужение слоем остаётся: уточнить, ограничить, назвать
инструмент, разобрать случай, который база предусмотрела.
### META-36. Вид приложения называется, если норма от него зависит
**ДОЛЖЕН.** Норма, верная не для всякого приложения, сопровождается областью
действия, называющей вид приложения, для которого она написана.
**ПОЧЕМУ.** Вид приложения — веб-сервис, программа командной строки, набор
плейбуков, библиотека — меняет вердикт там, где язык и инструмент его не
меняют: лог сервиса читают через месяц запросом, вывод команды — сейчас и
глазами, поэтому уровень записи у них выбирается по-разному. Осями это
измерение не выражено: они отвечают на вопрос, от чего правило умирает, а не
к чему оно применяется, — и единственное место, где вид может быть назван,
область действия. Не названный, он остаётся молчаливым допущением автора:
потребитель другого вида не отличает «правило написано не про меня» от «мы
его нарушаем» и записывает второе, хотя чинится первое — условие
применимости в каноне (META-15.2).
### META-37. Имя темы называет решение и адресата, а не место в архитектуре
**СЛЕДУЕТ.** Именем темы служит решение вместе с тем, кому оно адресовано, а
не роль части конкретного проекта.
**ПОЧЕМУ.** Имя темы вечно и не переиспользуется (META-29): оно стоит в
`origin:` каждой копии, в подписках, в чужих ссылках. Роль же принадлежит
сегодняшнему устройству одного проекта — «фронтенд», который через три года
рендерится на сервере, называется по-прежнему, а означает другое, и заметить
расхождение нечем: имя ни на что не ссылается, кроме привычки. Пара имён вида
`logging-backend` и `logging-frontend` вдобавок навязывает чтение «две
разновидности одного», хотя по границе это две темы: серверную запись читают
постфактум инструментом, клиентскую — разработчик в консоли или сборщик ошибок
на той стороне сети, и общего у них остаётся три правила из сорока. Названные
по адресату — `logging` и `client-logging` — они и читаются как разные.
Ступень ниже высшей потому, что «решение против роли» — суждение о слове: на
границе двое разойдутся.
### META-38. Ось слоя объявляется в шапке файла
**ДОЛЖЕН.** Принадлежность слоя оси объявляется в шапке ключами `lang:` и
`stack:`, а не выводится из пути файла; отсутствие обоих ключей означает
базовый слой темы.
**ПОЧЕМУ.** Ось, выведенная из пути, ломается тем же способом, что и тема,
выведенная из имени файла (META-28), только тише: переезд файла между
директориями не меняет ни одного идентификатора, но меняет состав копии у
каждого потребителя — слой начинает выбираться при другом языке или всегда.
Сверить это не с чем, потому что путь ничего не утверждает, а объявления нет.
Объявление вдобавок выражает то, чего дерево директорий не выражает: слой,
осмысленный только при совпадении языка и инструмента сразу; и набор, у
которого осей нет вовсе, перестаёт требовать директорий-заглушек. Дерево при
этом остаётся — но тем же, чем уже является `extends:`, документацией связи
для человека.
### META-28. Тема объявляется в шапке файла и стоит в манифесте набора
**ДОЛЖЕН.** Шапка несёт ключ `topic:` с именем темы, и это имя стоит в
манифесте набора.
**ПОЧЕМУ.** Тема — единица подписки и единица сборки: потребитель перечисляет
темы в своём манифесте, а сборщик складывает в один файл все слои темы. Пока
имя выводится из имени файла, у сборщика нет способа узнать, что два слоя,
названные по-разному, — один документ; переименование файла при этом молча
заводит новую тему, подписка перестаёт находить прежнюю, а ссылки «конвенция
`logging`» в копиях остаются висеть. Объявленное имя вдобавок сверяется с
манифестом — выведенное сверять не с чем.
### META-29. Имя темы не переиспользуется
**НЕ ДОЛЖЕН.** Снятое или переименованное имя темы другой теме не выдаётся:
оно уходит в раздел выбывших манифеста с причиной и датой.
**ПОЧЕМУ.** Имя темы живёт в чужих репозиториях — в шапке `origin:` каждой
копии, в подписке потребителя, в тексте ссылок. Выданное второй теме, оно
начинает указывать на другой набор правил, и обнаруживается это не на сборке,
а по содержанию: файл обновится штатно, изменится текст. Та же дисциплина и по
той же причине действует для префиксов правил.
### META-30. Правка словаря или формы правила доходит до документа для читателя
**ДОЛЖЕН.** Изменение ключевых слов, их значений или состава частей правила
вносится и в короткое описание языка, которое едет в копию.
**ПОЧЕМУ.** Полное описание языка остаётся у автора набора, а по правилам код
проверяет читатель копии — человек или агент в чужом репозитории, у которого
из двух документов есть только короткий. Разошедшись, он начинает толковать
слова по прежней версии: ДОПУСКАЕТСЯ читается как бытовое «можно», отступление
от ДОЛЖЕН перестаёт требовать записи — то есть отказывает ровно то, ради чего
слова вводились, и молча. Проверить расхождение дёшево: словари в двух
документах либо совпадают, либо нет.
### META-31. Нумерация правил в файле сплошная
**ДОЛЖЕН.** Номера идут от единицы до наибольшего без пропусков: снятое
правило остаётся на месте заглушкой с меткой СНЯТО, а не исчезает.
**ПОЧЕМУ.** Дыра в нумерации неотличима от опечатки в номере и от правила,
которое забыли дописать, — проверка, увидев пропуск, не может сказать, ошибка
это или норма, поэтому либо молчит всегда, либо краснеет на живом файле.
Заглушка отвечает на тот же вопрос текстом: номер занят, правило снято
тогда-то и по такой-то причине. Переиспользовать номер по-прежнему нельзя —
ссылка из чужого репозитория обязана указывать на то же утверждение, — но и
отдельный
реестр снятых номеров не нужен: он был бы вторым источником правды рядом с
файлом, который и так всё сказал.
### META-32. Ссылка ведёт на правило, которое существует
**НЕ ДОЛЖЕН.** Идентификатор в тексте не указывает на правило, которого в
наборе нет.
**ПОЧЕМУ.** Неразрешимая ссылка означает одно из двух: опечатку в номере или
след переноса правила в другой файл. Читатель — тем более в чужом
репозитории — не различит эти случаи и решит, что правила больше нет, хотя оно
могло переехать. С заглушками (META-31) проверка становится однозначной:
идентификатор либо ведёт к правилу, либо к объяснению, почему его сняли, а
третьего исхода нет — и любой неразрешённый идентификатор точно ошибка.
### META-2. Конвенция заводится, когда решение принимается третий раз ### META-2. Конвенция заводится, когда решение принимается третий раз
**СЛЕДУЕТ.** Поводом служит одно и то же решение, принятое третий раз и **СЛЕДУЕТ.** Поводом служит одно и то же решение, принятое третий раз и
@@ -81,9 +282,9 @@ prefix: META
### META-3. Новая конвенция пишется там, где заболело ### META-3. Новая конвенция пишется там, где заболело
**СЛЕДУЕТ.** Первые шаги пути «находка → конвенция → правило линтера **СЛЕДУЕТ.** Первые шаги пути «находка → конвенция → правило линтера»
удаление прозы» делаются в репозитории, где случилась находка; в канон делаются в репозитории, где случилась находка; в канон продвигается общая
продвигается общая часть. часть.
**ПОЧЕМУ.** Правило, написанное сразу в общем виде, не проверено ни одним **ПОЧЕМУ.** Правило, написанное сразу в общем виде, не проверено ни одним
применением, и условие применимости у него придумано, а не найдено, — применением, и условие применимости у него придумано, а не найдено, —
@@ -157,33 +358,52 @@ prefix: META
переписывает его снова. Направление «конвенция → код» держится ровно тем, переписывает его снова. Направление «конвенция → код» держится ровно тем,
что факт не считается аргументом. что факт не считается аргументом.
### META-6. `ДОЛЖЕН` без механической проверки либо механизируется, либо понижается ### META-6. Высшая модальность требует воспроизводимого вердикта
**ДОЛЖЕН.** Правило, нарушение которого объявлено ошибкой, получает **ДОЛЖЕН.** Правило со ступенью ДОЛЖЕН или НЕ ДОЛЖЕН формулируется так, что
машинную проверку или переводится в СЛЕДУЕТ. двое проверяющих по одному его тексту выносят один и тот же вердикт.
**ПОЧЕМУ.** Без проверки правило держится на внимании: нарушения копятся **ПОЧЕМУ.** Проверяют конвенцию в первую очередь агент и человек — они читают
молча и всплывают выборочно — на том ревью, куда дошли руки. Для СЛЕДУЕТ текст правила и по нему смотрят код. Проверка, стало быть, есть у каждого
это честно, а ДОЛЖЕН при этом обещает то, чего не делает, и через несколько правила с первого дня, и её инструмент — формулировка, а не скрипт. Отсюда
таких случаев обесценивает остальные ДОЛЖЕН в файле. цена невоспроизводимой нормы: вердикт зависит от того, кто читал, нарушения
всплывают выборочно, а отступление нечем записать — неизвестно, нарушено ли.
Для СЛЕДУЕТ это честно, там суждение и есть содержание правила; ДОЛЖЕН в
таком виде обещает то, чего не делает, и через несколько случаев обесценивает
остальные ДОЛЖЕН в файле.
Отсюда следствие: правило, машинная проверка которого невозможна в принципе Отсюда следствие: правило, вердикт которого зависит от суждения по построению
(вкус формулировки, выбор границы, суждение о ситуации), не может быть (вкус формулировки, выбор границы, уместность в конкретном месте), не может
ДОЛЖЕН — его модальность СЛЕДУЕТ по построению, а не по слабости. быть ДОЛЖЕН — его модальность СЛЕДУЕТ по природе нормы, а не по слабости.
### META-25. Высшая модальность выбирается, только когда назван вред ### META-25. Высшая модальность выбирается, только когда назван вред
**СЛЕДУЕТ.** Правило получает ДОЛЖЕН или НЕ ДОЛЖЕН, если в обосновании **СЛЕДУЕТ.** Правило получает ДОЛЖЕН или НЕ ДОЛЖЕН, если в обосновании
сказано, что́ ломается при нарушении. сказано, что́ ломается при нарушении.
**ПОЧЕМУ.** Машинная проверка — условие необходимое (META-6), но не **ПОЧЕМУ.** Воспроизводимость вердикта — условие необходимое (META-6), но не
достаточное: проверяемых мелочей больше, чем важных вещей, и без второго достаточное: воспроизводимо проверяемых мелочей больше, чем важных вещей, и
условия единственным фильтром остаётся удобство проверки. Шкала наполняется без второго условия единственным фильтром остаётся удобство проверки. Шкала
опрятностью, читатель перестаёт отличать «уронит прод» от «неаккуратно» — и наполняется опрятностью, читатель перестаёт отличать «уронит прод» от
обесцениваются все ДОЛЖЕН в файле, ровно то, от чего META-6 защищает с «неаккуратно» — и обесцениваются все ДОЛЖЕН в файле, ровно то, от чего META-6
другой стороны. Само это правило машиной не проверяется: «вред назван» защищает с другой стороны. Собственная ступень этого правила — СЛЕДУЕТ:
устанавливается чтением, поэтому его собственная модальность по META-6 — форма обоснования ничем не ограничена, поэтому «вред назван» вердикта не
СЛЕДУЕТ. даёт — один читатель увидит названный вред там, где другой увидит объяснение
мотива.
### META-27. Механизация правила желательна, но ступени не задаёт
**СЛЕДУЕТ.** Правило со ступенью ДОЛЖЕН получает машинную проверку, когда
такую проверку можно написать.
**ПОЧЕМУ.** Линтер не забывает, ничего не стоит на каждом прогоне и краснеет
до ревью, а не на нём: там, где проверка пишется, она дешевле самого
внимательного чтения, и путь «находка → конвенция → проверка» кончается ею.
Норму она при этом не заменяет и не отменяет (META-8). Условием ступени
механизация не является:
проверяющий по умолчанию — читатель правила (META-6), а если требовать
скрипт, весь канон стоит в СЛЕДУЕТ до того дня, когда скрипты появятся.
Ступень говорит о важности нормы, а не о состоянии инструментов.
### META-7. Факт механизации фиксируется в копии со ссылкой на правило ### META-7. Факт механизации фиксируется в копии со ссылкой на правило
@@ -196,36 +416,25 @@ prefix: META
читатель догадывается сам, к какому утверждению относится проверка, — и читатель догадывается сам, к какому утверждению относится проверка, — и
догадывается по-разному. догадывается по-разному.
### META-8. Формулировка не удаляется из канона, пока механизирована не у всех ### META-8. Норма из канона не удаляется, чем бы она ни проверялась
**НЕ ДОЛЖЕН.** Норма остаётся в каноне, пока хотя бы у одного потребителя **НЕ ДОЛЖЕН.** Формулировка нормы остаётся в правиле независимо от того, кто и
машинной проверки нет. чем её проверяет.
**ПОЧЕМУ.** У кого линтера нет, тот после удаления остаётся без правила **ПОЧЕМУ.** Проверяющий по умолчанию — читатель правила (META-6), и удаление
вообще: ни проверки, ни текста. Механизация у одного потребителя ничего не нормы забирает у него ровно то, чем он проверяет: линтер сообщает, что
говорит о прочих, поэтому удалять по факту «у нас уже проверяется» — нарушено, но не сообщает, что требуется. Условие «механизировано у всех»
значит чинить свой файл за чужой счёт. спасти не может: оно измеряется в день удаления, а подписчики появляются
после. Репозиторий, подключившийся через год, получил бы правило без нормы и
Списка подписчиков канон по построению не знает, поэтому факт «механизировано без проверки — заголовок с обоснованием и никакого способа узнать, что́ именно
у всех» устанавливается обходом репозиториев вручную — это часть работы по предписано, кроме git-истории канона, до которой он не дойдёт. Списка
удалению нормы, а не то, что можно проверить машиной (см. `LANGUAGE.md`, подписчиков у канона к тому же нет по построению, так что «у всех» ему всё
состояние МЕХАНИЗИРОВАНО). равно не проверить.
### META-9. Общая механизация разрешает удалить норму из канона
**ДОПУСКАЕТСЯ.** Правило, уехавшее в общий конфиг линтера или в общую роль,
удаляется из канона.
**ПОЧЕМУ.** Формулировка, дублирующая работающую у всех проверку,
размазывает внимание: файл на несколько сотен строк заставляет человека и
агента добросовестно вычитывать тривиальное именование и не доходить до
формы решения. Явное разрешение нужно, чтобы META-8 не читался как запрет
удалять вообще.
### META-10. Обоснование не удаляется никогда ### META-10. Обоснование не удаляется никогда
**НЕ ДОЛЖЕН.** Блок ПОЧЕМУ остаётся и после того, как норма уехала в **НЕ ДОЛЖЕН.** Блок ПОЧЕМУ остаётся и после того, как правило стало
линтер. проверяться линтером.
**ПОЧЕМУ.** Линтер сообщает, что нарушено, но не сообщает, зачем правило **ПОЧЕМУ.** Линтер сообщает, что нарушено, но не сообщает, зачем правило
существует. Без обоснования не видно, когда причина отпала, — проверка существует. Без обоснования не видно, когда причина отпала, — проверка
@@ -338,13 +547,26 @@ prefix: META
подробностями идут; перенос деталей туда же вернул бы задачу поддержки двух подробностями идут; перенос деталей туда же вернул бы задачу поддержки двух
текстов. текстов.
## Освободившиеся номера ## Снятые правила
Номер удалённого правила не переиспользуется, поэтому дыры в нумерации — Снятое правило остаётся здесь заглушкой: номер занят навсегда, ссылка на него
норма. Здесь перечислено, что́ под ними было: без этого упоминание номера в ведёт к объяснению, а нумерация в файле остаётся сплошной (META-31).
прозе не отличить от ссылки на исчезнувшее правило.
| Номер | Что было | Почему снято | ### META-9. Общая механизация разрешала удалить норму из канона
|---|---|---|
| META-16 | имя файла — kebab-case | вреда от нарушения нет (META-25); осталось прозой в «Оформлении» | **СНЯТО 2026-07-26.** Удаление нормы оставляло подписчика, пришедшего позже,
| META-26 | запрет слов обязательства в обосновании | правило о заглавных уже делает строчное слово ненормативным, а форму обоснования рамками не ограничивают | без текста и без проверки, а условие «механизировано у всех» набору не
проверить: списка подписчиков у него нет. Взамен — META-8, запрет удалять
норму вообще.
### META-16. Имя файла — kebab-case
**СНЯТО 2026-07-26.** Вреда от нарушения нет, а значит нет и высшей
модальности (META-25): сборка идёт по имени темы из шапки, а не по имени
файла. Осталось прозой в разделе «Оформление».
### META-26. Запрет слов обязательства в обосновании
**СНЯТО 2026-07-26.** Правило о заглавных уже делает строчное «обязан»
ненормативным, поэтому запрет ничего не добавлял, а форму обоснования рамками
не ограничивают.
+300 -83
View File
@@ -12,6 +12,18 @@ version: 1
пополниться, и текст, написанный по предыдущей версии, должен читаться по пополниться, и текст, написанный по предыдущей версии, должен читаться по
той, по которой написан. той, по которой написан.
Документ адресован автору набора и в репозиторий-потребитель не едет. К
читателю копии едет короткое `READING.md`: словарь со значениями, форма
правила и её граница, ссылки, локальная часть — без разделов о ведении набора.
Словарь в двух документах обязан совпадать (META-30), и это единственное
место, где между ними возможен дрейф.
Описание языка ни на один набор конвенций не опирается, поэтому все примеры
здесь — вымышленные правила с префиксами на `X` (`XKEY`, `XMIG`, `XLOG`).
Такие префиксы канон не занимает никогда, в манифесте набора их нет — значит
пример не спутать с настоящим правилом, а перенумерация конвенций описание
языка не задевает.
## Опора на стандарты ## Опора на стандарты
Язык не выводится из вкуса автора. Каждое решение о форме взято из Язык не выводится из вкуса автора. Каждое решение о форме взято из
@@ -22,9 +34,9 @@ version: 1
|---|---|---| |---|---|---|
| **BCP 14** (RFC 2119 + RFC 8174) | шкала модальности; правило «нормативно только заглавное»; сдержанность в высшей модальности; англоязычный словарь как готовый | синонимы `REQUIRED`, `RECOMMENDED`, `OPTIONAL`; `SHALL` как форма требования | | **BCP 14** (RFC 2119 + RFC 8174) | шкала модальности; правило «нормативно только заглавное»; сдержанность в высшей модальности; англоязычный словарь как готовый | синонимы `REQUIRED`, `RECOMMENDED`, `OPTIONAL`; `SHALL` как форма требования |
| **ISO/IEC Directives, Part 2** | разделение «требование — рекомендация — разрешение — возможность»; запрет модальных глаголов в утверждениях о факте | списки равнозначных словесных форм («is to», «is required to», «only … is permitted») | | **ISO/IEC Directives, Part 2** | разделение «требование — рекомендация — разрешение — возможность»; запрет модальных глаголов в утверждениях о факте | списки равнозначных словесных форм («is to», «is required to», «only … is permitted») |
| **ISO/IEC/IEEE 29148** | обоснование как обязательный атрибут; единичность нормы; проверяемость; метод верификации отдельным атрибутом | остальной аппарат требований: приоритеты, источники, матрицы трассируемости | | **ISO/IEC/IEEE 29148** | характеристики хорошего требования — единичность и проверяемость; обоснование и метод верификации как отдельные атрибуты требования | остальной аппарат требований: приоритеты, источники, матрицы трассируемости |
| **DMN** | таблица решений с объявленной политикой совпадения и требованием полноты | исполняемая семантика и всё, что предполагает движок решений | | **DMN** | таблица решений с объявленной политикой совпадения | исполняемая семантика и всё, что предполагает движок решений |
| **EARS** | вывод о том, что выигрыш даёт жёсткий шаблон, а не его конкретный вид; паттерн «нежелательное поведение» — в виде таблицы | шаблоны как форма записи правила: `WHEN`, `WHILE`, `WHERE` | | **EARS** | паттерн «нежелательное поведение» — в виде таблицы | шаблоны как форма записи правила: `WHEN`, `WHILE`, `WHERE` |
| **OpenSpec** | четырёхчастная форма правила; адресуемость правила идентификатором; форма «условие → следствие» для стыка правил | `SHALL`; `GIVEN/WHEN/THEN` как общая форма записи | | **OpenSpec** | четырёхчастная форма правила; адресуемость правила идентификатором; форма «условие → следствие» для стыка правил | `SHALL`; `GIVEN/WHEN/THEN` как общая форма записи |
Три отклонения стоят объяснения, потому что выглядят как произвол. Три отклонения стоят объяснения, потому что выглядят как произвол.
@@ -44,27 +56,50 @@ version: 1
система, и её поведение разворачивается во времени: состояние, событие, система, и её поведение разворачивается во времени: состояние, событие,
исход. У конвенции субъект — автор кода, и разворачивать нечего: есть исход. У конвенции субъект — автор кода, и разворачивать нечего: есть
ситуация выбора и вердикт. Это таблица, а не траектория. Тем же рассуждением ситуация выбора и вердикт. Это таблица, а не траектория. Тем же рассуждением
отклонены шаблоны EARS как форма записи правила, а взят из EARS другой отклонены шаблоны EARS как форма записи правила, а взято из EARS другое
результат: измеримый выигрыш дала там сама обязательность шаблона, а не его сообщённое снижение числа дефектов после введения шаблонов. Вывод, что дело в
конкретная форма. самой обязательности формы, а не в её конкретном виде, наш; он ниже, среди
усилений.
Исключение — стык правил, где субъект действительно система: там форма Исключение — стык правил, где субъект действительно система: там форма
«условие → следствие» берётся сознательно, вместе со служебными словами под «условие → следствие» берётся сознательно, вместе со служебными словами под
неё. Это единственное место, и оно описано в «Таблицах решений». неё. Это единственное место, и оно описано в «Таблицах решений».
## Где источник усилен
Три решения идут дальше источника, и это наши решения, а не его требования.
Названы они отдельно, чтобы довод не подменялся ссылкой: спорить с ними нужно
по существу, а не со стандартом.
- **Обоснование обязательно.** В 29148 rationale — из списка рекомендуемых
атрибутов требования; обязательный костяк там другой, это характеристики
самого требования. Здесь правило без блока ПОЧЕМУ не принимается, потому что
конвенция живёт годами и переживает автора: норма без причины через год либо
отменяется первым возражением, либо соблюдается там, где вредит.
- **Полнота таблицы решений.** DMN даёт политику совпадения как именованный
атрибут, а полноты не требует: индикатор полноты был в первой версии
спецификации и из последующих убран, полноту проверяют валидаторы
инструментов. Здесь она требуется, потому что таблицу и заводят ради
видимости пропуска: неперечисленный случай в прозе не виден, а пустая
клетка видна.
- **Вывод про обязательность шаблона.** В EARS сообщается о снижении числа
дефектов в требованиях после введения шаблонов. Вывод, что выигрыш даёт сама
обязательность формы, а не её конкретный вид, — наш: он объясняет, почему мы
берём из EARS результат, но не берём сами шаблоны.
## Что даёт формализация ## Что даёт формализация
Адресуемое правило — не украшение формы, а условие работы трёх механизмов: Адресуемое правило — не украшение формы, а условие работы трёх механизмов:
- **Механизация.** Запись о ней должна говорить «правило `MIGR-4` проверяет - **Механизация.** Запись о ней должна говорить «правило `XMIG-4` проверяет
`archrules`», а не «`AUTOINCREMENT` в новых миграциях — `archrules`»: во `archrules`», а не «`AUTOINCREMENT` в новых миграциях — `archrules`»: во
втором случае читатель сам догадывается, к какому утверждению это втором случае читатель сам догадывается, к какому утверждению это
относится, и догадывается по-разному. относится, и догадывается по-разному.
- **Отступления.** «У нас не так» бесполезно, пока не сказано, что именно - **Отступления.** «У нас не так» бесполезно, пока не сказано, что именно
«не так». Со ссылкой на правило отступления становятся счётными: видно, «не так». Со ссылкой на правило отступления становятся счётными: видно,
сколько правил конвенции репозиторий реально не соблюдает. сколько правил конвенции репозиторий реально не соблюдает.
- **Промоут находки.** Путь «находка → конвенция → правило линтера - **Промоут находки.** Путь «находка → конвенция → правило линтера» требует
удаление прозы» требует ручки, за которую берут конкретное правило. ручки, за которую берут конкретное правило.
Общий знаменатель — **идентификатор**. Модальные слова и таблицы полезны, Общий знаменатель — **идентификатор**. Модальные слова и таблицы полезны,
но вторичны. но вторичны.
@@ -72,7 +107,7 @@ version: 1
## Единица — правило ## Единица — правило
```markdown ```markdown
### KEYS-5. Разбор внешнего идентификатора на границе ### XKEY-5. Разбор внешнего идентификатора на границе
**ДОЛЖЕН.** Идентификатор, пришедший снаружи, проходит разбор до запроса **ДОЛЖЕН.** Идентификатор, пришедший снаружи, проходит разбор до запроса
к базе. к базе.
@@ -83,20 +118,112 @@ version: 1
``` ```
Четыре обязательные части: **идентификатор**, **заголовок**, **модальность Четыре обязательные части: **идентификатор**, **заголовок**, **модальность
с нормой**, **обоснование под меткой ПОЧЕМУ**. Норма — одна фраза; если в неё с нормой**, **обоснование под меткой ПОЧЕМУ**. Пятый блок, ПРИМЕРЫ,
не влезает, это два правила. Требование единичности взято из ISO/IEC/IEEE необязателен — о нём ниже. Норма — одна фраза; если в неё не влезает, это два
29148: составная норма не проверяема целиком, и нарушение одной её половины правила. Требование единичности взято из ISO/IEC/IEEE 29148: составная норма
нечем адресовать. не проверяема целиком, и нарушение одной её половины нечем адресовать.
Обе метки правила — модальное слово и ПОЧЕМУ — пишутся заглавными и Метки правила — модальное слово, ПОЧЕМУ, ПРИМЕРЫ — пишутся заглавными и
принадлежат словарю набора: скелет правила читается одинаково в любом языке, принадлежат словарю набора: скелет правила читается одинаково в любом языке,
на который канон переведён. на который канон переведён.
**Правило кончается перед следующим заголовком.** Область правила — от его
заголовка до следующего заголовка любого уровня. Внутри области текст
принадлежит последнему открытому блоку: метка блок открывает, и блок длится
до следующей метки или до конца области.
```markdown
### XKEY-3. Заголовок правила
**ДОЛЖЕН.** Норма одной фразой.
| № | ситуация | вердикт | ← блок нормы: таблица уточняет её
**ПОЧЕМУ.** Причина.
Продолжение причины, пример, ← блок обоснования продолжается
ссылка на внешнюю практику.
### XKEY-4. Следующее правило ← здесь область кончилась
```
Отсюда три следствия:
- **Хвост после ПОЧЕМУ — обоснование** до следующей метки или до конца
области, а не безымянная часть правила и не проза вокруг. Требований в нём
не живёт: то, что подлежит исполнению, стоит в блоке нормы, где у него есть
модальность и адрес. Требование, оставленное
в хвосте, требованием не является — сослаться на него нельзя и отступление
от него записать нельзя.
- **Таблица и список после модальной метки — часть нормы.** Правило,
классифицирующее ситуации, ровно так и записывается («Таблицы решений»), а
вердикт из такой таблицы адресуется номером строки.
- **Проза — это то, что лежит вне областей правил.** Тем самым проверка
«заглавных модальных слов вне правил нет» становится реализуемой: границу
считает разметка, а не читательское суждение о том, где правило кончилось.
Заглавное модальное слово внутри области правила законно, когда это
упоминание ступени в обосновании («для СЛЕДУЕТ это честно»). Метку от
упоминания отличает положение: метка стоит первой в своём абзаце, полужирным
и с точкой.
## Примеры к правилу
Пятый блок правила — необязательный, под меткой ПРИМЕРЫ. В нём код,
показывающий норму в деле, обычно парой «плохо → хорошо». Стоит он после
обоснования: сначала требование, потом причина, потом иллюстрация.
````markdown
### XKEY-5. Внешний идентификатор разбирается до обращения к базе
**ДОЛЖЕН.** Значение, пришедшее снаружи, проходит разбор и нормализацию
раньше, чем по нему делается запрос.
**ПОЧЕМУ.** Синтаксически невалидное значение не может соответствовать
записи, поэтому поход в базу за ним — заведомо холостой.
**ПРИМЕРЫ.**
Плохо — строка уходит в запрос как пришла:
```go
row := db.QueryRow("select … where id = ?", r.PathValue("id"))
```
Хорошо — разбор на границе, запроса при неудаче нет:
```go
id, err := ident.Parse(r.PathValue("id"))
if err != nil {
return notFound(w)
}
row := db.QueryRow("select … where id = ?", id)
```
````
**Пример иллюстрирует норму, а не задаёт её.** Три следствия, ради которых
это сказано:
- **требований в блоке нет.** Всё, что подлежит исполнению, стоит в блоке
нормы; деталь примера — имя переменной, конкретная функция, форма ответа —
требованием не становится. Разошёлся пример с нормой — действует норма, а
пример правят;
- **это не готовый сниппет.** Код в примере сокращён до того, что показывает
правило: обработка ошибок, контекст, импорты в нём условны, и копировать его
дословно не нужно;
- **пример стареет быстрее нормы.** Он привязан к сегодняшнему API, поэтому
расхождение примера с текущим кодом — повод поправить пример, а не отменять
правило.
Блок необязателен: он окупается там, где норму словами описать дороже, чем
показать, — форма вызова, структура записи в логе, раскладка файла. У правила
про выбор границы или про уровень лога иллюстрировать нечего.
## Обоснование обязательно ## Обоснование обязательно
Правило без блока ПОЧЕМУ не принимается. Это требование к форме, а не Правило без блока ПОЧЕМУ не принимается. Это требование к форме, а не
пожелание; в 29148 обоснование — атрибут требования наравне с самим пожелание. В 29148 обоснование — отдельный атрибут требования, но из
требованием, и по тем же причинам: рекомендуемых; здесь оно обязательно, и вот почему:
- **Обоснование — единственный способ увидеть, что правило устарело.** - **Обоснование — единственный способ увидеть, что правило устарело.**
Норма стареет молча; причина стареет заметно. Когда причина отпала, видно, Норма стареет молча; причина стареет заметно. Когда причина отпала, видно,
@@ -114,9 +241,9 @@ version: 1
Форма обоснования при этом ничем не ограничена: рамки здесь только Форма обоснования при этом ничем не ограничена: рамки здесь только
смысловые. Абзац может быть длинным, вести рассуждение, приводить пример, смысловые. Абзац может быть длинным, вести рассуждение, приводить пример,
ссылаться на внешние практики, стандарты и чужие проекты — канон это уже ссылаться на стандарты, внешние практики и чужие проекты — на устройство
делает («адаптация OpenTelemetry», «как по умолчанию в zap и zerolog», OpenTelemetry, на умолчания библиотек логирования, на процедуру миграции из
двенадцатишаговая процедура SQLite). Запрещённых слов и обязательной документации СУБД. Запрещённых слов и обязательной
структуры у обоснования нет, и заводить их не нужно: обязательность несёт структуры у обоснования нет, и заводить их не нужно: обязательность несёт
норма, а обоснование её объясняет — путаницу между этими двумя ролями норма, а обоснование её объясняет — путаницу между этими двумя ролями
исключает правило о заглавных. исключает правило о заглавных.
@@ -162,18 +289,19 @@ Directives, Part 2, по одной форме записи на ступень,
1. нарушение причиняет названный вред, а не расходится со вкусом — META-25, 1. нарушение причиняет названный вред, а не расходится со вкусом — META-25,
он же критерий BCP 14, где высшая модальность резервируется под то, что он же критерий BCP 14, где высшая модальность резервируется под то, что
действительно ломается, и не употребляется для навязывания метода; действительно ломается, и не употребляется для навязывания метода;
2. норма проверяема машиной — META-6, иначе обязательность держится на 2. вердикт о нарушении воспроизводим — META-6: по тексту правила двое
внимании и обещает то, чего не делает. проверяющих приходят к одному ответу, иначе обязательность держится на
том, кто читал.
Не выполнено первое — правилу место в СЛЕДУЕТ или нигде. Не выполнено Не выполнено первое — правилу место в СЛЕДУЕТ или нигде. Не выполнено
второе — в СЛЕДУЕТ. Проверяемость сама по себе не повышает правило до второе — в СЛЕДУЕТ. Воспроизводимость сама по себе не повышает правило до
ДОЛЖЕН: механически проверяемых мелочей больше, чем важных вещей, и ДОЛЖЕН: проверяемых мелочей больше, чем важных вещей, и безразборное
безразборное повышение обесценивает шкалу быстрее, чем её отсутствие. повышение обесценивает шкалу быстрее, чем её отсутствие.
Модальность живёт на **правиле**, а не на файле. Файловый статус Модальность живёт на **правиле**, а не на файле. Файловый статус
(`status: рекомендуемая` / `обязательная` в шапке) не используется: он (`status: рекомендуемая` / `обязательная` в шапке) не используется: он
неизбежно врёт, потому что один файл смешивает жёсткие требования с неизбежно врёт, потому что один файл смешивает жёсткие требования с
советами. В шапке остаются только `prefix` и `extends`. советами. В шапке остаются только `topic`, `prefix` и `extends`.
## Словарь другого языка ## Словарь другого языка
@@ -192,14 +320,18 @@ Directives, Part 2, по одной форме записи на ступень,
| рекомендация против | НЕ СЛЕДУЕТ | SHOULD NOT | | рекомендация против | НЕ СЛЕДУЕТ | SHOULD NOT |
| разрешение | ДОПУСКАЕТСЯ | MAY | | разрешение | ДОПУСКАЕТСЯ | MAY |
**Метки правила.** Обязательности не задают, а размечают его части. **Метки.** Обязательности не задают, а размечают: ПОЧЕМУ — обоснование,
Стандартом не даются ни в одном языке: в BCP 14 таких понятий нет, слова ПРИМЕРЫ — иллюстрации к норме, МЕХАНИЗИРОВАНО — запись о проверке в копии,
подбираются под язык так же, как остальные. СНЯТО — заглушку на месте убранного правила. Стандартом не даются ни в одном
языке: в BCP 14 таких понятий нет, слова подбираются под язык так же, как
остальные.
| Метка | Русский | Английский | | Метка | Русский | Английский |
|---|---|---| |---|---|---|
| обоснование | ПОЧЕМУ | WHY | | обоснование | ПОЧЕМУ | WHY |
| иллюстрации | ПРИМЕРЫ | EXAMPLES |
| способ проверки | МЕХАНИЗИРОВАНО | MECHANIZED | | способ проверки | МЕХАНИЗИРОВАНО | MECHANIZED |
| снятое правило | СНЯТО | RETIRED |
**Служебные слова сценарного блока** — `КОГДА`, `ТОГДА`, `И`, `ИЛИ`; таблица **Служебные слова сценарного блока** — `КОГДА`, `ТОГДА`, `И`, `ИЛИ`; таблица
и объяснение в разделе «Таблицы решений». и объяснение в разделе «Таблицы решений».
@@ -226,15 +358,15 @@ Directives, Part 2, по одной форме записи на ступень,
Каждая конвенция называет язык одной строкой во вводной прозе: Каждая конвенция называет язык одной строкой во вводной прозе:
> Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки > Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
> ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — > ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
> тогда и только тогда, когда написаны заглавными. > конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Слова в строке — из словаря того языка, на котором написан набор. Для Слова в строке — из словаря того языка, на котором написан набор. Для
англоязычного набора та же строка выглядит так: англоязычного набора та же строка выглядит так:
> The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY and the marks WHY > The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY and the marks WHY,
> and MECHANIZED are to be interpreted as described in the conventions > EXAMPLES, MECHANIZED and RETIRED are to be interpreted as described in the
> language, version 1, and only when written in capitals. > conventions language, version 1, and only when written in capitals.
Форма скопирована у BCP 14, где та же задача решается тем же способом: Форма скопирована у BCP 14, где та же задача решается тем же способом:
спецификация не прикладывает к себе словарь и не указывает путь к нему, а спецификация не прикладывает к себе словарь и не указывает путь к нему, а
@@ -249,40 +381,55 @@ Directives, Part 2, по одной форме записи на ступень,
правило обязательно, а о том, чем эта обязательность обеспечена. В 29148 это правило обязательно, а о том, чем эта обязательность обеспечена. В 29148 это
два разных атрибута требования, и здесь тоже два. два разных атрибута требования, и здесь тоже два.
Когда правило механизировано у всех потребителей, его норма из канона **Проверяющий по умолчанию — читатель правила**, человек или агент. Канон
удаляется, а модальность и обоснование остаются: пишется прежде всего под агента: он читает конвенцию и по ней смотрит код,
то есть проверка есть у каждого правила с первого дня, и её инструмент —
формулировка нормы. Поэтому вторым условием ДОЛЖЕН стоит воспроизводимость
вердикта (META-6), а не наличие скрипта: ступень говорит о важности нормы и о
том, сколько внимания она получает при проверке, а не о состоянии
инструментов. Линтер сильнее чтения — он не забывает, ничего не стоит на
каждом прогоне и краснеет до ревью, — поэтому механизация желательна везде,
где проверка пишется (META-27), и остаётся концом пути «находка → конвенция →
проверка». Но обязательным условием высшей ступени она не является: иначе весь
канон стоял бы в СЛЕДУЕТ до появления скриптов, которых пока нет ни одного.
**Механизация нормы не заменяет и не сокращает.** Норма остаётся в правиле
навсегда — как и обоснование (META-8, META-10), — сколько бы проверок её ни
подпирало. Причин три:
- **линтер сообщает, что нарушено, но не сообщает, что требуется.** Без нормы
правило нечем исполнить и не с чем сверить вердикт проверки, а проверяющий
по умолчанию читает именно норму;
- **подписчики появляются позже.** Репозиторий, подключившийся через год,
получил бы правило без нормы и без линтера — ни текста, ни проверки;
- **«механизировано у всех» набору не проверить:** списка подписчиков у него
нет по построению.
**Отметка — свойство репозитория, а не набора.** Механизирована норма или нет,
зависит от того, чей это репозиторий, поэтому в тексте конвенции отметки нет:
её место — запись о механизации в локальной части копии, со ссылкой на
идентификатор правила (META-7).
```markdown ```markdown
### MIGR-6. Дефолтов времени в схеме БД нет <!-- conv:local -->
**ДОЛЖЕН. МЕХАНИЗИРОВАНО.** Проверяется общим правилом линтера; XMIG-2, XMIG-4 — МЕХАНИЗИРОВАНО: `internal/archrules`, в новых миграциях.
формулировка удалена, потому что дублировала работающую проверку.
**ПОЧЕМУ.** Дефолт превращает забытую вставку в тихо работающий код…
``` ```
- Идентификатор и заголовок сохраняются: ссылки из репозиториев продолжают Так у правила остаются оба атрибута сразу: обязательность — в норме, которая
указывать на то же утверждение. приезжает из набора и одинакова у всех, способ проверки — в записи, которая
- Модальность сохраняется, поэтому «все ли ДОЛЖЕН механизированы» принадлежит репозиторию и у каждого своя.
остаётся вычислимым вопросом, а не предметом чтения всего канона.
- Обоснование остаётся навсегда — линтер сообщает, что нарушено, но не
сообщает, зачем правило существует, и без обоснования нельзя понять,
когда проверку пора отменять.
Факт «механизировано у всех» устанавливается вручную: канон по построению
не знает списка подписчиков, и обойти репозитории перед удалением нормы —
часть работы, а не то, что можно проверить автоматически.
## Таблицы решений ## Таблицы решений
Часть правил **классифицирует ситуации**: какой уровень лога, какая Часть правил **классифицирует ситуации**: какой уровень лога, какая
категория директории, что делать с невалидным вводом в зависимости от его категория директории, что делать с невалидным вводом в зависимости от его
источника. Каноническая форма для них — таблица «ситуация → вердикт», строки источника. Каноническая форма для них — таблица «ситуация → вердикт», строки
которой нумеруются как подпункты правила (`SLOG-8.1`). которой нумеруются как подпункты правила (`XLOG-8.1`).
Таблица плотнее прозы и не даёт пропустить ветку: пустая клетка видна, а Таблица плотнее прозы и не даёт пропустить ветку: пустая клетка видна, а
неупомянутый случай в абзаце — нет. Два свойства такой таблицы взяты из DMN, неупомянутый случай в абзаце — нет. От такой таблицы требуются два свойства —
где они называются и проверяются: первое названо в DMN, второе мы добавили сами («Где источник усилен»):
- **Политика совпадения.** По умолчанию строки взаимоисключающи: любой - **Политика совпадения.** По умолчанию строки взаимоисключающи: любой
ситуации соответствует ровно одна. Если это не так, таблица объявляет ситуации соответствует ровно одна. Если это не так, таблица объявляет
@@ -326,19 +473,30 @@ Directives, Part 2, по одной форме записи на ступень,
## Идентификаторы ## Идентификаторы
- Формат — `<ПРЕФИКС>-<номер>`: `KEYS-5`, `SLOG-27`. Префикс принадлежит Идентификаторов в языке два: **правило** адресуется префиксом с номером,
**конвенция целиком** — именем темы. Ссылаться путём к файлу нельзя ни на то,
ни на другое.
**Правило.**
- Формат — `<ПРЕФИКС>-<номер>`: `XKEY-5`, `XLOG-27`. Префикс принадлежит
файлу, нумерация внутри файла сквозная и начинается с единицы. файлу, нумерация внутри файла сквозная и начинается с единицы.
- Строка таблицы, если на неё нужно ссылаться отдельно, — `KEYS-5.1`, - Строка таблицы, если на неё нужно ссылаться отдельно, — `XKEY-5.1`,
`KEYS-5.2`. `XKEY-5.2`.
- **Идентификатор глобален.** Префикс уникален по всему канону, поэтому - **Идентификатор глобален.** Префикс уникален по всему канону, поэтому
путь файла в ссылке не нужен: `KEYS-5` адресует правило одинаково изнутри путь файла в ссылке не нужен: `XKEY-5` адресует правило одинаково изнутри
файла, из соседней конвенции и из чужого репозитория. В собранной копии файла, из соседней конвенции и из чужого репозитория. В собранной копии
слои разных осей лежат в одном документе, так что ссылка на базовый слой слои разных осей лежат в одном документе, так что ссылка на базовый слой
из языкового вообще никуда не ведёт — правило рядом. из языкового вообще никуда не ведёт — правило рядом.
- **Идентификаторы стабильны и не переиспользуются.** Удалённое правило - **Идентификаторы стабильны и не переиспользуются.** Занять номер снятого
оставляет дыру в нумерации; занимать её новым нельзя — иначе ссылка из правила новым нельзя — иначе ссылка из чужого репозитория начнёт указывать
чужого репозитория начнёт указывать на другое утверждение. То же на другое утверждение. То же относится к префиксам: выбывшие хранит манифест
относится к префиксам: выбывшие хранит `prefixes.toml`. набора.
- **Снятое правило остаётся заглушкой.** Заголовок и номер сохраняются, норму
с обоснованием заменяет блок СНЯТО с датой и причиной. Поэтому нумерация в
файле сплошная, а любая ссылка разрешается — либо в правило, либо в
объяснение, почему его сняли (META-31, META-32). Отдельного реестра снятых
номеров нет: он был бы вторым источником правды рядом с файлом.
- Префикс **выбирается под файл, а не выводится по формуле**: он нужен, - Префикс **выбирается под файл, а не выводится по формуле**: он нужен,
чтобы по нему искать, а не чтобы его разбирать. Выводимый префикс вдобавок чтобы по нему искать, а не чтобы его разбирать. Выводимый префикс вдобавок
привязал бы идентификатор к таксономии, которую канон перестраивает, и привязал бы идентификатор к таксономии, которую канон перестраивает, и
@@ -352,6 +510,25 @@ Directives, Part 2, по одной форме записи на ступень,
Порядок правил в файле выбирается по читаемости, не по номерам: номер — это Порядок правил в файле выбирается по читаемости, не по номерам: номер — это
идентификатор, а не позиция. идентификатор, а не позиция.
**Тема.**
- **Тема — набор правил об одном фокусе разработки:** время, конфигурация,
схема БД. Она же — единица подписки: потребитель берёт тему целиком, а не
отдельные правила.
- Имя темы записывается латиницей. Рекомендуется нижний kebab-case
(`db-identifiers`), но годится любой идентификатор, пригодный для имени
файла: имя попадает и в файловую систему потребителя, и в его манифест.
- **Тему объявляет файл, а не файловая система.** Имя стоит в шапке
(`topic:`) и зарегистрировано в манифесте набора. Слои одной темы несут
одно и то же имя — по нему они и собираются в один документ, как бы ни
назывались их файлы.
- **Имя темы не переиспользуется** — как и префикс: оно живёт в шапке
`origin:` каждой копии, в подписке манифеста и в тексте ссылок, поэтому
снятое имя уходит в раздел выбывших манифеста, а не достаётся другой теме.
- Ссылка на конвенцию — имя темы (конвенция `logging`), ссылка на правило —
идентификатор (`XLOG-27`). Путь файла не употребляется ни там, ни там: в
собранной копии путей канона не существует.
## Что правилом не является ## Что правилом не является
Заглавные модальные слова в этих частях **не употребляются** — иначе Заглавные модальные слова в этих частях **не употребляются** — иначе
@@ -364,6 +541,10 @@ Directives, Part 2, по одной форме записи на ступень,
- **Локальная часть копии** — содержимое принадлежит репозиторию. - **Локальная часть копии** — содержимое принадлежит репозиторию.
- Вводная проза, объясняющая предмет конвенции. - Вводная проза, объясняющая предмет конвенции.
Все четыре части лежат вне областей правил: до первого заголовка правила или
после заголовка, которым область закрылась. Хвост обоснования сюда не
относится — он внутри правила, и модальные слова в нём законны как упоминания.
## Как на правила ссылаются копии ## Как на правила ссылаются копии
Ниже маркера локальной части, в репозитории: Ниже маркера локальной части, в репозитории:
@@ -371,10 +552,9 @@ Directives, Part 2, по одной форме записи на ступень,
```markdown ```markdown
<!-- conv:local --> <!-- conv:local -->
MIGR-2, MIGR-4 механизированы — `internal/archrules` (проверяются в новых XMIG-2, XMIG-4 — МЕХАНИЗИРОВАНО: `internal/archrules`, в новых миграциях.
миграциях).
MIGR-6 не соблюдается в легаси-таблицах `show_history`, `queue`: составные XMIG-6 не соблюдается в легаси-таблицах `show_history`, `queue`: составные
ключи там появились до конвенции, переписывание требует миграции данных. ключи там появились до конвенции, переписывание требует миграции данных.
``` ```
@@ -383,38 +563,75 @@ MIGR-6 не соблюдается в легаси-таблицах `show_histor
## Что стоит проверять машиной ## Что стоит проверять машиной
Проверки применяются к файлам конвенций; обвязка канона в них не входит — Проверке подлежит всё, что язык **употребляет**: файлы конвенций и документ,
она ключевые слова цитирует, а не употребляет. Ни одна пока не реализована, которым канон сам себя ведёт (в этом каноне — `GUIDE.md`, префикс META). Файл,
поэтому при ревью их выполняют чтением. который язык **цитирует**, — это описание вроде текущего: ключевые слова стоят
в нём как предмет разговора, а не как норма, и проверки к нему не применяются.
Различает не расположение файла, а роль слова в нём.
Разбором текста: Ни одна проверка пока не реализована, поэтому при ревью их выполняют чтением.
**Форма правила** — разбором текста, в любом файле, который язык употребляет:
- модальные и служебные слова принадлежат объявленному словарю канона, а не - модальные и служебные слова принадлежат объявленному словарю канона, а не
смеси словарей; смеси словарей;
- префикс в шапке файла совпадает с реестром, состоит из четырёх заглавных - префикс в шапке файла совпадает с манифестом набора, состоит из четырёх
латинских букв, не начинается на `X` и не значится в списке выбывших; заглавных латинских букв, не начинается на `X` и не значится в списке
выбывших;
- заголовки правил файла используют только его собственный префикс; - заголовки правил файла используют только его собственный префикс;
- номера уникальны внутри файла и не имеют пропусков вниз (новое правило - нумерация внутри файла сплошная: от единицы до наибольшего номера без
берёт следующий свободный, а не первый освободившийся); пропусков, номера не повторяются, новое правило берёт следующий за
- у каждого `### <ПРЕФИКС>-<n>` есть модальное слово и блок ПОЧЕМУ; наибольшим (META-31);
отметка МЕХАНИЗИРОВАНО стоит рядом с модальностью, а не вместо неё; - у каждого `### <ПРЕФИКС>-<n>` есть либо модальное слово с нормой и блок
ПОЧЕМУ — ни норма, ни обоснование не удаляются никогда (META-8, META-10), —
либо блок СНЯТО с датой и причиной;
- блок ПРИМЕРЫ, если он есть, стоит после обоснования и не открывает правило:
порядок блоков — норма, ПОЧЕМУ, ПРИМЕРЫ;
- вводная проза содержит строку о версии языка; - вводная проза содержит строку о версии языка;
- ссылки вида `<ПРЕФИКС>-<n>` — хоть в тексте канона, хоть в локальной части - ссылки вида `<ПРЕФИКС>-<n>` — хоть в тексте набора, хоть в локальной части
копии — указывают на правила, которые ещё существуют; копии — разрешаются: неразрешённый идентификатор всегда ошибка (META-32);
- заглавные модальные слова не встречаются вне областей правил (область —
от заголовка правила до следующего заголовка) — кроме строки о версии
языка, которая их перечисляет по назначению;
- модальная метка стоит первой в своём абзаце: заглавное слово в середине
фразы — упоминание ступени, а не вторая норма правила.
**Распространение** — разбором текста, только в файлах конвенций: эти проверки
о том, что документ уезжает к потребителю, а документ, которым канон ведёт
себя, не уезжает никуда.
- шапка файла несёт имя темы, и это имя стоит в манифесте набора — среди
живых, а не среди выбывших;
- ось слоя объявлена в шапке, а не выведена из пути; у одной темы не больше
одного слоя без ключей оси — базовый слой единственный;
- если директории осей используются, объявленное в шапке совпадает с путём:
расхождение означает переезд файла без правки шапки;
- имя темы, названное в ссылке или в подписке потребителя, тоже разрешается по
манифесту: ссылка на снятую тему не проходит молча;
- префиксы локальных правил копии начинаются на `X`; - префиксы локальных правил копии начинаются на `X`;
- заглавные модальные слова не встречаются вне правил — кроме строки о - отметки МЕХАНИЗИРОВАНО в тексте конвенции нет: её место — запись о
версии языка, которая их перечисляет по назначению; механизации в локальной части копии (META-7);
- словарь в коротком описании языка совпадает с этим: те же ступени, те же
метки, те же значения (META-30);
- префикс **чужой темы** не встречается в абзаце с модальностью (META-20); - префикс **чужой темы** не встречается в абзаце с модальностью (META-20);
префикс арх-слоя своей темы там допустим (META-24), префикс другого языка префикс арх-слоя своей темы там допустим (META-24), префикс другого языка
или стека — нет; или стека — нет;
- путь файла канона не встречается в тексте конвенции (META-21). - путь файла канона не встречается в тексте конвенции (META-21).
Чтением, потому что машине не даётся: **Чтением**, потому что машине не даётся:
- строки таблицы взаимоисключающи либо политика совпадения объявлена; - строки таблицы взаимоисключающи либо политика совпадения объявлена;
- перечисленные в таблице случаи покрывают область действия; - перечисленные в таблице случаи покрывают область действия;
- норма исполнима без обращения к другим файлам; - норма исполнима без обращения к другим файлам (в файлах конвенций: они
- обоснование отвечает на «что сломается», а не пересказывает норму. уезжают по одной, а обвязка ссылается на соседей свободно);
- обоснование отвечает на «что сломается», а не пересказывает норму;
- хвост обоснования не вводит требований, которых нет в блоке нормы;
- примеры иллюстрируют норму и не расширяют её: деталь примера не читается
как требование.
Проверки выше — про запись правила. Граница самой темы (не собрала ли она
два решения сразу, нужна ли потребителю целиком) языку не принадлежит: её
вопросы стоят в документе, которым набор ведёт себя.
## Версия языка ## Версия языка
+134
View File
@@ -0,0 +1,134 @@
---
version: 1
---
# Как читать конвенцию
Файлы рядом с этим — конвенции: правила о том, как в этом репозитории пишут
код. Здесь сказано, как они записаны: что означают заглавные слова, из чего
состоит правило и как на него сослаться. Читается один раз, дальше нужен как
справка.
Файл кладёт сюда сборщик конвенций и перезаписывает целиком при каждом
обновлении. Правки в нём не живут.
## Ключевые слова
Заглавное слово в начале абзаца задаёт обязательность правила.
| Слово | Что означает | Если делаем иначе |
|---|---|---|
| **ДОЛЖЕН** | нарушение считается ошибкой | только с записью в отступления |
| **НЕ ДОЛЖЕН** | запрет, та же строгость | то же |
| **СЛЕДУЕТ** | сильная рекомендация: новый код пишем так | допустимо, причину записываем |
| **НЕ СЛЕДУЕТ** | обратное к СЛЕДУЕТ | то же |
| **ДОПУСКАЕТСЯ** | выбор за автором кода | ничего не требуется — правило не запрещает |
Две вещи, которые легко прочитать неверно:
- **ДОПУСКАЕТСЯ — не бытовое «можно».** У слова есть вторая половина: выбор,
помеченный им, на ревью не обсуждается. Возражение «сделай иначе» против
такого выбора не принимается — иначе разрешение ничего не значило бы.
- **Отступление от ДОЛЖЕН — не запрет на отступление.** Нарушать можно, но
тогда об этом появляется запись: какое правило, где именно, почему. Разница
между ДОЛЖЕН и СЛЕДУЕТ — в том, чем платят за отклонение, а не в том,
возможно ли оно.
Ещё четыре метки заглавными: **ПОЧЕМУ** открывает обоснование правила,
**ПРИМЕРЫ** — код, показывающий норму в деле, **МЕХАНИЗИРОВАНО** стоит при
записи о том, что правило проверяет линтер или скрипт, **СНЯТО** — на месте
правила, которое убрали.
Заглушка со СНЯТО занимает место убранного правила вместе с его номером:
так нумерация остаётся сплошной, а ссылка на снятое правило приводит к
объяснению, а не в пустоту. Требований в такой заглушке нет.
```markdown
### XLOG-4. Уровень записи выбирался по громкости отказа
**СНЯТО 2026-05-14.** Заменено на XLOG-8: громкость каждый оценивал
по-своему, и шкала расползалась.
```
**Нормативно только заглавное написание.** Строчное «должен» в прозе — обычная
речь, а не норма; спорить с ней как с правилом не нужно.
## Из чего состоит правило
Правило в примере вымышленное: префиксы на `X` общий набор не занимает
никогда, поэтому пример нельзя спутать с настоящим правилом.
```markdown
### XLOG-8. Уровень выбирается по адресату
**ДОЛЖЕН.** Уровень отвечает на вопрос «кому сообщение», а не «насколько
громко сломалось».
| № | Уровень | Кому и когда |
|---|---|---|
| XLOG-8.1 | `DEBUG` | разработчику при отладке; в проде выключен |
| XLOG-8.2 | `INFO` | владельцу, аудит постфактум |
**ПОЧЕМУ.** Адресат — единственный признак, по которому разные авторы в
разных местах кода выберут уровень одинаково…
```
- **Идентификатор и заголовок.** `XLOG-8` — адрес правила: по нему на правило
ссылаются, им помечают отступления и механизацию.
- **Модальность с нормой.** Собственно требование, одной фразой. Таблица или
список сразу за модальным словом — часть нормы: она уточняет вердикт, и на
её строку ссылаются номером (`XLOG-8.2`).
- **ПОЧЕМУ.** Зачем правило существует и что сломается, если сделать иначе.
Обоснование ничего не требует — по нему решают, применимо ли правило к
случаю, и видно, когда причина отпала.
- **ПРИМЕРЫ** — необязательный последний блок: код, обычно парой «плохо →
хорошо». Иллюстрация, а не спецификация: деталь примера требованием не
становится, дословно копировать его не нужно, а если пример разошёлся с
нормой — действует норма.
**Правило кончается перед следующим заголовком.** Абзацы после ПОЧЕМУ — это
продолжение обоснования: примеры, разбор границ, ссылки на внешние практики.
Требований в них нет; всё, что подлежит исполнению, стоит в блоке нормы.
## Как ссылаться
- На **правило** — идентификатором: `XLOG-27`. Путь к файлу не нужен,
идентификатор уникален.
- На **конвенцию целиком** — именем темы: конвенция `logging`. Имя темы стоит
в шапке файла (`origin:`).
- Строка таблицы адресуется номером с точкой: `XLOG-8.2`.
## Что ниже маркера
```markdown
<!-- conv:local -->
```
Всё выше маркера приезжает из общего набора и перезаписывается при
обновлении. Всё ниже принадлежит этому репозиторию и обновление переживает.
Там живёт:
- **отступления** — какое правило не соблюдается, где и почему: «`XMIG-6` не
соблюдается в `queue`: составные ключи там появились до конвенции»;
- **механизация** — кто проверяет правило машинно: «`XMIG-4`
МЕХАНИЗИРОВАНО: `internal/archrules`»;
- **разрешение условий**, которые правило оставило открытыми;
- **свои правила** — по той же форме, но с префиксом на `X` (`XLOG-1`).
Префиксы на `X` общий набор не занимает никогда, так что столкнуться они не
могут.
Правка выше маркера живёт до первого обновления и исчезает молча. Если
исправить нужно приехавший текст — либо правку переносят в общий набор, либо
файл перестаёт быть копией: из шапки убирают `origin:`.
## Чего в конвенции не бывает
- **Утверждений о том, как сейчас устроен этот репозиторий.** Правило пишется
в предписывающем времени; «у нас пока не так» — это отступление, и его
место ниже маркера.
- **Заглавных ключевых слов вне правил.** Во вводной прозе, в разделах
«Область действия» и «Связано» их нет, поэтому искать там требования не
нужно.
Язык записи — версия 1. Полное описание живёт в наборе конвенций, у автора;
здесь ровно то, что нужно читателю.
+231 -51
View File
@@ -12,13 +12,14 @@
| `README.md` | устройство канона, оси, сборка копий, жизненный цикл | | `README.md` | устройство канона, оси, сборка копий, жизненный цикл |
| [LANGUAGE.md](LANGUAGE.md) | язык записи правил: идентификаторы, модальность, обоснование | | [LANGUAGE.md](LANGUAGE.md) | язык записи правил: идентификаторы, модальность, обоснование |
| [GUIDE.md](GUIDE.md) | как ведут конвенции: когда заводить, механизация, отступления | | [GUIDE.md](GUIDE.md) | как ведут конвенции: когда заводить, механизация, отступления |
| `prefixes.toml` | реестр префиксов правил | | [READING.md](READING.md) | как читать конвенцию: то, что едет к потребителю |
| `conv` | сборка копий | | `.conventions-suite.toml` | манифест набора: язык, темы, префиксы правил |
Обвязка живёт только в каноне и в репозитории не оказывается — в копию едет К потребителю едет содержимое `conventions/` и один файл обвязки —
лишь содержимое `conventions/`. Самодостаточность копии это не нарушает: `READING.md`; остальная обвязка остаётся в каноне. Самодостаточность копии это
конвенция называет язык записи одной строкой с номером версии и не ссылается не нарушает: конвенция называет язык записи одной
на путь (`LANGUAGE.md`, раздел «Ссылка на язык из конвенции»). строкой с номером версии и не ссылается на путь (`LANGUAGE.md`, раздел «Ссылка
на язык из конвенции»).
Правило то же, что у ролей: **деплоится и читается только то, что лежит в Правило то же, что у ролей: **деплоится и читается только то, что лежит в
git репозитория**. Канон никем не подключается на лету. git репозитория**. Канон никем не подключается на лету.
@@ -51,12 +52,27 @@ conventions/
stack/<стек>/ привязка к инструменту, хранилищу, транспорту stack/<стек>/ привязка к инструменту, хранилищу, транспорту
``` ```
Оси — раскладка **канона**; в репозитории копия лежит плоско, файлом на Ось файл **объявляет в шапке**, а не наследует от директории (META-38):
тему. Пути файлов даются относительно `conventions/`
(`arch/db-identifiers.md`) и адресуют исходник канона, а не место в копии. ```yaml
На **правила** ссылаются идентификатором без пути: `KEYS-5`. Префикс topic: logging
уникален по всему канону (реестр — `prefixes.toml`), поэтому идентификатор prefix: SLOG
не зависит ни от оси, ни от того, как собран файл у потребителя. lang: go
```
Ключей оси нет — базовый слой темы. Слова те же, что в подписке потребителя
(`lang`, `stack`), так что переводить между двумя сторонами нечего. Дерево
директорий повторяет объявленное для человека и остаётся раскладкой
**канона**: в репозитории копия лежит плоско, файлом на тему. Пути файлов
даются относительно `conventions/` (`arch/db-identifiers.md`) и адресуют
исходник канона, а не место в копии. На **правила** ссылаются идентификатором
без пути: `KEYS-5`. Префикс уникален по всему канону (он перечислен в
манифесте набора), поэтому идентификатор не зависит ни от оси, ни от того, как
собран файл у потребителя.
Объявление вдобавок выражает то, чего дерево не умеет: слой, осмысленный
только при совпадении языка и инструмента сразу (`lang: go` и `stack: slog` в
одной шапке).
Тест — по тому, замена чего убивает правило: Тест — по тому, замена чего убивает правило:
@@ -81,6 +97,63 @@ conventions/
пласт `stack/sqlite/` (типы колонок). Это не принцип, а незавершённая пласт `stack/sqlite/` (типы колонок). Это не принцип, а незавершённая
работа. работа.
## Плоский набор
Осей может не быть вовсе. Набор, где у каждой темы ровно один слой, — не
особый режим, а низкий конец той же модели: сборка «база → язык → стек»
на нём даёт просто копию файла.
```
conventions/
logging.md topic: logging, prefix: LOGS
errors.md topic: errors, prefix: ERRS
time.md topic: time, prefix: TIME
```
Ключей оси в шапках нет, `lang` и `stack` в подписке не пишутся — выбирать
не из чего. Так дешевле начинать и так выглядит чужой набор, которому три оси
объяснять незачем, чтобы записать пять правил.
Цена платится при росте, и она не в инструменте: когда плоская тема
расслаивается, уехавшие в новый файл правила получают новый префикс и новую
нумерацию. Смягчается это тем, что уехавшее правило остаётся на месте
заглушкой СНЯТО с новым адресом в причине (META-31, META-32), и тем, что
резать нужно правильной стороной: база остаётся в исходном файле со своими
идентификаторами, а наружу уезжает специфичное. Если второй язык виден
заранее, дешевле сразу разложить по осям.
## Темы
**Тема — набор правил об одном фокусе разработки:** время, конфигурация,
схема БД. Она же единица подписки и единица сборки: потребитель берёт тему
целиком, а сборщик складывает в один файл все её слои.
Имя темы записывается латиницей; рекомендуется нижний kebab-case, но годится
любой идентификатор, пригодный для имени файла — имя попадает и в файловую
систему потребителя, и в его манифест. Файл конвенции объявляет тему в шапке:
```yaml
topic: db-identifiers
prefix: KEYS
```
Слои одной темы несут одно и то же имя — по нему они и собираются в один
документ, как бы ни назывались их файлы. Имя файла повторяет тему из
удобства, но истина — в шапке.
Темы перечислены в манифесте набора — `.conventions-suite.toml`,
секция `[topics.live]`: имя и однострочное описание. Имя темы не
переиспользуется по той же причине, что и префикс: оно живёт в чужих
репозиториях — в шапке `origin:` каждой копии, в подписке манифеста, в тексте
ссылок, — и выданное второй теме начинает указывать на другой набор правил.
Раз имя вечно, называют тему **решением и его адресатом**, а не ролью части
конкретного проекта (META-37). Логи сервера и логи браузера — это `logging` и
`client-logging`, а не `logging-backend` и `logging-frontend`: роль
принадлежит сегодняшнему устройству одного репозитория и молча начинает врать,
а суффиксная пара вдобавок навязывает чтение «две разновидности одного», хотя
по границе темы это разные решения — общего у них три правила из сорока.
## Префиксы ## Префиксы
Каждый файл канона объявляет в шапке свой префикс правил: Каждый файл канона объявляет в шапке свой префикс правил:
@@ -89,10 +162,17 @@ conventions/
prefix: KEYS prefix: KEYS
``` ```
Четыре заглавные латинские буквы, уникальные по всему канону; реестр — Четыре заглавные латинские буквы, уникальные по всему канону; перечислены в
[`prefixes.toml`](prefixes.toml). Префикс выбирается под файл, а не выводится манифесте набора, секция `[prefixes.live]`, путём **от корня репозитория**, а
по формуле, и не переиспользуется никогда. Правила адресуются идентификатором не от `conventions/` — манифест покрывает и обвязку тоже. Префикс выбирается
`KEYS-5` — без пути к файлу. Подробности формы — `LANGUAGE.md`. под файл, а не выводится по формуле, и не переиспользуется никогда. Правила
адресуются идентификатором `KEYS-5` — без пути к файлу. Подробности формы —
`LANGUAGE.md`.
`GUIDE.md` тоже несёт префикс и тоже проверяется как конвенция: правила в нём
записаны тем же языком и цитируются по номерам. Темы у него нет — подписаться
на него нельзя, к потребителю он не едет, — и манифест называет его отдельным
ключом `governance`, чтобы конвенция, потерявшая `topic`, не сошла за него.
Буква `X` в начале префикса зарезервирована за репозиториями: канон её не Буква `X` в начале префикса зарезервирована за репозиториями: канон её не
занимает никогда, а локальные правила потребителя берут префиксы только на занимает никогда, а локальные правила потребителя берут префиксы только на
@@ -114,23 +194,64 @@ extends: arch/db-identifiers.md
репозиторий на базу просто не подписан. репозиторий на базу просто не подписан.
`extends` — документация связи, а не механизм: за тем, чтобы база лежала `extends` — документация связи, а не механизм: за тем, чтобы база лежала
рядом, никто не следит. рядом, никто не следит. С объявленной осью база к тому же находится сама —
это слой той же темы без ключей `lang` и `stack`, — так что ключ остаётся
подсказкой человеку и ничего не выбирает.
## Компонент — адресат сборки
Подписка принадлежит репозиторию, а собранный документ адресован не
репозиторию, а куску кода. Пока проект однороден, разницы нет; два языка её
проявляют: `lang = ["go", "javascript"]` склеили бы в один файл го-слой и
js-слой, из которых к правимому коду относится ровно половина.
**Компонент — область репозитория, где все выбранные слои действуют
одновременно.** `sqlite` и `postgres` в теме схемы действуют вместе — разные
таблицы одного сервиса; го-слой и js-слой не действуют вместе никогда, потому
что строка кода написана на чём-то одном. Компонент поэтому совпадает с тем,
у чего один язык, один набор инструментов и один вид приложения (META-36).
Уровней в модели становится три: набор → проект → компонент. Сборка не
меняется — та же линейка «база → язык → стек», прогнанная по разу на
компонент.
## Копия в репозитории ## Копия в репозитории
Копия плоская: **один файл на тему**, слои осей идут внутри него секциями в Копия плоская: **один файл на тему**, слои осей идут внутри него секциями в
порядке `arch` → язык → стек. Пути канона в копии не воспроизводятся. порядке база → язык → стек. Пути канона в копии не воспроизводятся. Каждый
компонент получает свою директорию:
``` ```
docs/conventions/ .conventions.toml
backend/docs/conventions/
README.md собственный, не собирается README.md собственный, не собирается
READING.md как читать конвенцию — приезжает из канона
logging.md база + lang/go + stack/slog
time.md arch/time.md + lang/go/time.md time.md arch/time.md + lang/go/time.md
db-identifiers.md arch/db-identifiers.md + lang/go/db-identifiers.md web/docs/conventions/
app-directories.md arch/… + stack/ansible/… READING.md
client-logging.md база + lang/javascript + stack/express
``` ```
Так конвенция остаётся самодостаточным документом: тот, кто проверяет по ней Так конвенция остаётся самодостаточным документом: тот, кто проверяет по ней
код — человек или агент, — читает один файл и не собирает тему из трёх мест. код — человек или агент, — читает один файл и не собирает тему из трёх мест.
При одном компоненте это ровно прежняя раскладка — `docs/conventions/` в
корне.
Директории компонентов различны, и это единственное, что разводит копии:
`logging.md` двух компонентов — разные файлы с одинаковым `origin: logging`,
и какой из них какой, сборщик знает по манифесту, а читатель — по пути.
Локальные части у них независимы, ради чего всё и затевается: правило,
механизированное линтером в go-компоненте, в js-компоненте не механизировано,
и один общий файл этого не записал бы.
`READING.md` лежит рядом с копиями, то есть по одному на компонент. Файл
генерируемый, а директория с правилами обязана объяснять себя тому, кто в неё
попал.
Имена в директории делятся на три вида: `README.md` принадлежит репозиторию и
сборщик его не трогает, `READING.md` принадлежит канону и перезаписывается
целиком, остальные файлы — копии тем с шапкой `origin:` и локальной частью.
**Шапка копии** ставится при сборке и в каноне не хранится: **Шапка копии** ставится при сборке и в каноне не хранится:
@@ -140,10 +261,11 @@ origin: time
--- ---
``` ```
Больше в шапке ничего нет. Отпечатка канона и даты синхронизации в ней не В `origin:` стоит имя темы — то же, что в манифесте набора и в шапках
хранится: обновление перезаписывает файл в рабочем дереве, и что именно `topic:` слоёв, из которых файл собран. Больше в шапке ничего нет: отпечатка
изменилось, показывает `git diff` до коммита. Второй механизм сравнения канона и даты синхронизации в ней не хранится, потому что обновление
рядом с git не нужен. перезаписывает файл в рабочем дереве, и что именно изменилось, показывает
`git diff` до коммита. Второй механизм сравнения рядом с git не нужен.
**Маркер локальной части** — единственная машинно значимая разметка внутри **Маркер локальной части** — единственная машинно значимая разметка внутри
файла: файла:
@@ -151,7 +273,7 @@ origin: time
```markdown ```markdown
<!-- conv:local --> <!-- conv:local -->
MIGR-2, MIGR-4 механизированы — `internal/archrules`. MIGR-2, MIGR-4 — МЕХАНИЗИРОВАНО: `internal/archrules`.
MIGR-6 не соблюдается в `show_history`, `queue`: составные ключи там MIGR-6 не соблюдается в `show_history`, `queue`: составные ключи там
появились до конвенции, миграция данных не окупается. появились до конвенции, миграция данных не окупается.
``` ```
@@ -170,23 +292,63 @@ MIGR-6 не соблюдается в `show_history`, `queue`: составны
репозитория. Файл, оставивший шапку, при следующем обновлении потеряет репозитория. Файл, оставивший шапку, при следующем обновлении потеряет
всё, что выше маркера. всё, что выше маркера.
## Манифест ## Язык записи едет вместе с копиями
Откуда взяты копии и где брать обновления — `.conventions.toml` в корне Конвенция называет язык одной строкой с номером версии и без пути — строка
репозитория-потребителя: работает и сама по себе. Но семантика заглавных слов живёт в описании языка, а
описание в репозиторий-потребитель раньше не попадало: агент, читающий копию,
принимал ДОПУСКАЕТСЯ за бытовое «можно» и терял ровно то, ради чего слово
введено.
Поэтому в `docs/conventions/` сборщик кладёт `READING.md` — короткое описание
для читателя правил: словарь со значениями, правило заглавных, из чего состоит
правило и где его граница, как ссылаться, что живёт ниже маркера. Полное
[LANGUAGE.md](LANGUAGE.md) остаётся в каноне: три его раздела адресованы
автору набора и ссылаются на правила `GUIDE.md`, которых у потребителя нет.
Два документа — один словарь, и это единственное место, где возможен дрейф.
Правка ключевых слов или состава частей правила обязана дойти до `READING.md`
(META-30), а сверить их дёшево: таблицы либо совпадают, либо нет.
## Два манифеста
Манифестов в модели два, и они отвечают на разные вопросы:
| Файл | Где лежит | Что описывает |
|---|---|---|
| `.conventions-suite.toml` | в наборе | сам набор: язык, темы, префиксы правил |
| `.conventions.toml` | в проекте | подключение: откуда копии, компоненты и их подписки |
Манифест набора — единственное место, где перечислены оба идентификатора
канона; правила у них общие, поэтому и файл один. Манифест подключения
отвечает, откуда взяты копии и где брать обновления:
```toml ```toml
source = "ssh://git@git.vakhrushev.me:2222/av/dev-conventions.git" source = "ssh://git@git.vakhrushev.me:2222/av/dev-conventions.git"
[components.backend]
dir = "backend/docs/conventions"
lang = ["go"] lang = ["go"]
stack = ["sqlite", "htmx"] stack = ["slog", "sqlite"]
topics = ["logging", "errors", "time"]
topics = ["time", "config", "db-identifiers"] [components.web]
dir = "web/docs/conventions"
lang = ["javascript"]
stack = ["express"]
topics = ["client-logging"]
``` ```
`lang` и `stack` выбирают строку разреженной матрицы: файл темы собирает `lang` и `stack` выбирают строку разреженной матрицы: файл темы собирает
только те слои, которые репозиторию подходят. `topics` — подписка; списка только те слои, которые компоненту подходят, и совпадают со словами, которыми
подписчиков у канона по-прежнему нет, список тем есть только у потребителя. слой объявил свою ось. `topics` — подписка, именами из манифеста набора;
списка подписчиков у канона по-прежнему нет, список подписок есть только у
потребителя.
Компонент пишется всегда, даже когда он один: сокращённая плоская форма
сэкономила бы три строки и завела бы второй способ сказать то же самое.
Имя компонента при этом не служебное — им сборщик отвечает, что и куда
собрал. У плоского набора `lang` и `stack` в компоненте просто отсутствуют.
Как именно инструмент добирается до канона — путь на диске, git, HTTP — Как именно инструмент добирается до канона — путь на диске, git, HTTP —
дело инструмента, а не модели. Манифест отвечает на два вопроса: откуда это дело инструмента, а не модели. Манифест отвечает на два вопроса: откуда это
@@ -202,19 +364,37 @@ topics = ["time", "config", "db-identifiers"]
любого другого документа. Это главный канал тихого дрейфа, поэтому любого другого документа. Это главный канал тихого дрейфа, поэтому
`AGENTS.md` каждого потребителя должен явно говорить: `AGENTS.md` каждого потребителя должен явно говорить:
> Файлы в `docs/conventions/` с шапкой `origin:` — копии из канона > Файлы с шапкой `origin:` в директориях конвенций (пути — в
> `dev-conventions`. Репозиторное пишется только ниже `<!-- conv:local -->`; > `.conventions.toml`) — копии из канона `dev-conventions`. Репозиторное
> всё выше маркера перезаписывается при обновлении. Своё правило — с > пишется только ниже `<!-- conv:local -->`; всё выше маркера
> префиксом на `X`. > перезаписывается при обновлении. Своё правило — с префиксом на `X`.
## Команды ## Команды
Копии собирает `convy` — отдельный инструмент, живущий в своём репозитории и
ставящийся бинарём. Запускают его из корня репозитория-потребителя:
```bash ```bash
conv list # какие темы есть в каноне convy init --source <ссылка на канон> --component backend \
conv add time # добавить тему в манифест и собрать файл --dir docs/conventions --lang go
conv pull # пересобрать всё, что перечислено в манифесте convy add time # подписаться на тему и собрать файл
convy add time --for backend # то же, когда компонентов несколько
convy pull # пересобрать всё, что перечислено в манифесте
# (и обновить READING.md рядом с копиями)
convy pull --for web # только один компонент
convy sync # подвести раскладку файлов под манифест
convy list # что подключено и что ещё есть в каноне
convy check # проверить форму того, что лежит здесь
``` ```
При одном компоненте `--for` не нужен. При нескольких команда без него не
угадывает, а отказывает и перечисляет имена.
Манифест подключения правится и руками — это данные, а не текст с
комментариями. Что бы в нём ни поменяли, раскладку под него подводит `convy
sync`: чего не хватает — соберёт, что осиротело — уберёт, а копию с локальной
частью не тронет и назовёт.
Отчёт о том, что изменилось, отдельной командой не выдаётся: после `pull` Отчёт о том, что изменилось, отдельной командой не выдаётся: после `pull`
его показывает `git diff`, а решение — принять, поправить или откатить — его показывает `git diff`, а решение — принять, поправить или откатить —
принимает человек перед коммитом. принимает человек перед коммитом.
@@ -223,11 +403,9 @@ conv pull # пересобрать всё, что переч
репозитории, переносится в канон руками: это редкая операция, и её цена — репозитории, переносится в канон руками: это редкая операция, и её цена —
не аргумент против того, чтобы направление оставалось односторонним. не аргумент против того, чтобы направление оставалось односторонним.
Запускать из корня репозитория: Сам канон ведут те же командой под `suite`: `convy suite add` заводит
конвенцию, `convy suite rule` дописывает правило, `convy suite retire`
```bash снимает, `convy suite check` проверяет целостность набора.
~/projects/private/dev-conventions/conv pull
```
Обёртка в раннере репозитория (`inv conventions -- pull` для ansible, Обёртка в раннере репозитория (`inv conventions -- pull` для ansible,
`task conventions -- pull` для Go) — тонкий проброс аргументов, чтобы `task conventions -- pull` для Go) — тонкий проброс аргументов, чтобы
@@ -247,9 +425,11 @@ conv pull # пересобрать всё, что переч
## Состояние ## Состояние
Модель выше — согласованная, а не реализованная. `conv` пока собран под Модель выше реализована в `convy`: сборка копий, отбор слоёв по объявленной
прежнюю: зеркальное дерево копий вместо плоского, именованные регионы оси, маркер локальной части, `READING.md` рядом с копиями, проверка
`<!-- local:имя -->` вместо одного маркера, `origin_hash` в шапке и команды целостности набора. Прежний питоновский `conv` — с зеркальным деревом,
`status`, `diff`, `push`. Сами конвенции уже приведены к новой модели — именованными регионами и `origin_hash` — удалён вместе со своей моделью.
именованных регионов в каноне нет. Ни один репозиторий-потребитель не
подключён, поэтому переход никого не ломает. Ни один репозиторий-потребитель ещё не подключён: копий с шапкой `origin:` в
природе нет. Пока это так, непроверенным остаётся главное — как всё это живёт
в чужом репозитории через полгода после первой сборки.
+119 -105
View File
@@ -4,110 +4,14 @@
решения. Закрытый вопрос отсюда удаляется — принятое решение живёт в решения. Закрытый вопрос отсюда удаляется — принятое решение живёт в
`README.md`, `GUIDE.md` или `LANGUAGE.md`, а не в этом файле. `README.md`, `GUIDE.md` или `LANGUAGE.md`, а не в этом файле.
## 1. Тулинг: две разные задачи в одном `conv` Вопросы про инструмент здесь не живут — они собраны в его собственном
Сейчас в `conv` смешаны две категории работы, и они расходятся по всему —
по частоте запуска, по тому, кто запускает, и по тому, что считается
провалом.
**Целостность канона.** Префиксы уникальны и не переиспользованы, шапка
совпадает с реестром, номера без дыр вниз, у каждого правила модальность и
«Почему», ссылки разрешаются, префикс чужой темы не лезет в норму (META-20),
путей канона в тексте нет (META-21), строка о версии языка на месте.
Запускается в каноне, при каждой правке, провал — это ошибка. Логика уже
написана и много раз прогнана руками, но живёт в скретчпаде, а не в
репозитории. репозитории.
**Установка в проект.** Манифест `.conventions.toml`, сборка файла темы из Две секции: сначала язык и подход, потом сам набор и подключение.
слоёв (`arch` → язык → стек), сохранение при пересборке всего, что ниже
маркера, предупреждение о висячих ссылках на неподписанные темы. Запускается
в репозитории-потребителе, изредка, провал — это чаще «посмотри глазами»,
чем «ошибка». Отчёта «канон ушёл вперёд» здесь нет: его делает `git diff`
после пересборки.
Что обсудить: # Язык и подход
- Разделять ли на два исполняемых файла, или хватит подкоманд с честной ## 1. Одиннадцать таблиц не прочитаны на взаимоисключительность
границей внутри.
- Валидацию канона стоит ли отдать агенту скиллом вместо (или вдобавок к)
скрипту: часть проверок формулируется как «прочитай и скажи, самодостаточна
ли норма» — механически это не берётся, а агентом берётся.
- Куда в этой раскладке ложится запаркованное предупреждение о висячих
ссылках: это установка, а не целостность, но список подписок ему нужен из
манифеста.
Перед тем как переписывать, стоит посмотреть на два готовых прототипа:
дистрибуцию пакетов Vale (`.vale.ini``vale sync``styles/`) как образец
манифеста и `vendir.yml` — как пример того, где проходит граница между «чего
хочу» и «что получил».
## 2. Пары слоёв и темы без базы
Отложено сознательно, но список стоит держать перед глазами:
- `SLOG` объявляет `extends: arch/time.md` — расширение **чужой** темы.
Сборщик темы `logging` на это наткнётся: базового слоя с темой `logging`
нет, а `arch/time.md` он тянуть не должен. Чинится переводом в обычную
ссылку «связано».
- Темы без арх-слоя: `db-schema`, `errors`, `web-ui`. Собираются в файл с
одной секцией — само по себе не ломается, но это и есть тот невыделенный
арх-слой из известного долга.
- Имена тем в паре не совпадают: `arch/db-identifiers.md` против
`lang/go/db-schema.md`. При сборке по имени темы это две разные темы —
проверить, что так и задумано.
- В `lang/go/db-schema.md` сидит целый пласт `stack/sqlite/` (типы колонок),
тоже из известного долга README.
- Вынос арх-ядра из `errors` и `logging` закроет две хрупкие ссылки из
`web-ui` (стек) в go-слой — единственные ссылки стек → язык в каноне.
## 3. Подключение к репозиториям
Ничего ещё не подключено. Кандидаты — jellybit и pet-project-server.
Понадобится: заполнить локальную часть копий тем, что сейчас в этих
репозиториях записано по факту; обёртка в раннере (`inv conventions` /
`task conventions`, единый интерфейс команд у трёх ansible-репозиториев);
строка в `AGENTS.md` каждого потребителя про то, что файлы в
`docs/conventions/` — копии.
## 4. Описание языка отдельно от набора конвенций
`LANGUAGE.md` и `GUIDE.md` описывают, **как** пишутся конвенции;
`conventions/`**один конкретный** набор. Сейчас они склеены в одном
репозитории, и из одного описания нельзя собрать второй набор (рабочий,
доменный, чужой).
Срочности нет: версия языка объявлена в самом `LANGUAGE.md` (ключ
`version:`), и конвенции ссылаются на неё номером, а не путём, — то есть
самодостаточность копии выноса не требует.
Что осталось поводом:
- из одного описания по-прежнему нельзя собрать второй набор;
- тулинг валидирует правила, зашитые в его код, а не объявленную версию
языка.
Оба повода включаются, только когда появится второй набор. Цена — ещё одна
сущность и ещё одна синхронизация; при одном наборе лечение выходит хуже
болезни.
## 5. Тулинг на Go, живущий независимо
Сейчас `conv` — питоновский скрипт внутри канона, то есть тулинг и данные в
одном репозитории и правятся одним движением. Мысль: вынести в отдельный
Go-бинарь со своим релизным циклом, ставить через `eget` (механизм уже есть
в pet-project-server).
Что за этим стоит помимо вкуса: независимый бинарь физически не даёт править
инструмент «заодно» с правкой конвенции, работает против **любого** канона и
любого потребителя — что прямо требуется вопросом 4, — и снимает питон из
зависимостей репозиториев-потребителей.
Порядок обратный ожидаемому: пока вопрос 4 не сделан, инструмент всё равно
работает против одного конкретного канона, и независимый релизный цикл ему
нечего обслуживать. Сначала 4, потом 5. Разделение из вопроса 1 при этом
дешевле заложить сразу, чем отпиливать потом.
## 6. Одиннадцать таблиц не прочитаны на взаимоисключительность
`LANGUAGE.md` объявил, что строки таблицы решений взаимоисключающи по `LANGUAGE.md` объявил, что строки таблицы решений взаимоисключающи по
умолчанию, а иной порядок объявляется явно. Объявление не делает таблицы умолчанию, а иной порядок объявляется явно. Объявление не делает таблицы
@@ -122,9 +26,119 @@ Go-бинарь со своим релизным циклом, ставить ч
Работа читательская, машине не даётся; в список проверок она уже записана в Работа читательская, машине не даётся; в список проверок она уже записана в
разделе «Чтением, потому что машине не даётся». разделе «Чтением, потому что машине не даётся».
## Мелкое, не закрыто ## 2. Шесть сниппетов сидят в блоке нормы
- `conv check` и отличие ссылки на удалённое правило от упоминания дыры: С появлением блока ПРИМЕРЫ у кода в правиле есть своё место, но шесть правил
теперь освободившиеся номера перечислены в `GUIDE.md`, раздел несут сниппет **внутри блока нормы** — там, где он по границе правила читается
«Освободившиеся номера», — проверке остаётся читать этот список, а не как «требуется ровно такой код»: GCFG-7, GCFG-9, SLOG-20, GTIM-8, HTMX-7,
угадывать. Реализации по-прежнему нет. HTMX-24. Кода внутри обоснований в каноне нет ни одного, так что разбирать
нужно только эти шесть.
Разбор по одному, вердикт из двух: сниппет — часть требования или иллюстрация
к нему. У GTIM-8 (`ReplaceAttr` с приведением к UTC) это похоже на норму: там
важна конкретная точка вмешательства. У HTMX-7 и SLOG-20 — скорее иллюстрация
формы вызова, и ей место в ПРИМЕРЫ.
Цена ошибки в обе стороны понятна. Оставленный в норме пример превращает
деталь кода в требование, которое никто не имел в виду, и устаревает вместе с
API, а норму при этом нельзя поправить, не задев требование. Унесённая в
ПРИМЕРЫ норма, наоборот, перестаёт быть обязательной — блок иллюстративный.
## 3. Описание языка отдельно от набора конвенций
`LANGUAGE.md` и `GUIDE.md` описывают, **как** пишутся конвенции;
`conventions/`**один конкретный** набор. Сейчас они склеены в одном
репозитории, и из одного описания нельзя собрать второй набор (рабочий,
доменный, чужой).
Срочности нет: версия языка объявлена в самом `LANGUAGE.md` (ключ
`version:`), и конвенции ссылаются на неё номером, а не путём, — то есть
самодостаточность копии выноса не требует. Примеры в `LANGUAGE.md` вдобавок
переведены на вымышленные `X`-правила, так что на конкретный набор описание
языка больше не ссылается вовсе.
Что осталось поводом:
- из одного описания по-прежнему нельзя собрать второй набор;
- тулинг валидирует правила, зашитые в его код, а не объявленную версию
языка.
Оба повода включаются, только когда появится второй набор. Цена — ещё одна
сущность и ещё одна синхронизация; при одном наборе лечение выходит хуже
болезни.
# Канон и подключение
## 4. Значения осей нигде не зарегистрированы
Ось теперь объявлена в шапке (META-38), но её значения не сверяются ни с чем:
`lang: golang` вместо `lang: go` соберётся молча — слой просто не попадёт ни в
одну копию. Это ровно та болезнь, от которой лечили тему (META-28): объявление
без реестра проверяется только глазами.
Напрашивается секция в `.conventions-suite.toml` рядом с `[topics.live]` и
`[prefixes.live]` — перечень живых языков и стеков с однострочным описанием, и
те же правила выбытия. Против: третий реестр в манифесте, а значений сегодня
три (`go`, `ansible`, `htmx`). За: словарь общий у двух сторон — им же
потребитель пишет `lang` и `stack` в своём компоненте, и опечатка там стоит
столько же.
Заодно решается судьба `extends:`: с объявленной осью база находится сама —
это слой той же темы без ключей оси, — так что ключ остался подсказкой
человеку и кандидат на снятие.
## 5. Пары слоёв и темы без базы
Отложено сознательно, но список стоит держать перед глазами:
- `SLOG` объявляет `extends: arch/time.md` — расширение **чужой** темы.
Сборщик темы `logging` на это наткнётся: базового слоя с темой `logging`
нет, а `arch/time.md` он тянуть не должен. Чинится переводом в обычную
ссылку «связано». Вдобавок это ломает гарантию META-24: она верна только
для базы своей темы, а `arch/time.md` объявляет тему `time`, не `logging`.
С объявленной темой расхождение стало проверяемым машинно.
- Темы без арх-слоя: `db-schema`, `errors`, `web-ui`. Собираются в файл с
одной секцией — само по себе не ломается, но это и есть тот невыделенный
арх-слой из известного долга.
- Имена тем в паре не совпадают: `arch/db-identifiers.md` против
`lang/go/db-schema.md`. При сборке по имени темы это две разные темы —
проверить, что так и задумано.
- В `lang/go/db-schema.md` сидит целый пласт `stack/sqlite/` (типы колонок),
тоже из известного долга README.
- Вынос арх-ядра из `errors` и `logging` закроет две хрупкие ссылки из
`web-ui` (стек) в go-слой — единственные ссылки стек → язык в каноне.
## 6. Восемь тем не прогнаны по границе
Критерии границы записаны правилами (META-33 … META-37), но ни одна тема по
ним не прочитана. Работа по одной теме: выписать вопрос темы одной фразой и
пройти по правилам, помечая чужие.
Два подозреваемых видно уже сейчас.
`time` собрана вокруг вещества, а не решения (META-33): TIME-2 и TIME-3
(ширина и точность на носитель), TIME-6 (дефолтов в схеме БД нет), GTIM-4
(20 символов в БД), GTIM-8 и GTIM-9 (UTC и точность в логах) выносят вердикты
тем `db-schema` и `logging`. Остаток — представление момента, единая точка
«сейчас», длительность и часы, не-UTC на отображении — тема настоящая. Разрез
попутно снимает `extends: arch/time.md` из вопроса 5.
`logging` лежит целиком в `lang/go`, хотя внутри три страта: уровень по
адресату, «ошибка логируется один раз на границе», секреты — не про Go;
`JSONHandler`, `stdout`, разбор через `jq`/DuckDB — про стек; SLOG-30…33
(входящий запрос, healthcheck, 4xx) — про вид приложения (META-36), то есть
про веб-сервис. Здесь же лежит невыделенное арх-ядро из вопроса 5.
Отдельно проверить обратное: `errors` без арх-слоя — не дефект. Обработка
отказа в плейбуке (`failed_when`, `block`/`rescue`, идемпотентность) го-шные
правила не сужает, а решает другую задачу, значит для плейбуков это своя
тема, а не слой в `errors` (META-35).
## 7. Подключение к репозиториям
Ничего ещё не подключено. Кандидаты — jellybit и pet-project-server.
Понадобится: заполнить локальную часть копий тем, что сейчас в этих
репозиториях записано по факту; обёртка в раннере (`inv conventions` /
`task conventions`, единый интерфейс команд у трёх ansible-репозиториев);
строка в `AGENTS.md` каждого потребителя про то, что файлы в директориях
конвенций — копии. Компонент у обоих кандидатов один, но записывается явно.
-516
View File
@@ -1,516 +0,0 @@
#!/usr/bin/env python3
"""conv — синхронизация конвенций между каноном и репозиторием.
Канон — директория conventions/ рядом с этим скриптом. Репозиторий держит
закоммиченные копии нужных конвенций в docs/conventions/, повторяя её
структуру. Копия — источник правды для репозитория; канон — лавка, из
которой берут. Пути в origin даются относительно conventions/.
Служебная разметка копии:
---
origin: arch/time.md # откуда взято
origin_hash: a1b2c3d4 # отпечаток канона на момент синхронизации
synced: 2026-07-25
local: нет # или текст: чем и почему разошлись
---
Прочие ключи шапки (status, extends) — часть документа: они сравниваются
наравне с телом и приезжают из канона.
Локальные регионы — куски, которые по определению принадлежат репозиторию
(механизация, отступления, «здесь решили так»). Из сравнения исключаются:
<!-- local:механизировано -->
...
<!-- /local -->
Имя региона обязательно: перенос при pull идёт по именам.
Команды:
conv list что есть в каноне
conv add arch/time.md [...] взять конвенцию в репозиторий
conv status состояние копий репозитория
conv diff [arch/time.md] чем копия отличается от канона
conv pull arch/time.md забрать обновление канона
conv push arch/time.md вернуть локальное улучшение в канон
conv push --new lang/go/x.md завести в каноне новую конвенцию
Везде можно указать --repo <path> (по умолчанию — текущая директория)
и --dir <subpath> (по умолчанию docs/conventions), до или после команды.
"""
from __future__ import annotations
import argparse
import datetime
import difflib
import hashlib
import re
import sys
from pathlib import Path
from typing import NoReturn
CANON = Path(__file__).resolve().parent / "conventions"
CANON_TREES = ("arch", "lang", "stack")
SERVICE_KEYS = ("origin", "origin_hash", "synced", "local")
DEFAULT_DIR = "docs/conventions"
ENC = "utf-8"
# Маркеры распознаются только в начале строки: так пример разметки внутри
# текста конвенции не превращается в настоящий регион.
REGION_RE = re.compile(
r"^<!--[ \t]*local(?::[ \t]*([^>]*?))?[ \t]*-->(.*?)^<!--[ \t]*/local[ \t]*-->",
re.DOTALL | re.MULTILINE,
)
OPEN_RE = re.compile(r"^<!--[ \t]*local", re.MULTILINE)
CLOSE_RE = re.compile(r"^<!--[ \t]*/local", re.MULTILINE)
def die(message: str) -> NoReturn:
print(f"conv: {message}", file=sys.stderr)
sys.exit(1)
# --- разметка --------------------------------------------------------------
def read(path: Path) -> str:
return path.read_text(encoding=ENC)
def write(path: Path, text: str) -> None:
path.write_text(text, encoding=ENC)
def split_front(text: str) -> tuple[dict[str, str], str]:
"""Отделяет YAML-шапку (плоский key: value) от тела."""
if not text.startswith("---\n"):
return {}, text
end = text.find("\n---\n", 4)
if end == -1:
return {}, text
meta: dict[str, str] = {}
for line in text[4:end].splitlines():
if ":" in line:
key, value = line.split(":", 1)
meta[key.strip()] = value.strip()
return meta, text[end + 5 :]
def join_front(meta: dict[str, str], body: str) -> str:
if not meta:
return body
lines = "\n".join(f"{k}:{' ' + v if v else ''}" for k, v in meta.items())
return f"---\n{lines}\n---\n{body}"
def doc_keys(meta: dict[str, str]) -> dict[str, str]:
return {k: v for k, v in meta.items() if k not in SERVICE_KEYS}
def regions(body: str) -> dict[str, str]:
"""Содержимое локальных регионов по имени.
Поднимает ValueError на разметке, из-за которой регион молча превратился
бы в обычный текст и потерялся при pull.
"""
matched = len(REGION_RE.findall(body))
if len(OPEN_RE.findall(body)) != matched or len(CLOSE_RE.findall(body)) != matched:
raise ValueError("непарный или нераспознанный маркер локального региона")
found: dict[str, str] = {}
for match in REGION_RE.finditer(body):
name = (match.group(1) or "").strip()
content = match.group(2)
if not name:
if content.strip():
raise ValueError(
"безымянный локальный регион с содержимым — дай ему имя"
)
continue
if name in found:
raise ValueError(f"локальный регион '{name}' встречается дважды")
found[name] = content
return found
def checked_regions(body: str, where: str) -> dict[str, str]:
try:
return regions(body)
except ValueError as exc:
die(f"{where}: {exc}")
def blank_regions(body: str) -> str:
"""Тело с опустошёнными локальными регионами — то, что сравнивается."""
def repl(match: re.Match[str]) -> str:
raw = (match.group(1) or "").strip()
head = f"<!-- local:{raw} -->" if raw else "<!-- local -->"
return f"{head}\n<!-- /local -->"
return REGION_RE.sub(repl, body)
def fill_regions(body: str, values: dict[str, str]) -> tuple[str, list[str]]:
"""Вставляет содержимое регионов по имени. Возвращает тело и имена,
которым не нашлось места."""
used: set[str] = set()
def repl(match: re.Match[str]) -> str:
raw = (match.group(1) or "").strip()
head = f"<!-- local:{raw} -->" if raw else "<!-- local -->"
if raw in values:
used.add(raw)
return f"{head}{values[raw]}<!-- /local -->"
return match.group(0)
filled = REGION_RE.sub(repl, body)
lost = [n for n, v in values.items() if n not in used and v.strip()]
return filled, lost
def fingerprint(meta: dict[str, str], body: str) -> str:
"""Отпечаток документа: ключи шапки плюс тело без локальных регионов."""
head = "\n".join(f"{k}={v}" for k, v in sorted(doc_keys(meta).items()))
return hashlib.sha256(f"{head}\n\n{blank_regions(body)}".encode(ENC)).hexdigest()[
:8
]
def today() -> str:
return datetime.date.today().isoformat()
# --- канон и репозиторий ---------------------------------------------------
def canon_list() -> list[str]:
out: list[str] = []
for tree in CANON_TREES:
root = CANON / tree
if root.is_dir():
out += [str(p.relative_to(CANON)) for p in sorted(root.rglob("*.md"))]
return out
def canon_read(origin: str) -> tuple[dict[str, str], str]:
path = CANON / origin
if not path.is_file():
die(f"в каноне нет {origin}")
return split_front(read(path))
def normalize_origin(name: str, *, must_exist: bool = True) -> str:
"""Принимает 'arch/time.md', 'arch/time' и однозначный хвост вроде 'time'."""
name = name.strip("/")
if not name.endswith(".md"):
name += ".md"
candidate = (CANON / name).resolve()
if candidate.is_relative_to(CANON):
rel = str(candidate.relative_to(CANON))
if rel.split("/")[0] in CANON_TREES and (not must_exist or candidate.is_file()):
return rel
matches = [c for c in canon_list() if c == name or c.endswith("/" + name)]
if len(matches) == 1:
return matches[0]
if not matches:
die(f"в каноне нет {name} (путь должен начинаться с {'/'.join(CANON_TREES)})")
die(f"неоднозначно: {name} → {', '.join(matches)}")
def repo_dir(args: argparse.Namespace) -> Path:
return (Path(str(args.repo)) / str(args.dir)).resolve()
def repo_copies(base: Path) -> tuple[dict[str, Path], list[Path], list[str]]:
"""origin → копия; плюс .md без шапки и сообщения о нечитаемых файлах."""
found: dict[str, Path] = {}
untracked: list[Path] = []
problems: list[str] = []
if not base.is_dir():
return found, untracked, problems
for path in sorted(base.rglob("*.md")):
try:
meta, _ = split_front(read(path))
except (OSError, UnicodeDecodeError) as exc:
problems.append(f"{path.name}: не читается ({type(exc).__name__})")
continue
origin = meta.get("origin")
if not origin:
if path.name != "README.md":
untracked.append(path)
continue
if origin in found:
problems.append(
f"{origin}: две копии ({found[origin]}, {path}) — вторая скрыта"
)
continue
found[origin] = path
return found, untracked, problems
def locate(args: argparse.Namespace, origin: str) -> Path:
"""Путь копии: по шапке, если она лежит не по канонному пути."""
base = repo_dir(args)
copies, _, _ = repo_copies(base)
return copies.get(origin, base / origin)
# --- состояние -------------------------------------------------------------
def state(
meta: dict[str, str], body: str, canon_meta: dict[str, str], canon_body: str
) -> str:
copy_fp = fingerprint(meta, body)
canon_fp = fingerprint(canon_meta, canon_body)
if copy_fp == canon_fp:
return "ok"
base = meta.get("origin_hash")
if not base:
return "нет origin_hash в шапке"
if base == canon_fp:
return "изменено локально"
if base == copy_fp:
return "канон обновился"
return "разошлись"
# --- команды ---------------------------------------------------------------
def cmd_list(args: argparse.Namespace) -> int:
for origin in canon_list():
meta, _ = canon_read(origin)
marks = []
if "extends" in meta:
marks.append(f"расширяет {meta['extends']}")
if "status" in meta:
marks.append(meta["status"])
tail = f" ({'; '.join(marks)})" if marks else ""
print(f"{origin}{tail}")
return 0
def cmd_add(args: argparse.Namespace) -> int:
base = repo_dir(args)
added = False
for raw in args.names:
origin = normalize_origin(raw)
target = base / origin
if target.exists():
print(f"{origin}: уже есть ({target}), пропускаю")
continue
canon_meta, canon_body = canon_read(origin)
checked_regions(canon_body, f"канон/{origin}")
meta: dict[str, str] = {
"origin": origin,
"origin_hash": fingerprint(canon_meta, canon_body),
"synced": today(),
"local": "нет",
}
meta.update(doc_keys(canon_meta))
target.parent.mkdir(parents=True, exist_ok=True)
write(target, join_front(meta, canon_body))
added = True
print(f"{origin} → {target}")
if "extends" in canon_meta:
print(f" расширяет {canon_meta['extends']} — возможно, нужна и она")
if added:
print("не забудь строку в docs/conventions/README.md")
return 0
def cmd_status(args: argparse.Namespace) -> int:
base = repo_dir(args)
copies, untracked, problems = repo_copies(base)
if not copies and not untracked and not problems:
print(f"в {base} нет копий конвенций")
return 0
width = max((len(o) for o in copies), default=0)
for origin, path in copies.items():
try:
meta, body = split_front(read(path))
except (OSError, UnicodeDecodeError) as exc:
print(f"{origin:<{width}} не читается ({type(exc).__name__})")
continue
if not (CANON / origin).is_file():
print(f"{origin:<{width}} нет в каноне")
continue
canon_meta, canon_body = canon_read(origin)
try:
regions(body)
except ValueError as exc:
print(f"{origin:<{width}} разметка: {exc}")
continue
local = meta.get("local", "нет")
note = "" if local == "нет" else f" [{local}]"
print(f"{origin:<{width}} {state(meta, body, canon_meta, canon_body)}{note}")
for path in untracked:
print(f"{path.name}: без шапки origin — не отслеживается")
for problem in problems:
print(problem)
return 0
def cmd_diff(args: argparse.Namespace) -> int:
base = repo_dir(args)
copies, _, _ = repo_copies(base)
targets = [normalize_origin(args.name)] if args.name else list(copies)
for origin in targets:
path = copies.get(origin)
if path is None:
print(f"{origin}: нет копии в репозитории")
continue
if not (CANON / origin).is_file():
print(f"{origin}: нет в каноне")
continue
meta, body = split_front(read(path))
canon_meta, canon_body = canon_read(origin)
if fingerprint(meta, body) == fingerprint(canon_meta, canon_body):
continue
sys.stdout.writelines(
difflib.unified_diff(
join_front(doc_keys(canon_meta), blank_regions(canon_body)).splitlines(
keepends=True
),
join_front(doc_keys(meta), blank_regions(body)).splitlines(
keepends=True
),
fromfile=f"канон/{origin}",
tofile=f"репо/{origin}",
)
)
return 0
def cmd_pull(args: argparse.Namespace) -> int:
origin = normalize_origin(args.name)
path = locate(args, origin)
if not path.is_file():
die(f"нет копии {origin} — сначала conv add {origin}")
meta, body = split_front(read(path))
canon_meta, canon_body = canon_read(origin)
checked_regions(canon_body, f"канон/{origin}")
local = checked_regions(body, f"репо/{origin}")
st = state(meta, body, canon_meta, canon_body)
if st == "ok":
fresh = fingerprint(canon_meta, canon_body)
if meta.get("origin_hash") != fresh:
meta["origin_hash"] = fresh
meta["synced"] = today()
write(path, join_front(meta, body))
print(f"{origin}: тексты совпадают, отпечаток освежён")
else:
print(f"{origin}: уже совпадает")
return 0
if st in ("изменено локально", "разошлись") and not args.force:
die(
f"{origin}: {st} — правки вне локальных регионов будут потеряны.\n"
f" посмотри conv diff {origin}, затем conv pull --force "
f"или conv push {origin}"
)
merged, lost = fill_regions(canon_body, local)
if lost and not args.force:
die(
f"{origin}: в каноне нет регионов {', '.join(lost)} — их содержимое "
f"пропадёт.\n перенеси вручную или conv pull --force"
)
for name in lost:
print(f" потерян локальный регион {name}")
new_meta = {k: meta[k] for k in SERVICE_KEYS if k in meta}
new_meta["origin_hash"] = fingerprint(canon_meta, canon_body)
new_meta["synced"] = today()
new_meta.update(doc_keys(canon_meta))
write(path, join_front(new_meta, merged))
print(f"{origin}: обновлено из канона — перечитай глазами, регионы могли устареть")
return 0
def cmd_push(args: argparse.Namespace) -> int:
origin = normalize_origin(args.name, must_exist=not args.new)
path = locate(args, origin)
if not path.is_file():
die(f"нет копии {origin}")
meta, body = split_front(read(path))
checked_regions(body, f"репо/{origin}")
target = CANON / origin
if not target.is_file():
if not args.new:
die(f"в каноне нет {origin} — заведи новую конвенцию через conv push --new")
target.parent.mkdir(parents=True, exist_ok=True)
write(target, join_front(doc_keys(meta), blank_regions(body)))
meta["origin_hash"] = fingerprint(doc_keys(meta), blank_regions(body))
meta["synced"] = today()
write(path, join_front(meta, body))
print(f"{origin}: заведена в каноне")
return 0
canon_meta, canon_body = canon_read(origin)
st = state(meta, body, canon_meta, canon_body)
if st == "ok":
print(f"{origin}: канон уже такой")
return 0
if st == "канон обновился":
die(
f"{origin}: копия не менялась, а канон ушёл вперёд — пушить нечего, нужен pull"
)
if st == "разошлись" and not args.force:
die(
f"{origin}: разошлись — канон менялся после синхронизации, "
f"его правки затрутся.\n посмотри conv diff {origin}, "
f"затем conv push --force"
)
write(target, join_front(doc_keys(meta), blank_regions(body)))
meta["origin_hash"] = fingerprint(doc_keys(meta), blank_regions(body))
meta["synced"] = today()
write(path, join_front(meta, body))
print(f"{origin}: канон обновлён из репозитория")
return 0
def main() -> int:
common = argparse.ArgumentParser(add_help=False)
common.add_argument("--repo", default=".", help="корень репозитория")
common.add_argument("--dir", default=DEFAULT_DIR, help="где лежат конвенции")
parser = argparse.ArgumentParser(prog="conv", parents=[common], description=__doc__)
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("list", parents=[common], help="что есть в каноне").set_defaults(
fn=cmd_list
)
p_add = sub.add_parser(
"add", parents=[common], help="взять конвенцию в репозиторий"
)
p_add.add_argument("names", nargs="+")
p_add.set_defaults(fn=cmd_add)
sub.add_parser("status", parents=[common], help="состояние копий").set_defaults(
fn=cmd_status
)
p_diff = sub.add_parser(
"diff", parents=[common], help="чем копия отличается от канона"
)
p_diff.add_argument("name", nargs="?")
p_diff.set_defaults(fn=cmd_diff)
p_pull = sub.add_parser("pull", parents=[common], help="забрать обновление канона")
p_pull.add_argument("name")
p_pull.add_argument("--force", action="store_true")
p_pull.set_defaults(fn=cmd_pull)
p_push = sub.add_parser("push", parents=[common], help="вернуть улучшение в канон")
p_push.add_argument("name")
p_push.add_argument("--force", action="store_true")
p_push.add_argument("--new", action="store_true", help="завести новый файл канона")
p_push.set_defaults(fn=cmd_push)
args = parser.parse_args()
return int(args.fn(args))
if __name__ == "__main__":
sys.exit(main())
+3 -2
View File
@@ -1,4 +1,5 @@
--- ---
topic: app-directories
prefix: DIRS prefix: DIRS
--- ---
@@ -11,8 +12,8 @@ prefix: DIRS
механически выводится состав бэкапа. механически выводится состав бэкапа.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
## Область действия ## Область действия
+3 -2
View File
@@ -1,4 +1,5 @@
--- ---
topic: config
prefix: CONF prefix: CONF
--- ---
@@ -8,8 +9,8 @@ prefix: CONF
секретами и когда падает. секретами и когда падает.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
## Область действия ## Область действия
+3 -2
View File
@@ -1,4 +1,5 @@
--- ---
topic: db-identifiers
prefix: KEYS prefix: KEYS
--- ---
@@ -7,8 +8,8 @@ prefix: KEYS
Как выбираются и как выглядят первичные ключи сущностей. Как выбираются и как выглядят первичные ключи сущностей.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
## Область действия ## Область действия
+3 -2
View File
@@ -1,4 +1,5 @@
--- ---
topic: time
prefix: TIME prefix: TIME
--- ---
@@ -8,8 +9,8 @@ prefix: TIME
берётся значение и где появляется не-UTC. берётся значение и где появляется не-UTC.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
## Область действия ## Область действия
+4 -2
View File
@@ -1,5 +1,7 @@
--- ---
topic: config
prefix: GCFG prefix: GCFG
lang: go
extends: arch/config.md extends: arch/config.md
--- ---
@@ -9,8 +11,8 @@ extends: arch/config.md
запрета на окружение. запрета на окружение.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Секретов Go-специфика не касается: они приходят из деплоя уже в файле, а Секретов Go-специфика не касается: они приходят из деплоя уже в файле, а
проверка их непустоты идёт вместе с остальной валидацией — как описано в проверка их непустоты идёт вместе с остальной валидацией — как описано в
+4 -2
View File
@@ -1,5 +1,7 @@
--- ---
topic: db-identifiers
prefix: GKEY prefix: GKEY
lang: go
extends: arch/db-identifiers.md extends: arch/db-identifiers.md
--- ---
@@ -8,8 +10,8 @@ extends: arch/db-identifiers.md
Как базовый слой выглядит в Go-приложении. Как базовый слой выглядит в Go-приложении.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Единая точка из `KEYS-3` — пакет `internal/ident`: он Единая точка из `KEYS-3` — пакет `internal/ident`: он
порождает идентификаторы (`NewID`, `NewIDAt`) и он же их разбирает порождает идентификаторы (`NewID`, `NewIDAt`) и он же их разбирает
+4 -2
View File
@@ -1,5 +1,7 @@
--- ---
topic: db-schema
prefix: MIGR prefix: MIGR
lang: go
--- ---
# Схема и миграции (SQLite, Go) # Схема и миграции (SQLite, Go)
@@ -8,8 +10,8 @@ prefix: MIGR
Go-приложении. Go-приложении.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
## Область действия ## Область действия
+15 -10
View File
@@ -1,5 +1,7 @@
--- ---
topic: errors
prefix: GERR prefix: GERR
lang: go
--- ---
# Ошибки # Ошибки
@@ -9,8 +11,8 @@ prefix: GERR
границе). границе).
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Две границы, о которых говорят правила ниже: Две границы, о которых говорят правила ниже:
@@ -344,8 +346,9 @@ HTTP-клиентов, файловой системы, внешних SDK.
| № | Где перехвачена паника | Что дальше | | № | Где перехвачена паника | Что дальше |
|---|---|---| |---|---|---|
| GERR-26.1 | обработчик HTTP-запроса | ответ 500, если он ещё не начат; процесс и прочие запросы не затрагиваются | | GERR-26.1 | обработчик HTTP-запроса, паника любая, кроме сигнала намеренного прерывания | ответ 500, если он ещё не начат; процесс и прочие запросы не затрагиваются |
| GERR-26.2 | итерация цикла обработки — бот, воркер | цикл продолжается со следующего элемента, упавший элемент повторно не берётся | | GERR-26.2 | итерация цикла обработки — бот, воркер | цикл продолжается со следующего элемента, упавший элемент повторно не берётся |
| GERR-26.3 | обработчик HTTP-запроса, паника — сигнал намеренного прерывания (`http.ErrAbortHandler`) | значение пробрасывается дальше, ответ не подменяется |
**ПОЧЕМУ.** Паника внутри обработки одного элемента почти всегда говорит о **ПОЧЕМУ.** Паника внутри обработки одного элемента почти всегда говорит о
баге в работе с данными этого элемента, а не о порче общего состояния, — баге в работе с данными этого элемента, а не о порче общего состояния, —
@@ -356,10 +359,11 @@ HTTP-клиентов, файловой системы, внешних SDK.
обязательным (GERR-22): неперехваченная паника в любой горутине завершает весь обязательным (GERR-22): неперехваченная паника в любой горутине завершает весь
процесс. процесс.
Продолжать, не исключив упавший элемент, нельзя: детерминированная паника Исключение упавшего элемента в GERR-26.2 — не осторожность, а условие
даёт бесконечный цикл — тот же элемент, тот же стек, залитый лог и нулевой прогресса: детерминированная паника даёт бесконечный цикл — тот же элемент,
прогресс. Это классический poison message, и лекарство берём то же, что тот же стек, залитый лог и нулевой прогресс. Это классический poison message,
принято в очередях: элемент выводится из оборота, а не берётся снова. У и лекарство здесь то же, что принято в очередях: элемент выводится из
оборота, а не берётся снова. У
цикла, который и так подтверждает прогресс — сдвигает офсет, помечает цикла, который и так подтверждает прогресс — сдвигает офсет, помечает
строку состоянием, — механизм для этого уже есть, заводить отдельный не строку состоянием, — механизм для этого уже есть, заводить отдельный не
нужно. нужно.
@@ -369,9 +373,10 @@ HTTP-клиентов, файловой системы, внешних SDK.
клиент получит обрывок с кодом 200. Отсюда же общее предпочтение собирать клиент получит обрывок с кодом 200. Отсюда же общее предпочтение собирать
ответ целиком до записи там, где это возможно. ответ целиком до записи там, где это возможно.
Из GERR-26.1 есть одно исключение: `http.ErrAbortHandler`сигнал «прервать Отдельная строка GERR-26.3 нужна потому, что `http.ErrAbortHandler`не
обработку намеренно», и recover-обёртка пробрасывает его дальше, а не отказ, а сигнал «прервать обработку намеренно»: подмена его на 500 превратила
превращает в 500. Так поступают и стандартные обёртки вроде chi. бы штатный разрыв в ложную ошибку в логе и в метриках. Так поступают и
стандартные обёртки вроде chi.
### GERR-24. Независимые ошибки собираются `errors.Join` ### GERR-24. Независимые ошибки собираются `errors.Join`
+10 -6
View File
@@ -1,6 +1,7 @@
--- ---
topic: logging
prefix: SLOG prefix: SLOG
extends: arch/time.md lang: go
--- ---
# Логирование # Логирование
@@ -10,8 +11,8 @@ extends: arch/time.md
функциональности, живут в спеках. функциональности, живут в спеках.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Лог читают инструментами, а не глазами: повседневно — `jq` Лог читают инструментами, а не глазами: повседневно — `jq`
(`jq 'select(.download_id=="a1b2")' app.jsonl`), тяжёлое (агрегации, JOIN) — (`jq 'select(.download_id=="a1b2")' app.jsonl`), тяжёлое (агрегации, JOIN) —
@@ -337,6 +338,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
| SLOG-25.1 | штатный конфликт состояния или некорректный ввод | пользователю, он уже получил ответ | `DEBUG` | | SLOG-25.1 | штатный конфликт состояния или некорректный ввод | пользователю, он уже получил ответ | `DEBUG` |
| SLOG-25.2 | расхождение производного или учётного состояния, первичные данные целы | владельцу, «может стать проблемой» | `WARN` | | SLOG-25.2 | расхождение производного или учётного состояния, первичные данные целы | владельцу, «может стать проблемой» | `WARN` |
| SLOG-25.3 | сбой БД, ФС, недоступность зависимости | владельцу, в разбор | `ERROR` | | SLOG-25.3 | сбой БД, ФС, недоступность зависимости | владельцу, в разбор | `ERROR` |
| SLOG-25.4 | класса нет: отказ в классификацию не заведён | владельцу, как пропуск в классификации | `ERROR` с отметкой о непокрытом классе |
**ПОЧЕМУ.** Это применение SLOG-8 к отказам: пользователь уже увидел причину на **ПОЧЕМУ.** Это применение SLOG-8 к отказам: пользователь уже увидел причину на
экране — владельцу разбирать нечего; целостность первичных данных отделяет экране — владельцу разбирать нечего; целостность первичных данных отделяет
@@ -349,9 +351,11 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
таблицу не входит: это не доменный отказ, и логирует его recover-граница таблицу не входит: это не доменный отказ, и логирует его recover-граница
вместе со стеком (конвенция `errors`). Искать его класс здесь не нужно. вместе со стеком (конвенция `errors`). Искать его класс здесь не нужно.
Мимо таблицы идёт и доменная ошибка, которой нет в маппинге: класса у неё Строка SLOG-25.4 говорит не о классе отказа, а о пропуске в самой
нет, потому что её просто забыли завести. Она логируется `ERROR` с классификации: ошибку забыли завести в маппинге. `ERROR` здесь — громкость,
признаком непокрытой (`GERR-25`). по которой пропуск находят фильтром, а не оценка тяжести отказа; саму отметку
о непокрытом классе ставит трансляция ошибки (`GERR-25` в конвенции
`errors`).
### SLOG-26. Тот же отказ в асинхронной стадии — уровнем выше ### SLOG-26. Тот же отказ в асинхронной стадии — уровнем выше
+6 -4
View File
@@ -1,5 +1,7 @@
--- ---
topic: time
prefix: GTIM prefix: GTIM
lang: go
extends: arch/time.md extends: arch/time.md
--- ---
@@ -9,8 +11,8 @@ extends: arch/time.md
в каком виде время попадает в базу и в логи, что делать с зонами. в каком виде время попадает в базу и в логи, что делать с зонами.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
## Правила ## Правила
@@ -180,8 +182,8 @@ func utcTime(_ []string, a slog.Attr) slog.Attr {
текущего значения настройки: смена зоны задним числом сдвигает границы текущего значения настройки: смена зоны задним числом сдвигает границы
суток у того, что давно посчитано и сохранено. суток у того, что давно посчитано и сохранено.
Календарные вычисления бизнес-логики берут зону явно — как описано в Явную зону в календарных вычислениях требует TIME-12 — это его правило, а не
базовом слое. второе такое же здесь.
## Связано ## Связано
+4 -2
View File
@@ -1,5 +1,7 @@
--- ---
topic: app-directories
prefix: ANSD prefix: ANSD
stack: ansible
extends: arch/app-directories.md extends: arch/app-directories.md
--- ---
@@ -8,8 +10,8 @@ extends: arch/app-directories.md
Как категории из базового слоя раскладываются на сервере плейбуком. Как категории из базового слоя раскладываются на сервере плейбуком.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
## Область действия ## Область действия
+4 -2
View File
@@ -1,5 +1,7 @@
--- ---
topic: web-ui
prefix: HTMX prefix: HTMX
stack: htmx
--- ---
# Веб-UI на htmx # Веб-UI на htmx
@@ -9,8 +11,8 @@ prefix: HTMX
какие действия поддерживает — в спеках, не здесь. какие действия поддерживает — в спеках, не здесь.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
тогда и только тогда, когда написаны заглавными. конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
Логирование запросов — конвенция `logging` (HTTP-поля, рутинно-частое на Логирование запросов — конвенция `logging` (HTTP-поля, рутинно-частое на
`DEBUG`). Трансляция доменных ошибок наружу — конвенция `errors` `DEBUG`). Трансляция доменных ошибок наружу — конвенция `errors`
-49
View File
@@ -1,49 +0,0 @@
# Реестр префиксов правил.
#
# Префикс — четыре заглавные латинские буквы, уникальные по всему канону.
# Он выбирается под файл, а не выводится по формуле: префикс нужен, чтобы
# по нему искать, а не чтобы его разбирать. Поэтому подходящее слово лучше
# закономерности.
#
# Правила реестра:
#
# - префикс не переименовывается и не переиспользуется никогда — ссылка
# из чужого репозитория обязана продолжать указывать на то же место;
# - при удалении или разделении файла префикс уходит в [retired], а не
# освобождается;
# - переезд файла между осями префикс не меняет: идентификатор правила
# не зависит от таксономии;
# - вынос части правил в новый файл — это новый префикс и новая
# нумерация: перенос правила между документами есть смысловое
# изменение, а не переименование;
# - тот же префикс продублирован в шапке файла (`prefix:`), conv сверяет.
#
# Пути даются от корня репозитория, а не от `conventions/`: реестр покрывает
# и обвязку тоже.
#
# Буква `X` в начале префикса зарезервирована за репозиториями-потребителями:
# канон её не занимает никогда, локальные правила берут префиксы только на
# неё (XTIM, XLOG). Согласовывать их с этим реестром не нужно — столкновение
# невозможно по построению.
[live]
DIRS = "conventions/arch/app-directories.md"
CONF = "conventions/arch/config.md"
KEYS = "conventions/arch/db-identifiers.md"
TIME = "conventions/arch/time.md"
GCFG = "conventions/lang/go/config.md"
GKEY = "conventions/lang/go/db-identifiers.md"
MIGR = "conventions/lang/go/db-schema.md"
GERR = "conventions/lang/go/errors.md"
SLOG = "conventions/lang/go/logging.md"
GTIM = "conventions/lang/go/time.md"
ANSD = "conventions/stack/ansible/app-directories.md"
HTMX = "conventions/stack/htmx/web-ui.md"
# Обвязка канона: не синхронизируется в репозитории, но правила
# записаны тем же языком и цитируются по номерам, поэтому префикс нужен.
META = "GUIDE.md"
[retired]
# Пусто. Сюда попадают префиксы удалённых и разделённых файлов вместе с
# причиной и датой, чтобы их нельзя было выдать повторно.