diff --git a/GUIDE.md b/GUIDE.md index bbaabb7..d6f521d 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -26,6 +26,15 @@ - `docs/conventions/` — **правило на будущее**, применяемое многократно. Живой документ: правится, когда договорённость меняется. +## Оформление + +Имя файла — kebab-case по теме: `app-directories.md`. Правилом это не +записано: обоснование сводится к «чтобы адрес правила +(`stack/ansible/app-directories.md R4`) писался одним способом», а +проверить нарушение всё равно проще глазом, чем сформулировать норму. Номер +R16, под которым это правило существовало, оставлен свободным и не +переиспользуется. + ## Канон и копии Файлы в `docs/conventions/` с шапкой `origin:` — копии из общего канона @@ -106,6 +115,10 @@ это честно, а ДОЛЖЕН при этом обещает то, чего не делает, и через несколько таких случаев обесценивает остальные ДОЛЖЕН в файле. +Отсюда следствие: правило, машинная проверка которого невозможна в принципе +(вкус формулировки, выбор границы, суждение о ситуации), не может быть +ДОЛЖЕН — его модальность СЛЕДУЕТ по построению, а не по слабости. + ### R7. Факт механизации фиксируется в локальном регионе со ссылкой на номер **ДОЛЖЕН.** Регион `механизировано` называет номер правила и конкретную @@ -127,6 +140,11 @@ говорит о прочих, поэтому удалять по факту «у нас уже проверяется» — значит чинить свой файл за чужой счёт. +Списка подписчиков канон по построению не знает, поэтому факт «механизировано +у всех» устанавливается обходом репозиториев вручную — это часть работы по +удалению нормы, а не то, что можно проверить машиной (см. `LANGUAGE.md`, +состояние МЕХАНИЗИРОВАНО). + ### R9. Общая механизация разрешает удалить норму из канона **ДОПУСКАЕТСЯ.** Правило, уехавшее в общий конфиг линтера или в общую роль, @@ -205,16 +223,6 @@ правила в каноне, которую чинят один раз для всех, или лишнюю подписку, где файл просто не нужен. Оставленная отступлением, она прячет обе. -### R16. Имя файла — kebab-case по теме - -**СЛЕДУЕТ.** `app-directories.md`, а не вариации регистра и разделителя. - -**Почему.** Имя файла — часть глобального адреса правила -(`stack/ansible/app-directories.md R4`) и значение ключа `origin` в каждой -копии. Один способ записи избавляет от нескольких написаний одного адреса, -а ошибка в адресе обнаруживается только тем, кто по нему пришёл и ничего не -нашёл. - ### R17. Репо-специфичная часть «Связано» — в локальном регионе **ДОЛЖЕН.** Раздел «Связано» стоит в конце файла; канонические ссылки — в diff --git a/conventions/arch/app-directories.md b/conventions/arch/app-directories.md index 8fa05c8..d1dd6ef 100644 --- a/conventions/arch/app-directories.md +++ b/conventions/arch/app-directories.md @@ -82,9 +82,13 @@ ### R5. Список бэкапа ссылается на те же пути, что и создание директорий **ДОЛЖЕН.** Список выводится из категорий по R4 и ссылается на те же -объявления путей, по которым директории создаются, а не набирается +**объявления путей**, по которым директории создаются, а не набирается независимо. +Объявление пути — то единственное место, где путь директории записан +буквально: переменная деплоя, константа, поле конфигурации. Всё остальное +на него ссылается. + **Почему.** Правило вывода механическое, но применяет его человек или шаблон — то есть ошибиться можно. Общая ссылка делает целый класс ошибок невозможным: переименование директории отражается в обоих местах сразу. diff --git a/conventions/lang/go/config.md b/conventions/lang/go/config.md index 2b01b25..b262325 100644 --- a/conventions/lang/go/config.md +++ b/conventions/lang/go/config.md @@ -120,7 +120,7 @@ func (d Duration) Std() time.Duration { … } чтением всего кода — а узнают о нём обычно на сервере, где переменная не выставлена. -### R9. Проверка запрета покрывает все входы в окружение +### R9. Проверка запрета покрывает всю семью `os` **ДОЛЖЕН.** Механическая проверка R8 (`forbidigo`) ловит не только `os.Getenv`: @@ -134,6 +134,12 @@ func (d Duration) Std() time.Duration { … } незаметно: правило числится механизированным, и глазами его больше никто не проверяет. +Полного покрытия этот паттерн не даёт и дать не может: мимо него проходят +`syscall.Getenv`, вызов через алиас пакета и чтение `/proc/self/environ`. +Проверка закрывает обычные способы — те, которыми окружение читают не +нарочно; сознательный обход она не ловит, и считать R8 полностью +механизированным нельзя. + ### R10. За границей приложения запрет не действует **ДОПУСКАЕТСЯ.** Чтение окружения там, где читающий — не конфигурируемое diff --git a/conventions/lang/go/errors.md b/conventions/lang/go/errors.md index 21c479a..2516b59 100644 --- a/conventions/lang/go/errors.md +++ b/conventions/lang/go/errors.md @@ -4,6 +4,17 @@ `LANGUAGE.md`. Где и когда ошибку **логировать** — в `lang/go/logging.md` (коротко: лог один раз на доменной границе). +Две границы, о которых говорят правила ниже: + +- **доменная граница** — место, где определяется исход операции: use-case, + публичная команда воркера, стадия асинхронной обработки. Ниже неё ошибка + только накапливает контекст, выше — операция уже либо удалась, либо нет. +- **внешняя граница** — место, где ответ покидает процесс: обработчик HTTP, + рендер страницы, отправка сообщения ботом. + +Одна операция проходит обе: сначала доменную (там её исход логируется), +потом внешнюю (там он превращается в ответ). + ## Правила ### R1. Ошибки строятся средствами стандартной библиотеки diff --git a/conventions/lang/go/logging.md b/conventions/lang/go/logging.md index e94e617..35bdc6a 100644 --- a/conventions/lang/go/logging.md +++ b/conventions/lang/go/logging.md @@ -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-логгер) | `_id` и доменные атрибуты | | R16.3 | запись об ошибке | `error` | | R16.4 | вызов внешнего сервиса | `ext.service`, `ext.operation` (логическая операция, не URL), `ext.status_code`, `duration_ms`, `retry` | @@ -223,10 +223,12 @@ dev-выводом перестаёшь ежедневно гонять собс ### R17. `service.*` и `host.*` не заводим -**НЕ СЛЕДУЕТ.** Пока это один бинарь на одном хосте. +**НЕ СЛЕДУЕТ.** Поле, значение которого одинаково во всех записях, не +заводится — для одного бинаря на одном хосте это `service.*` и `host.*`. -**Почему.** Поле с одним и тем же значением во всех записях не несёт -информации, но стоит места в каждой строке и внимания при чтении. Условие +**Почему.** Такое поле не несёт информации, но стоит места в каждой строке +и внимания при чтении. Критерий один на все поля словаря — им же решается, +нужен ли `transport` (R16.1): пока транспорт один, поле постоянно. Условие названо явно, поэтому правило отпадёт вместе со своей причиной: с появлением нескольких инстансов различающее поле (`service.version`) добавляется одной строкой при старте. @@ -326,7 +328,8 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд ### R25. Уровень доменного отказа — по классу отказа **ДОЛЖЕН.** Уровень выбирает единственный логирующий (R23), и выбирает по -классу, а не по месту в коде. +классу, а не по месту в коде. Классификация покрывает **доменные** отказы — +те, что операция вернула значением `error`. | № | Класс отказа | Кому | Уровень | |---|---|---|---| @@ -341,6 +344,10 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд его вызвал, — и невалидный ввод из формы копился бы в `ERROR` наравне с упавшей базой. +Нарушение инварианта в собственном коде — паника, недостижимая ветка — в +таблицу не входит: это не доменный отказ, и логирует его recover-граница +вместе со стеком (`lang/go/errors.md`). Искать его класс здесь не нужно. + ### R26. Тот же отказ в асинхронной стадии — уровнем выше **ДОЛЖЕН.** Когда пользователь не ждёт результата, отказ адресован diff --git a/conventions/lang/go/time.md b/conventions/lang/go/time.md index 176f6d5..0c5ccc7 100644 --- a/conventions/lang/go/time.md +++ b/conventions/lang/go/time.md @@ -37,8 +37,8 @@ layout — а расхождение проявится не на записи, ### R3. Прямой `time.Now()` запрещён линтером, список исключений исчерпывающий -**ДОЛЖЕН.** Запрет механизируется `forbidigo`; исключений ровно два, и оба -прописаны явно: +**ДОЛЖЕН.** Запрет проверяется линтером (в Go — `forbidigo`); исключений +ровно два, и оба прописаны явно: | № | Исключение | Почему оно не покрывается R1 | |---|---|---|