@rt-tools/agent-kit 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +34 -6
  2. package/assets/checks/check-file-size.mjs +127 -0
  3. package/assets/checks/rt-kit-checks.config.mjs +10 -0
  4. package/assets/defaults/gate-map.sh +90 -34
  5. package/assets/defaults/project.sh +26 -3
  6. package/assets/hooks/skill-gate-layers.sh +156 -0
  7. package/assets/hooks/skill-gate.sh +11 -2
  8. package/assets/laws/code-structure.md +3 -0
  9. package/assets/laws/delivery.md +9 -0
  10. package/assets/laws/observability.md +46 -0
  11. package/assets/laws/project-documentation.md +4 -0
  12. package/assets/laws/reuse-first.md +2 -0
  13. package/assets/laws/verifiability.md +5 -0
  14. package/assets/patterns/browser-verification-stand.md +22 -2
  15. package/assets/patterns/doc-style-trace.md +111 -0
  16. package/assets/patterns/git-workflow-commit.github.md +1 -1
  17. package/assets/patterns/git-workflow-docker.md +203 -0
  18. package/assets/patterns/git-workflow-secrets.md +93 -0
  19. package/assets/patterns/observability-record.md +114 -0
  20. package/assets/patterns/ownership-session-procedure.md +102 -0
  21. package/assets/patterns/seo-verify.md +1 -1
  22. package/assets/patterns/spec-driven-rule.md +5 -0
  23. package/assets/patterns/styling-bem-sheet.md +178 -0
  24. package/assets/patterns/task-flow-close.md +20 -0
  25. package/assets/patterns/task-flow-resume.md +5 -0
  26. package/assets/patterns/translations-content.md +107 -0
  27. package/assets/patterns/translations-key.md +1 -1
  28. package/assets/rules/angular-patterns.md +5 -0
  29. package/assets/rules/browser-verification.md +17 -12
  30. package/assets/rules/component-structure.md +6 -2
  31. package/assets/rules/doc-style.md +16 -0
  32. package/assets/rules/git-workflow.azure.md +45 -1
  33. package/assets/rules/git-workflow.github.md +52 -1
  34. package/assets/rules/git-workflow.gitlab.md +46 -1
  35. package/assets/rules/lists.md +13 -0
  36. package/assets/rules/observability.md +147 -0
  37. package/assets/rules/ownership-scope.md +5 -2
  38. package/assets/rules/ownership-session.md +124 -0
  39. package/assets/rules/permissions.md +23 -0
  40. package/assets/rules/pricing.md +4 -0
  41. package/assets/rules/reuse-first.md +9 -0
  42. package/assets/rules/seo.md +57 -9
  43. package/assets/rules/shared-code.md +6 -0
  44. package/assets/rules/spec-driven.md +9 -0
  45. package/assets/rules/styling-bem.md +34 -1
  46. package/assets/rules/task-flow.md +5 -0
  47. package/assets/rules/testing.md +46 -8
  48. package/assets/rules/translations.md +11 -5
  49. package/assets/rules/typescript-conventions.md +5 -0
  50. package/package.json +1 -1
  51. package/rt-tools-agent-kit-0.6.0.tgz +0 -0
  52. package/rt-tools-agent-kit-0.5.2.tgz +0 -0
@@ -0,0 +1,147 @@
1
+ ---
2
+ name: observability
3
+ kind: rule
4
+ law: observability
5
+ description: Правило под «Закон о наблюдаемости». Брать при правке логгера, контекста запроса, домена отказов и домена оповещений, при заведении новой строки лога и когда решается, что владелец узнает об отказе. Называет уровни, номер обращения, вычистку секретов, сводку старта и то, что уходит наружу при отказе. Готовый код — в паттерне observability-record.
6
+ ---
7
+
8
+ # Наблюдаемость — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/observability.md`. Закон говорит, что владелец должен
11
+ знать о работе приложения; здесь — как это названо в этом дереве, где лежит и чего пока нет.
12
+
13
+ ## Как это называется здесь
14
+
15
+ | В законе | Здесь |
16
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
17
+ | то, что приложение о себе пишет | логи приложения: одна строка на каждый вызов логгера, в машинном виде |
18
+ | ступень важности | уровень: от подробностей разбора до отказа, поднявшего процесс |
19
+ | номер обращения | идентификатор запроса, он же заголовок ответа |
20
+ | признак, по которому находятся все отказы одного обращения | тот же номер обращения — он стоит в каждой строке лога |
21
+ | подробности отказа | разобранная причина: класс, текст, код протокола, код и подробности от хранилища, срезанный стек |
22
+ | общий текст ошибки наружу | внутренняя ошибка без подробностей |
23
+ | вычистка секретов | замена значений по имени поля |
24
+ | отказ в хранилище | то, что домен отказов сохранил: группа отказов и её вхождения |
25
+ | то, с чем приложение поднялось | сводка старта: одна строка лога с версией, портом, адресом хранилища и списками возможностей — включённых, выключенных и сломанных |
26
+
27
+ ## Где это лежит
28
+
29
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
30
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
31
+ же дереве, которое держит код иначе.
32
+
33
+ ## Как закон применяется здесь
34
+
35
+ - **Номер обращения заводится один раз на запрос и стоит в каждой строке лога о нём.** По
36
+ времени строки одного обращения не отобрать: обращений бывает десяток в секунду.
37
+ - **Номер обращения уходит вызывающему заголовком ответа и в подробностях отказа.** Он же стоит
38
+ в логах, поэтому названный человеком номер ищется прямым отбором. Заголовок при этом открыт
39
+ странице чужого домена: без явного разрешения браузер его не отдаёт.
40
+ - **Номер, пришедший снаружи, чистится и укорачивается, а пустой заводится заново.** Значение
41
+ из заголовка приходит от кого угодно, а попадает в отбор по хранилищу.
42
+ - **Наружу уходит код и общий текст ошибки, подробности остаются в логах.** Иначе гость на
43
+ сломанной выкатке прочитает имя колонки и устройство хранилища.
44
+ - **Отказ по вводу и правам пишется отдельно от поломки и без стека.** Это сработавшая
45
+ проверка. На одном уровне с поломками она забивает тревогу опечатками гостей.
46
+ - **Причина отказа разбирается в одном месте.** Иначе одна и та же ошибка приходит в логи
47
+ тремя разными формами.
48
+ - **Поля строки лога вычищаются всегда, а не по решению того, кто пишет.** Решать на каждом
49
+ вызове, есть ли в полях секрет, — значит однажды ошибиться.
50
+ - **Приложение при старте пишет, с чем поднялось.** Половина возможностей включается наличием
51
+ переменной окружения и без неё молча выключена. Иначе на вопрос «почему там не работает то,
52
+ что работает локально» отвечают чтением настроек прода по ssh.
53
+ - **В сводке старта стоят имена возможностей и их состояние, но не значения переменных.**
54
+ Состояния разложены тремя списками, а не картой «имя → состояние»: вычистка работает по
55
+ имени ключа, и карта приехала бы в лог вычищенной ровно там, где состояние и нужно.
56
+ - **Возможность, включаемая парой ключей, знает третье состояние.** Половины пары лежат по
57
+ разные стороны поставки, и заполнить можно ровно одну; тогда возможность не выключена и не
58
+ включена, а сломана — списком из двух состояний этот случай назвать нечем, и он читается как
59
+ включённый.
60
+ - **Порог уровня решает, появится ли строка лога в выводе, но не в хранилище отказов.** Порог
61
+ ставится ради объёма вывода, и при высоком пороге отобранные строки нижнего уровня пропали бы
62
+ из хранилища молча.
63
+ - **Отобранная строка лога сохраняется в хранилище отказом.** Отбирается уровень отказа и всё,
64
+ что выше, и строки уровнем ниже — по списку имён, объявленному в коде.
65
+ - **Логгер отдаёт отказ приёмнику, а хранилища не видит.** Интерфейс приёмника объявлен рядом с
66
+ логгером, исполняет его домен отказов, и ставится он извне при старте.
67
+ - **Строки лога клиента хранилища, домена отказов и домена оповещений в приёмник не идут.**
68
+ Отсекается источник, а не момент записи: медленная вставка в таблицу отказов сама порождает
69
+ строку лога клиента хранилища, и признаком «я внутри записи» её не поймать. Домен оповещений
70
+ отсечён по той же причине: сбой оповещения стал бы новым отказом, тот — новым оповещением.
71
+ - **Обращение к чужой службе идёт с явным пределом ожидания.** Без него молчащая — не
72
+ отказавшая — служба держит соединение до умолчания среды, а это минуты: отказа нет,
73
+ записывать нечего, и владельцу такое молчание неотличимо от исправной работы. Предел
74
+ называется числом рядом с клиентом, потому что цена ожидания у каждой службы своя.
75
+ - **Отправка наружу заводит свою строку до обращения, а исход дописывается в неё после.**
76
+ Строку заводит тот, кто видит хранилище, а не тот, кто отправляет: клиент внешней службы
77
+ хранилища не видит вовсе. Она же служит замком — второе обращение по той же записи не
78
+ уходит, — и по незакрытой строке видно разницу между «идёт прямо сейчас» и «упало посреди
79
+ отправки».
80
+ - **Запись отказа не задерживает ответ и не роняет запрос.** Между логгером и хранилищем стоит
81
+ очередь с пределом; переполненная очередь отбрасывает новый отказ и считает отброшенное.
82
+ - **Контекст запроса снимается в момент вызова логгера, а не в момент записи.** К моменту
83
+ записи хранилище исполнения уже отдано следующему запросу.
84
+ - **Записанное владелец читает своим разделом, закрытым отдельным правом.** Группы своего
85
+ владения, а под группой — её вхождения с причиной, полями строки лога и телами.
86
+ - **Хранилище не растёт без предела: раз в сутки лишнее и старое удаляются.** Удаляются
87
+ вхождения: сперва лишние за пределом, потом старые за сроком. Группа, у которой после этого
88
+ не осталось ни одного вхождения, уходит третьим шагом — и только если она старше часа: между
89
+ её заведением и первым вхождением проходит отдельный запрос.
90
+ - **Предел хранилища назван числом строк, а не байтами.** Удаление физический размер таблицы не
91
+ уменьшает, и условие «размер под пределом» не стало бы верным никогда.
92
+ - **О новом отказе владелец узнаёт сам — строкой журнала событий и письмом.** Обе дороги
93
+ открывает один порог: новая группа или группа, молчавшая дольше суток.
94
+ - **В письме нет ничего, что закрыто правом на экран отказов.** Оно уходит на адрес, который
95
+ правами не закрыт ничем, и подробности в нём обошли бы право почтой.
96
+ - **Частоту отказов владелец читает кривой над лентой: столбик — ведёрко выбранного периода.**
97
+ Счётчик группы говорит, сколько раз она случилась, но не когда: по нему не отличить поломку,
98
+ которая идёт потоком сейчас, от набравшейся за месяц.
99
+ - **Тревогу поднимает рост поломок, а не всех отказов.** Отклонённый вызов — сработавшая
100
+ проверка, и таких девять из десяти: на их фоне рост поломок не заметен вовсе.
101
+ - **Всплеск считается по последнему закрытому ведёрку, текущее правилу не отдаётся.** Оно ещё
102
+ набирается, и в начале каждого периода сравнение с ним показывало бы падение частоты.
103
+ - **Частоту разбирает такт расписания, а не запрос экрана.** Кривая считает тревогу при каждом
104
+ ответе, но экран владелец может и не открыть, а узнать о всплеске должен без этого.
105
+ - **О всплеске владелец узнаёт строкой журнала и письмом; повтор письма держит
106
+ предохранитель.** Одно происшествие занимает в ленте одну строку, а письмо о затяжной
107
+ поломке нужно и назавтра.
108
+
109
+ ## Чего из закона здесь нет
110
+
111
+ Отказ, случившийся до подъёма приложения, записать некуда: хранилища в этот момент ещё нет.
112
+ Такие остаются только в выводе контейнера. Туда же уходит отказ самого хранилища: его строки
113
+ лога отобраны из отказов целиком, иначе поломка хранилища порождает поток, который сам себя
114
+ разгоняет.
115
+
116
+ Список отобранных имён нижнего уровня не сверяется ни с чем. Новое место, которому надо в
117
+ хранилище, дописывает себя в него руками, а забытое молча остаётся только в выводе.
118
+
119
+ Правильность выбора уровня не проверяет ничто. Новое место само решает, какой уровень взять, и
120
+ ошибку видно только при чтении кода.
121
+
122
+ Полнота сводки старта не сверяется ничем. Имена возможностей перечислены руками, а переменную
123
+ окружения читают десятки файлов по всем доменам: новая возможность, забывшая дописать себя в
124
+ сводку, молча не попадёт ни в один из трёх списков — и на проде будет выглядеть не выключенной,
125
+ а несуществующей. Само чтение переменной окружения поэтому и требует этого правила вторым
126
+ слоем: гард видит обращение к окружению в тексте правки, но не то, дописали себя в сводку или
127
+ нет.
128
+
129
+ ## Паттерны
130
+
131
+ - `observability-record` — как завести новую строку лога.
132
+
133
+ ## Ловушки
134
+
135
+ - **Контекст запроса живёт в хранилище исполнения и в отложенную работу не переезжает.**
136
+ Отложенная запись прочитает контекст того обращения, которое заняло место исходного. Снимать
137
+ контекст надо в момент вызова логгера.
138
+ - **Логгер ставится на всё приложение, поэтому строки каркаса идут тем же путём, что и свои.**
139
+ Всё, что каркас пишет о себе, попадает в тот же вывод и в тот же отбор.
140
+ - **Отказ потока приходит после того, как ответ начался.** Управление возвращается, когда отдан
141
+ только заголовок. Без обёртки такой отказ не попадёт в логи вовсе.
142
+ - **Сериализация строки лога не должна бросать исключение.** Циклическая ссылка в полях иначе
143
+ уронит запрос, ради лога которого её и складывали.
144
+ - **Ключ, в который заворачивают чужое тело для вычистки, выбирается по правилам вычистки.**
145
+ Она работает по имени ключа, и голое значение мимо неё проходит вовсе, — но ключ, который сам
146
+ числится свободным текстом, увозит тело в хранилище отметкой о вычистке целиком. Новая
147
+ обёртка сверяется со списками ключей до того, как её так назвали.
@@ -59,5 +59,8 @@ description: Правило под «Закон о владеющей сущно
59
59
  вызывающего:** первое отвечает `FailedPrecondition`, второе — `NotFound`.
60
60
  - Таблица владеющих сущностей читается целиком одним запросом — она маленькая, и выборки ей не
61
61
  нужно.
62
- - Выбор сущности стоит в шапке и действует на разделы, которые говорят об одной. Снаружи выбора
63
- нет: пользователь приходит на страницу конкретной.
62
+ - **Выбор сущности живёт в общем сторе, а рисует переключатель тот экран, которому он нужен.**
63
+ Выбор переживает переход между разделами, но экран без своего переключателя показывает то,
64
+ что выбрали на другом, — и это читается как чужие числа в своём разделе. Где переключатель
65
+ стоит, названо в именах дерева; снаружи выбора нет вовсе: пользователь приходит на страницу
66
+ конкретной сущности.
@@ -0,0 +1,124 @@
1
+ ---
2
+ name: ownership-session
3
+ kind: rule
4
+ law: ownership
5
+ description: Правило под «Закон о владеющей сущности». Брать при правке процедуры, которой нужна сущность захода, перехватчика доступа и выборок принадлежности — чем названа сущность захода, откуда она берётся, каким условием отбираются записи и какими кодами отвечают отказы. Готовый код — в паттерне ownership-session-procedure. Не брать для разрешения идентификатора в запросе — это правило ownership-scope.
6
+ ---
7
+
8
+ # Владеющая сущность захода — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/application/ownership.md`. Закон говорит, что должно быть
11
+ верно, когда владеющих сущностей больше одной; здесь — как приложение узнаёт, в какой из них
12
+ работает вошедший, и чем держится граница между ними. Разрешение идентификатора, названного в
13
+ запросе, — правило `ownership-scope` под тем же законом.
14
+
15
+ ## Как это называется здесь
16
+
17
+ | В законе | Здесь |
18
+ | ---------------------------------------------- | -------------------------------------------------------------------------------- |
19
+ | владеющая сущность | запись своего домена; имя — в `implementation.md` рядом |
20
+ | принадлежность человека сущности | своя запись на пару «человек и сущность» |
21
+ | сущность захода | колонка учётной записи; внутри запроса — ключ контекста, читаемый одной функцией |
22
+ | потребность процедуры в сущности захода | декоратор на её классе, стоящий рядом с объявлением доступа |
23
+ | отказ вошедшему, у которого принадлежности нет | тот же код, что на неверный пароль, — вошедшего надо увести на вход |
24
+ | отказ на чужую сущность, названную в черновике | неверный аргумент |
25
+ | забытый декоратор | внутренняя ошибка — отказ приложения, а не вызывающему |
26
+
27
+ ## Где это лежит
28
+
29
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
30
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
31
+ же дереве, которое держит код иначе.
32
+
33
+ ## Как закон применяется здесь
34
+
35
+ - **Сущность захода приезжает в процедуру контекстом запроса, а не полем запроса.** Ключ и его
36
+ читатель лежат рядом, как ключ вошедшего и его читатель: вторая копия проверки разошлась бы с
37
+ первой в коде отказа.
38
+ - **Процедура объявляет потребность декоратором, и чтение без объявления — отказ приложения.**
39
+ Править такое нечего ни вызывающему, ни владельцу: это забытое объявление, и оно видно
40
+ отказом, а не работой неизвестно в какой сущности.
41
+ - **Сущность захода приезжает в контекст только процедурам, объявившим потребность.** Читает её
42
+ перехватчик всё равно — тем же обращением, которым читает права вошедшего, — но без
43
+ объявления в контекст она не попадает. Потребность стоит рядом с объявлением доступа и счёт
44
+ объявлений доступа не меняет.
45
+ - **Сущность захода лежит у учётной записи, а не в выданном входе.** Выбор переживает
46
+ перезагрузку и не зависит от того, что шлёт клиент; выданный вход говорит только о том, кто
47
+ пришёл.
48
+ - **Принадлежность человека сущности — своя запись, и в одной сущности она одна.** Две
49
+ принадлежности пришлось бы складывать, а результат сложения запретов и разрешений по двум
50
+ строкам не прочитать.
51
+ - **Человек, у которого принадлежности нет, не входит и не работает: оба отказа те же, что на
52
+ неверный пароль.** Принадлежность снимают, пока выданный вход ещё жив, — и тогда отказ
53
+ приходит уже посреди работы. Транспорт админки на этот код чистит хранилище и уводит на
54
+ страницу входа.
55
+ - **Чужая сущность, названная в черновике настроек, отбивается, а не выправляется молча.** Иначе
56
+ одним ответом скрывались бы и подделанный запрос, и админка, которая загрузила одну сущность,
57
+ а сохраняет в другую.
58
+ - **Выборка админки отбирает записи по сущности захода, и снять это условие нечем.** Оно
59
+ складывается с условиями отбора клиента, а не заменяется ими: иначе список, поиск и число в
60
+ сводке показывают чужое.
61
+ - **Запись, названную в запросе своим номером, выборка берёт с условием по сущности, и правка
62
+ со снятием идут тем же условием.** Сущность уезжает параметром рядом с идентификатором в то
63
+ же условие; у записи без своей колонки граница выводится связью. Чужая запись отвечает так же,
64
+ как несуществующая, — и связанный идентификатор сверяется наравне с основным.
65
+ - **Выборку по номеру без сущности в условии ловит проверка охвата.** Список точек умер бы со
66
+ слиянием: следующая процедура завелась бы без условия, и не заметил бы этого никто. Путь, где
67
+ сущности законно нет, назван в списке известного строкой с причиной.
68
+ - **Адрес публичной страницы уникален внутри сущности, а не по всей системе.** Пара «сущность и
69
+ адрес» стоит уникальным индексом и у записей, и у их прежних адресов; сквозную уникальность
70
+ между двумя таблицами держит хранилище — оно читает ту же пару. У прежнего адреса сущность
71
+ продублирована колонкой: связью пара в уникальный индекс не собирается.
72
+ - **Занятость адреса спрашивается внутри сущности, и чужая занятость владельцу не видна.** Отказ
73
+ «адрес занят» на имя, которое держит сосед, выдал бы существование его записи.
74
+ - **Публичный вход, нашедший под адресом больше одной записи, отвечает как на
75
+ несуществующую.** Гость приходит по адресу и сущности не называет, поэтому признак совпадения
76
+ едет отдельным полем результата выборки: пустой ответ забыли бы разобрать, а показ первого
77
+ попавшегося отдал бы гостю чужую запись молча. Владелец узнаёт о таком случае строкой лога,
78
+ заведённой в списке отбираемых.
79
+ - **Принадлежность записи сущности записана колонкой везде, где её не из чего вывести.** Там,
80
+ где у записи обязательна связь с другой записью сущности, граница выводится связью; у визита,
81
+ события и разговора выводить её не из чего — заход бывает записан на несуществующий путь, а
82
+ разговор начат с корня сайта.
83
+
84
+ ## Чего из закона здесь нет
85
+
86
+ Внутри сущности границ нет: право открывает раздел целиком, и сотрудник, которому доверена одна
87
+ запись, видит все записи своей сущности.
88
+
89
+ Признака, по которому гость назвал бы сущность, может не быть вовсе — тогда там, где гость не
90
+ назвал ничего, читатель берёт единственную сущность хранилища и отвечает отказом по состоянию,
91
+ когда их больше одной. Тем же вызвана и временная граница у адреса страницы: совпавший адрес
92
+ недоступен обеим сущностям, потому что выбрать между ними нечем.
93
+
94
+ Роль живёт у учётной записи, а не у принадлежности, пока принадлежность у человека одна:
95
+ колонки прав у принадлежности стоят и не читаются, а права читаются у учётной записи при каждом
96
+ вызове.
97
+
98
+ Выбора сущности сотрудником может не быть: сущностью захода остаётся то, что записано у учётной
99
+ записи, и сменить это значение нечем, пока нет ни селектора, ни процедуры. Тогда выбором
100
+ остаётся единственная принадлежность.
101
+
102
+ ## Паттерны
103
+
104
+ - `ownership-session-procedure` — процедура, которой нужна сущность захода: объявление, чтение,
105
+ отказы, контекст в тесте.
106
+
107
+ ## Ловушки
108
+
109
+ - **Сущность захода и принадлежность — разные вещи.** Колонка учётной записи говорит, в какой
110
+ сущности человек работает сейчас; принадлежность — в каких он вправе работать вообще.
111
+ Значение колонки без принадлежности к той же сущности ничего не значит: выборка сверяет их
112
+ между собой и отдаёт пусто, когда они разошлись.
113
+ - **Забытый декоратор не ловится ни сборкой, ни линтером.** Процедура, читающая сущность захода
114
+ без объявления потребности, собирается и проходит проверки — отказ приходит на первом же
115
+ вызове. Пишутся эти две строки вместе, а покрывается это тестом процедуры.
116
+ - **Потребность в сущности доступом не является.** Объявлений доступа у процедуры по-прежнему
117
+ ровно одно, и декоратор потребности к этому счёту не относится: сущность нужна и процедуре с
118
+ правом, и процедуре, открытой любому вошедшему.
119
+ - **Отказ снятой принадлежности — тот же, что на неверный пароль, а не отказ в доступе.**
120
+ Второй оставил бы человека на закрытом разделе с непонятной причиной и без выхода, хотя
121
+ лечится это только новым входом.
122
+ - **Снятое поле контракта не заводится заново под тем же номером.** Номер и имя помечены как
123
+ занятые, и метка снимается только вместе с решением отдать номер новому полю, — а такое
124
+ решение ломает уже выкаченного клиента молча: он прочитает чужое значение как своё.
@@ -36,6 +36,29 @@ description: Правило под «Закон о доступе». Брать
36
36
  Последний отдаёт вошедшему больше, чем гостю, — так владелец видит скрытые объекты в общем
37
37
  списке.
38
38
  - **Права пользователя — это права пресета, поверх которых применены его оверрайды.**
39
+ - **У человека одна роль во владении, и держит это хранилище.** Две принадлежности в одном
40
+ владении пришлось бы складывать, а результат сложения запретов и разрешений по двум строкам
41
+ не прочитать. Права разных владений не складываются вовсе: они записаны у принадлежности, а
42
+ не у учётной записи.
43
+ - **Право, о котором роль ничего не говорит, считается неданным.** Значение, которое не булево,
44
+ отбрасывается при сложении: отсутствующее право и прямо отобранное означают одно и то же, и
45
+ «непусто» правом не считается.
46
+ - **Права вошедшего читаются при каждом вызове, а не берутся из выданного входа.** Выданный
47
+ вход говорит только о том, кто пришёл: подписанное однажды живёт часами и правку прав не
48
+ переживает, поэтому отобранное право открывало бы раздел до конца дня.
49
+ - **Учётная запись, которой больше нет, вызовов, требующих входа, не открывает.** Тем же
50
+ чтением отбивается и человек, потерявший принадлежность в своём владении: работать ему не в
51
+ чем.
52
+ - **Счётчик и рекламные сигналы включает ответ гостя, а не наличие ключа в настройках.**
53
+ Решений два, и хранятся они парой: «разрешил счёт посещений, но не рекламу» — законное
54
+ состояние, а третьим значением перечисления его пришлось бы заводить заново на каждое новое
55
+ разрешение. Всё, что не пара булевых значений, читается как неотвеченный вопрос, то есть как
56
+ отказ: хранилище принимает что угодно, а решать по испорченной записи нельзя. Своя
57
+ статистика к согласию не привязана — она не уходит наружу.
58
+ - **Публичная процедура, заводящая запись, закрыта ещё и ограничителем частоты.** Право её не
59
+ сторожит, и без предела скорость роста таблицы задаёт отправитель, а не владелец. Считается
60
+ по ключу клиента, и ключ у всех таких процедур общий: второй ответ на вопрос «кто это»
61
+ разошёлся бы с первым. Публичная процедура, которая только читает, ограничителя не требует.
39
62
  - **Запрос без входа отбивается как неаутентифицированный, а вход без права — как отказ в
40
63
  доступе.** Это разные ответы: первый лечится входом, второй — нет.
41
64
  - **Право проверяется перехватчиком до тела процедуры.** Обработчик не решает, пускать ли
@@ -62,3 +62,7 @@ description: Правило под «Закон о деньгах». Брать
62
62
  одно, и живёт оно в расчёте.
63
63
  - **Справочная сумма рядом с суммой к оплате читается как цена.** В письме и в документе
64
64
  справочное число подписывается как справка.
65
+ - **Набор валют переключателя бывает шире набора курсов.** Набор переключателя собирается из
66
+ начальных валют локалей, и валюты, курса для которых никто не тянет, попадают в него сами:
67
+ гость такой локали получает отказ курса, ничего не выбирая. Два набора сверяются между собой,
68
+ а расхождение чинится задачей — правкой текста оно не лечится.
@@ -27,6 +27,10 @@ description: Правило под «Закон о единообразии пр
27
27
 
28
28
  ## Как закон применяется здесь
29
29
 
30
+ - **Источник вида выбирается по приложению, а не по привычке.** У каждого приложения дерева
31
+ своя опора: у публичного сайта — его дизайн-система, у остальных — кит. Перепутанный источник
32
+ приносит на экран форму, которой в этом приложении больше нигде нет. Какое приложение на что
33
+ опирается, названо в именах дерева.
30
34
  - **Работа начинается с чтения готового, а не с чистого файла.** Сначала находится опора —
31
35
  компонент кита, базовый класс, образец в соседнем домене, — потом пишется своё поверх неё.
32
36
  - **Свой примитив и своя основа заводятся только с явного одобрения владельца.** Спрашивается
@@ -76,6 +80,11 @@ description: Правило под «Закон о единообразии пр
76
80
 
77
81
  ## Ловушки
78
82
 
83
+ - **Образец ищется по именам кита, а не по тому, на чём кит написан.** Поиск по именам
84
+ библиотеки, поверх которой кит собран, не находит ни одного файла: наложение, портал и окно
85
+ закрыты китом и зовутся его именами. Обратное тоже бывает: библиотека стоит прямой
86
+ зависимостью, и то, чего кит не закрывает, зовётся в дереве её собственным именем. Прежде чем
87
+ решать, что имени в дереве нет, его ищут — переделок из-за этого выходит по две на приём.
79
88
  - Перенос переизобретением не считается: строку, которая уже лежит в файле, гард из
80
89
  проверяемого текста вычёркивает, а сверка идёт без отступов — при переезде блок меняет
81
90
  отступ, оставаясь тем же кодом.
@@ -2,7 +2,7 @@
2
2
  name: seo
3
3
  kind: rule
4
4
  law: search-visibility
5
- description: Правило под «Закон о видимости в поиске». Брать при любой правке, доходящей до разметки публичного сайта — шаблоны страниц, meta/title/description, JSON-LD, canonical, hreflang, маршруты сайта, sitemap, robots, nginx. Называет восемь локалей, где что лежит и чего у нас нет. Готовый код — в паттернах seo-page и seo-verify.
5
+ description: Правило под «Закон о видимости в поиске». Брать при любой правке, доходящей до разметки публичного сайта — шаблоны страниц, meta/title/description, JSON-LD, canonical, hreflang, маршруты сайта, sitemap, robots, nginx. Называет локали перевода, где что лежит и чего у нас нет. Готовый код — в паттернах seo-page и seo-verify.
6
6
  ---
7
7
 
8
8
  # Видимость в поиске — как это устроено здесь
@@ -12,14 +12,14 @@ description: Правило под «Закон о видимости в пои
12
12
 
13
13
  ## Как это называется здесь
14
14
 
15
- | В законе | Здесь |
16
- | ---------------------------- | ---------------------------------------------------------------------------- |
17
- | язык страницы | локаль; их восемь`en`, `ru`, `de`, `zh-Hans`, `zh-Hant`, `ko`, `th`, `hi` |
18
- | язык по умолчанию | `DEFAULT_LOCALE`, то есть `en`; отдаётся из корня, без префикса в адресе |
19
- | язык, перевод которого готов | `readyLocales` — поле объекта, а не список локалей сайта |
20
- | канонический адрес | `${SITE_ORIGIN}${propertyPath(slug, locale)}` |
21
- | структурированные данные | JSON-LD типа `LodgingBusiness` |
22
- | прежний адрес страницы | прежний slug объекта |
15
+ | В законе | Здесь |
16
+ | ---------------------------- | ------------------------------------------------------------------------ |
17
+ | язык страницы | локаль перевода; их наборв `implementation.md` рядом |
18
+ | язык по умолчанию | `DEFAULT_LOCALE`, то есть `en`; отдаётся из корня, без префикса в адресе |
19
+ | язык, перевод которого готов | `readyLocales` — поле объекта, а не список локалей сайта |
20
+ | канонический адрес | `${SITE_ORIGIN}${propertyPath(slug, locale)}` |
21
+ | структурированные данные | JSON-LD типа `LodgingBusiness` |
22
+ | прежний адрес страницы | прежний slug объекта |
23
23
 
24
24
  Локаль по умолчанию отдаётся из корня, остальные — с префиксом `/<код>/`. Префикс — часть
25
25
  маршрута, а не часть сборки: `<base href>` в разметке всегда `/`.
@@ -46,6 +46,54 @@ description: Правило под «Закон о видимости в пои
46
46
  - **Перенаправление с прежнего адреса отдаётся с `Cache-Control: max-age`** и покрыто
47
47
  `proxy_cache_valid 200 301`. Ключ кэша строится без `$args`, поэтому запросы со строкой
48
48
  запроса идут мимо кэша (`proxy_cache_bypass` / `proxy_no_cache $is_args`).
49
+ - **Страница отдаёт блоки JSON-LD каждый своим тегом**, а не одним графом: отказ одного не
50
+ уносит остальные, и в отданной разметке видно, какой из них собрался.
51
+ - **Цена уходит и предложением, и диапазоном** — суммой в валюте хранения и с единицей своего
52
+ периода. Цена по датам, скидки и пересчёт в валюту гостя в разметку не идут.
53
+ - **Сводная оценка появляется от первого отзыва** и считается из оценок самих отзывов. Ноль
54
+ отзывов — поля нет вовсе: ни нуля, ни пустой строки.
55
+ - **Блок вопросов собирается из вопросов записи**, а ноль вопросов блока не даёт.
56
+ - **Крошки ведут от корня локали к странице записи** адресами той же локали, что и открытая
57
+ страница. Без названия сайта крошки не выпускаются вовсе.
58
+ - **Сохранение записи уведомляет поисковика об изменении её адресов.** В уведомление уходят
59
+ страницы записи на готовых локалях по нынешнему и прежним адресам; корни локалей и карта
60
+ сайта — нет. Отказ приёмника сохранение не отменяет и возвращается владельцу исходом.
61
+ - **Приёмников несколько, и каждый получает свой запрос.** Опрашиваются они одновременно, а
62
+ потолок ожидания считается на приёмник: последовательный обход сложил бы все потолки в одно
63
+ ожидание владельца. Переменная окружения перекрывает адреса всех приёмников разом — этим
64
+ отправку и проверяют локальным приёмником.
65
+ - **Исход возвращается перечислением по приёмнику, а владельцу показывается списком.** Приёмник
66
+ в нём назван словом, а не адресом. Строка отвечает за приём запроса, а не за осведомлённость
67
+ поисковика: раздаёт полученное любой приёмник, поэтому отказ одного адреса не значит, что его
68
+ поисковик не узнал. Подписью объявляется только полный отказ.
69
+ - **Удаление записи уведомляет поисковика теми же адресами.** Отдельного «страница исчезла»
70
+ протокол не знает: площадка приходит по адресу и видит ответ сайта сама. Адреса собираются до
71
+ удаления строки — каскад унесёт прежние вместе с записью, — а исход наружу не выходит:
72
+ страницы уже нет, и читать подпись некому. Снятие с публикации идёт через сохранение и
73
+ уведомляет тем же путём.
74
+ - **Переименование адреса и снятие прежнего адреса тоже уведомляют поисковика.**
75
+ Переименование — всеми адресами записи, снятие — одним снятым; готовые локали у снятого
76
+ считаются по текстам записи, которой он принадлежал, и ради них выборка снятия отдаёт не
77
+ только адрес. Исход наружу не выходит ни там, ни там: владелец на странице адресов
78
+ спрашивает про адрес, а не про уведомление.
79
+ - **Правка настроек владельца уведомляет поисковика адресами всех действующих записей.**
80
+ Видимой гостю считается та же правка, которой сбрасывается кэш, — а правка невидимого поля
81
+ уведомления не шлёт. Уходят живые адреса, без прежних, и готовые локали считаются по каждой
82
+ записи отдельно; отправка идёт на владеющую сущность — записи разных сущностей в одну не
83
+ попадают. Работа идёт в фоне за сохранением настроек, поэтому исход остаётся в логах:
84
+ показать его владельцу негде.
85
+ - **Каждая отправка оставляет запись, и заводится она до обращения к приёмникам.** Повод,
86
+ записи, объявленные адреса и исход по каждому приёмнику ложатся рядом; пустой исход означает
87
+ «отправка идёт», а спустя несколько минут — что процесс упал посреди опроса. Пишут её все
88
+ места, откуда уведомление уходит. Отказ записи отправку не отменяет.
89
+ - **Непрошедшие запросы владелец повторяет нажатием, и повтор заводит новую запись.**
90
+ Повторяется последняя отправка записи — у ранней адреса могли устареть, — и запрос уходит
91
+ только тем приёмникам, чей исход не «принял»; у оборванной записи это все её приёмники. Повод
92
+ у новой записи свой, а право строже, чем у чтения: повтор объявляет адреса поисковику.
93
+ Автоповтора по расписанию нет.
94
+ - **Записи отправок держит ночная чистка: сперва предел, потом срок.** Тот же порядок, что у
95
+ ленты происшествий: срок, снятый первым, оставил бы предел меряться по уже почищенной
96
+ таблице.
49
97
 
50
98
  ## Чего из закона здесь нет
51
99
 
@@ -39,6 +39,12 @@ description: Правило под «Закон об общем коде при
39
39
  - **Общим стал только маппер страницы.** У сторон разная политика на непонятное значение, и
40
40
  общим может быть лишь то, где она одна: номер меньше единицы обе стороны читают как первую
41
41
  страницу.
42
+ - **Общая выборка держит форму запроса, а не набор условий.** Имена полей отбора объявляет сам
43
+ домен списком разрешённых, а в запрос к хранилищу их переводит его же выборка. Поэтому
44
+ условие вправе лечь на связанные строки, а не только на колонки самой записи, и отбор по
45
+ набору идентификаторов ей не запрещён: признак связанной записи — такое же поле набора, как
46
+ повод и объект. Общими здесь остаются разбор страницы, порядка и поиска, а не сам список
47
+ полей.
42
48
  - **Перечисления полей порядка и отбора домена копией не считаются.** `EActivitySortProperty`
43
49
  и подобные повторяют имена, по которым сортирует сервер именно этого домена.
44
50
  - **Строковая настройка и таблица соответствий сверяются по значению, а не по имени.** Имя
@@ -109,6 +109,15 @@ description: Правило под «Закон о документации пр
109
109
  про дерево, где того гарда не разложили; поправить это дерево не может ничем, если у ресурса
110
110
  нет надстройки. Требование ресурса к ресурсу при этом объявляется строкой в шапке, а не
111
111
  выводится из такой фразы.
112
+ - **Паттерн находится по полю `rule:`, а не по приставке имени.** Приставку имени несут не все
113
+ паттерны, и поиск по имени правила таких не видит: сверка ищет их полем, человек — разделом
114
+ «Паттерны» самого правила. Счёт паттернов, собранный приставками, выходит меньше настоящего, а
115
+ число потом уезжает в деление работы.
116
+ - **Якорь сверяется по сырому тексту файла, и комментарий засчитывается наравне с кодом.**
117
+ Существование символа проверка ищет словом по всему файлу, не вычищая комментарии, а живость
118
+ считает только у объявленного в коде. Имя, стоящее в одном лишь пояснении, проходит мимо обеих
119
+ сторон: якорем утверждения оказывается слово из комментария, тогда как объявление рядом
120
+ называется иначе.
112
121
 
113
122
  ## Чего из закона здесь нет
114
123
 
@@ -16,7 +16,7 @@ description: Правило под «Закон о фронтовом прило
16
16
 
17
17
  | В законе | Здесь |
18
18
  | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
19
- | общий набор значений оформления | шкалы кита `--rt-*`; своё поверх них — `--vm-*` в `styles.scss` приложения |
19
+ | общий набор значений оформления | шкалы кита `--rt-*`; своё поверх них — `--<префикс>-*` в `styles.scss` приложения |
20
20
  | класс в разметке | директивы `rtBlock` и `rtElem` из `@rt-tools`, а не строка в атрибуте |
21
21
  | правило стилей | объявление `&__<элемент>` в `.scss` — своём или в общем слое приложения |
22
22
  | общий слой раскладки | `apps/<app>/src/styles/`: `<префикс>-page`, `<префикс>-form`, `<префикс>-panel`, `<префикс>-window` у админки, `<префикс>-site-page` у сайта |
@@ -38,6 +38,19 @@ description: Правило под «Закон о фронтовом прило
38
38
  инъекцией от ближайшего предка с `rtBlock`, и повторить этот разбор по тексту шаблона нечем.
39
39
  - **Раскладка объявлена в общем слое приложения, а не в стилях экрана.** У компонента экрана
40
40
  вне кита файл стилей по умолчанию пустой.
41
+ - **Шторку и окно открывает служба кита, а не номер слоя.** Числа шкалы сравниваются только
42
+ между соседями по разметке; служба выносит разметку наружу, к `<body>`, и сравнивать её
43
+ становится не с чем.
44
+ - **Номер слоя берётся из шкалы, а не пишется числом в файле компонента.** Шкала — единственное
45
+ место, где слои видно рядом: написанное на месте число в неё не попадает, и следующий узел
46
+ занимает тот же номер, ничего об этом не узнав.
47
+ - **Размер элемента управления выбирается по признаку указателя, а не по ширине экрана.**
48
+ Планшет в ландшафте шире порога узкого вьюпорта, а нажимают по нему пальцем: `pointer: coarse`
49
+ отвечает про способ нажатия, ширина — про место под раскладку. Ступени берутся у кита, а не
50
+ назначаются пикселями.
51
+ - **Файл стилей не длиннее 500 строк.** Предел общий с кодом и текстами, но stylelint длину не
52
+ судит вовсе — держит его проверка дерева. Выросший файл экрана делится по блокам, а общая
53
+ раскладка уходит в свой слой.
41
54
  - **Предупреждение stylelint роняет прогон наравне с ошибкой.** `!important` объявлен
42
55
  предупреждением, а прогон идёт с `--max-warnings 0`: иначе запрет читается как пожелание —
43
56
  два таких предупреждения лежали в дереве, а `npm run stylelint` возвращал ноль и гейтом не был.
@@ -52,10 +65,30 @@ description: Правило под «Закон о фронтовом прило
52
65
 
53
66
  - `styling-bem-layout` — экран на общем слое раскладки, блоки приложения.
54
67
  - `styling-bem-component` — стили компонента кита, `:host`, модификаторы, язык оформления сайта.
68
+ - `styling-bem-sheet` — шторка и окно поверх страницы: чем открываются, подложка, замер.
55
69
 
56
70
  ## Ловушки
57
71
 
58
72
  - **`rtElem` без предка с `rtBlock` роняет отрисовку в рантайме** — сборка и линт молчат.
73
+ - **Спроецированный узел блока-предка не имеет.** `rtElem` берёт имя блока инъекцией от
74
+ ближайшего предка **по месту объявления шаблона**, а не по месту вставки: элемент, который
75
+ экран объявляет у себя и отдаёт в проекцию чужого компонента, ищет `rtBlock` в своём шаблоне
76
+ и не находит. Отрисовка падает в рантайме, сборка и линт зелёные. Класс на такой узел
77
+ вешается правилом по селектору кита в общем слое раскладки, а не директивой.
78
+ - **Элемент с `backdrop-filter` или своим `z-index` замыкает потомков в свой слой.** Липкая
79
+ шапка с размытием — самый частый случай: номер слоя у того, что лежит внутри неё,
80
+ сравнивается не с соседями по странице, а только с соседями внутри шапки, и нижняя панель
81
+ накрывает открытую шторку вместе с её кнопкой. Проверяется это `elementFromPoint` в центре
82
+ кнопки: сборка, линт и скриншот показывают тут целую страницу.
83
+ - **До узла, вынесенного к `<body>`, стили компонента не достают.** Превью и заглушку переноса
84
+ кладёт туда библиотека, а правила компонента заскоуплены атрибутом: файл выглядит рабочим и
85
+ не красит ничего. Такие правила объявляются в общем слое приложения. Ни сборка, ни линт, ни
86
+ проверка «класс без правила» этого не видят: класса такого в шаблоне нет вовсе, и пролежать
87
+ это может несколько задач подряд.
88
+ - **Имя токена не сверяется ничем.** Ссылка на несуществующий токен собирается, проходит
89
+ stylelint и проверку класса без правила, а свойство молча берёт наследованное значение:
90
+ правило выглядит написанным и не красит ничего. Ловится это только замером в браузере, а
91
+ имена берутся из объявлений кита, а не по догадке о том, как токен должен был бы называться.
59
92
  - **`rtBlock` на `<ng-container>` класса не ставит вовсе:** узел это комментарий, и имя блока
60
93
  он только объявляет потомкам. Класс блока экрана вешает хост через `host: { class: … }`.
61
94
  - **`justify-content: center` во flex-контейнере с `overflow-x` уводит первые элементы за
@@ -73,6 +73,11 @@ description: Правило под «Закон о ведении работы»
73
73
  — признак; правила, тексты, обвязка и зависимости под него не подпадают. Обход — строка
74
74
  `**Поведение:** не меняется — <причина владельца>` в замысле; пустая причина не
75
75
  принимается.
76
+ - **Гард замысла — нижняя граница, а не признак папки задачи.** Он требует её только под правку
77
+ кода приложения; нужна ли папка работе, которая туда не доходит, решает число заходов, а не
78
+ путь. Работа в один заход и один коммит целиком помещается в тело отчёта — так закрывается
79
+ разбор чужой папки. Работа с этапами и передачей папку заводит: между заходами её состояние
80
+ не держит ничто, кроме хода работы.
76
81
  - **Ход, в котором владельцу задан вопрос, не заканчивается, пока за этот же ход не читались
77
82
  законы и правила.** Чтением считается любой из трёх путей: загрузка правила, чтение файла
78
83
  законов или правил, поиск по ним. Отбивает гард разговора — на завершении хода, а не на