@rt-tools/agent-kit 0.2.0 → 0.3.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.
- package/README.md +59 -6
- package/assets/hooks/browser-device-id.sh +20 -0
- package/assets/hooks/browser-guard-device-id.sh +27 -0
- package/assets/hooks/browser-guard-no-listing.sh +17 -0
- package/assets/hooks/browser-guard-no-other-drivers.sh +78 -0
- package/assets/hooks/browser-guard-require-select.sh +53 -0
- package/assets/hooks/commit-msg.sh +26 -0
- package/assets/hooks/constitution-index.sh +42 -0
- package/assets/hooks/dev-server-guard.sh +113 -0
- package/assets/hooks/docs-guard.sh +96 -0
- package/assets/hooks/git-guard-delivery.sh +110 -0
- package/assets/hooks/git-guard-main.sh +72 -0
- package/assets/hooks/git-guard-push-tests.sh +73 -0
- package/assets/hooks/lint-after-edit.sh +94 -0
- package/assets/hooks/qa-dataid-guard.sh +81 -0
- package/assets/hooks/reuse-first-guard.sh +83 -0
- package/assets/hooks/skill-gate-rearm.sh +22 -0
- package/assets/hooks/skill-gate.sh +68 -0
- package/assets/hooks/skill-loaded.sh +20 -0
- package/assets/hooks/sql-guard.sh +129 -0
- package/assets/patterns/angular-patterns-state.md +94 -0
- package/assets/patterns/api-layer-pair.md +78 -0
- package/assets/patterns/browser-verification-measure.md +83 -0
- package/assets/patterns/browser-verification-stand.md +79 -0
- package/assets/patterns/component-structure-new.md +98 -0
- package/assets/patterns/doc-style-sweep.md +100 -0
- package/assets/patterns/doc-style-write.md +106 -0
- package/assets/patterns/git-workflow-commit.md +175 -0
- package/assets/patterns/git-workflow-merge.md +82 -0
- package/assets/patterns/git-workflow-migration.md +58 -0
- package/assets/patterns/git-workflow-restart.md +49 -0
- package/assets/patterns/lib-layers-move.md +77 -0
- package/assets/patterns/lib-layers-new.md +70 -0
- package/assets/patterns/permissions-procedure.md +69 -0
- package/assets/patterns/platform-access-di.md +70 -0
- package/assets/patterns/reuse-first-extend.md +73 -0
- package/assets/patterns/seo-page.md +92 -0
- package/assets/patterns/seo-verify.md +64 -0
- package/assets/patterns/shared-code-new.md +80 -0
- package/assets/patterns/spec-driven-domain.md +100 -0
- package/assets/patterns/spec-driven-rule.md +112 -0
- package/assets/patterns/styling-bem-component.md +77 -0
- package/assets/patterns/styling-bem-layout.md +67 -0
- package/assets/patterns/testing-e2e.md +90 -0
- package/assets/patterns/testing-unit.md +93 -0
- package/assets/patterns/translations-key.md +51 -0
- package/assets/patterns/ts-procedure.md +66 -0
- package/assets/rules/angular-patterns.md +52 -0
- package/assets/rules/api-layer.md +53 -0
- package/assets/rules/browser-verification.md +69 -0
- package/assets/rules/component-structure.md +48 -0
- package/assets/rules/doc-style.md +61 -0
- package/assets/rules/git-workflow.md +106 -0
- package/assets/rules/lib-layers.md +54 -0
- package/assets/rules/permissions.md +52 -0
- package/assets/rules/platform-access.md +49 -0
- package/assets/rules/reuse-first.md +69 -0
- package/assets/rules/seo.md +50 -0
- package/assets/rules/shared-code.md +45 -0
- package/assets/rules/spec-driven.md +89 -0
- package/assets/rules/styling-bem.md +59 -0
- package/assets/rules/testing.md +69 -0
- package/assets/rules/translations.md +52 -0
- package/assets/rules/typescript-conventions.md +46 -0
- package/assets/templates/gate-map.sh +37 -0
- package/assets/templates/implementation.md +38 -0
- package/assets/templates/pattern.md +4 -0
- package/assets/templates/project.sh +41 -0
- package/assets/templates/rule.md +12 -23
- package/lib/assets.d.ts +8 -0
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +12 -1
- package/lib/assets.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +21 -2
- package/lib/commands.js.map +1 -1
- package/lib/companion.d.ts +53 -0
- package/lib/companion.d.ts.map +1 -0
- package/lib/companion.js +33 -0
- package/lib/companion.js.map +1 -0
- package/lib/config.d.ts +24 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +33 -1
- package/lib/config.js.map +1 -1
- package/lib/stamp.d.ts +2 -5
- package/lib/stamp.d.ts.map +1 -1
- package/lib/stamp.js +25 -10
- package/lib/stamp.js.map +1 -1
- package/lib/sync.d.ts +3 -0
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +20 -1
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.3.0.tgz +0 -0
- package/rt-tools-agent-kit-0.2.0.tgz +0 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Гард пишущих запросов к хранилищу. PreToolUse.
|
|
3
|
+
#
|
|
4
|
+
# Правка данных — единственное действие, которое нельзя откатить правкой кода. Удаление по
|
|
5
|
+
# маске однажды уносит вместе с пробными записями настоящие: маска совпадает шире, чем ожидал
|
|
6
|
+
# автор запроса, и узнаётся это уже по восстановлению из копии.
|
|
7
|
+
#
|
|
8
|
+
# Отсюда правило: строки адресуются по первичному ключу. Перечисление идентификаторов
|
|
9
|
+
# затрагивает ровно столько строк, сколько их перечислено, и промах виден до выполнения;
|
|
10
|
+
# отбор по подстроке не виден никогда.
|
|
11
|
+
#
|
|
12
|
+
# Три уровня:
|
|
13
|
+
# отказ — снос, очистка, правка схемы, удаление и обновление без условия или с условием
|
|
14
|
+
# не по идентификатору;
|
|
15
|
+
# вопрос — остальная запись: адресная правка и вставка, решение за владельцем;
|
|
16
|
+
# пропуск — чтение.
|
|
17
|
+
#
|
|
18
|
+
# Боевое хранилище отдельно: его адреса перечисляет профиль проекта ({{projectProfile}},
|
|
19
|
+
# переменная RT_PROD_DSN) — там любая запись отказывается без опт-аута. Бой правится миграцией
|
|
20
|
+
# через выкатку, а не запросом из редактора.
|
|
21
|
+
#
|
|
22
|
+
# Опт-аут для остальных случаев: маркер `destructive-ok` в тексте запроса вместе с объяснением,
|
|
23
|
+
# почему адресация по идентификатору не подходит, понижает отказ до вопроса. Последнее слово
|
|
24
|
+
# остаётся за владельцем — гард лишь не пропускает такое молча.
|
|
25
|
+
#
|
|
26
|
+
# ОТКАЗ В ПОЛЬЗУ РАБОТЫ: нет разборщика, битый ввод, чужой инструмент — пропуск.
|
|
27
|
+
|
|
28
|
+
input="$(cat 2>/dev/null)"
|
|
29
|
+
[ -z "$input" ] && exit 0
|
|
30
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
31
|
+
|
|
32
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)"
|
|
33
|
+
|
|
34
|
+
sql=""
|
|
35
|
+
case "$tool" in
|
|
36
|
+
mcp__webstorm__execute_sql_query)
|
|
37
|
+
sql="$(printf '%s' "$input" | jq -r '.tool_input.queryText // empty' 2>/dev/null)"
|
|
38
|
+
;;
|
|
39
|
+
Bash | mcp__webstorm__execute_terminal_command | mcp__webstorm__execute_tool)
|
|
40
|
+
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)"
|
|
41
|
+
[ -z "$cmd" ] && exit 0
|
|
42
|
+
|
|
43
|
+
# Клиент ищется КАК СЛОВО в любом месте команды, а не только в начале: удаление на бою
|
|
44
|
+
# выглядит как заход по сети с вложенной командой клиента, и проверка одного лишь
|
|
45
|
+
# начала строки проходит мимо него целиком.
|
|
46
|
+
#
|
|
47
|
+
# Порядок принципиален. Отсечка «команда начинается с гита», снимающая ложные
|
|
48
|
+
# срабатывания на тексте коммита, открывает обход: в составной команде первое звено
|
|
49
|
+
# уносит с собой весь остальной запрос. Поэтому сперва ищется клиент.
|
|
50
|
+
printf '%s\n' "$cmd" | grep -qE '(^|[^[:alnum:]_.-])(psql|pg_restore|pg_dump|prisma)([^[:alnum:]_.-]|$)' || exit 0
|
|
51
|
+
|
|
52
|
+
# Ложные срабатывания на описаниях снимаются вырезанием текстов сообщений, а не отказом
|
|
53
|
+
# от проверки всей команды: в самом сообщении запрос не исполняется, но слова «удалить»
|
|
54
|
+
# и «обновить» в нём обычны.
|
|
55
|
+
if command -v perl >/dev/null 2>&1; then
|
|
56
|
+
# Флага файла в списке быть не должно: под него попадает файл запроса у клиента, и
|
|
57
|
+
# признак «запрос приехал файлом» умирает раньше, чем его проверят.
|
|
58
|
+
cmd="$(printf '%s' "$cmd" | perl -0pe '
|
|
59
|
+
s/(^|[^[:alnum:]])--?[a-zA-Z]*(m|message|body|title|body-file|F)(=|\s+)("([^"\\]|\\.)*"|\x27[^\x27]*\x27|[^\s;&|]+)/$1/gs
|
|
60
|
+
' 2>/dev/null || printf '%s' "$cmd")"
|
|
61
|
+
printf '%s\n' "$cmd" | grep -qE '(^|[^[:alnum:]_.-])(psql|pg_restore|pg_dump|prisma)([^[:alnum:]_.-]|$)' || exit 0
|
|
62
|
+
else
|
|
63
|
+
# Без разборщика вырезать текст сообщения нечем, и разбирать его как запрос нельзя:
|
|
64
|
+
# штатный коммит упирался бы в отказ. Доставки запроса внутри команды истории не
|
|
65
|
+
# бывает, поэтому здесь дешевле пропустить, чем ломать работу.
|
|
66
|
+
case "$cmd" in
|
|
67
|
+
git\ *|*/git\ *|gh\ *|*/gh\ *) exit 0 ;;
|
|
68
|
+
esac
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
sql="$cmd"
|
|
72
|
+
;;
|
|
73
|
+
*) exit 0 ;;
|
|
74
|
+
esac
|
|
75
|
+
|
|
76
|
+
[ -z "$sql" ] && exit 0
|
|
77
|
+
|
|
78
|
+
# Дальше разбираем без учёта регистра: запрос, разбитый на строки, — тот же запрос.
|
|
79
|
+
flat="$(printf '%s' "$sql" | tr '\n' ' ' | tr '[:upper:]' '[:lower:]')"
|
|
80
|
+
|
|
81
|
+
deny() {
|
|
82
|
+
echo "$1 Адресуй строки по первичному ключу: перечисление идентификаторов затрагивает ровно столько строк, сколько их названо, и промах виден до выполнения. Если адресация по идентификатору здесь не подходит, поставь маркер destructive-ok в текст запроса с объяснением — решение тогда примет владелец." >&2
|
|
83
|
+
exit 2
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
ask() {
|
|
87
|
+
jq -n --arg r "$1" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"ask",permissionDecisionReason:$r}}' 2>/dev/null
|
|
88
|
+
exit 0
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Бой: адреса знает профиль. Там любая запись отказывается, и опт-аут не действует.
|
|
92
|
+
profile="${CLAUDE_PROJECT_DIR:-.}/{{projectProfile}}"
|
|
93
|
+
if [ -f "$profile" ]; then
|
|
94
|
+
# shellcheck disable=SC1090
|
|
95
|
+
. "$profile" 2>/dev/null
|
|
96
|
+
if [ -n "${RT_PROD_DSN:-}" ] && printf '%s' "$flat" | grep -qF "$RT_PROD_DSN"; then
|
|
97
|
+
case "$flat" in
|
|
98
|
+
*insert\ *|*update\ *|*delete\ *|*drop\ *|*truncate\ *|*alter\ *)
|
|
99
|
+
echo "Запись в боевое хранилище запрещена совсем: схема меняется миграцией через выкатку, данные — через интерфейс." >&2
|
|
100
|
+
exit 2 ;;
|
|
101
|
+
esac
|
|
102
|
+
fi
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
opt_out=0
|
|
106
|
+
case "$flat" in *destructive-ok*) opt_out=1 ;; esac
|
|
107
|
+
|
|
108
|
+
case "$flat" in
|
|
109
|
+
*drop\ *|*truncate\ *|*alter\ table*)
|
|
110
|
+
[ "$opt_out" = 1 ] && ask "Схемная команда с маркером destructive-ok. Решение за тобой."
|
|
111
|
+
deny "Снос, очистка или правка схемы запросом." ;;
|
|
112
|
+
esac
|
|
113
|
+
|
|
114
|
+
case "$flat" in
|
|
115
|
+
*delete\ from*|*update\ *set\ *)
|
|
116
|
+
# Условие по идентификатору — единственная форма, где число задетых строк известно
|
|
117
|
+
# заранее. Отбор по подстроке им не является, даже когда сегодня совпадает точно.
|
|
118
|
+
if printf '%s' "$flat" | grep -qE 'where[^;]*\b(id|uuid)\b[[:space:]]*(=|in[[:space:]]*\()'; then
|
|
119
|
+
ask "Адресная правка данных. Решение за тобой."
|
|
120
|
+
fi
|
|
121
|
+
[ "$opt_out" = 1 ] && ask "Правка без адресации по идентификатору, с маркером destructive-ok. Решение за тобой."
|
|
122
|
+
deny "Удаление или обновление без условия по идентификатору." ;;
|
|
123
|
+
esac
|
|
124
|
+
|
|
125
|
+
case "$flat" in
|
|
126
|
+
*insert\ into*) ask "Вставка данных. Решение за тобой." ;;
|
|
127
|
+
esac
|
|
128
|
+
|
|
129
|
+
exit 0
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: angular-patterns-state
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: angular-patterns
|
|
5
|
+
description: Паттерн правила angular-patterns. Брать при объявлении состояния и потоков в классе фронтового каркаса — реактивные входы и выходы, производные значения, состояние службы, долгоживущая подписка с источником действия. Не брать для раскладки файла компонента — это паттерн component-structure-new.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Состояние и потоки
|
|
9
|
+
|
|
10
|
+
Паттерн правила `angular-patterns`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/frontend-application.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Объявляется состояние компонента или службы.
|
|
16
|
+
- Появляется поток, на который надо подписаться.
|
|
17
|
+
- Значение считается из другого значения.
|
|
18
|
+
|
|
19
|
+
## Реактивные входы и выходы
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
public readonly data: InputSignal<Item[]> = input.required<Item[]>();
|
|
23
|
+
public readonly isNarrow: InputSignal<boolean | undefined> = input<boolean>();
|
|
24
|
+
public readonly save: OutputEmitterRef<void> = output<void>();
|
|
25
|
+
|
|
26
|
+
protected readonly myButton: Signal<ElementRef | undefined> = viewChild<ElementRef>('button');
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Декораторной формы входов, выходов и запросов к разметке в дереве нет: у неё нет типа, который
|
|
30
|
+
видно в месте использования, и нет реактивности, на которую можно подписаться.
|
|
31
|
+
|
|
32
|
+
## Производное значение — вычисляемое, а не эффект
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
✗ effect((): void => { this.count.set(this.items().length); });
|
|
36
|
+
✓ protected readonly count: Signal<number> = computed((): number => this.items().length);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Эффект, кладущий значение в реактивное поле, — это ручной пересчёт, и он рано или поздно
|
|
40
|
+
отстаёт от источника. Геттера в компоненте не заводить: он пересчитывается на каждой
|
|
41
|
+
перерисовке, и цена его не видна ни в одном месте кода.
|
|
42
|
+
|
|
43
|
+
## Состояние службы
|
|
44
|
+
|
|
45
|
+
Наружу — только чтение:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
@Injectable({ providedIn: 'root' })
|
|
49
|
+
export class DomainStateService {
|
|
50
|
+
readonly #items: WritableSignal<Item[]> = signal<Item[]>([]);
|
|
51
|
+
|
|
52
|
+
public readonly items: Signal<Item[]> = this.#items.asReadonly();
|
|
53
|
+
public readonly itemCount: Signal<number> = computed((): number => this.#items().length);
|
|
54
|
+
|
|
55
|
+
public addItem(item: Item): void {
|
|
56
|
+
this.#items.update((items: Item[]): Item[] => [...items, item]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Подписка объявляется один раз
|
|
62
|
+
|
|
63
|
+
Метод действия толкает значение в источник, подписка живёт при создании владельца:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
readonly #loadSource: Subject<void> = new Subject<void>();
|
|
67
|
+
readonly #destroyRef: DestroyRef = inject(DestroyRef);
|
|
68
|
+
|
|
69
|
+
constructor() {
|
|
70
|
+
this.#loadSource
|
|
71
|
+
.pipe(
|
|
72
|
+
switchMap((): Observable<IResult> => this.#api.getList(this.#query())),
|
|
73
|
+
takeUntilDestroyed(this.#destroyRef)
|
|
74
|
+
)
|
|
75
|
+
.subscribe();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected reload(): void {
|
|
79
|
+
this.#loadSource.next();
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Оператор выбирается по тому, что делать с предыдущим запросом: список берёт последний ответ,
|
|
84
|
+
кнопка не плодит дублей, независимые строки идут параллельно.
|
|
85
|
+
|
|
86
|
+
## Частые промахи
|
|
87
|
+
|
|
88
|
+
- **Подписка внутри метода:** правило линтера отбивает, а вместе с ним отбивается и гонка
|
|
89
|
+
ответов на быстрых нажатиях.
|
|
90
|
+
- **Подписка без гашения:** она переживает владельца и держит уничтоженный экран в памяти.
|
|
91
|
+
- **Поле-поток без суффикса источника:** поток и значение в коде становятся неотличимы.
|
|
92
|
+
- **Параметры конструктора вместо функции внедрения** — везде, включая базовые классы.
|
|
93
|
+
- **Эффект без снятия слежения там, где зависимость не нужна:** он просыпается на каждое чужое
|
|
94
|
+
изменение.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-layer-pair
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: api-layer
|
|
5
|
+
description: Паттерн правила api-layer. Брать при заведении или правке слоя обращения к серверу во фронтовом домене — готовые фасад и служба, единственный вход выборки, общий конвертер страницы, типы порядка и отбора при сущности.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Фасад и служба домена
|
|
9
|
+
|
|
10
|
+
Паттерн правила `api-layer`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/frontend-application.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Заводится слой обращения к серверу у нового домена.
|
|
16
|
+
- Список переводится на общую выборку.
|
|
17
|
+
- Появляется новый обработчик, за которым ходит экран.
|
|
18
|
+
|
|
19
|
+
## Фасад
|
|
20
|
+
|
|
21
|
+
Принимает запрос контракта, отдаёт ответ контракта, ожидание заворачивает в поток. Ни выборки,
|
|
22
|
+
ни перевода моделей в нём нет:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
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));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Метод, которого у домена нет, не объявляется: список читают все, правят не все.
|
|
36
|
+
|
|
37
|
+
## Служба
|
|
38
|
+
|
|
39
|
+
Принимает доменные модели, отдаёт их же. Тип контракта до стора и шаблона не доходит:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export class EntityApiService implements IListApiService<IEntity.State, ESortProperty, EFilterProperty, IEntity.Draft> {
|
|
43
|
+
public getList(query: IEntity.Query): Observable<IEntity.ListResult> {
|
|
44
|
+
return this.#facade
|
|
45
|
+
.getList({ query: this.#queryMapper.mapTo(query) })
|
|
46
|
+
.pipe(
|
|
47
|
+
map((response: TListResponse): IEntity.ListResult =>
|
|
48
|
+
convertPaginationApiModelToStateModel((item: TItem): IEntity.State => this.#mapper.mapFrom(item), response)
|
|
49
|
+
)
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Выборка — единственный вход: объект, к которому привязан список, род ленты, состояние подписки
|
|
56
|
+
— это условия отбора, и лежат они в её условиях.
|
|
57
|
+
|
|
58
|
+
## Типы выборки при сущности
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
export type Query = IList.Query.State<ESortProperty, EFilterProperty>;
|
|
62
|
+
export type ListResult = IList.Result.State<IEntity.State, ESortProperty, EFilterProperty>;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Перечисления порядка и отбора объявляются рядом с сущностью и повторяют набор имён, по которым
|
|
66
|
+
сортирует и отбирает сервер именно этого домена.
|
|
67
|
+
|
|
68
|
+
## Частые промахи
|
|
69
|
+
|
|
70
|
+
- **Промежуточный объект между ответом и моделью:** ответ ложится в конвертер целиком.
|
|
71
|
+
- **Выборка из своего запроса вместо применённой из ответа:** умолчание сервера и отброшенное
|
|
72
|
+
им условие экран иначе не увидит.
|
|
73
|
+
- **Второй вход рядом с выборкой:** отбор, живущий отдельно, не виден ни стору, ни адресу.
|
|
74
|
+
- **Голая строка в поле порядка:** имя, по которому сервер не сортирует, компилируется и падает
|
|
75
|
+
запросом.
|
|
76
|
+
- **Один класс на две сущности:** подмена источника одной потянет за собой правку другой.
|
|
77
|
+
- **Служба на обещаниях в новом сторе:** основа списочного стора работает потоками.
|
|
78
|
+
- **Своя копия общих переводчиков страницы, порядка и отбора** — её ловит проверка повторов.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-verification-measure
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: browser-verification
|
|
5
|
+
description: Паттерн правила browser-verification. Брать, когда вывод о вёрстке надо подкрепить числом — готовые замеры, разбивка вычисленного значения по всем узлам, узкий экран вложенной рамкой, ловушки инструмента снимка экрана. Не брать для подъёма стенда — это паттерн browser-verification-stand.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Замер вместо взгляда
|
|
9
|
+
|
|
10
|
+
Паттерн правила `browser-verification`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/verifiability.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Правилась вёрстка, и надо подтвердить результат.
|
|
16
|
+
- Экран выглядит не так, а искать нечего: поиск по коду отвечает «чисто».
|
|
17
|
+
- Проверяется узкий экран.
|
|
18
|
+
|
|
19
|
+
## Вывод подкрепляется числом
|
|
20
|
+
|
|
21
|
+
Вычисленный стиль, прямоугольник элемента, контраст, совпадение центров, попадание в видимую
|
|
22
|
+
область. «Выглядит нормально» результатом проверки не является.
|
|
23
|
+
|
|
24
|
+
Замер отвечает только на тот вопрос, который задали. Совпадение перечисленных свойств ничего не
|
|
25
|
+
говорит о правиле, которого в списке замера нет. Если исходники образца доступны, расхождение
|
|
26
|
+
ищется чтением, а замер остаётся проверкой результата.
|
|
27
|
+
|
|
28
|
+
Вид элемента, которого сегодня не видно ни на одном экране, замером не подтверждается, и
|
|
29
|
+
правило о нём остаётся гипотезой.
|
|
30
|
+
|
|
31
|
+
## Значение, которого в коде нет
|
|
32
|
+
|
|
33
|
+
Гарнитуру, межстрочный интервал, цвет и оформление элементам формы задаёт браузер, и в дереве
|
|
34
|
+
этих значений нет. Поиск по коду на такой дефект отвечает «чисто», линтер и сборка молчат.
|
|
35
|
+
Ищется разбивкой вычисленного значения по всем узлам страницы, а не замером у пары элементов:
|
|
36
|
+
|
|
37
|
+
```javascript
|
|
38
|
+
[...document.querySelectorAll('*')].reduce((acc, el) => {
|
|
39
|
+
const key = getComputedStyle(el).fontFamily;
|
|
40
|
+
acc[key] = (acc[key] ?? 0) + 1;
|
|
41
|
+
return acc;
|
|
42
|
+
}, {});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Счёт годится любому наследуемому свойству: смотрится не одно значение, а число узлов с
|
|
46
|
+
неожиданным. Так находятся десятки элементов, набранных не той гарнитурой, — по одному их не
|
|
47
|
+
заметил бы никто.
|
|
48
|
+
|
|
49
|
+
## Узкий экран
|
|
50
|
+
|
|
51
|
+
Изменение размера окна не работает, когда браузер в полноэкранном режиме: инструмент рапортует
|
|
52
|
+
успех, ширина не меняется, медиазапросы остаются широкими. Узкие ширины проверяются во
|
|
53
|
+
вложенной рамке нужной ширины — внутри неё запрос ширины считается от рамки, и рамка своей
|
|
54
|
+
толщиной уменьшает внутреннюю ширину.
|
|
55
|
+
|
|
56
|
+
Правка числа элементов в контейнере — это правка раскладки: она проверяется на узкой ширине, а
|
|
57
|
+
не только кодами ответа.
|
|
58
|
+
|
|
59
|
+
## Ловушки инструмента снимка экрана
|
|
60
|
+
|
|
61
|
+
- **Координаты нажатия — координаты снимка, а не страницы.** При широком окне снимок приходит
|
|
62
|
+
уже, и нажатие по «увиденной» координате уходит мимо, давая ложный сигнал: пересчитывать по
|
|
63
|
+
фактическому масштабу либо целиться поиском элемента.
|
|
64
|
+
- **Область приближения должна целиком лежать внутри видимой области.**
|
|
65
|
+
- **Между нажатиями обязательно ожидание:** цикл «нажал — прочитал разметку» без него читает
|
|
66
|
+
состояние до перерисовки и возвращает устаревшие значения.
|
|
67
|
+
- **Выбор браузера протухает.** На длинной проверке это срабатывает посреди работы — не сбой
|
|
68
|
+
стенда, а повод повторить вызов и продолжить.
|
|
69
|
+
|
|
70
|
+
## Поведение маршрутизации воспроизводится нажатиями
|
|
71
|
+
|
|
72
|
+
Подстановка адреса, программный переход и заход по прямой ссылке поднимают приложение заново, и
|
|
73
|
+
накопленного состояния — открытой панели, стража прошлого экрана — у него нет. Чистый проход по
|
|
74
|
+
адресам читается как «дефект не подтверждается».
|
|
75
|
+
|
|
76
|
+
## Частые промахи
|
|
77
|
+
|
|
78
|
+
- **Комментарий в конфиге — гипотеза наравне с прочими.** Утверждение о поведении кэша
|
|
79
|
+
переживает несколько кругов разбора кода и опровергается одним запросом.
|
|
80
|
+
- **Вывод «дефекта нет, это кэш» закрывает разбор**, поэтому принимается только после проверки
|
|
81
|
+
на чистой сборке.
|
|
82
|
+
- **Дефект в клиентском куске от компиляции до правки выглядит как дефект кода** — признак
|
|
83
|
+
сборки для разработки — имена файлов без хеша.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-verification-stand
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: browser-verification
|
|
5
|
+
description: Паттерн правила browser-verification. Брать, когда нужен честный стенд — прод-сборка, стенд под настоящим прокси, вход в приложение, разбор того, что висит на порту, стенд серверной стороны с переменными окружения. Не брать для замеров вёрстки — это паттерн browser-verification-measure.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Честный стенд
|
|
9
|
+
|
|
10
|
+
Паттерн правила `browser-verification`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/verifiability.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Проверяется то, чего на сервере разработки не видно: разметка от сервера, локали, кэш,
|
|
16
|
+
перенаправления, заголовки, размер сборки.
|
|
17
|
+
- Порт отвечает не тем, чего ждали.
|
|
18
|
+
- Нужен вход в приложение.
|
|
19
|
+
|
|
20
|
+
## Сначала — что отвечает на порту
|
|
21
|
+
|
|
22
|
+
До первого запроса, а не после непонятного ответа:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
lsof -nP -iTCP:<порт> -sTCP:LISTEN
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
На порту регулярно висит собранный артефакт из прошлой сессии: он отвечает успехом на старом
|
|
29
|
+
коде, а заведённого в ветке обработчика у него нет вовсе. Таких процессов бывает несколько, и
|
|
30
|
+
снимать надо все — по идентификатору из вывода, каждый: завершение по шаблону команды не
|
|
31
|
+
попадает ни в один.
|
|
32
|
+
|
|
33
|
+
## Прод-сборка
|
|
34
|
+
|
|
35
|
+
Собранный сервер поднимается прямо, а не через сервер разработки: гард ловит запуск сервера
|
|
36
|
+
разработки, пакетные раннеры и статические серверы, а запуск собранного сервера пропускает.
|
|
37
|
+
|
|
38
|
+
Приложению, отдающему статику, нужен явный базовый адрес — без него стенд отдаёт пустую
|
|
39
|
+
страницу без единой ошибки в консоли. Стенду, на котором нужны отладочные инструменты каркаса,
|
|
40
|
+
нужна сборка для разработки: прод-сборка их не публикует. Выводы о размере сборки и минификации
|
|
41
|
+
на такой сборке делать нельзя.
|
|
42
|
+
|
|
43
|
+
## Вход в приложение
|
|
44
|
+
|
|
45
|
+
Сессия кладётся в хранилище браузера ровно в той форме, в какой её читает приложение: значение,
|
|
46
|
+
записанное иначе, приложение молча не увидит. Токен не подписывается руками, а берётся у живого
|
|
47
|
+
сервера входом.
|
|
48
|
+
|
|
49
|
+
Взять уже открытую сессию нельзя: чтение хранилища чужого профиля блокируется. Оба пути к
|
|
50
|
+
своему стенду упираются в пароль, поэтому остаётся третий — смотреть там, где вход уже сделан.
|
|
51
|
+
Свой стенд нужен, только когда проверяют прод-сборку, базовый адрес или конфиг прокси; чтобы
|
|
52
|
+
просто посмотреть экраны, он не нужен.
|
|
53
|
+
|
|
54
|
+
## Стенд серверной стороны
|
|
55
|
+
|
|
56
|
+
Собранный артефакт поднимается на свободном порту, а не на рабочем: на рабочем отвечает сервер
|
|
57
|
+
владельца, и окружение у него не то, которое проверяется.
|
|
58
|
+
|
|
59
|
+
Переменные окружения задаются в самой команде, по одной на проверяемый случай. Отказ на старте
|
|
60
|
+
— такой же результат проверки, как успешный ответ: приложение, упавшее при сборке зависимостей,
|
|
61
|
+
порт не слушает вовсе, и это видно по списку слушателей, а не по тексту в консоли.
|
|
62
|
+
|
|
63
|
+
Обработчик зовётся полным именем, как его объявляет контракт. Токен подписывается руками только
|
|
64
|
+
там, где проверяется сама подпись; во всех остальных случаях он берётся у живого сервера входом
|
|
65
|
+
— рукописный скрывает расхождение состава притязаний.
|
|
66
|
+
|
|
67
|
+
## Стенд под настоящим прокси
|
|
68
|
+
|
|
69
|
+
Кэш, перенаправления и заголовки живут в конфиге прокси, а не в приложении, и любой вывод о них
|
|
70
|
+
с голого сервера отдачи страниц неверен. Устройство такого стенда — паттерн `testing-e2e`.
|
|
71
|
+
|
|
72
|
+
## Частые промахи
|
|
73
|
+
|
|
74
|
+
- **Порт занят чужой сборкой, а ответ читается как дефект ветки.** Сначала список слушателей,
|
|
75
|
+
потом запрос.
|
|
76
|
+
- **Стенд без явного базового адреса отдаёт пустую страницу** и ни одной ошибки при этом не
|
|
77
|
+
печатает.
|
|
78
|
+
- **Токен, подписанный руками, скрывает расхождение состава притязаний** — вход берётся у
|
|
79
|
+
живого сервера.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: component-structure-new
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: component-structure
|
|
5
|
+
description: Паттерн правила component-structure. Брать при заведении или правке файла компонента — порядок свойств декоратора, группировка импортов, раскладка полей класса, договорённости шаблона и якорь для спек. Не брать для состояния и потоков — это паттерн angular-patterns-state.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Файл компонента
|
|
9
|
+
|
|
10
|
+
Паттерн правила `component-structure`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/frontend-application.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Заводится новый компонент.
|
|
16
|
+
- Правится декоратор, список импортов или шаблон существующего.
|
|
17
|
+
|
|
18
|
+
## Декоратор: порядок свойств
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
@Component({
|
|
22
|
+
selector: '<префикс>-component-name', // 1. селектор
|
|
23
|
+
templateUrl: './component-name.component.html', // 2. шаблон
|
|
24
|
+
styleUrl: './component-name.component.scss', // 3. стиль, в единственном числе
|
|
25
|
+
changeDetection: ChangeDetectionStrategy.OnPush, // 4. стратегия перерисовки
|
|
26
|
+
imports: [
|
|
27
|
+
// 5. импорты, группами
|
|
28
|
+
// каркас
|
|
29
|
+
FormsModule,
|
|
30
|
+
|
|
31
|
+
// директивы разметки
|
|
32
|
+
BlockDirective,
|
|
33
|
+
ElemDirective,
|
|
34
|
+
|
|
35
|
+
// компоненты
|
|
36
|
+
SomeChildComponent,
|
|
37
|
+
],
|
|
38
|
+
providers: [], // 6. провайдеры
|
|
39
|
+
host: { class: '<префикс>-component-name' }, // 7. привязки хоста
|
|
40
|
+
})
|
|
41
|
+
export class ComponentNameComponent {
|
|
42
|
+
readonly #someService: SomeService = inject(SomeService);
|
|
43
|
+
|
|
44
|
+
public readonly data: InputSignal<Item[]> = input.required<Item[]>();
|
|
45
|
+
public readonly save: OutputEmitterRef<void> = output<void>();
|
|
46
|
+
|
|
47
|
+
protected readonly items: WritableSignal<Item[]> = signal<Item[]>([]);
|
|
48
|
+
protected readonly itemCount: Signal<number> = computed((): number => this.items().length);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Группирующие комментарии в импортах обязательны: без них список растёт вперемешку, и первое,
|
|
53
|
+
что в нём теряется, — свои компоненты среди чужих.
|
|
54
|
+
|
|
55
|
+
## Шаблон
|
|
56
|
+
|
|
57
|
+
- Самозакрывающиеся теги у компонентов без содержимого.
|
|
58
|
+
- Лишних обёрток нет — корнем работает хост, класс блока приходит его привязкой.
|
|
59
|
+
- Сложный шаблон объявляет блок в корне отдельной директивой.
|
|
60
|
+
- Один компонент в обеих ветках условия — это условная привязка:
|
|
61
|
+
|
|
62
|
+
```html
|
|
63
|
+
<!-- ✗ -->
|
|
64
|
+
@if (isRangeMode()) {
|
|
65
|
+
<calendar [rangeMode]="true" />
|
|
66
|
+
} @else {
|
|
67
|
+
<calendar [rangeMode]="false" />
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
<!-- ✓ -->
|
|
71
|
+
<calendar [rangeMode]="isRangeMode()" />
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- Прокрутка по документу — средствами маршрутизатора, а не ссылкой на фрагмент: при объявленном
|
|
75
|
+
базовом адресе браузер разрешает фрагмент относительно него и уходит в полную навигацию.
|
|
76
|
+
|
|
77
|
+
## Якорь для спек — на каждый интерактивный элемент
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<button qa-dataid="calendar-retry-prices" type="button" (click)="retryPrices.emit()">Повторить</button>
|
|
81
|
+
<div rtElem="grid" qa-dataid="admin-calendar-grid"></div>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- Значение — через дефис, по смыслу элемента, без имени компонента-обёртки.
|
|
85
|
+
- Уникальность — в пределах экрана; повторяющиеся элементы списка носят один якорь и
|
|
86
|
+
различаются атрибутами данных.
|
|
87
|
+
- Декоративный элемент помечается признаком пропуска на самом теге.
|
|
88
|
+
|
|
89
|
+
## Частые промахи
|
|
90
|
+
|
|
91
|
+
- **Чужой префикс селектора** — компонент перестаёт узнаваться как свой.
|
|
92
|
+
- **Множественная форма свойства стилей вместо единственной** — стиль молча не подключается.
|
|
93
|
+
- **Вызов метода в привязке** — отбивается линтером; замена — вычисляемое значение, а там, где
|
|
94
|
+
оно зависит от контекста шаблона, — чистый преобразователь.
|
|
95
|
+
- **Обёртка, которая существует только чтобы быть контейнером раскладки вокруг всех детей:** её
|
|
96
|
+
раскладка уезжает на хост.
|
|
97
|
+
- **Глубокий относительный импорт между либами** вместо алиаса.
|
|
98
|
+
- **Своя разметка вместо готового компонента** — правило `reuse-first`.
|