@rt-tools/agent-kit 0.3.0 → 0.5.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 (202) hide show
  1. package/README.md +194 -30
  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 +329 -0
  9. package/assets/checks/check-board.github.mjs +181 -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 +1086 -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 +106 -0
  21. package/assets/defaults/project.sh +204 -0
  22. package/assets/hooks/browser-device-id.sh +0 -0
  23. package/assets/hooks/browser-guard-device-id.sh +2 -1
  24. package/assets/hooks/browser-guard-no-asking.sh +27 -0
  25. package/assets/hooks/browser-guard-no-listing.sh +2 -1
  26. package/assets/hooks/browser-guard-no-other-drivers.sh +2 -1
  27. package/assets/hooks/browser-guard-require-select.sh +2 -1
  28. package/assets/hooks/commit-msg.sh +1 -1
  29. package/assets/hooks/constitution-index.sh +5 -4
  30. package/assets/hooks/dev-server-guard.sh +8 -6
  31. package/assets/hooks/docs-guard.sh +223 -37
  32. package/assets/hooks/git-guard-delivery.sh +171 -31
  33. package/assets/hooks/git-guard-main.sh +1 -0
  34. package/assets/hooks/git-guard-push-tests.sh +34 -13
  35. package/assets/hooks/glossary-load.sh +23 -0
  36. package/assets/hooks/grill-gate.sh +96 -0
  37. package/assets/hooks/lint-after-edit.sh +155 -30
  38. package/assets/hooks/qa-dataid-guard.sh +72 -32
  39. package/assets/hooks/reuse-first-guard.sh +105 -34
  40. package/assets/hooks/skill-gate-rearm.sh +1 -0
  41. package/assets/hooks/skill-gate.sh +75 -15
  42. package/assets/hooks/skill-loaded.sh +1 -0
  43. package/assets/hooks/sql-guard.sh +606 -56
  44. package/assets/hooks/task-context-load.sh +100 -0
  45. package/assets/hooks/task-flow-guard.sh +118 -0
  46. package/assets/laws/{access.md → application/access.md} +1 -4
  47. package/assets/laws/{locales.md → application/locales.md} +1 -3
  48. package/assets/laws/application/money.md +41 -0
  49. package/assets/laws/application/ownership.md +32 -0
  50. package/assets/laws/{search-visibility.md → application/search-visibility.md} +1 -1
  51. package/assets/laws/code-structure.md +7 -6
  52. package/assets/laws/delivery.md +53 -3
  53. package/assets/laws/entity-editing.md +49 -55
  54. package/assets/laws/entity-models.md +4 -14
  55. package/assets/laws/frontend-application.md +5 -5
  56. package/assets/laws/lib-imports.md +14 -1
  57. package/assets/laws/lists.md +33 -0
  58. package/assets/laws/navigation.md +40 -0
  59. package/assets/laws/project-documentation.md +27 -8
  60. package/assets/laws/reuse-first.md +26 -21
  61. package/assets/laws/shared-code.md +13 -1
  62. package/assets/laws/verifiability.md +30 -1
  63. package/assets/laws/work-conduct.md +59 -0
  64. package/assets/patterns/admin-lists-screen.md +131 -0
  65. package/assets/patterns/admin-nav-item.md +71 -0
  66. package/assets/patterns/angular-patterns-state.md +29 -22
  67. package/assets/patterns/api-layer-pair.md +40 -30
  68. package/assets/patterns/browser-verification-measure.md +41 -38
  69. package/assets/patterns/browser-verification-stand.md +106 -42
  70. package/assets/patterns/component-structure-new.md +33 -32
  71. package/assets/patterns/dependencies-upgrade.md +65 -0
  72. package/assets/patterns/doc-style-sweep.md +65 -28
  73. package/assets/patterns/doc-style-write.md +36 -33
  74. package/assets/patterns/entity-aside.md +136 -0
  75. package/assets/patterns/entity-models-new.md +124 -0
  76. package/assets/patterns/entity-store.md +91 -0
  77. package/assets/patterns/git-workflow-commit.azure.md +259 -0
  78. package/assets/patterns/git-workflow-commit.github.md +337 -0
  79. package/assets/patterns/git-workflow-commit.gitlab.md +283 -0
  80. package/assets/patterns/git-workflow-merge.md +42 -25
  81. package/assets/patterns/git-workflow-migration.md +61 -31
  82. package/assets/patterns/git-workflow-restart.md +20 -20
  83. package/assets/patterns/lib-layers-move.md +50 -32
  84. package/assets/patterns/lib-layers-new.md +41 -29
  85. package/assets/patterns/ownership-scope-resolve.md +69 -0
  86. package/assets/patterns/permissions-procedure.md +35 -33
  87. package/assets/patterns/platform-access-di.md +39 -25
  88. package/assets/patterns/pricing-quote.md +71 -0
  89. package/assets/patterns/reuse-first-extend.md +22 -22
  90. package/assets/patterns/seo-page.md +52 -40
  91. package/assets/patterns/seo-verify.md +48 -29
  92. package/assets/patterns/shared-code-new.md +37 -31
  93. package/assets/patterns/spec-driven-domain.md +60 -37
  94. package/assets/patterns/spec-driven-rule.md +55 -40
  95. package/assets/patterns/styling-bem-component.md +43 -32
  96. package/assets/patterns/styling-bem-layout.md +30 -24
  97. package/assets/patterns/task-flow-close.md +154 -0
  98. package/assets/patterns/task-flow-resume.md +94 -0
  99. package/assets/patterns/task-flow-start.md +129 -0
  100. package/assets/patterns/testing-e2e.md +53 -51
  101. package/assets/patterns/testing-unit.md +70 -46
  102. package/assets/patterns/translations-key.md +32 -19
  103. package/assets/patterns/ts-procedure.md +24 -25
  104. package/assets/rules/angular-patterns.md +50 -27
  105. package/assets/rules/api-layer.md +46 -28
  106. package/assets/rules/browser-verification.md +67 -48
  107. package/assets/rules/component-structure.md +43 -27
  108. package/assets/rules/dependencies.md +66 -0
  109. package/assets/rules/doc-style.md +95 -39
  110. package/assets/rules/entity-conventions.md +78 -0
  111. package/assets/rules/entity-models.md +70 -0
  112. package/assets/rules/git-workflow.azure.md +116 -0
  113. package/assets/rules/git-workflow.github.md +123 -0
  114. package/assets/rules/git-workflow.gitlab.md +113 -0
  115. package/assets/rules/lib-layers.md +56 -30
  116. package/assets/rules/lists.md +73 -0
  117. package/assets/rules/navigation.md +78 -0
  118. package/assets/rules/ownership-scope.md +63 -0
  119. package/assets/rules/permissions.md +43 -25
  120. package/assets/rules/platform-access.md +57 -29
  121. package/assets/rules/pricing.md +64 -0
  122. package/assets/rules/reuse-first.md +57 -43
  123. package/assets/rules/seo.md +51 -30
  124. package/assets/rules/shared-code.md +51 -26
  125. package/assets/rules/spec-driven.md +107 -51
  126. package/assets/rules/styling-bem.md +54 -39
  127. package/assets/rules/task-flow.md +150 -0
  128. package/assets/rules/testing.md +78 -47
  129. package/assets/rules/translations.md +48 -31
  130. package/assets/rules/typescript-conventions.md +57 -27
  131. package/assets/skills/agent-kit.md +85 -0
  132. package/assets/skills/write-a-skill.md +108 -0
  133. package/assets/templates/gate-map.sh +23 -15
  134. package/assets/templates/implementation.md +14 -8
  135. package/assets/templates/pattern.md +1 -1
  136. package/assets/templates/project.sh +32 -19
  137. package/assets/templates/rule.md +2 -2
  138. package/assets/variants.json +20 -0
  139. package/assets/workflows/feature.js +134 -0
  140. package/assets/workflows/plan.js +150 -0
  141. package/bin/agent-kit.d.ts.map +1 -1
  142. package/bin/agent-kit.js +78 -5
  143. package/bin/agent-kit.js.map +1 -1
  144. package/bin/prompt.d.ts +5 -0
  145. package/bin/prompt.d.ts.map +1 -1
  146. package/bin/prompt.js +19 -7
  147. package/bin/prompt.js.map +1 -1
  148. package/index.d.ts +1 -0
  149. package/index.d.ts.map +1 -1
  150. package/index.js +1 -0
  151. package/index.js.map +1 -1
  152. package/lib/assets.d.ts +8 -3
  153. package/lib/assets.d.ts.map +1 -1
  154. package/lib/assets.js +13 -3
  155. package/lib/assets.js.map +1 -1
  156. package/lib/catalog.d.ts +52 -5
  157. package/lib/catalog.d.ts.map +1 -1
  158. package/lib/catalog.js +104 -16
  159. package/lib/catalog.js.map +1 -1
  160. package/lib/commands.d.ts +22 -1
  161. package/lib/commands.d.ts.map +1 -1
  162. package/lib/commands.js +202 -14
  163. package/lib/commands.js.map +1 -1
  164. package/lib/companion.d.ts +5 -1
  165. package/lib/companion.d.ts.map +1 -1
  166. package/lib/companion.js +29 -2
  167. package/lib/companion.js.map +1 -1
  168. package/lib/config.d.ts +26 -9
  169. package/lib/config.d.ts.map +1 -1
  170. package/lib/config.js +41 -15
  171. package/lib/config.js.map +1 -1
  172. package/lib/freshness.d.ts +14 -0
  173. package/lib/freshness.d.ts.map +1 -0
  174. package/lib/freshness.js +116 -0
  175. package/lib/freshness.js.map +1 -0
  176. package/lib/hooks-map.d.ts +27 -0
  177. package/lib/hooks-map.d.ts.map +1 -0
  178. package/lib/hooks-map.js +77 -0
  179. package/lib/hooks-map.js.map +1 -0
  180. package/lib/integrity.d.ts +36 -0
  181. package/lib/integrity.d.ts.map +1 -0
  182. package/lib/integrity.js +44 -0
  183. package/lib/integrity.js.map +1 -0
  184. package/lib/picker.d.ts +11 -1
  185. package/lib/picker.d.ts.map +1 -1
  186. package/lib/picker.js +44 -6
  187. package/lib/picker.js.map +1 -1
  188. package/lib/sync.d.ts +26 -0
  189. package/lib/sync.d.ts.map +1 -1
  190. package/lib/sync.js +59 -4
  191. package/lib/sync.js.map +1 -1
  192. package/lib/variants.d.ts +44 -0
  193. package/lib/variants.d.ts.map +1 -0
  194. package/lib/variants.js +82 -0
  195. package/lib/variants.js.map +1 -0
  196. package/package.json +1 -1
  197. package/rt-tools-agent-kit-0.5.0.tgz +0 -0
  198. package/assets/laws/admin-lists.md +0 -35
  199. package/assets/laws/admin-navigation.md +0 -38
  200. package/assets/patterns/git-workflow-commit.md +0 -175
  201. package/assets/rules/git-workflow.md +0 -106
  202. package/rt-tools-agent-kit-0.3.0.tgz +0 -0
@@ -2,89 +2,91 @@
2
2
  name: testing-e2e
3
3
  kind: pattern
4
4
  rule: testing
5
- description: Паттерн правила testing. Брать при правке и прогоне сквозных спек — что вообще закрывается сквозной спекой, прогон одним рабочим, стенд из прод-сборки под настоящим прокси, выключатели разрушающих спек. Не брать для юнитов — это паттерн testing-unit.
5
+ description: Паттерн правила testing. Брать при правке и прогоне сквозных спек в apps/site-e2e и apps/admin-e2e — что закрывается сквозной спекой, готовые команды прогона, стенд из прод-сборки под настоящим nginx, выключатели спек. Не брать для юнитов — это паттерн testing-unit.
6
6
  ---
7
7
 
8
8
  # Сквозные спеки
9
9
 
10
10
  Паттерн правила `testing`. Что при этом должно быть верно — закон
11
- `{{lawsDir}}/verifiability.md`.
11
+ `docs/constitution/verifiability.md`.
12
12
 
13
13
  ## Когда брать
14
14
 
15
- - Пишется или правится сквозная спека.
15
+ - Пишется или правится спека в `apps/site-e2e/src` или `apps/admin-e2e/src`.
16
16
  - Надо воспроизвести дефект, который виден только на прод-конфигурации.
17
- - Готовится стенд под прогон против прокси.
17
+ - Готовится стенд под прогон против nginx.
18
18
 
19
19
  ## Сквозной спекой закрывается накопленное состояние
20
20
 
21
- Случай для неё — тот, где состояние копится нажатиями: открытая панель, активный маршрут, страж
22
- прошлого экрана. Всё, что проверяется одним заходом по адресу, дешевле закрыть юнитом на чистой
23
- функции или замером экрана.
21
+ Случай для неё — тот, где состояние копится нажатиями: открытая панель, активный маршрут,
22
+ гард прошлого экрана. Всё, что проверяется одним заходом по адресу, дешевле закрыть юнитом на
23
+ чистой функции или замером экрана.
24
24
 
25
25
  Отсюда и способ: нажатия по тем же элементам, что нажимает владелец, несколько подряд, без
26
26
  перезагрузки между ними. Заход по прямому адресу поднимает приложение заново, накопленного
27
- состояния у него нет, и проход читается как «дефект не подтверждается».
27
+ состояния у него нет, и проход читается как «дефект не подтверждается» — так на прод уехала
28
+ панель, застревавшая в адресе при уходе в соседний раздел.
28
29
 
29
- Элементы находятся по якорям спек: классы меняются вместе с вёрсткой, а поиск по роли и тексту
30
+ Элементы находятся по `qa-dataid`: классы меняются вместе с вёрсткой, а поиск по роли и тексту
30
31
  ломается на переводах.
31
32
 
32
33
  ## Прогон
33
34
 
34
- По умолчанию конфиг идёт на порт разработки и подхватывает уже поднятый сервер. С заданным
35
- адресом стенда свой сервер не стартует вовсе.
36
-
37
- Полный набор спек с настоящей сессией гоняется **одним рабочим**: такие тесты правят одни и те
38
- же записи живого хранилища и в параллельном прогоне мешают друг другу. Одни и те же файлы дают
39
- падения на многих рабочих и ноль на одном.
35
+ ```bash
36
+ npx nx e2e site-e2e -- --project=chromium
37
+ npx nx e2e admin-e2e -- --project=chromium
38
+ BASE_URL=http://localhost:{{dockerSitePort}} npx nx e2e site-e2e -- --project=chromium # против внешнего стенда
39
+ ```
40
40
 
41
- ## Стенд из прод-сборки под настоящим прокси
41
+ По умолчанию конфиг идёт на {{sitePort}} и подхватывает уже поднятый сервер. С `BASE_URL` свой сервер
42
+ не стартует вовсе.
42
43
 
43
- Дешевле полного состава прода: один контейнер прокси с боевым конфигом, а приложения за ним —
44
- процессы на хосте.
44
+ Полный набор админки гоняется **одним воркером** (`--workers=1`): тесты с настоящей сессией
45
+ правят одни и те же объекты живой базы и в параллельном прогоне мешают друг другу. Одни и те
46
+ же файлы дали три падения на восьми воркерах и ноль на одном.
45
47
 
46
- ```bash
47
- <сборка всех приложений>
48
- <адрес хранилища> <порт> node <собранный сервер> &
49
- docker run -d --name <стенд> \
50
- --add-host <имя апстрима>:host-gateway \
51
- -p <внешний порт>:80 \
52
- -v "$PWD/<конфиг прокси>:/etc/nginx/conf.d/default.conf:ro" \
53
- -v "$PWD/<каталог сборки>:/usr/share/nginx/html:ro" \
54
- <образ прокси>
55
- ```
48
+ ## Стенд из прод-сборки под настоящим nginx
56
49
 
57
- Порты апстримов зашиты в конфиг именами менять их нельзя, подстановка хоста заменяет только
58
- адрес.
50
+ Как его поднять паттерн `browser-verification-stand`; там же сказано, почему главный конфиг
51
+ монтируется отдельной строкой и почему каталогом, а не файлом. Здесь важно одно: против такого
52
+ стенда прогон идёт с `BASE_URL`, и вместе с ним включаются проверки перенаправлений локалей.
59
53
 
60
54
  ## Выключатели
61
55
 
62
- ```
63
- const РАЗРЕШЕНО = окружение['<имя переменной>'] === '1';
64
- тест.пропустить(!РАЗРЕШЕНО, 'меняет живой адрес записи: включается переменной');
56
+ ```typescript
57
+ const ALLOW_RENAME: boolean = process.env['E2E_ALLOW_SLUG_RENAME'] === '1';
58
+ test.skip(!ALLOW_RENAME, 'меняет живой адрес объекта: включается E2E_ALLOW_SLUG_RENAME=1');
65
59
  ```
66
60
 
67
61
  - Спека, необратимо меняющая данные стенда, по умолчанию пропускается и включается своей
68
62
  переменной.
69
- - Проверки, которым нужен прокси, просыпаются вместе с адресом стенда. Голый сервер отдачи
70
- страниц их не проходит, и падения выглядят регрессией.
71
- - Правило линтера, запрещающее выключенный тест, снимается в конфиге: выключатель здесь — приём,
72
- а не забытый пропуск.
63
+ - `BEHIND_NGINX` в `home.smoke.spec.ts` это `!!process.env['BASE_URL']`: вместе с ним
64
+ просыпаются проверки перенаправлений локалей. Голый сервер отдачи страниц их не проходит, и
65
+ два падения выглядят регрессией.
66
+ - `HAS_ADMIN_SESSION` в `sign-in.ts` — пара `E2E_ADMIN_EMAIL` и `E2E_ADMIN_PASSWORD`.
67
+ - `playwright/no-skipped-test` выключен в `eslint.config.mjs`: выключатель теста здесь —
68
+ приём, а не забытый `test.skip`.
73
69
 
74
70
  ## Частые промахи
75
71
 
76
- - **Порт приложения занимать осторожно:** стенд разработчика ходит по тому же имени через
77
- подстановку хоста, и пока на нём висит чужой процесс, стенд отдаёт чужую сборку.
78
- - **Браузер обычно установлен один.** Ошибка «Executable doesn't exist» разобрана в правиле
79
- `testing`: она же приходит после смены версии прогонщика.
80
- - **Спеки без учётных данных пропускаются молча** прогон выглядит успешным, а проверено
81
- меньше половины.
82
- - **Конфиг прокси монтируется каталогом, а не одиночным файлом:** редактор пересоздаёт файл, и
72
+ - **Порт {{ssrPort}} занимать осторожно:** стенд разработчика на {{sitePort}} ходит по тому же имени
73
+ `ssr:{{ssrPort}}` через `host-gateway`, и пока на нём висит чужой процесс, стенд отдаёт чужую
74
+ сборку.
75
+ - Браузер стоит один chromium; узкий экран — `--project=mobile-chrome`. Ошибка «Executable
76
+ doesn't exist» разобрана в правиле `testing`: она же приходит после смены версии Playwright.
77
+ - Спеки админки без сессии пропускаются молча — прогон выглядит успешным, а проверено меньше
78
+ половины.
79
+ - Конфиг nginx монтируется каталогом, а не одиночным файлом: редактор пересоздаёт файл, и
83
80
  контейнеру остаётся обрезанная копия.
84
- - **Подменяется не только то, что запрашивает экран, но и то, что запрашивает шапка.** Общий
85
- запрос идёт на каждом экране; без подмены на него отвечает настоящий сервер, поддельный вход
86
- он отбивает, перехватчик сбрасывает сессию и десятки тестов падают на пропавшей шапке, что
87
- выглядит дефектом экрана.
88
- - **Каталог сборки, удалённый под смонтированным томом, оставляет контейнер с пустым корнем:**
89
- стенд отвечает отказом на всё, и падают сразу все тесты. Контейнер после такого удаления
90
- пересоздаётся.
81
+ - **Подменяется не только то, что запрашивает экран, но и то, что запрашивает шапка.** Счётчик
82
+ непрочитанного запрашивается на каждом экране под шапкой. Без подмены на этот запрос
83
+ отвечает настоящий сервер, поддельный вход он отбивает, интерцептор сбрасывает сессию, и
84
+ пятьдесят девять тестов падают на пропавшей шапке — выглядит это дефектом экрана.
85
+ - **Состояние, которое живёт только во время операции, тестом не проверяется.** Список заливки
86
+ под кнопкой виден, пока файлы летят: на подменённых ответах они долетают раньше, чем тест
87
+ успевает его прочитать, и тест краснеет через раз. Проверяют либо конечное состояние
88
+ (`data-state` строки стал `done`), либо то же промежуточное — но на ответе, который тест сам
89
+ задержал и сам отпускает.
90
+ - **Каталог сборки, удалённый под смонтированным томом, оставляет контейнер с пустым
91
+ корнем:** стенд отвечает 403 на всё, и падают сразу все тесты. Контейнер после
92
+ `rm -rf dist/apps/<приложение>` пересоздаётся.
@@ -2,92 +2,116 @@
2
2
  name: testing-unit
3
3
  kind: pattern
4
4
  rule: testing
5
- description: Паттерн правила testing. Брать при заведении или правке файла спеки рядом с исходником раскладка блоков, сборщик фикстур, идентификатор сценария в заголовке, спека обработчика серверной стороны с рукописным двойником хранилища, разовый тест-доказательство. Не брать для сквозных спек — это паттерн testing-e2e.
5
+ description: Паттерн правила testing. Брать при заведении или правке *.spec.ts под Vitest готовая раскладка describe и it, сборщик фикстур, идентификатор сценария в заголовке, спека процедуры Connect с рукописным двойником базы. Не брать для сквозных спек — это паттерн testing-e2e.
6
6
  ---
7
7
 
8
- # Спека на чистую функцию и на обработчик
8
+ # Спека на чистую функцию и на процедуру
9
9
 
10
10
  Паттерн правила `testing`. Что при этом должно быть верно — закон
11
- `{{lawsDir}}/verifiability.md`.
11
+ `docs/constitution/verifiability.md`.
12
12
 
13
13
  ## Когда брать
14
14
 
15
- - Заводится или правится файл спеки рядом с исходником.
16
- - Логику надо вынести из компонента или службы, чтобы её стало чем проверить.
17
- - Пишется спека на обработчик серверной стороны.
15
+ - Заводится или правится `*.spec.ts` рядом с исходником.
16
+ - Логику надо вынести из компонента или сервиса, чтобы её стало чем проверить.
17
+ - Пишется спека на процедуру Connect.
18
18
 
19
19
  ## Импорты явные
20
20
 
21
- Даже когда прогонщик кладёт свои имена в глобальную область, список пишется: файл, читаемый
22
- без конфига, не должен зависеть от настройки, о которой в нём ни слова.
21
+ `globals: true` в конфиге стоит, но список всё равно пишется:
23
22
 
24
- ## Один блок на функцию
23
+ ```typescript
24
+ import { describe, expect, it } from 'vitest';
25
+ ```
25
26
 
26
- Имя блока совпадает с именем функции дословно; заголовки тестов — предложения настоящим
27
- временем, о поведении, а не об устройстве:
27
+ ## Один `describe` на функцию
28
28
 
29
- ```
30
- описание('applyDayClick', () => {
31
- тест('SC-<ПРЕФИКС>-19 — нажатие по занятому дню ничего не меняет', () => {
32
- ожидать(applyDayClick(day('2026-08-12'), selection)).равно(selection);
29
+ Имя блока совпадает с именем функции дословно; заголовки `it` — предложения по-русски,
30
+ настоящим временем, о поведении, а не об устройстве:
31
+
32
+ ```typescript
33
+ describe('applyDayClick', () => {
34
+ it('SC-BK-19 — клик по занятому дню ничего не меняет', () => {
35
+ expect(applyDayClick(day('2026-08-12'), selection)).toEqual(selection);
33
36
  });
34
37
  });
35
38
  ```
36
39
 
37
- Идентификатор сценария из спека домена стоит в начале заголовка, через тире. Краевые случаи —
38
- отдельные тесты в том же блоке, а не один тест с десятком проверок.
40
+ Идентификатор сценария из `docs/specs/<домен>/scenarios.md` стоит в начале заголовка, через
41
+ тире. Краевые случаи — отдельные `it` в том же блоке, а не один тест с десятком проверок.
39
42
 
40
- ## Фикстура собирается функцией с частичной подменой
43
+ ## Фикстура собирается функцией с `Partial<T>`
41
44
 
42
- Не повторяющимся литералом: литерал, размноженный по файлу, при первой же новой обязательной
43
- колонке правится в каждом месте — и в одном обязательно забывается.
45
+ Не повторяющимся литералом:
44
46
 
45
- ```
46
- функция day(iso, подмена = {}) {
47
- вернуть { iso, dayOfMonth: число(iso.срез(8)), busyNight: занято(iso), past: ложь, ...подмена };
47
+ ```typescript
48
+ function day(iso: string, overrides: Partial<ICalendarDay> = {}): ICalendarDay {
49
+ return {
50
+ iso,
51
+ dayOfMonth: Number(iso.slice(8)),
52
+ priceThb: 6000,
53
+ busyNight: isNightBusy(iso, BUSY),
54
+ past: false,
55
+ ...overrides,
56
+ };
48
57
  }
49
58
  ```
50
59
 
51
- Общие константы лежат наверху файла, в области модуля.
60
+ Общие константы (`PRICING`, `BUSY`) лежат наверху файла, в области модуля.
52
61
 
53
62
  ## Решение выносится в чистую функцию
54
63
 
55
- Логика уезжает в отдельный файл рядом и проверяется вызовом без подъёма каркаса и без
56
- подмены зависимостей. Компонент и служба остаются обёрткой, у которой своего ветвления нет.
64
+ Господствующая форма в этом дереве: логика уезжает в `*.logic.ts`, `*.util.ts` или
65
+ `*.calculator.ts`, и проверяется вызовом без `TestBed`, без подмены зависимостей. Образцы
66
+ `libs/site/common/booking/util/src/lib/availability-calendar.logic.ts` и
67
+ `libs/api/<домен расчёта>/util/src/lib/quote.calculator.ts`.
57
68
 
58
- ## Обработчик зовётся напрямую
69
+ ## Процедура зовётся напрямую
59
70
 
60
- Обработчик — метод класса; двойник хранилища пишется руками. Проверяются порядок действий,
61
- откат при отказе половины, идемпотентность повтора и то, что именно ушло в хранилище. Раскладку
62
- полей стерегут тесты слоя обращения к серверу, и здесь она не повторяется.
71
+ Обработчик — метод `handle` класса процедуры в слое `feature` своего домена. Двойник базы
72
+ пишется руками; образец `FakePrismaClient` в
73
+ `libs/api/<домен заявок>/feature/src/lib/link-booking.procedure.spec.ts`:
74
+
75
+ ```typescript
76
+ const prisma: FakePrismaClient = new FakePrismaClient();
77
+ await procedureWith(prisma, emitter).handle(request());
78
+ ```
79
+
80
+ Проверяются порядок действий, откат при отказе половины, идемпотентность повтора и то, что
81
+ именно ушло в базу. Раскладку полей стерегут тесты слоя `api`, и здесь она не повторяется.
63
82
 
64
83
  ## Разовый тест-доказательство
65
84
 
66
85
  Дефект, который иначе подтверждается только чтением кода, доказывается тестом, написанным на
67
- время разбора: он поднимает настоящий обработчик, подменяет его единственный выход наружу и
86
+ время разбора: он поднимает настоящую процедуру, подменяет её единственный выход наружу и
68
87
  считает походы.
69
88
 
70
- Число до правки и число после — единственная форма ответа, которую такая проверка даёт: «сто из
71
- ста» и «двадцать из ста» различимы, а «код выглядит правильным» — нет.
89
+ ```typescript
90
+ globalThis.fetch = (): Promise<Response> => Promise.resolve(Response.json({ success: false }));
91
+ ```
92
+
93
+ Число до правки и число после — единственная форма ответа, которую такая проверка даёт: «сто
94
+ походов из ста» и «двадцать из ста» различимы, а «код выглядит правильным» — нет.
72
95
 
73
- Второй способ снять «до» — вернуть на место версию файла из точки расхождения ветки и прогнать
74
- тест ветки: падение теста и есть воспроизведение дефекта. Файл после этого восстанавливается
75
- копией, и восстановление проверяется прогоном того же теста, а не памятью.
96
+ Второй способ снять «до» — вернуть на место версию файла из точки расхождения ветки и
97
+ прогнать тест ветки: падение теста и есть воспроизведение дефекта. Файл после этого
98
+ восстанавливается копией, и восстановление проверяется прогоном того же теста, а не памятью.
76
99
 
77
- Двойники соседей не выдумываются: рабочий набор берётся из теста соседнего обработчика того же
100
+ Двойники соседей не выдумываются: рабочий набор берётся из теста соседней процедуры того же
78
101
  домена. Двойник, собранный по типу, падает не на утверждении, а на вызове метода, которого у
79
102
  него нет.
80
103
 
81
104
  **Такой файл не коммитится.** Он живёт до конца разбора и удаляется вместе с ним; проверка,
82
- которую стоит оставить, переписывается в обычную спеку с идентификатором сценария в заголовке.
83
- Признак временного файла — его заголовок не называет ни одного сценария.
105
+ которую стоит оставить, переписывается в обычный `*.spec.ts` с идентификатором сценария в
106
+ заголовке и едет в ветке. Признак временного файла — его заголовок не называет ни одного
107
+ сценария.
84
108
 
85
109
  ## Частые промахи
86
110
 
87
- - **Либа без своего конфига прогонщика проходит зелёной, не запустив ни одного файла.** Прежде
88
- чем писать первый тест в либе, проверить, что конфиг рядом есть.
89
- - **Подмена модуля скрыла бы то, ради чего тест и заводится** — какой именно вызов ушёл в
90
- хранилище и в каком порядке. Двойник пишется руками.
91
- - **Своих помощников для проверок не заводить** утверждения пишутся прямо.
92
- - **Заголовок с несуществующим идентификатором сценария роняет проверку спеков**, а сами тесты
93
- при этом остаются зелёными.
111
+ - Либа без своего `vitest.config.mts`: `nx test <проект>` пройдёт зелёным, не запустив ни
112
+ одного файла. Прежде чем писать первый тест в либе, проверить, что конфиг рядом есть.
113
+ - Подмена модуля (`vi.mock`) скрыла бы то, ради чего тест и заводится, — какой именно вызов
114
+ ушёл в базу и в каком порядке. Двойник пишется руками.
115
+ - Своих помощников для проверок не заводить: `expect(...).toBe(...)` и `.toEqual(...)` прямо.
116
+ - Заголовок с несуществующим идентификатором сценария роняет `npm run check:specs`, а сами
117
+ тесты при этом остаются зелёными.
@@ -2,12 +2,13 @@
2
2
  name: translations-key
3
3
  kind: pattern
4
4
  rule: translations
5
- description: Паттерн правила translations. Брать, когда в интерфейсе появляется видимый текст — куда положить ключ, как подставить его в разметку и в класс, чем дозаполнить остальные локали и чем проверить полноту. Не брать для перевода содержимого записи — его заполняет серверная сторона.
5
+ description: Паттерн правила translations. Брать, когда в интерфейсе появляется видимый текст — куда положить ключ, как подставить его в разметку и в класс, чем дозаполнить остальные семь локалей и чем проверить полноту. Не брать для перевода контента объекта — его заполняет бэкенд.
6
6
  ---
7
7
 
8
8
  # Ключ перевода
9
9
 
10
- Паттерн правила `translations`. Что при этом должно быть верно — закон `{{lawsDir}}/locales.md`.
10
+ Паттерн правила `translations`. Что при этом должно быть верно — закон
11
+ `docs/constitution/application/locales.md`.
11
12
 
12
13
  ## Когда брать
13
14
 
@@ -16,36 +17,48 @@ description: Паттерн правила translations. Брать, когда
16
17
 
17
18
  ## Куда кладётся ключ
18
19
 
19
- Словари разложены по разделам, и наборы ключей сверяются **внутри раздела**: общее для всех
20
- приложений, публичная часть, внутренняя часть, письма. Ключ заводится во всех локалях сразу —
21
- пустое значение считается пропуском, а не переводом.
20
+ `libs/common/i18n/src/lib/dictionaries/<локаль>/<раздел>.json`. Разделов четыре, и наборы
21
+ ключей сверяются внутри раздела:
22
+
23
+ | Раздел | Что в нём |
24
+ | ------------- | --------------------------- |
25
+ | `common.json` | общее для сайта и админки |
26
+ | `site.json` | публичный сайт |
27
+ | `admin.json` | админ-панель |
28
+ | `mail.json` | письма и документ-основание |
29
+
30
+ Локалей восемь: `en`, `ru`, `de`, `zh-Hans`, `zh-Hant`, `ko`, `th`, `hi`. Ключ заводится во
31
+ всех — пустое значение считается пропуском, а не переводом.
22
32
 
23
33
  ## Подстановка
24
34
 
25
- В разметке — преобразователем, в классе — реактивным значением:
35
+ В разметке — пайпом:
26
36
 
27
37
  ```html
28
- <h1 rtElem="title">{{ '<ключ заголовка>' | <перевод> }}</h1>
29
- <table [emptyMessage]="'<ключ пустого списка>' | <перевод>"></table>
38
+ <h1 rtElem="title">{{ 'bookingsTitle' | transloco }}</h1>
39
+ <<префикс>-table [emptyMessage]="'bookingsEmpty' | transloco" [ariaLabel]="'bookingsTableAria' | transloco">
30
40
  ```
31
41
 
42
+ В классе — сигналом:
43
+
32
44
  ```typescript
33
- protected readonly title: Signal<string> = translateSignal('<ключ заголовка>');
45
+ protected readonly title: Signal<string> = translateSignal('promoCodesTitle');
34
46
  ```
35
47
 
36
48
  ## Дозаполнить и проверить
37
49
 
38
- Дозаполнение недостающего делается командой, а полноту держит тест: он роняет прогон на
39
- недостающем или пустом ключе. Глазами это не проверяется — ключей тысячи.
50
+ ```bash
51
+ npm run i18n:fill # дозаполняет недостающее в остальных локалях
52
+ npx nx test common-i18n # роняет сборку на недостающем или пустом ключе
53
+ ```
40
54
 
41
55
  ## Частые промахи
42
56
 
43
- - **Текст строкой прямо в шаблоне:** он уедет в интерфейс на одном языке во всех локалях.
44
- - **Ключ заведён только в паре локалей:** тест полноты падает, но замечают это уже в гейте
45
- пуша.
46
- - **Пустая строка вместо перевода:** на экране она выглядит как задуманная — кнопка без
47
- подписи, заголовок без текста.
48
- - **Ключ положен не в свой раздел:** наборы сверяются внутри раздела, и расхождение вылезет как
57
+ - Текст строкой прямо в шаблоне: он уедет в интерфейс по-английски во всех локалях перевода.
58
+ - Ключ заведён только в `en` и `ru`: тест полноты падает, но замечают это уже в гейте пуша.
59
+ - Пустая строка вместо перевода: на экране она выглядит как задуманная — кнопка без подписи,
60
+ заголовок без текста.
61
+ - Ключ положен не в свой раздел: наборы сверяются внутри раздела, и расхождение вылезет как
49
62
  недостача в другом.
50
- - **Свой ключ успеха у панели правки записи:** текст успеха принадлежит самой операции и
51
- заводится во всех локалях сразу.
63
+ - Свой ключ успеха у панели правки записи: текст успеха принадлежит мутации, и заводится он во
64
+ всех локалях перевода сразу.
@@ -2,30 +2,30 @@
2
2
  name: ts-procedure
3
3
  kind: pattern
4
4
  rule: typescript-conventions
5
- description: Паттерн правила typescript-conventions. Брать при заведении или правке обработчика серверной стороны — класс с полем метода контракта и методом обработки, зависимости конструктором, имя файла и класса, почему форма именно такая. Не брать для объявления доступа — это паттерн permissions-procedure.
5
+ description: Паттерн правила typescript-conventions. Брать при заведении или правке процедуры Connect на бэкенде готовый класс с полем method и методом handle, зависимости конструктором, имя файла и класса, почему форма именно такая. Не брать для объявления доступа к процедуре — это паттерн permissions-procedure.
6
6
  ---
7
7
 
8
- # Обработчик серверной стороны
8
+ # Процедура Connect
9
9
 
10
10
  Паттерн правила `typescript-conventions`. Что при этом должно быть верно — закон
11
- `{{lawsDir}}/code-structure.md`.
11
+ `docs/constitution/code-structure.md`.
12
12
 
13
13
  ## Когда брать
14
14
 
15
- - Заводится новый обработчик серверной стороны.
16
- - Правится тело существующего.
15
+ - Заводится новая процедура бэкенда.
16
+ - Правится тело существующей.
17
17
  - Домен переезжает на вертикальную нарезку.
18
18
 
19
- ## Один обработчик — один класс
19
+ ## Одна процедура — один класс
20
20
 
21
- Файл рядом со своим доменом, класс с публичным полем метода контракта и публичным методом
22
- обработки. Зависимости приходят конструктором: на серверной стороне внедрение конструкторное, и
23
- функции внедрения фронта там нет.
21
+ Файл `<процедура>.procedure.ts` в слое `feature` своего домена, класс `<Процедура>Procedure`,
22
+ публичное поле `method` с дескриптором из контракта и публичный метод `handle` с телом.
23
+ Зависимости приходят конструктором — на бэкенде DI нестовский, `inject()` там нет.
24
24
 
25
25
  ```typescript
26
26
  @Injectable()
27
27
  @ConnectProcedure()
28
- @RequiresPermission('<ресурс>:<действие>')
28
+ @RequiresPermission('bookings:manage')
29
29
  export class PingProcedure implements IConnectProcedure<typeof HealthService.method.ping> {
30
30
  readonly #health: HealthCheckService;
31
31
 
@@ -45,22 +45,21 @@ export class PingProcedure implements IConnectProcedure<typeof HealthService.met
45
45
 
46
46
  ## Почему форма такая
47
47
 
48
- Прежняя — регистрация с телами в замыканиях внутри вызова роутера — делала обработчик
49
- недостижимым для спеки: наружу торчал только метод регистрации. А регистрация сервиса целиком
50
- заглушает каждый непереданный метод ответом «не реализовано», поэтому один сервис контракта не
51
- мог обслуживаться двумя доменами.
48
+ Прежняя — `register(router)` с телами в замыканиях внутри `router.service(...)` — делала
49
+ обработчик недостижимым для спеки: наружу торчал только класс с методом `register`. А
50
+ `router.service` заглушает каждый непереданный метод ответом `Unimplemented`, поэтому один
51
+ proto-сервис не мог обслуживаться двумя доменами.
52
52
 
53
- Класс решает и то, и другое: метод обработки зовётся спекой напрямую, а реестр кладёт
54
- обработчики поштучно.
53
+ Класс решает и то, и другое: `handle` зовётся спекой напрямую, а реестр кладёт процедуры
54
+ поштучно через `router.rpc`.
55
55
 
56
56
  ## Частые промахи
57
57
 
58
- - **Третий суффикс имени файла:** прежние имена уходят вместе с последним переехавшим доменом,
59
- и новых таких файлов не заводится.
60
- - **Функция внедрения фронта в классе обработчика:** зависимости идут конструктором.
61
- - **Тело в замыкании внутри регистрации:** спека до него не дотянется.
62
- - **Обработчик без объявления доступа:** приложение не поднимется.
63
- - **Приведение к типу в переводе моделей:** на серверной стороне оно запрещено так же, как во
64
- фронтовом переводчике.
65
- - **Свой тип у результата группирующей выборки:** он условный, собирается из аргументов вызова
66
- и с выписанным руками не сходится.
58
+ - Третий суффикс: `*.rpc.ts` и `*.connect.ts` — прежние имена, они уходят вместе с последним
59
+ переехавшим доменом, и новых таких файлов не заводится.
60
+ - `inject()` в классе процедуры: на бэкенде зависимости идут конструктором.
61
+ - Тело в замыкании внутри регистрации роутера: спека до него не дотянется.
62
+ - Процедура без объявления доступа: приложение не поднимется.
63
+ - Приведение `as` в переводе моделей: на бэкенде оно запрещено так же, как в маппере фронта.
64
+ - Свой тип у результата `groupBy` Prisma: он условный, собирается из аргументов вызова и с
65
+ выписанным руками не сходится. Там, где ключей единицы, идёт `count` на ключ в `Promise.all`.
@@ -2,51 +2,74 @@
2
2
  name: angular-patterns
3
3
  kind: rule
4
4
  law: frontend-application
5
- description: Правило под закон «Фронтовое приложение». Брать при правке любого класса фронтового каркаса — компонента, стора, сервиса, директивы, преобразователя, стража, перехватчика. Реактивное состояние вместо ручного пересчёта, перерисовка по требованию, место подписки и её владелец. Не действует на серверной стороне. Готовый код — в паттерне angular-patterns-state. Чем это названо здесь — в implementation.md рядом.
5
+ description: Правило под «Закон о фронтовом приложении». Брать при правке любого класса Angular — компонента, стора админки, сервиса, директивы, пайпа, гарда, интерцептора. Называет сигнальный API входов, OnPush, zoneless, inject и место, где живёт подписка. Не действует под libs/api и apps/api. Готовый код — в паттерне angular-patterns-state.
6
6
  ---
7
7
 
8
- # Реактивность экрана — каким приёмом
8
+ # Реактивность экрана — как это устроено здесь
9
9
 
10
- Правило под закон `{{lawsDir}}/frontend-application.md`. Закон говорит, что должно быть верно;
11
- здесь — каким приёмом это держится. Чем названы реактивное значение, входы и выходы и как
12
- зовётся гашение подписки — `implementation.md` рядом. Раскладка файла компонента
13
- `component-structure`, оформление — `styling-bem`, окружение браузера — `platform-access`, слой
10
+ Правило под закон `docs/constitution/frontend-application.md`. Закон говорит, что должно быть
11
+ верно; здесь — на чём это стоит в этом дереве. Раскладка файла компонента
12
+ `component-structure`, стили — `styling-bem`, окружение браузера `platform-access`, слой
14
13
  обращения к серверу — `api-layer`. Все пять под одним законом.
15
14
 
16
- Правило про фронт: на серверной стороне своя среда, и ничего из перечисленного к ней не
17
- относится.
15
+ Правило про фронт: `libs/api/**` и `apps/api/**` — это NestJS, там своя среда, и ничего из
16
+ перечисленного не применяется.
18
17
 
19
- ## Когда берётся
18
+ ## Как это называется здесь
20
19
 
21
- Правка любого класса фронтового каркаса: компонента, стора, сервиса, директивы,
22
- преобразователя, стража, перехватчика.
20
+ | В законе | Здесь |
21
+ | --------------------------------------- | -------------------------------------------------------------------------------------------------- |
22
+ | состояние, которое пересчитывается само | `signal()` и `computed()`; Angular без Zone.js (`provideZonelessChangeDetection()`) |
23
+ | вход и выход компонента | `input()`, `input.required()`, `output()`; `viewChild()`, `contentChild()` и их множественные пары |
24
+ | перерисовка по требованию | `ChangeDetectionStrategy.OnPush` — на каждом компоненте |
25
+ | владелец подписки | `takeUntilDestroyed(this.#destroyRef)` |
26
+ | источник действия | `Subject` с суффиксом `Source` в имени поля |
23
27
 
24
- ## Что здесь действует
28
+ ## Где это лежит
25
29
 
26
- - **Подписка объявляется один раз, а не в методе действия.** Метод толкает значение в источник,
27
- а долгоживущая подписка с переключением, отбрасыванием или очередью объявляется при создании
28
- владельца.
29
- - **Подписка гасится вместе с владельцем.** Гашение ставится в тот же поток, где объявлена
30
- подписка, иначе поток переживает экран, на котором заведён.
31
- - **Источник действия носит суффикс в имени.** Иначе поток и значение в коде неотличимы, и
32
- проталкивание уходит не туда.
30
+ В этом дереве таблица в `implementation.md` рядом. Пути живут там, а не здесь: правило
31
+ переносится между репозиториями, раскладка нет, и путь, названный в правиле, врёт в первом
32
+ же дереве, которое держит код иначе.
33
+
34
+ ## Как закон применяется здесь
35
+
36
+ - **Подписка объявляется один раз, а не в методе действия.** Метод толкает значение в
37
+ источник, а долгоживущая подписка со `switchMap`, `exhaustMap` или `concatMap` объявляется в
38
+ конструкторе, в `ngOnInit` или в инициализаторе поля.
39
+ - **Подписка гасится вместе с владельцем.** `takeUntilDestroyed` ставится в тот же поток, где
40
+ объявлена подписка.
41
+ - **Источник действия носит суффикс `Source` в имени.** Иначе поток и значение в коде
42
+ неотличимы, и `next` уходит не туда.
33
43
  - **Списочный стор наследует общую основу.** Записи, страница, порядок, условия отбора, строка
34
- поиска и настройка выборки уже там, и наследнику остаются несколько строк.
44
+ поиска и конфиг выборки уже там, и наследнику остаются четыре строки.
45
+
46
+ ## Чего из закона здесь нет
47
+
48
+ Ни `OnPush`, ни сигнальный API входов, ни отсутствие геттеров в компонентах не проверяет
49
+ ничто: `@Input()` и геттер компилируются и работают, а расхождение видно только чтением.
50
+ Обращение к окружению браузера тоже не проверяется — это `Q-FA-1` в законе.
35
51
 
36
52
  ## Паттерны
37
53
 
38
- - `angular-patterns-state` — реактивные значения, производные, состояние сервиса, подписка.
54
+ - `angular-patterns-state` — сигналы, производные значения, состояние сервиса, подписка.
39
55
 
40
56
  ## Ловушки
41
57
 
42
- - **Производное значение объявляется вычисляемым, а не эффектом.** Эффект, кладущий значение в
43
- реактивное поле, — это ручной пересчёт, и он рано или поздно отстаёт от источника.
58
+ - **Производное значение считается `computed`, а не эффектом.** `effect`, кладущий значение в
59
+ сигнал, — это ручной пересчёт, и он рано или поздно отстаёт от источника.
44
60
  - **Геттеров в компонентах нет.** Геттер пересчитывается на каждой перерисовке, и цена его не
45
61
  видна ни в одном месте кода.
62
+ - **Статический атрибут без значения задаёт входу пустую строку, а не умолчание.**
63
+ `<ng-template someControl>` даёт `''`, и вход с осмысленным умолчанием молча его теряет;
64
+ сигнальный вход с алиасом здесь ничем не отличается от `@Input()`. Вход, у которого умолчание
65
+ что-то значит, приводит пустую строку к нему сам — `transform` или проверка в `computed`.
46
66
  - **Подписка на каждый вызов метода не даёт выбрать, что делать с предыдущим запросом.**
47
67
  Быстрые нажатия дают гонку ответов, и побеждает тот, что вернулся последним, а не тот, что
48
68
  нажали последним.
49
- - **Запрет подписки в методе идёт по имени, а не по типу.** Вызов с таким именем у чего угодно
50
- считается подпиской, а взятие метода без вызова — нет.
51
- - **Инициализация разметки после первой отрисовки делается своим крючком, а не крючком
52
- жизненного цикла представления.** Когда страницу отдаёт сервер, разметка появляется позже.
69
+ - Запрет подписки в методе идёт по имени `subscribe`, а не по типу: вызов с таким именем у
70
+ чего угодно считается подпиской, а `const fn = stream$.subscribe` без вызова — нет.
71
+ Разрешены конструктор, `ngOnInit`, инициализатор поля и всё, что объявлено вне класса;
72
+ запрещены остальные методы, включая приватные с `#`, геттеры и `ngAfterViewInit`.
73
+ - Инициализация DOM после первой отрисовки — `afterNextRender()`, а не `ngAfterViewInit`:
74
+ сайт отдаётся сервером, и DOM там появляется позже.
75
+ - `viewChild` на поле с `#` Angular не принимает — поле объявляется `protected`.