@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
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: ownership-scope-resolve
3
+ kind: pattern
4
+ rule: ownership-scope
5
+ description: Паттерн правила ownership-scope. Брать, когда процедура принимает идентификатор владеющей сущности — готовое разрешение одной и всех действующих, коды отказа на каждый случай, условие отбора вместо своего поля запроса. Не брать для объявления доступа к процедуре — это паттерн permissions-procedure.
6
+ ---
7
+
8
+ # Разрешение владеющей сущности в процедуре
9
+
10
+ Паттерн правила `ownership-scope`. Что при этом должно быть верно — закон
11
+ `docs/constitution/application/ownership.md`.
12
+
13
+ ## Когда брать
14
+
15
+ - Процедура принимает идентификатор владеющей сущности.
16
+ - Экран или сводка отвечает сразу по всем действующим.
17
+ - Заводится новый домен, записи которого принадлежат владеющей сущности.
18
+
19
+ ## Одна сущность
20
+
21
+ ```typescript
22
+ const ownerId: string = await resolveRequestedOwner(this.#db, request.ownerId);
23
+ ```
24
+
25
+ Обёртка сама разбирает случаи и переводит их в отказ:
26
+
27
+ | Что пришло | Что происходит |
28
+ | ----------------------------------------------------- | ------------------------- |
29
+ | пусто, действующая одна | берётся она |
30
+ | пусто, действующих две и больше | `Code.InvalidArgument` |
31
+ | пусто, действующих нет вовсе | `Code.FailedPrecondition` |
32
+ | названная явно, есть — хоть действующая, хоть скрытая | берётся она |
33
+ | названная явно, такой нет | `Code.NotFound` |
34
+
35
+ Своего разбора не заводить: чистая функция разбирает случай, обёртка ходит в хранилище. Вторая
36
+ реализация того же правила однажды уже жила у обработчика статистики соседнего домена и успела
37
+ разойтись порядком выборки.
38
+
39
+ ## Все действующие
40
+
41
+ Там, где показана сводка, пустой идентификатор означает все действующие:
42
+
43
+ ```typescript
44
+ const ownerIds: string[] = await resolveRequestedOwners(this.#db, request.ownerId);
45
+ ```
46
+
47
+ Так отвечают списки, статистика и дашборд.
48
+
49
+ ## Отбор вместо своего поля запроса
50
+
51
+ У списка, переведённого на общую выборку, сущность приходит условием отбора, а не отдельным
52
+ полем: прежний номер поля в контракте помечен `reserved`.
53
+
54
+ ```
55
+ reserved 4;
56
+ reserved "owner_id";
57
+ ```
58
+
59
+ ## Частые промахи
60
+
61
+ - Свой разбор пустого идентификатора в обработчике: он разойдётся с общим при первой же правке.
62
+ - Один и тот же код отказа на «действующих нет» и «такой сущности нет»: для вызывающего это
63
+ разные случаи.
64
+ - Подстановка скрытой, когда действующих нет: скрытая вместо отсутствующей действующей не
65
+ берётся.
66
+ - Общая настройка, действующая сразу на все: заведение второй не должно менять поведение
67
+ первой.
68
+ - Новое поле идентификатора рядом с выборкой у списка: отбор, живущий отдельно, не виден ни
69
+ стору, ни адресу.
@@ -2,68 +2,70 @@
2
2
  name: permissions-procedure
3
3
  kind: pattern
4
4
  rule: permissions
5
- description: Паттерн правила permissions. Брать при заведении обработчика серверной стороны и при закрытии раздела интерфейсаметки доступа, отбивка без входа и без права, декларация пункта меню с правом и признаком незавершённости.
5
+ description: Паттерн правила permissions. Брать при заведении процедуры Connect и при закрытии раздела админкиготовые декораторы доступа, отбивка без входа и без права, декларация пункта меню с правом и флагом. Не брать для устройства самого меню — это правило navigation.
6
6
  ---
7
7
 
8
8
  # Объявление доступа
9
9
 
10
- Паттерн правила `permissions`. Что при этом должно быть верно — закон `{{lawsDir}}/access.md`.
10
+ Паттерн правила `permissions`. Что при этом должно быть верно — закон
11
+ `docs/constitution/application/access.md`.
11
12
 
12
13
  ## Когда брать
13
14
 
14
- - Заводится обработчик серверной стороны.
15
- - Раздел интерфейса закрывается правом.
16
- - Обработчик должен отвечать гостю.
15
+ - Заводится процедура Connect.
16
+ - Раздел админки закрывается правом.
17
+ - Процедура должна отвечать гостю.
17
18
 
18
- ## Метка на классе обработчика
19
+ ## Декоратор на классе процедуры
19
20
 
20
21
  Объявление ровно одно; без него приложение не поднимается:
21
22
 
22
23
  ```typescript
23
24
  @Injectable()
24
25
  @ConnectProcedure()
25
- @RequiresPermission('<ресурс>:<действие>')
26
- export class LinkEntityProcedure implements IConnectProcedure<typeof DomainService.method.linkEntity> {
27
- public readonly method: typeof DomainService.method.linkEntity = DomainService.method.linkEntity;
26
+ @RequiresPermission('chat:manage')
27
+ export class LinkBookingProcedure implements IConnectProcedure<typeof ChatService.method.linkBooking> {
28
+ public readonly method: typeof ChatService.method.linkBooking = ChatService.method.linkBooking;
28
29
  }
29
30
  ```
30
31
 
31
- | Метка | Кому доступно |
32
- | -------------------------------- | ------------------------------------------------- |
33
- | требование права | вошедшему с этим правом |
34
- | требование входа | любому вошедшему; так живут профиль и выбор языка |
35
- | публичный доступ | гостю без входа |
36
- | публичный доступ с чтением входа | гостю, но токен читается, если он есть |
32
+ | Декоратор | Кому доступно |
33
+ | -------------------------------------------- | ------------------------------------------------- |
34
+ | `@RequiresPermission('<ресурс>:<действие>')` | вошедшему с этим правом |
35
+ | `@RequiresAuth('<причина>')` | любому вошедшему; так живут профиль и выбор языка |
36
+ | `@PublicProcedure('<причина>')` | гостю без входа |
37
+ | `@OptionalAuthProcedure('<причина>')` | гостю, но токен читается, если он есть |
37
38
 
38
- Аргумент — причина для читателя кода. Ни в ответ, ни в журнал она не уходит.
39
+ Аргумент — причина для читателя кода. Ни в ответ, ни в лог она не уходит.
39
40
 
40
41
  ## Отбивка
41
42
 
42
- Перехватчик отвечает до тела обработчика:
43
+ Перехватчик отвечает до тела процедуры:
43
44
 
44
- - нет входа там, где вход нужен, — неаутентифицирован;
45
- - вход есть, права нет — отказ в доступе;
46
- - обработчик, о котором перехватчик ничего не знает, — тоже отказ, а не пропуск.
45
+ - нет входа там, где вход нужен, — `Code.Unauthenticated`;
46
+ - вход есть, права нет — `Code.PermissionDenied`;
47
+ - процедура, о которой перехватчик ничего не знает, — тоже отказ в доступе, а не пропуск.
47
48
 
48
- Сам обработчик решения о допуске не принимает.
49
+ Обработчик решения о допуске не принимает.
49
50
 
50
- ## Раздел интерфейса
51
+ ## Раздел админки
51
52
 
52
- Пункт меню и адрес закрываются одной декларацией: шапка берёт из неё подписи и адреса, страж —
53
- права. Второго объявления этой связи не заводится.
53
+ Пункт меню и адрес закрываются одной декларацией в
54
+ `libs/admin/common/container/util`: шапка берёт из неё подписи и адреса, гвард — права.
55
+ Второго объявления этой связи не заводится.
54
56
 
55
- Закрытие двухслойное: право пользователя и признак незавершённости раздела. Пункт с признаком
56
- объявляется без прав и без адреса — право открывает экран, а экрана нет. Появится экран —
57
- признак снимается, права добавляются.
57
+ Гейтинг двухслойный: право пользователя и флаг раздела. Пункт с флагом объявляется без прав и
58
+ без адреса — право открывает экран, а экрана нет. Появится экран — флаг снимается, права
59
+ добавляются.
58
60
 
59
61
  ## Частые промахи
60
62
 
61
- - **Два объявления доступа на одном обработчике:** приложение не поднимется, и увидено это
62
- будет только при запуске.
63
- - **Проверка права внутри тела:** право проверяется до тела.
64
- - **Своё объявление прав рядом с маршрутами:** оно разойдётся с декларацией меню, и получится
63
+ - Два объявления доступа на одной процедуре: приложение не поднимется, и увидено это будет
64
+ только при запуске.
65
+ - Проверка права внутри `handle`: право проверяется до тела.
66
+ - Своё объявление прав рядом с маршрутами: оно разойдётся с декларацией меню, и получится
65
67
  «пункта не видно, а страница открывается».
66
- - **Страж, повешенный на защищённую группу целиком:** он отрабатывает один раз за загрузку
68
+ - Гвард, повешенный на защищённую группу целиком: он отрабатывает один раз за загрузку
67
69
  страницы и переходов между разделами не видит.
68
- - **Ожидание прав, которое роняется на отказе запроса:** с неизвестными правами не закрывается
70
+ - Ожидание прав, которое роняется на отказе запроса: с неизвестными правами не закрывается
69
71
  ничего, и пустая шапка выхода владельцу не оставляет.
@@ -2,23 +2,28 @@
2
2
  name: platform-access-di
3
3
  kind: pattern
4
4
  rule: platform-access
5
- description: Паттерн правила platform-access. Брать, когда в код заходит окно, документ или проверка среды — внедрение токенов, приведение к глобальной области, окно параметром в чистой функции, работа с разметкой после первой отрисовки. Не брать на серверной стороне.
5
+ description: Паттерн правила platform-access. Брать, когда в код заходит окно, документ или проверка среды — готовые инжекты токенов, приведение к Window & typeof globalThis, окно параметром в чистой функции, инициализация DOM после первой отрисовки. Не брать под libs/api и apps/api.
6
6
  ---
7
7
 
8
8
  # Окно, документ и проверка среды
9
9
 
10
10
  Паттерн правила `platform-access`. Что при этом должно быть верно — закон
11
- `{{lawsDir}}/frontend-application.md`.
11
+ `docs/constitution/frontend-application.md`.
12
12
 
13
13
  ## Когда брать
14
14
 
15
- - В компонент, службу или директиву заходит окно, документ или проверка среды.
16
- - Появляется работа с разметкой, которой нельзя случиться до первой отрисовки.
15
+ - В компонент, сервис или директиву заходит `window`, `document` или проверка среды.
16
+ - Появляется работа с DOM, которой нельзя случиться до первой отрисовки.
17
17
  - Чистой функции нужен доступ к окну.
18
18
 
19
- ## Внедрение
19
+ ## Инжекты
20
20
 
21
21
  ```typescript
22
+ import { DOCUMENT } from '@angular/common';
23
+ import { inject, Injectable } from '@angular/core';
24
+
25
+ import { PlatformService, WINDOW } from '@rt-tools/core';
26
+
22
27
  @Injectable({ providedIn: 'root' })
23
28
  export class SomeService {
24
29
  readonly #document: Document = inject(DOCUMENT);
@@ -27,44 +32,53 @@ export class SomeService {
27
32
  }
28
33
  ```
29
34
 
30
- ## Когда нужен тип глобальной области
35
+ ## Когда нужен `Window & typeof globalThis`
31
36
 
32
- Интерфейс окна не описывает глобальные конструкторы и пространства имён наблюдателей
33
- пересечения и размера, объекты внешних карт. Токен отдаёт тот же самый объект, поэтому тип
34
- уточняется приведением, и рядом ставится комментарий с причиной:
37
+ Интерфейс `Window` не описывает глобальные конструкторы и неймспейсы
38
+ `IntersectionObserver`, `ResizeObserver`, `google` из `@types/google.maps`. Токен отдаёт тот же
39
+ самый объект, поэтому тип уточняется приведением, и рядом ставится комментарий с причиной:
35
40
 
36
41
  ```typescript
37
- // Конструкторы наблюдателей объявлены на глобальной области, а не на интерфейсе окна —
38
- // токен отдаёт тот же объект, тип лишь уточняется.
42
+ // Конструкторы вроде IntersectionObserver объявлены на globalThis, а не на
43
+ // интерфейсе Window — токен отдаёт тот же объект, тип лишь уточняется.
39
44
  readonly #window: Window & typeof globalThis = inject(WINDOW) as Window & typeof globalThis;
40
45
  ```
41
46
 
42
47
  ## Чистая функция принимает окно параметром
43
48
 
44
- В файлах чистой логики внедрения нет, и глобал внутрь не тянется:
49
+ В `*.logic.ts` и `*.util.ts` нет DI, и глобал внутрь не тянется:
45
50
 
46
51
  ```typescript
47
52
  export function mapsReady(windowRef: Window & typeof globalThis): boolean {
48
- return typeof windowRef.maps?.importLibrary === 'function';
53
+ return typeof windowRef.google?.maps?.importLibrary === 'function';
49
54
  }
50
55
  ```
51
56
 
52
- Внедряет его вызывающий компонент.
57
+ Инжектит его вызывающий компонент.
53
58
 
54
59
  ## Проверка среды и первая отрисовка
55
60
 
56
- Среда спрашивается у службы платформы, а работа с разметкой уходит в крючок первой отрисовки.
57
- Проверка «глобал определён» не годится: она верна случайно и ломается на первой же среде, где
58
- глобал подставлен.
61
+ ```typescript
62
+ if (!this.#platform.isPlatformBrowser) {
63
+ return;
64
+ }
65
+ ```
66
+
67
+ ```typescript
68
+ afterNextRender((): void => {
69
+ // работа с DOM, которой не должно быть до первой отрисовки
70
+ });
71
+ ```
72
+
73
+ `typeof window !== 'undefined'` не годится: проверка по наличию глобала верна случайно.
59
74
 
60
75
  ## Частые промахи
61
76
 
62
- - **Проверка среды прямым обращением к признаку платформы** вместо службы.
63
- - **Проверка среды вокруг чтения и записи в хранилище:** служба хранилища и так уходит в память
64
- вне браузера, и такое условие — мёртвый код.
65
- - **Окно полем класса в службе, обязанной работать без разметки вовсе:** там оно берётся внутри
66
- метода под проверкой среды.
67
- - **Правка добавила окно в службу, создающуюся на подъёме, а проверили одной сборкой:** падение
77
+ - `isPlatformBrowser(inject(PLATFORM_ID))` вместо `PlatformService`.
78
+ - Проверка среды вокруг чтения и записи в хранилище: `StorageService` и так уходит в память
79
+ вне браузера, и такой `if` — мёртвый код.
80
+ - `WINDOW` полем класса в сервисе, который обязан работать без DOM вообще: там окно берётся
81
+ внутри метода под проверкой среды.
82
+ - Правка добавила `WINDOW` в сервис, создающийся на подъёме, а проверили одной сборкой: падение
68
83
  видно только на поднятом сервере отдачи страниц.
69
- - **Приведение без комментария:** в переводчиках моделей приведение запрещено, и строка
70
- читается как нарушение.
84
+ - Приведение без комментария: в мапперах приведение запрещено, и строка читается как нарушение.
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: pricing-quote
3
+ kind: pattern
4
+ rule: pricing
5
+ description: Паттерн правила pricing. Брать при правке расчёта цены, механик скидки, работы с курсом валют и сумм в письмах и документах — где округлять, как выбирается одна скидка, что кладётся в бронь. Не брать для доступа к процедурам цен — это паттерн permissions-procedure.
6
+ ---
7
+
8
+ # Расчёт цены
9
+
10
+ Паттерн правила `pricing`. Что при этом должно быть верно — закон
11
+ `docs/constitution/application/money.md`.
12
+
13
+ ## Когда брать
14
+
15
+ - Правится расчёт цены или механика скидки.
16
+ - В код заходит сумма в чужой валюте.
17
+ - Сумма попадает в заказ, письмо, документ или сводку.
18
+
19
+ ## Всё считается один раз
20
+
21
+ Расчёт живёт в `calculateQuote` и отдаёт готовые числа: ночи, подытог, скидку, итог, признак
22
+ минимального срока. Показ, письмо и запись в базу берут одни и те же числа, а не пересчитывают
23
+ их каждый у себя.
24
+
25
+ ```typescript
26
+ const quote: IQuoteResult = calculateQuote({
27
+ checkIn,
28
+ checkOut,
29
+ basePriceThb,
30
+ defaultMinNights,
31
+ seasons,
32
+ lengthDiscounts,
33
+ promoCode,
34
+ });
35
+ ```
36
+
37
+ ## Одна скидка — наибольшая
38
+
39
+ Механики считаются по отдельности, а выбор между ними один:
40
+
41
+ ```typescript
42
+ const best: IDiscountCandidate | undefined = bestCandidateOf([
43
+ lengthCandidateOf(nights, input, subtotalThb),
44
+ promoCandidateOf(input.promoCode, subtotalThb),
45
+ ]);
46
+ ```
47
+
48
+ При равных суммах побеждает промокод: владелец выдал его адресно, а скидка за длительность
49
+ досталась бы гостю и без него. Новая механика добавляется третьим кандидатом в тот же вызов, а
50
+ не отдельным вычитанием из итога.
51
+
52
+ ## Чужая валюта — на показ
53
+
54
+ В заказе лежат сумма в валюте хранения и код валюты, в которой гость смотрел цену. Само число в чужой
55
+ валюте не хранится нигде:
56
+
57
+ ```typescript
58
+ export const SUPPORTED_QUOTE_CURRENCIES: readonly string[] = ['USD', 'RUB', 'EUR', 'CNY'];
59
+ ```
60
+
61
+ Курс тянется по расписанию и кэшируется в хранилище. Курса нет — сумма показывается в валюте хранения без
62
+ пересчёта.
63
+
64
+ ## Частые промахи
65
+
66
+ - Округление на показе: сумма гостя, сумма письма и сумма в базе разойдутся на единицы.
67
+ - Сложение двух скидок: владелец таких сумм не закладывал, и объяснить гостю итог нечем.
68
+ - Сохранённое число в чужой валюте: оно устареет вместе с курсом.
69
+ - Справочная сумма, подписанная как сумма к оплате, — оплата идёт в валюте хранения.
70
+ - Письмо в валюте, отличной от той, что гость выбирал на сайте: оно читается как другая цена.
71
+ - Дробная часть в сумме: единица хранения целая.
@@ -2,39 +2,39 @@
2
2
  name: reuse-first-extend
3
3
  kind: pattern
4
4
  rule: reuse-first
5
- description: Паттерн правила reuse-first. Брать, когда готового в источнике вида или в базовом классе не хватило — что проверить перед тем, как писать своё, как расширить готовое, как объявить разовое отступление маркером и когда его снимать.
5
+ description: Паттерн правила reuse-first. Брать, когда готового в ките или в базовом классе не хватило — что проверить перед тем, как писать своё, как расширить готовое, как объявить разовое отступление маркером native-ok и когда его снимать.
6
6
  ---
7
7
 
8
8
  # Готового не хватило
9
9
 
10
- Паттерн правила `reuse-first`. Что при этом должно быть верно — закон `{{lawsDir}}/reuse-first.md`.
10
+ Паттерн правила `reuse-first`. Что при этом должно быть верно — закон
11
+ `docs/constitution/reuse-first.md`.
11
12
 
12
13
  ## Когда брать
13
14
 
14
- Готовый компонент или базовый класс не покрывает случай, и рука тянется написать своё рядом.
15
+ Компонент кита или базовый класс не покрывает случай, и рука тянется написать своё рядом.
15
16
 
16
17
  ## Сначала — проверить, что действительно не хватает
17
18
 
18
19
  ```bash
19
- grep -c "export" <барель источника вида>
20
- grep -rn "<похожий приём>" <каталоги кода> --include='*.html' | head
20
+ grep -n "export" @rt-tools/ui-kit-v2/src/index.ts | wc -l
21
+ grep -rn "<похожий приём>" libs/admin libs/site --include='*.html' | head
21
22
  ```
22
23
 
23
- Источник вида большой, и большинства его компонентов нет ни в одной таблице. Соседний домен
24
- читается целиком: приём, который кажется новым, обычно уже написан — и переименованный при
25
- переносе он перестаёт узнаваться.
24
+ Кит большой, и большинства его компонентов нет ни в одной таблице. Соседний домен читается
25
+ целиком: приём, который кажется новым, обычно уже написан — и переименованный при переносе он
26
+ перестаёт узнаваться.
26
27
 
27
- Частые подмены, которые находятся при чтении: готовый мерцающий заполнитель вместо своего,
28
- готовый диалог вместо своей вуали, готовое сообщение вместо своей области оповещения.
28
+ Частые подмены, которые находятся при чтении: `<префикс>-skeleton` вместо своего мерцания,
29
+ `<префикс>-dialog` вместо своей вуали, `<префикс>-message` вместо своего `role="alert"`.
29
30
 
30
31
  ## Расширять, а не клонировать
31
32
 
32
- Недостающий вариант заводится **в источнике вида или в базовом классе**, и его видят остальные
33
- экраны.
33
+ Недостающий вариант заводится **в ките или в базовом классе**, и его видят остальные экраны.
34
34
 
35
35
  ```
36
- <домен>/ui/my-dialog/ клон «почти как готовый»
37
- <источник вида>/dialog/ новый вариант у готового
36
+ libs/admin/<домен>/ui/my-dialog/ клон «почти как китовый»
37
+ @rt-tools/ui-kit-v2/src/lib/components/dialog/ новый вариант у готового
38
38
  ```
39
39
 
40
40
  Клон, написанный рядом, забирает правки на себя и расходится с оригиналом с первой же.
@@ -42,31 +42,31 @@ grep -rn "<похожий приём>" <каталоги кода> --include='*.
42
42
 
43
43
  ## Свой примитив — только с одобрения владельца
44
44
 
45
- Спрашивается до того, как написан первый файл. То же относится к своей основе и к своему
46
- оформлению на месте.
45
+ Спрашивается до того, как написан первый файл. То же относится к своей базе и к своему
46
+ инлайновому стилю.
47
47
 
48
48
  ## Разовое отступление объявляется маркером
49
49
 
50
50
  ```html
51
- <!-- <маркер>: в источнике вида нет поля с маской телефона, заведено задачей -->
51
+ <!-- native-ok: в ките нет поля с маской телефона, заведено задачей <КЛЮЧ>-000 -->
52
52
  <input type="tel" qa-dataid="phone-input" />
53
53
  ```
54
54
 
55
- Маркер ставится в той же строке и объясняет, **чего именно нет в готовом**. «Эти строки были
56
- здесь раньше» причиной не считается: гард вычёркивает из проверяемого текста то, что уже лежит
57
- в файле, поэтому отказ означает новый текст.
55
+ Маркер `native-ok` ставится в той же строке и объясняет, **чего именно нет в ките**. «Эти
56
+ строки были здесь раньше» причиной не считается: гард вычёркивает из проверяемого текста то,
57
+ что уже лежит в файле, поэтому отказ означает новый текст.
58
58
 
59
59
  Сверка идёт без отступов — при переезде блок меняет отступ, оставаясь тем же кодом.
60
60
 
61
61
  ## Снятие отступления
62
62
 
63
- Когда недостающее появилось в готовом, маркер снимается вместе с обходом. Комментарий,
63
+ Когда недостающее появилось в ките, маркер снимается вместе с обходом. Комментарий,
64
64
  оправдывающий отклонение, держит это отклонение на себе: пока объяснение выглядит убедительно,
65
65
  его не трогают.
66
66
 
67
67
  ## Частые промахи
68
68
 
69
- - Своё написано до чтения источника вида и соседнего домена.
69
+ - Своё написано до чтения кита и соседнего домена.
70
70
  - Клон рядом вместо нового варианта у готового.
71
71
  - Маркер поставлен без объяснения, чего не хватает.
72
72
  - Маркер оставлен после того, как готовое появилось.
@@ -2,91 +2,103 @@
2
2
  name: seo-page
3
3
  kind: pattern
4
4
  rule: seo
5
- description: Паттерн правила seo. Брать, когда правится разметка публичной страницы, заводится новый маршрут или новая страница должна попасть в карту сайта — вызов общей службы тегов, ветки локалей в маршрутах, запись в карте сайта. Не брать для проверки отданной разметки — это паттерн seo-verify.
5
+ description: Паттерн правила seo. Брать, когда правится разметка страницы публичного сайта, заводится новый маршрут или новая страница должна попасть в карту сайта — готовый вызов PropertySeoService, ветки локалей в app.routes.ts, строка в sitemap. Не брать для проверки отданной разметки — это паттерн seo-verify.
6
6
  ---
7
7
 
8
- # Разметка публичной страницы
8
+ # Разметка страницы публичного сайта
9
9
 
10
- Паттерн правила `seo`. Что при этом должно быть верно — закон `{{lawsDir}}/search-visibility.md`.
10
+ Паттерн правила `seo`. Что при этом должно быть верно — закон
11
+ `docs/constitution/application/search-visibility.md`.
11
12
 
12
13
  ## Когда брать
13
14
 
14
- - Правится шаблон публичной страницы, её заголовок, описание или картинка для соцсетей.
15
- - Заводится новый публичный маршрут.
16
- - Появилась страница, которая должна попасть в карту сайта.
15
+ - Правится шаблон страницы сайта, её заголовок, описание или картинка для соцсетей.
16
+ - Заводится новый маршрут сайта.
17
+ - Появилась страница, которая должна попасть в `sitemap.xml`.
17
18
 
18
- ## Разметку ставит служба, а не шаблон
19
+ ## Разметку ставит сервис, а не шаблон
19
20
 
20
- Страница собирает данные и одним вызовом отдаёт их общей службе тегов. Своих тегов в шаблоне не
21
- заводить: они не помечены и потому не переписываются при переходе — тег от прошлой страницы
22
- переживёт следующую.
21
+ Страница собирает данные и одним вызовом отдаёт их `PropertySeoService`. Своих `<meta>` в
22
+ шаблоне не заводить: они не помечены `data-<префикс>-seo`, не переписываются при переходе и
23
+ переживут смену страницы.
24
+
25
+ Образец — `property-page.component.ts:#applySeo`:
23
26
 
24
27
  ```typescript
25
- readonly #seo: PageSeoService = inject(PageSeoService);
28
+ readonly #seo: PropertySeoService = inject(PropertySeoService);
26
29
 
27
30
  constructor() {
28
31
  effect((): void => this.#applySeo());
29
32
  }
30
33
 
31
34
  #applySeo(): void {
32
- const entity: IEntity.State | null = this.entity();
33
- if (!entity) {
35
+ const property: IProperty.State | null = this.property();
36
+ if (!property) {
34
37
  return;
35
38
  }
36
39
 
40
+ const name: string = this.propertyName();
41
+ const cover: IPhotoView | null = this.coverDesktop();
42
+
37
43
  this.#seo.apply({
38
- entity,
39
- pageTitle: this.title(),
40
- description: entity.shortDescription || this.#descriptionFallback(),
41
- ogImageUrl: this.cover() ? ogUrl(this.cover()) : this.#brandImage(),
44
+ property,
45
+ name,
46
+ pageTitle: name ? `${name} — ${this.#titleSuffix()}` : this.#titleSuffix(),
47
+ description: property.shortDescription || this.#descriptionFallback(),
48
+ ogImageUrl: cover ? photoOgUrl(cover.baseUrl) : `${SITE_ORIGIN}/${BRAND_OG_IMAGE}`,
49
+ ogImageAlt: cover ? cover.alt : name,
50
+ ogImageWidth: OG_IMAGE_WIDTH,
51
+ ogImageHeight: OG_IMAGE_HEIGHT,
42
52
  locale: this.#localeId,
43
53
  });
44
54
  }
45
55
  ```
46
56
 
47
- Вызов идёт из эффекта, а не из конструктора напрямую: запись приходит реактивным значением, и
48
- на первом кадре её ещё нет. Ранний выход по пустой записи обязателен — без него разметка встала
49
- бы на пустых значениях и второй раз уже не переписалась бы.
57
+ Вызов идёт из `effect`, а не из конструктора напрямую: объект приходит сигналом, и на первом
58
+ кадре его ещё нет. Ранний выход по пустому объекту обязателен — без него разметка встала бы на
59
+ пустых значениях и второй раз уже не переписалась бы.
50
60
 
51
- У картинки есть запасной вариант: запись без обложки отдаёт брендовый кадр того же формата,
61
+ У картинки есть запасной вариант: объект без обложки отдаёт брендовое изображение того же формата,
52
62
  иначе превью в соцсети пустует.
53
63
 
54
64
  ## Новый маршрут заводится веткой на каждую локаль
55
65
 
56
- Ветки собираются из списка локалей; локаль по умолчанию своей ветки не имеет — она отдаётся из
57
- корня.
66
+ Ветки собирает `apps/site/src/app/app.routes.ts` из `LOCALE_CODES`. Локаль по умолчанию своей
67
+ ветки не имеет — она отдаётся из корня.
58
68
 
59
69
  ```typescript
60
70
  export const appRoutes: Route[] = [
61
71
  ...LOCALE_CODES.filter((code: ELocale): boolean => code !== DEFAULT_LOCALE).map((code: ELocale): Route => ({
62
72
  path: code,
63
- children: publicRoutes,
73
+ children: propertyRoutes,
64
74
  })),
65
- ...publicRoutes,
75
+ ...propertyRoutes,
66
76
  ];
67
77
  ```
68
78
 
69
- Новый путь дописывается в общий набор — тогда он появляется во всех ветках сразу. Ветки
70
- перечислены явными кодами, а не параметром: иначе первый же сегмент адреса был бы принят за
71
- язык.
79
+ Новый путь дописывается в `propertyRoutes` — тогда он появляется во всех восьми ветках сразу.
80
+ Ветки перечислены явными кодами, а не `:locale`: иначе `/<адрес страницы>` был бы принят за
81
+ язык, а не за адрес объекта.
72
82
 
73
83
  ## Страница попадает в карту сайта явно
74
84
 
75
- Карта строится из записей, а не из маршрутов, и новая страница сама туда не попадёт. Правка
76
- идёт вместе со спекой на сборщик карты.
85
+ Карта строится из объектов, а не из маршрутов, и новая страница сама туда не попадёт. Записи
86
+ собирает `buildSitemap` (слой `util` домена страницы объекта), отдаёт обработчик
87
+ `/sitemap.xml` в `apps/site/src/server.ts`. Правка идёт вместе со спекой в
88
+ `sitemap.util.spec.ts`.
77
89
 
78
90
  ## Тексты идут из словарей
79
91
 
80
- Заголовок и описание собираются из переведённых значений и заводятся во всех локалях. Без
81
- перевода они уезжают в выдачу на одном языке, и заметно это только в чужой локали.
92
+ `pageTitle` и `description` собираются из переведённых значений и заводятся во всех восьми
93
+ локалях. Без перевода они уезжают в выдачу по-английски, и заметно это только в чужой локали.
82
94
 
83
95
  ## Частые промахи
84
96
 
85
- - **Тег в шаблоне вместо вызова службы**он не помечен и переживёт переход.
86
- - **Вызов без раннего выхода по пустой записи** — разметка встаёт на пустых значениях.
87
- - **Новый путь дописан мимо общего набора** — работает только в локали по умолчанию, остальные
88
- адреса отдают отказ.
89
- - **Свой разбор адреса вместо общего** — теряется всё после второго знака вопроса, и источник
90
- обращения считается неверно.
91
- - **Правка разметки без проверки на прод-сборке:** на сервере разработки теги ставит другой
92
- путь. Проверка описана паттерном `seo-verify`.
97
+ - `<meta>` в шаблоне вместо вызова сервисатег не помечен `data-<префикс>-seo` и переживёт переход.
98
+ - Вызов без раннего выхода по пустому объекту — разметка встаёт на пустых значениях.
99
+ - Новый путь дописан мимо `propertyRoutes` — работает только в локали по умолчанию,
100
+ `/de/<путь>` отдаёт 404.
101
+ - Свой разбор адреса вместо `splitRequestUrl` — теряется всё после второго `?`, и источник
102
+ заявки считается неверно.
103
+ - Правка разметки без проверки на прод-сборке: в дев-сервере теги ставит не тот путь. Проверка
104
+ описана паттерном `seo-verify`.