исправлены дефекты формулировок в конвенциях

- убраны неверные утверждения: покрытие forbidigo сужено до честного,
  таблица классов доменного отказа больше не претендует на полноту,
  механизация не подаётся как факт канона
- введены недостающие определения (доменная и внешняя границы, объявление
  пути), критерий постоянного поля сведён к одному на R16.1 и R17
- kebab-case имени файла убран из правил в прозу: обоснование не
  формулировалось, номер R16 оставлен свободным
This commit is contained in:
av
2026-07-25 19:34:41 +03:00
parent 0842850fae
commit 6456b81d91
6 changed files with 55 additions and 19 deletions
+18 -10
View File
@@ -26,6 +26,15 @@
- `docs/conventions/`**правило на будущее**, применяемое многократно. - `docs/conventions/`**правило на будущее**, применяемое многократно.
Живой документ: правится, когда договорённость меняется. Живой документ: правится, когда договорённость меняется.
## Оформление
Имя файла — kebab-case по теме: `app-directories.md`. Правилом это не
записано: обоснование сводится к «чтобы адрес правила
(`stack/ansible/app-directories.md R4`) писался одним способом», а
проверить нарушение всё равно проще глазом, чем сформулировать норму. Номер
R16, под которым это правило существовало, оставлен свободным и не
переиспользуется.
## Канон и копии ## Канон и копии
Файлы в `docs/conventions/` с шапкой `origin:` — копии из общего канона Файлы в `docs/conventions/` с шапкой `origin:` — копии из общего канона
@@ -106,6 +115,10 @@
это честно, а ДОЛЖЕН при этом обещает то, чего не делает, и через несколько это честно, а ДОЛЖЕН при этом обещает то, чего не делает, и через несколько
таких случаев обесценивает остальные ДОЛЖЕН в файле. таких случаев обесценивает остальные ДОЛЖЕН в файле.
Отсюда следствие: правило, машинная проверка которого невозможна в принципе
(вкус формулировки, выбор границы, суждение о ситуации), не может быть
ДОЛЖЕН — его модальность СЛЕДУЕТ по построению, а не по слабости.
### R7. Факт механизации фиксируется в локальном регионе со ссылкой на номер ### R7. Факт механизации фиксируется в локальном регионе со ссылкой на номер
**ДОЛЖЕН.** Регион `механизировано` называет номер правила и конкретную **ДОЛЖЕН.** Регион `механизировано` называет номер правила и конкретную
@@ -127,6 +140,11 @@
говорит о прочих, поэтому удалять по факту «у нас уже проверяется» — говорит о прочих, поэтому удалять по факту «у нас уже проверяется» —
значит чинить свой файл за чужой счёт. значит чинить свой файл за чужой счёт.
Списка подписчиков канон по построению не знает, поэтому факт «механизировано
у всех» устанавливается обходом репозиториев вручную — это часть работы по
удалению нормы, а не то, что можно проверить машиной (см. `LANGUAGE.md`,
состояние МЕХАНИЗИРОВАНО).
### R9. Общая механизация разрешает удалить норму из канона ### R9. Общая механизация разрешает удалить норму из канона
**ДОПУСКАЕТСЯ.** Правило, уехавшее в общий конфиг линтера или в общую роль, **ДОПУСКАЕТСЯ.** Правило, уехавшее в общий конфиг линтера или в общую роль,
@@ -205,16 +223,6 @@
правила в каноне, которую чинят один раз для всех, или лишнюю подписку, правила в каноне, которую чинят один раз для всех, или лишнюю подписку,
где файл просто не нужен. Оставленная отступлением, она прячет обе. где файл просто не нужен. Оставленная отступлением, она прячет обе.
### R16. Имя файла — kebab-case по теме
**СЛЕДУЕТ.** `app-directories.md`, а не вариации регистра и разделителя.
**Почему.** Имя файла — часть глобального адреса правила
(`stack/ansible/app-directories.md R4`) и значение ключа `origin` в каждой
копии. Один способ записи избавляет от нескольких написаний одного адреса,
а ошибка в адресе обнаруживается только тем, кто по нему пришёл и ничего не
нашёл.
### R17. Репо-специфичная часть «Связано» — в локальном регионе ### R17. Репо-специфичная часть «Связано» — в локальном регионе
**ДОЛЖЕН.** Раздел «Связано» стоит в конце файла; канонические ссылки — в **ДОЛЖЕН.** Раздел «Связано» стоит в конце файла; канонические ссылки — в
+5 -1
View File
@@ -82,9 +82,13 @@
### R5. Список бэкапа ссылается на те же пути, что и создание директорий ### R5. Список бэкапа ссылается на те же пути, что и создание директорий
**ДОЛЖЕН.** Список выводится из категорий по R4 и ссылается на те же **ДОЛЖЕН.** Список выводится из категорий по R4 и ссылается на те же
объявления путей, по которым директории создаются, а не набирается **объявления путей**, по которым директории создаются, а не набирается
независимо. независимо.
Объявление пути — то единственное место, где путь директории записан
буквально: переменная деплоя, константа, поле конфигурации. Всё остальное
на него ссылается.
**Почему.** Правило вывода механическое, но применяет его человек или **Почему.** Правило вывода механическое, но применяет его человек или
шаблон — то есть ошибиться можно. Общая ссылка делает целый класс ошибок шаблон — то есть ошибиться можно. Общая ссылка делает целый класс ошибок
невозможным: переименование директории отражается в обоих местах сразу. невозможным: переименование директории отражается в обоих местах сразу.
+7 -1
View File
@@ -120,7 +120,7 @@ func (d Duration) Std() time.Duration { … }
чтением всего кода — а узнают о нём обычно на сервере, где переменная не чтением всего кода — а узнают о нём обычно на сервере, где переменная не
выставлена. выставлена.
### R9. Проверка запрета покрывает все входы в окружение ### R9. Проверка запрета покрывает всю семью `os`
**ДОЛЖЕН.** Механическая проверка R8 (`forbidigo`) ловит не только **ДОЛЖЕН.** Механическая проверка R8 (`forbidigo`) ловит не только
`os.Getenv`: `os.Getenv`:
@@ -134,6 +134,12 @@ func (d Duration) Std() time.Duration { … }
незаметно: правило числится механизированным, и глазами его больше никто не незаметно: правило числится механизированным, и глазами его больше никто не
проверяет. проверяет.
Полного покрытия этот паттерн не даёт и дать не может: мимо него проходят
`syscall.Getenv`, вызов через алиас пакета и чтение `/proc/self/environ`.
Проверка закрывает обычные способы — те, которыми окружение читают не
нарочно; сознательный обход она не ловит, и считать R8 полностью
механизированным нельзя.
### R10. За границей приложения запрет не действует ### R10. За границей приложения запрет не действует
**ДОПУСКАЕТСЯ.** Чтение окружения там, где читающий — не конфигурируемое **ДОПУСКАЕТСЯ.** Чтение окружения там, где читающий — не конфигурируемое
+11
View File
@@ -4,6 +4,17 @@
`LANGUAGE.md`. Где и когда ошибку **логировать** — в `LANGUAGE.md`. Где и когда ошибку **логировать** — в
`lang/go/logging.md` (коротко: лог один раз на доменной границе). `lang/go/logging.md` (коротко: лог один раз на доменной границе).
Две границы, о которых говорят правила ниже:
- **доменная граница** — место, где определяется исход операции: use-case,
публичная команда воркера, стадия асинхронной обработки. Ниже неё ошибка
только накапливает контекст, выше — операция уже либо удалась, либо нет.
- **внешняя граница** — место, где ответ покидает процесс: обработчик HTTP,
рендер страницы, отправка сообщения ботом.
Одна операция проходит обе: сначала доменную (там её исход логируется),
потом внешнюю (там он превращается в ответ).
## Правила ## Правила
### R1. Ошибки строятся средствами стандартной библиотеки ### R1. Ошибки строятся средствами стандартной библиотеки
+12 -5
View File
@@ -209,7 +209,7 @@ dev-выводом перестаёшь ежедневно гонять собс
| № | Когда добавляем | Поля | | № | Когда добавляем | Поля |
|---|---|---| |---|---|---|
| R16.1 | входящий HTTP-запрос (middleware) | `http.method`, `http.route`, `http.status_code`, `duration_ms`, `transport`если транспортов больше одного | | R16.1 | входящий HTTP-запрос (middleware) | `http.method`, `http.route`, `http.status_code`, `duration_ms`, `transport`пока его значение различается между записями (R17) |
| R16.2 | работа с сущностью (scoped-логгер) | `<entity>_id` и доменные атрибуты | | R16.2 | работа с сущностью (scoped-логгер) | `<entity>_id` и доменные атрибуты |
| R16.3 | запись об ошибке | `error` | | R16.3 | запись об ошибке | `error` |
| R16.4 | вызов внешнего сервиса | `ext.service`, `ext.operation` (логическая операция, не URL), `ext.status_code`, `duration_ms`, `retry` | | R16.4 | вызов внешнего сервиса | `ext.service`, `ext.operation` (логическая операция, не URL), `ext.status_code`, `duration_ms`, `retry` |
@@ -223,10 +223,12 @@ dev-выводом перестаёшь ежедневно гонять собс
### R17. `service.*` и `host.*` не заводим ### R17. `service.*` и `host.*` не заводим
**НЕ СЛЕДУЕТ.** Пока это один бинарь на одном хосте. **НЕ СЛЕДУЕТ.** Поле, значение которого одинаково во всех записях, не
заводится — для одного бинаря на одном хосте это `service.*` и `host.*`.
**Почему.** Поле с одним и тем же значением во всех записях не несёт **Почему.** Такое поле не несёт информации, но стоит места в каждой строке
информации, но стоит места в каждой строке и внимания при чтении. Условие и внимания при чтении. Критерий один на все поля словаря — им же решается,
нужен ли `transport` (R16.1): пока транспорт один, поле постоянно. Условие
названо явно, поэтому правило отпадёт вместе со своей причиной: с названо явно, поэтому правило отпадёт вместе со своей причиной: с
появлением нескольких инстансов различающее поле (`service.version`) появлением нескольких инстансов различающее поле (`service.version`)
добавляется одной строкой при старте. добавляется одной строкой при старте.
@@ -326,7 +328,8 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
### R25. Уровень доменного отказа — по классу отказа ### R25. Уровень доменного отказа — по классу отказа
**ДОЛЖЕН.** Уровень выбирает единственный логирующий (R23), и выбирает по **ДОЛЖЕН.** Уровень выбирает единственный логирующий (R23), и выбирает по
классу, а не по месту в коде. классу, а не по месту в коде. Классификация покрывает **доменные** отказы —
те, что операция вернула значением `error`.
| № | Класс отказа | Кому | Уровень | | № | Класс отказа | Кому | Уровень |
|---|---|---|---| |---|---|---|---|
@@ -341,6 +344,10 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
его вызвал, — и невалидный ввод из формы копился бы в `ERROR` наравне с его вызвал, — и невалидный ввод из формы копился бы в `ERROR` наравне с
упавшей базой. упавшей базой.
Нарушение инварианта в собственном коде — паника, недостижимая ветка — в
таблицу не входит: это не доменный отказ, и логирует его recover-граница
вместе со стеком (`lang/go/errors.md`). Искать его класс здесь не нужно.
### R26. Тот же отказ в асинхронной стадии — уровнем выше ### R26. Тот же отказ в асинхронной стадии — уровнем выше
**ДОЛЖЕН.** Когда пользователь не ждёт результата, отказ адресован **ДОЛЖЕН.** Когда пользователь не ждёт результата, отказ адресован
+2 -2
View File
@@ -37,8 +37,8 @@ layout — а расхождение проявится не на записи,
### R3. Прямой `time.Now()` запрещён линтером, список исключений исчерпывающий ### R3. Прямой `time.Now()` запрещён линтером, список исключений исчерпывающий
**ДОЛЖЕН.** Запрет механизируется `forbidigo`; исключений ровно два, и оба **ДОЛЖЕН.** Запрет проверяется линтером (в Go — `forbidigo`); исключений
прописаны явно: ровно два, и оба прописаны явно:
| № | Исключение | Почему оно не покрывается R1 | | № | Исключение | Почему оно не покрывается R1 |
|---|---|---| |---|---|---|