ПОЧЕМУ стало ключевым словом, язык поднят до версии 2

- метка обоснования пишется заглавными и вошла в словарь набора: скелет
  правила теперь целиком из ключевых слов, а не смесь `**ДОЛЖЕН.**` и
  `**Почему.**`; в переводе на другой язык метка меняется как остальные слова
  (ПОЧЕМУ / WHY), 235 вхождений заменены
- метки правила выделены из шкалы в отдельный перечень: ПОЧЕМУ и
  МЕХАНИЗИРОВАНО обязательности не задают, а размечают части, и стандартом не
  даются ни в одном языке — раньше МЕХАНИЗИРОВАНО висело строкой в таблице
  модальности
- версия языка поднята до 2, потому что изменение формы меняет чтение уже
  написанного текста; строка о версии в двенадцати конвенциях перечисляет
  теперь и метки, а служебные слова сценария в неё по-прежнему не входят
This commit is contained in:
av
2026-07-26 14:28:37 +03:00
parent c8071dc438
commit 72d77d74bf
16 changed files with 346 additions and 318 deletions
+12 -12
View File
@@ -10,9 +10,9 @@ prefix: DIRS
**кто создаёт** содержимое и **что будет, если его потерять**. Из категорий
механически выводится состав бэкапа.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 тогда и
только тогда, когда написаны заглавными.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2
тогда и только тогда, когда написаны заглавными.
## Область действия
@@ -37,7 +37,7 @@ prefix: DIRS
Имена в таблице — умолчание для случая «одна директория на категорию».
**Почему.** Все дальнейшие решения — что попадает в бэкап (DIRS-4), что можно
**ПОЧЕМУ.** Все дальнейшие решения — что попадает в бэкап (DIRS-4), что можно
снести при нехватке места, что переживает переезд на другой диск —
читаются из категории, а не выясняются по коду приложения. Без единой
классификации каждое такое решение принимается заново и каждый раз чуть
@@ -49,7 +49,7 @@ prefix: DIRS
**ДОПУСКАЕТСЯ.** Директорий в категории столько, сколько нужно раскладке;
принадлежность к категории задаётся не именем, а участием в списке бэкапа.
**Почему.** Явное разрешение снимает вопрос, не читается ли DIRS-1 как «ровно
**ПОЧЕМУ.** Явное разрешение снимает вопрос, не читается ли DIRS-1 как «ровно
три директории». Крупные файлы отделяют от базы, чтобы двигать их между
дисками независимо (`media/`, `uploads/` — та же категория «данные», что и
`data/`); запрет на такое деление вынуждал бы либо держать всё на одном
@@ -67,7 +67,7 @@ prefix: DIRS
| DIRS-3.2 | миниатюры, распакованные ассеты, кеш внешних ответов, перестраиваемые индексы | кеш |
| DIRS-3.3 | спорный случай | `rm -rf` и запуск приложения заново: поднялось и наверстало само — кеш; не поднялось или поднялось пустым — данные |
**Почему.** Без внешнего теста граница проводится по ощущению «жалко
**ПОЧЕМУ.** Без внешнего теста граница проводится по ощущению «жалко
потерять», а оно смещено в одну сторону: дорогой в пересборке кеш
переезжает в данные и раздувает каждый снапшот. Дорого ≠ невосполнимо, и
разделяет эти два свойства именно способность приложения пересоздать
@@ -80,7 +80,7 @@ prefix: DIRS
**ДОЛЖЕН.** Директории категории «данные» — в списке бэкапа; конфигурация и
кеш — нет.
**Почему.** Кеш раздувает снапшот содержимым, которое приложение
**ПОЧЕМУ.** Кеш раздувает снапшот содержимым, которое приложение
восстановит само. Конфигурацию бэкапить не только незачем, но и вредно: там
лежат секреты, а бэкапы уезжают в облако — источник истины для
конфигурации остаётся в репозитории и хранилище секретов, а не в снапшоте.
@@ -97,7 +97,7 @@ prefix: DIRS
буквально: переменная деплоя, константа, поле конфигурации. Всё остальное
на него ссылается.
**Почему.** Правило вывода механическое, но применяет его человек или
**ПОЧЕМУ.** Правило вывода механическое, но применяет его человек или
шаблон — то есть ошибиться можно. Общая ссылка делает целый класс ошибок
невозможным: переименование директории отражается в обоих местах сразу.
Независимо набранный список расходится тихо — ни деплой, ни прогон бэкапа
@@ -113,7 +113,7 @@ prefix: DIRS
| DIRS-6.1 | файлы самодостаточны на любой момент времени | копированием |
| DIRS-6.2 | консистентность обеспечивает только сама СУБД | дампом: бэкапится директория дампов, сырой каталог базы — нет |
**Почему.** Файловый снапшот работающей СУБД не гарантирует
**ПОЧЕМУ.** Файловый снапшот работающей СУБД не гарантирует
консистентности: скопированный каталог может не восстановиться, и узнают
об этом при восстановлении. Директория дампов — тоже данные, просто
производные, поэтому DIRS-4 покрывает её без оговорок. Сырой каталог базы из
@@ -125,7 +125,7 @@ prefix: DIRS
**ДОЛЖЕН.** Решение «копировать или дампить» (DIRS-6) принимается, когда
приложение заводят.
**Почему.** Неверный выбор ничем себя не проявляет, пока бэкап не
**ПОЧЕМУ.** Неверный выбор ничем себя не проявляет, пока бэкап не
понадобился: прогоны копирования проходят успешно и накапливают снапшоты, из
которых база не поднимется. Отложить решение — значит принять его по факту
первой неудачной попытки восстановления, то есть тогда, когда данных уже
@@ -136,7 +136,7 @@ prefix: DIRS
**ДОЛЖЕН.** Конфигурация приложения задаёт отдельные пути для данных и для
кеша, а не один каталог на всё.
**Почему.** Снаружи категория определяется только тогда, когда разным
**ПОЧЕМУ.** Снаружи категория определяется только тогда, когда разным
категориям соответствуют разные директории. Всё, сложенное в один каталог,
заставляет составлять список бэкапа вручную, читая код приложения, — и
пересматривать его при каждом обновлении, потому что новый подкаталог
@@ -148,7 +148,7 @@ prefix: DIRS
**НЕ ДОЛЖЕН.** Записываемые пути приложения не указывают внутрь
конфигурации.
**Почему.** Конфигурация восстанавливается прогоном деплоя (DIRS-1.1), поэтому
**ПОЧЕМУ.** Конфигурация восстанавливается прогоном деплоя (DIRS-1.1), поэтому
всё, что приложение туда записало, следующий деплой затирает без
предупреждения. Вдобавок директория конфигурации может быть подключена
только на чтение — тогда запись отказывает в рантайме. Ни то ни другое не
+24 -24
View File
@@ -7,9 +7,9 @@ prefix: CONF
Как устроена конфигурация: где лежит, как попадает в процесс, что с
секретами и когда падает.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 тогда и
только тогда, когда написаны заглавными.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2
тогда и только тогда, когда написаны заглавными.
## Область действия
@@ -25,7 +25,7 @@ prefix: CONF
**ДОЛЖЕН.** Приложение читает параметры из файла конфигурации; переменные
окружения источником конфигурации не служат.
**Почему.** Три довода, по убыванию веса:
**ПОЧЕМУ.** Три довода, по убыванию веса:
- **Один типизированный источник.** Файл несёт секции, комментарии,
единицы измерения и валидируется целиком. Окружение — плоский набор
@@ -48,7 +48,7 @@ prefix: CONF
**СЛЕДУЕТ.** Конкретный формат (TOML, YAML) выбирается по стеку.
**Почему.** Комментарий у каждого поля (CONF-9) — часть того, ради чего конфиг
**ПОЧЕМУ.** Комментарий у каждого поля (CONF-9) — часть того, ради чего конфиг
вообще читают; формат, в котором комментарий негде разместить, делает CONF-9
невыполнимым. Секции дают структуру, которую валидатор проверяет целиком, —
плоский список пар такой возможности не даёт и возвращает нас к тем же
@@ -59,7 +59,7 @@ prefix: CONF
**СЛЕДУЕТ.** Имя по умолчанию ищется в рабочей директории процесса, а путь
задаётся опцией командной строки.
**Почему.** Запуск без аргументов работает одинаково в разработке, в
**ПОЧЕМУ.** Запуск без аргументов работает одинаково в разработке, в
контейнере и на сервере, и способ запуска не приходится помнить отдельно
для каждой среды. Опция нужна ровно для случаев, когда конфигов несколько
(тесты, второй инстанс): без неё их разводят переменной окружения — тем
@@ -71,7 +71,7 @@ prefix: CONF
рабочей директории (CONF-3), приложение не стартует: сообщение называет
искомый путь, код возврата ненулевой.
**Почему.** Конфиг — артефакт деплоя (CONF-12), и его отсутствие означает, что
**ПОЧЕМУ.** Конфиг — артефакт деплоя (CONF-12), и его отсутствие означает, что
развёртывание не довело работу до конца, а не что приложение попросили
работать на умолчаниях. Умолчания (CONF-7) существуют, чтобы работал
**неполный** файл, а не отсутствующий, — именно здесь читатель спотыкается
@@ -89,7 +89,7 @@ prefix: CONF
**НЕ ДОЛЖЕН.** Реальный конфиг не коммитится; в репозитории — образец.
**Почему.** Рабочий конфиг содержит отрендеренные секреты (CONF-12), а секрет,
**ПОЧЕМУ.** Рабочий конфиг содержит отрендеренные секреты (CONF-12), а секрет,
попавший в историю, чинится ротацией, а не удалением файла. Кроме того,
закоммиченный конфиг конкретной среды становится вторым источником истины:
он расходится с тем, что реально развёрнуто, и расходится молча.
@@ -99,7 +99,7 @@ prefix: CONF
**ДОЛЖЕН.** Разбор — при старте, в одну типизированную структуру; чтения
файла конфигурации в бизнес-коде нет.
**Почему.** Второе место чтения — это второй момент времени: две части кода
**ПОЧЕМУ.** Второе место чтения — это второй момент времени: две части кода
начинают видеть разные значения одного параметра, и расхождение не
воспроизводится, потому что зависит от того, когда файл потрогали.
Типизированная структура вдобавок переносит ошибку формата в старт (CONF-17),
@@ -109,7 +109,7 @@ prefix: CONF
**ДОЛЖЕН.** Смена параметров — рестарт процесса.
**Почему.** Изменяемый конфиг делает поведение функцией момента: один
**ПОЧЕМУ.** Изменяемый конфиг делает поведение функцией момента: один
запрос обслуживается наполовину старыми, наполовину новыми значениями, а
разбор инцидента требует знать хронологию правок файла, а не его текущее
содержимое.
@@ -121,7 +121,7 @@ prefix: CONF
**ДОЛЖЕН.** Значение по умолчанию задаётся в коде, файл его перекрывает.
**Почему.** Умолчание, живущее в образце, действует только для тех, кто
**ПОЧЕМУ.** Умолчание, живущее в образце, действует только для тех, кто
образец скопировал: конфиг без этого поля даёт другое поведение, и ни одно
из двух значений не выглядит ошибкой. Умолчание в коде — одно определённое
поведение для неполного конфига и одно место, где это значение меняется.
@@ -131,7 +131,7 @@ prefix: CONF
**ДОЛЖЕН.** В образце присутствуют все секции и все поля, включая те, у
которых есть умолчание (CONF-7).
**Почему.** Поле, живущее только в коде, для читателя конфига не
**ПОЧЕМУ.** Поле, живущее только в коде, для читателя конфига не
существует: он не знает, что параметр вообще можно менять, и добивается
нужного поведения обходным путём. Полнота образца — цена, которой CONF-7
покупает себе видимость.
@@ -145,7 +145,7 @@ prefix: CONF
- **единицы измерения**, если применимо: секунды/миллисекунды, байты, доля
`01`.
**Почему.** Так конфиг читается без открывания кода — этим он и полезен;
**ПОЧЕМУ.** Так конфиг читается без открывания кода — этим он и полезен;
без комментария читатель всё равно идёт в код, и образец перестаёт быть
справочником. Единицы стоят отдельно: перепутанные секунды и миллисекунды
дают валидное значение и работающий процесс, а ошибка обнаруживается по
@@ -161,7 +161,7 @@ prefix: CONF
| CONF-10.1 | поддерживаемое | обязательны поля этого варианта; поля прочих вариантов не требуются |
| CONF-10.2 | неизвестное | ошибка на старте с перечислением поддерживаемых значений |
**Почему.** Фиксированный на секцию набор обязательных полей оставляет
**ПОЧЕМУ.** Фиксированный на секцию набор обязательных полей оставляет
выбор из двух плохих: заполнять поля бекенда, который не используется, или
не проверять обязательность вовсе — то есть выключить валидацию ровно там,
где вариантов много и ошибиться легче всего. Перечисление поддерживаемых
@@ -174,7 +174,7 @@ prefix: CONF
альтернативные — блоками-комментариями ниже, каждый со своим описанием
полей.
**Почему.** Иначе набор вариантов виден только из кода валидации, и образец
**ПОЧЕМУ.** Иначе набор вариантов виден только из кода валидации, и образец
теряет свойство справочника (CONF-8, CONF-9) ровно на той секции, где выбор
действительно есть. Закомментированный блок вдобавок переключается правкой
на месте, а не сборкой секции с нуля по документации.
@@ -184,7 +184,7 @@ prefix: CONF
**ДОЛЖЕН.** Деплой рендерит значения секретов прямо в файл конфигурации;
отдельного слоя секретов в приложении нет.
**Почему.** Источник истины секрета — внешнее хранилище деплоя, не
**ПОЧЕМУ.** Источник истины секрета — внешнее хранилище деплоя, не
репозиторий и не окружение. Любой второй канал — переменная окружения рядом
с файлом, собственный клиент к хранилищу внутри приложения — возвращает
вопрос «откуда взялось значение, которое сейчас в процессе». Приложение при
@@ -196,7 +196,7 @@ prefix: CONF
**ДОЛЖЕН.** Права `0600`, владелец — пользователь, от имени которого
работает процесс.
**Почему.** После CONF-1 и CONF-12 файл конфигурации — единственная
**ПОЧЕМУ.** После CONF-1 и CONF-12 файл конфигурации — единственная
поверхность, на которой секреты лежат, и весь довод «файл вместо окружения»
держится на его правах: конфиг, читаемый всеми на машине, раздаёт секреты
шире, чем раздало бы окружение, — и тогда CONF-1 меняет одну утечку на
@@ -207,7 +207,7 @@ prefix: CONF
**ДОЛЖЕН.** Значение секретного поля в образце — пустая строка, а не
пример.
**Почему.** Правдоподобная заглушка доезжает до продакшена как настоящее
**ПОЧЕМУ.** Правдоподобная заглушка доезжает до продакшена как настоящее
значение: шаблон отрендерился криво, поле осталось от образца, и проверка
непустоты (CONF-15) его пропускает. Пустая строка делает недорендеренный конфиг
механически отличимым от заполненного.
@@ -216,7 +216,7 @@ prefix: CONF
**ДОЛЖЕН.** Непустота обязательных секретов проверяется на старте.
**Почему.** Это ловит криво отрендеренный шаблон до того, как он превратится
**ПОЧЕМУ.** Это ловит криво отрендеренный шаблон до того, как он превратится
в 401 от внешнего API через час работы, — то есть в момент, когда причина
ещё очевидна и связана с деплоем.
@@ -225,7 +225,7 @@ prefix: CONF
**НЕ ДОЛЖЕН.** Значение секретного поля не появляется в записи лога ни на
одном уровне.
**Почему.** У логов круг доступа шире, чем у файла под `0600` (CONF-13): они
**ПОЧЕМУ.** У логов круг доступа шире, чем у файла под `0600` (CONF-13): они
собираются, пересылаются и попадают в бэкапы, где права исходного файла уже
ничего не значат. Попавший в лог секрет чинится ротацией, а не удалением
записи. Типичный источник утечки — отладочный дамп разобранного конфига при
@@ -236,7 +236,7 @@ prefix: CONF
**ДОЛЖЕН.** Невалидный конфиг — запись уровня `ERROR` и выход с ненулевым
кодом; процесс не стартует «наполовину».
**Почему.** Наполовину стартовавший процесс проходит проверку живости и
**ПОЧЕМУ.** Наполовину стартовавший процесс проходит проверку живости и
падает позже — на первом запросе, который трогает испорченный параметр, — и
падение выглядит дефектом кода, а не ошибкой деплоя. Ненулевой код нужен,
чтобы неудачный старт увидел супервизор: без него он неотличим от штатного
@@ -254,7 +254,7 @@ prefix: CONF
| CONF-18.4 | строки, которые парсятся во что-то (длительности, зоны, URL, идентификаторы сущностей), реально парсятся | при первом обращении к тому, что за ними стоит |
| CONF-18.5 | включённые секции консистентны: у включённой интеграции заданы все обязательные поля | при первом вызове интеграции, часто по расписанию |
**Почему.** Список минимальный и собран по одному признаку — правый
**ПОЧЕМУ.** Список минимальный и собран по одному признаку — правый
столбец: каждая из этих ошибок иначе всплывает там, где связь с деплоем уже
потеряна, и диагностируется как дефект приложения. Проверка на старте
сводит их все к одному моменту и одному сообщению.
@@ -264,7 +264,7 @@ prefix: CONF
**ДОЛЖЕН.** Валидация собирает все найденные проблемы и выводит их одним
списком, а не падает на первой.
**Почему.** Иначе цикл «запуск — одна ошибка — правка» повторяется столько
**ПОЧЕМУ.** Иначе цикл «запуск — одна ошибка — правка» повторяется столько
раз, сколько в конфиге ошибок, а на сервере каждая итерация — это ещё и
деплой. Криво отрендеренный шаблон обычно ломает не одно поле, а все поля
одного источника: разом они читаются как одна причина, по одной — как
@@ -280,7 +280,7 @@ prefix: CONF
| CONF-21.1 | несекретное | имя поля, ожидание и полученное значение: «ожидалось 0–1, получено `1.5`» |
| CONF-21.2 | секретное | имя поля и суть нарушения, без значения |
**Почему.** Сообщение без значения отправляет читателя в файл — сличать
**ПОЧЕМУ.** Сообщение без значения отправляет читателя в файл — сличать
глазами каждую строку списка CONF-19; ошибки вида «секунды вместо миллисекунд»
или пробел в конце значения из такого сообщения не читаются вовсе. Значение
секретного поля при этом печатать некуда: вывод старта уходит в лог
+10 -10
View File
@@ -6,9 +6,9 @@ prefix: KEYS
Как выбираются и как выглядят первичные ключи сущностей.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 тогда и
только тогда, когда написаны заглавными.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2
тогда и только тогда, когда написаны заглавными.
## Область действия
@@ -27,7 +27,7 @@ prefix: KEYS
который порождает приложение, — во **всех** таблицах, включая те, что
снаружи не адресуются.
**Почему.** Ветвления здесь нет намеренно, хотя напрашивается: «эту
**ПОЧЕМУ.** Ветвления здесь нет намеренно, хотя напрашивается: «эту
сущность снаружи не адресуют, ей хватит целого числа». Внутренние сущности
имеют привычку становиться внешними — и тогда целочисленный идентификатор
утекает в URL задним числом, а миграция ключа на живых данных стоит
@@ -47,7 +47,7 @@ prefix: KEYS
**ДОЛЖЕН.** Значение ключа известно до вставки строки.
**Почему.** Идентификатор нужен раньше, чем база ответит: его пишут в лог
**ПОЧЕМУ.** Идентификатор нужен раньше, чем база ответит: его пишут в лог
начатой операции, кладут в связанные записи одной транзакции и возвращают
клиенту. Генерация на стороне базы вынуждает либо ждать `last_insert_rowid`
и достраивать связи вторым проходом, либо иметь два источника истины о
@@ -58,7 +58,7 @@ prefix: KEYS
**ДОЛЖЕН.** Один модуль порождает идентификаторы, он же их разбирает.
Самодельных генераторов и парсеров в коде нет.
**Почему.** Нормализация регистра (KEYS-4) и проверка формата обязаны
**ПОЧЕМУ.** Нормализация регистра (KEYS-4) и проверка формата обязаны
применяться ко всем идентификаторам без исключения. Любая вторая точка
входа рано или поздно окажется той, где нормализацию забыли, — и дефект
проявится не там, где создан.
@@ -67,7 +67,7 @@ prefix: KEYS
**ДОЛЖЕН.** Идентификаторы порождаются и хранятся в нижнем регистре.
**Почему.** Сравнение строк в базе обычно побайтовое, поэтому регистр — не
**ПОЧЕМУ.** Сравнение строк в базе обычно побайтовое, поэтому регистр — не
косметика, а корректность поиска. Спецификация ULID канонизирует **верхний**
регистр, и библиотеки по умолчанию отдают именно его: без единой точки (KEYS-3)
разный регистр появится в базе сам собой.
@@ -83,7 +83,7 @@ prefix: KEYS
| KEYS-5.1 | путь или query URL | «не найдено» без обращения к хранилищу |
| KEYS-5.2 | собственная форма, данные кнопки | «некорректный ввод» либо «элемент устарел» |
**Почему.** Синтаксически невалидное значение не может соответствовать
**ПОЧЕМУ.** Синтаксически невалидное значение не может соответствовать
записи, поэтому поход в базу за ним — заведомо холостой; отсекая его на
границе, мы дёшево снимаем целый класс мусорного трафика.
@@ -106,7 +106,7 @@ prefix: KEYS
**ДОПУСКАЕТСЯ.** Когда ключ есть по природе данных, отдельный
сгенерированный идентификатор не заводится.
**Почему.** Суррогат поверх естественного ключа создаёт второй способ
**ПОЧЕМУ.** Суррогат поверх естественного ключа создаёт второй способ
адресовать ту же строку — а значит, возможность рассинхрона между ними и
лишний вопрос «по какому из них искать» на каждом запросе. Дополнительной
информации он не несёт.
@@ -117,7 +117,7 @@ prefix: KEYS
задание, корреляционный ключ), порождаются тем же модулем (KEYS-3) и в том же
формате.
**Почему.** Единый формат делает работающим главный побочный эффект
**ПОЧЕМУ.** Единый формат делает работающим главный побочный эффект
строковых идентификаторов: `grep` по голому значению собирает все
упоминания сущности в логах независимо от имени поля. Второй формат
идентификаторов эту возможность отменяет ровно для тех записей, где она
+16 -16
View File
@@ -7,9 +7,9 @@ prefix: TIME
Как приложение записывает моменты и длительности: в каком формате, откуда
берётся значение и где появляется не-UTC.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 тогда и
только тогда, когда написаны заглавными.
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2
тогда и только тогда, когда написаны заглавными.
## Область действия
@@ -26,7 +26,7 @@ prefix: TIME
**ДОЛЖЕН.** Момент времени записывается как `2026-06-28T11:23:45Z`
одинаково в хранении, логах, API и обмене с внешними системами.
**Почему.** Разные форматы в разных слоях требуют преобразования на каждой
**ПОЧЕМУ.** Разные форматы в разных слоях требуют преобразования на каждой
границе, а ошибка в таком преобразовании не видна сразу: она всплывает через
полгода, на переходе на летнее время, когда реальное смещение перестаёт
совпадать с тем, которое подразумевалось при написании кода. UTC с явным `Z`
@@ -37,7 +37,7 @@ prefix: TIME
**ДОЛЖЕН.** Внутри одной колонки БД и внутри одного потока логов длина
строки времени одна и от записи к записи не плавает.
**Почему.** Лексикографическая сортировка совпадает с хронологией только
**ПОЧЕМУ.** Лексикографическая сортировка совпадает с хронологией только
среди строк одинаковой длины: `…00.123Z` сортируется раньше `…00.12Z`, хотя
произошло позже. Ради этого ширина и фиксируется — `ORDER BY created_at` по
текстовому полю обязан давать порядок событий. Плавающая ширина (типичный
@@ -49,7 +49,7 @@ prefix: TIME
**ДОПУСКАЕТСЯ.** У колонки БД и у потока логов каждая своя точность.
**Почему.** Квантор в TIME-2 — на носитель, а не на приложение, потому что
**ПОЧЕМУ.** Квантор в TIME-2 — на носитель, а не на приложение, потому что
строки разных носителей между собой не сравниваются: сортировка идёт внутри
колонки, чтение — внутри потока. Явное разрешение нужно, чтобы TIME-2 не читался
как «одна точность на всё приложение»: от подгонки формата логов под формат
@@ -61,7 +61,7 @@ prefix: TIME
**НЕ ДОЛЖЕН.** Ни в базе, ни в логах, ни в JSON API нет меток в локальной
зоне.
**Почему.** Метка без зоны неинтерпретируема вне процесса, который её
**ПОЧЕМУ.** Метка без зоны неинтерпретируема вне процесса, который её
записал: чтобы понять, какому моменту она соответствует, читателю нужно
знать настройки чужой машины на момент записи. И даже зная их, он не
разберёт час перехода на зимнее время: этот час идёт дважды, две записи
@@ -73,7 +73,7 @@ prefix: TIME
долями секунды принимается от внешней системы и приводится к каноническому
виду (TIME-1) в точке разбора (TIME-5).
**Почему.** Канонический вид — обязательство нашего писателя, а не
**ПОЧЕМУ.** Канонический вид — обязательство нашего писателя, а не
контракт, наложенный на внешние системы: `…14:23:45+03:00` называет тот же
момент, что `…11:23:45Z`, и отклонять его — значит ломать интеграцию со
стороной, которая стандарт соблюла. Пропущенное же как есть, такое значение
@@ -88,7 +88,7 @@ prefix: TIME
**ДОЛЖЕН.** Один модуль отдаёт текущий момент, он же форматирует и разбирает
метки; прямые вызовы часов по коду не разбросаны.
**Почему.** Формат, зона (TIME-1) и ширина (TIME-2) обязаны выполняться для всех
**ПОЧЕМУ.** Формат, зона (TIME-1) и ширина (TIME-2) обязаны выполняться для всех
меток без исключения, а каждый прямой вызов часов заводит ещё одно место,
где их можно не соблюсти. Промах такого вызова проявляется не в коде, а в
данных, и обнаруживается, когда испорченных записей уже накопилось.
@@ -98,7 +98,7 @@ prefix: TIME
**НЕ ДОЛЖЕН.** Колонки времени не имеют `DEFAULT` с текущим моментом.
**Почему.** Дефолт превращает забытую вставку `created_at` в тихо работающий
**ПОЧЕМУ.** Дефолт превращает забытую вставку `created_at` в тихо работающий
код: значение появляется, но приходит от сервера БД — то есть с других часов
и в формате, который выбирала не единая точка (TIME-5). Без дефолта та же ошибка
падает громко и чинится в момент написания, а не при разборе расхождения
@@ -110,7 +110,7 @@ prefix: TIME
**ДОЛЖЕН.** Длительность операции записывается числом (обычно
миллисекундами) в поле вида `duration_ms`.
**Почему.** Метка отвечает на вопрос «когда», длительность — на «сколько».
**ПОЧЕМУ.** Метка отвечает на вопрос «когда», длительность — на «сколько».
Пара меток заставляет каждого потребителя знать, какие именно две из них
образуют операцию, и вычитать их самому — в запросе, в дашборде и глазами в
логе; число сравнивается, агрегируется и попадает в перцентили без этого
@@ -121,7 +121,7 @@ prefix: TIME
**СЛЕДУЕТ.** Замер живёт там же, где вызов, границы которого он измеряет.
**Почему.** Обе границы операции видит только этот слой: замер уровнем выше
**ПОЧЕМУ.** Обе границы операции видит только этот слой: замер уровнем выше
приписывает операции чужие накладные расходы, уровнем ниже — теряет часть
вызова. В обоих случаях число остаётся правдоподобным и потому не
оспаривается, хотя отвечает не на тот вопрос, который к нему задают.
@@ -135,7 +135,7 @@ prefix: TIME
| TIME-9.1 | момент события | стенные часы через единую точку (TIME-5) |
| TIME-9.2 | длительность операции | монотонные часы процесса |
**Почему.** Стенные часы подводит NTP: они могут шагнуть назад, и тогда
**ПОЧЕМУ.** Стенные часы подводит NTP: они могут шагнуть назад, и тогда
интервал, посчитанный вычитанием, выйдет отрицательным, а при шаге вперёд —
правдоподобным, но выдуманным. Монотонные часы, наоборот, не годятся для
меток: их ноль произволен и не переживает перезапуск процесса, так что вне
@@ -148,7 +148,7 @@ prefix: TIME
**ДОЛЖЕН.** Преобразование в зону пользователя происходит при выводе и не
проникает в хранение, сортировку и логи.
**Почему.** Как только конвертация уходит вглубь, результат вычислений
**ПОЧЕМУ.** Как только конвертация уходит вглубь, результат вычислений
начинает зависеть от настроек конкретного зрителя: одна и та же выборка даёт
разные группировки, а порядок записей перестаёт быть общим для всех. Ещё
хуже, что при конвертации в нескольких слоях её легко выполнить дважды —
@@ -160,7 +160,7 @@ prefix: TIME
**ДОЛЖЕН.** Значение приходит из конфигурации, значение по умолчанию —
`UTC`.
**Почему.** Зашитая в код зона превращает переезд или второго пользователя в
**ПОЧЕМУ.** Зашитая в код зона превращает переезд или второго пользователя в
другом поясе в правку кода и релиз. Значение по умолчанию `UTC` выбрано
потому, что оно не притворяется настроенным: показанное время совпадает с
тем, что лежит в базе и в логах, поэтому несовпадение с ожиданиями читается
@@ -171,7 +171,7 @@ prefix: TIME
**ДОЛЖЕН.** «Сегодня», «за месяц» и прочие календарные границы считаются с
явно переданной зоной, а не с системной зоной процесса.
**Почему.** Системная зона разная на ноутбуке разработчика и в контейнере на
**ПОЧЕМУ.** Системная зона разная на ноутбуке разработчика и в контейнере на
сервере, поэтому граница суток уезжает, а вместе с ней — содержимое отчёта:
расхождение не воспроизводится там, где его заметили, и объясняется средой,
а не кодом. Явно переданная зона делает результат функцией от аргументов.