@rt-tools/agent-kit 0.3.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 (201) 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 +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 +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 +86 -29
  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/lint-after-edit.sh +155 -30
  37. package/assets/hooks/qa-dataid-guard.sh +72 -32
  38. package/assets/hooks/reuse-first-guard.sh +105 -34
  39. package/assets/hooks/skill-gate-rearm.sh +1 -0
  40. package/assets/hooks/skill-gate.sh +75 -15
  41. package/assets/hooks/skill-loaded.sh +1 -0
  42. package/assets/hooks/sql-guard.sh +606 -56
  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 +29 -22
  66. package/assets/patterns/api-layer-pair.md +40 -30
  67. package/assets/patterns/browser-verification-measure.md +41 -38
  68. package/assets/patterns/browser-verification-stand.md +106 -42
  69. package/assets/patterns/component-structure-new.md +33 -32
  70. package/assets/patterns/dependencies-upgrade.md +65 -0
  71. package/assets/patterns/doc-style-sweep.md +65 -28
  72. package/assets/patterns/doc-style-write.md +36 -33
  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 +42 -25
  80. package/assets/patterns/git-workflow-migration.md +61 -31
  81. package/assets/patterns/git-workflow-restart.md +20 -20
  82. package/assets/patterns/lib-layers-move.md +50 -32
  83. package/assets/patterns/lib-layers-new.md +41 -29
  84. package/assets/patterns/ownership-scope-resolve.md +69 -0
  85. package/assets/patterns/permissions-procedure.md +35 -33
  86. package/assets/patterns/platform-access-di.md +39 -25
  87. package/assets/patterns/pricing-quote.md +71 -0
  88. package/assets/patterns/reuse-first-extend.md +22 -22
  89. package/assets/patterns/seo-page.md +52 -40
  90. package/assets/patterns/seo-verify.md +48 -29
  91. package/assets/patterns/shared-code-new.md +37 -31
  92. package/assets/patterns/spec-driven-domain.md +44 -37
  93. package/assets/patterns/spec-driven-rule.md +55 -40
  94. package/assets/patterns/styling-bem-component.md +43 -32
  95. package/assets/patterns/styling-bem-layout.md +30 -24
  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 +53 -51
  100. package/assets/patterns/testing-unit.md +70 -46
  101. package/assets/patterns/translations-key.md +32 -19
  102. package/assets/patterns/ts-procedure.md +24 -25
  103. package/assets/rules/angular-patterns.md +46 -27
  104. package/assets/rules/api-layer.md +46 -28
  105. package/assets/rules/browser-verification.md +66 -48
  106. package/assets/rules/component-structure.md +43 -27
  107. package/assets/rules/dependencies.md +66 -0
  108. package/assets/rules/doc-style.md +81 -39
  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 +56 -30
  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 +43 -25
  119. package/assets/rules/platform-access.md +57 -29
  120. package/assets/rules/pricing.md +64 -0
  121. package/assets/rules/reuse-first.md +57 -43
  122. package/assets/rules/seo.md +51 -30
  123. package/assets/rules/shared-code.md +51 -26
  124. package/assets/rules/spec-driven.md +96 -50
  125. package/assets/rules/styling-bem.md +54 -39
  126. package/assets/rules/task-flow.md +110 -0
  127. package/assets/rules/testing.md +78 -47
  128. package/assets/rules/translations.md +48 -31
  129. package/assets/rules/typescript-conventions.md +57 -27
  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 +23 -15
  133. package/assets/templates/implementation.md +14 -8
  134. package/assets/templates/pattern.md +1 -1
  135. package/assets/templates/project.sh +32 -19
  136. package/assets/templates/rule.md +1 -1
  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 +8 -3
  152. package/lib/assets.d.ts.map +1 -1
  153. package/lib/assets.js +13 -3
  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 +202 -14
  162. package/lib/commands.js.map +1 -1
  163. package/lib/companion.d.ts +5 -1
  164. package/lib/companion.d.ts.map +1 -1
  165. package/lib/companion.js +29 -2
  166. package/lib/companion.js.map +1 -1
  167. package/lib/config.d.ts +26 -9
  168. package/lib/config.d.ts.map +1 -1
  169. package/lib/config.js +41 -15
  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/sync.d.ts +26 -0
  188. package/lib/sync.d.ts.map +1 -1
  189. package/lib/sync.js +59 -4
  190. package/lib/sync.js.map +1 -1
  191. package/lib/variants.d.ts +44 -0
  192. package/lib/variants.d.ts.map +1 -0
  193. package/lib/variants.js +82 -0
  194. package/lib/variants.js.map +1 -0
  195. package/package.json +1 -1
  196. package/rt-tools-agent-kit-0.4.0.tgz +0 -0
  197. package/assets/laws/admin-lists.md +0 -35
  198. package/assets/laws/admin-navigation.md +0 -38
  199. package/assets/patterns/git-workflow-commit.md +0 -175
  200. package/assets/rules/git-workflow.md +0 -106
  201. package/rt-tools-agent-kit-0.3.0.tgz +0 -0
@@ -2,45 +2,50 @@
2
2
  name: angular-patterns-state
3
3
  kind: pattern
4
4
  rule: angular-patterns
5
- description: Паттерн правила angular-patterns. Брать при объявлении состояния и потоков в классе фронтового каркаса реактивные входы и выходы, производные значения, состояние службы, долгоживущая подписка с источником действия. Не брать для раскладки файла компонента — это паттерн component-structure-new.
5
+ description: Паттерн правила angular-patterns. Брать при объявлении состояния и потоков в классе Angularготовые сигналы, производные значения, состояние сервиса, долгоживущая подписка с источником действия. Не брать для раскладки файла компонента — это правило component-structure.
6
6
  ---
7
7
 
8
8
  # Состояние и потоки
9
9
 
10
10
  Паттерн правила `angular-patterns`. Что при этом должно быть верно — закон
11
- `{{lawsDir}}/frontend-application.md`.
11
+ `docs/constitution/frontend-application.md`.
12
12
 
13
13
  ## Когда брать
14
14
 
15
- - Объявляется состояние компонента или службы.
15
+ - Объявляется состояние компонента или сервиса.
16
16
  - Появляется поток, на который надо подписаться.
17
17
  - Значение считается из другого значения.
18
18
 
19
- ## Реактивные входы и выходы
19
+ ## Сигнальный API входов и выходов
20
20
 
21
21
  ```typescript
22
22
  public readonly data: InputSignal<Item[]> = input.required<Item[]>();
23
- public readonly isNarrow: InputSignal<boolean | undefined> = input<boolean>();
23
+ public readonly isMobile: InputSignal<boolean | undefined> = input<boolean>();
24
24
  public readonly save: OutputEmitterRef<void> = output<void>();
25
25
 
26
26
  protected readonly myButton: Signal<ElementRef | undefined> = viewChild<ElementRef>('button');
27
27
  ```
28
28
 
29
- Декораторной формы входов, выходов и запросов к разметке в дереве нет: у неё нет типа, который
30
- видно в месте использования, и нет реактивности, на которую можно подписаться.
29
+ Декораторов `@Input()`, `@Output()`, `@ViewChild()`, `@ContentChild()` и их множественных пар
30
+ в дереве нет.
31
31
 
32
- ## Производное значение — вычисляемое, а не эффект
32
+ ## Производное значение — `computed`, а не эффект
33
+
34
+ ```typescript
35
+ protected readonly items: WritableSignal<Item[]> = signal<Item[]>([]);
36
+ protected readonly itemCount: Signal<number> = computed((): number => this.items().length);
37
+ protected readonly hasItems: Signal<boolean> = computed((): boolean => this.itemCount() > 0);
38
+ ```
33
39
 
34
40
  ```typescript
35
41
  ✗ effect((): void => { this.count.set(this.items().length); });
36
42
  ✓ protected readonly count: Signal<number> = computed((): number => this.items().length);
37
43
  ```
38
44
 
39
- Эффект, кладущий значение в реактивное поле, это ручной пересчёт, и он рано или поздно
40
- отстаёт от источника. Геттера в компоненте не заводить: он пересчитывается на каждой
41
- перерисовке, и цена его не видна ни в одном месте кода.
45
+ Геттера в компоненте не заводить: он пересчитывается на каждой перерисовке, и цена его не
46
+ видна ни в одном месте кода.
42
47
 
43
- ## Состояние службы
48
+ ## Состояние сервиса
44
49
 
45
50
  Наружу — только чтение:
46
51
 
@@ -60,7 +65,7 @@ export class DomainStateService {
60
65
 
61
66
  ## Подписка объявляется один раз
62
67
 
63
- Метод действия толкает значение в источник, подписка живёт при создании владельца:
68
+ Метод действия толкает значение в источник, подписка живёт в конструкторе:
64
69
 
65
70
  ```typescript
66
71
  readonly #loadSource: Subject<void> = new Subject<void>();
@@ -69,7 +74,7 @@ readonly #destroyRef: DestroyRef = inject(DestroyRef);
69
74
  constructor() {
70
75
  this.#loadSource
71
76
  .pipe(
72
- switchMap((): Observable<IResult> => this.#api.getList(this.#query())),
77
+ switchMap((): Observable<IPromoCode.ListResult> => this.#api.getList(this.#query())),
73
78
  takeUntilDestroyed(this.#destroyRef)
74
79
  )
75
80
  .subscribe();
@@ -80,15 +85,17 @@ protected reload(): void {
80
85
  }
81
86
  ```
82
87
 
83
- Оператор выбирается по тому, что делать с предыдущим запросом: список берёт последний ответ,
84
- кнопка не плодит дублей, независимые строки идут параллельно.
88
+ Оператор выбирается по тому, что делать с предыдущим запросом: список берёт последний ответ
89
+ (`switchMap`), кнопка не плодит дублей (`exhaustMap`), соседние строки идут независимо
90
+ (`mergeMap`).
85
91
 
86
92
  ## Частые промахи
87
93
 
88
- - **Подписка внутри метода:** правило линтера отбивает, а вместе с ним отбивается и гонка
94
+ - `.subscribe()` внутри метода: правило линтера отбивает, а вместе с ним отбивается и гонка
89
95
  ответов на быстрых нажатиях.
90
- - **Подписка без гашения:** она переживает владельца и держит уничтоженный экран в памяти.
91
- - **Поле-поток без суффикса источника:** поток и значение в коде становятся неотличимы.
92
- - **Параметры конструктора вместо функции внедрения** везде, включая базовые классы.
93
- - **Эффект без снятия слежения там, где зависимость не нужна:** он просыпается на каждое чужое
94
- изменение.
96
+ - Подписка без `takeUntilDestroyed`: она переживает владельца и держит уничтоженный экран в
97
+ памяти.
98
+ - Поле-поток без суффикса `Source`: поток и значение в коде становятся неотличимы.
99
+ - `inject()` вместо параметров конструктора везде, включая базовые классы.
100
+ - `untracked()` там, где эффекту не нужна зависимость от сигнала: без него эффект просыпается
101
+ на каждое чужое изменение.
@@ -2,19 +2,19 @@
2
2
  name: api-layer-pair
3
3
  kind: pattern
4
4
  rule: api-layer
5
- description: Паттерн правила api-layer. Брать при заведении или правке слоя обращения к серверу во фронтовом домене — готовые фасад и служба, единственный вход выборки, общий конвертер страницы, типы порядка и отбора при сущности.
5
+ description: Паттерн правила api-layer. Брать при заведении или правке слоя api фронтового домена — готовые фасад и сервис, вход выборки, конвертер страницы, типы порядка и отбора в неймспейсе сущности. Не брать для модели и её маппера — это паттерн entity-models-new.
6
6
  ---
7
7
 
8
- # Фасад и служба домена
8
+ # Фасад и сервис домена
9
9
 
10
10
  Паттерн правила `api-layer`. Что при этом должно быть верно — закон
11
- `{{lawsDir}}/frontend-application.md`.
11
+ `docs/constitution/frontend-application.md`.
12
12
 
13
13
  ## Когда брать
14
14
 
15
- - Заводится слой обращения к серверу у нового домена.
15
+ - Заводится слой `api` нового домена.
16
16
  - Список переводится на общую выборку.
17
- - Появляется новый обработчик, за которым ходит экран.
17
+ - Появляется новая процедура, за которой ходит экран.
18
18
 
19
19
  ## Фасад
20
20
 
@@ -23,56 +23,66 @@ description: Паттерн правила api-layer. Брать при заве
23
23
 
24
24
  ```typescript
25
25
  @Injectable({ providedIn: 'root' })
26
- export class EntityApiFacade implements IListApiFacade<TListRequest, TListResponse, TItemResponse> {
27
- readonly #client: Client<typeof DomainService> = injectClient(DomainService);
28
-
29
- public getList(request: TListRequest): Observable<TListResponse> {
30
- return from(this.#client.listItems(request));
26
+ export class PromoCodeApiFacade implements IListApiFacade<
27
+ MessageInitShape<typeof ListPromoCodesRequestSchema>,
28
+ ListPromoCodesResponse,
29
+ GetPromoCodeResponse
30
+ > {
31
+ readonly #client: Client<typeof PricingService> = injectConnectClient(PricingService);
32
+
33
+ public getList(request: MessageInitShape<typeof ListPromoCodesRequestSchema>): Observable<ListPromoCodesResponse> {
34
+ return from(this.#client.listPromoCodes(request));
31
35
  }
32
36
  }
33
37
  ```
34
38
 
35
39
  Метод, которого у домена нет, не объявляется: список читают все, правят не все.
36
40
 
37
- ## Служба
41
+ ## Сервис
38
42
 
39
43
  Принимает доменные модели, отдаёт их же. Тип контракта до стора и шаблона не доходит:
40
44
 
41
45
  ```typescript
42
- export class EntityApiService implements IListApiService<IEntity.State, ESortProperty, EFilterProperty, IEntity.Draft> {
43
- public getList(query: IEntity.Query): Observable<IEntity.ListResult> {
46
+ export class PromoCodeApiService implements IListApiService<
47
+ IPromoCode.State,
48
+ EPromoCodeSortProperty,
49
+ EPromoCodeFilterProperty,
50
+ IPromoCode.Draft
51
+ > {
52
+ public getList(query: IPromoCode.Query): Observable<IPromoCode.ListResult> {
44
53
  return this.#facade
45
54
  .getList({ query: this.#queryMapper.mapTo(query) })
46
55
  .pipe(
47
- map((response: TListResponse): IEntity.ListResult =>
48
- convertPaginationApiModelToStateModel((item: TItem): IEntity.State => this.#mapper.mapFrom(item), response)
56
+ map((response: ListPromoCodesResponse): IPromoCode.ListResult =>
57
+ convertPaginationApiModelToStateModel((item: PromoCodeInfo): IPromoCode.State => this.#mapper.mapFrom(item), response)
49
58
  )
50
59
  );
51
60
  }
52
61
  }
53
62
  ```
54
63
 
55
- Выборка единственный вход: объект, к которому привязан список, род ленты, состояние подписки
56
- — это условия отбора, и лежат они в её условиях.
64
+ `getList` принимает выборку и больше ничего: объект, к которому привязан список, тип фида,
65
+ состояние подписки — это условия отбора, и лежат они в `filterModel`.
57
66
 
58
- ## Типы выборки при сущности
67
+ ## Типы выборки в неймспейсе сущности
59
68
 
60
69
  ```typescript
61
- export type Query = IList.Query.State<ESortProperty, EFilterProperty>;
62
- export type ListResult = IList.Result.State<IEntity.State, ESortProperty, EFilterProperty>;
70
+ export type Query = IList.Query.State<EPromoCodeSortProperty, EPromoCodeFilterProperty>;
71
+ export type ListResult = IList.Result.State<IPromoCode.State, EPromoCodeSortProperty, EPromoCodeFilterProperty>;
63
72
  ```
64
73
 
65
- Перечисления порядка и отбора объявляются рядом с сущностью и повторяют набор имён, по которым
66
- сортирует и отбирает сервер именно этого домена.
74
+ Перечисления `EPromoCodeSortProperty` и `EPromoCodeFilterProperty` объявляются в модели рядом с
75
+ сущностью и повторяют набор имён, по которым сортирует и отбирает сервер этого домена.
67
76
 
68
77
  ## Частые промахи
69
78
 
70
- - **Промежуточный объект между ответом и моделью:** ответ ложится в конвертер целиком.
71
- - **Выборка из своего запроса вместо применённой из ответа:** умолчание сервера и отброшенное
72
- им условие экран иначе не увидит.
73
- - **Второй вход рядом с выборкой:** отбор, живущий отдельно, не виден ни стору, ни адресу.
74
- - **Голая строка в поле порядка:** имя, по которому сервер не сортирует, компилируется и падает
79
+ - Промежуточный объект между ответом и моделью: ответ ложится в конвертер целиком.
80
+ - Выборка из своего запроса вместо применённой из ответа: умолчание сервера и отброшенное им
81
+ условие экран иначе не увидит.
82
+ - Второй вход рядом с выборкой (`propertyId`, `feedType`): отбор, живущий отдельно, не виден ни
83
+ стору, ни адресу.
84
+ - Голая `string` в поле порядка: имя, по которому сервер не сортирует, компилируется и падает
75
85
  запросом.
76
- - **Один класс на две сущности:** подмена источника одной потянет за собой правку другой.
77
- - **Служба на обещаниях в новом сторе:** основа списочного стора работает потоками.
78
- - **Своя копия общих переводчиков страницы, порядка и отбора** — её ловит проверка повторов.
86
+ - Один класс на две сущности: подмена источника одной потянет за собой правку другой.
87
+ - Промисный сервис в новом сторе: основа списочного стора работает потоками.
88
+ - Своя копия общих мапперов страницы, порядка и отбора — её ловит `npm run check:dupes`.
@@ -2,13 +2,13 @@
2
2
  name: browser-verification-measure
3
3
  kind: pattern
4
4
  rule: browser-verification
5
- description: Паттерн правила browser-verification. Брать, когда вывод о вёрстке надо подкрепить числом — готовые замеры, разбивка вычисленного значения по всем узлам, узкий экран вложенной рамкой, ловушки инструмента снимка экрана. Не брать для подъёма стенда — это паттерн browser-verification-stand.
5
+ description: Паттерн правила browser-verification. Брать, когда вывод о вёрстке надо подкрепить числом — готовые замеры, разбивка вычисленного значения по всем узлам, узкий экран через iframe, ловушки инструмента computer. Не брать для подъёма стенда — это паттерн browser-verification-stand.
6
6
  ---
7
7
 
8
8
  # Замер вместо взгляда
9
9
 
10
10
  Паттерн правила `browser-verification`. Что при этом должно быть верно — закон
11
- `{{lawsDir}}/verifiability.md`.
11
+ `docs/constitution/verifiability.md`.
12
12
 
13
13
  ## Когда брать
14
14
 
@@ -18,21 +18,23 @@ description: Паттерн правила browser-verification. Брать, к
18
18
 
19
19
  ## Вывод подкрепляется числом
20
20
 
21
- Вычисленный стиль, прямоугольник элемента, контраст, совпадение центров, попадание в видимую
22
- область. «Выглядит нормально» результатом проверки не является.
21
+ `getComputedStyle`, `getBoundingClientRect`, контраст, совпадение центров, попадание во
22
+ вьюпорт. «Выглядит нормально» результатом проверки не является.
23
23
 
24
- Замер отвечает только на тот вопрос, который задали. Совпадение перечисленных свойств ничего не
25
- говорит о правиле, которого в списке замера нет. Если исходники образца доступны, расхождение
26
- ищется чтением, а замер остаётся проверкой результата.
24
+ Замер отвечает только на тот вопрос, который задали. Совпадение перечисленных свойств ничего
25
+ не говорит о правиле, которого в списке замера нет: строки попапа профиля сошлись с образцом
26
+ по отступам, кеглю и скруглению, а фон на наведении образец в этом месте не красит вовсе —
27
+ полноширинная подсветка держалась два круга при верных числах. Если исходники образца
28
+ доступны, расхождение ищется чтением, а замер остаётся проверкой результата.
27
29
 
28
30
  Вид элемента, которого сегодня не видно ни на одном экране, замером не подтверждается, и
29
31
  правило о нём остаётся гипотезой.
30
32
 
31
33
  ## Значение, которого в коде нет
32
34
 
33
- Гарнитуру, межстрочный интервал, цвет и оформление элементам формы задаёт браузер, и в дереве
34
- этих значений нет. Поиск по коду на такой дефект отвечает «чисто», линтер и сборка молчат.
35
- Ищется разбивкой вычисленного значения по всем узлам страницы, а не замером у пары элементов:
35
+ Гарнитуру, `line-height`, цвет и `appearance` элементам формы задаёт браузер, и в дереве этих
36
+ значений нет. Поиск по коду на такой дефект отвечает «чисто», линт и сборка молчат. Ищется
37
+ разбивкой вычисленного значения по всем узлам страницы, а не замером у пары элементов:
36
38
 
37
39
  ```javascript
38
40
  [...document.querySelectorAll('*')].reduce((acc, el) => {
@@ -42,42 +44,43 @@ description: Паттерн правила browser-verification. Брать, к
42
44
  }, {});
43
45
  ```
44
46
 
45
- Счёт годится любому наследуемому свойству: смотрится не одно значение, а число узлов с
46
- неожиданным. Так находятся десятки элементов, набранных не той гарнитурой, по одному их не
47
- заметил бы никто.
47
+ Так на дашборде админки нашёлся 91 контрол из 106, набранный не той гарнитурой, и 13 из 87 на
48
+ главной сайта. Счёт годится любому наследуемому свойству: смотрится не одно значение, а число
49
+ узлов с неожиданным.
48
50
 
49
51
  ## Узкий экран
50
52
 
51
- Изменение размера окна не работает, когда браузер в полноэкранном режиме: инструмент рапортует
52
- успех, ширина не меняется, медиазапросы остаются широкими. Узкие ширины проверяются во
53
- вложенной рамке нужной ширины — внутри неё запрос ширины считается от рамки, и рамка своей
54
- толщиной уменьшает внутреннюю ширину.
53
+ `resize_window` не работает, когда Chrome в полноэкранном режиме: инструмент рапортует успех,
54
+ `innerWidth` не меняется, медиазапросы остаются десктопными. Узкие ширины проверять во
55
+ вложенном iframe нужной ширины — внутри него `matchMedia` считается от рамки; при
56
+ `width: 375px` и `border: 2px` внутренний `innerWidth` равен 371.
55
57
 
56
- Правка числа элементов в контейнере — это правка раскладки: она проверяется на узкой ширине, а
57
- не только кодами ответа.
58
+ Правка числа элементов в контейнере — это правка раскладки: она проверяется при 375, а не
59
+ только кодами ответа.
58
60
 
59
- ## Ловушки инструмента снимка экрана
61
+ ## Ловушки инструмента `computer`
60
62
 
61
- - **Координаты нажатия — координаты снимка, а не страницы.** При широком окне снимок приходит
62
- уже, и нажатие по «увиденной» координате уходит мимо, давая ложный сигнал: пересчитывать по
63
- фактическому масштабу либо целиться поиском элемента.
64
- - **Область приближения должна целиком лежать внутри видимой области.**
65
- - **Между нажатиями обязательно ожидание:** цикл «нажал — прочитал разметку» без него читает
66
- состояние до перерисовки и возвращает устаревшие значения.
67
- - **Выбор браузера протухает.** На длинной проверке это срабатывает посреди работы — не сбой
68
- стенда, а повод повторить вызов и продолжить.
63
+ - Координаты клика — координаты **скриншота**, а не CSS-пиксели: при вьюпорте 2560 скриншот
64
+ приходит шириной 1568, и клик по «увиденной» координате уходит мимо, давая ложный сигнал.
65
+ Пересчитывать по фактическому масштабу либо целиться через `find`.
66
+ - Область `zoom` должна целиком лежать внутри вьюпорта.
67
+ - Между кликами обязателен `await`: синхронный цикл «кликнул — прочитал DOM» читает состояние
68
+ до перерисовки и возвращает устаревшие значения.
69
+ - `select_browser` протухает через 300 секунд. На длинной проверке это срабатывает посреди
70
+ работы это не сбой стенда, повторить вызов и продолжить. Закреплённый профиль — тот, что записан в дереве, идентификатор `062b17db-ae82-4264-927c-e9904d0dd5be`.
69
71
 
70
- ## Поведение маршрутизации воспроизводится нажатиями
72
+ ## Поведение роутера воспроизводится нажатиями
71
73
 
72
- Подстановка адреса, программный переход и заход по прямой ссылке поднимают приложение заново, и
73
- накопленного состояния — открытой панели, стража прошлого экрана — у него нет. Чистый проход по
74
- адресам читается как «дефект не подтверждается».
74
+ Подстановка адреса, `history.pushState` с `popstate` и заход по прямой ссылке поднимают
75
+ приложение заново, и накопленного состояния — открытого аутлета, гарда прошлой панели — у него
76
+ нет. Чистый проход по адресам читается как «дефект не подтверждается»: панель ленты событий
77
+ застревала на первом же нажатии из меню и трижды прошла проверку адресами.
75
78
 
76
79
  ## Частые промахи
77
80
 
78
- - **Комментарий в конфиге — гипотеза наравне с прочими.** Утверждение о поведении кэша
79
- переживает несколько кругов разбора кода и опровергается одним запросом.
80
- - **Вывод «дефекта нет, это кэш» закрывает разбор**, поэтому принимается только после проверки
81
- на чистой сборке.
82
- - **Дефект в клиентском куске от компиляции до правки выглядит как дефект кода** признак
83
- сборки для разработки — имена файлов без хеша.
81
+ - Комментарий в конфиге — гипотеза наравне с прочими. Утверждение о вытеснении записи кэша
82
+ продержалось три круга состязательного разбора кода и было опровергнуто одним `curl`.
83
+ - Вывод «дефекта нет, это кэш» закрывает разбор, поэтому принимается только после проверки на
84
+ чистой сборке.
85
+ - Дефект в клиентском чанке от компиляции до правки выглядит как дефект кода: признак
86
+ дев-сборки — имена бандла без хеша.
@@ -2,78 +2,142 @@
2
2
  name: browser-verification-stand
3
3
  kind: pattern
4
4
  rule: browser-verification
5
- description: Паттерн правила browser-verification. Брать, когда нужен честный стенд — прод-сборка, стенд под настоящим прокси, вход в приложение, разбор того, что висит на порту, стенд серверной стороны с переменными окружения. Не брать для замеров вёрстки — это паттерн browser-verification-measure.
5
+ description: Паттерн правила browser-verification. Брать, когда нужен честный стенд — прод-сборка сайта, стенд админки, стенд под настоящим nginx, вход в админку, разбор того, что висит на порту. Не брать для замеров вёрстки — это паттерн browser-verification-measure.
6
6
  ---
7
7
 
8
8
  # Честный стенд
9
9
 
10
10
  Паттерн правила `browser-verification`. Что при этом должно быть верно — закон
11
- `{{lawsDir}}/verifiability.md`.
11
+ `docs/constitution/verifiability.md`.
12
12
 
13
13
  ## Когда брать
14
14
 
15
- - Проверяется то, чего на сервере разработки не видно: разметка от сервера, локали, кэш,
16
- перенаправления, заголовки, размер сборки.
15
+ - Проверяется то, чего на дев-сервере не видно: разметка от сервера, локали, кэш,
16
+ перенаправления, заголовки, размер бандла.
17
17
  - Порт отвечает не тем, чего ждали.
18
- - Нужен вход в приложение.
18
+ - Нужен вход в админку.
19
19
 
20
20
  ## Сначала — что отвечает на порту
21
21
 
22
22
  До первого запроса, а не после непонятного ответа:
23
23
 
24
24
  ```bash
25
- lsof -nP -iTCP:<порт> -sTCP:LISTEN
25
+ lsof -nP -iTCP:{{apiPort}} -sTCP:LISTEN
26
26
  ```
27
27
 
28
- На порту регулярно висит собранный артефакт из прошлой сессии: он отвечает успехом на старом
29
- коде, а заведённого в ветке обработчика у него нет вовсе. Таких процессов бывает несколько, и
30
- снимать надо все по идентификатору из вывода, каждый: завершение по шаблону команды не
31
- попадает ни в один.
28
+ На {{apiPort}} регулярно висит собранный артефакт из прошлой сессии
29
+ (`node -r dotenv/config dist/apps/api/main.js`): он отвечает 200 старым кодом, а процедуры,
30
+ заведённой в ветке, у него нет вовсе. Таких процессов бывает несколько, и снимать надо все —
31
+ по PID из `lsof`, каждый: `pkill` по шаблону `nx serve api` не попадает ни в один.
32
32
 
33
- ## Прод-сборка
33
+ ## Прод-сборка сайта
34
34
 
35
- Собранный сервер поднимается прямо, а не через сервер разработки: гард ловит запуск сервера
36
- разработки, пакетные раннеры и статические серверы, а запуск собранного сервера пропускает.
35
+ ```bash
36
+ npx nx build site
37
+ PORT={{prodSitePort}} node dist/apps/site/server/server.mjs
38
+ ```
39
+
40
+ Это не дев-сервер: гард ловит `nx|ng serve`, пакетные раннеры и статические серверы, а запуск
41
+ собранного сервера пропускает.
37
42
 
38
- Приложению, отдающему статику, нужен явный базовый адрес — без него стенд отдаёт пустую
39
- страницу без единой ошибки в консоли. Стенду, на котором нужны отладочные инструменты каркаса,
40
- нужна сборка для разработки: прод-сборка их не публикует. Выводы о размере сборки и минификации
41
- на такой сборке делать нельзя.
43
+ ## Стенд админки
44
+
45
+ ```bash
46
+ npx nx build admin --base-href=/
47
+ ```
42
48
 
43
- ## Вход в приложение
49
+ Без `--base-href` стенд отдаёт пустую страницу без ошибок в консоли. Стенду, на котором нужен
50
+ Angular DevTools, нужна ещё и dev-конфигурация (`--configuration=development`): прод-сборка не
51
+ публикует `window.ng`. Выводы о размере бандла и минификации на такой сборке делать нельзя.
44
52
 
45
- Сессия кладётся в хранилище браузера ровно в той форме, в какой её читает приложение: значение,
46
- записанное иначе, приложение молча не увидит. Токен не подписывается руками, а берётся у живого
47
- сервера входом.
53
+ Сессия кладётся в `localStorage['vm.admin.token']` **строкой JSON** (`JSON.stringify(token)`),
54
+ иначе приложение её не прочитает. Токен не подписывается руками, а берётся у живого API:
55
+ `POST /<область>.v1.AuthService/Login`. Команду с паролем классификатор блокирует — обходить не
56
+ надо, спрашивать разрешение у владельца.
48
57
 
49
- Взять уже открытую сессию нельзя: чтение хранилища чужого профиля блокируется. Оба пути к
50
- своему стенду упираются в пароль, поэтому остаётся третий — смотреть там, где вход уже сделан.
51
- Свой стенд нужен, только когда проверяют прод-сборку, базовый адрес или конфиг прокси; чтобы
52
- просто посмотреть экраны, он не нужен.
58
+ Взять уже открытую сессию нельзя: чтение `localStorage['vm.admin.token']` из браузера
59
+ блокируется. Оба пути к своему стенду упираются в пароль, поэтому остаётся третий — смотреть
60
+ на админке владельца, где вход уже сделан. Свой стенд нужен, только когда проверяют
61
+ прод-сборку, `--base-href` или конфиг nginx; чтобы просто посмотреть экраны, он не нужен.
53
62
 
54
- ## Стенд серверной стороны
63
+ ## Стенд API
55
64
 
56
- Собранный артефакт поднимается на свободном порту, а не на рабочем: на рабочем отвечает сервер
65
+ Собранный артефакт поднимается на свободном порту, а не на {{apiPort}}: на {{apiPort}} отвечает API
57
66
  владельца, и окружение у него не то, которое проверяется.
58
67
 
59
- Переменные окружения задаются в самой команде, по одной на проверяемый случай. Отказ на старте
60
- такой же результат проверки, как успешный ответ: приложение, упавшее при сборке зависимостей,
61
- порт не слушает вовсе, и это видно по списку слушателей, а не по тексту в консоли.
68
+ ```bash
69
+ npx nx build api
70
+ env -u JWT_SECRET NODE_ENV=production API_PORT={{prodApiPort}} DATABASE_URL=… node dist/apps/api/main.js
71
+ ```
72
+
73
+ Переменные окружения задаются в самой команде, по одной на проверяемый случай. Отказ на
74
+ старте — такой же результат проверки, как ответ 200: приложение, упавшее в фабрике
75
+ провайдера, порт не слушает вовсе, и это видно по `lsof`, а не по тексту в консоли.
62
76
 
63
- Обработчик зовётся полным именем, как его объявляет контракт. Токен подписывается руками только
64
- там, где проверяется сама подпись; во всех остальных случаях он берётся у живого сервера входом
65
- — рукописный скрывает расхождение состава притязаний.
77
+ Процедура зовётся полным именем, как её объявляет контракт:
78
+
79
+ ```bash
80
+ curl -sS -X POST http://localhost:{{prodApiPort}}/<область>.v1.AuthService/GetMe \
81
+ -H 'content-type: application/json' -H "authorization: Bearer $TOKEN" -d '{}'
82
+ ```
66
83
 
67
- ## Стенд под настоящим прокси
84
+ Токен подписывается руками только там, где проверяется сама подпись: доказать, что отладочный
85
+ ключ принимается, другого пути не имеет. Во всех остальных случаях токен берётся у живого API
86
+ входом — рукописный скрывает расхождение состава притязаний.
87
+
88
+ ## Стенд под настоящим nginx
89
+
90
+ Кэш, перенаправления и заголовки живут в `deploy/nginx.conf`, а не в приложении. Любой вывод
91
+ про `301`, `Cache-Control` и `X-Cache-Status` делается только здесь:
92
+
93
+ ```bash
94
+ docker run -d --name <префикс>-stand-nginx \
95
+ --add-host api:host-gateway --add-host ssr:host-gateway \
96
+ -p {{dockerSitePort}}:80 -p {{dockerAdminPort}}:8081 \
97
+ -v "$PWD/deploy/nginx/main.conf:/etc/nginx/nginx.conf:ro" \
98
+ -v "$PWD/deploy:/etc/nginx/conf.d:ro" \
99
+ -v "$PWD/dist/apps/admin/browser:/usr/share/nginx/html/admin:ro" \
100
+ nginx:1.27-alpine
101
+ ```
102
+
103
+ - Конфиг монтируется **каталогом**, а не одиночным файлом: редактор пересоздаёт файл,
104
+ контейнеру остаётся обрезанная копия, и `nginx -t` внутри падает на «unexpected end of file»
105
+ при целом файле снаружи. Лечится пересозданием контейнера, а не правкой конфига.
106
+ - Главный конфиг подставляется отдельной строкой: он лежит в `deploy/nginx/`, а не рядом с
107
+ `nginx.conf`, — файл с расширением `.conf` в смонтированном каталоге попал бы в `include`
108
+ и уронил бы nginx на директиве `user`. Без него стенд поднимается на конфиге образа, и
109
+ предел соединений на воркер там свой.
110
+ - Сервер отдачи страниц отвечает `400` на чужой `Host`: все запросы идут с
111
+ `-H "Host: localhost"`.
112
+ - Переменные окружения стенда обязаны смотреть на процессы стенда. `CACHE_REFRESH_URL`,
113
+ направленный на {{sitePort}}, сбрасывает кэш мимо того процесса, который держит справочник
114
+ перенаправлений в памяти, — исправный механизм при этом выглядит сломанным.
115
+
116
+ ## Дерево для сравнения
117
+
118
+ Сказать «это сломала правка» можно, только если видно, что до правки было иначе. Проверяют это
119
+ вторым деревом, а не по памяти:
120
+
121
+ ```bash
122
+ git worktree add ../<префикс>-base <коммит> # ветка задачи не трогается
123
+ pnpm install --frozen-lockfile # из ../<префикс>-base: node_modules у него свои
124
+ ```
68
125
 
69
- Кэш, перенаправления и заголовки живут в конфиге прокси, а не в приложении, и любой вывод о них
70
- с голого сервера отдачи страниц неверен. Устройство такого стенда паттерн `testing-e2e`.
126
+ - Для сравнения берут не главную ветку, а последний коммит, на котором дерево собирается:
127
+ главная бывает сломана, и тогда «до» и «после» различаются не из-за правки. Собирается ли
128
+ коммит — проверяют сборкой, а не тем, что он в главной ветке.
129
+ - Стенд второго дерева поднимают на своих портах: {{sitePort}}, {{adminPort}} и {{apiPort}} заняты владельцем, а {{ssrPort}}
130
+ занимать нельзя — стенд разработчика ходит по имени `ssr:{{ssrPort}}`.
131
+ - Оба стенда держат поднятыми одновременно: если сравнивать по памяти между двумя запусками,
132
+ заметишь только то, что успел запомнить.
71
133
 
72
134
  ## Частые промахи
73
135
 
74
- - **Порт занят чужой сборкой, а ответ читается как дефект ветки.** Сначала список слушателей,
75
- потом запрос.
76
- - **Стенд без явного базового адреса отдаёт пустую страницу** и ни одной ошибки при этом не
77
- печатает.
78
- - **Токен, подписанный руками, скрывает расхождение состава притязаний** вход берётся у
79
- живого сервера.
136
+ - Свой дев-сервер не поднимать: сайт на {{sitePort}}, админка на {{adminPort}}, API на {{apiPort}} уже подняты
137
+ владельцем, и второй экземпляр отбивается гардом.
138
+ - **Общая сборка глушит все три дев-сервера владельца, а не только API.** После
139
+ `nx run-many -t build` ложатся и сайт на {{sitePort}}, и админка на {{adminPort}}. Собирать надо то, что
140
+ проверяешь (`npx nx build site`), а не всё дерево. Если серверы легли, поднять их обратно
141
+ агент не может — мешает гард, поэтому владельцу говорят об этом сразу, а не в конце сессии.
142
+ - Порт {{ssrPort}} занимать осторожно: стенд разработчика на {{sitePort}} ходит по тому же имени `ssr:{{ssrPort}}`
143
+ через `host-gateway`, и пока на нём висит чужой процесс, стенд отдаёт чужую сборку.