Compare commits
30
Commits
a961aa2b40
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
271603122d
|
||
|
|
34d53d667d
|
||
|
|
0d335ff58d
|
||
|
|
9a3a89358f
|
||
|
|
7fb60828db
|
||
|
|
9c86d9f2de
|
||
|
|
787d0bb5ea
|
||
|
|
11fc9e1fee
|
||
|
|
b516bfb02c
|
||
|
|
e8fdc98557
|
||
|
|
c96566d4b4
|
||
|
|
59a1c23f55
|
||
|
|
1d19e0357b
|
||
|
|
682fa075bb
|
||
|
|
170c06c1da
|
||
|
|
67d51db212
|
||
|
|
fe61ecd6c5
|
||
|
|
c1cb240540
|
||
|
|
d5118336cb
|
||
|
|
df8c58671f
|
||
|
|
4943bf1dd2
|
||
|
|
7b2869d3a4
|
||
|
|
4cd0c97ed0
|
||
|
|
72d77d74bf
|
||
|
|
c8071dc438
|
||
|
|
3e0ec46134
|
||
|
|
98fc69d585
|
||
|
|
3fce663d7a
|
||
|
|
f99a513058
|
||
|
|
7fca0e8cb8
|
@@ -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"
|
||||||
@@ -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).
|
||||||
@@ -18,45 +19,83 @@ code in this repository.
|
|||||||
## Форма правила
|
## Форма правила
|
||||||
|
|
||||||
- Четыре обязательные части: `### <ПРЕФИКС>-<N>. Заголовок`, абзац
|
- Четыре обязательные части: `### <ПРЕФИКС>-<N>. Заголовок`, абзац
|
||||||
`**МОДАЛЬНОСТЬ.** норма`, абзац `**Почему.** …`. Правило без «Почему» не
|
`**МОДАЛЬНОСТЬ.** норма`, абзац `**ПОЧЕМУ.** …`. Правило без обоснования не
|
||||||
принимается.
|
принимается.
|
||||||
|
- `**ПРИМЕРЫ.**` — необязательный пятый блок после обоснования: код парой
|
||||||
|
«плохо → хорошо». Иллюстрация нормы, а не спецификация — требований в блоке
|
||||||
|
нет, дословным сниппетом он не является, при расхождении действует норма.
|
||||||
- Норма — одна фраза; если в неё не влезает, это два правила.
|
- Норма — одна фраза; если в неё не влезает, это два правила.
|
||||||
|
- Область правила — от его заголовка до следующего заголовка любого уровня;
|
||||||
|
метка открывает блок, блок длится до следующей метки или до конца области.
|
||||||
|
Абзацы после `**ПОЧЕМУ.**` — продолжение обоснования: требований в них не
|
||||||
|
живёт, требование ставят в блок нормы. Таблица и список после модальной
|
||||||
|
метки — часть нормы.
|
||||||
- Шкала инвариантна, словарь — параметр языка набора. Канон русский, значит
|
- Шкала инвариантна, словарь — параметр языка набора. Канон русский, значит
|
||||||
слова: **ДОЛЖЕН**, **НЕ ДОЛЖЕН**, **СЛЕДУЕТ**, **НЕ СЛЕДУЕТ**,
|
слова: **ДОЛЖЕН**, **НЕ ДОЛЖЕН**, **СЛЕДУЕТ**, **НЕ СЛЕДУЕТ**,
|
||||||
**ДОПУСКАЕТСЯ**. Словарь один на канон, синонимов на ступень нет.
|
**ДОПУСКАЕТСЯ**. Словарь один на канон, синонимов на ступень нет.
|
||||||
- `SHALL` не используется ни в одном словаре — занято OpenSpec.
|
- `SHALL` не используется ни в одном словаре — занято OpenSpec.
|
||||||
- Нормативно только заглавное написание (правило RFC 8174): строчное
|
- Нормативно только заглавное написание (правило RFC 8174): строчное
|
||||||
«должен» в прозе нормой не является.
|
«должен» в прозе нормой не является.
|
||||||
- ДОЛЖЕН требует двух условий сразу: нарушение причиняет названный вред
|
- ДОЛЖЕН требует двух условий сразу: назван вред от нарушения (META-25) и
|
||||||
и норма проверяема машиной. Проверяемость сама по себе до ДОЛЖЕН не
|
вердикт о нарушении воспроизводим (META-6). Воспроизводимость сама по себе
|
||||||
повышает.
|
до ДОЛЖЕН не повышает — иначе шкала наполняется проверяемыми мелочами.
|
||||||
- ДОПУСКАЕТСЯ адресовано рецензенту: помеченный им выбор на ревью не
|
- ДОПУСКАЕТСЯ адресовано рецензенту: помеченный им выбор на ревью не
|
||||||
обсуждается.
|
обсуждается.
|
||||||
- **МЕХАНИЗИРОВАНО** — не модальность, а способ проверки: отметка стоит
|
- **МЕХАНИЗИРОВАНО** — не модальность, а способ проверки, и свойство
|
||||||
рядом с модальным словом (`**ДОЛЖЕН. МЕХАНИЗИРОВАНО.**`), а не вместо него.
|
репозитория, а не канона: в тексте конвенции отметки нет, она стоит при
|
||||||
|
записи о механизации в локальной части копии (META-7).
|
||||||
|
- META-8: норма не удаляется из канона никогда, чем бы её ни проверяли.
|
||||||
|
Механизация её не заменяет и не сокращает.
|
||||||
|
- Метки правила — **ПОЧЕМУ**, **ПРИМЕРЫ**, **МЕХАНИЗИРОВАНО** и **СНЯТО** —
|
||||||
|
тоже словарь набора и перечислены в строке о версии языка наравне с
|
||||||
|
модальными словами.
|
||||||
|
- META-30: правка словаря или состава частей правила доходит до `READING.md` —
|
||||||
|
документа, который едет к потребителю. Словари двух описаний совпадают.
|
||||||
- Заглавные модальные слова не употребляются вне правил: ни в «Область
|
- Заглавные модальные слова не употребляются вне правил: ни в «Область
|
||||||
действия», ни в «Связано», ни в локальной части копии, ни во вводной прозе.
|
действия», ни в «Связано», ни в локальной части копии, ни во вводной прозе.
|
||||||
Исключение — строка о версии языка, которая их перечисляет.
|
Исключение — строка о версии языка, которая их перечисляет.
|
||||||
- «Почему» отвечает на «что сломается, если сделать иначе», а не
|
- Обоснование отвечает на «что сломается, если сделать иначе», а не
|
||||||
пересказывает норму. «Потому что так принято» — не обоснование.
|
пересказывает норму. «Потому что так принято» — не обоснование.
|
||||||
|
- Форма обоснования не ограничена: рамки смысловые. Длина, рассуждение,
|
||||||
|
примеры, ссылки на внешние практики и чужие проекты — всё допустимо;
|
||||||
|
запрещённых слов нет. Обязательность несёт норма, и путаницу исключает
|
||||||
|
правило о заглавных.
|
||||||
|
- Служебные слова сценарного блока — тоже словарь набора: **КОГДА**,
|
||||||
|
**ТОГДА**, **И**, **ИЛИ** (по-английски `WHEN`/`THEN`/`AND`/`OR`). Одна
|
||||||
|
форма на роль, заглавными. Модальностью не являются, в строку о версии
|
||||||
|
языка не попадают.
|
||||||
- Правило, классифицирующее ситуации, пишется таблицей «ситуация → вердикт»;
|
- Правило, классифицирующее ситуации, пишется таблицей «ситуация → вердикт»;
|
||||||
строки нумеруются `KEYS-5.1`. Строки взаимоисключающи по умолчанию; иной
|
строки нумеруются `KEYS-5.1`. Строки взаимоисключающи по умолчанию; иной
|
||||||
порядок объявляется явно, а перечисленные случаи покрывают область
|
порядок объявляется явно, а перечисленные случаи покрывают область
|
||||||
действия.
|
действия.
|
||||||
- Модальность принадлежит правилу, а не файлу: `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` канон не занимает: они зарезервированы за локальными
|
||||||
правилами репозиториев-потребителей.
|
правилами репозиториев-потребителей.
|
||||||
- Перенос правила в другой файл — смысловое изменение: новый префикс и новый
|
- Перенос правила в другой файл — смысловое изменение: новый префикс и новый
|
||||||
@@ -65,13 +104,16 @@ code in this repository.
|
|||||||
## Ссылки
|
## Ссылки
|
||||||
|
|
||||||
- META-20: норму можно исполнить, имея один этот файл. Ссылка на правило
|
- META-20: норму можно исполнить, имея один этот файл. Ссылка на правило
|
||||||
чужой темы допустима в «Почему», в «Связано» и в разграничении области
|
чужой темы допустима в обосновании, в «Связано» и в разграничении области
|
||||||
действия — но не в самой норме. Нужен концепт соседней темы — коротко
|
действия — но не в самой норме. Нужен концепт соседней темы — коротко
|
||||||
повторить его здесь, соседа назвать в «Почему».
|
повторить его здесь, соседа назвать в обосновании.
|
||||||
- META-21: на соседнюю конвенцию ссылаются именем темы (конвенция
|
- META-21: на соседнюю конвенцию ссылаются именем темы (конвенция
|
||||||
`logging`), на правило — идентификатором (`SLOG-27`), на другой слой своей
|
`logging`), на правило — идентификатором (`SLOG-27`). Пути файлов канона в
|
||||||
темы — словами «базовый слой». Пути файлов канона в тексте конвенции нет
|
тексте конвенции нет (в обвязке — можно).
|
||||||
(в обвязке — можно).
|
- META-24: слой `lang/` или `stack/` называет идентификатор правила арх-слоя
|
||||||
|
**своей** темы прямо в норме — базовый слой в собранной копии всегда рядом.
|
||||||
|
На слои других языков и стеков это не распространяется: их состав зависит
|
||||||
|
от манифеста.
|
||||||
|
|
||||||
## Что в каноне писать нельзя
|
## Что в каноне писать нельзя
|
||||||
|
|
||||||
@@ -80,33 +122,69 @@ code in this repository.
|
|||||||
- META-5: расхождение кода с правилом — отступление, а не повод переписать
|
- META-5: расхождение кода с правилом — отступление, а не повод переписать
|
||||||
правило. Направление всегда конвенция → код; факт «в приложении уже иначе»
|
правило. Направление всегда конвенция → код; факт «в приложении уже иначе»
|
||||||
не является аргументом.
|
не является аргументом.
|
||||||
- META-6: ДОЛЖЕН без механической проверки либо механизируется, либо
|
- META-6: ДОЛЖЕН требует воспроизводимого вердикта — двое проверяющих по
|
||||||
понижается в СЛЕДУЕТ. Правило, непроверяемое машиной в принципе (вкус
|
тексту правила отвечают одинаково. Правило, вердикт которого зависит от
|
||||||
формулировки, суждение о ситуации), — СЛЕДУЕТ по построению.
|
суждения (вкус формулировки, уместность в конкретном месте), — СЛЕДУЕТ по
|
||||||
- META-10: блок «Почему» не удаляется никогда, в том числе после того, как
|
построению. META-27: машинная проверка желательна, но ступени не задаёт;
|
||||||
норма уехала в линтер.
|
проверяющий по умолчанию — читатель правила, человек или агент.
|
||||||
|
- 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). Имя файла — kebab-case по теме. Проза
|
ссылки есть (META-17; пустого раздела не заводят). Имя файла повторяет имя
|
||||||
переносится по ~76 колонок; таблицы и блоки кода не переносятся.
|
темы. Проза переносится по ~76 колонок; таблицы и блоки кода не переносятся.
|
||||||
|
|
||||||
## Ревью формы
|
## Ревью формы
|
||||||
|
|
||||||
@@ -114,6 +192,12 @@ code in this repository.
|
|||||||
проверять машиной». Ни одна из проверок не реализована, поэтому при ревью их
|
проверять машиной». Ни одна из проверок не реализована, поэтому при ревью их
|
||||||
выполняют чтением.
|
выполняют чтением.
|
||||||
|
|
||||||
|
Проверяется всё, что язык **употребляет**: конвенции и `GUIDE.md` (он несёт
|
||||||
|
правила META и строку о версии языка). `LANGUAGE.md` и `README.md` язык
|
||||||
|
цитируют — ключевые слова в них предмет описания, а не норма. Проверки
|
||||||
|
распространения (тема в шапке, самодостаточность нормы, отсутствие путей
|
||||||
|
канона) касаются только конвенций: обвязка к потребителю не едет.
|
||||||
|
|
||||||
## Коммиты
|
## Коммиты
|
||||||
|
|
||||||
Русский, строчная буква, без точки в конце, прошедшее время или страдательный
|
Русский, строчная буква, без точки в конце, прошедшее время или страдательный
|
||||||
@@ -124,14 +208,12 @@ code in this repository.
|
|||||||
|
|
||||||
## Состояние репозитория
|
## Состояние репозитория
|
||||||
|
|
||||||
- Тестов, линтеров и 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, а не код.
|
||||||
канона ещё лежит 31 пустой регион `<!-- local:имя -->` — их предстоит
|
|
||||||
удалить. При правке обвязки истина — README, а не код `conv`.
|
|
||||||
- Ни один репозиторий-потребитель ещё не подключён: копий с шапкой `origin:`
|
- Ни один репозиторий-потребитель ещё не подключён: копий с шапкой `origin:`
|
||||||
в природе нет.
|
в природе нет.
|
||||||
- `TODO.md` — площадка для обсуждения на будущее, а не принятые решения; при
|
- `TODO.md` — площадка для обсуждения на будущее, а не принятые решения; при
|
||||||
|
|||||||
@@ -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-28), и сборка идёт по
|
||||||
сформулировать норму. Номер META-16, под которым это правило существовало,
|
объявленному имени, а не по имени файла. Значит, и высшей модальности нет, а
|
||||||
оставлен свободным и не переиспользуется.
|
ступенью ниже такое правило не окупает строчку.
|
||||||
|
|
||||||
## Канон и копии
|
## Канон и копии
|
||||||
|
|
||||||
@@ -56,24 +81,200 @@ prefix: META
|
|||||||
канон, или документ, переставший быть копией, — тогда `origin:` из шапки
|
канон, или документ, переставший быть копией, — тогда `origin:` из шапки
|
||||||
убирают.
|
убирают.
|
||||||
|
|
||||||
|
## Как проверить границу темы
|
||||||
|
|
||||||
|
Готовая тема проходится по шести вопросам; на каждый отвечает своё правило:
|
||||||
|
|
||||||
|
- на какой вопрос отвечает правило — и тот ли это вопрос, что у темы
|
||||||
|
(META-33);
|
||||||
|
- нужна ли тема правдоподобному потребителю целиком (META-34);
|
||||||
|
- слой сужает базу или отменяет её (META-35);
|
||||||
|
- зависит ли норма от вида приложения и назван ли он (META-36);
|
||||||
|
- названа ли тема решением и адресатом, а не ролью части проекта (META-37);
|
||||||
|
- исполнима ли норма, если соседних тем в репозитории нет (META-20).
|
||||||
|
|
||||||
|
Расхождение на любом из них означает, что граница проходит не там, где
|
||||||
|
нарисована: тема собрана вокруг вещества, склеила два решения или молча
|
||||||
|
предполагает вид приложения. Чинится это разрезом темы или областью
|
||||||
|
действия, а не смягчением нормы.
|
||||||
|
|
||||||
## Правила
|
## Правила
|
||||||
|
|
||||||
### META-1. Одна конвенция — один файл
|
### META-1. Одна конвенция — один файл
|
||||||
|
|
||||||
**ДОЛЖЕН.** Файл описывает ровно один повторяющийся выбор.
|
**ДОЛЖЕН.** Файл описывает ровно один повторяющийся выбор.
|
||||||
|
|
||||||
**Почему.** Подписка перечисляется по темам, и тему берут целиком. Файл,
|
**ПОЧЕМУ.** Подписка перечисляется по темам, и тему берут целиком. Файл,
|
||||||
собравший две темы, вынуждает репозиторий взять правила, которые ему не
|
собравший две темы, вынуждает репозиторий взять правила, которые ему не
|
||||||
нужны, и вычитывать чужую половину при каждом обновлении. Разрезать позже
|
нужны, и вычитывать чужую половину при каждом обновлении. Разрезать позже
|
||||||
дорого: перенос правила в другой файл — это новый префикс и новая
|
дорого: перенос правила в другой файл — это новый префикс и новая
|
||||||
нумерация, поэтому после разреза все внешние ссылки обходят руками.
|
нумерация, поэтому после разреза все внешние ссылки обходят руками.
|
||||||
|
|
||||||
|
### 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. Конвенция заводится, когда решение принимается третий раз
|
||||||
|
|
||||||
**СЛЕДУЕТ.** Поводом служит одно и то же решение, принятое третий раз и
|
**СЛЕДУЕТ.** Поводом служит одно и то же решение, принятое третий раз и
|
||||||
каждый раз чуть по-другому.
|
каждый раз чуть по-другому.
|
||||||
|
|
||||||
**Почему.** По одному-двум случаям не видно, что в решении повторяется, а
|
**ПОЧЕМУ.** По одному-двум случаям не видно, что в решении повторяется, а
|
||||||
что было частностью места: правило, выведенное из первого случая, кодирует
|
что было частностью места: правило, выведенное из первого случая, кодирует
|
||||||
частность и дальше мешает больше, чем помогает. Единичный выбор, к тому же
|
частность и дальше мешает больше, чем помогает. Единичный выбор, к тому же
|
||||||
спорный, читателю нужен вместе с мотивом — это ADR, где мотив и есть
|
спорный, читателю нужен вместе с мотивом — это ADR, где мотив и есть
|
||||||
@@ -81,11 +282,11 @@ prefix: META
|
|||||||
|
|
||||||
### META-3. Новая конвенция пишется там, где заболело
|
### META-3. Новая конвенция пишется там, где заболело
|
||||||
|
|
||||||
**СЛЕДУЕТ.** Первые шаги пути «находка → конвенция → правило линтера →
|
**СЛЕДУЕТ.** Первые шаги пути «находка → конвенция → правило линтера»
|
||||||
удаление прозы» делаются в репозитории, где случилась находка; в канон
|
делаются в репозитории, где случилась находка; в канон продвигается общая
|
||||||
продвигается общая часть.
|
часть.
|
||||||
|
|
||||||
**Почему.** Правило, написанное сразу в общем виде, не проверено ни одним
|
**ПОЧЕМУ.** Правило, написанное сразу в общем виде, не проверено ни одним
|
||||||
применением, и условие применимости у него придумано, а не найдено, —
|
применением, и условие применимости у него придумано, а не найдено, —
|
||||||
платят за это все потребители сразу. Формулировка, обкатанная на одном
|
платят за это все потребители сразу. Формулировка, обкатанная на одном
|
||||||
репозитории, приезжает в канон уже с известной границей.
|
репозитории, приезжает в канон уже с известной границей.
|
||||||
@@ -95,7 +296,7 @@ prefix: META
|
|||||||
**НЕ ДОЛЖЕН.** Норма пишется в настоящем предписывающем времени, без
|
**НЕ ДОЛЖЕН.** Норма пишется в настоящем предписывающем времени, без
|
||||||
описаний того, как сейчас устроен конкретный репозиторий.
|
описаний того, как сейчас устроен конкретный репозиторий.
|
||||||
|
|
||||||
**Почему.** Такое утверждение устаревает молча и подменяет норму описанием:
|
**ПОЧЕМУ.** Такое утверждение устаревает молча и подменяет норму описанием:
|
||||||
читатель перестаёт понимать, что от него требуется, а что просто
|
читатель перестаёт понимать, что от него требуется, а что просто
|
||||||
констатировано. В копиях у остальных потребителей чужой факт вдобавок ложен
|
констатировано. В копиях у остальных потребителей чужой факт вдобавок ложен
|
||||||
с первого дня. «Так сделано у нас» — содержимое региона отступлений, где оно
|
с первого дня. «Так сделано у нас» — содержимое региона отступлений, где оно
|
||||||
@@ -104,12 +305,12 @@ prefix: META
|
|||||||
### META-20. Норма самодостаточна, наружу смотрит только обоснование
|
### META-20. Норма самодостаточна, наружу смотрит только обоснование
|
||||||
|
|
||||||
**ДОЛЖЕН.** Норму правила можно исполнить, имея один этот файл. Ссылка на
|
**ДОЛЖЕН.** Норму правила можно исполнить, имея один этот файл. Ссылка на
|
||||||
правило чужой темы допустима в «Почему», в «Связано» и в разграничении
|
правило чужой темы допустима в обосновании, в «Связано» и в разграничении
|
||||||
области действия — но не в самой норме. Если норме нужен концепт соседней
|
области действия — но не в самой норме. Если норме нужен концепт соседней
|
||||||
темы, он коротко повторяется здесь, а сосед называется в «Почему» как
|
темы, он коротко повторяется здесь, а сосед называется в обосновании как
|
||||||
источник решения.
|
источник решения.
|
||||||
|
|
||||||
**Почему.** Репозиторий подписывается на произвольное подмножество
|
**ПОЧЕМУ.** Репозиторий подписывается на произвольное подмножество
|
||||||
конвенций, и графа зависимостей у него нет по построению. Норма, которую
|
конвенций, и графа зависимостей у него нет по построению. Норма, которую
|
||||||
нельзя исполнить без отсутствующего файла, делает такое подмножество
|
нельзя исполнить без отсутствующего файла, делает такое подмножество
|
||||||
невалидным молча: читатель видит связный текст и не замечает, что часть
|
невалидным молча: читатель видит связный текст и не замечает, что часть
|
||||||
@@ -121,15 +322,30 @@ prefix: META
|
|||||||
### META-21. Ссылка ведёт на тему или на правило, но не на путь в каноне
|
### META-21. Ссылка ведёт на тему или на правило, но не на путь в каноне
|
||||||
|
|
||||||
**ДОЛЖЕН.** На соседнюю конвенцию ссылаются именем темы (конвенция
|
**ДОЛЖЕН.** На соседнюю конвенцию ссылаются именем темы (конвенция
|
||||||
`logging`), на конкретное правило — идентификатором (`SLOG-27`), на другой
|
`logging`), на конкретное правило — идентификатором (`SLOG-27`); путь файла
|
||||||
слой своей же темы — словами «базовый слой». Путь файла канона в тексте
|
канона в тексте конвенции не употребляется.
|
||||||
конвенции не употребляется.
|
|
||||||
|
|
||||||
**Почему.** В репозитории конвенция лежит собранной: слои одной темы — это
|
**ПОЧЕМУ.** В репозитории конвенция лежит собранной: слои одной темы — это
|
||||||
секции одного файла, и пути `lang/go/logging.md` там не существует. Ссылка
|
секции одного файла, и пути `lang/go/logging.md` там не существует. Ссылка
|
||||||
на путь канона умирает при сборке, причём молча — текст остаётся связным.
|
на путь канона умирает при сборке, причём молча — текст остаётся связным.
|
||||||
Имя темы и идентификатор правила переживают и сборку, и переезд файла между
|
Имя темы и идентификатор правила переживают и сборку, и переезд файла между
|
||||||
осями.
|
осями. Слой своей темы поэтому называют идентификатором его правила, а не
|
||||||
|
словами «базовый слой»: слова не проверяются и не ведут к утверждению.
|
||||||
|
|
||||||
|
### META-24. Слой ссылается на идентификаторы своего базового слоя
|
||||||
|
|
||||||
|
**ДОПУСКАЕТСЯ.** Правило языкового или стекового слоя называет идентификатор
|
||||||
|
правила арх-слоя своей темы прямо в норме.
|
||||||
|
|
||||||
|
**ПОЧЕМУ.** Подписываются темой, а не слоем: собранный файл начинается с
|
||||||
|
арх-слоя независимо от того, какие язык и стек выбраны, — базовое правило в
|
||||||
|
копии всегда рядом, и ссылка на него никуда не ведёт. Повтор его концепта
|
||||||
|
здесь заводил бы второй источник правды внутри одного документа: META-20
|
||||||
|
требует повторять концепт там, где соседнего файла может не быть, а базовый
|
||||||
|
слой отсутствовать не может. Остальные слои темы попадают в копию по
|
||||||
|
манифесту, и такой гарантии у них нет — отсюда узость разрешения. Записано
|
||||||
|
оно явно, потому что META-20 читают строже, чем он есть, и без этой строки
|
||||||
|
базу дублируют без нужды.
|
||||||
|
|
||||||
### META-5. Расхождение кода с правилом — отступление, а не повод переписать правило
|
### META-5. Расхождение кода с правилом — отступление, а не повод переписать правило
|
||||||
|
|
||||||
@@ -137,68 +353,90 @@ prefix: META
|
|||||||
фактическую ошибку, внутреннее противоречие или условие применимости,
|
фактическую ошибку, внутреннее противоречие или условие применимости,
|
||||||
которое не даёт ответа.
|
которое не даёт ответа.
|
||||||
|
|
||||||
**Почему.** Правило, подогнанное под текущий код, перестаёт что-либо
|
**ПОЧЕМУ.** Правило, подогнанное под текущий код, перестаёт что-либо
|
||||||
требовать — оно описывает то, что и так происходит, и первое же расхождение
|
требовать — оно описывает то, что и так происходит, и первое же расхождение
|
||||||
переписывает его снова. Направление «конвенция → код» держится ровно тем,
|
переписывает его снова. Направление «конвенция → код» держится ровно тем,
|
||||||
что факт не считается аргументом.
|
что факт не считается аргументом.
|
||||||
|
|
||||||
### META-6. `ДОЛЖЕН` без механической проверки либо механизируется, либо понижается
|
### META-6. Высшая модальность требует воспроизводимого вердикта
|
||||||
|
|
||||||
**ДОЛЖЕН.** Правило, нарушение которого объявлено ошибкой, получает
|
**ДОЛЖЕН.** Правило со ступенью ДОЛЖЕН или НЕ ДОЛЖЕН формулируется так, что
|
||||||
машинную проверку или переводится в СЛЕДУЕТ.
|
двое проверяющих по одному его тексту выносят один и тот же вердикт.
|
||||||
|
|
||||||
**Почему.** Без проверки правило держится на внимании: нарушения копятся
|
**ПОЧЕМУ.** Проверяют конвенцию в первую очередь агент и человек — они читают
|
||||||
молча и всплывают выборочно — на том ревью, куда дошли руки. Для СЛЕДУЕТ
|
текст правила и по нему смотрят код. Проверка, стало быть, есть у каждого
|
||||||
это честно, а ДОЛЖЕН при этом обещает то, чего не делает, и через несколько
|
правила с первого дня, и её инструмент — формулировка, а не скрипт. Отсюда
|
||||||
таких случаев обесценивает остальные ДОЛЖЕН в файле.
|
цена невоспроизводимой нормы: вердикт зависит от того, кто читал, нарушения
|
||||||
|
всплывают выборочно, а отступление нечем записать — неизвестно, нарушено ли.
|
||||||
|
Для СЛЕДУЕТ это честно, там суждение и есть содержание правила; ДОЛЖЕН в
|
||||||
|
таком виде обещает то, чего не делает, и через несколько случаев обесценивает
|
||||||
|
остальные ДОЛЖЕН в файле.
|
||||||
|
|
||||||
Отсюда следствие: правило, машинная проверка которого невозможна в принципе
|
Отсюда следствие: правило, вердикт которого зависит от суждения по построению
|
||||||
(вкус формулировки, выбор границы, суждение о ситуации), не может быть
|
(вкус формулировки, выбор границы, уместность в конкретном месте), не может
|
||||||
ДОЛЖЕН — его модальность СЛЕДУЕТ по построению, а не по слабости.
|
быть ДОЛЖЕН — его модальность СЛЕДУЕТ по природе нормы, а не по слабости.
|
||||||
|
|
||||||
|
### META-25. Высшая модальность выбирается, только когда назван вред
|
||||||
|
|
||||||
|
**СЛЕДУЕТ.** Правило получает ДОЛЖЕН или НЕ ДОЛЖЕН, если в обосновании
|
||||||
|
сказано, что́ ломается при нарушении.
|
||||||
|
|
||||||
|
**ПОЧЕМУ.** Воспроизводимость вердикта — условие необходимое (META-6), но не
|
||||||
|
достаточное: воспроизводимо проверяемых мелочей больше, чем важных вещей, и
|
||||||
|
без второго условия единственным фильтром остаётся удобство проверки. Шкала
|
||||||
|
наполняется опрятностью, читатель перестаёт отличать «уронит прод» от
|
||||||
|
«неаккуратно» — и обесцениваются все ДОЛЖЕН в файле, ровно то, от чего META-6
|
||||||
|
защищает с другой стороны. Собственная ступень этого правила — СЛЕДУЕТ:
|
||||||
|
форма обоснования ничем не ограничена, поэтому «вред назван» вердикта не
|
||||||
|
даёт — один читатель увидит названный вред там, где другой увидит объяснение
|
||||||
|
мотива.
|
||||||
|
|
||||||
|
### META-27. Механизация правила желательна, но ступени не задаёт
|
||||||
|
|
||||||
|
**СЛЕДУЕТ.** Правило со ступенью ДОЛЖЕН получает машинную проверку, когда
|
||||||
|
такую проверку можно написать.
|
||||||
|
|
||||||
|
**ПОЧЕМУ.** Линтер не забывает, ничего не стоит на каждом прогоне и краснеет
|
||||||
|
до ревью, а не на нём: там, где проверка пишется, она дешевле самого
|
||||||
|
внимательного чтения, и путь «находка → конвенция → проверка» кончается ею.
|
||||||
|
Норму она при этом не заменяет и не отменяет (META-8). Условием ступени
|
||||||
|
механизация не является:
|
||||||
|
проверяющий по умолчанию — читатель правила (META-6), а если требовать
|
||||||
|
скрипт, весь канон стоит в СЛЕДУЕТ до того дня, когда скрипты появятся.
|
||||||
|
Ступень говорит о важности нормы, а не о состоянии инструментов.
|
||||||
|
|
||||||
### META-7. Факт механизации фиксируется в копии со ссылкой на правило
|
### META-7. Факт механизации фиксируется в копии со ссылкой на правило
|
||||||
|
|
||||||
**ДОЛЖЕН.** Запись о механизации называет идентификатор правила и
|
**ДОЛЖЕН.** Запись о механизации называет идентификатор правила и
|
||||||
конкретную проверку.
|
конкретную проверку.
|
||||||
|
|
||||||
**Почему.** Механизация — состояние конкретного репозитория, канон о ней не
|
**ПОЧЕМУ.** Механизация — состояние конкретного репозитория, канон о ней не
|
||||||
знает, а без записи следующий автор либо заведёт вторую проверку того же,
|
знает, а без записи следующий автор либо заведёт вторую проверку того же,
|
||||||
либо будет вычитывать глазами уже проверенное машиной. Без идентификатора
|
либо будет вычитывать глазами уже проверенное машиной. Без идентификатора
|
||||||
читатель догадывается сам, к какому утверждению относится проверка, — и
|
читатель догадывается сам, к какому утверждению относится проверка, — и
|
||||||
догадывается по-разному.
|
догадывается по-разному.
|
||||||
|
|
||||||
### META-8. Формулировка не удаляется из канона, пока механизирована не у всех
|
### META-8. Норма из канона не удаляется, чем бы она ни проверялась
|
||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Норма остаётся в каноне, пока хотя бы у одного потребителя
|
**НЕ ДОЛЖЕН.** Формулировка нормы остаётся в правиле независимо от того, кто и
|
||||||
машинной проверки нет.
|
чем её проверяет.
|
||||||
|
|
||||||
**Почему.** У кого линтера нет, тот после удаления остаётся без правила
|
**ПОЧЕМУ.** Проверяющий по умолчанию — читатель правила (META-6), и удаление
|
||||||
вообще: ни проверки, ни текста. Механизация у одного потребителя ничего не
|
нормы забирает у него ровно то, чем он проверяет: линтер сообщает, что
|
||||||
говорит о прочих, поэтому удалять по факту «у нас уже проверяется» —
|
нарушено, но не сообщает, что требуется. Условие «механизировано у всех»
|
||||||
значит чинить свой файл за чужой счёт.
|
спасти не может: оно измеряется в день удаления, а подписчики появляются
|
||||||
|
после. Репозиторий, подключившийся через год, получил бы правило без нормы и
|
||||||
Списка подписчиков канон по построению не знает, поэтому факт «механизировано
|
без проверки — заголовок с обоснованием и никакого способа узнать, что́ именно
|
||||||
у всех» устанавливается обходом репозиториев вручную — это часть работы по
|
предписано, кроме git-истории канона, до которой он не дойдёт. Списка
|
||||||
удалению нормы, а не то, что можно проверить машиной (см. `LANGUAGE.md`,
|
подписчиков у канона к тому же нет по построению, так что «у всех» ему всё
|
||||||
состояние МЕХАНИЗИРОВАНО).
|
равно не проверить.
|
||||||
|
|
||||||
### META-9. Общая механизация разрешает удалить норму из канона
|
|
||||||
|
|
||||||
**ДОПУСКАЕТСЯ.** Правило, уехавшее в общий конфиг линтера или в общую роль,
|
|
||||||
удаляется из канона.
|
|
||||||
|
|
||||||
**Почему.** Формулировка, дублирующая работающую у всех проверку,
|
|
||||||
размазывает внимание: файл на несколько сотен строк заставляет человека и
|
|
||||||
агента добросовестно вычитывать тривиальное именование и не доходить до
|
|
||||||
формы решения. Явное разрешение нужно, чтобы META-8 не читался как запрет
|
|
||||||
удалять вообще.
|
|
||||||
|
|
||||||
### META-10. Обоснование не удаляется никогда
|
### META-10. Обоснование не удаляется никогда
|
||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Блок «Почему» остаётся и после того, как норма уехала в
|
**НЕ ДОЛЖЕН.** Блок ПОЧЕМУ остаётся и после того, как правило стало
|
||||||
линтер.
|
проверяться линтером.
|
||||||
|
|
||||||
**Почему.** Линтер сообщает, что нарушено, но не сообщает, зачем правило
|
**ПОЧЕМУ.** Линтер сообщает, что нарушено, но не сообщает, зачем правило
|
||||||
существует. Без обоснования не видно, когда причина отпала, — проверка
|
существует. Без обоснования не видно, когда причина отпала, — проверка
|
||||||
продолжает работать по инерции, и возразить ей нечем, кроме как отключив.
|
продолжает работать по инерции, и возразить ей нечем, кроме как отключив.
|
||||||
|
|
||||||
@@ -208,7 +446,7 @@ prefix: META
|
|||||||
называет, к чему применяется: к новым таблицам и миграциям, а не к
|
называет, к чему применяется: к новым таблицам и миграциям, а не к
|
||||||
состоянию схемы.
|
состоянию схемы.
|
||||||
|
|
||||||
**Почему.** Здесь не работает привычное «новое пишем правильно, старое
|
**ПОЧЕМУ.** Здесь не работает привычное «новое пишем правильно, старое
|
||||||
переезжает по мере касания»: таблица не переезжает от того, что её
|
переезжает по мере касания»: таблица не переезжает от того, что её
|
||||||
потрогали. Без явной рамки правило читается как требование к текущему
|
потрогали. Без явной рамки правило читается как требование к текущему
|
||||||
состоянию, и оба исхода плохи — миграция живых данных ради опрятности либо
|
состоянию, и оба исхода плохи — миграция живых данных ради опрятности либо
|
||||||
@@ -219,7 +457,7 @@ prefix: META
|
|||||||
**ДОЛЖЕН.** Проверка запрещает нарушение в новых миграциях, а не в уже
|
**ДОЛЖЕН.** Проверка запрещает нарушение в новых миграциях, а не в уже
|
||||||
существующей схеме.
|
существующей схеме.
|
||||||
|
|
||||||
**Почему.** Проверка состояния краснеет на легаси с первого дня: её
|
**ПОЧЕМУ.** Проверка состояния краснеет на легаси с первого дня: её
|
||||||
отключают или обвешивают вечным списком исключений — и она перестаёт ловить
|
отключают или обвешивают вечным списком исключений — и она перестаёт ловить
|
||||||
новое, ради чего заводилась. Проверка границы оставляет старое в покое и
|
новое, ради чего заводилась. Проверка границы оставляет старое в покое и
|
||||||
делает новую ошибку невозможной.
|
делает новую ошибку невозможной.
|
||||||
@@ -229,7 +467,7 @@ prefix: META
|
|||||||
**ДОЛЖЕН.** Перечисленные старые таблицы и раскладки живут как есть, а не
|
**ДОЛЖЕН.** Перечисленные старые таблицы и раскладки живут как есть, а не
|
||||||
как задачи на дочистку.
|
как задачи на дочистку.
|
||||||
|
|
||||||
**Почему.** Список, записанный долгом, требует либо мигрировать живые данные
|
**ПОЧЕМУ.** Список, записанный долгом, требует либо мигрировать живые данные
|
||||||
без выгоды, либо год за годом объяснять невыполненный план. Второе кончается
|
без выгоды, либо год за годом объяснять невыполненный план. Второе кончается
|
||||||
тем, что список перестают вести, — и пропадает единственное место, где видно,
|
тем, что список перестают вести, — и пропадает единственное место, где видно,
|
||||||
где именно правило не действует.
|
где именно правило не действует.
|
||||||
@@ -239,7 +477,7 @@ prefix: META
|
|||||||
**ДОЛЖЕН.** В локальной части копии перечислены отступления, которые уже
|
**ДОЛЖЕН.** В локальной части копии перечислены отступления, которые уже
|
||||||
есть в коде, с идентификатором правила и причиной.
|
есть в коде, с идентификатором правила и причиной.
|
||||||
|
|
||||||
**Почему.** Иначе репозиторий выглядит соблюдающим конвенцию, а проверить
|
**ПОЧЕМУ.** Иначе репозиторий выглядит соблюдающим конвенцию, а проверить
|
||||||
это можно только чтением всего кода. Со ссылками отступления счётны: видно,
|
это можно только чтением всего кода. Со ссылками отступления счётны: видно,
|
||||||
сколько правил конвенции репозиторий реально не соблюдает. Пустой список при
|
сколько правил конвенции репозиторий реально не соблюдает. Пустой список при
|
||||||
этом почти всегда означает не отсутствие отступлений, а то, что их не искали.
|
этом почти всегда означает не отсутствие отступлений, а то, что их не искали.
|
||||||
@@ -254,7 +492,7 @@ prefix: META
|
|||||||
| META-15.2 | правило не применяется в репозитории целиком | чинится условие применимости — в каноне |
|
| META-15.2 | правило не применяется в репозитории целиком | чинится условие применимости — в каноне |
|
||||||
| META-15.3 | конвенция репозиторию не нужна | копия удаляется, подписки нет |
|
| META-15.3 | конвенция репозиторию не нужна | копия удаляется, подписки нет |
|
||||||
|
|
||||||
**Почему.** Отступление описывает исключение, и по нему видно, какая часть
|
**ПОЧЕМУ.** Отступление описывает исключение, и по нему видно, какая часть
|
||||||
правила нарушена. Запись «мы это правило вообще не применяем» такой
|
правила нарушена. Запись «мы это правило вообще не применяем» такой
|
||||||
информации не несёт и маскирует одну из двух чинимых причин: неверную рамку
|
информации не несёт и маскирует одну из двух чинимых причин: неверную рамку
|
||||||
правила в каноне, которую чинят один раз для всех, или лишнюю подписку,
|
правила в каноне, которую чинят один раз для всех, или лишнюю подписку,
|
||||||
@@ -266,7 +504,7 @@ prefix: META
|
|||||||
потребителей; ссылки на ADR, код и файлы конкретного репозитория — в
|
потребителей; ссылки на ADR, код и файлы конкретного репозитория — в
|
||||||
локальной части копии.
|
локальной части копии.
|
||||||
|
|
||||||
**Почему.** Текст канона приезжает ко всем потребителям, и ссылка на чужой
|
**ПОЧЕМУ.** Текст канона приезжает ко всем потребителям, и ссылка на чужой
|
||||||
файл у них битая с первого дня. Ниже маркера та же ссылка никого не
|
файл у них битая с первого дня. Ниже маркера та же ссылка никого не
|
||||||
задевает и переживает обновление, потому что обновление её не трогает.
|
задевает и переживает обновление, потому что обновление её не трогает.
|
||||||
|
|
||||||
@@ -275,7 +513,7 @@ prefix: META
|
|||||||
**ДОЛЖЕН.** Правки репозитория вносятся ниже маркера, а не в текст,
|
**ДОЛЖЕН.** Правки репозитория вносятся ниже маркера, а не в текст,
|
||||||
пришедший из канона.
|
пришедший из канона.
|
||||||
|
|
||||||
**Почему.** Обновление перезаписывает всё, что выше маркера, поэтому правка
|
**ПОЧЕМУ.** Обновление перезаписывает всё, что выше маркера, поэтому правка
|
||||||
там живёт до первого `pull`. Заметить пропажу можно, только вычитав
|
там живёт до первого `pull`. Заметить пропажу можно, только вычитав
|
||||||
`git diff` целиком — а он в этот момент и без того полон изменений канона,
|
`git diff` целиком — а он в этот момент и без того полон изменений канона,
|
||||||
и своя строка теряется среди чужих.
|
и своя строка теряется среди чужих.
|
||||||
@@ -285,7 +523,7 @@ prefix: META
|
|||||||
**НЕ ДОЛЖЕН.** Файл, который развели с каноном намеренно, шапку `origin:`
|
**НЕ ДОЛЖЕН.** Файл, который развели с каноном намеренно, шапку `origin:`
|
||||||
не сохраняет.
|
не сохраняет.
|
||||||
|
|
||||||
**Почему.** По `origin:` решается, какие файлы пересобирать из канона. Форк,
|
**ПОЧЕМУ.** По `origin:` решается, какие файлы пересобирать из канона. Форк,
|
||||||
оставивший шапку, при первом же обновлении теряет ровно то, ради чего его
|
оставивший шапку, при первом же обновлении теряет ровно то, ради чего его
|
||||||
заводили. Происхождение такого документа остаётся в истории коммита, где оно
|
заводили. Происхождение такого документа остаётся в истории коммита, где оно
|
||||||
никого не вводит в заблуждение.
|
никого не вводит в заблуждение.
|
||||||
@@ -294,7 +532,7 @@ prefix: META
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** Одна плоская таблица: файл и строка о том, про что он.
|
**СЛЕДУЕТ.** Одна плоская таблица: файл и строка о том, про что он.
|
||||||
|
|
||||||
**Почему.** Подписка — это набор лежащих файлов, и без описаний вопрос
|
**ПОЧЕМУ.** Подписка — это набор лежащих файлов, и без описаний вопрос
|
||||||
«какая из них про мой случай» решается открыванием каждой. Ценой в десяток
|
«какая из них про мой случай» решается открыванием каждой. Ценой в десяток
|
||||||
файлов это означает, что не открывают ни одной.
|
файлов это означает, что не открывают ни одной.
|
||||||
|
|
||||||
@@ -303,8 +541,32 @@ prefix: META
|
|||||||
**ДОЛЖЕН.** В `AGENTS.md` / `CLAUDE.md` едет одна строка на правило с его
|
**ДОЛЖЕН.** В `AGENTS.md` / `CLAUDE.md` едет одна строка на правило с его
|
||||||
идентификатором; детали остаются в конвенции.
|
идентификатором; детали остаются в конвенции.
|
||||||
|
|
||||||
**Почему.** Сама по себе конвенция агенту не видна: он дойдёт до неё, только
|
**ПОЧЕМУ.** Сама по себе конвенция агенту не видна: он дойдёт до неё, только
|
||||||
если его туда отправили, — а безусловно он читает точку входа. Строка с
|
если его туда отправили, — а безусловно он читает точку входа. Строка с
|
||||||
идентификатором служит и напоминанием, и адресом, по которому за
|
идентификатором служит и напоминанием, и адресом, по которому за
|
||||||
подробностями идут; перенос деталей туда же вернул бы задачу поддержки двух
|
подробностями идут; перенос деталей туда же вернул бы задачу поддержки двух
|
||||||
текстов.
|
текстов.
|
||||||
|
|
||||||
|
## Снятые правила
|
||||||
|
|
||||||
|
Снятое правило остаётся здесь заглушкой: номер занят навсегда, ссылка на него
|
||||||
|
ведёт к объяснению, а нумерация в файле остаётся сплошной (META-31).
|
||||||
|
|
||||||
|
### META-9. Общая механизация разрешала удалить норму из канона
|
||||||
|
|
||||||
|
**СНЯТО 2026-07-26.** Удаление нормы оставляло подписчика, пришедшего позже,
|
||||||
|
без текста и без проверки, а условие «механизировано у всех» набору не
|
||||||
|
проверить: списка подписчиков у него нет. Взамен — META-8, запрет удалять
|
||||||
|
норму вообще.
|
||||||
|
|
||||||
|
### META-16. Имя файла — kebab-case
|
||||||
|
|
||||||
|
**СНЯТО 2026-07-26.** Вреда от нарушения нет, а значит нет и высшей
|
||||||
|
модальности (META-25): сборка идёт по имени темы из шапки, а не по имени
|
||||||
|
файла. Осталось прозой в разделе «Оформление».
|
||||||
|
|
||||||
|
### META-26. Запрет слов обязательства в обосновании
|
||||||
|
|
||||||
|
**СНЯТО 2026-07-26.** Правило о заглавных уже делает строчное «обязан»
|
||||||
|
ненормативным, поэтому запрет ничего не добавлял, а форму обоснования рамками
|
||||||
|
не ограничивают.
|
||||||
|
|||||||
+388
-111
@@ -12,6 +12,18 @@ version: 1
|
|||||||
пополниться, и текст, написанный по предыдущей версии, должен читаться по
|
пополниться, и текст, написанный по предыдущей версии, должен читаться по
|
||||||
той, по которой написан.
|
той, по которой написан.
|
||||||
|
|
||||||
|
Документ адресован автору набора и в репозиторий-потребитель не едет. К
|
||||||
|
читателю копии едет короткое `READING.md`: словарь со значениями, форма
|
||||||
|
правила и её граница, ссылки, локальная часть — без разделов о ведении набора.
|
||||||
|
Словарь в двух документах обязан совпадать (META-30), и это единственное
|
||||||
|
место, где между ними возможен дрейф.
|
||||||
|
|
||||||
|
Описание языка ни на один набор конвенций не опирается, поэтому все примеры
|
||||||
|
здесь — вымышленные правила с префиксами на `X` (`XKEY`, `XMIG`, `XLOG`).
|
||||||
|
Такие префиксы канон не занимает никогда, в манифесте набора их нет — значит
|
||||||
|
пример не спутать с настоящим правилом, а перенумерация конвенций описание
|
||||||
|
языка не задевает.
|
||||||
|
|
||||||
## Опора на стандарты
|
## Опора на стандарты
|
||||||
|
|
||||||
Язык не выводится из вкуса автора. Каждое решение о форме взято из
|
Язык не выводится из вкуса автора. Каждое решение о форме взято из
|
||||||
@@ -22,10 +34,10 @@ 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` как общая форма записи |
|
||||||
|
|
||||||
Три отклонения стоят объяснения, потому что выглядят как произвол.
|
Три отклонения стоят объяснения, потому что выглядят как произвол.
|
||||||
|
|
||||||
@@ -40,26 +52,54 @@ version: 1
|
|||||||
контракт, которого конвенция не даёт. Для англоязычного словаря это означает
|
контракт, которого конвенция не даёт. Для англоязычного словаря это означает
|
||||||
выбор в пользу `MUST` из BCP 14, а не `shall` из ISO/IEC Directives.
|
выбор в пользу `MUST` из BCP 14, а не `shall` из ISO/IEC Directives.
|
||||||
|
|
||||||
**`GIVEN/WHEN/THEN` не берётся.** У спецификации субъект — система, и её
|
**`GIVEN/WHEN/THEN` не берётся как общая форма.** У спецификации субъект —
|
||||||
поведение разворачивается во времени: состояние, событие, исход. У конвенции
|
система, и её поведение разворачивается во времени: состояние, событие,
|
||||||
субъект — автор кода, и разворачивать нечего: есть ситуация выбора и вердикт.
|
исход. У конвенции субъект — автор кода, и разворачивать нечего: есть
|
||||||
Это таблица, а не траектория. Тем же рассуждением отклонены шаблоны EARS с
|
ситуация выбора и вердикт. Это таблица, а не траектория. Тем же рассуждением
|
||||||
субъектом-системой, а взят из EARS другой результат: измеримый выигрыш дала
|
отклонены шаблоны EARS как форма записи правила, а взято из EARS другое —
|
||||||
там сама обязательность шаблона, а не его конкретная форма.
|
сообщённое снижение числа дефектов после введения шаблонов. Вывод, что дело в
|
||||||
|
самой обязательности формы, а не в её конкретном виде, наш; он ниже, среди
|
||||||
|
усилений.
|
||||||
|
|
||||||
|
Исключение — стык правил, где субъект действительно система: там форма
|
||||||
|
«условие → следствие» берётся сознательно, вместе со служебными словами под
|
||||||
|
неё. Это единственное место, и оно описано в «Таблицах решений».
|
||||||
|
|
||||||
|
## Где источник усилен
|
||||||
|
|
||||||
|
Три решения идут дальше источника, и это наши решения, а не его требования.
|
||||||
|
Названы они отдельно, чтобы довод не подменялся ссылкой: спорить с ними нужно
|
||||||
|
по существу, а не со стандартом.
|
||||||
|
|
||||||
|
- **Обоснование обязательно.** В 29148 rationale — из списка рекомендуемых
|
||||||
|
атрибутов требования; обязательный костяк там другой, это характеристики
|
||||||
|
самого требования. Здесь правило без блока ПОЧЕМУ не принимается, потому что
|
||||||
|
конвенция живёт годами и переживает автора: норма без причины через год либо
|
||||||
|
отменяется первым возражением, либо соблюдается там, где вредит.
|
||||||
|
- **Полнота таблицы решений.** DMN даёт политику совпадения как именованный
|
||||||
|
атрибут, а полноты не требует: индикатор полноты был в первой версии
|
||||||
|
спецификации и из последующих убран, полноту проверяют валидаторы
|
||||||
|
инструментов. Здесь она требуется, потому что таблицу и заводят ради
|
||||||
|
видимости пропуска: неперечисленный случай в прозе не виден, а пустая
|
||||||
|
клетка видна.
|
||||||
|
- **Вывод про обязательность шаблона.** В EARS сообщается о снижении числа
|
||||||
|
дефектов в требованиях после введения шаблонов. Вывод, что выигрыш даёт сама
|
||||||
|
обязательность формы, а не её конкретный вид, — наш: он объясняет, почему мы
|
||||||
|
берём из EARS результат, но не берём сами шаблоны.
|
||||||
|
|
||||||
## Что даёт формализация
|
## Что даёт формализация
|
||||||
|
|
||||||
Адресуемое правило — не украшение формы, а условие работы трёх механизмов:
|
Адресуемое правило — не украшение формы, а условие работы трёх механизмов:
|
||||||
|
|
||||||
- **Механизация.** Запись о ней должна говорить «правило `MIGR-4` проверяет
|
- **Механизация.** Запись о ней должна говорить «правило `XMIG-4` проверяет
|
||||||
`archrules`», а не «`AUTOINCREMENT` в новых миграциях — `archrules`»: во
|
`archrules`», а не «`AUTOINCREMENT` в новых миграциях — `archrules`»: во
|
||||||
втором случае читатель сам догадывается, к какому утверждению это
|
втором случае читатель сам догадывается, к какому утверждению это
|
||||||
относится, и догадывается по-разному.
|
относится, и догадывается по-разному.
|
||||||
- **Отступления.** «У нас не так» бесполезно, пока не сказано, что именно
|
- **Отступления.** «У нас не так» бесполезно, пока не сказано, что именно
|
||||||
«не так». Со ссылкой на правило отступления становятся счётными: видно,
|
«не так». Со ссылкой на правило отступления становятся счётными: видно,
|
||||||
сколько правил конвенции репозиторий реально не соблюдает.
|
сколько правил конвенции репозиторий реально не соблюдает.
|
||||||
- **Промоут находки.** Путь «находка → конвенция → правило линтера →
|
- **Промоут находки.** Путь «находка → конвенция → правило линтера» требует
|
||||||
удаление прозы» требует ручки, за которую берут конкретное правило.
|
ручки, за которую берут конкретное правило.
|
||||||
|
|
||||||
Общий знаменатель — **идентификатор**. Модальные слова и таблицы полезны,
|
Общий знаменатель — **идентификатор**. Модальные слова и таблицы полезны,
|
||||||
но вторичны.
|
но вторичны.
|
||||||
@@ -67,27 +107,123 @@ version: 1
|
|||||||
## Единица — правило
|
## Единица — правило
|
||||||
|
|
||||||
```markdown
|
```markdown
|
||||||
### KEYS-5. Разбор внешнего идентификатора на границе
|
### XKEY-5. Разбор внешнего идентификатора на границе
|
||||||
|
|
||||||
**ДОЛЖЕН.** Идентификатор, пришедший снаружи, проходит разбор до запроса
|
**ДОЛЖЕН.** Идентификатор, пришедший снаружи, проходит разбор до запроса
|
||||||
к базе.
|
к базе.
|
||||||
|
|
||||||
**Почему.** Разбор валидирует формат и нормализует регистр. Сравнение строк
|
**ПОЧЕМУ.** Разбор валидирует формат и нормализует регистр. Сравнение строк
|
||||||
в базе побайтовое, поэтому без нормализации запрос молча не находит
|
в базе побайтовое, поэтому без нормализации запрос молча не находит
|
||||||
существующую запись — отладка такого случая стоит дороже, чем сам разбор.
|
существующую запись — отладка такого случая стоит дороже, чем сам разбор.
|
||||||
```
|
```
|
||||||
|
|
||||||
Четыре обязательные части: **идентификатор**, **заголовок**, **модальность
|
Четыре обязательные части: **идентификатор**, **заголовок**, **модальность
|
||||||
с нормой**, **обоснование**. Норма — одна фраза; если в неё не влезает, это
|
с нормой**, **обоснование под меткой ПОЧЕМУ**. Пятый блок, ПРИМЕРЫ,
|
||||||
два правила. Требование единичности взято из 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 обоснование — отдельный атрибут требования, но из
|
||||||
требованием, и по тем же причинам:
|
рекомендуемых; здесь оно обязательно, и вот почему:
|
||||||
|
|
||||||
- **Обоснование — единственный способ увидеть, что правило устарело.**
|
- **Обоснование — единственный способ увидеть, что правило устарело.**
|
||||||
Норма стареет молча; причина стареет заметно. Когда причина отпала, видно,
|
Норма стареет молча; причина стареет заметно. Когда причина отпала, видно,
|
||||||
@@ -99,8 +235,18 @@ version: 1
|
|||||||
Если причина не формулируется, перед нами привычка или вкусовщина; ей
|
Если причина не формулируется, перед нами привычка или вкусовщина; ей
|
||||||
место в черновиках, а не в конвенции.
|
место в черновиках, а не в конвенции.
|
||||||
|
|
||||||
«Почему» отвечает на «что сломается, если сделать иначе», а не пересказывает
|
Обоснование отвечает на «что сломается, если сделать иначе», а не
|
||||||
норму другими словами. «Потому что так принято» — не обоснование.
|
пересказывает норму другими словами. «Потому что так принято» — не
|
||||||
|
обоснование.
|
||||||
|
|
||||||
|
Форма обоснования при этом ничем не ограничена: рамки здесь только
|
||||||
|
смысловые. Абзац может быть длинным, вести рассуждение, приводить пример,
|
||||||
|
ссылаться на стандарты, внешние практики и чужие проекты — на устройство
|
||||||
|
OpenTelemetry, на умолчания библиотек логирования, на процедуру миграции из
|
||||||
|
документации СУБД. Запрещённых слов и обязательной
|
||||||
|
структуры у обоснования нет, и заводить их не нужно: обязательность несёт
|
||||||
|
норма, а обоснование её объясняет — путаницу между этими двумя ролями
|
||||||
|
исключает правило о заглавных.
|
||||||
|
|
||||||
## Модальные слова
|
## Модальные слова
|
||||||
|
|
||||||
@@ -140,27 +286,31 @@ Directives, Part 2, по одной форме записи на ступень,
|
|||||||
|
|
||||||
**ДОЛЖЕН требует двух условий сразу:**
|
**ДОЛЖЕН требует двух условий сразу:**
|
||||||
|
|
||||||
1. нарушение причиняет названный вред, а не расходится со вкусом — критерий
|
1. нарушение причиняет названный вред, а не расходится со вкусом — META-25,
|
||||||
BCP 14, где высшая модальность резервируется под то, что действительно
|
он же критерий BCP 14, где высшая модальность резервируется под то, что
|
||||||
ломается, и не употребляется для навязывания метода;
|
действительно ломается, и не употребляется для навязывания метода;
|
||||||
2. норма проверяема машиной — критерий META-6, иначе обязательность
|
2. вердикт о нарушении воспроизводим — META-6: по тексту правила двое
|
||||||
держится на внимании и обещает то, чего не делает.
|
проверяющих приходят к одному ответу, иначе обязательность держится на
|
||||||
|
том, кто читал.
|
||||||
|
|
||||||
Не выполнено первое — правилу место в СЛЕДУЕТ или нигде. Не выполнено
|
Не выполнено первое — правилу место в СЛЕДУЕТ или нигде. Не выполнено
|
||||||
второе — в СЛЕДУЕТ. Проверяемость сама по себе не повышает правило до
|
второе — в СЛЕДУЕТ. Воспроизводимость сама по себе не повышает правило до
|
||||||
ДОЛЖЕН: механически проверяемых мелочей больше, чем важных вещей, и
|
ДОЛЖЕН: проверяемых мелочей больше, чем важных вещей, и безразборное
|
||||||
безразборное повышение обесценивает шкалу быстрее, чем её отсутствие.
|
повышение обесценивает шкалу быстрее, чем её отсутствие.
|
||||||
|
|
||||||
Модальность живёт на **правиле**, а не на файле. Файловый статус
|
Модальность живёт на **правиле**, а не на файле. Файловый статус
|
||||||
(`status: рекомендуемая` / `обязательная` в шапке) не используется: он
|
(`status: рекомендуемая` / `обязательная` в шапке) не используется: он
|
||||||
неизбежно врёт, потому что один файл смешивает жёсткие требования с
|
неизбежно врёт, потому что один файл смешивает жёсткие требования с
|
||||||
советами. В шапке остаются только `prefix` и `extends`.
|
советами. В шапке остаются только `topic`, `prefix` и `extends`.
|
||||||
|
|
||||||
## Словарь другого языка
|
## Словарь другого языка
|
||||||
|
|
||||||
Для английского готовый словарь даёт BCP 14. Для любого другого языка слова
|
Словарь набора — три перечня, и требования к ним одни и те же.
|
||||||
берут из перевода стандарта, если он есть, или переводят сами: шкала и
|
|
||||||
семантика ступеней при этом не меняются — меняется только запись.
|
**Шкала обязательности.** Для английского готовый словарь даёт BCP 14; для
|
||||||
|
любого другого языка слова берут из перевода стандарта, если он есть, или
|
||||||
|
переводят сами. Шкала и семантика ступеней при этом не меняются — меняется
|
||||||
|
только запись.
|
||||||
|
|
||||||
| Ступень | Русский | Английский (BCP 14) |
|
| Ступень | Русский | Английский (BCP 14) |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
@@ -169,43 +319,54 @@ Directives, Part 2, по одной форме записи на ступень,
|
|||||||
| рекомендация | СЛЕДУЕТ | SHOULD |
|
| рекомендация | СЛЕДУЕТ | SHOULD |
|
||||||
| рекомендация против | НЕ СЛЕДУЕТ | SHOULD NOT |
|
| рекомендация против | НЕ СЛЕДУЕТ | SHOULD NOT |
|
||||||
| разрешение | ДОПУСКАЕТСЯ | MAY |
|
| разрешение | ДОПУСКАЕТСЯ | MAY |
|
||||||
| отметка о способе проверки | МЕХАНИЗИРОВАНО | MECHANIZED |
|
|
||||||
|
|
||||||
Последняя строка стандартом не даётся ни в одном языке: способа проверки в
|
**Метки.** Обязательности не задают, а размечают: ПОЧЕМУ — обоснование,
|
||||||
шкале BCP 14 нет, слово подбирается под язык так же, как остальные.
|
ПРИМЕРЫ — иллюстрации к норме, МЕХАНИЗИРОВАНО — запись о проверке в копии,
|
||||||
|
СНЯТО — заглушку на месте убранного правила. Стандартом не даются ни в одном
|
||||||
|
языке: в BCP 14 таких понятий нет, слова подбираются под язык так же, как
|
||||||
|
остальные.
|
||||||
|
|
||||||
|
| Метка | Русский | Английский |
|
||||||
|
|---|---|---|
|
||||||
|
| обоснование | ПОЧЕМУ | WHY |
|
||||||
|
| иллюстрации | ПРИМЕРЫ | EXAMPLES |
|
||||||
|
| способ проверки | МЕХАНИЗИРОВАНО | MECHANIZED |
|
||||||
|
| снятое правило | СНЯТО | RETIRED |
|
||||||
|
|
||||||
|
**Служебные слова сценарного блока** — `КОГДА`, `ТОГДА`, `И`, `ИЛИ`; таблица
|
||||||
|
и объяснение в разделе «Таблицы решений».
|
||||||
|
|
||||||
Что требуется от любого словаря:
|
Что требуется от любого словаря:
|
||||||
|
|
||||||
- **одна форма на ступень.** Синонимы отклонены не из аскетизма: проверка
|
- **одна форма на ступень и на метку.** Синонимы отклонены не из аскетизма:
|
||||||
«модальное слово вне правила» перечисляет формы, и синонимический ряд
|
проверка «модальное слово вне правила» перечисляет формы, и синонимический
|
||||||
превращает перечисление в разбор.
|
ряд превращает перечисление в разбор.
|
||||||
- **слово заглавными не встречается в обычной прозе этого языка.** Иначе
|
- **слово заглавными не встречается в обычной прозе этого языка.** Иначе
|
||||||
правило «нормативно только заглавное» перестаёт спасать: проверка ловит
|
правило «нормативно только заглавное» перестаёт спасать: проверка ловит
|
||||||
оформление, а не модальность.
|
оформление, а не модальность.
|
||||||
- **словарь перечислен целиком в строке о версии языка.** Читателю копии он
|
- **модальные слова и метки перечислены в строке о версии языка.** Читателю
|
||||||
известен из самого файла, без обращения к этому документу, — иначе
|
копии они известны из самого файла, без обращения к этому документу, —
|
||||||
конвенция в чужом репозитории теряет ключ к собственному тексту.
|
иначе конвенция в чужом репозитории теряет ключ к собственному тексту.
|
||||||
|
Служебные слова сценария в строку не входят: структура блока читается из
|
||||||
|
самого блока, и в файле без стыков правил их нет вовсе.
|
||||||
- **словарь один на канон.** Два словаря параллельно дают две формы записи
|
- **словарь один на канон.** Два словаря параллельно дают две формы записи
|
||||||
одного требования и удваивают каждую проверку; выбор языка — свойство
|
одного требования и удваивают каждую проверку; выбор языка — свойство
|
||||||
набора, а не отдельного файла.
|
набора, а не отдельного файла.
|
||||||
|
|
||||||
Смена словаря версию языка не меняет: версия принадлежит шкале и правилам
|
|
||||||
формы, а не буквам.
|
|
||||||
|
|
||||||
## Ссылка на язык из конвенции
|
## Ссылка на язык из конвенции
|
||||||
|
|
||||||
Каждая конвенция называет язык одной строкой во вводной прозе:
|
Каждая конвенция называет язык одной строкой во вводной прозе:
|
||||||
|
|
||||||
> Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и
|
> Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
> отметка МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 —
|
> ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
> тогда и только тогда, когда написаны заглавными.
|
> конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
Слова в строке — из словаря того языка, на котором написан набор. Для
|
Слова в строке — из словаря того языка, на котором написан набор. Для
|
||||||
англоязычного набора та же строка выглядит так:
|
англоязычного набора та же строка выглядит так:
|
||||||
|
|
||||||
> The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY and the mark
|
> The key words MUST, MUST NOT, SHOULD, SHOULD NOT, MAY and the marks WHY,
|
||||||
> MECHANIZED are to be interpreted as described in the conventions language,
|
> EXAMPLES, MECHANIZED and RETIRED are to be interpreted as described in the
|
||||||
> version 1, and only when written in capitals.
|
> conventions language, version 1, and only when written in capitals.
|
||||||
|
|
||||||
Форма скопирована у BCP 14, где та же задача решается тем же способом:
|
Форма скопирована у BCP 14, где та же задача решается тем же способом:
|
||||||
спецификация не прикладывает к себе словарь и не указывает путь к нему, а
|
спецификация не прикладывает к себе словарь и не указывает путь к нему, а
|
||||||
@@ -220,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, второе мы добавили сами («Где источник усилен»):
|
||||||
|
|
||||||
- **Политика совпадения.** По умолчанию строки взаимоисключающи: любой
|
- **Политика совпадения.** По умолчанию строки взаимоисключающи: любой
|
||||||
ситуации соответствует ровно одна. Если это не так, таблица объявляет
|
ситуации соответствует ровно одна. Если это не так, таблица объявляет
|
||||||
@@ -264,32 +440,63 @@ Directives, Part 2, по одной форме записи на ступень,
|
|||||||
возможен случай вне перечисленных, он назван отдельной строкой, а не
|
возможен случай вне перечисленных, он назван отдельной строкой, а не
|
||||||
оставлен на догадку.
|
оставлен на догадку.
|
||||||
|
|
||||||
Прозаический сценарий остаётся точечным инструментом — для **стыка правил**,
|
Сценарный блок остаётся точечным инструментом — для **стыка правил**, когда
|
||||||
когда два правила вместе дают неочевидный результат:
|
два правила вместе дают неочевидный результат:
|
||||||
|
|
||||||
```
|
```
|
||||||
WHEN зависимость недоступна и ретраи вызова исчерпаны → ext-запись ERROR
|
КОГДА зависимость недоступна И ретраи вызова исчерпаны
|
||||||
AND тик фонового цикла упал по той же причине → доменная запись WARN
|
ТОГДА внешний вызов даёт запись ERROR,
|
||||||
|
И тик фонового цикла, упавший по той же причине, — запись WARN
|
||||||
```
|
```
|
||||||
|
|
||||||
Такой блок ставится после обоих правил и ссылается на их идентификаторы.
|
Такой блок ставится после обоих правил и ссылается на их идентификаторы.
|
||||||
Если стыков нет — сценариев в файле нет.
|
Если стыков нет — сценариев в файле нет.
|
||||||
|
|
||||||
|
Форма «условие → следствие» здесь взята намеренно, хотя как **общая** форма
|
||||||
|
записи она отклонена: на стыке правил субъект действительно система, и
|
||||||
|
результат разворачивается во времени — то самое, для чего эта форма и
|
||||||
|
придумана. Служебные слова блока перечислены ниже и подчиняются тем же
|
||||||
|
требованиям, что модальные: одна форма на роль, заглавными, набор один на
|
||||||
|
канон.
|
||||||
|
|
||||||
|
| Роль | Русский | Английский |
|
||||||
|
|---|---|---|
|
||||||
|
| условие | КОГДА | WHEN |
|
||||||
|
| следствие | ТОГДА | THEN |
|
||||||
|
| соединение | И | AND |
|
||||||
|
| выбор | ИЛИ | OR |
|
||||||
|
|
||||||
|
Модальными словами они не являются: обязательности не задают, только
|
||||||
|
структуру. Поэтому в строку о версии языка они не попадают — там
|
||||||
|
перечисляется то, чему нужно определение, а логическая связка читается сама,
|
||||||
|
— и под проверку «модальные слова вне правил» не подпадают.
|
||||||
|
|
||||||
## Идентификаторы
|
## Идентификаторы
|
||||||
|
|
||||||
- Формат — `<ПРЕФИКС>-<номер>`: `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). Отдельного реестра снятых
|
||||||
|
номеров нет: он был бы вторым источником правды рядом с файлом.
|
||||||
- Префикс **выбирается под файл, а не выводится по формуле**: он нужен,
|
- Префикс **выбирается под файл, а не выводится по формуле**: он нужен,
|
||||||
чтобы по нему искать, а не чтобы его разбирать. Выводимый префикс вдобавок
|
чтобы по нему искать, а не чтобы его разбирать. Выводимый префикс вдобавок
|
||||||
привязал бы идентификатор к таксономии, которую канон перестраивает, и
|
привязал бы идентификатор к таксономии, которую канон перестраивает, и
|
||||||
@@ -303,6 +510,25 @@ AND тик фонового цикла упал по той же причине
|
|||||||
Порядок правил в файле выбирается по читаемости, не по номерам: номер — это
|
Порядок правил в файле выбирается по читаемости, не по номерам: номер — это
|
||||||
идентификатор, а не позиция.
|
идентификатор, а не позиция.
|
||||||
|
|
||||||
|
**Тема.**
|
||||||
|
|
||||||
|
- **Тема — набор правил об одном фокусе разработки:** время, конфигурация,
|
||||||
|
схема БД. Она же — единица подписки: потребитель берёт тему целиком, а не
|
||||||
|
отдельные правила.
|
||||||
|
- Имя темы записывается латиницей. Рекомендуется нижний kebab-case
|
||||||
|
(`db-identifiers`), но годится любой идентификатор, пригодный для имени
|
||||||
|
файла: имя попадает и в файловую систему потребителя, и в его манифест.
|
||||||
|
- **Тему объявляет файл, а не файловая система.** Имя стоит в шапке
|
||||||
|
(`topic:`) и зарегистрировано в манифесте набора. Слои одной темы несут
|
||||||
|
одно и то же имя — по нему они и собираются в один документ, как бы ни
|
||||||
|
назывались их файлы.
|
||||||
|
- **Имя темы не переиспользуется** — как и префикс: оно живёт в шапке
|
||||||
|
`origin:` каждой копии, в подписке манифеста и в тексте ссылок, поэтому
|
||||||
|
снятое имя уходит в раздел выбывших манифеста, а не достаётся другой теме.
|
||||||
|
- Ссылка на конвенцию — имя темы (конвенция `logging`), ссылка на правило —
|
||||||
|
идентификатор (`XLOG-27`). Путь файла не употребляется ни там, ни там: в
|
||||||
|
собранной копии путей канона не существует.
|
||||||
|
|
||||||
## Что правилом не является
|
## Что правилом не является
|
||||||
|
|
||||||
Заглавные модальные слова в этих частях **не употребляются** — иначе
|
Заглавные модальные слова в этих частях **не употребляются** — иначе
|
||||||
@@ -315,6 +541,10 @@ AND тик фонового цикла упал по той же причине
|
|||||||
- **Локальная часть копии** — содержимое принадлежит репозиторию.
|
- **Локальная часть копии** — содержимое принадлежит репозиторию.
|
||||||
- Вводная проза, объясняющая предмет конвенции.
|
- Вводная проза, объясняющая предмет конвенции.
|
||||||
|
|
||||||
|
Все четыре части лежат вне областей правил: до первого заголовка правила или
|
||||||
|
после заголовка, которым область закрылась. Хвост обоснования сюда не
|
||||||
|
относится — он внутри правила, и модальные слова в нём законны как упоминания.
|
||||||
|
|
||||||
## Как на правила ссылаются копии
|
## Как на правила ссылаются копии
|
||||||
|
|
||||||
Ниже маркера локальной части, в репозитории:
|
Ниже маркера локальной части, в репозитории:
|
||||||
@@ -322,10 +552,9 @@ AND тик фонового цикла упал по той же причине
|
|||||||
```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`: составные
|
||||||
ключи там появились до конвенции, переписывание требует миграции данных.
|
ключи там появились до конвенции, переписывание требует миграции данных.
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -334,35 +563,83 @@ 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-21).
|
- путь файла канона не встречается в тексте конвенции (META-21).
|
||||||
|
|
||||||
Чтением, потому что машине не даётся:
|
**Чтением**, потому что машине не даётся:
|
||||||
|
|
||||||
- строки таблицы взаимоисключающи либо политика совпадения объявлена;
|
- строки таблицы взаимоисключающи либо политика совпадения объявлена;
|
||||||
- перечисленные в таблице случаи покрывают область действия;
|
- перечисленные в таблице случаи покрывают область действия;
|
||||||
- норма исполнима без обращения к другим файлам;
|
- норма исполнима без обращения к другим файлам (в файлах конвенций: они
|
||||||
- обоснование отвечает на «что сломается», а не пересказывает норму.
|
уезжают по одной, а обвязка ссылается на соседей свободно);
|
||||||
|
- обоснование отвечает на «что сломается», а не пересказывает норму;
|
||||||
|
- хвост обоснования не вводит требований, которых нет в блоке нормы;
|
||||||
|
- примеры иллюстрируют норму и не расширяют её: деталь примера не читается
|
||||||
|
как требование.
|
||||||
|
|
||||||
|
Проверки выше — про запись правила. Граница самой темы (не собрала ли она
|
||||||
|
два решения сразу, нужна ли потребителю целиком) языку не принадлежит: её
|
||||||
|
вопросы стоят в документе, которым набор ведёт себя.
|
||||||
|
|
||||||
|
## Версия языка
|
||||||
|
|
||||||
|
Номер версии называется в каждой конвенции, поэтому он двигается, когда
|
||||||
|
изменение формы способно изменить чтение **уже разданной** копии: копия
|
||||||
|
ссылается на номер, а не на текст, и обязана читаться по той версии, по
|
||||||
|
которой написана. Правки формы до того, как копии разошлись, номер не двигают
|
||||||
|
— читать по ним пока нечего.
|
||||||
|
|
||||||
|
Смена словаря под другой естественный язык версию не двигает никогда: версия
|
||||||
|
принадлежит шкале, меткам и правилам формы, а не буквам.
|
||||||
|
|||||||
+134
@@ -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. Полное описание живёт в наборе конвенций, у автора;
|
||||||
|
здесь ровно то, что нужно читателю.
|
||||||
@@ -10,15 +10,16 @@
|
|||||||
| Файл | Что описывает |
|
| Файл | Что описывает |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `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`. Двенадцать файлов канона всё ещё несут 31 пустой
|
именованными регионами и `origin_hash` — удалён вместе со своей моделью.
|
||||||
именованный регион. Ни один репозиторий-потребитель не подключён, поэтому
|
|
||||||
переход никого не ломает.
|
Ни один репозиторий-потребитель ещё не подключён: копий с шапкой `origin:` в
|
||||||
|
природе нет. Пока это так, непроверенным остаётся главное — как всё это живёт
|
||||||
|
в чужом репозитории через полгода после первой сборки.
|
||||||
|
|||||||
@@ -4,179 +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. Именованные регионы — удалить из канона
|
|
||||||
|
|
||||||
Решение принято и записано: регионов нет, есть один маркер
|
|
||||||
`<!-- conv:local -->`, который ставит сборщик в копии. Значит регионы не
|
|
||||||
переносятся в единую секцию, а **удаляются**: в каноне их нечем наполнять,
|
|
||||||
локальное принадлежит копии.
|
|
||||||
|
|
||||||
Остаётся механическая работа — **31 регион в 12 файлах**:
|
|
||||||
|
|
||||||
```
|
|
||||||
7 связано 7 отступления 7 механизировано
|
|
||||||
1 эталон / эталоны / словарь / секреты / секретные-поля
|
|
||||||
1 проверки / поля / модель-владельца / маппинг / границы
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Пары слоёв и темы без базы
|
|
||||||
|
|
||||||
Отложено сознательно, но список стоит держать перед глазами:
|
|
||||||
|
|
||||||
- `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-слой — единственные ссылки стек → язык в каноне.
|
|
||||||
|
|
||||||
## 4. Подключение к репозиториям
|
|
||||||
|
|
||||||
Ничего ещё не подключено. Кандидаты — jellybit и pet-project-server.
|
|
||||||
Понадобится: заполнить локальную часть копий тем, что сейчас в этих
|
|
||||||
репозиториях записано по факту; обёртка в раннере (`inv conventions` /
|
|
||||||
`task conventions`, единый интерфейс команд у трёх ansible-репозиториев);
|
|
||||||
строка в `AGENTS.md` каждого потребителя про то, что файлы в
|
|
||||||
`docs/conventions/` — копии.
|
|
||||||
|
|
||||||
## 5. Описание языка отдельно от набора конвенций
|
|
||||||
|
|
||||||
`LANGUAGE.md` и `GUIDE.md` описывают, **как** пишутся конвенции;
|
|
||||||
`conventions/` — **один конкретный** набор. Сейчас они склеены в одном
|
|
||||||
репозитории, и из одного описания нельзя собрать второй набор (рабочий,
|
|
||||||
доменный, чужой).
|
|
||||||
|
|
||||||
Срочности нет: версия языка объявлена в самом `LANGUAGE.md` (ключ
|
|
||||||
`version:`), и конвенции ссылаются на неё номером, а не путём, — то есть
|
|
||||||
самодостаточность копии выноса не требует.
|
|
||||||
|
|
||||||
Что осталось поводом:
|
|
||||||
|
|
||||||
- из одного описания по-прежнему нельзя собрать второй набор;
|
|
||||||
- тулинг валидирует правила, зашитые в его код, а не объявленную версию
|
|
||||||
языка.
|
|
||||||
|
|
||||||
Оба повода включаются, только когда появится второй набор. Цена — ещё одна
|
|
||||||
сущность и ещё одна синхронизация; при одном наборе лечение выходит хуже
|
|
||||||
болезни.
|
|
||||||
|
|
||||||
## 6. Тулинг на Go, живущий независимо
|
|
||||||
|
|
||||||
Сейчас `conv` — питоновский скрипт внутри канона, то есть тулинг и данные в
|
|
||||||
одном репозитории и правятся одним движением. Мысль: вынести в отдельный
|
|
||||||
Go-бинарь со своим релизным циклом, ставить через `eget` (механизм уже есть
|
|
||||||
в pet-project-server).
|
|
||||||
|
|
||||||
Что за этим стоит помимо вкуса: независимый бинарь физически не даёт править
|
|
||||||
инструмент «заодно» с правкой конвенции, работает против **любого** канона и
|
|
||||||
любого потребителя — что прямо требуется вопросом 5, — и снимает питон из
|
|
||||||
зависимостей репозиториев-потребителей.
|
|
||||||
|
|
||||||
Порядок обратный ожидаемому: пока вопрос 5 не сделан, инструмент всё равно
|
|
||||||
работает против одного конкретного канона, и независимый релизный цикл ему
|
|
||||||
нечего обслуживать. Сначала 5, потом 6. Разделение из вопроса 1 при этом
|
|
||||||
дешевле заложить сразу, чем отпиливать потом.
|
|
||||||
|
|
||||||
## 7. META-20 и родительский слой своей темы
|
|
||||||
|
|
||||||
Из `lang/` и `stack/` ссылаться на идентификаторы своего же арх-слоя
|
|
||||||
безопасно: при сборке они оказываются секциями одного файла, и ссылка
|
|
||||||
никуда не ведёт — правило рядом. Ограничение META-20 писалось про **чужую
|
|
||||||
тему**, так что формально это уже разрешено, и формулировка проверки в
|
|
||||||
`LANGUAGE.md` под это исправлена.
|
|
||||||
|
|
||||||
Осталось решить одно: добавлять ли в META-20 явную строку **ДОПУСКАЕТСЯ**
|
|
||||||
про родительский слой. За — правило, которое читают строже, чем оно есть,
|
|
||||||
заставляет авторов дублировать текст без нужды. Против — норма META-20 уже
|
|
||||||
говорит «чужой темы», и второе правило про то же место придётся держать
|
|
||||||
согласованным с первым.
|
|
||||||
|
|
||||||
## 8. `WHEN`/`AND` в блоке стыка правил
|
|
||||||
|
|
||||||
Блок для стыка двух правил записан английскими словами:
|
|
||||||
|
|
||||||
```
|
|
||||||
WHEN зависимость недоступна и ретраи вызова исчерпаны → ext-запись ERROR
|
|
||||||
AND тик фонового цикла упал по той же причине → доменная запись WARN
|
|
||||||
```
|
|
||||||
|
|
||||||
Рядом сказано, что `SHALL` не берётся ни в один словарь, потому что занят
|
|
||||||
OpenSpec. `WHEN` и `AND` — из того же набора и по той же причине должны бы
|
|
||||||
не браться, но взяты. Это нестыковка, а не решение.
|
|
||||||
|
|
||||||
Варианты:
|
|
||||||
|
|
||||||
- **перевести** на `КОГДА` / `И`: словарь набора один, и служебные слова
|
|
||||||
внутри канона следуют ему же;
|
|
||||||
- **оставить и объяснить**: блок стыка описывает поведение системы во
|
|
||||||
времени, а не выбор автора, — то есть это единственное место, где форма
|
|
||||||
спецификации уместна, и заимствование её синтаксиса намеренно.
|
|
||||||
|
|
||||||
Второе честнее по смыслу (субъект там действительно система), но требует
|
|
||||||
явной оговорки в `LANGUAGE.md`, иначе читается как недосмотр. Заодно решить,
|
|
||||||
подпадают ли `WHEN`/`AND` под проверку «заглавные модальные слова не
|
|
||||||
встречаются вне правил»: сейчас формально нет, потому что в словаре их нет.
|
|
||||||
|
|
||||||
## 9. Критерий «названного вреда» никого не обязывает
|
|
||||||
|
|
||||||
`LANGUAGE.md` говорит, что ДОЛЖЕН требует двух условий сразу: нарушение
|
|
||||||
причиняет названный вред (критерий BCP 14) и норма проверяема машиной
|
|
||||||
(META-6). Но правила под первое условие нет: META-6 работает только в одну
|
|
||||||
сторону — «нет машинной проверки, понижай в СЛЕДУЕТ». Обратной, «проверка
|
|
||||||
есть, а вреда нет — не повышай», не существует, и автор ничем не связан.
|
|
||||||
|
|
||||||
Решить, заводить ли META-24 под первое условие или оставить его семантикой
|
|
||||||
шкалы в описании языка. За правило: механически проверяемых мелочей больше,
|
|
||||||
чем важных вещей, и без нормы шкала размывается тем же способом, от которого
|
|
||||||
META-6 её защищает. Против: критерий «вред назван» проверяется чтением, а не
|
|
||||||
машиной, — то есть по META-6 сам он может быть только СЛЕДУЕТ.
|
|
||||||
|
|
||||||
## 10. Одиннадцать таблиц не прочитаны на взаимоисключительность
|
|
||||||
|
|
||||||
`LANGUAGE.md` объявил, что строки таблицы решений взаимоисключающи по
|
`LANGUAGE.md` объявил, что строки таблицы решений взаимоисключающи по
|
||||||
умолчанию, а иной порядок объявляется явно. Объявление не делает таблицы
|
умолчанию, а иной порядок объявляется явно. Объявление не делает таблицы
|
||||||
@@ -191,19 +26,119 @@ META-6 её защищает. Против: критерий «вред назв
|
|||||||
Работа читательская, машине не даётся; в список проверок она уже записана в
|
Работа читательская, машине не даётся; в список проверок она уже записана в
|
||||||
разделе «Чтением, потому что машине не даётся».
|
разделе «Чтением, потому что машине не даётся».
|
||||||
|
|
||||||
## 11. Возможность, записанная модальным словом
|
## 2. Шесть сниппетов сидят в блоке нормы
|
||||||
|
|
||||||
Четвёртая категория ISO — возможность и осуществимость — ключевого слова не
|
С появлением блока ПРИМЕРЫ у кода в правиле есть своё место, но шесть правил
|
||||||
имеет: такие утверждения пишутся обычной прозой. Значит канон надо просмотреть
|
несут сниппет **внутри блока нормы** — там, где он по границе правила читается
|
||||||
на обратную ошибку: где утверждение о факте («библиотеки по умолчанию отдают
|
как «требуется ровно такой код»: GCFG-7, GCFG-9, SLOG-20, GTIM-8, HTMX-7,
|
||||||
именно его», «SQLite сравнивает строки побайтово») записано модальным словом
|
HTMX-24. Кода внутри обоснований в каноне нет ни одного, так что разбирать
|
||||||
и тем самым превратилось в норму, которую никто не вводил.
|
нужно только эти шесть.
|
||||||
|
|
||||||
Смотреть в первую очередь абзацы «Почему»: там факты и стоят, там же соблазн
|
Разбор по одному, вердикт из двух: сниппет — часть требования или иллюстрация
|
||||||
усилить их модальностью выше всего.
|
к нему. У GTIM-8 (`ReplaceAttr` с приведением к UTC) это похоже на норму: там
|
||||||
|
важна конкретная точка вмешательства. У HTMX-7 и SLOG-20 — скорее иллюстрация
|
||||||
|
формы вызова, и ей место в ПРИМЕРЫ.
|
||||||
|
|
||||||
## Мелкое, не закрыто
|
Цена ошибки в обе стороны понятна. Оставленный в норме пример превращает
|
||||||
|
деталь кода в требование, которое никто не имел в виду, и устаревает вместе с
|
||||||
|
API, а норму при этом нельзя поправить, не задев требование. Унесённая в
|
||||||
|
ПРИМЕРЫ норма, наоборот, перестаёт быть обязательной — блок иллюстративный.
|
||||||
|
|
||||||
- `conv check` должен уметь отличать ссылку на удалённое правило от
|
## 3. Описание языка отдельно от набора конвенций
|
||||||
упоминания дыры: сейчас `META-16` в прозе «Оформления» даёт ложное
|
|
||||||
срабатывание.
|
`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` каждого потребителя про то, что файлы в директориях
|
||||||
|
конвенций — копии. Компонент у обоих кандидатов один, но записывается явно.
|
||||||
|
|||||||
@@ -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())
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
topic: app-directories
|
||||||
prefix: DIRS
|
prefix: DIRS
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -10,9 +11,9 @@ prefix: DIRS
|
|||||||
**кто создаёт** содержимое и **что будет, если его потерять**. Из категорий
|
**кто создаёт** содержимое и **что будет, если его потерять**. Из категорий
|
||||||
механически выводится состав бэкапа.
|
механически выводится состав бэкапа.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
## Область действия
|
## Область действия
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ prefix: DIRS
|
|||||||
|
|
||||||
Имена в таблице — умолчание для случая «одна директория на категорию».
|
Имена в таблице — умолчание для случая «одна директория на категорию».
|
||||||
|
|
||||||
**Почему.** Все дальнейшие решения — что попадает в бэкап (DIRS-4), что можно
|
**ПОЧЕМУ.** Все дальнейшие решения — что попадает в бэкап (DIRS-4), что можно
|
||||||
снести при нехватке места, что переживает переезд на другой диск —
|
снести при нехватке места, что переживает переезд на другой диск —
|
||||||
читаются из категории, а не выясняются по коду приложения. Без единой
|
читаются из категории, а не выясняются по коду приложения. Без единой
|
||||||
классификации каждое такое решение принимается заново и каждый раз чуть
|
классификации каждое такое решение принимается заново и каждый раз чуть
|
||||||
@@ -49,7 +50,7 @@ prefix: DIRS
|
|||||||
**ДОПУСКАЕТСЯ.** Директорий в категории столько, сколько нужно раскладке;
|
**ДОПУСКАЕТСЯ.** Директорий в категории столько, сколько нужно раскладке;
|
||||||
принадлежность к категории задаётся не именем, а участием в списке бэкапа.
|
принадлежность к категории задаётся не именем, а участием в списке бэкапа.
|
||||||
|
|
||||||
**Почему.** Явное разрешение снимает вопрос, не читается ли DIRS-1 как «ровно
|
**ПОЧЕМУ.** Явное разрешение снимает вопрос, не читается ли DIRS-1 как «ровно
|
||||||
три директории». Крупные файлы отделяют от базы, чтобы двигать их между
|
три директории». Крупные файлы отделяют от базы, чтобы двигать их между
|
||||||
дисками независимо (`media/`, `uploads/` — та же категория «данные», что и
|
дисками независимо (`media/`, `uploads/` — та же категория «данные», что и
|
||||||
`data/`); запрет на такое деление вынуждал бы либо держать всё на одном
|
`data/`); запрет на такое деление вынуждал бы либо держать всё на одном
|
||||||
@@ -67,7 +68,7 @@ prefix: DIRS
|
|||||||
| DIRS-3.2 | миниатюры, распакованные ассеты, кеш внешних ответов, перестраиваемые индексы | кеш |
|
| DIRS-3.2 | миниатюры, распакованные ассеты, кеш внешних ответов, перестраиваемые индексы | кеш |
|
||||||
| DIRS-3.3 | спорный случай | `rm -rf` и запуск приложения заново: поднялось и наверстало само — кеш; не поднялось или поднялось пустым — данные |
|
| DIRS-3.3 | спорный случай | `rm -rf` и запуск приложения заново: поднялось и наверстало само — кеш; не поднялось или поднялось пустым — данные |
|
||||||
|
|
||||||
**Почему.** Без внешнего теста граница проводится по ощущению «жалко
|
**ПОЧЕМУ.** Без внешнего теста граница проводится по ощущению «жалко
|
||||||
потерять», а оно смещено в одну сторону: дорогой в пересборке кеш
|
потерять», а оно смещено в одну сторону: дорогой в пересборке кеш
|
||||||
переезжает в данные и раздувает каждый снапшот. Дорого ≠ невосполнимо, и
|
переезжает в данные и раздувает каждый снапшот. Дорого ≠ невосполнимо, и
|
||||||
разделяет эти два свойства именно способность приложения пересоздать
|
разделяет эти два свойства именно способность приложения пересоздать
|
||||||
@@ -80,7 +81,7 @@ prefix: DIRS
|
|||||||
**ДОЛЖЕН.** Директории категории «данные» — в списке бэкапа; конфигурация и
|
**ДОЛЖЕН.** Директории категории «данные» — в списке бэкапа; конфигурация и
|
||||||
кеш — нет.
|
кеш — нет.
|
||||||
|
|
||||||
**Почему.** Кеш раздувает снапшот содержимым, которое приложение
|
**ПОЧЕМУ.** Кеш раздувает снапшот содержимым, которое приложение
|
||||||
восстановит само. Конфигурацию бэкапить не только незачем, но и вредно: там
|
восстановит само. Конфигурацию бэкапить не только незачем, но и вредно: там
|
||||||
лежат секреты, а бэкапы уезжают в облако — источник истины для
|
лежат секреты, а бэкапы уезжают в облако — источник истины для
|
||||||
конфигурации остаётся в репозитории и хранилище секретов, а не в снапшоте.
|
конфигурации остаётся в репозитории и хранилище секретов, а не в снапшоте.
|
||||||
@@ -97,7 +98,7 @@ prefix: DIRS
|
|||||||
буквально: переменная деплоя, константа, поле конфигурации. Всё остальное
|
буквально: переменная деплоя, константа, поле конфигурации. Всё остальное
|
||||||
на него ссылается.
|
на него ссылается.
|
||||||
|
|
||||||
**Почему.** Правило вывода механическое, но применяет его человек или
|
**ПОЧЕМУ.** Правило вывода механическое, но применяет его человек или
|
||||||
шаблон — то есть ошибиться можно. Общая ссылка делает целый класс ошибок
|
шаблон — то есть ошибиться можно. Общая ссылка делает целый класс ошибок
|
||||||
невозможным: переименование директории отражается в обоих местах сразу.
|
невозможным: переименование директории отражается в обоих местах сразу.
|
||||||
Независимо набранный список расходится тихо — ни деплой, ни прогон бэкапа
|
Независимо набранный список расходится тихо — ни деплой, ни прогон бэкапа
|
||||||
@@ -113,7 +114,7 @@ prefix: DIRS
|
|||||||
| DIRS-6.1 | файлы самодостаточны на любой момент времени | копированием |
|
| DIRS-6.1 | файлы самодостаточны на любой момент времени | копированием |
|
||||||
| DIRS-6.2 | консистентность обеспечивает только сама СУБД | дампом: бэкапится директория дампов, сырой каталог базы — нет |
|
| DIRS-6.2 | консистентность обеспечивает только сама СУБД | дампом: бэкапится директория дампов, сырой каталог базы — нет |
|
||||||
|
|
||||||
**Почему.** Файловый снапшот работающей СУБД не гарантирует
|
**ПОЧЕМУ.** Файловый снапшот работающей СУБД не гарантирует
|
||||||
консистентности: скопированный каталог может не восстановиться, и узнают
|
консистентности: скопированный каталог может не восстановиться, и узнают
|
||||||
об этом при восстановлении. Директория дампов — тоже данные, просто
|
об этом при восстановлении. Директория дампов — тоже данные, просто
|
||||||
производные, поэтому DIRS-4 покрывает её без оговорок. Сырой каталог базы из
|
производные, поэтому DIRS-4 покрывает её без оговорок. Сырой каталог базы из
|
||||||
@@ -125,7 +126,7 @@ prefix: DIRS
|
|||||||
**ДОЛЖЕН.** Решение «копировать или дампить» (DIRS-6) принимается, когда
|
**ДОЛЖЕН.** Решение «копировать или дампить» (DIRS-6) принимается, когда
|
||||||
приложение заводят.
|
приложение заводят.
|
||||||
|
|
||||||
**Почему.** Неверный выбор ничем себя не проявляет, пока бэкап не
|
**ПОЧЕМУ.** Неверный выбор ничем себя не проявляет, пока бэкап не
|
||||||
понадобился: прогоны копирования проходят успешно и накапливают снапшоты, из
|
понадобился: прогоны копирования проходят успешно и накапливают снапшоты, из
|
||||||
которых база не поднимется. Отложить решение — значит принять его по факту
|
которых база не поднимется. Отложить решение — значит принять его по факту
|
||||||
первой неудачной попытки восстановления, то есть тогда, когда данных уже
|
первой неудачной попытки восстановления, то есть тогда, когда данных уже
|
||||||
@@ -136,7 +137,7 @@ prefix: DIRS
|
|||||||
**ДОЛЖЕН.** Конфигурация приложения задаёт отдельные пути для данных и для
|
**ДОЛЖЕН.** Конфигурация приложения задаёт отдельные пути для данных и для
|
||||||
кеша, а не один каталог на всё.
|
кеша, а не один каталог на всё.
|
||||||
|
|
||||||
**Почему.** Снаружи категория определяется только тогда, когда разным
|
**ПОЧЕМУ.** Снаружи категория определяется только тогда, когда разным
|
||||||
категориям соответствуют разные директории. Всё, сложенное в один каталог,
|
категориям соответствуют разные директории. Всё, сложенное в один каталог,
|
||||||
заставляет составлять список бэкапа вручную, читая код приложения, — и
|
заставляет составлять список бэкапа вручную, читая код приложения, — и
|
||||||
пересматривать его при каждом обновлении, потому что новый подкаталог
|
пересматривать его при каждом обновлении, потому что новый подкаталог
|
||||||
@@ -148,19 +149,8 @@ prefix: DIRS
|
|||||||
**НЕ ДОЛЖЕН.** Записываемые пути приложения не указывают внутрь
|
**НЕ ДОЛЖЕН.** Записываемые пути приложения не указывают внутрь
|
||||||
конфигурации.
|
конфигурации.
|
||||||
|
|
||||||
**Почему.** Конфигурация восстанавливается прогоном деплоя (DIRS-1.1), поэтому
|
**ПОЧЕМУ.** Конфигурация восстанавливается прогоном деплоя (DIRS-1.1), поэтому
|
||||||
всё, что приложение туда записало, следующий деплой затирает без
|
всё, что приложение туда записало, следующий деплой затирает без
|
||||||
предупреждения. Вдобавок директория конфигурации может быть подключена
|
предупреждения. Вдобавок директория конфигурации может быть подключена
|
||||||
только на чтение — тогда запись отказывает в рантайме. Ни то ни другое не
|
только на чтение — тогда запись отказывает в рантайме. Ни то ни другое не
|
||||||
видно в момент, когда приложение настраивают.
|
видно в момент, когда приложение настраивают.
|
||||||
|
|
||||||
<!-- local:отступления -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Связано
|
|
||||||
|
|
||||||
<!-- local:эталон -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
<!-- local:связано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
+25
-32
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
topic: config
|
||||||
prefix: CONF
|
prefix: CONF
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -7,9 +8,9 @@ prefix: CONF
|
|||||||
Как устроена конфигурация: где лежит, как попадает в процесс, что с
|
Как устроена конфигурация: где лежит, как попадает в процесс, что с
|
||||||
секретами и когда падает.
|
секретами и когда падает.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
## Область действия
|
## Область действия
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ prefix: CONF
|
|||||||
**ДОЛЖЕН.** Приложение читает параметры из файла конфигурации; переменные
|
**ДОЛЖЕН.** Приложение читает параметры из файла конфигурации; переменные
|
||||||
окружения источником конфигурации не служат.
|
окружения источником конфигурации не служат.
|
||||||
|
|
||||||
**Почему.** Три довода, по убыванию веса:
|
**ПОЧЕМУ.** Три довода, по убыванию веса:
|
||||||
|
|
||||||
- **Один типизированный источник.** Файл несёт секции, комментарии,
|
- **Один типизированный источник.** Файл несёт секции, комментарии,
|
||||||
единицы измерения и валидируется целиком. Окружение — плоский набор
|
единицы измерения и валидируется целиком. Окружение — плоский набор
|
||||||
@@ -48,7 +49,7 @@ prefix: CONF
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** Конкретный формат (TOML, YAML) выбирается по стеку.
|
**СЛЕДУЕТ.** Конкретный формат (TOML, YAML) выбирается по стеку.
|
||||||
|
|
||||||
**Почему.** Комментарий у каждого поля (CONF-9) — часть того, ради чего конфиг
|
**ПОЧЕМУ.** Комментарий у каждого поля (CONF-9) — часть того, ради чего конфиг
|
||||||
вообще читают; формат, в котором комментарий негде разместить, делает CONF-9
|
вообще читают; формат, в котором комментарий негде разместить, делает CONF-9
|
||||||
невыполнимым. Секции дают структуру, которую валидатор проверяет целиком, —
|
невыполнимым. Секции дают структуру, которую валидатор проверяет целиком, —
|
||||||
плоский список пар такой возможности не даёт и возвращает нас к тем же
|
плоский список пар такой возможности не даёт и возвращает нас к тем же
|
||||||
@@ -59,7 +60,7 @@ prefix: CONF
|
|||||||
**СЛЕДУЕТ.** Имя по умолчанию ищется в рабочей директории процесса, а путь
|
**СЛЕДУЕТ.** Имя по умолчанию ищется в рабочей директории процесса, а путь
|
||||||
задаётся опцией командной строки.
|
задаётся опцией командной строки.
|
||||||
|
|
||||||
**Почему.** Запуск без аргументов работает одинаково в разработке, в
|
**ПОЧЕМУ.** Запуск без аргументов работает одинаково в разработке, в
|
||||||
контейнере и на сервере, и способ запуска не приходится помнить отдельно
|
контейнере и на сервере, и способ запуска не приходится помнить отдельно
|
||||||
для каждой среды. Опция нужна ровно для случаев, когда конфигов несколько
|
для каждой среды. Опция нужна ровно для случаев, когда конфигов несколько
|
||||||
(тесты, второй инстанс): без неё их разводят переменной окружения — тем
|
(тесты, второй инстанс): без неё их разводят переменной окружения — тем
|
||||||
@@ -71,7 +72,7 @@ prefix: CONF
|
|||||||
рабочей директории (CONF-3), приложение не стартует: сообщение называет
|
рабочей директории (CONF-3), приложение не стартует: сообщение называет
|
||||||
искомый путь, код возврата ненулевой.
|
искомый путь, код возврата ненулевой.
|
||||||
|
|
||||||
**Почему.** Конфиг — артефакт деплоя (CONF-12), и его отсутствие означает, что
|
**ПОЧЕМУ.** Конфиг — артефакт деплоя (CONF-12), и его отсутствие означает, что
|
||||||
развёртывание не довело работу до конца, а не что приложение попросили
|
развёртывание не довело работу до конца, а не что приложение попросили
|
||||||
работать на умолчаниях. Умолчания (CONF-7) существуют, чтобы работал
|
работать на умолчаниях. Умолчания (CONF-7) существуют, чтобы работал
|
||||||
**неполный** файл, а не отсутствующий, — именно здесь читатель спотыкается
|
**неполный** файл, а не отсутствующий, — именно здесь читатель спотыкается
|
||||||
@@ -85,13 +86,11 @@ prefix: CONF
|
|||||||
Приложение, которое запускается вообще без конфигурации, этой конвенцией не
|
Приложение, которое запускается вообще без конфигурации, этой конвенцией не
|
||||||
описывается: это отдельный случай и отдельная конвенция.
|
описывается: это отдельный случай и отдельная конвенция.
|
||||||
|
|
||||||
<!-- local:проверки -->
|
|
||||||
<!-- /local -->
|
|
||||||
### CONF-4. В репозитории лежит образец, а не рабочий конфиг
|
### CONF-4. В репозитории лежит образец, а не рабочий конфиг
|
||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Реальный конфиг не коммитится; в репозитории — образец.
|
**НЕ ДОЛЖЕН.** Реальный конфиг не коммитится; в репозитории — образец.
|
||||||
|
|
||||||
**Почему.** Рабочий конфиг содержит отрендеренные секреты (CONF-12), а секрет,
|
**ПОЧЕМУ.** Рабочий конфиг содержит отрендеренные секреты (CONF-12), а секрет,
|
||||||
попавший в историю, чинится ротацией, а не удалением файла. Кроме того,
|
попавший в историю, чинится ротацией, а не удалением файла. Кроме того,
|
||||||
закоммиченный конфиг конкретной среды становится вторым источником истины:
|
закоммиченный конфиг конкретной среды становится вторым источником истины:
|
||||||
он расходится с тем, что реально развёрнуто, и расходится молча.
|
он расходится с тем, что реально развёрнуто, и расходится молча.
|
||||||
@@ -101,7 +100,7 @@ prefix: CONF
|
|||||||
**ДОЛЖЕН.** Разбор — при старте, в одну типизированную структуру; чтения
|
**ДОЛЖЕН.** Разбор — при старте, в одну типизированную структуру; чтения
|
||||||
файла конфигурации в бизнес-коде нет.
|
файла конфигурации в бизнес-коде нет.
|
||||||
|
|
||||||
**Почему.** Второе место чтения — это второй момент времени: две части кода
|
**ПОЧЕМУ.** Второе место чтения — это второй момент времени: две части кода
|
||||||
начинают видеть разные значения одного параметра, и расхождение не
|
начинают видеть разные значения одного параметра, и расхождение не
|
||||||
воспроизводится, потому что зависит от того, когда файл потрогали.
|
воспроизводится, потому что зависит от того, когда файл потрогали.
|
||||||
Типизированная структура вдобавок переносит ошибку формата в старт (CONF-17),
|
Типизированная структура вдобавок переносит ошибку формата в старт (CONF-17),
|
||||||
@@ -111,7 +110,7 @@ prefix: CONF
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Смена параметров — рестарт процесса.
|
**ДОЛЖЕН.** Смена параметров — рестарт процесса.
|
||||||
|
|
||||||
**Почему.** Изменяемый конфиг делает поведение функцией момента: один
|
**ПОЧЕМУ.** Изменяемый конфиг делает поведение функцией момента: один
|
||||||
запрос обслуживается наполовину старыми, наполовину новыми значениями, а
|
запрос обслуживается наполовину старыми, наполовину новыми значениями, а
|
||||||
разбор инцидента требует знать хронологию правок файла, а не его текущее
|
разбор инцидента требует знать хронологию правок файла, а не его текущее
|
||||||
содержимое.
|
содержимое.
|
||||||
@@ -123,7 +122,7 @@ prefix: CONF
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Значение по умолчанию задаётся в коде, файл его перекрывает.
|
**ДОЛЖЕН.** Значение по умолчанию задаётся в коде, файл его перекрывает.
|
||||||
|
|
||||||
**Почему.** Умолчание, живущее в образце, действует только для тех, кто
|
**ПОЧЕМУ.** Умолчание, живущее в образце, действует только для тех, кто
|
||||||
образец скопировал: конфиг без этого поля даёт другое поведение, и ни одно
|
образец скопировал: конфиг без этого поля даёт другое поведение, и ни одно
|
||||||
из двух значений не выглядит ошибкой. Умолчание в коде — одно определённое
|
из двух значений не выглядит ошибкой. Умолчание в коде — одно определённое
|
||||||
поведение для неполного конфига и одно место, где это значение меняется.
|
поведение для неполного конфига и одно место, где это значение меняется.
|
||||||
@@ -133,7 +132,7 @@ prefix: CONF
|
|||||||
**ДОЛЖЕН.** В образце присутствуют все секции и все поля, включая те, у
|
**ДОЛЖЕН.** В образце присутствуют все секции и все поля, включая те, у
|
||||||
которых есть умолчание (CONF-7).
|
которых есть умолчание (CONF-7).
|
||||||
|
|
||||||
**Почему.** Поле, живущее только в коде, для читателя конфига не
|
**ПОЧЕМУ.** Поле, живущее только в коде, для читателя конфига не
|
||||||
существует: он не знает, что параметр вообще можно менять, и добивается
|
существует: он не знает, что параметр вообще можно менять, и добивается
|
||||||
нужного поведения обходным путём. Полнота образца — цена, которой CONF-7
|
нужного поведения обходным путём. Полнота образца — цена, которой CONF-7
|
||||||
покупает себе видимость.
|
покупает себе видимость.
|
||||||
@@ -147,7 +146,7 @@ prefix: CONF
|
|||||||
- **единицы измерения**, если применимо: секунды/миллисекунды, байты, доля
|
- **единицы измерения**, если применимо: секунды/миллисекунды, байты, доля
|
||||||
`0–1`.
|
`0–1`.
|
||||||
|
|
||||||
**Почему.** Так конфиг читается без открывания кода — этим он и полезен;
|
**ПОЧЕМУ.** Так конфиг читается без открывания кода — этим он и полезен;
|
||||||
без комментария читатель всё равно идёт в код, и образец перестаёт быть
|
без комментария читатель всё равно идёт в код, и образец перестаёт быть
|
||||||
справочником. Единицы стоят отдельно: перепутанные секунды и миллисекунды
|
справочником. Единицы стоят отдельно: перепутанные секунды и миллисекунды
|
||||||
дают валидное значение и работающий процесс, а ошибка обнаруживается по
|
дают валидное значение и работающий процесс, а ошибка обнаруживается по
|
||||||
@@ -163,7 +162,7 @@ prefix: CONF
|
|||||||
| CONF-10.1 | поддерживаемое | обязательны поля этого варианта; поля прочих вариантов не требуются |
|
| CONF-10.1 | поддерживаемое | обязательны поля этого варианта; поля прочих вариантов не требуются |
|
||||||
| CONF-10.2 | неизвестное | ошибка на старте с перечислением поддерживаемых значений |
|
| CONF-10.2 | неизвестное | ошибка на старте с перечислением поддерживаемых значений |
|
||||||
|
|
||||||
**Почему.** Фиксированный на секцию набор обязательных полей оставляет
|
**ПОЧЕМУ.** Фиксированный на секцию набор обязательных полей оставляет
|
||||||
выбор из двух плохих: заполнять поля бекенда, который не используется, или
|
выбор из двух плохих: заполнять поля бекенда, который не используется, или
|
||||||
не проверять обязательность вовсе — то есть выключить валидацию ровно там,
|
не проверять обязательность вовсе — то есть выключить валидацию ровно там,
|
||||||
где вариантов много и ошибиться легче всего. Перечисление поддерживаемых
|
где вариантов много и ошибиться легче всего. Перечисление поддерживаемых
|
||||||
@@ -176,7 +175,7 @@ prefix: CONF
|
|||||||
альтернативные — блоками-комментариями ниже, каждый со своим описанием
|
альтернативные — блоками-комментариями ниже, каждый со своим описанием
|
||||||
полей.
|
полей.
|
||||||
|
|
||||||
**Почему.** Иначе набор вариантов виден только из кода валидации, и образец
|
**ПОЧЕМУ.** Иначе набор вариантов виден только из кода валидации, и образец
|
||||||
теряет свойство справочника (CONF-8, CONF-9) ровно на той секции, где выбор
|
теряет свойство справочника (CONF-8, CONF-9) ровно на той секции, где выбор
|
||||||
действительно есть. Закомментированный блок вдобавок переключается правкой
|
действительно есть. Закомментированный блок вдобавок переключается правкой
|
||||||
на месте, а не сборкой секции с нуля по документации.
|
на месте, а не сборкой секции с нуля по документации.
|
||||||
@@ -186,7 +185,7 @@ prefix: CONF
|
|||||||
**ДОЛЖЕН.** Деплой рендерит значения секретов прямо в файл конфигурации;
|
**ДОЛЖЕН.** Деплой рендерит значения секретов прямо в файл конфигурации;
|
||||||
отдельного слоя секретов в приложении нет.
|
отдельного слоя секретов в приложении нет.
|
||||||
|
|
||||||
**Почему.** Источник истины секрета — внешнее хранилище деплоя, не
|
**ПОЧЕМУ.** Источник истины секрета — внешнее хранилище деплоя, не
|
||||||
репозиторий и не окружение. Любой второй канал — переменная окружения рядом
|
репозиторий и не окружение. Любой второй канал — переменная окружения рядом
|
||||||
с файлом, собственный клиент к хранилищу внутри приложения — возвращает
|
с файлом, собственный клиент к хранилищу внутри приложения — возвращает
|
||||||
вопрос «откуда взялось значение, которое сейчас в процессе». Приложение при
|
вопрос «откуда взялось значение, которое сейчас в процессе». Приложение при
|
||||||
@@ -198,7 +197,7 @@ prefix: CONF
|
|||||||
**ДОЛЖЕН.** Права `0600`, владелец — пользователь, от имени которого
|
**ДОЛЖЕН.** Права `0600`, владелец — пользователь, от имени которого
|
||||||
работает процесс.
|
работает процесс.
|
||||||
|
|
||||||
**Почему.** После CONF-1 и CONF-12 файл конфигурации — единственная
|
**ПОЧЕМУ.** После CONF-1 и CONF-12 файл конфигурации — единственная
|
||||||
поверхность, на которой секреты лежат, и весь довод «файл вместо окружения»
|
поверхность, на которой секреты лежат, и весь довод «файл вместо окружения»
|
||||||
держится на его правах: конфиг, читаемый всеми на машине, раздаёт секреты
|
держится на его правах: конфиг, читаемый всеми на машине, раздаёт секреты
|
||||||
шире, чем раздало бы окружение, — и тогда CONF-1 меняет одну утечку на
|
шире, чем раздало бы окружение, — и тогда CONF-1 меняет одну утечку на
|
||||||
@@ -209,7 +208,7 @@ prefix: CONF
|
|||||||
**ДОЛЖЕН.** Значение секретного поля в образце — пустая строка, а не
|
**ДОЛЖЕН.** Значение секретного поля в образце — пустая строка, а не
|
||||||
пример.
|
пример.
|
||||||
|
|
||||||
**Почему.** Правдоподобная заглушка доезжает до продакшена как настоящее
|
**ПОЧЕМУ.** Правдоподобная заглушка доезжает до продакшена как настоящее
|
||||||
значение: шаблон отрендерился криво, поле осталось от образца, и проверка
|
значение: шаблон отрендерился криво, поле осталось от образца, и проверка
|
||||||
непустоты (CONF-15) его пропускает. Пустая строка делает недорендеренный конфиг
|
непустоты (CONF-15) его пропускает. Пустая строка делает недорендеренный конфиг
|
||||||
механически отличимым от заполненного.
|
механически отличимым от заполненного.
|
||||||
@@ -218,7 +217,7 @@ prefix: CONF
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Непустота обязательных секретов проверяется на старте.
|
**ДОЛЖЕН.** Непустота обязательных секретов проверяется на старте.
|
||||||
|
|
||||||
**Почему.** Это ловит криво отрендеренный шаблон до того, как он превратится
|
**ПОЧЕМУ.** Это ловит криво отрендеренный шаблон до того, как он превратится
|
||||||
в 401 от внешнего API через час работы, — то есть в момент, когда причина
|
в 401 от внешнего API через час работы, — то есть в момент, когда причина
|
||||||
ещё очевидна и связана с деплоем.
|
ещё очевидна и связана с деплоем.
|
||||||
|
|
||||||
@@ -227,21 +226,18 @@ prefix: CONF
|
|||||||
**НЕ ДОЛЖЕН.** Значение секретного поля не появляется в записи лога ни на
|
**НЕ ДОЛЖЕН.** Значение секретного поля не появляется в записи лога ни на
|
||||||
одном уровне.
|
одном уровне.
|
||||||
|
|
||||||
**Почему.** У логов круг доступа шире, чем у файла под `0600` (CONF-13): они
|
**ПОЧЕМУ.** У логов круг доступа шире, чем у файла под `0600` (CONF-13): они
|
||||||
собираются, пересылаются и попадают в бэкапы, где права исходного файла уже
|
собираются, пересылаются и попадают в бэкапы, где права исходного файла уже
|
||||||
ничего не значат. Попавший в лог секрет чинится ротацией, а не удалением
|
ничего не значат. Попавший в лог секрет чинится ротацией, а не удалением
|
||||||
записи. Типичный источник утечки — отладочный дамп разобранного конфига при
|
записи. Типичный источник утечки — отладочный дамп разобранного конфига при
|
||||||
старте.
|
старте.
|
||||||
|
|
||||||
<!-- local:секретные-поля -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
### CONF-17. Конфиг валидируется на старте, до приёма трафика
|
### CONF-17. Конфиг валидируется на старте, до приёма трафика
|
||||||
|
|
||||||
**ДОЛЖЕН.** Невалидный конфиг — запись уровня `ERROR` и выход с ненулевым
|
**ДОЛЖЕН.** Невалидный конфиг — запись уровня `ERROR` и выход с ненулевым
|
||||||
кодом; процесс не стартует «наполовину».
|
кодом; процесс не стартует «наполовину».
|
||||||
|
|
||||||
**Почему.** Наполовину стартовавший процесс проходит проверку живости и
|
**ПОЧЕМУ.** Наполовину стартовавший процесс проходит проверку живости и
|
||||||
падает позже — на первом запросе, который трогает испорченный параметр, — и
|
падает позже — на первом запросе, который трогает испорченный параметр, — и
|
||||||
падение выглядит дефектом кода, а не ошибкой деплоя. Ненулевой код нужен,
|
падение выглядит дефектом кода, а не ошибкой деплоя. Ненулевой код нужен,
|
||||||
чтобы неудачный старт увидел супервизор: без него он неотличим от штатного
|
чтобы неудачный старт увидел супервизор: без него он неотличим от штатного
|
||||||
@@ -259,7 +255,7 @@ prefix: CONF
|
|||||||
| CONF-18.4 | строки, которые парсятся во что-то (длительности, зоны, URL, идентификаторы сущностей), реально парсятся | при первом обращении к тому, что за ними стоит |
|
| CONF-18.4 | строки, которые парсятся во что-то (длительности, зоны, URL, идентификаторы сущностей), реально парсятся | при первом обращении к тому, что за ними стоит |
|
||||||
| CONF-18.5 | включённые секции консистентны: у включённой интеграции заданы все обязательные поля | при первом вызове интеграции, часто по расписанию |
|
| CONF-18.5 | включённые секции консистентны: у включённой интеграции заданы все обязательные поля | при первом вызове интеграции, часто по расписанию |
|
||||||
|
|
||||||
**Почему.** Список минимальный и собран по одному признаку — правый
|
**ПОЧЕМУ.** Список минимальный и собран по одному признаку — правый
|
||||||
столбец: каждая из этих ошибок иначе всплывает там, где связь с деплоем уже
|
столбец: каждая из этих ошибок иначе всплывает там, где связь с деплоем уже
|
||||||
потеряна, и диагностируется как дефект приложения. Проверка на старте
|
потеряна, и диагностируется как дефект приложения. Проверка на старте
|
||||||
сводит их все к одному моменту и одному сообщению.
|
сводит их все к одному моменту и одному сообщению.
|
||||||
@@ -269,7 +265,7 @@ prefix: CONF
|
|||||||
**ДОЛЖЕН.** Валидация собирает все найденные проблемы и выводит их одним
|
**ДОЛЖЕН.** Валидация собирает все найденные проблемы и выводит их одним
|
||||||
списком, а не падает на первой.
|
списком, а не падает на первой.
|
||||||
|
|
||||||
**Почему.** Иначе цикл «запуск — одна ошибка — правка» повторяется столько
|
**ПОЧЕМУ.** Иначе цикл «запуск — одна ошибка — правка» повторяется столько
|
||||||
раз, сколько в конфиге ошибок, а на сервере каждая итерация — это ещё и
|
раз, сколько в конфиге ошибок, а на сервере каждая итерация — это ещё и
|
||||||
деплой. Криво отрендеренный шаблон обычно ломает не одно поле, а все поля
|
деплой. Криво отрендеренный шаблон обычно ломает не одно поле, а все поля
|
||||||
одного источника: разом они читаются как одна причина, по одной — как
|
одного источника: разом они читаются как одна причина, по одной — как
|
||||||
@@ -285,7 +281,7 @@ prefix: CONF
|
|||||||
| CONF-21.1 | несекретное | имя поля, ожидание и полученное значение: «ожидалось 0–1, получено `1.5`» |
|
| CONF-21.1 | несекретное | имя поля, ожидание и полученное значение: «ожидалось 0–1, получено `1.5`» |
|
||||||
| CONF-21.2 | секретное | имя поля и суть нарушения, без значения |
|
| CONF-21.2 | секретное | имя поля и суть нарушения, без значения |
|
||||||
|
|
||||||
**Почему.** Сообщение без значения отправляет читателя в файл — сличать
|
**ПОЧЕМУ.** Сообщение без значения отправляет читателя в файл — сличать
|
||||||
глазами каждую строку списка CONF-19; ошибки вида «секунды вместо миллисекунд»
|
глазами каждую строку списка CONF-19; ошибки вида «секунды вместо миллисекунд»
|
||||||
или пробел в конце значения из такого сообщения не читаются вовсе. Значение
|
или пробел в конце значения из такого сообщения не читаются вовсе. Значение
|
||||||
секретного поля при этом печатать некуда: вывод старта уходит в лог
|
секретного поля при этом печатать некуда: вывод старта уходит в лог
|
||||||
@@ -301,6 +297,3 @@ prefix: CONF
|
|||||||
конфигурируемый параметр времени, семантика описана там.
|
конфигурируемый параметр времени, семантика описана там.
|
||||||
- конвенция `app-directories` — конфиг лежит в категории «конфигурация» и
|
- конвенция `app-directories` — конфиг лежит в категории «конфигурация» и
|
||||||
доступен приложению только на чтение.
|
доступен приложению только на чтение.
|
||||||
|
|
||||||
<!-- local:связано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
topic: db-identifiers
|
||||||
prefix: KEYS
|
prefix: KEYS
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -6,9 +7,9 @@ prefix: KEYS
|
|||||||
|
|
||||||
Как выбираются и как выглядят первичные ключи сущностей.
|
Как выбираются и как выглядят первичные ключи сущностей.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
## Область действия
|
## Область действия
|
||||||
|
|
||||||
@@ -27,7 +28,7 @@ prefix: KEYS
|
|||||||
который порождает приложение, — во **всех** таблицах, включая те, что
|
который порождает приложение, — во **всех** таблицах, включая те, что
|
||||||
снаружи не адресуются.
|
снаружи не адресуются.
|
||||||
|
|
||||||
**Почему.** Ветвления здесь нет намеренно, хотя напрашивается: «эту
|
**ПОЧЕМУ.** Ветвления здесь нет намеренно, хотя напрашивается: «эту
|
||||||
сущность снаружи не адресуют, ей хватит целого числа». Внутренние сущности
|
сущность снаружи не адресуют, ей хватит целого числа». Внутренние сущности
|
||||||
имеют привычку становиться внешними — и тогда целочисленный идентификатор
|
имеют привычку становиться внешними — и тогда целочисленный идентификатор
|
||||||
утекает в URL задним числом, а миграция ключа на живых данных стоит
|
утекает в URL задним числом, а миграция ключа на живых данных стоит
|
||||||
@@ -47,7 +48,7 @@ prefix: KEYS
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Значение ключа известно до вставки строки.
|
**ДОЛЖЕН.** Значение ключа известно до вставки строки.
|
||||||
|
|
||||||
**Почему.** Идентификатор нужен раньше, чем база ответит: его пишут в лог
|
**ПОЧЕМУ.** Идентификатор нужен раньше, чем база ответит: его пишут в лог
|
||||||
начатой операции, кладут в связанные записи одной транзакции и возвращают
|
начатой операции, кладут в связанные записи одной транзакции и возвращают
|
||||||
клиенту. Генерация на стороне базы вынуждает либо ждать `last_insert_rowid`
|
клиенту. Генерация на стороне базы вынуждает либо ждать `last_insert_rowid`
|
||||||
и достраивать связи вторым проходом, либо иметь два источника истины о
|
и достраивать связи вторым проходом, либо иметь два источника истины о
|
||||||
@@ -58,7 +59,7 @@ prefix: KEYS
|
|||||||
**ДОЛЖЕН.** Один модуль порождает идентификаторы, он же их разбирает.
|
**ДОЛЖЕН.** Один модуль порождает идентификаторы, он же их разбирает.
|
||||||
Самодельных генераторов и парсеров в коде нет.
|
Самодельных генераторов и парсеров в коде нет.
|
||||||
|
|
||||||
**Почему.** Нормализация регистра (KEYS-4) и проверка формата обязаны
|
**ПОЧЕМУ.** Нормализация регистра (KEYS-4) и проверка формата обязаны
|
||||||
применяться ко всем идентификаторам без исключения. Любая вторая точка
|
применяться ко всем идентификаторам без исключения. Любая вторая точка
|
||||||
входа рано или поздно окажется той, где нормализацию забыли, — и дефект
|
входа рано или поздно окажется той, где нормализацию забыли, — и дефект
|
||||||
проявится не там, где создан.
|
проявится не там, где создан.
|
||||||
@@ -67,7 +68,7 @@ prefix: KEYS
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Идентификаторы порождаются и хранятся в нижнем регистре.
|
**ДОЛЖЕН.** Идентификаторы порождаются и хранятся в нижнем регистре.
|
||||||
|
|
||||||
**Почему.** Сравнение строк в базе обычно побайтовое, поэтому регистр — не
|
**ПОЧЕМУ.** Сравнение строк в базе обычно побайтовое, поэтому регистр — не
|
||||||
косметика, а корректность поиска. Спецификация ULID канонизирует **верхний**
|
косметика, а корректность поиска. Спецификация ULID канонизирует **верхний**
|
||||||
регистр, и библиотеки по умолчанию отдают именно его: без единой точки (KEYS-3)
|
регистр, и библиотеки по умолчанию отдают именно его: без единой точки (KEYS-3)
|
||||||
разный регистр появится в базе сам собой.
|
разный регистр появится в базе сам собой.
|
||||||
@@ -83,7 +84,7 @@ prefix: KEYS
|
|||||||
| KEYS-5.1 | путь или query URL | «не найдено» без обращения к хранилищу |
|
| KEYS-5.1 | путь или query URL | «не найдено» без обращения к хранилищу |
|
||||||
| KEYS-5.2 | собственная форма, данные кнопки | «некорректный ввод» либо «элемент устарел» |
|
| KEYS-5.2 | собственная форма, данные кнопки | «некорректный ввод» либо «элемент устарел» |
|
||||||
|
|
||||||
**Почему.** Синтаксически невалидное значение не может соответствовать
|
**ПОЧЕМУ.** Синтаксически невалидное значение не может соответствовать
|
||||||
записи, поэтому поход в базу за ним — заведомо холостой; отсекая его на
|
записи, поэтому поход в базу за ним — заведомо холостой; отсекая его на
|
||||||
границе, мы дёшево снимаем целый класс мусорного трафика.
|
границе, мы дёшево снимаем целый класс мусорного трафика.
|
||||||
|
|
||||||
@@ -106,7 +107,7 @@ prefix: KEYS
|
|||||||
**ДОПУСКАЕТСЯ.** Когда ключ есть по природе данных, отдельный
|
**ДОПУСКАЕТСЯ.** Когда ключ есть по природе данных, отдельный
|
||||||
сгенерированный идентификатор не заводится.
|
сгенерированный идентификатор не заводится.
|
||||||
|
|
||||||
**Почему.** Суррогат поверх естественного ключа создаёт второй способ
|
**ПОЧЕМУ.** Суррогат поверх естественного ключа создаёт второй способ
|
||||||
адресовать ту же строку — а значит, возможность рассинхрона между ними и
|
адресовать ту же строку — а значит, возможность рассинхрона между ними и
|
||||||
лишний вопрос «по какому из них искать» на каждом запросе. Дополнительной
|
лишний вопрос «по какому из них искать» на каждом запросе. Дополнительной
|
||||||
информации он не несёт.
|
информации он не несёт.
|
||||||
@@ -117,7 +118,7 @@ prefix: KEYS
|
|||||||
задание, корреляционный ключ), порождаются тем же модулем (KEYS-3) и в том же
|
задание, корреляционный ключ), порождаются тем же модулем (KEYS-3) и в том же
|
||||||
формате.
|
формате.
|
||||||
|
|
||||||
**Почему.** Единый формат делает работающим главный побочный эффект
|
**ПОЧЕМУ.** Единый формат делает работающим главный побочный эффект
|
||||||
строковых идентификаторов: `grep` по голому значению собирает все
|
строковых идентификаторов: `grep` по голому значению собирает все
|
||||||
упоминания сущности в логах независимо от имени поля. Второй формат
|
упоминания сущности в логах независимо от имени поля. Второй формат
|
||||||
идентификаторов эту возможность отменяет ровно для тех записей, где она
|
идентификаторов эту возможность отменяет ровно для тех записей, где она
|
||||||
@@ -134,12 +135,6 @@ KEYS-1 требует **сортируемый** строковый иденти
|
|||||||
внутри одной миллисекунды порядок произволен, если генератор не монотонный,
|
внутри одной миллисекунды порядок произволен, если генератор не монотонный,
|
||||||
— на порядок событий это не влияет.
|
— на порядок событий это не влияет.
|
||||||
|
|
||||||
<!-- local:отступления -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Связано
|
## Связано
|
||||||
|
|
||||||
- конвенция `time` — метки времени тоже генерирует приложение, а не схема.
|
- конвенция `time` — метки времени тоже генерирует приложение, а не схема.
|
||||||
|
|
||||||
<!-- local:связано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
+17
-25
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
topic: time
|
||||||
prefix: TIME
|
prefix: TIME
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -7,9 +8,9 @@ prefix: TIME
|
|||||||
Как приложение записывает моменты и длительности: в каком формате, откуда
|
Как приложение записывает моменты и длительности: в каком формате, откуда
|
||||||
берётся значение и где появляется не-UTC.
|
берётся значение и где появляется не-UTC.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
## Область действия
|
## Область действия
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ prefix: TIME
|
|||||||
**ДОЛЖЕН.** Момент времени записывается как `2026-06-28T11:23:45Z` —
|
**ДОЛЖЕН.** Момент времени записывается как `2026-06-28T11:23:45Z` —
|
||||||
одинаково в хранении, логах, API и обмене с внешними системами.
|
одинаково в хранении, логах, API и обмене с внешними системами.
|
||||||
|
|
||||||
**Почему.** Разные форматы в разных слоях требуют преобразования на каждой
|
**ПОЧЕМУ.** Разные форматы в разных слоях требуют преобразования на каждой
|
||||||
границе, а ошибка в таком преобразовании не видна сразу: она всплывает через
|
границе, а ошибка в таком преобразовании не видна сразу: она всплывает через
|
||||||
полгода, на переходе на летнее время, когда реальное смещение перестаёт
|
полгода, на переходе на летнее время, когда реальное смещение перестаёт
|
||||||
совпадать с тем, которое подразумевалось при написании кода. UTC с явным `Z`
|
совпадать с тем, которое подразумевалось при написании кода. UTC с явным `Z`
|
||||||
@@ -37,7 +38,7 @@ prefix: TIME
|
|||||||
**ДОЛЖЕН.** Внутри одной колонки БД и внутри одного потока логов длина
|
**ДОЛЖЕН.** Внутри одной колонки БД и внутри одного потока логов длина
|
||||||
строки времени одна и от записи к записи не плавает.
|
строки времени одна и от записи к записи не плавает.
|
||||||
|
|
||||||
**Почему.** Лексикографическая сортировка совпадает с хронологией только
|
**ПОЧЕМУ.** Лексикографическая сортировка совпадает с хронологией только
|
||||||
среди строк одинаковой длины: `…00.123Z` сортируется раньше `…00.12Z`, хотя
|
среди строк одинаковой длины: `…00.123Z` сортируется раньше `…00.12Z`, хотя
|
||||||
произошло позже. Ради этого ширина и фиксируется — `ORDER BY created_at` по
|
произошло позже. Ради этого ширина и фиксируется — `ORDER BY created_at` по
|
||||||
текстовому полю обязан давать порядок событий. Плавающая ширина (типичный
|
текстовому полю обязан давать порядок событий. Плавающая ширина (типичный
|
||||||
@@ -49,7 +50,7 @@ prefix: TIME
|
|||||||
|
|
||||||
**ДОПУСКАЕТСЯ.** У колонки БД и у потока логов каждая своя точность.
|
**ДОПУСКАЕТСЯ.** У колонки БД и у потока логов каждая своя точность.
|
||||||
|
|
||||||
**Почему.** Квантор в TIME-2 — на носитель, а не на приложение, потому что
|
**ПОЧЕМУ.** Квантор в TIME-2 — на носитель, а не на приложение, потому что
|
||||||
строки разных носителей между собой не сравниваются: сортировка идёт внутри
|
строки разных носителей между собой не сравниваются: сортировка идёт внутри
|
||||||
колонки, чтение — внутри потока. Явное разрешение нужно, чтобы TIME-2 не читался
|
колонки, чтение — внутри потока. Явное разрешение нужно, чтобы TIME-2 не читался
|
||||||
как «одна точность на всё приложение»: от подгонки формата логов под формат
|
как «одна точность на всё приложение»: от подгонки формата логов под формат
|
||||||
@@ -61,7 +62,7 @@ prefix: TIME
|
|||||||
**НЕ ДОЛЖЕН.** Ни в базе, ни в логах, ни в JSON API нет меток в локальной
|
**НЕ ДОЛЖЕН.** Ни в базе, ни в логах, ни в JSON API нет меток в локальной
|
||||||
зоне.
|
зоне.
|
||||||
|
|
||||||
**Почему.** Метка без зоны неинтерпретируема вне процесса, который её
|
**ПОЧЕМУ.** Метка без зоны неинтерпретируема вне процесса, который её
|
||||||
записал: чтобы понять, какому моменту она соответствует, читателю нужно
|
записал: чтобы понять, какому моменту она соответствует, читателю нужно
|
||||||
знать настройки чужой машины на момент записи. И даже зная их, он не
|
знать настройки чужой машины на момент записи. И даже зная их, он не
|
||||||
разберёт час перехода на зимнее время: этот час идёт дважды, две записи
|
разберёт час перехода на зимнее время: этот час идёт дважды, две записи
|
||||||
@@ -73,7 +74,7 @@ prefix: TIME
|
|||||||
долями секунды принимается от внешней системы и приводится к каноническому
|
долями секунды принимается от внешней системы и приводится к каноническому
|
||||||
виду (TIME-1) в точке разбора (TIME-5).
|
виду (TIME-1) в точке разбора (TIME-5).
|
||||||
|
|
||||||
**Почему.** Канонический вид — обязательство нашего писателя, а не
|
**ПОЧЕМУ.** Канонический вид — обязательство нашего писателя, а не
|
||||||
контракт, наложенный на внешние системы: `…14:23:45+03:00` называет тот же
|
контракт, наложенный на внешние системы: `…14:23:45+03:00` называет тот же
|
||||||
момент, что `…11:23:45Z`, и отклонять его — значит ломать интеграцию со
|
момент, что `…11:23:45Z`, и отклонять его — значит ломать интеграцию со
|
||||||
стороной, которая стандарт соблюла. Пропущенное же как есть, такое значение
|
стороной, которая стандарт соблюла. Пропущенное же как есть, такое значение
|
||||||
@@ -88,7 +89,7 @@ prefix: TIME
|
|||||||
**ДОЛЖЕН.** Один модуль отдаёт текущий момент, он же форматирует и разбирает
|
**ДОЛЖЕН.** Один модуль отдаёт текущий момент, он же форматирует и разбирает
|
||||||
метки; прямые вызовы часов по коду не разбросаны.
|
метки; прямые вызовы часов по коду не разбросаны.
|
||||||
|
|
||||||
**Почему.** Формат, зона (TIME-1) и ширина (TIME-2) обязаны выполняться для всех
|
**ПОЧЕМУ.** Формат, зона (TIME-1) и ширина (TIME-2) обязаны выполняться для всех
|
||||||
меток без исключения, а каждый прямой вызов часов заводит ещё одно место,
|
меток без исключения, а каждый прямой вызов часов заводит ещё одно место,
|
||||||
где их можно не соблюсти. Промах такого вызова проявляется не в коде, а в
|
где их можно не соблюсти. Промах такого вызова проявляется не в коде, а в
|
||||||
данных, и обнаруживается, когда испорченных записей уже накопилось.
|
данных, и обнаруживается, когда испорченных записей уже накопилось.
|
||||||
@@ -98,7 +99,7 @@ prefix: TIME
|
|||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Колонки времени не имеют `DEFAULT` с текущим моментом.
|
**НЕ ДОЛЖЕН.** Колонки времени не имеют `DEFAULT` с текущим моментом.
|
||||||
|
|
||||||
**Почему.** Дефолт превращает забытую вставку `created_at` в тихо работающий
|
**ПОЧЕМУ.** Дефолт превращает забытую вставку `created_at` в тихо работающий
|
||||||
код: значение появляется, но приходит от сервера БД — то есть с других часов
|
код: значение появляется, но приходит от сервера БД — то есть с других часов
|
||||||
и в формате, который выбирала не единая точка (TIME-5). Без дефолта та же ошибка
|
и в формате, который выбирала не единая точка (TIME-5). Без дефолта та же ошибка
|
||||||
падает громко и чинится в момент написания, а не при разборе расхождения
|
падает громко и чинится в момент написания, а не при разборе расхождения
|
||||||
@@ -110,7 +111,7 @@ prefix: TIME
|
|||||||
**ДОЛЖЕН.** Длительность операции записывается числом (обычно
|
**ДОЛЖЕН.** Длительность операции записывается числом (обычно
|
||||||
миллисекундами) в поле вида `duration_ms`.
|
миллисекундами) в поле вида `duration_ms`.
|
||||||
|
|
||||||
**Почему.** Метка отвечает на вопрос «когда», длительность — на «сколько».
|
**ПОЧЕМУ.** Метка отвечает на вопрос «когда», длительность — на «сколько».
|
||||||
Пара меток заставляет каждого потребителя знать, какие именно две из них
|
Пара меток заставляет каждого потребителя знать, какие именно две из них
|
||||||
образуют операцию, и вычитать их самому — в запросе, в дашборде и глазами в
|
образуют операцию, и вычитать их самому — в запросе, в дашборде и глазами в
|
||||||
логе; число сравнивается, агрегируется и попадает в перцентили без этого
|
логе; число сравнивается, агрегируется и попадает в перцентили без этого
|
||||||
@@ -121,7 +122,7 @@ prefix: TIME
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** Замер живёт там же, где вызов, границы которого он измеряет.
|
**СЛЕДУЕТ.** Замер живёт там же, где вызов, границы которого он измеряет.
|
||||||
|
|
||||||
**Почему.** Обе границы операции видит только этот слой: замер уровнем выше
|
**ПОЧЕМУ.** Обе границы операции видит только этот слой: замер уровнем выше
|
||||||
приписывает операции чужие накладные расходы, уровнем ниже — теряет часть
|
приписывает операции чужие накладные расходы, уровнем ниже — теряет часть
|
||||||
вызова. В обоих случаях число остаётся правдоподобным и потому не
|
вызова. В обоих случаях число остаётся правдоподобным и потому не
|
||||||
оспаривается, хотя отвечает не на тот вопрос, который к нему задают.
|
оспаривается, хотя отвечает не на тот вопрос, который к нему задают.
|
||||||
@@ -135,7 +136,7 @@ prefix: TIME
|
|||||||
| TIME-9.1 | момент события | стенные часы через единую точку (TIME-5) |
|
| TIME-9.1 | момент события | стенные часы через единую точку (TIME-5) |
|
||||||
| TIME-9.2 | длительность операции | монотонные часы процесса |
|
| TIME-9.2 | длительность операции | монотонные часы процесса |
|
||||||
|
|
||||||
**Почему.** Стенные часы подводит NTP: они могут шагнуть назад, и тогда
|
**ПОЧЕМУ.** Стенные часы подводит NTP: они могут шагнуть назад, и тогда
|
||||||
интервал, посчитанный вычитанием, выйдет отрицательным, а при шаге вперёд —
|
интервал, посчитанный вычитанием, выйдет отрицательным, а при шаге вперёд —
|
||||||
правдоподобным, но выдуманным. Монотонные часы, наоборот, не годятся для
|
правдоподобным, но выдуманным. Монотонные часы, наоборот, не годятся для
|
||||||
меток: их ноль произволен и не переживает перезапуск процесса, так что вне
|
меток: их ноль произволен и не переживает перезапуск процесса, так что вне
|
||||||
@@ -148,7 +149,7 @@ prefix: TIME
|
|||||||
**ДОЛЖЕН.** Преобразование в зону пользователя происходит при выводе и не
|
**ДОЛЖЕН.** Преобразование в зону пользователя происходит при выводе и не
|
||||||
проникает в хранение, сортировку и логи.
|
проникает в хранение, сортировку и логи.
|
||||||
|
|
||||||
**Почему.** Как только конвертация уходит вглубь, результат вычислений
|
**ПОЧЕМУ.** Как только конвертация уходит вглубь, результат вычислений
|
||||||
начинает зависеть от настроек конкретного зрителя: одна и та же выборка даёт
|
начинает зависеть от настроек конкретного зрителя: одна и та же выборка даёт
|
||||||
разные группировки, а порядок записей перестаёт быть общим для всех. Ещё
|
разные группировки, а порядок записей перестаёт быть общим для всех. Ещё
|
||||||
хуже, что при конвертации в нескольких слоях её легко выполнить дважды —
|
хуже, что при конвертации в нескольких слоях её легко выполнить дважды —
|
||||||
@@ -160,7 +161,7 @@ prefix: TIME
|
|||||||
**ДОЛЖЕН.** Значение приходит из конфигурации, значение по умолчанию —
|
**ДОЛЖЕН.** Значение приходит из конфигурации, значение по умолчанию —
|
||||||
`UTC`.
|
`UTC`.
|
||||||
|
|
||||||
**Почему.** Зашитая в код зона превращает переезд или второго пользователя в
|
**ПОЧЕМУ.** Зашитая в код зона превращает переезд или второго пользователя в
|
||||||
другом поясе в правку кода и релиз. Значение по умолчанию `UTC` выбрано
|
другом поясе в правку кода и релиз. Значение по умолчанию `UTC` выбрано
|
||||||
потому, что оно не притворяется настроенным: показанное время совпадает с
|
потому, что оно не притворяется настроенным: показанное время совпадает с
|
||||||
тем, что лежит в базе и в логах, поэтому несовпадение с ожиданиями читается
|
тем, что лежит в базе и в логах, поэтому несовпадение с ожиданиями читается
|
||||||
@@ -171,7 +172,7 @@ prefix: TIME
|
|||||||
**ДОЛЖЕН.** «Сегодня», «за месяц» и прочие календарные границы считаются с
|
**ДОЛЖЕН.** «Сегодня», «за месяц» и прочие календарные границы считаются с
|
||||||
явно переданной зоной, а не с системной зоной процесса.
|
явно переданной зоной, а не с системной зоной процесса.
|
||||||
|
|
||||||
**Почему.** Системная зона разная на ноутбуке разработчика и в контейнере на
|
**ПОЧЕМУ.** Системная зона разная на ноутбуке разработчика и в контейнере на
|
||||||
сервере, поэтому граница суток уезжает, а вместе с ней — содержимое отчёта:
|
сервере, поэтому граница суток уезжает, а вместе с ней — содержимое отчёта:
|
||||||
расхождение не воспроизводится там, где его заметили, и объясняется средой,
|
расхождение не воспроизводится там, где его заметили, и объясняется средой,
|
||||||
а не кодом. Явно переданная зона делает результат функцией от аргументов.
|
а не кодом. Явно переданная зона делает результат функцией от аргументов.
|
||||||
@@ -179,17 +180,8 @@ prefix: TIME
|
|||||||
Зона по умолчанию здесь та же, что и для отображения (TIME-11); календарная
|
Зона по умолчанию здесь та же, что и для отображения (TIME-11); календарная
|
||||||
логика, которой нужна другая, получает её тем же явным аргументом.
|
логика, которой нужна другая, получает её тем же явным аргументом.
|
||||||
|
|
||||||
<!-- local:механизировано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
<!-- local:отступления -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Связано
|
## Связано
|
||||||
|
|
||||||
- конвенция `config` — где задаётся зона отображения.
|
- конвенция `config` — где задаётся зона отображения.
|
||||||
- конвенция `db-identifiers` — то же правило «генерирует приложение» для
|
- конвенция `db-identifiers` — то же правило «генерирует приложение» для
|
||||||
идентификаторов.
|
идентификаторов.
|
||||||
|
|
||||||
<!-- local:связано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
topic: config
|
||||||
prefix: GCFG
|
prefix: GCFG
|
||||||
|
lang: go
|
||||||
extends: arch/config.md
|
extends: arch/config.md
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -8,9 +10,9 @@ extends: arch/config.md
|
|||||||
Как базовый слой выглядит в Go-приложении: формат, загрузчик, границы
|
Как базовый слой выглядит в Go-приложении: формат, загрузчик, границы
|
||||||
запрета на окружение.
|
запрета на окружение.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
Секретов Go-специфика не касается: они приходят из деплоя уже в файле, а
|
Секретов Go-специфика не касается: они приходят из деплоя уже в файле, а
|
||||||
проверка их непустоты идёт вместе с остальной валидацией — как описано в
|
проверка их непустоты идёт вместе с остальной валидацией — как описано в
|
||||||
@@ -22,7 +24,7 @@ extends: arch/config.md
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Конфиг — файл TOML.
|
**ДОЛЖЕН.** Конфиг — файл TOML.
|
||||||
|
|
||||||
**Почему.** Базовая конвенция оставляет формат за стеком, и этот выбор
|
**ПОЧЕМУ.** Базовая конвенция оставляет формат за стеком, и этот выбор
|
||||||
делается один раз на язык, а не в каждом приложении: разные форматы в
|
делается один раз на язык, а не в каждом приложении: разные форматы в
|
||||||
соседних сервисах означают разные загрузчики, разные шаблоны рендера
|
соседних сервисах означают разные загрузчики, разные шаблоны рендера
|
||||||
конфига в деплое и разное поведение при синтаксической ошибке. TOML при
|
конфига в деплое и разное поведение при синтаксической ошибке. TOML при
|
||||||
@@ -35,7 +37,7 @@ extends: arch/config.md
|
|||||||
**ДОЛЖЕН.** Чтение файла, наложение умолчаний и проверки живут в
|
**ДОЛЖЕН.** Чтение файла, наложение умолчаний и проверки живут в
|
||||||
`internal/config`; наружу пакет отдаёт готовую структуру `Config`.
|
`internal/config`; наружу пакет отдаёт готовую структуру `Config`.
|
||||||
|
|
||||||
**Почему.** Пока значение не покинуло пакет, оно может быть невалидным;
|
**ПОЧЕМУ.** Пока значение не покинуло пакет, оно может быть невалидным;
|
||||||
после — уже нет, и это единственная граница, на которой такое утверждение
|
после — уже нет, и это единственная граница, на которой такое утверждение
|
||||||
проверяемо. Валидация, размазанная по потребителям, отвечает на вопрос
|
проверяемо. Валидация, размазанная по потребителям, отвечает на вопрос
|
||||||
«проверено ли это поле» только чтением всех вызывающих, часть полей
|
«проверено ли это поле» только чтением всех вызывающих, часть полей
|
||||||
@@ -48,7 +50,7 @@ extends: arch/config.md
|
|||||||
**ДОЛЖЕН.** Конфиг представлен одним значением типа `Config`, собранным из
|
**ДОЛЖЕН.** Конфиг представлен одним значением типа `Config`, собранным из
|
||||||
под-структур по секциям.
|
под-структур по секциям.
|
||||||
|
|
||||||
**Почему.** Один корень даёт одну точку, после которой конфиг проверен
|
**ПОЧЕМУ.** Один корень даёт одну точку, после которой конфиг проверен
|
||||||
целиком, и дальше передаётся как обычный аргумент. Несколько независимых
|
целиком, и дальше передаётся как обычный аргумент. Несколько независимых
|
||||||
структур конфига означают несколько загрузок и вопрос «какая из них уже
|
структур конфига означают несколько загрузок и вопрос «какая из них уже
|
||||||
провалидирована» на каждом использовании; связанные между собой поля
|
провалидирована» на каждом использовании; связанные между собой поля
|
||||||
@@ -59,7 +61,7 @@ extends: arch/config.md
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** Имя под-структуры совпадает с именем секции TOML.
|
**СЛЕДУЕТ.** Имя под-структуры совпадает с именем секции TOML.
|
||||||
|
|
||||||
**Почему.** Имя секции из файла — то, с чего начинают, когда конфиг ведёт
|
**ПОЧЕМУ.** Имя секции из файла — то, с чего начинают, когда конфиг ведёт
|
||||||
себя не так, как ожидали. При совпадении имён поиск по нему сразу приводит
|
себя не так, как ожидали. При совпадении имён поиск по нему сразу приводит
|
||||||
в код и обратно; при расхождении связь между полем файла и полем структуры
|
в код и обратно; при расхождении связь между полем файла и полем структуры
|
||||||
восстанавливается чтением тегов, и проделывать это приходится для каждой
|
восстанавливается чтением тегов, и проделывать это приходится для каждой
|
||||||
@@ -70,7 +72,7 @@ extends: arch/config.md
|
|||||||
**ДОЛЖЕН.** Умолчания собраны в функции `Default()`, разобранный файл
|
**ДОЛЖЕН.** Умолчания собраны в функции `Default()`, разобранный файл
|
||||||
накладывается поверх.
|
накладывается поверх.
|
||||||
|
|
||||||
**Почему.** Нулевое значение в Go неотличимо от «поле не задано»: нулевой
|
**ПОЧЕМУ.** Нулевое значение в Go неотличимо от «поле не задано»: нулевой
|
||||||
таймаут и отсутствующий таймаут — один и тот же `0`. Умолчание,
|
таймаут и отсутствующий таймаут — один и тот же `0`. Умолчание,
|
||||||
подставленное по месту использования (`if x == 0 { x = … }`), поэтому не
|
подставленное по месту использования (`if x == 0 { x = … }`), поэтому не
|
||||||
видно ни целиком, ни из образца, и два потребителя одного поля со временем
|
видно ни целиком, ни из образца, и два потребителя одного поля со временем
|
||||||
@@ -83,7 +85,7 @@ extends: arch/config.md
|
|||||||
путь переопределяет флаг `--config=path`, образец рядом —
|
путь переопределяет флаг `--config=path`, образец рядом —
|
||||||
`config.example.toml`.
|
`config.example.toml`.
|
||||||
|
|
||||||
**Почему.** Фиксированное имя и переопределение из командной строки требует
|
**ПОЧЕМУ.** Фиксированное имя и переопределение из командной строки требует
|
||||||
базовая конвенция, но конкретные имена она не задаёт. Одинаковые имена во
|
базовая конвенция, но конкретные имена она не задаёт. Одинаковые имена во
|
||||||
всех сервисах означают, что unit-файл, `docker run` и инструкция по запуску
|
всех сервисах означают, что unit-файл, `docker run` и инструкция по запуску
|
||||||
пишутся, не открывая код приложения. Соседство `config.toml` и
|
пишутся, не открывая код приложения. Соседство `config.toml` и
|
||||||
@@ -102,7 +104,7 @@ func (d *Duration) UnmarshalText(b []byte) error { … }
|
|||||||
func (d Duration) Std() time.Duration { … }
|
func (d Duration) Std() time.Duration { … }
|
||||||
```
|
```
|
||||||
|
|
||||||
**Почему.** `time.Duration` — это `int64`, и TOML разбирает его в голое
|
**ПОЧЕМУ.** `time.Duration` — это `int64`, и TOML разбирает его в голое
|
||||||
число: в конфиге остаётся `poll_interval = 5000`, о котором читатель не
|
число: в конфиге остаётся `poll_interval = 5000`, о котором читатель не
|
||||||
знает, секунды это, миллисекунды или наносекунды. Ошибка в тысячу раз
|
знает, секунды это, миллисекунды или наносекунды. Ошибка в тысячу раз
|
||||||
проходит и разбор, и проверку диапазона, а проявляется нагрузкой или
|
проходит и разбор, и проверку диапазона, а проявляется нагрузкой или
|
||||||
@@ -118,7 +120,7 @@ func (d Duration) Std() time.Duration { … }
|
|||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Значения конфигурации не берутся из переменных окружения.
|
**НЕ ДОЛЖЕН.** Значения конфигурации не берутся из переменных окружения.
|
||||||
|
|
||||||
**Почему.** Второй канал конфигурации — то, против чего написана базовая
|
**ПОЧЕМУ.** Второй канал конфигурации — то, против чего написана базовая
|
||||||
конвенция, но в Go он ещё и бесследный: `os.Getenv` вызывается из любого
|
конвенция, но в Go он ещё и бесследный: `os.Getenv` вызывается из любого
|
||||||
пакета, не появляется ни в `Config`, ни в образце и не проходит валидацию.
|
пакета, не появляется ни в `Config`, ни в образце и не проходит валидацию.
|
||||||
Заведённый так параметр нельзя ни увидеть в конфиге, ни узнать иначе как
|
Заведённый так параметр нельзя ни увидеть в конфиге, ни узнать иначе как
|
||||||
@@ -134,7 +136,7 @@ func (d Duration) Std() time.Duration { … }
|
|||||||
^os\.(Getenv|LookupEnv|Environ|ExpandEnv)$
|
^os\.(Getenv|LookupEnv|Environ|ExpandEnv)$
|
||||||
```
|
```
|
||||||
|
|
||||||
**Почему.** `LookupEnv`, `Environ` и `ExpandEnv` читают ровно то же самое,
|
**ПОЧЕМУ.** `LookupEnv`, `Environ` и `ExpandEnv` читают ровно то же самое,
|
||||||
поэтому паттерн на одно имя оставляет три обхода. Хуже, что оставляет
|
поэтому паттерн на одно имя оставляет три обхода. Хуже, что оставляет
|
||||||
незаметно: правило числится механизированным, и глазами его больше никто не
|
незаметно: правило числится механизированным, и глазами его больше никто не
|
||||||
проверяет.
|
проверяет.
|
||||||
@@ -155,7 +157,7 @@ func (d Duration) Std() time.Duration { … }
|
|||||||
| GCFG-10.1 | тесты, включая интеграционные | допустимо: креды и адреса внешних сервисов взять больше неоткуда |
|
| GCFG-10.1 | тесты, включая интеграционные | допустимо: креды и адреса внешних сервисов взять больше неоткуда |
|
||||||
| GCFG-10.2 | Go-рантайм и ОС, а не наш код (`GOMEMLIMIT`, `GOMAXPROCS`, `GODEBUG`, `TZ`) | вне запрета: значение читает не приложение |
|
| GCFG-10.2 | Go-рантайм и ОС, а не наш код (`GOMEMLIMIT`, `GOMAXPROCS`, `GODEBUG`, `TZ`) | вне запрета: значение читает не приложение |
|
||||||
|
|
||||||
**Почему.** GCFG-8 — про конфигурацию приложения; расширенный до «никто не
|
**ПОЧЕМУ.** GCFG-8 — про конфигурацию приложения; расширенный до «никто не
|
||||||
трогает окружение», он запрещает то, чем не управляет: `GOMEMLIMIT` читает
|
трогает окружение», он запрещает то, чем не управляет: `GOMEMLIMIT` читает
|
||||||
рантайм, а тесту креды внешнего сервиса взять неоткуда — в репозиторий их
|
рантайм, а тесту креды внешнего сервиса взять неоткуда — в репозиторий их
|
||||||
не положишь, а отдельный конфиг тестов пришлось бы заводить как ещё один
|
не положишь, а отдельный конфиг тестов пришлось бы заводить как ещё один
|
||||||
@@ -167,7 +169,7 @@ func (d Duration) Std() time.Duration { … }
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Исходящий прокси приходит полем конфига и явным `Transport`.
|
**ДОЛЖЕН.** Исходящий прокси приходит полем конфига и явным `Transport`.
|
||||||
|
|
||||||
**Почему.** `HTTP_PROXY`/`HTTPS_PROXY` формально читает не наш код, а
|
**ПОЧЕМУ.** `HTTP_PROXY`/`HTTPS_PROXY` формально читает не наш код, а
|
||||||
дефолтный `http.Transport` — но читает он их от имени приложения и меняет
|
дефолтный `http.Transport` — но читает он их от имени приложения и меняет
|
||||||
поведение приложения, а не рантайма. Оставленные окружению, они дают ровно
|
поведение приложения, а не рантайма. Оставленные окружению, они дают ровно
|
||||||
тот второй канал, который запрещает GCFG-8, и притом самый неудобный: маршрут
|
тот второй канал, который запрещает GCFG-8, и притом самый неудобный: маршрут
|
||||||
@@ -180,7 +182,7 @@ func (d Duration) Std() time.Duration { … }
|
|||||||
**ДОЛЖЕН.** Проверки не прерываются на первой неудаче, результат — одна
|
**ДОЛЖЕН.** Проверки не прерываются на первой неудаче, результат — одна
|
||||||
ошибка, собранная `errors.Join`.
|
ошибка, собранная `errors.Join`.
|
||||||
|
|
||||||
**Почему.** Возврат первой ошибки превращает починку конфига в серию
|
**ПОЧЕМУ.** Возврат первой ошибки превращает починку конфига в серию
|
||||||
перезапусков по одному полю за раз, причём каждый следующий запуск
|
перезапусков по одному полю за раз, причём каждый следующий запуск
|
||||||
обнаруживает ещё одно. `errors.Join` даёт разом весь список, не требуя
|
обнаруживает ещё одно. `errors.Join` даёт разом весь список, не требуя
|
||||||
своего типа ошибки, и `errors.Is`/`errors.As` продолжают работать по каждой
|
своего типа ошибки, и `errors.Is`/`errors.As` продолжают работать по каждой
|
||||||
@@ -190,7 +192,7 @@ func (d Duration) Std() time.Duration { … }
|
|||||||
|
|
||||||
**ДОЛЖЕН.** IANA-зона из конфига загружается на старте, в общей валидации.
|
**ДОЛЖЕН.** IANA-зона из конфига загружается на старте, в общей валидации.
|
||||||
|
|
||||||
**Почему.** Проверить имя зоны нечем, кроме загрузки: оно валидно ровно
|
**ПОЧЕМУ.** Проверить имя зоны нечем, кроме загрузки: оно валидно ровно
|
||||||
тогда, когда база зон его знает, и никакая проверка формата не отличит
|
тогда, когда база зон его знает, и никакая проверка формата не отличит
|
||||||
`Europe/Moscow` от `Europe/Moskow`. Без загрузки на старте опечатка
|
`Europe/Moscow` от `Europe/Moskow`. Без загрузки на старте опечатка
|
||||||
доживает до первого форматирования времени — то есть до рантайма, мимо
|
доживает до первого форматирования времени — то есть до рантайма, мимо
|
||||||
@@ -201,7 +203,7 @@ fail-fast (GCFG-15).
|
|||||||
**ДОЛЖЕН.** Импорт `_ "time/tzdata"` стоит в `main`, а не в библиотечном
|
**ДОЛЖЕН.** Импорт `_ "time/tzdata"` стоит в `main`, а не в библиотечном
|
||||||
пакете.
|
пакете.
|
||||||
|
|
||||||
**Почему.** Импорт в библиотеке навязывает ~450 КБ zoneinfo каждому
|
**ПОЧЕМУ.** Импорт в библиотеке навязывает ~450 КБ zoneinfo каждому
|
||||||
импортёру, включая тех, кому зоны не нужны: выбор «встраивать базу или
|
импортёру, включая тех, кому зоны не нужны: выбор «встраивать базу или
|
||||||
полагаться на системную» принадлежит собираемой программе. Со встроенной
|
полагаться на системную» принадлежит собираемой программе. Со встроенной
|
||||||
базой ошибка `LoadLocation` (GCFG-13) означает ровно одно — битое имя зоны; без
|
базой ошибка `LoadLocation` (GCFG-13) означает ровно одно — битое имя зоны; без
|
||||||
@@ -213,23 +215,14 @@ zoneinfo, а сообщение указывает не на ту причину
|
|||||||
**ДОЛЖЕН.** `main` пишет `slog` уровня `ERROR` и вызывает `os.Exit(1)` до
|
**ДОЛЖЕН.** `main` пишет `slog` уровня `ERROR` и вызывает `os.Exit(1)` до
|
||||||
старта серверов и воркеров.
|
старта серверов и воркеров.
|
||||||
|
|
||||||
**Почему.** Выход именно из `main`: `os.Exit` в библиотечном пакете не
|
**ПОЧЕМУ.** Выход именно из `main`: `os.Exit` в библиотечном пакете не
|
||||||
оставляет вызывающему возможности ни залогировать причину, ни дописать
|
оставляет вызывающему возможности ни залогировать причину, ни дописать
|
||||||
контекст, и отложенные `defer` при нём не выполняются вовсе. Выход именно
|
контекст, и отложенные `defer` при нём не выполняются вовсе. Выход именно
|
||||||
до старта воркеров: горутина, поднятая раньше валидации, успевает сходить
|
до старта воркеров: горутина, поднятая раньше валидации, успевает сходить
|
||||||
во внешний сервис и записать в базу от имени процесса, который потом
|
во внешний сервис и записать в базу от имени процесса, который потом
|
||||||
объявит, что не стартовал.
|
объявит, что не стартовал.
|
||||||
|
|
||||||
<!-- local:поля -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
<!-- local:механизировано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Связано
|
## Связано
|
||||||
|
|
||||||
- конвенция `time` — зона отображения и формат времени.
|
- конвенция `time` — зона отображения и формат времени.
|
||||||
- конвенция `logging` — `slog`, которым падает невалидный конфиг.
|
- конвенция `logging` — `slog`, которым падает невалидный конфиг.
|
||||||
|
|
||||||
<!-- local:связано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
topic: db-identifiers
|
||||||
prefix: GKEY
|
prefix: GKEY
|
||||||
|
lang: go
|
||||||
extends: arch/db-identifiers.md
|
extends: arch/db-identifiers.md
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -7,9 +9,9 @@ extends: arch/db-identifiers.md
|
|||||||
|
|
||||||
Как базовый слой выглядит в Go-приложении.
|
Как базовый слой выглядит в Go-приложении.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
Единая точка из `KEYS-3` — пакет `internal/ident`: он
|
Единая точка из `KEYS-3` — пакет `internal/ident`: он
|
||||||
порождает идентификаторы (`NewID`, `NewIDAt`) и он же их разбирает
|
порождает идентификаторы (`NewID`, `NewIDAt`) и он же их разбирает
|
||||||
@@ -22,7 +24,7 @@ extends: arch/db-identifiers.md
|
|||||||
**ДОЛЖЕН.** Идентификаторы порождаются и разбираются функциями пакета
|
**ДОЛЖЕН.** Идентификаторы порождаются и разбираются функциями пакета
|
||||||
`internal/ident`; других генераторов и парсеров id в коде нет.
|
`internal/ident`; других генераторов и парсеров id в коде нет.
|
||||||
|
|
||||||
**Почему.** Реализация `KEYS-3` и `KEYS-4`. Вызов
|
**ПОЧЕМУ.** Реализация `KEYS-3` и `KEYS-4`. Вызов
|
||||||
ULID-библиотеки — одна строка, доступная из любого пакета, и на ревью он не
|
ULID-библиотеки — одна строка, доступная из любого пакета, и на ревью он не
|
||||||
выглядит нарушением: значение получается валидное, просто мимо нормализации
|
выглядит нарушением: значение получается валидное, просто мимо нормализации
|
||||||
регистра. Отдельный пакет переводит запрет в проверяемое свойство — импорт
|
регистра. Отдельный пакет переводит запрет в проверяемое свойство — импорт
|
||||||
@@ -35,7 +37,7 @@ ULID-библиотеки — одна строка, доступная из л
|
|||||||
**ДОЛЖЕН.** Новая сущность получает значение PK вызовом `ident.NewID()`
|
**ДОЛЖЕН.** Новая сущность получает значение PK вызовом `ident.NewID()`
|
||||||
внутри `Create`-метода слоя store.
|
внутри `Create`-метода слоя store.
|
||||||
|
|
||||||
**Почему.** `KEYS-2` требует, чтобы значение было
|
**ПОЧЕМУ.** `KEYS-2` требует, чтобы значение было
|
||||||
известно до вставки, но не говорит, кто его присваивает. Store — последний
|
известно до вставки, но не говорит, кто его присваивает. Store — последний
|
||||||
слой, через который проходят все пути создания строки, включая импорт,
|
слой, через который проходят все пути создания строки, включая импорт,
|
||||||
фоновые задания и тесты. Генерация выше по стеку делает присвоение
|
фоновые задания и тесты. Генерация выше по стеку делает присвоение
|
||||||
@@ -48,7 +50,7 @@ ULID-библиотеки — одна строка, доступная из л
|
|||||||
**ДОЛЖЕН.** Идентификатор батча, задания или корреляционный ключ создаётся
|
**ДОЛЖЕН.** Идентификатор батча, задания или корреляционный ключ создаётся
|
||||||
вызовом `ident.NewID()` там, где операция начинается.
|
вызовом `ident.NewID()` там, где операция начинается.
|
||||||
|
|
||||||
**Почему.** Смысл такого идентификатора (`KEYS-7`) —
|
**ПОЧЕМУ.** Смысл такого идентификатора (`KEYS-7`) —
|
||||||
сшивать записи лога всей операции. Созданный ниже по стеку или в момент
|
сшивать записи лога всей операции. Созданный ниже по стеку или в момент
|
||||||
первой записи в базу, он не покрывает начальные шаги — а именно они нужны,
|
первой записи в базу, он не покрывает начальные шаги — а именно они нужны,
|
||||||
когда операция упала до того, как что-либо записала: без общего ключа эти
|
когда операция упала до того, как что-либо записала: без общего ключа эти
|
||||||
@@ -59,7 +61,7 @@ ULID-библиотеки — одна строка, доступная из л
|
|||||||
**ДОЛЖЕН.** Идентификаторы, проставляемые существующим строкам в
|
**ДОЛЖЕН.** Идентификаторы, проставляемые существующим строкам в
|
||||||
Go-миграции, порождаются с историческим временем строки, а не с текущим.
|
Go-миграции, порождаются с историческим временем строки, а не с текущим.
|
||||||
|
|
||||||
**Почему.** Сортировка id тогда сохраняет историческую хронологию, а не
|
**ПОЧЕМУ.** Сортировка id тогда сохраняет историческую хронологию, а не
|
||||||
момент прогона миграции. Иначе все затронутые строки получают метку одного
|
момент прогона миграции. Иначе все затронутые строки получают метку одного
|
||||||
момента, склеиваются в нём и встают в порядке обхода — `ORDER BY id`
|
момента, склеиваются в нём и встают в порядке обхода — `ORDER BY id`
|
||||||
начинает врать ровно на том массиве данных, который старше всего.
|
начинает врать ровно на том массиве данных, который старше всего.
|
||||||
@@ -71,7 +73,7 @@ Go-миграции, порождаются с историческим врем
|
|||||||
**ДОЛЖЕН.** `ident.Parse()` вызывается в обработчике HTTP-роута, формы или
|
**ДОЛЖЕН.** `ident.Parse()` вызывается в обработчике HTTP-роута, формы или
|
||||||
callback'а бота — раньше, чем идентификатор попадёт в store.
|
callback'а бота — раньше, чем идентификатор попадёт в store.
|
||||||
|
|
||||||
**Почему.** Реализация `KEYS-5`. Граница выбрана
|
**ПОЧЕМУ.** Реализация `KEYS-5`. Граница выбрана
|
||||||
транспортная, потому что только на ней известен источник значения, от
|
транспортная, потому что только на ней известен источник значения, от
|
||||||
которого зависит реакция (GKEY-8): store видит одинаковую строку независимо от
|
которого зависит реакция (GKEY-8): store видит одинаковую строку независимо от
|
||||||
того, пришла она из URL или из собственной формы, и ответить по-разному
|
того, пришла она из URL или из собственной формы, и ответить по-разному
|
||||||
@@ -82,7 +84,7 @@ callback'а бота — раньше, чем идентификатор поп
|
|||||||
**СЛЕДУЕТ.** Поля идентификаторов в доменных и store-структурах имеют тип
|
**СЛЕДУЕТ.** Поля идентификаторов в доменных и store-структурах имеют тип
|
||||||
`string`.
|
`string`.
|
||||||
|
|
||||||
**Почему.** Отдельный тип окупается только тогда, когда компилятор ловит им
|
**ПОЧЕМУ.** Отдельный тип окупается только тогда, когда компилятор ловит им
|
||||||
ошибку. От перепутывания двух идентификаторов одной семьи (`userID` и
|
ошибку. От перепутывания двух идентификаторов одной семьи (`userID` и
|
||||||
`authorID`) он не спасает — оба будут одного типа, и различают их имена
|
`authorID`) он не спасает — оба будут одного типа, и различают их имена
|
||||||
параметров. Зато он требует конверсий на каждой границе с sql-драйвером,
|
параметров. Зато он требует конверсий на каждой границе с sql-драйвером,
|
||||||
@@ -93,7 +95,7 @@ json и шаблонами, то есть даёт цену без выгоды.
|
|||||||
**ДОПУСКАЕТСЯ.** Когда в коде оказываются два вида идентификаторов, которые
|
**ДОПУСКАЕТСЯ.** Когда в коде оказываются два вида идентификаторов, которые
|
||||||
можно перепутать, для них заводятся различимые типы.
|
можно перепутать, для них заводятся различимые типы.
|
||||||
|
|
||||||
**Почему.** Явное разрешение нужно, чтобы GKEY-6 не читался как запрет на
|
**ПОЧЕМУ.** Явное разрешение нужно, чтобы GKEY-6 не читался как запрет на
|
||||||
типизацию навсегда. Условие названо ровно то, при котором тип начинает
|
типизацию навсегда. Условие названо ровно то, при котором тип начинает
|
||||||
работать: пока все идентификаторы — `string`, подстановка одного вида
|
работать: пока все идентификаторы — `string`, подстановка одного вида
|
||||||
вместо другого компилируется и обнаруживается только на данных.
|
вместо другого компилируется и обнаруживается только на данных.
|
||||||
@@ -108,7 +110,7 @@ json и шаблонами, то есть даёт цену без выгоды.
|
|||||||
| GKEY-8.1 | путь или query URL | 404 без обращения к store |
|
| GKEY-8.1 | путь или query URL | 404 без обращения к store |
|
||||||
| GKEY-8.2 | собственная форма, callback-данные кнопки | 400 либо понятное сообщение («кнопка устарела») |
|
| GKEY-8.2 | собственная форма, callback-данные кнопки | 400 либо понятное сообщение («кнопка устарела») |
|
||||||
|
|
||||||
**Почему.** Реализация `KEYS-5.1` и `KEYS-5.2` в терминах
|
**ПОЧЕМУ.** Реализация `KEYS-5.1` и `KEYS-5.2` в терминах
|
||||||
HTTP-кодов. В случае GKEY-8.1 снаружи это неотличимо от несуществующей записи —
|
HTTP-кодов. В случае GKEY-8.1 снаружи это неотличимо от несуществующей записи —
|
||||||
и хорошо: чужая или протухшая ссылка описывается так точно. В случае GKEY-8.2
|
и хорошо: чужая или протухшая ссылка описывается так точно. В случае GKEY-8.2
|
||||||
значение сформировало само приложение, и невалидность означает баг
|
значение сформировало само приложение, и невалидность означает баг
|
||||||
@@ -121,14 +123,8 @@ HTTP-кодов. В случае GKEY-8.1 снаружи это неотличи
|
|||||||
**НЕ ДОЛЖЕН.** Обработчик не конструирует доменный sentinel (например
|
**НЕ ДОЛЖЕН.** Обработчик не конструирует доменный sentinel (например
|
||||||
`ErrNotFound`), чтобы тут же сопоставить его со своим ответом.
|
`ErrNotFound`), чтобы тут же сопоставить его со своим ответом.
|
||||||
|
|
||||||
**Почему.** Инверсия правила «трансляция у источника» из конвенции
|
**ПОЧЕМУ.** Инверсия правила «трансляция у источника» из конвенции
|
||||||
`errors`. Sentinel — сообщение от слоя, который знает факт:
|
`errors`. Sentinel — сообщение от слоя, который знает факт:
|
||||||
строка не найдена, потому что store её искал. Сфабрикованный транспортом,
|
строка не найдена, потому что store её искал. Сфабрикованный транспортом,
|
||||||
он утверждает непроверенное, и по типу ошибки перестаёт быть видно, был ли
|
он утверждает непроверенное, и по типу ошибки перестаёт быть видно, был ли
|
||||||
вообще поход в хранилище — а на этом держится вся диагностика по ошибкам.
|
вообще поход в хранилище — а на этом держится вся диагностика по ошибкам.
|
||||||
|
|
||||||
<!-- local:механизировано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
<!-- local:отступления -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
topic: db-schema
|
||||||
prefix: MIGR
|
prefix: MIGR
|
||||||
|
lang: go
|
||||||
---
|
---
|
||||||
|
|
||||||
# Схема и миграции (SQLite, Go)
|
# Схема и миграции (SQLite, Go)
|
||||||
@@ -7,9 +9,9 @@ prefix: MIGR
|
|||||||
Как пишутся миграции и какие типы колонок выбираются для схемы SQLite в
|
Как пишутся миграции и какие типы колонок выбираются для схемы SQLite в
|
||||||
Go-приложении.
|
Go-приложении.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
## Область действия
|
## Область действия
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ Go-приложении.
|
|||||||
**ДОЛЖЕН.** Набор миграций репозитория применяется одним инструментом —
|
**ДОЛЖЕН.** Набор миграций репозитория применяется одним инструментом —
|
||||||
goose.
|
goose.
|
||||||
|
|
||||||
**Почему.** Журнал применённых версий goose держит в самой базе
|
**ПОЧЕМУ.** Журнал применённых версий goose держит в самой базе
|
||||||
(`goose_db_version`) и по нему решает, что ещё не накатывалось. Второй
|
(`goose_db_version`) и по нему решает, что ещё не накатывалось. Второй
|
||||||
инструмент заводит второй журнал: миграция, применённая одним, для другого
|
инструмент заводит второй журнал: миграция, применённая одним, для другого
|
||||||
выглядит неприменённой, и попытка накатить её повторно упирается в уже
|
выглядит неприменённой, и попытка накатить её повторно упирается в уже
|
||||||
@@ -37,7 +39,7 @@ goose.
|
|||||||
**СЛЕДУЕТ.** Миграции хранятся рядом с кодом, который работает с этой
|
**СЛЕДУЕТ.** Миграции хранятся рядом с кодом, который работает с этой
|
||||||
схемой.
|
схемой.
|
||||||
|
|
||||||
**Почему.** Миграция и код, читающий схему, — одно изменение: колонка
|
**ПОЧЕМУ.** Миграция и код, читающий схему, — одно изменение: колонка
|
||||||
появляется вместе с полем структуры и запросом. Лежащие в другом конце
|
появляется вместе с полем структуры и запросом. Лежащие в другом конце
|
||||||
дерева миграции выпадают из поля зрения при правке store, и уезжает либо
|
дерева миграции выпадают из поля зрения при правке store, и уезжает либо
|
||||||
код без миграции, либо миграция без кода; расходятся они на сервере, где
|
код без миграции, либо миграция без кода; расходятся они на сервере, где
|
||||||
@@ -52,7 +54,7 @@ goose.
|
|||||||
| MIGR-3.1 | DDL: создание таблиц, индексы, изменение структуры | SQL-файл |
|
| MIGR-3.1 | DDL: создание таблиц, индексы, изменение структуры | SQL-файл |
|
||||||
| MIGR-3.2 | требует кода: генерация идентификаторов, backfill, перенос данных между формами | Go-миграция (`goose.AddMigrationContext`) |
|
| MIGR-3.2 | требует кода: генерация идентификаторов, backfill, перенос данных между формами | Go-миграция (`goose.AddMigrationContext`) |
|
||||||
|
|
||||||
**Почему.** DDL ничего не вычисляет, и SQL-файл показывает ровно тот текст,
|
**ПОЧЕМУ.** DDL ничего не вычисляет, и SQL-файл показывает ровно тот текст,
|
||||||
который уедет в базу; обёртка на Go вокруг него добавляет место, где можно
|
который уедет в базу; обёртка на Go вокруг него добавляет место, где можно
|
||||||
ошибиться, не добавляя ничего к результату.
|
ошибиться, не добавляя ничего к результату.
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@ goose.
|
|||||||
**НЕ ДОЛЖЕН.** Откат схемы на сервере не выполняется down-миграцией;
|
**НЕ ДОЛЖЕН.** Откат схемы на сервере не выполняется down-миграцией;
|
||||||
ошибка исправляется новой миграцией вперёд.
|
ошибка исправляется новой миграцией вперёд.
|
||||||
|
|
||||||
**Почему.** Down на сервере не возвращает прежнее состояние, а имитирует
|
**ПОЧЕМУ.** Down на сервере не возвращает прежнее состояние, а имитирует
|
||||||
его: колонка, которую убрал up, восстанавливается пустой, а строки,
|
его: колонка, которую убрал up, восстанавливается пустой, а строки,
|
||||||
записанные уже по новой схеме, в старую форму не ложатся. Потеря при этом
|
записанные уже по новой схеме, в старую форму не ложатся. Потеря при этом
|
||||||
происходит молча — миграция отчитывается об успехе. Исправление, приехавшее
|
происходит молча — миграция отчитывается об успехе. Исправление, приехавшее
|
||||||
@@ -84,7 +86,7 @@ goose.
|
|||||||
| MIGR-5.1 | добавляет структуру: таблицу, колонку, индекс | пишется, убирает добавленное |
|
| MIGR-5.1 | добавляет структуру: таблицу, колонку, индекс | пишется, убирает добавленное |
|
||||||
| MIGR-5.2 | необратимо преобразует данные | не пишется |
|
| MIGR-5.2 | необратимо преобразует данные | не пишется |
|
||||||
|
|
||||||
**Почему.** Down — инструмент разработки, где ветку переключают туда-сюда,
|
**ПОЧЕМУ.** Down — инструмент разработки, где ветку переключают туда-сюда,
|
||||||
и именно там он обязан действительно обращать up. Имитация опаснее
|
и именно там он обязан действительно обращать up. Имитация опаснее
|
||||||
отсутствия: разработчик применяет её, получает схему прежней формы и
|
отсутствия: разработчик применяет её, получает схему прежней формы и
|
||||||
продолжает работу, не заметив, что колонка вернулась пустой. Отсутствующий
|
продолжает работу, не заметив, что колонка вернулась пустой. Отсутствующий
|
||||||
@@ -96,7 +98,7 @@ down останавливает сразу и заставляет пересо
|
|||||||
**ДОЛЖЕН.** Изменение структуры и правка ER-схемы в спеках едут одним
|
**ДОЛЖЕН.** Изменение структуры и правка ER-схемы в спеках едут одним
|
||||||
изменением.
|
изменением.
|
||||||
|
|
||||||
**Почему.** Диаграмму читают вместо DDL — в этом весь её смысл.
|
**ПОЧЕМУ.** Диаграмму читают вместо DDL — в этом весь её смысл.
|
||||||
Разошедшаяся с базой, она не бесполезна, а даёт неверный ответ, и заметить
|
Разошедшаяся с базой, она не бесполезна, а даёт неверный ответ, и заметить
|
||||||
это можно, только сверив её с миграциями, то есть проделав работу, которую
|
это можно, только сверив её с миграциями, то есть проделав работу, которую
|
||||||
диаграмма экономит. Отложенное обновление не делается: изменение уже
|
диаграмма экономит. Отложенное обновление не делается: изменение уже
|
||||||
@@ -112,7 +114,7 @@ down останавливает сразу и заставляет пересо
|
|||||||
**ДОЛЖЕН.** Поле-перечисление (`state`, `kind`, …) объявляется как `TEXT`
|
**ДОЛЖЕН.** Поле-перечисление (`state`, `kind`, …) объявляется как `TEXT`
|
||||||
без `CHECK`-ограничения на список значений.
|
без `CHECK`-ограничения на список значений.
|
||||||
|
|
||||||
**Почему.** `ALTER TABLE` в SQLite не умеет менять ограничения ни в одной
|
**ПОЧЕМУ.** `ALTER TABLE` в SQLite не умеет менять ограничения ни в одной
|
||||||
версии. Поэтому каждое новое значение перечисления в `CHECK (... IN (...))`
|
версии. Поэтому каждое новое значение перечисления в `CHECK (... IN (...))`
|
||||||
превращается из строки в коде в пересоздание таблицы по 12-шаговой
|
превращается из строки в коде в пересоздание таблицы по 12-шаговой
|
||||||
процедуре, с копированием данных и восстановлением внешних ключей.
|
процедуре, с копированием данных и восстановлением внешних ключей.
|
||||||
@@ -128,7 +130,7 @@ down останавливает сразу и заставляет пересо
|
|||||||
**ДОЛЖЕН.** Колонка с меткой времени объявляется как `TEXT`, значения
|
**ДОЛЖЕН.** Колонка с меткой времени объявляется как `TEXT`, значения
|
||||||
пишутся как RFC 3339 в UTC с суффиксом `Z` и фиксированной шириной.
|
пишутся как RFC 3339 в UTC с суффиксом `Z` и фиксированной шириной.
|
||||||
|
|
||||||
**Почему.** Типа даты в SQLite нет, поэтому единственное, что делает
|
**ПОЧЕМУ.** Типа даты в SQLite нет, поэтому единственное, что делает
|
||||||
значения сравнимыми, — договорённость о формате; сам формат выбран не
|
значения сравнимыми, — договорённость о формате; сам формат выбран не
|
||||||
здесь, а конвенцией `time` (`TIME-1`, `TIME-2`). Текст в нём сортируется
|
здесь, а конвенцией `time` (`TIME-1`, `TIME-2`). Текст в нём сортируется
|
||||||
лексикографически в том же порядке, что и
|
лексикографически в том же порядке, что и
|
||||||
@@ -141,7 +143,7 @@ down останавливает сразу и заставляет пересо
|
|||||||
**НЕ ДОЛЖЕН.** Колонка с меткой времени не получает значение по умолчанию
|
**НЕ ДОЛЖЕН.** Колонка с меткой времени не получает значение по умолчанию
|
||||||
на уровне схемы.
|
на уровне схемы.
|
||||||
|
|
||||||
**Почему.** Время ставит приложение, и умолчание в схеме заводит второй
|
**ПОЧЕМУ.** Время ставит приложение, и умолчание в схеме заводит второй
|
||||||
источник этого значения: пропущенное приложением поле не падает, а тихо
|
источник этого значения: пропущенное приложением поле не падает, а тихо
|
||||||
получает время сервера базы — расхождение обнаруживается по данным, а не
|
получает время сервера базы — расхождение обнаруживается по данным, а не
|
||||||
по ошибке.
|
по ошибке.
|
||||||
@@ -154,7 +156,7 @@ down останавливает сразу и заставляет пересо
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Булево значение хранится как `INTEGER` 0/1.
|
**ДОЛЖЕН.** Булево значение хранится как `INTEGER` 0/1.
|
||||||
|
|
||||||
**Почему.** Отдельного булева типа в SQLite нет, поэтому от разнобоя
|
**ПОЧЕМУ.** Отдельного булева типа в SQLite нет, поэтому от разнобоя
|
||||||
колонку удерживает только договорённость о представлении. Цена ошибки
|
колонку удерживает только договорённость о представлении. Цена ошибки
|
||||||
здесь несимметрична: строка `'true'` в булевом контексте приводится к
|
здесь несимметрична: строка `'true'` в булевом контексте приводится к
|
||||||
**0**, то есть даёт противоположный ответ, а не пустую выборку и не ошибку
|
**0**, то есть даёт противоположный ответ, а не пустую выборку и не ошибку
|
||||||
@@ -166,7 +168,7 @@ down останавливает сразу и заставляет пересо
|
|||||||
**ДОЛЖЕН.** Колонка ключа объявляется как `TEXT`, значение приходит из
|
**ДОЛЖЕН.** Колонка ключа объявляется как `TEXT`, значение приходит из
|
||||||
приложения.
|
приложения.
|
||||||
|
|
||||||
**Почему.** Здесь конвенция схемы ничего не решает — она реализует решение,
|
**ПОЧЕМУ.** Здесь конвенция схемы ничего не решает — она реализует решение,
|
||||||
принятое конвенцией `db-identifiers` (`KEYS-1`, `KEYS-2`). Повторить там
|
принятое конвенцией `db-identifiers` (`KEYS-1`, `KEYS-2`). Повторить там
|
||||||
ветвление или условие значило бы завести второй источник правды, и соседние
|
ветвление или условие значило бы завести второй источник правды, и соседние
|
||||||
таблицы разъехались бы по разным ответам на один вопрос.
|
таблицы разъехались бы по разным ответам на один вопрос.
|
||||||
@@ -179,7 +181,7 @@ down останавливает сразу и заставляет пересо
|
|||||||
**ДОЛЖЕН.** Там, где первичный ключ всё-таки целочисленный — существующая
|
**ДОЛЖЕН.** Там, где первичный ключ всё-таки целочисленный — существующая
|
||||||
схема, миграция легаси-таблицы, — он объявляется с `AUTOINCREMENT`.
|
схема, миграция легаси-таблицы, — он объявляется с `AUTOINCREMENT`.
|
||||||
|
|
||||||
**Почему.** Без него SQLite выдаёт rowid как `max(rowid)+1`, поэтому после
|
**ПОЧЕМУ.** Без него SQLite выдаёт rowid как `max(rowid)+1`, поэтому после
|
||||||
удаления последней строки номер переиспользуется. Протухшая ссылка на
|
удаления последней строки номер переиспользуется. Протухшая ссылка на
|
||||||
удалённую запись — из закладки, из чужой таблицы, из старого лога — молча
|
удалённую запись — из закладки, из чужой таблицы, из старого лога — молча
|
||||||
наводится на другую сущность и возвращает правдоподобный, но чужой ответ.
|
наводится на другую сущность и возвращает правдоподобный, но чужой ответ.
|
||||||
@@ -189,18 +191,9 @@ down останавливает сразу и заставляет пересо
|
|||||||
вставке — против этого пренебрежима. Для новых таблиц вопрос не возникает:
|
вставке — против этого пренебрежима. Для новых таблиц вопрос не возникает:
|
||||||
там ключ строковый (MIGR-11).
|
там ключ строковый (MIGR-11).
|
||||||
|
|
||||||
<!-- local:механизировано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
<!-- local:отступления -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Связано
|
## Связано
|
||||||
|
|
||||||
- конвенция `time` — формат меток времени.
|
- конвенция `time` — формат меток времени.
|
||||||
- конвенция `db-identifiers` — выбор первичных ключей.
|
- конвенция `db-identifiers` — выбор первичных ключей.
|
||||||
- конвенция `errors` — граничные ошибки `database/sql` транслируются в
|
- конвенция `errors` — граничные ошибки `database/sql` транслируются в
|
||||||
доменные у источника, в слое store.
|
доменные у источника, в слое store.
|
||||||
|
|
||||||
<!-- local:связано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
topic: errors
|
||||||
prefix: GERR
|
prefix: GERR
|
||||||
|
lang: go
|
||||||
---
|
---
|
||||||
|
|
||||||
# Ошибки
|
# Ошибки
|
||||||
@@ -8,9 +10,9 @@ prefix: GERR
|
|||||||
**логировать** — в конвенции `logging` (коротко: лог один раз на доменной
|
**логировать** — в конвенции `logging` (коротко: лог один раз на доменной
|
||||||
границе).
|
границе).
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
Две границы, о которых говорят правила ниже:
|
Две границы, о которых говорят правила ниже:
|
||||||
|
|
||||||
@@ -30,7 +32,7 @@ prefix: GERR
|
|||||||
**ДОЛЖЕН.** Ошибки создаются и оборачиваются через `errors` и
|
**ДОЛЖЕН.** Ошибки создаются и оборачиваются через `errors` и
|
||||||
`fmt.Errorf`; библиотеки со стек-трейсами не подключаются.
|
`fmt.Errorf`; библиотеки со стек-трейсами не подключаются.
|
||||||
|
|
||||||
**Почему.** Стек и цепочка обёрток решают одну задачу — локализацию места.
|
**ПОЧЕМУ.** Стек и цепочка обёрток решают одну задачу — локализацию места.
|
||||||
При дисциплине «каждый слой добавляет свой контекст» (GERR-3) цепочка сообщений
|
При дисциплине «каждый слой добавляет свой контекст» (GERR-3) цепочка сообщений
|
||||||
локализует не хуже, а стоит ничего: остальной контекст ошибки уже несёт
|
локализует не хуже, а стоит ничего: остальной контекст ошибки уже несёт
|
||||||
`slog`. Библиотека со стеками добавляет зависимость, собственный тип ошибки
|
`slog`. Библиотека со стеками добавляет зависимость, собственный тип ошибки
|
||||||
@@ -45,7 +47,7 @@ prefix: GERR
|
|||||||
**НЕ ДОЛЖЕН.** Пакет со стек-трейсами не заводится в отдельном месте
|
**НЕ ДОЛЖЕН.** Пакет со стек-трейсами не заводится в отдельном месте
|
||||||
кодовой базы ради конкретной отладки.
|
кодовой базы ради конкретной отладки.
|
||||||
|
|
||||||
**Почему.** В коде появляются два способа устроить ошибку, и вызывающий
|
**ПОЧЕМУ.** В коде появляются два способа устроить ошибку, и вызывающий
|
||||||
перестаёт знать, какой перед ним: обёртки склеиваются по-разному,
|
перестаёт знать, какой перед ним: обёртки склеиваются по-разному,
|
||||||
`errors.Is` работает не везде одинаково. Хуже второе: боль, снятая
|
`errors.Is` работает не везде одинаково. Хуже второе: боль, снятая
|
||||||
локально, перестаёт накапливаться — а накопление и есть единственный
|
локально, перестаёт накапливаться — а накопление и есть единственный
|
||||||
@@ -56,7 +58,7 @@ prefix: GERR
|
|||||||
**ДОЛЖЕН.** Ошибка, возвращаемая на уровень выше, оборачивается с
|
**ДОЛЖЕН.** Ошибка, возвращаемая на уровень выше, оборачивается с
|
||||||
контекстом: `fmt.Errorf("parse magnet: %w", err)`.
|
контекстом: `fmt.Errorf("parse magnet: %w", err)`.
|
||||||
|
|
||||||
**Почему.** На этом держится GERR-1: цепочка заменяет стек ровно настолько,
|
**ПОЧЕМУ.** На этом держится GERR-1: цепочка заменяет стек ровно настолько,
|
||||||
насколько слои в неё пишут. Слой, пробросивший ошибку без своего контекста,
|
насколько слои в неё пишут. Слой, пробросивший ошибку без своего контекста,
|
||||||
стирает участок пути — по итоговому сообщению нельзя сказать, через какую
|
стирает участок пути — по итоговому сообщению нельзя сказать, через какую
|
||||||
операцию ошибка прошла, и отладка «no such file» начинается с чтения всего
|
операцию ошибка прошла, и отладка «no such file» начинается с чтения всего
|
||||||
@@ -72,7 +74,7 @@ prefix: GERR
|
|||||||
| GERR-4.1 | вызывающий может инспектировать причину (обычный случай) | `%w` |
|
| GERR-4.1 | вызывающий может инспектировать причину (обычный случай) | `%w` |
|
||||||
| GERR-4.2 | причину сознательно не раскрываем | `%v` |
|
| GERR-4.2 | причину сознательно не раскрываем | `%v` |
|
||||||
|
|
||||||
**Почему.** Возражение против дефолтного `%w` — «обёрнутая ошибка
|
**ПОЧЕМУ.** Возражение против дефолтного `%w` — «обёрнутая ошибка
|
||||||
становится частью API» — относится к библиотекам с внешними потребителями.
|
становится частью API» — относится к библиотекам с внешними потребителями.
|
||||||
Сервис же — приложение: внешнего Go-API нет, весь код наш, контракт
|
Сервис же — приложение: внешнего Go-API нет, весь код наш, контракт
|
||||||
меняется вместе с вызывающими. Зато `%v` в середине цепочки обрывает
|
меняется вместе с вызывающими. Зато `%v` в середине цепочки обрывает
|
||||||
@@ -86,7 +88,7 @@ prefix: GERR
|
|||||||
**НЕ ДОЛЖЕН.** `%v` не используется как средство не пустить внутреннюю
|
**НЕ ДОЛЖЕН.** `%v` не используется как средство не пустить внутреннюю
|
||||||
ошибку наружу.
|
ошибку наружу.
|
||||||
|
|
||||||
**Почему.** Обрыв цепочки внутри кода не мешает тексту уехать наружу
|
**ПОЧЕМУ.** Обрыв цепочки внутри кода не мешает тексту уехать наружу
|
||||||
целиком: наружу отдаёт внешняя граница, и если она отдаёт `err.Error()`,
|
целиком: наружу отдаёт внешняя граница, и если она отдаёт `err.Error()`,
|
||||||
детали утекут при любом глаголе. Подмена не решает задачу, ради которой
|
детали утекут при любом глаголе. Подмена не решает задачу, ради которой
|
||||||
сделана, а плату берёт сразу — `errors.Is` ломается у всех вызывающих.
|
сделана, а плату берёт сразу — `errors.Is` ломается у всех вызывающих.
|
||||||
@@ -96,7 +98,7 @@ prefix: GERR
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** Без точки в конце, без «failed to» и «error».
|
**СЛЕДУЕТ.** Без точки в конце, без «failed to» и «error».
|
||||||
|
|
||||||
**Почему.** Цепочка склеивается в одну строку через `": "`, и обёртка
|
**ПОЧЕМУ.** Цепочка склеивается в одну строку через `": "`, и обёртка
|
||||||
читается как «контекст: причина» — заглавные буквы и точки рвут эту строку
|
читается как «контекст: причина» — заглавные буквы и точки рвут эту строку
|
||||||
на середине. Слова «failed» и «error» не несут информации: то, что перед
|
на середине. Слова «failed» и «error» не несут информации: то, что перед
|
||||||
нами ошибка, известно из того, что это ошибка. Зато повторяются они на
|
нами ошибка, известно из того, что это ошибка. Зато повторяются они на
|
||||||
@@ -106,7 +108,7 @@ prefix: GERR
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** В обёртку идёт то, что делал слой: `"link target: %w"`.
|
**СЛЕДУЕТ.** В обёртку идёт то, что делал слой: `"link target: %w"`.
|
||||||
|
|
||||||
**Почему.** Обёртка ценна ровно тем, что сужает место (GERR-3). «something
|
**ПОЧЕМУ.** Обёртка ценна ровно тем, что сужает место (GERR-3). «something
|
||||||
failed» не сужает ничего и при этом занимает в сообщении место, которое мог
|
failed» не сужает ничего и при этом занимает в сообщении место, которое мог
|
||||||
бы занять единственный полезный здесь факт — имя операции.
|
бы занять единственный полезный здесь факт — имя операции.
|
||||||
|
|
||||||
@@ -115,7 +117,7 @@ failed» не сужает ничего и при этом занимает в
|
|||||||
**НЕ СЛЕДУЕТ.** Обёртка не пересказывает то, что уже сказал уровень ниже:
|
**НЕ СЛЕДУЕТ.** Обёртка не пересказывает то, что уже сказал уровень ниже:
|
||||||
`"add to qbt: %w"`, а не `"add download failed: add to qbt failed: …"`.
|
`"add to qbt: %w"`, а не `"add download failed: add to qbt failed: …"`.
|
||||||
|
|
||||||
**Почему.** Повтор удлиняет сообщение, не добавляя локализации: одно и то
|
**ПОЧЕМУ.** Повтор удлиняет сообщение, не добавляя локализации: одно и то
|
||||||
же событие названо дважды. Читателю приходится проверять, не два ли это
|
же событие названо дважды. Читателю приходится проверять, не два ли это
|
||||||
разных места в коде, — то есть заикание не просто бесполезно, оно стоит
|
разных места в коде, — то есть заикание не просто бесполезно, оно стоит
|
||||||
времени при каждом чтении лога.
|
времени при каждом чтении лога.
|
||||||
@@ -132,7 +134,7 @@ failed» не сужает ничего и при этом занимает в
|
|||||||
возникла: `sql.ErrNoRows` → `store.ErrNotFound` в слое store; то же для
|
возникла: `sql.ErrNoRows` → `store.ErrNotFound` в слое store; то же для
|
||||||
HTTP-клиентов, файловой системы, внешних SDK.
|
HTTP-клиентов, файловой системы, внешних SDK.
|
||||||
|
|
||||||
**Почему.** Иначе тип зависимости становится частью контракта всех слоёв
|
**ПОЧЕМУ.** Иначе тип зависимости становится частью контракта всех слоёв
|
||||||
выше: чтобы отличить «нет записи», доменный код импортирует `database/sql`
|
выше: чтобы отличить «нет записи», доменный код импортирует `database/sql`
|
||||||
и сравнивает с его sentinel'ом. Замена хранилища или SDK правит тогда не
|
и сравнивает с его sentinel'ом. Замена хранилища или SDK правит тогда не
|
||||||
адаптер, а все ветвления в приложении — притом что снаружи адаптера
|
адаптер, а все ветвления в приложении — притом что снаружи адаптера
|
||||||
@@ -148,7 +150,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
| GERR-10.1 | ветвление по условию: нет записи, дубликат, неподдерживаемый источник | sentinel `var ErrNotFound = errors.New("not found")`, проверка `errors.Is` |
|
| GERR-10.1 | ветвление по условию: нет записи, дубликат, неподдерживаемый источник | sentinel `var ErrNotFound = errors.New("not found")`, проверка `errors.Is` |
|
||||||
| GERR-10.2 | данные ошибки: поле валидации, код, лимит | тип с полями и методом `Error()`, извлечение `errors.As` |
|
| GERR-10.2 | данные ошибки: поле валидации, код, лимит | тип с полями и методом `Error()`, извлечение `errors.As` |
|
||||||
|
|
||||||
**Почему.** Sentinel — одно значение; сравнение с ним не зависит от
|
**ПОЧЕМУ.** Sentinel — одно значение; сравнение с ним не зависит от
|
||||||
структуры ошибки и переживает добавление полей. Тип заводится ради данных,
|
структуры ошибки и переживает добавление полей. Тип заводится ради данных,
|
||||||
и тип без данных отвечает вызывающему ровно то же, что sentinel, но ценой
|
и тип без данных отвечает вызывающему ровно то же, что sentinel, но ценой
|
||||||
объявления, `errors.As` и вопроса «сравнивать по типу или по значению» на
|
объявления, `errors.As` и вопроса «сравнивать по типу или по значению» на
|
||||||
@@ -159,7 +161,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Ветвление по содержимому `err.Error()` не используется.
|
**НЕ ДОЛЖЕН.** Ветвление по содержимому `err.Error()` не используется.
|
||||||
|
|
||||||
**Почему.** Текст сообщения — не контракт: GERR-6–GERR-8 разрешают
|
**ПОЧЕМУ.** Текст сообщения — не контракт: GERR-6–GERR-8 разрешают
|
||||||
переписывать его свободно. Правка формулировки в нижнем слое молча ломает
|
переписывать его свободно. Правка формулировки в нижнем слое молча ломает
|
||||||
ветвление наверху, и компилятор этого не видит. Это то же самое, что
|
ветвление наверху, и компилятор этого не видит. Это то же самое, что
|
||||||
публичный API из строки лога.
|
публичный API из строки лога.
|
||||||
@@ -175,7 +177,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
**ДОЛЖЕН.** В лог уходит вся цепочка `%w` с контекстом; где и когда именно
|
**ДОЛЖЕН.** В лог уходит вся цепочка `%w` с контекстом; где и когда именно
|
||||||
— конвенция `logging`.
|
— конвенция `logging`.
|
||||||
|
|
||||||
**Почему.** Цепочка — единственный носитель диагностики (GERR-1), и
|
**ПОЧЕМУ.** Цепочка — единственный носитель диагностики (GERR-1), и
|
||||||
единственный канал, где её можно показать целиком, — тот, который видит
|
единственный канал, где её можно показать целиком, — тот, который видит
|
||||||
владелец. Не записанная там, она не сохранится нигде: наружу идёт
|
владелец. Не записанная там, она не сохранится нигде: наружу идёт
|
||||||
нейтральное сообщение (GERR-13), и восстанавливать причину будет не из чего.
|
нейтральное сообщение (GERR-13), и восстанавливать причину будет не из чего.
|
||||||
@@ -185,7 +187,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
**ДОЛЖЕН.** Наружу идёт человекочитаемый текст по доменной ошибке, а не
|
**ДОЛЖЕН.** Наружу идёт человекочитаемый текст по доменной ошибке, а не
|
||||||
`err.Error()` и не детали реализации (`database/sql`, пути, стек).
|
`err.Error()` и не детали реализации (`database/sql`, пути, стек).
|
||||||
|
|
||||||
**Почему.** Внутренние детали пользователю нечитаемы, а владельцу не нужны
|
**ПОЧЕМУ.** Внутренние детали пользователю нечитаемы, а владельцу не нужны
|
||||||
— у него есть лог (GERR-12). Зато они раскрывают устройство системы — имена
|
— у него есть лог (GERR-12). Зато они раскрывают устройство системы — имена
|
||||||
таблиц, пути на диске, версии зависимостей — тому, кто их знать не должен,
|
таблиц, пути на диске, версии зависимостей — тому, кто их знать не должен,
|
||||||
причём раскрывают именно в момент, когда что-то пошло не так.
|
причём раскрывают именно в момент, когда что-то пошло не так.
|
||||||
@@ -196,7 +198,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
«При обработке загрузки произошла ошибка, download_id=…» вместо «произошла
|
«При обработке загрузки произошла ошибка, download_id=…» вместо «произошла
|
||||||
ошибка».
|
ошибка».
|
||||||
|
|
||||||
**Почему.** GERR-13 забирает у пользователя всю фактуру; без ключа его
|
**ПОЧЕМУ.** GERR-13 забирает у пользователя всю фактуру; без ключа его
|
||||||
обращение звучит как «у меня что-то не работает», и владелец ищет запись в
|
обращение звучит как «у меня что-то не работает», и владелец ищет запись в
|
||||||
логе по времени и догадкам. Ключ соединяет нейтральный ответ с полной
|
логе по времени и догадкам. Ключ соединяет нейтральный ответ с полной
|
||||||
ошибкой в логе, не раскрывая наружу ничего сверх того, что пользователь уже
|
ошибкой в логе, не раскрывая наружу ничего сверх того, что пользователь уже
|
||||||
@@ -208,7 +210,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
задаётся один раз; транспорт без статусов (бот) берёт из него только
|
задаётся один раз; транспорт без статусов (бот) берёт из него только
|
||||||
сообщение.
|
сообщение.
|
||||||
|
|
||||||
**Почему.** Иначе одна и та же ошибка отвечает по-разному в HTTP и в боте,
|
**ПОЧЕМУ.** Иначе одна и та же ошибка отвечает по-разному в HTTP и в боте,
|
||||||
и расхождение обнаруживается не как дефект, а как жалоба. Вторая причина
|
и расхождение обнаруживается не как дефект, а как жалоба. Вторая причина
|
||||||
важнее: единственная точка — это место, куда механически дописывается новая
|
важнее: единственная точка — это место, куда механически дописывается новая
|
||||||
ветвь (GERR-16). Маппинг, размазанный по хендлерам, требование «дописать везде»
|
ветвь (GERR-16). Маппинг, размазанный по хендлерам, требование «дописать везде»
|
||||||
@@ -219,21 +221,18 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
**ДОЛЖЕН.** Ожидаемый отказ (конфликт, валидация) заводится sentinel'ом и
|
**ДОЛЖЕН.** Ожидаемый отказ (конфликт, валидация) заводится sentinel'ом и
|
||||||
добавляется в маппинг (GERR-15) тем же изменением.
|
добавляется в маппинг (GERR-15) тем же изменением.
|
||||||
|
|
||||||
**Почему.** Ветка `default` врёт в обе стороны: транспорт отдаёт 500
|
**ПОЧЕМУ.** Ветка `default` врёт в обе стороны: транспорт отдаёт 500
|
||||||
«внутренняя ошибка» на нормальный конфликт, а логирующая граница списывает
|
«внутренняя ошибка» на нормальный конфликт, а логирующая граница списывает
|
||||||
его в `ERROR` вместо `DEBUG`. Второе хуже первого — штатные отказы начинают
|
его в `ERROR` вместо `DEBUG`. Второе хуже первого — штатные отказы начинают
|
||||||
шуметь в логе ровно там, где по нему ищут настоящие поломки.
|
шуметь в логе ровно там, где по нему ищут настоящие поломки.
|
||||||
|
|
||||||
<!-- local:маппинг -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
### GERR-25. Непокрытая маппингом ошибка — 500 и `ERROR` с признаком
|
### GERR-25. Непокрытая маппингом ошибка — 500 и `ERROR` с признаком
|
||||||
|
|
||||||
**ДОЛЖЕН.** Доменная ошибка, для которой в маппинге (GERR-15) нет ветви, отдаёт
|
**ДОЛЖЕН.** Доменная ошибка, для которой в маппинге (GERR-15) нет ветви, отдаёт
|
||||||
наружу 500 и нейтральное «внутренняя ошибка», а в лог идёт `ERROR` с
|
наружу 500 и нейтральное «внутренняя ошибка», а в лог идёт `ERROR` с
|
||||||
признаком того, что маппинг её не знает.
|
признаком того, что маппинг её не знает.
|
||||||
|
|
||||||
**Почему.** Непокрытая ошибка — не класс отказа, а дефект: ветвь забыли
|
**ПОЧЕМУ.** Непокрытая ошибка — не класс отказа, а дефект: ветвь забыли
|
||||||
завести вопреки GERR-16. Адресат у неё владелец в смысле «надо чинить», отсюда
|
завести вопреки GERR-16. Адресат у неё владелец в смысле «надо чинить», отсюда
|
||||||
`ERROR` — уровень выбирается по адресату (конвенция `logging`). Статус
|
`ERROR` — уровень выбирается по адресату (конвенция `logging`). Статус
|
||||||
тоже не выбирается: известное пользовательское состояние лежало бы в
|
тоже не выбирается: известное пользовательское состояние лежало бы в
|
||||||
@@ -259,7 +258,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
Появился второй зритель или публичный доступ к экрану состояния —
|
Появился второй зритель или публичный доступ к экрану состояния —
|
||||||
поверхность стала публичным каналом, и на неё распространяется GERR-17.1.
|
поверхность стала публичным каналом, и на неё распространяется GERR-17.1.
|
||||||
|
|
||||||
**Почему.** Транзиентный ответ читает тот, кто нажал кнопку: сырой текст
|
**ПОЧЕМУ.** Транзиентный ответ читает тот, кто нажал кнопку: сырой текст
|
||||||
ему ничего не объясняет, а владельцу не нужен — у него лог. Персистентную
|
ему ничего не объясняет, а владельцу не нужен — у него лог. Персистентную
|
||||||
диагностику читает владелец, и она отвечает на вопрос «почему сломалась вот
|
диагностику читает владелец, и она отвечает на вопрос «почему сломалась вот
|
||||||
эта запись» через месяц, когда лог уже ротировался; нейтральное «произошла
|
эта запись» через месяц, когда лог уже ротировался; нейтральное «произошла
|
||||||
@@ -272,7 +271,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
**НЕ ДОЛЖЕН.** Токены, пароли и ключи не попадают ни в транзиентный ответ,
|
**НЕ ДОЛЖЕН.** Токены, пароли и ключи не попадают ни в транзиентный ответ,
|
||||||
ни в персистентную диагностику; источник вычищается на границе клиента.
|
ни в персистентную диагностику; источник вычищается на границе клиента.
|
||||||
|
|
||||||
**Почему.** Запрет абсолютен, потому что персистентная диагностика живёт в
|
**ПОЧЕМУ.** Запрет абсолютен, потому что персистентная диагностика живёт в
|
||||||
БД: уезжает в бэкапы, попадает в скриншоты и выгрузки и переживает ротацию
|
БД: уезжает в бэкапы, попадает в скриншоты и выгрузки и переживает ротацию
|
||||||
самого секрета. Вычистка на границе клиента — единственное место, где ещё
|
самого секрета. Вычистка на границе клиента — единственное место, где ещё
|
||||||
известно, какие поля запроса секретны: дальше ошибка едет как текст, и
|
известно, какие поля запроса секретны: дальше ошибка едет как текст, и
|
||||||
@@ -283,7 +282,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
**ДОЛЖЕН.** Персистентная диагностика не кладётся в доменное поле, которое
|
**ДОЛЖЕН.** Персистентная диагностика не кладётся в доменное поле, которое
|
||||||
показывают пользователю.
|
показывают пользователю.
|
||||||
|
|
||||||
**Почему.** Различие GERR-17.1 и GERR-17.2 держится на том, что у поверхностей
|
**ПОЧЕМУ.** Различие GERR-17.1 и GERR-17.2 держится на том, что у поверхностей
|
||||||
разные поля. Одно поле на оба назначения означает, что при первом же показе
|
разные поля. Одно поле на оба назначения означает, что при первом же показе
|
||||||
записи наружу сырой текст уедет туда же — не по решению, а потому что поле
|
записи наружу сырой текст уедет туда же — не по решению, а потому что поле
|
||||||
одно.
|
одно.
|
||||||
@@ -295,7 +294,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
**ДОЛЖЕН.** Паникой отмечается нарушенный инвариант (баг программиста) и
|
**ДОЛЖЕН.** Паникой отмечается нарушенный инвариант (баг программиста) и
|
||||||
ошибка инициализации, из которой нельзя стартовать.
|
ошибка инициализации, из которой нельзя стартовать.
|
||||||
|
|
||||||
**Почему.** Паника не оставляет вызывающему выбора: обработать её на месте
|
**ПОЧЕМУ.** Паника не оставляет вызывающему выбора: обработать её на месте
|
||||||
нельзя, можно только уронить единицу обработки. Это верный ответ, когда
|
нельзя, можно только уронить единицу обработки. Это верный ответ, когда
|
||||||
состояние процесса перестало описываться кодом: работа с нарушенным
|
состояние процесса перестало описываться кодом: работа с нарушенным
|
||||||
инвариантом опаснее падения, а сервис, стартовавший без обязательной
|
инвариантом опаснее падения, а сервис, стартовавший без обязательной
|
||||||
@@ -306,7 +305,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
**НЕ ДОЛЖЕН.** Паника не используется для управления потоком: нет сети,
|
**НЕ ДОЛЖЕН.** Паника не используется для управления потоком: нет сети,
|
||||||
плохой ввод, отсутствующая запись возвращаются как `error`.
|
плохой ввод, отсутствующая запись возвращаются как `error`.
|
||||||
|
|
||||||
**Почему.** Сигнатура — единственное, что сообщает вызывающему о возможном
|
**ПОЧЕМУ.** Сигнатура — единственное, что сообщает вызывающему о возможном
|
||||||
отказе; отказ, брошенный паникой, из неё не виден, и компилятор не заставит
|
отказе; отказ, брошенный паникой, из неё не виден, и компилятор не заставит
|
||||||
его обработать. Дальше такая паника долетает до recover-границы (GERR-22), где
|
его обработать. Дальше такая паника долетает до recover-границы (GERR-22), где
|
||||||
неотличима от бага: штатный отказ попадает в лог со стеком и с интонацией
|
неотличима от бага: штатный отказ попадает в лог со стеком и с интонацией
|
||||||
@@ -321,7 +320,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
| GERR-22.1 | HTTP-хендлер | `net/http` восстанавливает панику сам и процесс не роняет; свой `recover` нужен, чтобы отдать контролируемый 500 и записать событие в `slog`, а не в stdlib-логгер |
|
| GERR-22.1 | HTTP-хендлер | `net/http` восстанавливает панику сам и процесс не роняет; свой `recover` нужен, чтобы отдать контролируемый 500 и записать событие в `slog`, а не в stdlib-логгер |
|
||||||
| GERR-22.2 | цикл обработки апдейтов бота, фоновый воркер | паника в горутине роняет процесс; `recover` ставится в той же горутине |
|
| GERR-22.2 | цикл обработки апдейтов бота, фоновый воркер | паника в горутине роняет процесс; `recover` ставится в той же горутине |
|
||||||
|
|
||||||
**Почему.** `recover` работает только в той горутине, где случилась паника,
|
**ПОЧЕМУ.** `recover` работает только в той горутине, где случилась паника,
|
||||||
поэтому «у нас есть recover в HTTP» не защищает воркер — граница нужна у
|
поэтому «у нас есть recover в HTTP» не защищает воркер — граница нужна у
|
||||||
каждой единицы отдельно. Без неё один плохой апдейт бота или одна запись с
|
каждой единицы отдельно. Без неё один плохой апдейт бота или одна запись с
|
||||||
неожиданным полем гасят весь сервис, включая части, к этой ошибке
|
неожиданным полем гасят весь сервис, включая части, к этой ошибке
|
||||||
@@ -333,7 +332,7 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Логирующий `recover` кладёт в запись стек.
|
**ДОЛЖЕН.** Логирующий `recover` кладёт в запись стек.
|
||||||
|
|
||||||
**Почему.** Это единственное место, где стек нужен (GERR-1): у восстановленной
|
**ПОЧЕМУ.** Это единственное место, где стек нужен (GERR-1): у восстановленной
|
||||||
паники цепочки `%w` нет вовсе. «index out of range» без стека не
|
паники цепочки `%w` нет вовсе. «index out of range» без стека не
|
||||||
диагностируется в принципе — сообщение не называет ни файла, ни операции,
|
диагностируется в принципе — сообщение не называет ни файла, ни операции,
|
||||||
по нему нельзя сказать даже, в каком пакете упало.
|
по нему нельзя сказать даже, в каком пакете упало.
|
||||||
@@ -347,10 +346,11 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
|
|
||||||
| № | Где перехвачена паника | Что дальше |
|
| № | Где перехвачена паника | Что дальше |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| GERR-26.1 | обработчик HTTP-запроса | ответ 500, если он ещё не начат; процесс и прочие запросы не затрагиваются |
|
| GERR-26.1 | обработчик HTTP-запроса, паника любая, кроме сигнала намеренного прерывания | ответ 500, если он ещё не начат; процесс и прочие запросы не затрагиваются |
|
||||||
| GERR-26.2 | итерация цикла обработки — бот, воркер | цикл продолжается со следующего элемента, упавший элемент повторно не берётся |
|
| GERR-26.2 | итерация цикла обработки — бот, воркер | цикл продолжается со следующего элемента, упавший элемент повторно не берётся |
|
||||||
|
| GERR-26.3 | обработчик HTTP-запроса, паника — сигнал намеренного прерывания (`http.ErrAbortHandler`) | значение пробрасывается дальше, ответ не подменяется |
|
||||||
|
|
||||||
**Почему.** Паника внутри обработки одного элемента почти всегда говорит о
|
**ПОЧЕМУ.** Паника внутри обработки одного элемента почти всегда говорит о
|
||||||
баге в работе с данными этого элемента, а не о порче общего состояния, —
|
баге в работе с данными этого элемента, а не о порче общего состояния, —
|
||||||
останавливать всё остальное не за что. Довод «let it crash» здесь работает
|
останавливать всё остальное не за что. Довод «let it crash» здесь работает
|
||||||
не буквально: в OTP падает изолированный процесс под супервизором, а не узел
|
не буквально: в OTP падает изолированный процесс под супервизором, а не узел
|
||||||
@@ -359,10 +359,11 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
обязательным (GERR-22): неперехваченная паника в любой горутине завершает весь
|
обязательным (GERR-22): неперехваченная паника в любой горутине завершает весь
|
||||||
процесс.
|
процесс.
|
||||||
|
|
||||||
Продолжать, не исключив упавший элемент, нельзя: детерминированная паника
|
Исключение упавшего элемента в GERR-26.2 — не осторожность, а условие
|
||||||
даёт бесконечный цикл — тот же элемент, тот же стек, залитый лог и нулевой
|
прогресса: детерминированная паника даёт бесконечный цикл — тот же элемент,
|
||||||
прогресс. Это классический poison message, и лекарство берём то же, что
|
тот же стек, залитый лог и нулевой прогресс. Это классический poison message,
|
||||||
принято в очередях: элемент выводится из оборота, а не берётся снова. У
|
и лекарство здесь то же, что принято в очередях: элемент выводится из
|
||||||
|
оборота, а не берётся снова. У
|
||||||
цикла, который и так подтверждает прогресс — сдвигает офсет, помечает
|
цикла, который и так подтверждает прогресс — сдвигает офсет, помечает
|
||||||
строку состоянием, — механизм для этого уже есть, заводить отдельный не
|
строку состоянием, — механизм для этого уже есть, заводить отдельный не
|
||||||
нужно.
|
нужно.
|
||||||
@@ -372,16 +373,17 @@ 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`
|
||||||
|
|
||||||
**СЛЕДУЕТ.** Валидация конфига и подобные проверки отдают все проблемы
|
**СЛЕДУЕТ.** Валидация конфига и подобные проверки отдают все проблемы
|
||||||
разом; проверка собранного — по-прежнему через `errors.Is`.
|
разом; проверка собранного — по-прежнему через `errors.Is`.
|
||||||
|
|
||||||
**Почему.** Возврат первой ошибки превращает починку конфига в серию
|
**ПОЧЕМУ.** Возврат первой ошибки превращает починку конфига в серию
|
||||||
перезапусков, по одной проблеме за прогон. Склейка сообщений в строку даёт
|
перезапусков, по одной проблеме за прогон. Склейка сообщений в строку даёт
|
||||||
тот же список, но убивает ветвление: `errors.Is` по такому результату не
|
тот же список, но убивает ветвление: `errors.Is` по такому результату не
|
||||||
находит ничего, и вызывающий остаётся с текстом, матчить который запрещено
|
находит ничего, и вызывающий остаётся с текстом, матчить который запрещено
|
||||||
@@ -391,6 +393,3 @@ HTTP-клиентов, файловой системы, внешних SDK.
|
|||||||
|
|
||||||
- конвенция `logging` — где и когда ошибка попадает в лог.
|
- конвенция `logging` — где и когда ошибка попадает в лог.
|
||||||
- `KEYS-7` — формат корреляционного ключа из `GERR-14`.
|
- `KEYS-7` — формат корреляционного ключа из `GERR-14`.
|
||||||
|
|
||||||
<!-- local:механизировано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
topic: logging
|
||||||
prefix: SLOG
|
prefix: SLOG
|
||||||
extends: arch/time.md
|
lang: go
|
||||||
---
|
---
|
||||||
|
|
||||||
# Логирование
|
# Логирование
|
||||||
@@ -9,9 +10,9 @@ extends: arch/time.md
|
|||||||
спецификация поведения: наблюдаемые требования к логам, входящие в контракт
|
спецификация поведения: наблюдаемые требования к логам, входящие в контракт
|
||||||
функциональности, живут в спеках.
|
функциональности, живут в спеках.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
Лог читают инструментами, а не глазами: повседневно — `jq`
|
Лог читают инструментами, а не глазами: повседневно — `jq`
|
||||||
(`jq 'select(.download_id=="a1b2")' app.jsonl`), тяжёлое (агрегации, JOIN) —
|
(`jq 'select(.download_id=="a1b2")' app.jsonl`), тяжёлое (агрегации, JOIN) —
|
||||||
@@ -29,7 +30,7 @@ DuckDB поверх JSONL прямо из файла. Отсюда почти в
|
|||||||
**ДОЛЖЕН.** Хендлер — `slog.JSONHandler`, одинаково в разработке и в
|
**ДОЛЖЕН.** Хендлер — `slog.JSONHandler`, одинаково в разработке и в
|
||||||
проде.
|
проде.
|
||||||
|
|
||||||
**Почему.** Довод не в том, что текстовый вывод «расходит поля»: смена
|
**ПОЧЕМУ.** Довод не в том, что текстовый вывод «расходит поля»: смена
|
||||||
хендлера структуру атрибутов не меняет. Довод в читателе — с текстовым
|
хендлера структуру атрибутов не меняет. Довод в читателе — с текстовым
|
||||||
dev-выводом перестаёшь ежедневно гонять собственные `jq`-пайплайны, и
|
dev-выводом перестаёшь ежедневно гонять собственные `jq`-пайплайны, и
|
||||||
поломки словаря (опечатка в имени поля, потерянный атрибут, склеенное
|
поломки словаря (опечатка в имени поля, потерянный атрибут, склеенное
|
||||||
@@ -40,7 +41,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Каждая величина — отдельный ключ со значением своего типа.
|
**ДОЛЖЕН.** Каждая величина — отдельный ключ со значением своего типа.
|
||||||
|
|
||||||
**Почему.** Фильтрация и агрегация работают по ключам; величина, вклеенная
|
**ПОЧЕМУ.** Фильтрация и агрегация работают по ключам; величина, вклеенная
|
||||||
в текст, достаётся только регуляркой, а регулярка ломается при первой же
|
в текст, достаётся только регуляркой, а регулярка ломается при первой же
|
||||||
правке формулировки. Тип важен отдельно от ключа: число внутри строки не
|
правке формулировки. Тип важен отдельно от ключа: число внутри строки не
|
||||||
сравнивается и не суммируется, то есть попадает в лог, но не в отчёт.
|
сравнивается и не суммируется, то есть попадает в лог, но не в отчёт.
|
||||||
@@ -50,7 +51,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
**ДОЛЖЕН.** `time` приводится к UTC через `ReplaceAttr` по `slog.TimeKey`
|
**ДОЛЖЕН.** `time` приводится к UTC через `ReplaceAttr` по `slog.TimeKey`
|
||||||
(см. конвенцию `time`).
|
(см. конвенцию `time`).
|
||||||
|
|
||||||
**Почему.** По умолчанию UTC не получится: встроенные хендлеры пишут время
|
**ПОЧЕМУ.** По умолчанию UTC не получится: встроенные хендлеры пишут время
|
||||||
в зоне самого `time.Time`, то есть в локальной зоне процесса. Записи одного
|
в зоне самого `time.Time`, то есть в локальной зоне процесса. Записи одного
|
||||||
процесса до и после смены TZ (или записи рядом с данными из БД) перестают
|
процесса до и после смены TZ (или записи рядом с данными из БД) перестают
|
||||||
складываться в одну хронологию, причём сдвиг на целые часы глазом не виден
|
складываться в одну хронологию, причём сдвиг на целые часы глазом не виден
|
||||||
@@ -68,7 +69,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
**ДОЛЖЕН.** Текст сообщения не собирается из переменных:
|
**ДОЛЖЕН.** Текст сообщения не собирается из переменных:
|
||||||
`log.Info("download accepted", "download_id", id)`.
|
`log.Info("download accepted", "download_id", id)`.
|
||||||
|
|
||||||
**Почему.** `msg` — то, по чему записи группируют и считают. Интерполяция
|
**ПОЧЕМУ.** `msg` — то, по чему записи группируют и считают. Интерполяция
|
||||||
превращает одну категорию в множество уникальных строк, и вопрос «сколько
|
превращает одну категорию в множество уникальных строк, и вопрос «сколько
|
||||||
раз это случилось» перестаёт решаться группировкой. Нижний регистр — чтобы
|
раз это случилось» перестаёт решаться группировкой. Нижний регистр — чтобы
|
||||||
одна категория не двоилась на варианты, различающиеся только заглавной
|
одна категория не двоилась на варианты, различающиеся только заглавной
|
||||||
@@ -79,7 +80,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
**НЕ ДОЛЖЕН.** `recognition done`, а не `recognize: done`; подсистема —
|
**НЕ ДОЛЖЕН.** `recognition done`, а не `recognize: done`; подсистема —
|
||||||
отдельное поле.
|
отдельное поле.
|
||||||
|
|
||||||
**Почему.** Префикс кладёт в текст ровно то, по чему потом фильтруют, и
|
**ПОЧЕМУ.** Префикс кладёт в текст ровно то, по чему потом фильтруют, и
|
||||||
фильтр по подсистеме становится сопоставлением с началом строки вместо
|
фильтр по подсистеме становится сопоставлением с началом строки вместо
|
||||||
сравнения значения поля. Заодно это второй способ записать одно и то же:
|
сравнения значения поля. Заодно это второй способ записать одно и то же:
|
||||||
категория дробится на варианты с префиксом и без, а совпадать они обязаны
|
категория дробится на варианты с префиксом и без, а совпадать они обязаны
|
||||||
@@ -90,7 +91,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
**ДОЛЖЕН.** `state transition` с полями `from`/`to`/`code`; какое именно
|
**ДОЛЖЕН.** `state transition` с полями `from`/`to`/`code`; какое именно
|
||||||
состояние и по какой причине — данные, а не текст.
|
состояние и по какой причине — данные, а не текст.
|
||||||
|
|
||||||
**Почему.** С отдельной категорией на каждый переход жизненный цикл
|
**ПОЧЕМУ.** С отдельной категорией на каждый переход жизненный цикл
|
||||||
сущности собирается перечислением всех известных `msg` — и переход,
|
сущности собирается перечислением всех известных `msg` — и переход,
|
||||||
добавленный в код позже, в это перечисление не попадёт: выборка тихо
|
добавленный в код позже, в это перечисление не попадёт: выборка тихо
|
||||||
останется неполной. Единая категория даёт весь цикл одним фильтром и не
|
останется неполной. Единая категория даёт весь цикл одним фильтром и не
|
||||||
@@ -101,7 +102,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
**НЕ ДОЛЖЕН.** Запись о действии, сопровождающем переход, не подменяет
|
**НЕ ДОЛЖЕН.** Запись о действии, сопровождающем переход, не подменяет
|
||||||
запись самого перехода.
|
запись самого перехода.
|
||||||
|
|
||||||
**Почему.** Иначе из выборки по SLOG-6 выпадают именно те переходы, у которых
|
**ПОЧЕМУ.** Иначе из выборки по SLOG-6 выпадают именно те переходы, у которых
|
||||||
был заметный эффект, — то есть самые интересные. Вторая запись стоит одной
|
был заметный эффект, — то есть самые интересные. Вторая запись стоит одной
|
||||||
строки в логе; восстановление пропущенного перехода не стоит ничего, потому
|
строки в логе; восстановление пропущенного перехода не стоит ничего, потому
|
||||||
что невозможно.
|
что невозможно.
|
||||||
@@ -120,7 +121,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
| SLOG-8.3 | `WARN` | владельцу, «может стать проблемой» |
|
| SLOG-8.3 | `WARN` | владельцу, «может стать проблемой» |
|
||||||
| SLOG-8.4 | `ERROR` | владельцу, в разбор |
|
| SLOG-8.4 | `ERROR` | владельцу, в разбор |
|
||||||
|
|
||||||
**Почему.** Адресат — единственный признак, по которому разные авторы в
|
**ПОЧЕМУ.** Адресат — единственный признак, по которому разные авторы в
|
||||||
разных местах кода выберут уровень одинаково. «Насколько серьёзно» каждый
|
разных местах кода выберут уровень одинаково. «Насколько серьёзно» каждый
|
||||||
оценивает по-своему, шкала расползается — и вместе с ней теряет смысл
|
оценивает по-своему, шкала расползается — и вместе с ней теряет смысл
|
||||||
базовый порог в проде (SLOG-40), потому что он отсекает уже не то, что
|
базовый порог в проде (SLOG-40), потому что он отсекает уже не то, что
|
||||||
@@ -131,7 +132,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
**НЕ ДОЛЖЕН.** Происхождение записи на выбор уровня не влияет: `ERROR`
|
**НЕ ДОЛЖЕН.** Происхождение записи на выбор уровня не влияет: `ERROR`
|
||||||
везде одинаково серьёзен.
|
везде одинаково серьёзен.
|
||||||
|
|
||||||
**Почему.** Фильтр по уровню собирает записи из всех подсистем сразу. Если
|
**ПОЧЕМУ.** Фильтр по уровню собирает записи из всех подсистем сразу. Если
|
||||||
в шумной подсистеме `ERROR` «дешевле», читателю приходится помнить
|
в шумной подсистеме `ERROR` «дешевле», читателю приходится помнить
|
||||||
происхождение каждой записи, чтобы понять, надо ли реагировать, — то есть
|
происхождение каждой записи, чтобы понять, надо ли реагировать, — то есть
|
||||||
уровень перестаёт быть фильтром и становится подсказкой, требующей знания
|
уровень перестаёт быть фильтром и становится подсказкой, требующей знания
|
||||||
@@ -141,7 +142,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Если «может» не про эту запись, уровень — `INFO`.
|
**ДОЛЖЕН.** Если «может» не про эту запись, уровень — `INFO`.
|
||||||
|
|
||||||
**Почему.** `WARN` разбирают вручную и целиком. Как только в нём заводится
|
**ПОЧЕМУ.** `WARN` разбирают вручную и целиком. Как только в нём заводится
|
||||||
«ничего страшного», его перестают читать — и вместе с шумом теряется то
|
«ничего страшного», его перестают читать — и вместе с шумом теряется то
|
||||||
единственное, ради чего уровень существует: предупреждение, на которое ещё
|
единственное, ради чего уровень существует: предупреждение, на которое ещё
|
||||||
есть время отреагировать.
|
есть время отреагировать.
|
||||||
@@ -155,7 +156,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
| SLOG-11.1 | по реальному действию или изменению | `INFO` |
|
| SLOG-11.1 | по реальному действию или изменению | `INFO` |
|
||||||
| SLOG-11.2 | повторяющаяся служебная, по таймеру или поллингу, сама по себе события не несущая (healthcheck, опрос статуса, авто-рефреш UI) | `DEBUG` |
|
| SLOG-11.2 | повторяющаяся служебная, по таймеру или поллингу, сама по себе события не несущая (healthcheck, опрос статуса, авто-рефреш UI) | `DEBUG` |
|
||||||
|
|
||||||
**Почему.** `INFO` — аудит постфактум (SLOG-8.2), и его пригодность
|
**ПОЧЕМУ.** `INFO` — аудит постфактум (SLOG-8.2), и его пригодность
|
||||||
определяется долей записей, за которыми что-то стоит. Периодическая
|
определяется долей записей, за которыми что-то стоит. Периодическая
|
||||||
операция даёт ровный поток при нулевой информации, в котором настоящие
|
операция даёт ровный поток при нулевой информации, в котором настоящие
|
||||||
события тонут количественно: их не отфильтровать, потому что фильтровать
|
события тонут количественно: их не отфильтровать, потому что фильтровать
|
||||||
@@ -163,14 +164,16 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
|
|
||||||
### SLOG-12. Фатальный сбой на старте — `ERROR` и ненулевой код возврата
|
### SLOG-12. Фатальный сбой на старте — `ERROR` и ненулевой код возврата
|
||||||
|
|
||||||
**ДОЛЖЕН.** `slog` не разделяет CRITICAL/FATAL, поэтому недостающую
|
**ДОЛЖЕН.** Фатальный сбой на старте пишется как `ERROR` и завершает процесс
|
||||||
степень даёт завершение процесса.
|
ненулевым кодом.
|
||||||
|
|
||||||
**Почему.** Супервизор (docker, journald, systemd) отличает падение от
|
**ПОЧЕМУ.** `slog` не разделяет CRITICAL и FATAL, поэтому недостающую степень
|
||||||
штатной остановки по коду возврата, а не по уровню последней записи.
|
выражает не уровень записи, а сам факт завершения. Супервизор (docker,
|
||||||
Процесс, который написал `ERROR` и продолжил жить с неработающей
|
journald, systemd) отличает падение от штатной остановки по коду возврата, а
|
||||||
конфигурацией, выглядит здоровым и будет получать трафик; изобретать же
|
не по уровню последней записи. Процесс, который написал `ERROR` и продолжил
|
||||||
уровень выше `ERROR` не нужно — сам факт завершения информативнее.
|
жить с неработающей конфигурацией, выглядит здоровым и будет получать трафик;
|
||||||
|
изобретать же уровень выше `ERROR` не нужно — сам факт завершения
|
||||||
|
информативнее.
|
||||||
|
|
||||||
## Поля: единый словарь
|
## Поля: единый словарь
|
||||||
|
|
||||||
@@ -178,7 +181,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Не `mediaType`/`media`/`media_type` вперемешку.
|
**ДОЛЖЕН.** Не `mediaType`/`media`/`media_type` вперемешку.
|
||||||
|
|
||||||
**Почему.** Имя поля — и есть интерфейс запроса к логам. Второе имя для той
|
**ПОЧЕМУ.** Имя поля — и есть интерфейс запроса к логам. Второе имя для той
|
||||||
же величины делает любую выборку по ней молча неполной: фильтр отработает,
|
же величины делает любую выборку по ней молча неполной: фильтр отработает,
|
||||||
часть записей в него не попадёт, и заметить это можно, только заранее зная,
|
часть записей в него не попадёт, и заметить это можно, только заранее зная,
|
||||||
что они должны были быть.
|
что они должны были быть.
|
||||||
@@ -192,7 +195,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
| SLOG-14.1 | бизнес-поле | плоский `snake_case`: `download_id`, `media_type` |
|
| SLOG-14.1 | бизнес-поле | плоский `snake_case`: `download_id`, `media_type` |
|
||||||
| SLOG-14.2 | системный домен | точечная иерархия (адаптация OpenTelemetry): `http.*`, `ext.*` |
|
| SLOG-14.2 | системный домен | точечная иерархия (адаптация OpenTelemetry): `http.*`, `ext.*` |
|
||||||
|
|
||||||
**Почему.** Точка отделяет поля, приходящие от инфраструктуры и одинаковые
|
**ПОЧЕМУ.** Точка отделяет поля, приходящие от инфраструктуры и одинаковые
|
||||||
в любом проекте, от доменных, которые в каждом свои: по общему префиксу
|
в любом проекте, от доменных, которые в каждом свои: по общему префиксу
|
||||||
запрос «все внешние вызовы» пишется без перечисления имён. Заимствование
|
запрос «все внешние вызовы» пишется без перечисления имён. Заимствование
|
||||||
словаря OpenTelemetry снимает необходимость изобретать имена тому, что уже
|
словаря OpenTelemetry снимает необходимость изобретать имена тому, что уже
|
||||||
@@ -203,7 +206,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
**НЕ ДОЛЖЕН.** Вложенных объектов в записи нет; точка в имени — часть
|
**НЕ ДОЛЖЕН.** Вложенных объектов в записи нет; точка в имени — часть
|
||||||
имени, а не уровень вложенности.
|
имени, а не уровень вложенности.
|
||||||
|
|
||||||
**Почему.** Плоский ключ адресуется одинаково в `jq`, в DuckDB и в любой
|
**ПОЧЕМУ.** Плоский ключ адресуется одинаково в `jq`, в DuckDB и в любой
|
||||||
записи независимо от её категории. Вложенность требует знать глубину
|
записи независимо от её категории. Вложенность требует знать глубину
|
||||||
заранее, а она у разных категорий разная — и один запрос перестаёт покрывать
|
заранее, а она у разных категорий разная — и один запрос перестаёт покрывать
|
||||||
весь лог, распадаясь на запрос под каждую форму записи.
|
весь лог, распадаясь на запрос под каждую форму записи.
|
||||||
@@ -219,7 +222,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
| SLOG-16.3 | запись об ошибке | `error` |
|
| SLOG-16.3 | запись об ошибке | `error` |
|
||||||
| SLOG-16.4 | вызов внешнего сервиса | `ext.service`, `ext.operation` (логическая операция, не URL), `ext.status_code`, `duration_ms`, `retry` |
|
| SLOG-16.4 | вызов внешнего сервиса | `ext.service`, `ext.operation` (логическая операция, не URL), `ext.status_code`, `duration_ms`, `retry` |
|
||||||
|
|
||||||
**Почему.** Набор задан не «на всякий случай»: без него запись не отвечает
|
**ПОЧЕМУ.** Набор задан не «на всякий случай»: без него запись не отвечает
|
||||||
на свой вопрос. HTTP-запись без `duration_ms` не показывает деградацию,
|
на свой вопрос. HTTP-запись без `duration_ms` не показывает деградацию,
|
||||||
`ext`-запись без `ext.service` не отделяет «легла зависимость» от «у нас
|
`ext`-запись без `ext.service` не отделяет «легла зависимость» от «у нас
|
||||||
баг», запись о сущности без идентификатора не корреллируется (SLOG-19). Полный
|
баг», запись о сущности без идентификатора не корреллируется (SLOG-19). Полный
|
||||||
@@ -231,16 +234,13 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
**НЕ СЛЕДУЕТ.** Поле, значение которого одинаково во всех записях, не
|
**НЕ СЛЕДУЕТ.** Поле, значение которого одинаково во всех записях, не
|
||||||
заводится — для одного бинаря на одном хосте это `service.*` и `host.*`.
|
заводится — для одного бинаря на одном хосте это `service.*` и `host.*`.
|
||||||
|
|
||||||
**Почему.** Такое поле не несёт информации, но стоит места в каждой строке
|
**ПОЧЕМУ.** Такое поле не несёт информации, но стоит места в каждой строке
|
||||||
и внимания при чтении. Критерий один на все поля словаря — им же решается,
|
и внимания при чтении. Критерий один на все поля словаря — им же решается,
|
||||||
нужен ли `transport` (SLOG-16.1): пока транспорт один, поле постоянно. Условие
|
нужен ли `transport` (SLOG-16.1): пока транспорт один, поле постоянно. Условие
|
||||||
названо явно, поэтому правило отпадёт вместе со своей причиной: с
|
названо явно, поэтому правило отпадёт вместе со своей причиной: с
|
||||||
появлением нескольких инстансов различающее поле (`service.version`)
|
появлением нескольких инстансов различающее поле (`service.version`)
|
||||||
добавляется одной строкой при старте.
|
добавляется одной строкой при старте.
|
||||||
|
|
||||||
<!-- local:словарь -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Корреляция
|
## Корреляция
|
||||||
|
|
||||||
### SLOG-18. Ключ корреляции — идентификатор сущности, а не `trace_id`
|
### SLOG-18. Ключ корреляции — идентификатор сущности, а не `trace_id`
|
||||||
@@ -249,7 +249,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
сущностей есть стабильные уникальные идентификаторы. (Как их выбирают —
|
сущностей есть стабильные уникальные идентификаторы. (Как их выбирают —
|
||||||
конвенция `db-identifiers`, если взята.)
|
конвенция `db-identifiers`, если взята.)
|
||||||
|
|
||||||
**Почему.** Идентификатор сущности уже существует, стабилен между
|
**ПОЧЕМУ.** Идентификатор сущности уже существует, стабилен между
|
||||||
процессами и во времени — по нему собираются записи не одного прохода, а
|
процессами и во времени — по нему собираются записи не одного прохода, а
|
||||||
всей истории сущности, включая вчерашнюю. `trace_id` даёт то же самое
|
всей истории сущности, включая вчерашнюю. `trace_id` даёт то же самое
|
||||||
только внутри одной операции, то есть дублирует ключ и добавляет второй
|
только внутри одной операции, то есть дублирует ключ и добавляет второй
|
||||||
@@ -260,7 +260,7 @@ dev-выводом перестаёшь ежедневно гонять собс
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Поле `<entity>_id` в каждой записи, относящейся к сущности.
|
**ДОЛЖЕН.** Поле `<entity>_id` в каждой записи, относящейся к сущности.
|
||||||
|
|
||||||
**Почему.** Принадлежность записи восстанавливается только в момент
|
**ПОЧЕМУ.** Принадлежность записи восстанавливается только в момент
|
||||||
записи; постфактум её не вывести — остаётся воспроизводить инцидент заново.
|
записи; постфактум её не вывести — остаётся воспроизводить инцидент заново.
|
||||||
Это же условие, при котором работает SLOG-18: отказ от `trace_id` оплачен тем,
|
Это же условие, при котором работает SLOG-18: отказ от `trace_id` оплачен тем,
|
||||||
что идентификатор стоит везде, а не в удобных местах.
|
что идентификатор стоит везде, а не в удобных местах.
|
||||||
@@ -280,7 +280,7 @@ log := log.With("download_id", id)
|
|||||||
ctx = logctx.With(ctx, log) // достаём логгер из ctx в каждой стадии
|
ctx = logctx.With(ctx, log) // достаём логгер из ctx в каждой стадии
|
||||||
```
|
```
|
||||||
|
|
||||||
**Почему.** Ручное дописывание ключа пропускают не в основном сценарии, а в
|
**ПОЧЕМУ.** Ручное дописывание ключа пропускают не в основном сценарии, а в
|
||||||
редких ветках — обработке ошибок и ранних выходах, где корреляция нужнее
|
редких ветках — обработке ошибок и ранних выходах, где корреляция нужнее
|
||||||
всего. Логгер из контекста дописывает ключ сам, и запись без
|
всего. Логгер из контекста дописывает ключ сам, и запись без
|
||||||
идентификатора становится невозможной, а не маловероятной.
|
идентификатора становится невозможной, а не маловероятной.
|
||||||
@@ -291,7 +291,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
|
|
||||||
**ДОЛЖЕН.** `log.Error("layout failed", "error", err, "download_id", id)`.
|
**ДОЛЖЕН.** `log.Error("layout failed", "error", err, "download_id", id)`.
|
||||||
|
|
||||||
**Почему.** Ошибка, вклеенная в текст сообщения, дробит категорию (SLOG-4) и
|
**ПОЧЕМУ.** Ошибка, вклеенная в текст сообщения, дробит категорию (SLOG-4) и
|
||||||
уносит текст туда, где по нему нельзя отфильтровать. Ключ единый — так же,
|
уносит текст туда, где по нему нельзя отфильтровать. Ключ единый — так же,
|
||||||
как по умолчанию в zap/zerolog: выборка «все записи с ошибкой» не должна
|
как по умолчанию в zap/zerolog: выборка «все записи с ошибкой» не должна
|
||||||
зависеть от того, кто писал конкретный вызов, и ради этого единообразия
|
зависеть от того, кто писал конкретный вызов, и ради этого единообразия
|
||||||
@@ -302,7 +302,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
**НЕ ДОЛЖЕН.** Слой, возвращающий ошибку выше, её не логирует — только
|
**НЕ ДОЛЖЕН.** Слой, возвращающий ошибку выше, её не логирует — только
|
||||||
оборачивает (`%w`).
|
оборачивает (`%w`).
|
||||||
|
|
||||||
**Почему.** Иначе один сбой даёт столько записей, сколько слоёв он прошёл,
|
**ПОЧЕМУ.** Иначе один сбой даёт столько записей, сколько слоёв он прошёл,
|
||||||
и количество `ERROR` перестаёт соответствовать количеству отказов — а
|
и количество `ERROR` перестаёт соответствовать количеству отказов — а
|
||||||
считают именно его. Контекст при этом не теряется: он накапливается в
|
считают именно его. Контекст при этом не теряется: он накапливается в
|
||||||
цепочке обёрток и попадает в единственную запись на границе (SLOG-23).
|
цепочке обёрток и попадает в единственную запись на границе (SLOG-23).
|
||||||
@@ -311,21 +311,18 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Логирует единый чокпоинт, определяющий исход операции.
|
**ДОЛЖЕН.** Логирует единый чокпоинт, определяющий исход операции.
|
||||||
|
|
||||||
**Почему.** У ошибки нужен ровно один логирующий, иначе неизбежны дубли; и
|
**ПОЧЕМУ.** У ошибки нужен ровно один логирующий, иначе неизбежны дубли; и
|
||||||
этим местом выбрана доменная граница, а не транспорт, потому что там
|
этим местом выбрана доменная граница, а не транспорт, потому что там
|
||||||
известен исход операции целиком и, значит, класс отказа (SLOG-25) — транспорт
|
известен исход операции целиком и, значит, класс отказа (SLOG-25) — транспорт
|
||||||
знает лишь то, что ему вернули ошибку. Побочный эффект того же выбора:
|
знает лишь то, что ему вернули ошибку. Побочный эффект того же выбора:
|
||||||
транспорты остаются тонкими.
|
транспорты остаются тонкими.
|
||||||
|
|
||||||
<!-- local:границы -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
### SLOG-24. Транспорт не логирует ошибку повторно
|
### SLOG-24. Транспорт не логирует ошибку повторно
|
||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Транспорт переводит возвращённую ошибку в свой ответ
|
**НЕ ДОЛЖЕН.** Транспорт переводит возвращённую ошибку в свой ответ
|
||||||
(статус, сообщение пользователю) и на этом останавливается.
|
(статус, сообщение пользователю) и на этом останавливается.
|
||||||
|
|
||||||
**Почему.** Запись уже сделана на границе (SLOG-23); вторая отличается от неё
|
**ПОЧЕМУ.** Запись уже сделана на границе (SLOG-23); вторая отличается от неё
|
||||||
только формулировкой и читается как второй сбой. Когда транспортов над
|
только формулировкой и читается как второй сбой. Когда транспортов над
|
||||||
одним доменом несколько, дублирование ещё и множится, а расследование
|
одним доменом несколько, дублирование ещё и множится, а расследование
|
||||||
начинается с вопроса, один это инцидент или два.
|
начинается с вопроса, один это инцидент или два.
|
||||||
@@ -341,8 +338,9 @@ 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 к отказам: пользователь уже увидел причину на
|
||||||
экране — владельцу разбирать нечего; целостность первичных данных отделяет
|
экране — владельцу разбирать нечего; целостность первичных данных отделяет
|
||||||
«надо посмотреть» от «надо чинить сейчас». Привязка к месту дала бы разный
|
«надо посмотреть» от «надо чинить сейчас». Привязка к месту дала бы разный
|
||||||
уровень для одного и того же отказа в зависимости от того, какой транспорт
|
уровень для одного и того же отказа в зависимости от того, какой транспорт
|
||||||
@@ -353,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. Тот же отказ в асинхронной стадии — уровнем выше
|
||||||
|
|
||||||
@@ -363,7 +363,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
владельцу как деградация автоматики: коллизия в ручном действии — `DEBUG`,
|
владельцу как деградация автоматики: коллизия в ручном действии — `DEBUG`,
|
||||||
она же в авто-обработке — `WARN`.
|
она же в авто-обработке — `WARN`.
|
||||||
|
|
||||||
**Почему.** В ручном действии человек видит причину на экране и сам решает,
|
**ПОЧЕМУ.** В ручном действии человек видит причину на экране и сам решает,
|
||||||
что делать дальше; запись нужна только для отладки. В автоматике не увидел
|
что делать дальше; запись нужна только для отладки. В автоматике не увидел
|
||||||
никто, задача осталась недоведённой, и лог — единственное место, где это
|
никто, задача осталась недоведённой, и лог — единственное место, где это
|
||||||
вообще проявится.
|
вообще проявится.
|
||||||
@@ -373,7 +373,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
**ДОЛЖЕН.** Тот же класс сбоя внутри синхронной операции — `ERROR`:
|
**ДОЛЖЕН.** Тот же класс сбоя внутри синхронной операции — `ERROR`:
|
||||||
уровень задаёт наличие штатного повтора, а не текст ошибки.
|
уровень задаёт наличие штатного повтора, а не текст ошибки.
|
||||||
|
|
||||||
**Почему.** Одиночный промах тика транзиентен — следующий тик повторит, и
|
**ПОЧЕМУ.** Одиночный промах тика транзиентен — следующий тик повторит, и
|
||||||
вмешательство не требуется; `ERROR` на каждый такой промах обесценивает
|
вмешательство не требуется; `ERROR` на каждый такой промах обесценивает
|
||||||
уровень, на который смотрят в первую очередь. Синхронная операция повтора
|
уровень, на который смотрят в первую очередь. Синхронная операция повтора
|
||||||
не имеет: она провалилась целиком, результат никто не восстановит, и это
|
не имеет: она провалилась целиком, результат никто не восстановит, и это
|
||||||
@@ -385,7 +385,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Все вызовы, включая успешные; поля — по SLOG-16.4.
|
**ДОЛЖЕН.** Все вызовы, включая успешные; поля — по SLOG-16.4.
|
||||||
|
|
||||||
**Почему.** Это единственный способ отличить «у нас баг» от «зависимость
|
**ПОЧЕМУ.** Это единственный способ отличить «у нас баг» от «зависимость
|
||||||
легла»: на своей стороне видно лишь то, что операция не удалась.
|
легла»: на своей стороне видно лишь то, что операция не удалась.
|
||||||
Выборочное логирование ломает и второе применение — доля неуспехов и
|
Выборочное логирование ломает и второе применение — доля неуспехов и
|
||||||
распределение `duration_ms` считаются, только если знаменатель полный.
|
распределение `duration_ms` считаются, только если знаменатель полный.
|
||||||
@@ -401,7 +401,7 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
| SLOG-29.3 | попытка не удалась, делается retry | `WARN` |
|
| SLOG-29.3 | попытка не удалась, делается retry | `WARN` |
|
||||||
| SLOG-29.4 | ретраи исчерпаны, сервис недоступен | `ERROR` |
|
| SLOG-29.4 | ретраи исчерпаны, сервис недоступен | `ERROR` |
|
||||||
|
|
||||||
**Почему.** Неудачная попытка, за которой следует повтор, — ещё не отказ:
|
**ПОЧЕМУ.** Неудачная попытка, за которой следует повтор, — ещё не отказ:
|
||||||
операция может завершиться успешно, и `ERROR` на каждую попытку сделал бы
|
операция может завершиться успешно, и `ERROR` на каждую попытку сделал бы
|
||||||
уровень непригодным для главного вопроса «зависимость доступна?».
|
уровень непригодным для главного вопроса «зависимость доступна?».
|
||||||
Исчерпание ретраев и есть момент, когда транспорт сдался и дальше
|
Исчерпание ретраев и есть момент, когда транспорт сдался и дальше
|
||||||
@@ -417,10 +417,10 @@ ctx = logctx.With(ctx, log) // достаём логгер из ctx в кажд
|
|||||||
уровень доменной записи об исходе тика.
|
уровень доменной записи об исходе тика.
|
||||||
|
|
||||||
```
|
```
|
||||||
WHEN зависимость недоступна и ретраи вызова исчерпаны
|
КОГДА зависимость недоступна И ретраи вызова исчерпаны
|
||||||
→ ext-запись `ERROR` (SLOG-29.4)
|
ТОГДА ext-запись `ERROR` (SLOG-29.4)
|
||||||
AND тик фонового цикла упал по той же причине
|
И тик фонового цикла, упавший по той же причине,
|
||||||
→ доменная запись `WARN` (SLOG-27)
|
даёт доменную запись `WARN` (SLOG-27)
|
||||||
```
|
```
|
||||||
|
|
||||||
Из этого следует, что у лежащей зависимости `ext`-запись пишет `ERROR`
|
Из этого следует, что у лежащей зависимости `ext`-запись пишет `ERROR`
|
||||||
@@ -435,7 +435,7 @@ AND тик фонового цикла упал по той же причине
|
|||||||
(`ext.status_code` записан); решение «это ошибка» принимает доменный
|
(`ext.status_code` записан); решение «это ошибка» принимает доменный
|
||||||
вызывающий.
|
вызывающий.
|
||||||
|
|
||||||
**Почему.** Транспорт своё дело сделал: запрос доставлен, ответ получен и
|
**ПОЧЕМУ.** Транспорт своё дело сделал: запрос доставлен, ответ получен и
|
||||||
разобран. Классифицировать 4xx как сбой транспорта значит смешать «сервис
|
разобран. Классифицировать 4xx как сбой транспорта значит смешать «сервис
|
||||||
недоступен» с «сервис ответил нам нет» — это разные инциденты с разной
|
недоступен» с «сервис ответил нам нет» — это разные инциденты с разной
|
||||||
реакцией, и различает их как раз `ext`-уровень. Что 404 значит для
|
реакцией, и различает их как раз `ext`-уровень. Что 404 значит для
|
||||||
@@ -448,7 +448,7 @@ AND тик фонового цикла упал по той же причине
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Поля по SLOG-16.1; 4xx остаётся `INFO`-записью доступа.
|
**ДОЛЖЕН.** Поля по SLOG-16.1; 4xx остаётся `INFO`-записью доступа.
|
||||||
|
|
||||||
**Почему.** Это аудит обращений, а не отладка: запись отвечает на «кто и
|
**ПОЧЕМУ.** Это аудит обращений, а не отладка: запись отвечает на «кто и
|
||||||
когда приходил», и ценность у неё одинаковая при любом коде ответа.
|
когда приходил», и ценность у неё одинаковая при любом коде ответа.
|
||||||
Уровень, зависящий от кода, делает аудит неполным именно на тех запросах,
|
Уровень, зависящий от кода, делает аудит неполным именно на тех запросах,
|
||||||
которые чаще всего разбирают. Доменную оценку исхода даёт отдельная запись
|
которые чаще всего разбирают. Доменную оценку исхода даёт отдельная запись
|
||||||
@@ -458,7 +458,7 @@ AND тик фонового цикла упал по той же причине
|
|||||||
|
|
||||||
**ДОПУСКАЕТСЯ.** Это отдельный слой от корреляции по сущности.
|
**ДОПУСКАЕТСЯ.** Это отдельный слой от корреляции по сущности.
|
||||||
|
|
||||||
**Почему.** Явное разрешение снимает вопрос, не запрещает ли `request_id`
|
**ПОЧЕМУ.** Явное разрешение снимает вопрос, не запрещает ли `request_id`
|
||||||
правило SLOG-18. Не запрещает: SLOG-18 отказывается от случайного ключа там, где
|
правило SLOG-18. Не запрещает: SLOG-18 отказывается от случайного ключа там, где
|
||||||
уже есть стабильный идентификатор сущности, а у HTTP-запроса собственной
|
уже есть стабильный идентификатор сущности, а у HTTP-запроса собственной
|
||||||
сущности нет — связать его записи между собой больше нечем.
|
сущности нет — связать его записи между собой больше нечем.
|
||||||
@@ -467,7 +467,7 @@ AND тик фонового цикла упал по той же причине
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Периодические проверки живости пишутся на отладочном уровне.
|
**ДОЛЖЕН.** Периодические проверки живости пишутся на отладочном уровне.
|
||||||
|
|
||||||
**Почему.** Частный случай SLOG-11.2, названный отдельно, потому что нарушают
|
**ПОЧЕМУ.** Частный случай SLOG-11.2, названный отдельно, потому что нарушают
|
||||||
его чаще всего: проверку дёргают по таймеру, и на `INFO` она вытесняет из
|
его чаще всего: проверку дёргают по таймеру, и на `INFO` она вытесняет из
|
||||||
аудита всё остальное — в проде с базовым `INFO` (SLOG-40) лог превратился бы в
|
аудита всё остальное — в проде с базовым `INFO` (SLOG-40) лог превратился бы в
|
||||||
опрос самого себя. На `DEBUG` она не пишется вовсе и при этом остаётся
|
опрос самого себя. На `DEBUG` она не пишется вовсе и при этом остаётся
|
||||||
@@ -481,7 +481,7 @@ AND тик фонового цикла упал по той же причине
|
|||||||
API-ключи и токены, `Authorization`-заголовки, аутентификационные параметры
|
API-ключи и токены, `Authorization`-заголовки, аутентификационные параметры
|
||||||
в ссылках.
|
в ссылках.
|
||||||
|
|
||||||
**Почему.** Лог уезжает целиком в чужое хранилище, читается шире, чем код,
|
**ПОЧЕМУ.** Лог уезжает целиком в чужое хранилище, читается шире, чем код,
|
||||||
и переживает ротацию самого секрета. Попавший в него секрет скомпрометирован
|
и переживает ротацию самого секрета. Попавший в него секрет скомпрометирован
|
||||||
с момента записи, а не с момента, когда это заметили, и вычистить его задним
|
с момента записи, а не с момента, когда это заметили, и вычистить его задним
|
||||||
числом из уже собранных копий нельзя.
|
числом из уже собранных копий нельзя.
|
||||||
@@ -491,7 +491,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
|||||||
**ДОЛЖЕН.** Тела запросов и ответов внешних API, сырой вывод LLM —
|
**ДОЛЖЕН.** Тела запросов и ответов внешних API, сырой вывод LLM —
|
||||||
`DEBUG`, с вычисткой секретов и обрезкой по длине.
|
`DEBUG`, с вычисткой секретов и обрезкой по длине.
|
||||||
|
|
||||||
**Почему.** Содержимое пришло снаружи: размер не ограничен, состав
|
**ПОЧЕМУ.** Содержимое пришло снаружи: размер не ограничен, состав
|
||||||
неизвестен, а секрет в нём возможен по недосмотру той стороны. `DEBUG`
|
неизвестен, а секрет в нём возможен по недосмотру той стороны. `DEBUG`
|
||||||
выключен в проде (SLOG-40), поэтому цена ошибки ограничена отладочной сессией;
|
выключен в проде (SLOG-40), поэтому цена ошибки ограничена отладочной сессией;
|
||||||
обрезка не даёт одной записи вытеснить весь остальной лог за период.
|
обрезка не даёт одной записи вытеснить весь остальной лог за период.
|
||||||
@@ -500,7 +500,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** `"has_api_key", true` вместо самого значения.
|
**СЛЕДУЕТ.** `"has_api_key", true` вместо самого значения.
|
||||||
|
|
||||||
**Почему.** Для отладки почти всегда достаточно ответа «значение было или
|
**ПОЧЕМУ.** Для отладки почти всегда достаточно ответа «значение было или
|
||||||
не было» — потеря полезности близка к нулю, а риск снимается целиком.
|
не было» — потеря полезности близка к нулю, а риск снимается целиком.
|
||||||
Правило нужно потому, что решение принимается в момент написания строки,
|
Правило нужно потому, что решение принимается в момент написания строки,
|
||||||
когда чувствительность значения ещё неочевидна, а перечитывать этот выбор
|
когда чувствительность значения ещё неочевидна, а перечитывать этот выбор
|
||||||
@@ -511,7 +511,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
|||||||
**ДОЛЖЕН.** Ошибка разворачивается в первопричину **до** лога и до
|
**ДОЛЖЕН.** Ошибка разворачивается в первопричину **до** лога и до
|
||||||
обёртки — раньше трансляции в доменную (конвенция `errors`).
|
обёртки — раньше трансляции в доменную (конвенция `errors`).
|
||||||
|
|
||||||
**Почему.** `*url.Error` встраивает полный URL запроса, а секрет живёт
|
**ПОЧЕМУ.** `*url.Error` встраивает полный URL запроса, а секрет живёт
|
||||||
прямо в нём: токен в пути, `api_key` в query. Go редактирует только пароль
|
прямо в нём: токен в пути, `api_key` в query. Go редактирует только пароль
|
||||||
из userinfo, остального не трогает, поэтому ошибка уносит секрет и в
|
из userinfo, остального не трогает, поэтому ошибка уносит секрет и в
|
||||||
обёртку, и в лог целиком. Порядок — часть нормы: санитизация после
|
обёртку, и в лог целиком. Порядок — часть нормы: санитизация после
|
||||||
@@ -525,14 +525,11 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
|||||||
**НЕ ДОЛЖЕН.** Аутентификация параметром ссылки — только когда другого
|
**НЕ ДОЛЖЕН.** Аутентификация параметром ссылки — только когда другого
|
||||||
способа нет.
|
способа нет.
|
||||||
|
|
||||||
**Почему.** Секрет в URL попадает не только в ошибку транспорта (SLOG-37), но и
|
**ПОЧЕМУ.** Секрет в URL попадает не только в ошибку транспорта (SLOG-37), но и
|
||||||
в любую запись, куда URL попал целиком, — то есть обязывает помнить про
|
в любую запись, куда URL попал целиком, — то есть обязывает помнить про
|
||||||
санитизацию в каждой такой точке, и одна забытая сводит остальные на нет.
|
санитизацию в каждой такой точке, и одна забытая сводит остальные на нет.
|
||||||
Заголовок снимает задачу в источнике: чего нет в URL, того нет и в ошибке.
|
Заголовок снимает задачу в источнике: чего нет в URL, того нет и в ошибке.
|
||||||
|
|
||||||
<!-- local:секреты -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Куда пишем
|
## Куда пишем
|
||||||
|
|
||||||
### SLOG-39. Логи идут в `stdout` одним потоком
|
### SLOG-39. Логи идут в `stdout` одним потоком
|
||||||
@@ -540,7 +537,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
|||||||
**ДОЛЖЕН.** Сбор и ротацию делает окружение (docker, journald); по файлам
|
**ДОЛЖЕН.** Сбор и ротацию делает окружение (docker, journald); по файлам
|
||||||
не маршрутизируем.
|
не маршрутизируем.
|
||||||
|
|
||||||
**Почему.** Приложение, которое само решает, что куда писать, дублирует
|
**ПОЧЕМУ.** Приложение, которое само решает, что куда писать, дублирует
|
||||||
работу супервизора и расходится с ней при первой же смене окружения: срок
|
работу супервизора и расходится с ней при первой же смене окружения: срок
|
||||||
хранения, сжатие и ротация оказываются настроены в двух местах и по-разному.
|
хранения, сжатие и ротация оказываются настроены в двух местах и по-разному.
|
||||||
Один поток вдобавок сохраняет порядок записей — маршрутизация по файлам
|
Один поток вдобавок сохраняет порядок записей — маршрутизация по файлам
|
||||||
@@ -550,7 +547,7 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
|||||||
|
|
||||||
**ДОЛЖЕН.** `DEBUG` в проде включается конфигом.
|
**ДОЛЖЕН.** `DEBUG` в проде включается конфигом.
|
||||||
|
|
||||||
**Почему.** Уровень — единственный регулятор объёма, доступный без
|
**ПОЧЕМУ.** Уровень — единственный регулятор объёма, доступный без
|
||||||
пересборки; если `DEBUG` в проде включается только правкой кода, его не
|
пересборки; если `DEBUG` в проде включается только правкой кода, его не
|
||||||
включают, и разбор инцидента идёт вслепую. `INFO` выбран базовым потому,
|
включают, и разбор инцидента идёт вслепую. `INFO` выбран базовым потому,
|
||||||
что на нём аудит полон (SLOG-8.2), а рутинно-частое уже отсечено (SLOG-11.2).
|
что на нём аудит полон (SLOG-8.2), а рутинно-частое уже отсечено (SLOG-11.2).
|
||||||
@@ -563,6 +560,3 @@ API-ключи и токены, `Authorization`-заголовки, аутент
|
|||||||
санитизации (SLOG-37).
|
санитизации (SLOG-37).
|
||||||
- конвенция `db-identifiers` — откуда берутся стабильные идентификаторы,
|
- конвенция `db-identifiers` — откуда берутся стабильные идентификаторы,
|
||||||
на которых держится корреляция (SLOG-18).
|
на которых держится корреляция (SLOG-18).
|
||||||
|
|
||||||
<!-- local:механизировано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
+20
-21
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
topic: time
|
||||||
prefix: GTIM
|
prefix: GTIM
|
||||||
|
lang: go
|
||||||
extends: arch/time.md
|
extends: arch/time.md
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -8,9 +10,9 @@ extends: arch/time.md
|
|||||||
Как требования базового слоя выполняются в Go-коде: откуда берётся «сейчас»,
|
Как требования базового слоя выполняются в Go-коде: откуда берётся «сейчас»,
|
||||||
в каком виде время попадает в базу и в логи, что делать с зонами.
|
в каком виде время попадает в базу и в логи, что делать с зонами.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
## Правила
|
## Правила
|
||||||
|
|
||||||
@@ -19,7 +21,7 @@ extends: arch/time.md
|
|||||||
**ДОЛЖЕН.** Текущее время приходит из `store.Now()`, возвращающего
|
**ДОЛЖЕН.** Текущее время приходит из `store.Now()`, возвращающего
|
||||||
`time.Now().UTC()`, а не из `time.Now()` по коду.
|
`time.Now().UTC()`, а не из `time.Now()` по коду.
|
||||||
|
|
||||||
**Почему.** Единая точка даёт гарантированный UTC и один формат: ни одна
|
**ПОЧЕМУ.** Единая точка даёт гарантированный UTC и один формат: ни одна
|
||||||
ветка кода не может о них забыть. Разъехавшиеся зоны чинятся только чтением
|
ветка кода не может о них забыть. Разъехавшиеся зоны чинятся только чтением
|
||||||
всех записей — по метке `2026-06-28T11:23:45Z` уже не видно, была ли она
|
всех записей — по метке `2026-06-28T11:23:45Z` уже не видно, была ли она
|
||||||
когда-то локальной, и восстановить смещение задним числом не по чему.
|
когда-то локальной, и восстановить смещение задним числом не по чему.
|
||||||
@@ -33,7 +35,7 @@ extends: arch/time.md
|
|||||||
**ДОЛЖЕН.** Пара функций поверх `time.RFC3339` — единственный способ
|
**ДОЛЖЕН.** Пара функций поверх `time.RFC3339` — единственный способ
|
||||||
получить строку времени и прочитать её обратно.
|
получить строку времени и прочитать её обратно.
|
||||||
|
|
||||||
**Почему.** Layout, набранный по месту вызова, превращает формат хранения в
|
**ПОЧЕМУ.** Layout, набранный по месту вызова, превращает формат хранения в
|
||||||
свойство каждой отдельной строки кода. Фиксированная ширина (GTIM-4) и
|
свойство каждой отдельной строки кода. Фиксированная ширина (GTIM-4) и
|
||||||
взаимная обратимость записи и чтения держатся ровно до первого второго
|
взаимная обратимость записи и чтения держатся ровно до первого второго
|
||||||
layout — а расхождение проявится не на записи, а при сравнении значений,
|
layout — а расхождение проявится не на записи, а при сравнении значений,
|
||||||
@@ -49,7 +51,7 @@ layout — а расхождение проявится не на записи,
|
|||||||
| GTIM-3.1 | сама точка `store.Now()` | внутри неё вызов `time.Now()` и происходит |
|
| GTIM-3.1 | сама точка `store.Now()` | внутри неё вызов `time.Now()` и происходит |
|
||||||
| GTIM-3.2 | обёртка измерения длительности | приведение к UTC срезает монотонную составляющую (GTIM-10) |
|
| GTIM-3.2 | обёртка измерения длительности | приведение к UTC срезает монотонную составляющую (GTIM-10) |
|
||||||
|
|
||||||
**Почему.** GTIM-1 без механической проверки держится на внимании, а
|
**ПОЧЕМУ.** GTIM-1 без механической проверки держится на внимании, а
|
||||||
`time.Now()` пишется рефлекторно — и в новом коде, и в мелкой правке;
|
`time.Now()` пишется рефлекторно — и в новом коде, и в мелкой правке;
|
||||||
нарушение молчит до тех пор, пока процесс не окажется в не-UTC зоне.
|
нарушение молчит до тех пор, пока процесс не окажется в не-UTC зоне.
|
||||||
Исключения перечисляются исчерпывающе, потому что каждое из них — само по
|
Исключения перечисляются исчерпывающе, потому что каждое из них — само по
|
||||||
@@ -63,7 +65,7 @@ layout — а расхождение проявится не на записи,
|
|||||||
`//nolint:forbidigo // <причина>` на строке вызова; exclude-записи в
|
`//nolint:forbidigo // <причина>` на строке вызова; exclude-записи в
|
||||||
конфигурации линтера для него не заводятся.
|
конфигурации линтера для него не заводятся.
|
||||||
|
|
||||||
**Почему.** Запись в конфиге — второй реестр тех же двух мест: она адресует
|
**ПОЧЕМУ.** Запись в конфиге — второй реестр тех же двух мест: она адресует
|
||||||
исключение путём к файлу, отвязывается при переносе кода и продолжает
|
исключение путём к файлу, отвязывается при переносе кода и продолжает
|
||||||
разрешать `time.Now()` там, где исключения уже нет, — молча. Директива
|
разрешать `time.Now()` там, где исключения уже нет, — молча. Директива
|
||||||
переезжает вместе с кодом, и grep по `nolint:forbidigo` даёт весь список —
|
переезжает вместе с кодом, и grep по `nolint:forbidigo` даёт весь список —
|
||||||
@@ -77,7 +79,7 @@ layout — а расхождение проявится не на записи,
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Каноническое значение в колонке — `2026-06-28T11:23:45Z`.
|
**ДОЛЖЕН.** Каноническое значение в колонке — `2026-06-28T11:23:45Z`.
|
||||||
|
|
||||||
**Почему.** Строки в колонке `TEXT` сравниваются побайтово, поэтому
|
**ПОЧЕМУ.** Строки в колонке `TEXT` сравниваются побайтово, поэтому
|
||||||
лексикографический порядок совпадает с хронологическим только при
|
лексикографический порядок совпадает с хронологическим только при
|
||||||
одинаковых ширине и форме. Значение с долями секунды сортируется **раньше**
|
одинаковых ширине и форме. Значение с долями секунды сортируется **раньше**
|
||||||
целой секунды того же момента (`.` меньше `Z`), то есть ломаются и
|
целой секунды того же момента (`.` меньше `Z`), то есть ломаются и
|
||||||
@@ -91,7 +93,7 @@ layout — а расхождение проявится не на записи,
|
|||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Ни как layout вывода, ни как формат хранения.
|
**НЕ ДОЛЖЕН.** Ни как layout вывода, ни как формат хранения.
|
||||||
|
|
||||||
**Почему.** Он отбрасывает хвостовые нули, из-за чего длина строки зависит
|
**ПОЧЕМУ.** Он отбрасывает хвостовые нули, из-за чего длина строки зависит
|
||||||
от значения: соседние записи получают разную ширину, и свойство, на котором
|
от значения: соседние записи получают разную ширину, и свойство, на котором
|
||||||
держится GTIM-4, исчезает незаметно. Проверка «формат корректен» при этом
|
держится GTIM-4, исчезает незаметно. Проверка «формат корректен» при этом
|
||||||
проходит — отказывает только порядок.
|
проходит — отказывает только порядок.
|
||||||
@@ -102,7 +104,7 @@ layout — а расхождение проявится не на записи,
|
|||||||
к каноническому виду явно, а не считается каноническим по факту успешного
|
к каноническому виду явно, а не считается каноническим по факту успешного
|
||||||
разбора.
|
разбора.
|
||||||
|
|
||||||
**Почему.** `time.Parse(time.RFC3339, …)` принимает и доли секунды, и
|
**ПОЧЕМУ.** `time.Parse(time.RFC3339, …)` принимает и доли секунды, и
|
||||||
офсеты, отличные от `Z`, — разбор шире вывода. Канонический вид гарантирует
|
офсеты, отличные от `Z`, — разбор шире вывода. Канонический вид гарантирует
|
||||||
**писатель**, а не читатель; пока писатель один, этого достаточно, но
|
**писатель**, а не читатель; пока писатель один, этого достаточно, но
|
||||||
значение из чужой системы, положенное в базу как пришло, нарушает GTIM-4 и
|
значение из чужой системы, положенное в базу как пришло, нарушает GTIM-4 и
|
||||||
@@ -114,7 +116,7 @@ Go-механика, из-за которой его легко нарушить
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** В запрос идёт результат `FormatTime`.
|
**СЛЕДУЕТ.** В запрос идёт результат `FormatTime`.
|
||||||
|
|
||||||
**Почему.** Колонка — `TEXT`, и передача `time.Time` отдаёт форматирование
|
**ПОЧЕМУ.** Колонка — `TEXT`, и передача `time.Time` отдаёт форматирование
|
||||||
драйверу: появляется вторая точка формата вне `FormatTime` (GTIM-2), с
|
драйверу: появляется вторая точка формата вне `FormatTime` (GTIM-2), с
|
||||||
собственным layout, который меняется вместе с версией драйвера, а не вместе
|
собственным layout, который меняется вместе с версией драйвера, а не вместе
|
||||||
с конвенцией.
|
с конвенцией.
|
||||||
@@ -132,7 +134,7 @@ func utcTime(_ []string, a slog.Attr) slog.Attr {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**Почему.** `slog` по умолчанию UTC не даёт: встроенные хендлеры пишут
|
**ПОЧЕМУ.** `slog` по умолчанию UTC не даёт: встроенные хендлеры пишут
|
||||||
время в зоне самого `time.Time`, то есть в локальной зоне процесса — на
|
время в зоне самого `time.Time`, то есть в локальной зоне процесса — на
|
||||||
ноутбуке разработчика логи молча уезжают в `+03:00`. Умолчание тихое,
|
ноутбуке разработчика логи молча уезжают в `+03:00`. Умолчание тихое,
|
||||||
неверная зона выглядит как совершенно валидное время, а записи из разных
|
неверная зона выглядит как совершенно валидное время, а записи из разных
|
||||||
@@ -143,7 +145,7 @@ func utcTime(_ []string, a slog.Attr) slog.Attr {
|
|||||||
**ДОПУСКАЕТСЯ.** `JSONHandler` пишет миллисекунды — три знака, и это не
|
**ДОПУСКАЕТСЯ.** `JSONHandler` пишет миллисекунды — три знака, и это не
|
||||||
приводится к секундной точности GTIM-4.
|
приводится к секундной точности GTIM-4.
|
||||||
|
|
||||||
**Почему.** Явное разрешение нужно, чтобы GTIM-4 не читался как требование
|
**ПОЧЕМУ.** Явное разрешение нужно, чтобы GTIM-4 не читался как требование
|
||||||
одной точности везде. Ширина фиксируется на носитель: три знака в логе —
|
одной точности везде. Ширина фиксируется на носитель: три знака в логе —
|
||||||
такая же фиксированная ширина, и свойство, ради которого GTIM-4 существует, не
|
такая же фиксированная ширина, и свойство, ради которого GTIM-4 существует, не
|
||||||
нарушено. Общее у лога и базы одно — зона (GTIM-8).
|
нарушено. Общее у лога и базы одно — зона (GTIM-8).
|
||||||
@@ -153,7 +155,7 @@ func utcTime(_ []string, a slog.Attr) slog.Attr {
|
|||||||
**ДОПУСКАЕТСЯ.** Обёртка вызывает `time.Now()` и считает `time.Since` — с
|
**ДОПУСКАЕТСЯ.** Обёртка вызывает `time.Now()` и считает `time.Since` — с
|
||||||
локальным `//nolint`.
|
локальным `//nolint`.
|
||||||
|
|
||||||
**Почему.** `store.Now()` приводит время к UTC через `.UTC()`, а это
|
**ПОЧЕМУ.** `store.Now()` приводит время к UTC через `.UTC()`, а это
|
||||||
срезает монотонную составляющую `time.Time`. Интервал, посчитанный по таким
|
срезает монотонную составляющую `time.Time`. Интервал, посчитанный по таким
|
||||||
меткам, зависит от подводки часов: перевод назад даёт отрицательную
|
меткам, зависит от подводки часов: перевод назад даёт отрицательную
|
||||||
длительность, скачок вперёд — выброс в измерениях, и оба случая
|
длительность, скачок вперёд — выброс в измерениях, и оба случая
|
||||||
@@ -164,7 +166,7 @@ func utcTime(_ []string, a slog.Attr) slog.Attr {
|
|||||||
|
|
||||||
**ДОЛЖЕН.** База зон вшивается в бинарь.
|
**ДОЛЖЕН.** База зон вшивается в бинарь.
|
||||||
|
|
||||||
**Почему.** Без неё `LoadLocation` зависит от файлов зон в системе, которых
|
**ПОЧЕМУ.** Без неё `LoadLocation` зависит от файлов зон в системе, которых
|
||||||
в минимальном образе нет: отказ происходит в рантайме, на первой же попытке
|
в минимальном образе нет: отказ происходит в рантайме, на первой же попытке
|
||||||
применить зону, — то есть после выкладки, а не на сборке. Импорт именно в
|
применить зону, — то есть после выкладки, а не на сборке. Импорт именно в
|
||||||
`main` держит это решение в одном видимом месте, а не в случайном пакете,
|
`main` держит это решение в одном видимом месте, а не в случайном пакете,
|
||||||
@@ -175,16 +177,13 @@ func utcTime(_ []string, a slog.Attr) slog.Attr {
|
|||||||
**ДОЛЖЕН.** Зона из конфига используется в шаблонах и форматтерах
|
**ДОЛЖЕН.** Зона из конфига используется в шаблонах и форматтерах
|
||||||
представления, но не в хранимых значениях и не в вычислениях.
|
представления, но не в хранимых значениях и не в вычислениях.
|
||||||
|
|
||||||
**Почему.** Зона отображения — настройка, и её меняют. Протекая в
|
**ПОЧЕМУ.** Зона отображения — настройка, и её меняют. Протекая в
|
||||||
вычисления и хранение, она делает уже записанные данные зависимыми от
|
вычисления и хранение, она делает уже записанные данные зависимыми от
|
||||||
текущего значения настройки: смена зоны задним числом сдвигает границы
|
текущего значения настройки: смена зоны задним числом сдвигает границы
|
||||||
суток у того, что давно посчитано и сохранено.
|
суток у того, что давно посчитано и сохранено.
|
||||||
|
|
||||||
Календарные вычисления бизнес-логики берут зону явно — как описано в
|
Явную зону в календарных вычислениях требует TIME-12 — это его правило, а не
|
||||||
базовом слое.
|
второе такое же здесь.
|
||||||
|
|
||||||
<!-- local:механизировано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Связано
|
## Связано
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
topic: app-directories
|
||||||
prefix: ANSD
|
prefix: ANSD
|
||||||
|
stack: ansible
|
||||||
extends: arch/app-directories.md
|
extends: arch/app-directories.md
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -7,9 +9,9 @@ extends: arch/app-directories.md
|
|||||||
|
|
||||||
Как категории из базового слоя раскладываются на сервере плейбуком.
|
Как категории из базового слоя раскладываются на сервере плейбуком.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
## Область действия
|
## Область действия
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ extends: arch/app-directories.md
|
|||||||
состоит из нескольких директорий, имя даётся по содержимому (`media_dir`,
|
состоит из нескольких директорий, имя даётся по содержимому (`media_dir`,
|
||||||
`uploads_dir`, `dumps_dir`).
|
`uploads_dir`, `dumps_dir`).
|
||||||
|
|
||||||
**Почему.** Переменная — единственная ссылка, которую разделяют задача
|
**ПОЧЕМУ.** Переменная — единственная ссылка, которую разделяют задача
|
||||||
создания директории и список бэкапа (ANSD-4). Литерал пути в одном из этих мест
|
создания директории и список бэкапа (ANSD-4). Литерал пути в одном из этих мест
|
||||||
означает, что переименование директории молча разойдётся с бэкапом, и
|
означает, что переименование директории молча разойдётся с бэкапом, и
|
||||||
обнаружится это при восстановлении.
|
обнаружится это при восстановлении.
|
||||||
@@ -36,7 +38,7 @@ extends: arch/app-directories.md
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** Список директорий в единственной задаче создания.
|
**СЛЕДУЕТ.** Список директорий в единственной задаче создания.
|
||||||
|
|
||||||
**Почему.** Этот список — единственное место, где декларировано всё, что
|
**ПОЧЕМУ.** Этот список — единственное место, где декларировано всё, что
|
||||||
приложение пишет на диск. Разнесённое по нескольким задачам создание
|
приложение пишет на диск. Разнесённое по нескольким задачам создание
|
||||||
отвечает на вопрос «какие директории есть у приложения» только чтением
|
отвечает на вопрос «какие директории есть у приложения» только чтением
|
||||||
всего плейбука, а именно этот вопрос задают при заведении бэкапа и при
|
всего плейбука, а именно этот вопрос задают при заведении бэкапа и при
|
||||||
@@ -48,7 +50,7 @@ extends: arch/app-directories.md
|
|||||||
(`app_owner_uid == app_owner_gid`) или общий `primary_user` — выбирается на
|
(`app_owner_uid == app_owner_gid`) или общий `primary_user` — выбирается на
|
||||||
репозиторий и фиксируется ниже.
|
репозиторий и фиксируется ниже.
|
||||||
|
|
||||||
**Почему.** Правило про соответствие владельца рантайму, а не про
|
**ПОЧЕМУ.** Правило про соответствие владельца рантайму, а не про
|
||||||
конкретную модель: приложение в контейнере пишет от определённого uid, и
|
конкретную модель: приложение в контейнере пишет от определённого uid, и
|
||||||
если директория принадлежит другому, отказ произойдёт не при деплое, а при
|
если директория принадлежит другому, отказ произойдёт не при деплое, а при
|
||||||
первой записи — то есть после того, как плейбук отчитался об успехе. Выбор
|
первой записи — то есть после того, как плейбук отчитался об успехе. Выбор
|
||||||
@@ -56,15 +58,12 @@ extends: arch/app-directories.md
|
|||||||
изоляцией по приложениям решают разные задачи, и навязывать одну модель
|
изоляцией по приложениям решают разные задачи, и навязывать одну модель
|
||||||
обоим значит гарантировать вечное отступление.
|
обоим значит гарантировать вечное отступление.
|
||||||
|
|
||||||
<!-- local:модель-владельца -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
### ANSD-4. Список бэкапа собирается из тех же переменных
|
### ANSD-4. Список бэкапа собирается из тех же переменных
|
||||||
|
|
||||||
**ДОЛЖЕН.** Плейбук кладёт в `base_dir` файл `backup-targets`, строки
|
**ДОЛЖЕН.** Плейбук кладёт в `base_dir` файл `backup-targets`, строки
|
||||||
которого ссылаются на переменные `*_dir` из ANSD-1, а не на литеральные пути.
|
которого ссылаются на переменные `*_dir` из ANSD-1, а не на литеральные пути.
|
||||||
|
|
||||||
**Почему.** Правило вывода списка механическое (ANSD-5), но применяет его
|
**ПОЧЕМУ.** Правило вывода списка механическое (ANSD-5), но применяет его
|
||||||
человек или шаблон — то есть ошибиться можно. Общая переменная делает целый
|
человек или шаблон — то есть ошибиться можно. Общая переменная делает целый
|
||||||
класс ошибок невозможным: переименовал директорию — переименовалось в
|
класс ошибок невозможным: переименовал директорию — переименовалось в
|
||||||
обоих местах. Независимо набранный список расходится тихо и проявляется в
|
обоих местах. Независимо набранный список расходится тихо и проявляется в
|
||||||
@@ -75,7 +74,7 @@ extends: arch/app-directories.md
|
|||||||
**ДОЛЖЕН.** Директории категории «данные», включая директорию дампов, — в
|
**ДОЛЖЕН.** Директории категории «данные», включая директорию дампов, — в
|
||||||
списке; конфигурация и кеш — нет.
|
списке; конфигурация и кеш — нет.
|
||||||
|
|
||||||
**Почему.** Реализация правила базовой конвенции. Кеш раздувает снапшот без
|
**ПОЧЕМУ.** Реализация правила базовой конвенции. Кеш раздувает снапшот без
|
||||||
пользы, а конфигурация содержит отрендеренные секреты — бэкап уезжает в
|
пользы, а конфигурация содержит отрендеренные секреты — бэкап уезжает в
|
||||||
облако, и источником истины для секретов остаётся vault, а не снапшот.
|
облако, и источником истины для секретов остаётся vault, а не снапшот.
|
||||||
|
|
||||||
@@ -83,7 +82,7 @@ extends: arch/app-directories.md
|
|||||||
|
|
||||||
**СЛЕДУЕТ.** В compose конфигурация подключается с `:ro`.
|
**СЛЕДУЕТ.** В compose конфигурация подключается с `:ro`.
|
||||||
|
|
||||||
**Почему.** Плейбук — источник истины для конфигурации, и `:ro` превращает
|
**ПОЧЕМУ.** Плейбук — источник истины для конфигурации, и `:ro` превращает
|
||||||
это из договорённости в свойство системы: приложение, которое втихую
|
это из договорённости в свойство системы: приложение, которое втихую
|
||||||
переписывает свой конфиг, падает сразу, а не расходится с репозиторием
|
переписывает свой конфиг, падает сразу, а не расходится с репозиторием
|
||||||
незаметно. Приложение, которому запись в конфиг нужна по устройству,
|
незаметно. Приложение, которому запись в конфиг нужна по устройству,
|
||||||
@@ -93,7 +92,7 @@ extends: arch/app-directories.md
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Файл не переносится во вложенную директорию.
|
**ДОЛЖЕН.** Файл не переносится во вложенную директорию.
|
||||||
|
|
||||||
**Почему.** Туда смотрит `project_src` модуля `docker_compose_v2`. Правило
|
**ПОЧЕМУ.** Туда смотрит `project_src` модуля `docker_compose_v2`. Правило
|
||||||
внешнее по происхождению, но нарушается легко — при попытке «навести
|
внешнее по происхождению, но нарушается легко — при попытке «навести
|
||||||
порядок» и убрать compose в `config/`, где ему по смыслу категорий было бы
|
порядок» и убрать compose в `config/`, где ему по смыслу категорий было бы
|
||||||
место.
|
место.
|
||||||
@@ -103,7 +102,7 @@ extends: arch/app-directories.md
|
|||||||
**СЛЕДУЕТ.** Значения приходят из vault-переменных и попадают в файл,
|
**СЛЕДУЕТ.** Значения приходят из vault-переменных и попадают в файл,
|
||||||
принадлежащий пользователю приложения.
|
принадлежащий пользователю приложения.
|
||||||
|
|
||||||
**Почему.** Файл под `0600` не наследуется дочерними процессами, не виден в
|
**ПОЧЕМУ.** Файл под `0600` не наследуется дочерними процессами, не виден в
|
||||||
`docker inspect` и не оседает в compose-файле на диске. Это те же три
|
`docker inspect` и не оседает в compose-файле на диске. Это те же три
|
||||||
довода, по которым базовая конвенция конфигурации выбирает файл вместо
|
довода, по которым базовая конвенция конфигурации выбирает файл вместо
|
||||||
окружения.
|
окружения.
|
||||||
@@ -112,15 +111,7 @@ extends: arch/app-directories.md
|
|||||||
|
|
||||||
**ДОПУСКАЕТСЯ.** Задача рендера идёт с `no_log: true`.
|
**ДОПУСКАЕТСЯ.** Задача рендера идёт с `no_log: true`.
|
||||||
|
|
||||||
**Почему.** Явное разрешение нужно, чтобы ANSD-8 не читался как запрет на
|
**ПОЧЕМУ.** Явное разрешение нужно, чтобы ANSD-8 не читался как запрет на
|
||||||
деплой такого приложения. Способ вынужденный: секрет попадает в метаданные
|
деплой такого приложения. Способ вынужденный: секрет попадает в метаданные
|
||||||
контейнера и в compose-файл на диске. Приложение, научившееся читать
|
контейнера и в compose-файл на диске. Приложение, научившееся читать
|
||||||
секреты из файла, переводится на ANSD-8 при ближайшем касании.
|
секреты из файла, переводится на ANSD-8 при ближайшем касании.
|
||||||
|
|
||||||
<!-- local:отступления -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
## Связано
|
|
||||||
|
|
||||||
<!-- local:связано -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
---
|
---
|
||||||
|
topic: web-ui
|
||||||
prefix: HTMX
|
prefix: HTMX
|
||||||
|
stack: htmx
|
||||||
---
|
---
|
||||||
|
|
||||||
# Веб-UI на htmx
|
# Веб-UI на htmx
|
||||||
@@ -8,9 +10,9 @@ prefix: HTMX
|
|||||||
обработчики действий, деградация без JS, ошибки. Что именно UI показывает и
|
обработчики действий, деградация без JS, ошибки. Что именно UI показывает и
|
||||||
какие действия поддерживает — в спеках, не здесь.
|
какие действия поддерживает — в спеках, не здесь.
|
||||||
|
|
||||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
ПОЧЕМУ, ПРИМЕРЫ, МЕХАНИЗИРОВАНО и СНЯТО толкуются как описано в языке
|
||||||
только тогда, когда написаны заглавными.
|
конвенций версии 1 — тогда и только тогда, когда написаны заглавными.
|
||||||
|
|
||||||
Логирование запросов — конвенция `logging` (HTTP-поля, рутинно-частое на
|
Логирование запросов — конвенция `logging` (HTTP-поля, рутинно-частое на
|
||||||
`DEBUG`). Трансляция доменных ошибок наружу — конвенция `errors`
|
`DEBUG`). Трансляция доменных ошибок наружу — конвенция `errors`
|
||||||
@@ -30,7 +32,7 @@ prefix: HTMX
|
|||||||
**ДОЛЖЕН.** UI собирается из серверных шаблонов и htmx — без шага сборки,
|
**ДОЛЖЕН.** UI собирается из серверных шаблонов и htmx — без шага сборки,
|
||||||
без Node и бандлера, без реактивного фреймворка.
|
без Node и бандлера, без реактивного фреймворка.
|
||||||
|
|
||||||
**Почему.** Шаг сборки — это второй язык, второй менеджер зависимостей и
|
**ПОЧЕМУ.** Шаг сборки — это второй язык, второй менеджер зависимостей и
|
||||||
артефакт, который расходится с исходником; приложению, где разметку целиком
|
артефакт, который расходится с исходником; приложению, где разметку целиком
|
||||||
отдаёт сервер, он не покупает ничего. Реактивный фреймворк добавляет вторую
|
отдаёт сервер, он не покупает ничего. Реактивный фреймворк добавляет вторую
|
||||||
модель состояния рядом с серверной (HTMX-2), и дальше на каждом экране
|
модель состояния рядом с серверной (HTMX-2), и дальше на каждом экране
|
||||||
@@ -44,7 +46,7 @@ prefix: HTMX
|
|||||||
(копирование в буфер обмена и подобное); доменное состояние считает сервер,
|
(копирование в буфер обмена и подобное); доменное состояние считает сервер,
|
||||||
клиент свопит присланную разметку.
|
клиент свопит присланную разметку.
|
||||||
|
|
||||||
**Почему.** Пересчёт на клиенте — вторая реализация той же логики, которую
|
**ПОЧЕМУ.** Пересчёт на клиенте — вторая реализация той же логики, которую
|
||||||
никто не сверяет: расходится она тихо, а проявляется как «на экране одно, в
|
никто не сверяет: расходится она тихо, а проявляется как «на экране одно, в
|
||||||
базе другое». Вдобавок клиентский пересчёт по определению не работает в
|
базе другое». Вдобавок клиентский пересчёт по определению не работает в
|
||||||
деградированном режиме (HTMX-11, HTMX-12) — значит, серверную версию того же
|
деградированном режиме (HTMX-11, HTMX-12) — значит, серверную версию того же
|
||||||
@@ -56,7 +58,7 @@ prefix: HTMX
|
|||||||
только когда есть виджет, которому он действительно нужен, и отдельным
|
только когда есть виджет, которому он действительно нужен, и отдельным
|
||||||
решением.
|
решением.
|
||||||
|
|
||||||
**Почему.** Реактивный слой, попавший в проект ради одного выпадающего
|
**ПОЧЕМУ.** Реактивный слой, попавший в проект ради одного выпадающего
|
||||||
списка, немедленно доступен всему остальному коду — и граница HTMX-1/HTMX-2
|
списка, немедленно доступен всему остальному коду — и граница HTMX-1/HTMX-2
|
||||||
перестаёт держаться сама собой. Отдельное решение — единственный момент,
|
перестаёт держаться сама собой. Отдельное решение — единственный момент,
|
||||||
когда цену видно целиком: она не в килобайтах, а в том, что дальше на
|
когда цену видно целиком: она не в килобайтах, а в том, что дальше на
|
||||||
@@ -70,7 +72,7 @@ prefix: HTMX
|
|||||||
`partials/`, и он же рендерится инлайн на странице и как ответ-фрагмент
|
`partials/`, и он же рендерится инлайн на странице и как ответ-фрагмент
|
||||||
обработчика; отдельной разметки под фрагмент нет.
|
обработчика; отдельной разметки под фрагмент нет.
|
||||||
|
|
||||||
**Почему.** Две копии одной разметки расходятся молча: правку вносят в ту,
|
**ПОЧЕМУ.** Две копии одной разметки расходятся молча: правку вносят в ту,
|
||||||
что открыта, и страница начинает выглядеть иначе, чем результат свопа того
|
что открыта, и страница начинает выглядеть иначе, чем результат свопа того
|
||||||
же региона. Заметно это становится только на глаз и только тому, кто открыл
|
же региона. Заметно это становится только на глаз и только тому, кто открыл
|
||||||
оба пути подряд.
|
оба пути подряд.
|
||||||
@@ -80,7 +82,7 @@ prefix: HTMX
|
|||||||
**ДОЛЖЕН.** Корневой узел шаблона несёт тот `id`, по которому адресуют
|
**ДОЛЖЕН.** Корневой узел шаблона несёт тот `id`, по которому адресуют
|
||||||
регион, и ответный фрагмент несёт тот же `id`.
|
регион, и ответный фрагмент несёт тот же `id`.
|
||||||
|
|
||||||
**Почему.** `hx-swap="outerHTML"` заменяет корневой узел целиком, вместе с
|
**ПОЧЕМУ.** `hx-swap="outerHTML"` заменяет корневой узел целиком, вместе с
|
||||||
его атрибутами. Если пришедший фрагмент несёт другой `id` или не несёт его
|
его атрибутами. Если пришедший фрагмент несёт другой `id` или не несёт его
|
||||||
вовсе, первый своп проходит успешно, а следующее действие и поллер уже не
|
вовсе, первый своп проходит успешно, а следующее действие и поллер уже не
|
||||||
находят таргет: регион застывает без единой ошибки — ни в консоли, ни в
|
находят таргет: регион застывает без единой ошибки — ни в консоли, ни в
|
||||||
@@ -91,7 +93,7 @@ prefix: HTMX
|
|||||||
**СЛЕДУЕТ.** Один view-builder зовут и обработчик полной страницы, и
|
**СЛЕДУЕТ.** Один view-builder зовут и обработчик полной страницы, и
|
||||||
htmx-ветка.
|
htmx-ветка.
|
||||||
|
|
||||||
**Почему.** Общий шаблон (HTMX-4) гарантирует одинаковую разметку, но не
|
**ПОЧЕМУ.** Общий шаблон (HTMX-4) гарантирует одинаковую разметку, но не
|
||||||
одинаковые данные: скопированная сборка view расходится по набору полей, и
|
одинаковые данные: скопированная сборка view расходится по набору полей, и
|
||||||
фрагмент начинает показывать не то, что показала бы страница. Это ровно тот
|
фрагмент начинает показывать не то, что показала бы страница. Это ровно тот
|
||||||
класс расхождений, который HTMX-4 закрывает для разметки.
|
класс расхождений, который HTMX-4 закрывает для разметки.
|
||||||
@@ -124,7 +126,7 @@ if actionErr != nil {
|
|||||||
s.render(w, "source_block", view) // фрагмент = тот же шаблон
|
s.render(w, "source_block", view) // фрагмент = тот же шаблон
|
||||||
```
|
```
|
||||||
|
|
||||||
**Почему.** Ветвление до вызова даёт две реализации одного действия, и
|
**ПОЧЕМУ.** Ветвление до вызова даёт две реализации одного действия, и
|
||||||
дальше дефект воспроизводится только на одной поверхности — причём
|
дальше дефект воспроизводится только на одной поверхности — причём
|
||||||
деградированный путь (HTMX-11) открывают реже, то есть чинить будут не тот.
|
деградированный путь (HTMX-11) открывают реже, то есть чинить будут не тот.
|
||||||
Перечитанное состояние в htmx-ветке нужно потому, что своп заменяет регион
|
Перечитанное состояние в htmx-ветке нужно потому, что своп заменяет регион
|
||||||
@@ -136,7 +138,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОЛЖЕН.** Именованный шаблон собирается целиком в буфер, и только затем
|
**ДОЛЖЕН.** Именованный шаблон собирается целиком в буфер, и только затем
|
||||||
буфер пишется в ответ.
|
буфер пишется в ответ.
|
||||||
|
|
||||||
**Почему.** Прямая запись в ответ отправляет клиенту статус и часть
|
**ПОЧЕМУ.** Прямая запись в ответ отправляет клиенту статус и часть
|
||||||
разметки раньше, чем шаблон дошёл до ошибки: сообщить об отказе уже нечем,
|
разметки раньше, чем шаблон дошёл до ошибки: сообщить об отказе уже нечем,
|
||||||
а htmx свопит в DOM полученный обрывок. Внешне это «исчезла половина
|
а htmx свопит в DOM полученный обрывок. Внешне это «исчезла половина
|
||||||
региона», и причина по такому симптому не читается.
|
региона», и причина по такому симптому не читается.
|
||||||
@@ -149,7 +151,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
отдаётся в том же ответе с `hx-swap-oob="true"` — обычным именованным
|
отдаётся в том же ответе с `hx-swap-oob="true"` — обычным именованным
|
||||||
партиалом с тем же `id`, что и на странице (HTMX-4, HTMX-5).
|
партиалом с тем же `id`, что и на странице (HTMX-4, HTMX-5).
|
||||||
|
|
||||||
**Почему.** Второй запрос с клиента вводит гонку: два ответа считают
|
**ПОЧЕМУ.** Второй запрос с клиента вводит гонку: два ответа считают
|
||||||
состояние в разные моменты и приезжают в произвольном порядке, поэтому
|
состояние в разные моменты и приезжают в произвольном порядке, поэтому
|
||||||
панель действий может отразить состояние до действия. Плюс лишний
|
панель действий может отразить состояние до действия. Плюс лишний
|
||||||
раунд-трип на каждое действие.
|
раунд-трип на каждое действие.
|
||||||
@@ -159,7 +161,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОПУСКАЕТСЯ.** `HX-Trigger` в ответе плюс отдельный `hx-get`, если второй
|
**ДОПУСКАЕТСЯ.** `HX-Trigger` в ответе плюс отдельный `hx-get`, если второй
|
||||||
регион меняется не на каждое действие.
|
регион меняется не на каждое действие.
|
||||||
|
|
||||||
**Почему.** Явное разрешение нужно, чтобы HTMX-9 не читался как запрет любого
|
**ПОЧЕМУ.** Явное разрешение нужно, чтобы HTMX-9 не читался как запрет любого
|
||||||
второго запроса. Когда регион обновляется редко, oob-ветка гоняет
|
второго запроса. Когда регион обновляется редко, oob-ветка гоняет
|
||||||
одинаковую разметку на каждое действие и связывает два шаблона там, где
|
одинаковую разметку на каждое действие и связывает два шаблона там, где
|
||||||
связи нет; гонка же тем менее наблюдаема, чем реже обновление.
|
связи нет; гонка же тем менее наблюдаема, чем реже обновление.
|
||||||
@@ -172,7 +174,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
`hx-post`/`hx-target`/`hx-swap` накладываются сверху; `action` ведёт на
|
`hx-post`/`hx-target`/`hx-swap` накладываются сверху; `action` ведёт на
|
||||||
рабочий обработчик.
|
рабочий обработчик.
|
||||||
|
|
||||||
**Почему.** htmx может не загрузиться — ошибка вендоринга, блокировщик,
|
**ПОЧЕМУ.** htmx может не загрузиться — ошибка вендоринга, блокировщик,
|
||||||
медленная сеть, — и без рабочего `action` форма в этот момент не отправляет
|
медленная сеть, — и без рабочего `action` форма в этот момент не отправляет
|
||||||
ничего, молча. Тот же `action` — единственное, что делает действие
|
ничего, молча. Тот же `action` — единственное, что делает действие
|
||||||
проверяемым без браузера с JS.
|
проверяемым без браузера с JS.
|
||||||
@@ -182,7 +184,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОЛЖЕН.** Отбор списка задаётся GET-параметрами и выполняется на сервере;
|
**ДОЛЖЕН.** Отбор списка задаётся GET-параметрами и выполняется на сервере;
|
||||||
клиентской фильтрации загруженной разметки нет.
|
клиентской фильтрации загруженной разметки нет.
|
||||||
|
|
||||||
**Почему.** Клиент видит только текущую страницу списка, поэтому клиентский
|
**ПОЧЕМУ.** Клиент видит только текущую страницу списка, поэтому клиентский
|
||||||
фильтр отвечает по неполным данным и делает это молча — результат выглядит
|
фильтр отвечает по неполным данным и делает это молча — результат выглядит
|
||||||
валидным. Вдобавок состояние отбора в query переживает своп (HTMX-25) и
|
валидным. Вдобавок состояние отбора в query переживает своп (HTMX-25) и
|
||||||
перезагрузку, его можно послать ссылкой и увидеть в логе.
|
перезагрузку, его можно послать ссылкой и увидеть в логе.
|
||||||
@@ -196,7 +198,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
| HTMX-13.1 | действия и навигация | работают полностью (HTMX-11, HTMX-12) |
|
| HTMX-13.1 | действия и навигация | работают полностью (HTMX-11, HTMX-12) |
|
||||||
| HTMX-13.2 | интерактивный виджет выбора, у которого нет осмысленного не-JS поведения | может не работать; записывается в отступления |
|
| HTMX-13.2 | интерактивный виджет выбора, у которого нет осмысленного не-JS поведения | может не работать; записывается в отступления |
|
||||||
|
|
||||||
**Почему.** Без явной границы правило деградации читается как запрет на
|
**ПОЧЕМУ.** Без явной границы правило деградации читается как запрет на
|
||||||
любой JS-виджет — и тогда его либо тихо нарушают, либо отказываются от
|
любой JS-виджет — и тогда его либо тихо нарушают, либо отказываются от
|
||||||
виджета, который был нужен. Запись в отступления держит список честным:
|
виджета, который был нужен. Запись в отступления держит список честным:
|
||||||
видно, какие именно места ломаются с выключенным JS, а не «где-то
|
видно, какие именно места ломаются с выключенным JS, а не «где-то
|
||||||
@@ -209,7 +211,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОЛЖЕН.** Провалившееся действие отдаёт статус 200 и фрагмент с
|
**ДОЛЖЕН.** Провалившееся действие отдаёт статус 200 и фрагмент с
|
||||||
сообщением; доменная ошибка на htmx-пути не транслируется в HTTP-статус.
|
сообщением; доменная ошибка на htmx-пути не транслируется в HTTP-статус.
|
||||||
|
|
||||||
**Почему.** В htmx 2.x ответы 4xx/5xx по умолчанию не свопят DOM — то есть
|
**ПОЧЕМУ.** В htmx 2.x ответы 4xx/5xx по умолчанию не свопят DOM — то есть
|
||||||
пользователь не увидит ничего. Своп ошибочных ответов настраивается
|
пользователь не увидит ничего. Своп ошибочных ответов настраивается
|
||||||
(`htmx.config.responseHandling`, расширение `response-targets`), но любая
|
(`htmx.config.responseHandling`, расширение `response-targets`), но любая
|
||||||
такая настройка — свой JS-конфиг на клиенте, и платится она из HTMX-1 и HTMX-2.
|
такая настройка — свой JS-конфиг на клиенте, и платится она из HTMX-1 и HTMX-2.
|
||||||
@@ -228,7 +230,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
ошибочных ответов в целевые регионы (`htmx.config.responseHandling`,
|
ошибочных ответов в целевые регионы (`htmx.config.responseHandling`,
|
||||||
`response-targets`) не настраивается.
|
`response-targets`) не настраивается.
|
||||||
|
|
||||||
**Почему.** HTMX-14 закрывает доменный отказ, до которого обработчик дошёл.
|
**ПОЧЕМУ.** HTMX-14 закрывает доменный отказ, до которого обработчик дошёл.
|
||||||
Паника, сбой шаблона и обрыв сети отдают 5xx или ничего, htmx 2.x такое не
|
Паника, сбой шаблона и обрыв сети отдают 5xx или ничего, htmx 2.x такое не
|
||||||
свопит — регион не меняется, интерфейс замирает без единого признака сбоя,
|
свопит — регион не меняется, интерфейс замирает без единого признака сбоя,
|
||||||
и пользователь повторяет действие, которое могло уже примениться. Слушатель
|
и пользователь повторяет действие, которое могло уже примениться. Слушатель
|
||||||
@@ -244,7 +246,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОЛЖЕН.** Во фрагмент попадает нейтральный текст публичного канала;
|
**ДОЛЖЕН.** Во фрагмент попадает нейтральный текст публичного канала;
|
||||||
`err.Error()` в разметку не рендерится.
|
`err.Error()` в разметку не рендерится.
|
||||||
|
|
||||||
**Почему.** htmx-фрагмент выглядит внутренней деталью приложения, и на нём
|
**ПОЧЕМУ.** htmx-фрагмент выглядит внутренней деталью приложения, и на нём
|
||||||
легче всего забыть, что это тот же публичный канал, что и страница:
|
легче всего забыть, что это тот же публичный канал, что и страница:
|
||||||
разметка уезжает в браузер пользователя целиком. Статус 200 (HTMX-14)
|
разметка уезжает в браузер пользователя целиком. Статус 200 (HTMX-14)
|
||||||
дополнительно снимает ощущение «это ошибочный ответ, его никто не увидит».
|
дополнительно снимает ощущение «это ошибочный ответ, его никто не увидит».
|
||||||
@@ -254,7 +256,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОЛЖЕН.** У view есть поле под ошибку действия; доменные поля под
|
**ДОЛЖЕН.** У view есть поле под ошибку действия; доменные поля под
|
||||||
сообщение не переиспользуются.
|
сообщение не переиспользуются.
|
||||||
|
|
||||||
**Почему.** У доменного поля может быть своё непустое значение, и сообщение
|
**ПОЧЕМУ.** У доменного поля может быть своё непустое значение, и сообщение
|
||||||
его перекроет: пользователь получит текст ошибки вместо данных, а шаблон —
|
его перекроет: пользователь получит текст ошибки вместо данных, а шаблон —
|
||||||
необходимость угадывать, что сейчас лежит в поле. Отдельное поле делает оба
|
необходимость угадывать, что сейчас лежит в поле. Отдельное поле делает оба
|
||||||
состояния — данные и ошибку — выразимыми одновременно, а это ровно то, чего
|
состояния — данные и ошибку — выразимыми одновременно, а это ровно то, чего
|
||||||
@@ -265,7 +267,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**НЕ ДОЛЖЕН.** Фрагмент, отданный после неудачного действия, показывает
|
**НЕ ДОЛЖЕН.** Фрагмент, отданный после неудачного действия, показывает
|
||||||
прежний выбор плюс сообщение.
|
прежний выбор плюс сообщение.
|
||||||
|
|
||||||
**Почему.** Своп заменяет регион целиком, поэтому фрагмент — единственное,
|
**ПОЧЕМУ.** Своп заменяет регион целиком, поэтому фрагмент — единственное,
|
||||||
что пользователь узнает о состоянии. Показав намеренное состояние вместо
|
что пользователь узнает о состоянии. Показав намеренное состояние вместо
|
||||||
фактического, интерфейс расходится с сервером, и следующее действие человек
|
фактического, интерфейс расходится с сервером, и следующее действие человек
|
||||||
делает по ложной картине — на сервере оно применится к другому объекту.
|
делает по ложной картине — на сервере оно применится к другому объекту.
|
||||||
@@ -288,7 +290,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОЛЖЕН.** Когда состояние вышло из «живого», фрагмент возвращается без
|
**ДОЛЖЕН.** Когда состояние вышло из «живого», фрагмент возвращается без
|
||||||
`hx-*`-атрибутов.
|
`hx-*`-атрибутов.
|
||||||
|
|
||||||
**Почему.** Иначе опрос не прекращается никогда: каждая открытая вкладка
|
**ПОЧЕМУ.** Иначе опрос не прекращается никогда: каждая открытая вкладка
|
||||||
держит постоянный поток запросов за неизменными данными, и закрывает его
|
держит постоянный поток запросов за неизменными данными, и закрывает его
|
||||||
только пользователь. Условие остановки живёт в разметке ответа, потому что
|
только пользователь. Условие остановки живёт в разметке ответа, потому что
|
||||||
это единственный канал, которым сервер управляет поллером.
|
это единственный канал, которым сервер управляет поллером.
|
||||||
@@ -302,7 +304,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОЛЖЕН.** Признак «живо ли ещё» вычисляется по состоянию, которым владеет
|
**ДОЛЖЕН.** Признак «живо ли ещё» вычисляется по состоянию, которым владеет
|
||||||
приложение, а не по ответу внешнего сервиса.
|
приложение, а не по ответу внешнего сервиса.
|
||||||
|
|
||||||
**Почему.** Внешний сервис отвечает не всегда и не одинаково: на его
|
**ПОЧЕМУ.** Внешний сервис отвечает не всегда и не одинаково: на его
|
||||||
недоступности поллер либо останавливается, пока работа идёт, либо не
|
недоступности поллер либо останавливается, пока работа идёт, либо не
|
||||||
останавливается никогда. Приложение — единственный участник, который знает
|
останавливается никогда. Приложение — единственный участник, который знает
|
||||||
про операцию всё и может ответить на каждом тике.
|
про операцию всё и может ответить на каждом тике.
|
||||||
@@ -312,7 +314,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**ДОЛЖЕН.** Тик заменяет весь фрагмент (`hx-swap="outerHTML"`), а не его
|
**ДОЛЖЕН.** Тик заменяет весь фрагмент (`hx-swap="outerHTML"`), а не его
|
||||||
содержимое.
|
содержимое.
|
||||||
|
|
||||||
**Почему.** `outerHTML` удаляет старый узел вместе с его `hx-trigger` и
|
**ПОЧЕМУ.** `outerHTML` удаляет старый узел вместе с его `hx-trigger` и
|
||||||
инициализирует новый — так поллер живёт ровно в одном экземпляре и так же
|
инициализирует новый — так поллер живёт ровно в одном экземпляре и так же
|
||||||
выключается (HTMX-18). Своп содержимого оставил бы старый узел с его таймером,
|
выключается (HTMX-18). Своп содержимого оставил бы старый узел с его таймером,
|
||||||
и через несколько обновлений опрос шёл бы в несколько потоков. Работает это
|
и через несколько обновлений опрос шёл бы в несколько потоков. Работает это
|
||||||
@@ -323,7 +325,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
**НЕ ДОЛЖЕН.** Живость включается только в тех состояниях фрагмента, где
|
**НЕ ДОЛЖЕН.** Живость включается только в тех состояниях фрагмента, где
|
||||||
редактировать нечего.
|
редактировать нечего.
|
||||||
|
|
||||||
**Почему.** Своп поддерева теряет фокус, выделение и незасабмиченный текст
|
**ПОЧЕМУ.** Своп поддерева теряет фокус, выделение и незасабмиченный текст
|
||||||
внутри него. У поллера это происходит по таймеру, то есть в момент, который
|
внутри него. У поллера это происходит по таймеру, то есть в момент, который
|
||||||
пользователь не выбирал: текст исчезает посреди набора и воспроизводится
|
пользователь не выбирал: текст исчезает посреди набора и воспроизводится
|
||||||
как «приложение стирает мой ввод».
|
как «приложение стирает мой ввод».
|
||||||
@@ -332,7 +334,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Поллинг и прочие запросы страницы идут на свой сервер.
|
**НЕ ДОЛЖЕН.** Поллинг и прочие запросы страницы идут на свой сервер.
|
||||||
|
|
||||||
**Почему.** Прямой запрос из браузера выносит наружу адрес и учётные данные
|
**ПОЧЕМУ.** Прямой запрос из браузера выносит наружу адрес и учётные данные
|
||||||
внешнего сервиса и делает страницу заложником его CORS-политики. Вдобавок
|
внешнего сервиса и делает страницу заложником его CORS-политики. Вдобавок
|
||||||
контракт внешнего сервиса протекает в разметку: его смена перестаёт быть
|
контракт внешнего сервиса протекает в разметку: его смена перестаёт быть
|
||||||
серверным изменением.
|
серверным изменением.
|
||||||
@@ -346,7 +348,7 @@ s.render(w, "source_block", view) // фрагмент = тот же шаб
|
|||||||
| HTMX-23.1 | состояние внешнего сервиса | in-memory снимок, обновляемый воркером |
|
| HTMX-23.1 | состояние внешнего сервиса | in-memory снимок, обновляемый воркером |
|
||||||
| HTMX-23.2 | собственное состояние приложения | своё хранилище; снимок не требуется |
|
| HTMX-23.2 | собственное состояние приложения | своё хранилище; снимок не требуется |
|
||||||
|
|
||||||
**Почему.** Тик умножается на число открытых вкладок, поэтому сеть на
|
**ПОЧЕМУ.** Тик умножается на число открытых вкладок, поэтому сеть на
|
||||||
каждом тике превращает интерфейс в генератор нагрузки на внешний сервис — и
|
каждом тике превращает интерфейс в генератор нагрузки на внешний сервис — и
|
||||||
его недоступность становится недоступностью страницы. Снимок разрывает эту
|
его недоступность становится недоступностью страницы. Снимок разрывает эту
|
||||||
связь: частоту обращений к внешнему сервису задаёт воркер, а не
|
связь: частоту обращений к внешнему сервису задаёт воркер, а не
|
||||||
@@ -365,7 +367,7 @@ hx-get="/item/{{.ID}}" hx-trigger="every 3s"
|
|||||||
hx-select="#item-main" hx-swap="outerHTML"
|
hx-select="#item-main" hx-swap="outerHTML"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Почему.** Отдельный `/fragments/…`-роут в этом случае дублирует
|
**ПОЧЕМУ.** Отдельный `/fragments/…`-роут в этом случае дублирует
|
||||||
обработчик страницы целиком — вместе с перечитыванием состояния и сборкой
|
обработчик страницы целиком — вместе с перечитыванием состояния и сборкой
|
||||||
view, — и дальше два обработчика расходятся по тому же сценарию, что и две
|
view, — и дальше два обработчика расходятся по тому же сценарию, что и две
|
||||||
копии разметки (HTMX-4).
|
копии разметки (HTMX-4).
|
||||||
@@ -379,7 +381,7 @@ view, — и дальше два обработчика расходятся п
|
|||||||
|
|
||||||
**НЕ ДОЛЖЕН.** Такое действие свопит свой регион на месте.
|
**НЕ ДОЛЖЕН.** Такое действие свопит свой регион на месте.
|
||||||
|
|
||||||
**Почему.** Своп сохраняет прокрутку и не трогает серверные фильтр, поиск и
|
**ПОЧЕМУ.** Своп сохраняет прокрутку и не трогает серверные фильтр, поиск и
|
||||||
пагинацию — они в query (HTMX-12). Полная навигация ради изменения одного
|
пагинацию — они в query (HTMX-12). Полная навигация ради изменения одного
|
||||||
региона возвращает пользователя в начало списка и стоит перерисовки всей
|
региона возвращает пользователя в начало списка и стоит перерисовки всей
|
||||||
страницы. Не сохраняется при свопе только контекст внутри самого
|
страницы. Не сохраняется при свопе только контекст внутри самого
|
||||||
@@ -390,7 +392,7 @@ view, — и дальше два обработчика расходятся п
|
|||||||
**ДОЛЖЕН.** Действие, после которого предмет покидает страницу, остаётся
|
**ДОЛЖЕН.** Действие, после которого предмет покидает страницу, остаётся
|
||||||
обычной POST-формой без htmx-атрибутов, то есть полной навигацией.
|
обычной POST-формой без htmx-атрибутов, то есть полной навигацией.
|
||||||
|
|
||||||
**Почему.** Своп для такого действия оставил бы на месте регион,
|
**ПОЧЕМУ.** Своп для такого действия оставил бы на месте регион,
|
||||||
описывающий объект, которого на странице больше нет. Отсутствие
|
описывающий объект, которого на странице больше нет. Отсутствие
|
||||||
htmx-атрибутов при этом само работает маркером «это выход»: намерение видно
|
htmx-атрибутов при этом само работает маркером «это выход»: намерение видно
|
||||||
прямо в разметке, и не нужен `HX-Redirect` — то есть ещё один способ
|
прямо в разметке, и не нужен `HX-Redirect` — то есть ещё один способ
|
||||||
@@ -401,7 +403,7 @@ htmx-атрибутов при этом само работает маркеро
|
|||||||
**ДОЛЖЕН.** Если работу доделывает воркер, ответ на действие показывает
|
**ДОЛЖЕН.** Если работу доделывает воркер, ответ на действие показывает
|
||||||
промежуточное состояние, а итог догоняет самозавершающийся поллер (HTMX-18).
|
промежуточное состояние, а итог догоняет самозавершающийся поллер (HTMX-18).
|
||||||
|
|
||||||
**Почему.** Мнимый результат расходится с сервером до следующего тика, и
|
**ПОЧЕМУ.** Мнимый результат расходится с сервером до следующего тика, и
|
||||||
всё это время пользователь принимает решения по несуществующему исходу —
|
всё это время пользователь принимает решения по несуществующему исходу —
|
||||||
включая повтор действия, которое на самом деле выполняется. Промежуточное
|
включая повтор действия, которое на самом деле выполняется. Промежуточное
|
||||||
состояние вдобавок объясняет, почему регион продолжает обновляться сам.
|
состояние вдобавок объясняет, почему регион продолжает обновляться сам.
|
||||||
@@ -414,7 +416,7 @@ htmx-атрибутов при этом само работает маркеро
|
|||||||
фрагментом, поверхность передаётся явным скрытым полем
|
фрагментом, поверхность передаётся явным скрытым полем
|
||||||
(`surface=list|detail`), а не выводится из `HX-Target` или `Referer`.
|
(`surface=list|detail`), а не выводится из `HX-Target` или `Referer`.
|
||||||
|
|
||||||
**Почему.** `HX-Target` — свойство разметки вызывающей страницы, `Referer`
|
**ПОЧЕМУ.** `HX-Target` — свойство разметки вызывающей страницы, `Referer`
|
||||||
может не прийти вовсе; и то и другое меняется без участия обработчика, и
|
может не прийти вовсе; и то и другое меняется без участия обработчика, и
|
||||||
ответ начинает приходить не тем фрагментом. Поле же видно в форме рядом с
|
ответ начинает приходить не тем фрагментом. Поле же видно в форме рядом с
|
||||||
действием, поэтому связь «эта страница → этот фрагмент» читается там, где
|
действием, поэтому связь «эта страница → этот фрагмент» читается там, где
|
||||||
@@ -426,7 +428,7 @@ htmx-атрибутов при этом само работает маркеро
|
|||||||
400, когда поля `surface` в запросе нет; поверхность по умолчанию не
|
400, когда поля `surface` в запросе нет; поверхность по умолчанию не
|
||||||
выбирается.
|
выбирается.
|
||||||
|
|
||||||
**Почему.** Поле кладёт в форму наш же шаблон, поэтому его отсутствие —
|
**ПОЧЕМУ.** Поле кладёт в форму наш же шаблон, поэтому его отсутствие —
|
||||||
дефект формы, а не вход пользователя. Поверхность по умолчанию маскирует
|
дефект формы, а не вход пользователя. Поверхность по умолчанию маскирует
|
||||||
такой дефект молча неверным фрагментом: своп с чужим `id` проходит, после
|
такой дефект молча неверным фрагментом: своп с чужим `id` проходит, после
|
||||||
чего регион перестаёт находиться таргетом (HTMX-5), и ошибка воспроизводится
|
чего регион перестаёт находиться таргетом (HTMX-5), и ошибка воспроизводится
|
||||||
@@ -446,7 +448,7 @@ htmx-атрибутов при этом само работает маркеро
|
|||||||
**ДОЛЖЕН.** Статика подключается через `go:embed` и отдаётся с
|
**ДОЛЖЕН.** Статика подключается через `go:embed` и отдаётся с
|
||||||
`Cache-Control: public, max-age=31536000, immutable`.
|
`Cache-Control: public, max-age=31536000, immutable`.
|
||||||
|
|
||||||
**Почему.** Встроенные ассеты делают деплой одним артефактом: нет второго
|
**ПОЧЕМУ.** Встроенные ассеты делают деплой одним артефактом: нет второго
|
||||||
шага раскладки файлов, который может отстать от бинаря и оставить новую
|
шага раскладки файлов, который может отстать от бинаря и оставить новую
|
||||||
разметку со старым css. Иммутабельный кэш безопасен ровно потому, что URL
|
разметку со старым css. Иммутабельный кэш безопасен ровно потому, что URL
|
||||||
меняется вместе с содержимым (HTMX-30, HTMX-31); без этого условия год кэша
|
меняется вместе с содержимым (HTMX-30, HTMX-31); без этого условия год кэша
|
||||||
@@ -457,7 +459,7 @@ htmx-атрибутов при этом само работает маркеро
|
|||||||
**ДОЛЖЕН.** css и js адресуются с `?v=<короткий sha256 содержимого>`, и URL
|
**ДОЛЖЕН.** css и js адресуются с `?v=<короткий sha256 содержимого>`, и URL
|
||||||
строит хелпер шаблона.
|
строит хелпер шаблона.
|
||||||
|
|
||||||
**Почему.** Хеш содержимого — единственная версия, которую невозможно
|
**ПОЧЕМУ.** Хеш содержимого — единственная версия, которую невозможно
|
||||||
забыть обновить: она меняется от самой правки. Ручной номер и дата сборки
|
забыть обновить: она меняется от самой правки. Ручной номер и дата сборки
|
||||||
от этого не защищают, а цена промаха при иммутабельном кэше (HTMX-29) —
|
от этого не защищают, а цена промаха при иммутабельном кэше (HTMX-29) —
|
||||||
устаревший файл у пользователя до ручной очистки кэша. Хелпер нужен, чтобы
|
устаревший файл у пользователя до ручной очистки кэша. Хелпер нужен, чтобы
|
||||||
@@ -468,7 +470,7 @@ htmx-атрибутов при этом само работает маркеро
|
|||||||
**ДОПУСКАЕТСЯ.** Вендор адресуется по неизменному имени файла, без
|
**ДОПУСКАЕТСЯ.** Вендор адресуется по неизменному имени файла, без
|
||||||
параметра версии.
|
параметра версии.
|
||||||
|
|
||||||
**Почему.** Содержимое под этим именем не меняется: обновление вендора
|
**ПОЧЕМУ.** Содержимое под этим именем не меняется: обновление вендора
|
||||||
приходит новым именем файла, то есть новым URL. Кэш-бастер защищает от
|
приходит новым именем файла, то есть новым URL. Кэш-бастер защищает от
|
||||||
подмены содержимого под тем же адресом, а такой ситуации здесь нет — и
|
подмены содержимого под тем же адресом, а такой ситуации здесь нет — и
|
||||||
явное разрешение снимает вопрос, не нарушает ли это HTMX-30.
|
явное разрешение снимает вопрос, не нарушает ли это HTMX-30.
|
||||||
@@ -479,7 +481,7 @@ htmx-атрибутов при этом само работает маркеро
|
|||||||
(`путь url sha256`) с проверкой контрольной суммы; сборка зависит от этой
|
(`путь url sha256`) с проверкой контрольной суммы; сборка зависит от этой
|
||||||
задачи.
|
задачи.
|
||||||
|
|
||||||
**Почему.** Манифест делает версию и происхождение ассета видимыми в
|
**ПОЧЕМУ.** Манифест делает версию и происхождение ассета видимыми в
|
||||||
diff'е — у закоммиченного минифицированного файла обновление выглядит
|
diff'е — у закоммиченного минифицированного файла обновление выглядит
|
||||||
стеной непрозрачных изменений, и подмену в ней не разглядеть. Sha256 —
|
стеной непрозрачных изменений, и подмену в ней не разглядеть. Sha256 —
|
||||||
единственная проверка, что скачали то же самое, что проверяли; зависимость
|
единственная проверка, что скачали то же самое, что проверяли; зависимость
|
||||||
@@ -489,13 +491,7 @@ diff'е — у закоммиченного минифицированного
|
|||||||
|
|
||||||
**ДОЛЖЕН.** Внешних хостов во время выполнения нет.
|
**ДОЛЖЕН.** Внешних хостов во время выполнения нет.
|
||||||
|
|
||||||
**Почему.** Каждый внешний хост — это чужой аптайм внутри своей страницы и
|
**ПОЧЕМУ.** Каждый внешний хост — это чужой аптайм внутри своей страницы и
|
||||||
третья сторона, видящая каждый запрос пользователя. Самодостаточный бинарь
|
третья сторона, видящая каждый запрос пользователя. Самодостаточный бинарь
|
||||||
вдобавок разворачивается в сети без выхода наружу, где CDN просто не
|
вдобавок разворачивается в сети без выхода наружу, где CDN просто не
|
||||||
отвечает.
|
отвечает.
|
||||||
|
|
||||||
<!-- local:эталоны -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|
||||||
<!-- local:отступления -->
|
|
||||||
<!-- /local -->
|
|
||||||
|
|||||||
@@ -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]
|
|
||||||
# Пусто. Сюда попадают префиксы удалённых и разделённых файлов вместе с
|
|
||||||
# причиной и датой, чтобы их нельзя было выдать повторно.
|
|
||||||
Reference in New Issue
Block a user