@rt-tools/agent-kit 0.2.0 → 0.4.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 (203) hide show
  1. package/README.md +235 -18
  2. package/assets/agents/business-analyst.md +74 -0
  3. package/assets/agents/project-manager.md +70 -0
  4. package/assets/agents/qa-engineer.md +72 -0
  5. package/assets/agents/skill-curator.md +110 -0
  6. package/assets/agents/spec-critic.md +44 -0
  7. package/assets/agents/spec-writer.md +50 -0
  8. package/assets/checks/board.github.mjs +286 -0
  9. package/assets/checks/check-board.github.mjs +188 -0
  10. package/assets/checks/check-doc-paths.mjs +163 -0
  11. package/assets/checks/check-dupes.mjs +277 -0
  12. package/assets/checks/check-lib-layers.mjs +573 -0
  13. package/assets/checks/check-reuse.mjs +208 -0
  14. package/assets/checks/check-schema-drift.mjs +186 -0
  15. package/assets/checks/check-specs.mjs +1007 -0
  16. package/assets/checks/check-styles.mjs +109 -0
  17. package/assets/checks/rt-kit-checks.config.mjs +134 -0
  18. package/assets/checks/task-new.github.mjs +198 -0
  19. package/assets/commands/skill-curator.md +70 -0
  20. package/assets/defaults/gate-map.sh +100 -0
  21. package/assets/defaults/project.sh +179 -0
  22. package/assets/hooks/browser-device-id.sh +20 -0
  23. package/assets/hooks/browser-guard-device-id.sh +28 -0
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +18 -0
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +79 -0
  27. package/assets/hooks/browser-guard-require-select.sh +54 -0
  28. package/assets/hooks/commit-msg.sh +26 -0
  29. package/assets/hooks/constitution-index.sh +43 -0
  30. package/assets/hooks/dev-server-guard.sh +115 -0
  31. package/assets/hooks/docs-guard.sh +282 -0
  32. package/assets/hooks/git-guard-delivery.sh +167 -0
  33. package/assets/hooks/git-guard-main.sh +73 -0
  34. package/assets/hooks/git-guard-push-tests.sh +94 -0
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/lint-after-edit.sh +219 -0
  37. package/assets/hooks/qa-dataid-guard.sh +121 -0
  38. package/assets/hooks/reuse-first-guard.sh +154 -0
  39. package/assets/hooks/skill-gate-rearm.sh +23 -0
  40. package/assets/hooks/skill-gate.sh +128 -0
  41. package/assets/hooks/skill-loaded.sh +21 -0
  42. package/assets/hooks/sql-guard.sh +679 -0
  43. package/assets/hooks/task-context-load.sh +100 -0
  44. package/assets/hooks/task-flow-guard.sh +107 -0
  45. package/assets/laws/{access.md → application/access.md} +1 -4
  46. package/assets/laws/{locales.md → application/locales.md} +1 -3
  47. package/assets/laws/application/money.md +41 -0
  48. package/assets/laws/application/ownership.md +32 -0
  49. package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
  50. package/assets/laws/code-structure.md +7 -6
  51. package/assets/laws/delivery.md +53 -3
  52. package/assets/laws/entity-editing.md +49 -55
  53. package/assets/laws/entity-models.md +4 -14
  54. package/assets/laws/frontend-application.md +5 -5
  55. package/assets/laws/lib-imports.md +14 -1
  56. package/assets/laws/lists.md +33 -0
  57. package/assets/laws/navigation.md +40 -0
  58. package/assets/laws/project-documentation.md +17 -8
  59. package/assets/laws/reuse-first.md +26 -21
  60. package/assets/laws/shared-code.md +13 -1
  61. package/assets/laws/verifiability.md +17 -1
  62. package/assets/laws/work-conduct.md +48 -0
  63. package/assets/patterns/admin-lists-screen.md +131 -0
  64. package/assets/patterns/admin-nav-item.md +71 -0
  65. package/assets/patterns/angular-patterns-state.md +101 -0
  66. package/assets/patterns/api-layer-pair.md +88 -0
  67. package/assets/patterns/browser-verification-measure.md +86 -0
  68. package/assets/patterns/browser-verification-stand.md +143 -0
  69. package/assets/patterns/component-structure-new.md +99 -0
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +137 -0
  72. package/assets/patterns/doc-style-write.md +109 -0
  73. package/assets/patterns/entity-aside.md +136 -0
  74. package/assets/patterns/entity-models-new.md +124 -0
  75. package/assets/patterns/entity-store.md +91 -0
  76. package/assets/patterns/git-workflow-commit.azure.md +259 -0
  77. package/assets/patterns/git-workflow-commit.github.md +333 -0
  78. package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
  79. package/assets/patterns/git-workflow-merge.md +99 -0
  80. package/assets/patterns/git-workflow-migration.md +88 -0
  81. package/assets/patterns/git-workflow-restart.md +49 -0
  82. package/assets/patterns/lib-layers-move.md +95 -0
  83. package/assets/patterns/lib-layers-new.md +82 -0
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +71 -0
  86. package/assets/patterns/platform-access-di.md +84 -0
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +73 -0
  89. package/assets/patterns/seo-page.md +104 -0
  90. package/assets/patterns/seo-verify.md +83 -0
  91. package/assets/patterns/shared-code-new.md +86 -0
  92. package/assets/patterns/spec-driven-domain.md +107 -0
  93. package/assets/patterns/spec-driven-rule.md +127 -0
  94. package/assets/patterns/styling-bem-component.md +88 -0
  95. package/assets/patterns/styling-bem-layout.md +73 -0
  96. package/assets/patterns/task-flow-close.md +90 -0
  97. package/assets/patterns/task-flow-resume.md +94 -0
  98. package/assets/patterns/task-flow-start.md +117 -0
  99. package/assets/patterns/testing-e2e.md +92 -0
  100. package/assets/patterns/testing-unit.md +117 -0
  101. package/assets/patterns/translations-key.md +64 -0
  102. package/assets/patterns/ts-procedure.md +65 -0
  103. package/assets/rules/angular-patterns.md +71 -0
  104. package/assets/rules/api-layer.md +71 -0
  105. package/assets/rules/browser-verification.md +87 -0
  106. package/assets/rules/component-structure.md +64 -0
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +103 -0
  109. package/assets/rules/entity-conventions.md +78 -0
  110. package/assets/rules/entity-models.md +70 -0
  111. package/assets/rules/git-workflow.azure.md +116 -0
  112. package/assets/rules/git-workflow.github.md +123 -0
  113. package/assets/rules/git-workflow.gitlab.md +113 -0
  114. package/assets/rules/lib-layers.md +80 -0
  115. package/assets/rules/lists.md +73 -0
  116. package/assets/rules/navigation.md +78 -0
  117. package/assets/rules/ownership-scope.md +63 -0
  118. package/assets/rules/permissions.md +70 -0
  119. package/assets/rules/platform-access.md +77 -0
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +83 -0
  122. package/assets/rules/seo.md +71 -0
  123. package/assets/rules/shared-code.md +70 -0
  124. package/assets/rules/spec-driven.md +135 -0
  125. package/assets/rules/styling-bem.md +74 -0
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +100 -0
  128. package/assets/rules/translations.md +69 -0
  129. package/assets/rules/typescript-conventions.md +76 -0
  130. package/assets/skills/agent-kit.md +81 -0
  131. package/assets/skills/write-a-skill.md +108 -0
  132. package/assets/templates/gate-map.sh +45 -0
  133. package/assets/templates/implementation.md +44 -0
  134. package/assets/templates/pattern.md +5 -1
  135. package/assets/templates/project.sh +54 -0
  136. package/assets/templates/rule.md +12 -23
  137. package/assets/variants.json +20 -0
  138. package/assets/workflows/feature.js +134 -0
  139. package/assets/workflows/plan.js +150 -0
  140. package/bin/agent-kit.d.ts.map +1 -1
  141. package/bin/agent-kit.js +78 -5
  142. package/bin/agent-kit.js.map +1 -1
  143. package/bin/prompt.d.ts +5 -0
  144. package/bin/prompt.d.ts.map +1 -1
  145. package/bin/prompt.js +19 -7
  146. package/bin/prompt.js.map +1 -1
  147. package/index.d.ts +1 -0
  148. package/index.d.ts.map +1 -1
  149. package/index.js +1 -0
  150. package/index.js.map +1 -1
  151. package/lib/assets.d.ts +14 -1
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +23 -2
  154. package/lib/assets.js.map +1 -1
  155. package/lib/catalog.d.ts +52 -5
  156. package/lib/catalog.d.ts.map +1 -1
  157. package/lib/catalog.js +104 -16
  158. package/lib/catalog.js.map +1 -1
  159. package/lib/commands.d.ts +22 -1
  160. package/lib/commands.d.ts.map +1 -1
  161. package/lib/commands.js +218 -11
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +57 -0
  164. package/lib/companion.d.ts.map +1 -0
  165. package/lib/companion.js +60 -0
  166. package/lib/companion.js.map +1 -0
  167. package/lib/config.d.ts +42 -2
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +60 -2
  170. package/lib/config.js.map +1 -1
  171. package/lib/freshness.d.ts +14 -0
  172. package/lib/freshness.d.ts.map +1 -0
  173. package/lib/freshness.js +116 -0
  174. package/lib/freshness.js.map +1 -0
  175. package/lib/hooks-map.d.ts +24 -0
  176. package/lib/hooks-map.d.ts.map +1 -0
  177. package/lib/hooks-map.js +72 -0
  178. package/lib/hooks-map.js.map +1 -0
  179. package/lib/integrity.d.ts +36 -0
  180. package/lib/integrity.d.ts.map +1 -0
  181. package/lib/integrity.js +44 -0
  182. package/lib/integrity.js.map +1 -0
  183. package/lib/picker.d.ts +11 -1
  184. package/lib/picker.d.ts.map +1 -1
  185. package/lib/picker.js +44 -6
  186. package/lib/picker.js.map +1 -1
  187. package/lib/stamp.d.ts +2 -5
  188. package/lib/stamp.d.ts.map +1 -1
  189. package/lib/stamp.js +25 -10
  190. package/lib/stamp.js.map +1 -1
  191. package/lib/sync.d.ts +29 -0
  192. package/lib/sync.d.ts.map +1 -1
  193. package/lib/sync.js +78 -4
  194. package/lib/sync.js.map +1 -1
  195. package/lib/variants.d.ts +44 -0
  196. package/lib/variants.d.ts.map +1 -0
  197. package/lib/variants.js +82 -0
  198. package/lib/variants.js.map +1 -0
  199. package/package.json +1 -1
  200. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  201. package/assets/laws/admin-lists.md +0 -35
  202. package/assets/laws/admin-navigation.md +0 -38
  203. package/rt-tools-agent-kit-0.2.0.tgz +0 -0
@@ -0,0 +1,78 @@
1
+ ---
2
+ name: navigation
3
+ kind: rule
4
+ law: navigation
5
+ description: Правило под «Закон о навигации по разделам». Брать при правке декларации меню, libs/admin/common/container/**, apps/admin/src/app/app.routes.ts и любого libs/admin/**/*.routes.ts. Называет декларацию меню, колонки и группы панели, гейтинг и вложение адресов. Готовый код — в паттерне admin-nav-item.
6
+ ---
7
+
8
+ # Навигация админки — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/navigation.md`. Закон говорит, как пользователь
11
+ находит раздел и попадает в него; здесь — из чего меню собрано в этом дереве и как
12
+ выглядит. Про вид говорит правило: закон о нём молчит намеренно.
13
+
14
+ ## Как это называется здесь
15
+
16
+ | В законе | Здесь |
17
+ | ------------------------ | ----------------------------------------------------------------------- |
18
+ | пункт меню | запись в декларации `admin-nav.items.ts` |
19
+ | раздел с панелью | пункт с колонками; своего адреса не имеет |
20
+ | панель второго уровня | попап `rt-page-header` из пакета кита, разложенный колонками и группами |
21
+ | признак непросмотренного | точка, приходит предикатом по идентификатору пункта |
22
+ | флаг «экрана ещё нет» | `disabled` в декларации |
23
+
24
+ ## Где это лежит
25
+
26
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
27
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
28
+ же дереве, которое держит код иначе.
29
+
30
+ ## Как закон применяется здесь
31
+
32
+ - **Пункт объявляется один раз и служит источником и меню, и гейтинга маршрутов.** Второе
33
+ объявление рядом с маршрутами разошлось бы с первым, и получилось бы «пункта не видно, а
34
+ страница открывается».
35
+ - **Декларация не импортирует ни один `shell`.** Иначе граф `shell → container/feature → shell`
36
+ замкнётся и проверка раскладки встанет.
37
+ - **Гейтинг двухслойный: право пользователя и флаг раздела.** Пункт с флагом объявляется без
38
+ прав и без адреса — право открывает экран, а экрана нет; виден он при этом всем.
39
+ - **Данные домена приходят в шапку токеном, а не импортом.** Интерфейс и `InjectionToken` живут
40
+ в `container/util`, связывает их с реализацией композиционный корень.
41
+
42
+ ## Чего из закона здесь нет
43
+
44
+ В меню стоят семнадцать пунктов второго уровня и раздел «Финансы», у которых экрана пока нет:
45
+ они выкачены недоступными, потому что исчезнувший пункт неотличим от того, которого никогда
46
+ не было.
47
+
48
+ Отрисовка самой шапки живёт в `rt-page-header` из `@rt-tools/ui-kit-v2`. Проект её не пишет и
49
+ не может нарушить: он объявляет пункты, а рисует их кит. Китом заданы:
50
+
51
+ - подсказка у недоступного пункта и `aria-disabled` вместо нативного `disabled`;
52
+ - подсветка раздела по префиксу адреса;
53
+ - открытие панели второго уровня наведением, а на касании — нажатием;
54
+ - раскладка панели колонками, внутри колонки — группами с подписью; если первая группа в
55
+ колонке без заголовка, её пункты начинаются от верха панели;
56
+ - ширина панели по числу колонок, а не по длине подписей, и её помещаемость от своего
57
+ раздела до правого края экрана;
58
+ - указатель раскрытия у раздела с панелью и его отсутствие у раздела без неё;
59
+ - отдельная мобильная раскладка: меню сворачивается в кнопку, раскрывается теми же разделами
60
+ и группами и прокручивается, когда пункты не помещаются по высоте.
61
+
62
+ Ничего из этого правило к коду проекта не привязывает: привязывать нечего.
63
+
64
+ ## Паттерны
65
+
66
+ - `admin-nav-item` — завести пункт меню и его маршрут: декларация, права, флаг, вложение
67
+ адреса, подпись во всех локалях.
68
+
69
+ ## Ловушки
70
+
71
+ - Обработчика нажатия у недоступного пункта нет вовсе: `aria-disabled` нажатие не отбивает.
72
+ - Ветка, куда забыли подмешать константу ro-маршрута, отличается только тем, что кнопка в
73
+ шапке на ней ничего не открывает.
74
+ - Группы внутри раскрытого раздела на узком экране отдельно не сворачиваются, поэтому в
75
+ множестве раскрытых лежат только идентификаторы разделов.
76
+ - Ширина панели считается числом колонок, а не содержимым; числа задаются токенами `--rt-*`,
77
+ сырые значения запрещены правилом стилей.
78
+ - Переезд адреса трогает ссылки и спеки e2e — они ходят по адресам.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: ownership-scope
3
+ kind: rule
4
+ law: ownership
5
+ description: Правило под «Закон о владеющей сущности». Брать, когда процедура или экран работает с записью, принадлежащей владеющей сущности — разрешение идентификатора, умолчание при единственной действующей, сводки по всем. Называет обе формы разрешения и коды отказа. Готовый код — в паттерне ownership-scope-resolve. Чем это названо здесь — в implementation.md рядом.
6
+ ---
7
+
8
+ # Владеющая сущность — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/application/ownership.md`. Закон говорит, что должно быть
11
+ верно; здесь — каким приёмом это держится. Как владеющая сущность названа в этом дереве и какие
12
+ экраны считаются сводками — в `implementation.md` рядом.
13
+
14
+ ## Как это называется здесь
15
+
16
+ | В законе | Здесь |
17
+ | ---------------------------------- | ------------------------------------------------------------------------------- |
18
+ | владеющая сущность | запись своего домена; в запросах — поле `<сущность>_id` |
19
+ | пустой идентификатор | пустая строка, а не отсутствующее поле: необязательных скаляров в контракте нет |
20
+ | сводка | списки и статистика, отвечающие сразу по всем действующим |
21
+ | отказ при двух и более действующих | `Code.InvalidArgument` |
22
+ | отказ, когда действующих нет вовсе | `Code.FailedPrecondition` |
23
+ | ненайденный явный идентификатор | `Code.NotFound` |
24
+
25
+ У списков, переведённых на общую выборку, идентификатор отдельным полем запроса не приходит
26
+ вовсе: это условие отбора в модели фильтра, а прежний номер поля помечен `reserved`.
27
+
28
+ ## Где это лежит
29
+
30
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
31
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
32
+ же дереве, которое держит код иначе.
33
+
34
+ ## Как закон применяется здесь
35
+
36
+ - **Запись принадлежит владеющей сущности, а не системе.** Общих настроек, действующих сразу
37
+ на все, нет: заведение второй не меняет поведение первой.
38
+ - **Пустой идентификатор означает единственную действующую.** Пока она одна, клиент вправе её
39
+ не называть.
40
+ - **При двух и более действующих запрос обязан назвать сущность.** Умолчание перестаёт быть
41
+ однозначным, и запрос отбивается как неверный.
42
+ - **Названная явно находится, даже если она скрыта.** У скрытой остаются записи, деньги и
43
+ переписка.
44
+ - **Там, где показана сводка, пустой идентификатор означает все действующие.** Списки и
45
+ статистика отвечают по всему набору, а не требуют сперва выбрать одну.
46
+ - **Когда действующих нет вовсе, запрос отбивается.** Скрытая вместо отсутствующей действующей
47
+ не подставляется.
48
+
49
+ ## Паттерны
50
+
51
+ - `ownership-scope-resolve` — разрешение сущности в процедуре и обе формы умолчания.
52
+
53
+ ## Ловушки
54
+
55
+ - **Разрешение разложено на две части намеренно:** чистая функция разбирает случай, обёртка
56
+ ходит в хранилище и переводит случай в отказ. Вторая реализация того же правила однажды уже
57
+ жила у обработчика статистики соседнего домена и успела разойтись порядком выборки.
58
+ - **Отсутствие действующей сущности и ненайденный явный идентификатор — разные случаи для
59
+ вызывающего:** первое отвечает `FailedPrecondition`, второе — `NotFound`.
60
+ - Таблица владеющих сущностей читается целиком одним запросом — она маленькая, и выборки ей не
61
+ нужно.
62
+ - Выбор сущности стоит в шапке и действует на разделы, которые говорят об одной. Снаружи выбора
63
+ нет: пользователь приходит на страницу конкретной.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: permissions
3
+ kind: rule
4
+ law: access
5
+ description: Правило под «Закон о доступе». Брать при заведении или правке процедуры Connect, перехватчика входа, гварда маршрута админки и декларации меню. Называет четыре вида доступа, декораторы, сложение пресета с оверрайдами и гейтинг разделов админки. Готовый код — в паттерне permissions-procedure.
6
+ ---
7
+
8
+ # Доступ — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/application/access.md`. Закон говорит, что должно быть верно;
11
+ здесь — чем это названо в этом дереве и где лежит. Устройство самих разделов админки —
12
+ `navigation`.
13
+
14
+ ## Как это называется здесь
15
+
16
+ | В законе | Здесь |
17
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
+ | право | пара «ресурс и действие» строкой: `bookings:manage`, `chat:manage` |
19
+ | объявление доступа | декоратор на классе процедуры: `@RequiresPermission('bookings:manage')`, `@RequiresAuth('причина')`, `@PublicProcedure('причина')`, `@OptionalAuthProcedure('причина')` |
20
+ | пресет | именованный набор прав, выдаваемый пользователю целиком |
21
+ | оверрайд | точечная правка права поверх пресета для одного пользователя |
22
+ | отбивка без входа | `Code.Unauthenticated` |
23
+ | отбивка без права | `Code.PermissionDenied` |
24
+
25
+ ## Где это лежит
26
+
27
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
28
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
29
+ же дереве, которое держит код иначе.
30
+
31
+ ## Как закон применяется здесь
32
+
33
+ - **Каждая процедура объявляет свой доступ декоратором, и объявление ровно одно.**
34
+ Процедура без объявления или с двумя объявлениями не даёт приложению подняться.
35
+ - **Видов доступа четыре: по праву, любому вошедшему, публично и публично с чтением входа.**
36
+ Последний отдаёт вошедшему больше, чем гостю, — так владелец видит скрытые объекты в общем
37
+ списке.
38
+ - **Права пользователя — это права пресета, поверх которых применены его оверрайды.**
39
+ - **Запрос без входа отбивается как неаутентифицированный, а вход без права — как отказ в
40
+ доступе.** Это разные ответы: первый лечится входом, второй — нет.
41
+ - **Право проверяется перехватчиком до тела процедуры.** Обработчик не решает, пускать ли
42
+ вызывающего.
43
+ - **Публичность объявляется с причиной.** Причина — аргумент декоратора, записанный для
44
+ читателя кода; ни в ответ, ни в лог она не уходит.
45
+ - **Пункт меню и адрес раздела закрыты по одной декларации.** Иначе скрытый пункт закрывает
46
+ раздел лишь на вид: адрес открывается по прямой ссылке.
47
+ - **Пока права не получены, админка ничего не прячет.** Пустая шапка после сетевого сбоя
48
+ выглядит как сломанная админка и не оставляет выхода.
49
+
50
+ ## Чего из закона здесь нет
51
+
52
+ Право, отобранное посреди сессии, до перевхода не действует: токен живёт со своими правами до
53
+ истечения — это `Q-A-1` в законе. Смены пароля из интерфейса нет вовсе: ни экрана, ни
54
+ процедуры, ни восстановления забытого — `Q-A-2`.
55
+
56
+ ## Паттерны
57
+
58
+ - `permissions-procedure` — объявление доступа у процедуры и гейтинг раздела админки.
59
+
60
+ ## Ловушки
61
+
62
+ - **Процедура, о которой перехватчик ничего не знает, отбивается как отказ в доступе, а не
63
+ пропускается.**
64
+ - **Гвард стоит на дочерних маршрутах защищённой группы, а не на самой группе:** гвард группы
65
+ отрабатывает один раз за загрузку страницы и переходов между разделами не видит.
66
+ - **Права приходят ответом профиля уже внутри защищённой группы**, поэтому гвард дожидается
67
+ запуска админки. Отказ запроса ожидание не роняет: с неизвестными правами не закрывается
68
+ ничего.
69
+ - Декораторы живут в `util` домена аутентификации, а не рядом с перехватчиком: их ставит
70
+ каждый домен с процедурами, и ребро к объявлениям дешевле ребра к секрету и базе.
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: platform-access
3
+ kind: rule
4
+ law: frontend-application
5
+ description: Правило под «Закон о фронтовом приложении». Брать, когда правка задевает глобальный объект или среду исполнения — window, globalThis, PLATFORM_ID, isPlatformBrowser, document.defaultView, localStorage, IntersectionObserver. Называет токены DI, приведение типа и подводные камни отдачи страницы сервером. Не действует под libs/api и apps/api. Готовый код — в паттерне platform-access-di.
6
+ ---
7
+
8
+ # Окружение браузера — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/frontend-application.md`. Закон говорит, что должно быть
11
+ верно; здесь — чем в этом дереве заменяется прямое обращение к глобальному объекту. Состояние
12
+ — `angular-patterns`, файл компонента — `component-structure`, стили — `styling-bem`, слой
13
+ обращения к серверу — `api-layer`. Все пять под одним законом.
14
+
15
+ Правило про фронт: `libs/api/**` и `apps/api/**` — это NestJS, там своя среда, и ничего из
16
+ перечисленного не применяется.
17
+
18
+ ## Как это называется здесь
19
+
20
+ | Прямое обращение | Здесь |
21
+ | ---------------------------------------- | -------------------------------------------------- |
22
+ | `globalThis.open(...)`, `window.open()` | `inject(WINDOW).open(...)` |
23
+ | `globalThis.crypto.randomUUID()` | `inject(WINDOW).crypto.randomUUID()` |
24
+ | `isPlatformBrowser(inject(PLATFORM_ID))` | `inject(PlatformService).isPlatformBrowser` |
25
+ | `document.defaultView` | `inject(WINDOW)` |
26
+ | прямой `document` | `inject(DOCUMENT)` из `@angular/common` — как было |
27
+ | инициализация DOM после отрисовки | `afterNextRender()` |
28
+
29
+ `WINDOW`, `PlatformService` и `StorageService` приходят из `@rt-tools/core`.
30
+
31
+ ## Где это лежит
32
+
33
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
34
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
35
+ же дереве, которое держит код иначе.
36
+
37
+ ## Как закон применяется здесь
38
+
39
+ - **Глобальный объект приходит токеном `WINDOW`, а не берётся напрямую.** Тип уточняется
40
+ приведением к `Window & typeof globalThis`: конструкторы вроде `IntersectionObserver`
41
+ объявлены на глобальном объекте, а не на интерфейсе `Window`.
42
+ - **Среда проверяется через `PlatformService`, а не через `typeof window`.** Проверка по
43
+ наличию глобала верна случайно и ломается на первой же среде, где глобал подставлен.
44
+ - **Прямое обращение к глобальному объекту отбивает линтер.** Шестнадцать имён окружения
45
+ браузера запрещены на фронтовых деревьях; три места, где прямой доступ осознан, выведены
46
+ из-под запрета списком в конфиге, а не отключением в строке.
47
+
48
+ ## Чего из закона здесь нет
49
+
50
+ Запрет действует на имена, а не на пути к ним: `this.#document.defaultView` линтер пропускает,
51
+ потому что глобального имени в такой строке нет. Такое обращение законно — окно там уже пришло
52
+ внедрением, — но и настоящий обход выглядел бы так же, и увидеть его можно только чтением.
53
+
54
+ Прямой доступ остаётся в трёх местах, и это осознанно: встроенный скрипт против мелькания темы
55
+ в `apps/site/src/index.html` (работает до подъёма приложения), обработчик отказа подъёма в
56
+ `apps/*/src/main.ts`, и код внутри `page.evaluate` в сквозных спеках — он исполняется в
57
+ странице, вне DI. Новое место в этот список не добавляется без явного согласования владельца.
58
+
59
+ ## Паттерны
60
+
61
+ - `platform-access-di` — готовые инжекты, приведение типа, чистые функции, проверка среды.
62
+
63
+ ## Ловушки
64
+
65
+ - **Фабрика токена бросает `Window is not available`, если у документа нет `defaultView`.**
66
+ Под отдачей страницы сервером `defaultView` есть, и инжект полем класса в root-сервисе
67
+ безопасен, но сервис, обязанный работать без DOM вообще, берёт окно внутри метода под
68
+ проверкой среды.
69
+ - **После правки, добавляющей `WINDOW` в сервис, который создаётся на подъёме, нужна не только
70
+ сборка, но и поднятый сервер отдачи страниц.** Сайт — настоящий SSR, и падение видно только
71
+ там.
72
+ - **Вокруг хранилища проверка среды не нужна:** `StorageService` и так уходит в память вне
73
+ браузера, и лишний `if (!isBrowser)` вокруг чтения и записи — мёртвый код.
74
+ - **В `*.logic.ts` и `*.util.ts` нет DI:** окно принимается параметром, а инжектит его
75
+ вызывающий компонент.
76
+ - **Подготавливать состояние сквозной спеки записью в хранилище нельзя:** спека проходит те же
77
+ шаги, что и пользователь.
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: pricing
3
+ kind: rule
4
+ law: money
5
+ description: Правило под «Закон о деньгах». Брать при правке расчёта цены, скидок, курсов валют, сумм в заказах, письмах и сводках. Называет хранение в целых единицах валюты хранения, единственное округление, выбор одной наибольшей скидки и справочный пересчёт. Готовый код — в паттерне pricing-quote. Чем это названо здесь — в implementation.md рядом.
6
+ ---
7
+
8
+ # Суммы — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/application/money.md`. Закон говорит, что должно быть
11
+ верно; здесь — каким приёмом это держится. Валюта хранения, набор справочных валют и имена
12
+ колонок — при этом дереве, в `implementation.md` рядом.
13
+
14
+ ## Как это называется здесь
15
+
16
+ | В законе | Здесь |
17
+ | ------------- | ---------------------------------------------------------------------------------------------- |
18
+ | сумма | целое число единиц валюты хранения; код валюты стоит в имени поля — `total<Код>`, `price<Код>` |
19
+ | валюта показа | поле заказа с кодом валюты, в которой пользователь смотрел цену |
20
+ | расчёт цены | одна чистая функция: на входе — что заказано, на выходе — подытог, скидка и итог |
21
+ | скидка | механики перечислены в `implementation.md`; правило про выбор одно на все |
22
+ | курс | кэш в хранилище, обновляется по расписанию |
23
+
24
+ ## Где это лежит
25
+
26
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
27
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
28
+ же дереве, которое держит код иначе.
29
+
30
+ ## Как закон применяется здесь
31
+
32
+ - **Сумма хранится в целых единицах валюты хранения.** Дробная часть даёт расхождение между
33
+ подытогом и итогом: показанное построчно перестаёт складываться в показанное внизу. Если
34
+ дробная часть в предметной области значима, единицей хранения становится она сама.
35
+ - **Код валюты стоит в имени поля, а не подразумевается.** Поле `total` без кода читается как
36
+ «сумма вообще», и первый же пересчёт кладёт в него чужую валюту.
37
+ - **Округление происходит один раз — при расчёте цены.** Сумма на экране, сумма в письме и
38
+ сумма в хранилище — одно и то же число, а не три результата одного пересчёта.
39
+ - **Из подходящих скидок применяется одна — наибольшая.** Сравниваются они в валюте хранения:
40
+ доля и сумма иначе несравнимы.
41
+ - **При равной выгоде побеждает та скидка, которую пользователь ввёл руками.** Он ждёт
42
+ подтверждения своему действию, а «код не сработал» при той же итоговой цене читается как
43
+ поломка.
44
+ - **Скидка суммой ограничена подытогом.** Иначе она увела бы цену ниже нуля.
45
+ - **Пересчёт в чужую валюту не хранится.** В заказе лежат сумма в валюте хранения и код валюты
46
+ показа; само число вычисляется на показ.
47
+ - **Оплата идёт в валюте хранения, остальные валюты — справка.** Ни подтверждение, ни документ
48
+ не называют справочное число суммой к оплате.
49
+ - **Валюта показа следует за пользователем.** Он выбрал её на экране — в письме сумма
50
+ пересчитана в неё же.
51
+ - **Курс недоступен — сумма показывается в валюте хранения без пересчёта.** Пересчёт по
52
+ неизвестно какому курсу хуже отсутствующего: отсутствие видно, а неверный курс — нет.
53
+
54
+ ## Паттерны
55
+
56
+ - `pricing-quote` — расчёт цены, выбор скидки, работа с курсом.
57
+
58
+ ## Ловушки
59
+
60
+ - **Курс тянется по расписанию и кэшируется.** Само число в чужой валюте нигде не сохраняется.
61
+ - **Дробная сумма в контракте — признак того, что округление уехало на показ.** Округление
62
+ одно, и живёт оно в расчёте.
63
+ - **Справочная сумма рядом с суммой к оплате читается как цена.** В письме и в документе
64
+ справочное число подписывается как справка.
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: reuse-first
3
+ kind: rule
4
+ law: reuse-first
5
+ description: Правило под «Закон о единообразии приложения». Брать перед заведением любого нового экрана, компонента, поля, стора, сервиса, маппера или процедуры — на что опираться, что уже готово в ките и базовых классах, по каким признакам видно, что готовое обошли. Что делать, когда готового не хватило, — в паттерне reuse-first-extend.
6
+ ---
7
+
8
+ # Единообразие — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/reuse-first.md`. Закон говорит, что одинаковые вещи ведут
11
+ себя одинаково; здесь — на чём это стоит в этом дереве и чем названо.
12
+
13
+ ## Как это называется здесь
14
+
15
+ | В законе | Здесь |
16
+ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | готовое | компоненты кита `@rt-tools/ui-kit-v2` с префиксом `rt-` — сегодня их больше семидесяти — и базовые классы без селектора, наследуемые в `@Component` экрана |
18
+ | раскладка страниц, форм и окон | `apps/<app>/src/styles/`, применяется директивами BEM |
19
+ | оформление части приложения | файл `.scss` рядом с компонентом |
20
+ | отступление, решённое владельцем | маркер `native-ok` в той же строке с объяснением |
21
+
22
+ ## Где это лежит
23
+
24
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
25
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
26
+ же дереве, которое держит код иначе.
27
+
28
+ ## Как закон применяется здесь
29
+
30
+ - **Работа начинается с чтения готового, а не с чистого файла.** Сначала находится опора —
31
+ компонент кита, базовый класс, образец в соседнем домене, — потом пишется своё поверх неё.
32
+ - **Свой примитив и своя основа заводятся только с явного одобрения владельца.** Спрашивается
33
+ это до того, как написан первый файл.
34
+ - **Панель правки записи наследует общую основу, а не собирается своей разметкой.** Тогда она
35
+ открывается, закрывается и спрашивает про несохранённое одинаково во всех разделах.
36
+ - **Об удаче и об отказе сообщает общая шина, а не своя разметка на экране.** Своё сообщение
37
+ расходится с соседним видом, местом и временем показа.
38
+ - **Компонент объявляется тремя файлами: `.ts`, `.html`, `.scss`.** `template:` и `styles:` в
39
+ декораторе, атрибут `style=` и правка размеров через `[ngStyle]` — это стили, до которых не
40
+ дотянется ни кит, ни `stylelint`.
41
+ - **У компонента экрана вне кита файл стилей по умолчанию пустой.** Раскладка объявлена один
42
+ раз в общем слое приложения, а экран её только применяет теми же директивами BEM.
43
+ - **Готовое расширяется, а не клонируется рядом.** Недостающий вариант заводится в ките или в
44
+ базовом классе, и его видят остальные экраны.
45
+ - **Накопленное до гарда сосчитано сплошной проверкой и в список только не растёт.** Признаки
46
+ у неё те же, что у гарда, а смотрит она файл целиком: гейт падает на новом месте, старое
47
+ остаётся числом в сводке.
48
+
49
+ ## Чего из закона здесь нет
50
+
51
+ Одинаковый ответ поля ввода на ошибку держит кит: `RtFormControlBase` в этом дереве не
52
+ наследует никто, потому что поля берутся его готовыми компонентами. Привязать эту статью
53
+ закона здесь не к чему.
54
+
55
+ Инлайновый шаблон стоит у одного компонента, и у него же единственный `styles:` — это шапка
56
+ админки. `role="alert"` написан руками в шестнадцати шаблонах, а `rt-message` зовёт один
57
+ потребитель. Это долг, а не разрешённое отступление.
58
+
59
+ ## Паттерны
60
+
61
+ - `reuse-first-extend` — что делать, когда готового не хватило: расширить кит, объявить
62
+ отступление маркером, снять его.
63
+
64
+ ## Признаки, по которым видно, что готовое обошли
65
+
66
+ - в шаблоне фичи стоит нативный `<input>`, `<button>`, `<select>`, `<table>` или `<dialog>`;
67
+ - отказ или предупреждение собраны руками — свой `role="alert"` вместо `rt-message`;
68
+ - в `.scss` фичи появились `position: fixed` на весь экран, свой backdrop, `z-index` от тысячи,
69
+ `@keyframes` вращения или мерцания;
70
+ - в файле стилей экрана объявлена раскладка — `display: flex` с `gap` и `padding` на `:host`;
71
+ - компонент реализует `ControlValueAccessor` сам, а не наследует `RtFormControlBase`;
72
+ - имя файла кончается на `-button`, `-input`, `-dialog`, `-spinner` или `-table` вне
73
+ кита;
74
+ - маппер переводит поля вручную, без `BaseMapper`;
75
+ - процедура бэкенда объявлена без `@ConnectProcedure()`.
76
+
77
+ ## Ловушки
78
+
79
+ - Перенос переизобретением не считается: строку, которая уже лежит в файле, гард из
80
+ проверяемого текста вычёркивает, а сверка идёт без отступов — при переезде блок меняет
81
+ отступ, оставаясь тем же кодом.
82
+ - Ответ, данный до чтения образца, образец отменяет: согласованная форма выборки списка
83
+ переигрывалась вместе с контрактом через два вопроса после того, как была принята.
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: seo
3
+ kind: rule
4
+ law: search-visibility
5
+ description: Правило под «Закон о видимости в поиске». Брать при любой правке, доходящей до разметки публичного сайта — шаблоны страниц, meta/title/description, JSON-LD, canonical, hreflang, маршруты сайта, sitemap, robots, nginx. Называет восемь локалей, где что лежит и чего у нас нет. Готовый код — в паттернах seo-page и seo-verify.
6
+ ---
7
+
8
+ # Видимость в поиске — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/application/search-visibility.md`. Закон говорит, что должно быть
11
+ верно; здесь — чем это названо в этом коде, где лежит и что из закона у нас не применяется.
12
+
13
+ ## Как это называется здесь
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 объекта |
23
+
24
+ Локаль по умолчанию отдаётся из корня, остальные — с префиксом `/<код>/`. Префикс — часть
25
+ маршрута, а не часть сборки: `<base href>` в разметке всегда `/`.
26
+
27
+ ## Где это лежит
28
+
29
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
30
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
31
+ же дереве, которое держит код иначе.
32
+
33
+ ## Как закон применяется здесь
34
+
35
+ - **Свои теги помечены атрибутом `data-<префикс>-seo`** и при повторном применении переписываются.
36
+ Чужое в `<head>` не трогается: после гидратации там остаются теги от отдачи сервером.
37
+ - **`canonical` ведёт на локализованный путь** `${SITE_ORIGIN}${propertyPath(slug, locale)}`,
38
+ а не на корень и не на адрес локали по умолчанию.
39
+ - **`hreflang` строится по `readyLocales`**, плюс `x-default` на локаль по умолчанию. Список
40
+ локалей сайта для этого не годится: переводы контента заполняет бэк, и готовы они не всегда.
41
+ - **`og:locale:alternate` не включает локаль самой страницы** — иначе она объявлена и
42
+ основной, и альтернативной сразу.
43
+ - **`url` в JSON-LD — канонический адрес страницы объекта**, не корень сайта.
44
+ - **Корень локали отдаёт страницу единственного объекта** с каноническим адресом на `/{slug}`;
45
+ посетителя уводит туда nginx ответом 302.
46
+ - **Перенаправление с прежнего адреса отдаётся с `Cache-Control: max-age`** и покрыто
47
+ `proxy_cache_valid 200 301`. Ключ кэша строится без `$args`, поэтому запросы со строкой
48
+ запроса идут мимо кэша (`proxy_cache_bypass` / `proxy_no_cache $is_args`).
49
+
50
+ ## Чего из закона здесь нет
51
+
52
+ Листинга объектов нет — корень локали отдаёт единственный объект. Появится листинг —
53
+ канонический адрес корня пересматривается; это `Q-SV-1` в законе.
54
+
55
+ ## Паттерны
56
+
57
+ - `seo-page` — правка разметки страницы: теги, JSON-LD, новый маршрут, новая страница в карте.
58
+ - `seo-verify` — проверка отданной разметки на прод-сборке.
59
+
60
+ ## Ловушки
61
+
62
+ - Тег, добавленный мимо `PropertySeoService`, не помечен `data-<префикс>-seo` — он не будет ни
63
+ переписан, ни удалён при следующем применении и переживёт переход между страницами.
64
+ - Новый маршрут сайта без ветки в `app.routes.ts` под каждую локаль существует только в
65
+ локали по умолчанию: `/de/<путь>` отдаст 404 и поисковику, и гостю.
66
+ - Новая страница не попадает в `sitemap.xml` сама — карта строится из объектов, а не из
67
+ маршрутов.
68
+ - Адрес режется по **первому** `?` и только им: `req.url.split('?')` теряет всё после второго
69
+ знака, и перенаправление приходит на страницу без UTM-хвоста — источник заявки считается
70
+ неверно. Разрез живёт в `splitRequestUrl`
71
+ (слой `util` домена страницы объекта) и покрыт спеками; свой не заводить.
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: shared-code
3
+ kind: rule
4
+ law: shared-code
5
+ description: Правило под «Закон об общем коде приложений». Брать, когда значение должно одинаково пониматься сайтом, админкой и бэкендом — предел выборки, набор операторов условия, направление порядка, длина поля, форма запроса и ответа списка. Называет, что берётся из @rt-tools/utils, что из @<область>/common/util и что ловит check:dupes. Готовый код — в паттерне shared-code-new.
6
+ ---
7
+
8
+ # Общий код — как это устроено здесь
9
+
10
+ Правило под закон `docs/constitution/shared-code.md`. Закон говорит, что общим быть обязано;
11
+ здесь — откуда это берётся в этом дереве, чем названо и чего у нас нет.
12
+
13
+ ## Как это называется здесь
14
+
15
+ | В законе | Здесь |
16
+ | ------------------------- | --------------------------------------------------------------------------------------------- |
17
+ | общий пакет | `@rt-tools/utils` — собран без фреймворка, зависит от одного `tslib`, грузится под голым Node |
18
+ | общая либа проекта | `@<область>/common/util`; её тег входит в набор `UNIVERSAL` и виден всем трём приложениям |
19
+ | число-настройка | `DEFAULT_PAGE_SIZE`, `MAX_PAGE_SIZE` |
20
+ | набор значений | `FILTER_OPERATOR_TYPE_ENUM`, `LIST_SORT_ORDER_ENUM` |
21
+ | сверка значения с набором | `listSortOrderOf`, `listFilterOperatorOf` |
22
+ | выборка списка | `IPageModel`, `ISortModel`, `IFilterModel`, `IListState` |
23
+
24
+ ## Где это лежит
25
+
26
+ В этом дереве — таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
27
+ переносится между репозиториями, раскладка — нет, и путь, названный в правиле, врёт в первом
28
+ же дереве, которое держит код иначе.
29
+
30
+ ## Как закон применяется здесь
31
+
32
+ - **Число-настройка лежит в `libs/common/util` и оттуда берётся обеими сторонами.** Умолчание
33
+ доводом не передаётся: пока довод есть, домен вправе назвать своё число, и у промокодов так
34
+ появилось `25` против `20` у остальных.
35
+ - **Набор значений из `@rt-tools/utils` заново не объявляется.** Своё перечисление с теми же
36
+ членами считается копией, даже если имена разошлись.
37
+ - **Значение из набора сверяется парой общих функций, а промах разбирает вызывающий.** Сервер
38
+ отбивает запрос `InvalidArgument`, экран берёт умолчание.
39
+ - **Общим стал только маппер страницы.** У сторон разная политика на непонятное значение, и
40
+ общим может быть лишь то, где она одна: номер меньше единицы обе стороны читают как первую
41
+ страницу.
42
+ - **Перечисления полей порядка и отбора домена копией не считаются.** `EActivitySortProperty`
43
+ и подобные повторяют имена, по которым сортирует сервер именно этого домена.
44
+ - **Строковая настройка и таблица соответствий сверяются по значению, а не по имени.** Имя
45
+ здесь не ключ: `BEM_BLOCK` и `LOG_CONTEXT` объявлены десятками, и значения у них свои, а
46
+ один и тот же перевод статуса живёт под тремя разными именами.
47
+
48
+ ## Чего из закона здесь нет
49
+
50
+ Перевод сущности устроен по-разному: фронт переводит наследником `BaseMapper`, бэкенд —
51
+ свободными функциями `xxxToProto` без общей основы. Это долг `Q-S-1`, а не выбор: новый код на
52
+ бэкенде общей основы не заводит, но и своей второй не пишет.
53
+
54
+ Найденные совпадения строк и таблиц приняты долгом целиком: перевод статуса брони в контракт
55
+ лежит тремя копиями, набор принимаемых вложений — двумя, имя события правки — тремя.
56
+ Сокращать это — работа по доменам, и она заведена вопросом `Q-S-3`.
57
+
58
+ ## Паттерны
59
+
60
+ - `shared-code-new` — как завести новое общее число, функцию или тип и не оставить копию.
61
+
62
+ ## Ловушки
63
+
64
+ - `typeCast.getAsType` для сверки с набором не годится: значение вне набора он пишет в консоль
65
+ и возвращает строкой `'unknown'`.
66
+ - Накопленные повторы лежат в `tools/dupes-allowlist.json` под ключом `debt` и отказом не
67
+ считаются — гейт падает только на новом. Список только сокращается.
68
+ - Ту же логику, написанную заново под другим именем, проверка не ловит, и такой проверки не
69
+ будет: две одинаковые по форме проверки из разных доменов копией не считаются. Заметить это
70
+ может только тот, кто читает правку, — так сказано и в законе.