@rt-tools/agent-kit 0.1.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 +88 -9
- 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/laws/access.md +3 -12
- package/assets/laws/admin-lists.md +9 -21
- package/assets/laws/admin-navigation.md +3 -15
- package/assets/laws/code-structure.md +5 -16
- package/assets/laws/delivery.md +2 -16
- package/assets/laws/entity-editing.md +7 -20
- package/assets/laws/entity-models.md +10 -17
- package/assets/laws/frontend-application.md +3 -14
- package/assets/laws/lib-imports.md +0 -13
- package/assets/laws/locales.md +2 -11
- package/assets/laws/project-documentation.md +7 -20
- package/assets/laws/reuse-first.md +0 -9
- package/assets/laws/search-visibility.md +0 -13
- package/assets/laws/shared-code.md +0 -13
- package/assets/laws/verifiability.md +0 -13
- 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/bin/agent-kit.d.ts +1 -1
- package/bin/agent-kit.d.ts.map +1 -1
- package/bin/agent-kit.js +63 -7
- package/bin/agent-kit.js.map +1 -1
- package/bin/prompt.d.ts +9 -0
- package/bin/prompt.d.ts.map +1 -0
- package/bin/prompt.js +57 -0
- package/bin/prompt.js.map +1 -0
- package/index.d.ts +2 -0
- package/index.d.ts.map +1 -1
- package/index.js +2 -0
- package/index.js.map +1 -1
- package/lib/assets.d.ts +10 -2
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +24 -28
- package/lib/assets.js.map +1 -1
- package/lib/catalog.d.ts +44 -0
- package/lib/catalog.d.ts.map +1 -0
- package/lib/catalog.js +86 -0
- package/lib/catalog.js.map +1 -0
- package/lib/commands.d.ts +13 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +106 -11
- 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 +29 -1
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +43 -6
- package/lib/config.js.map +1 -1
- package/lib/picker.d.ts +47 -0
- package/lib/picker.d.ts.map +1 -0
- package/lib/picker.js +112 -0
- package/lib/picker.js.map +1 -0
- 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.1.0.tgz +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: styling-bem-component
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: styling-bem
|
|
5
|
+
description: Паттерн правила styling-bem. Брать при правке стилей компонента источника вида — готовый хост, модификаторы, значения только токенами, перебивание умолчаний источника вида. Не брать для раскладки экрана — это паттерн styling-bem-layout.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Стили компонента
|
|
9
|
+
|
|
10
|
+
Паттерн правила `styling-bem`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/frontend-application.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Правится компонент источника вида.
|
|
16
|
+
- Правится оформление отдельного компонента.
|
|
17
|
+
- Нужно перебить умолчание источника вида в своём компоненте.
|
|
18
|
+
|
|
19
|
+
## Хост — сам блок
|
|
20
|
+
|
|
21
|
+
Хост несёт класс блока, раскладка на хосте, элементы вложены внутрь:
|
|
22
|
+
|
|
23
|
+
```scss
|
|
24
|
+
:host {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
|
|
27
|
+
.<блок > {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
gap: var(--<токен отступа>);
|
|
30
|
+
align-items: center;
|
|
31
|
+
|
|
32
|
+
&--<модификатор > {
|
|
33
|
+
border-radius: var(--<токен скругления>);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Модификатор — вложенным селектором или привязкой класса на хосте. У экрана вне источника вида
|
|
40
|
+
такого раздела нет: раскладка на хосте — это общий слой приложения, а не файл экрана.
|
|
41
|
+
|
|
42
|
+
## Значения — только токенами
|
|
43
|
+
|
|
44
|
+
```scss
|
|
45
|
+
✗ color: #fff;
|
|
46
|
+
✗ box-shadow: 0 1px 2px rgb(0 0 0 / 12%);
|
|
47
|
+
✓ color: var(--<токен цвета поверхности>);
|
|
48
|
+
✓ box-shadow: var(--<токен тени>);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Составное значение берётся готовым токеном целиком, а не собирается из частей. Переменные
|
|
52
|
+
препроцессора для значений оформления не используются вовсе: они разрешаются на сборке и темой
|
|
53
|
+
не переключаются.
|
|
54
|
+
|
|
55
|
+
## Перебить умолчание источника вида
|
|
56
|
+
|
|
57
|
+
Стили компонента без инкапсуляции весят столько же, сколько умолчания источника вида: у обоих
|
|
58
|
+
селекторов по одному классу, и исход решает порядок подключения. Переопределение пишется
|
|
59
|
+
потомком блока — так вес растёт на единицу, и порядок перестаёт что-либо решать:
|
|
60
|
+
|
|
61
|
+
```scss
|
|
62
|
+
& &__item {
|
|
63
|
+
color: var(--<токен приглушённого текста>);
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Частые промахи
|
|
68
|
+
|
|
69
|
+
- **Комментарий-выключатель линтера стилей:** селекторы объединяются вложенностью, а не
|
|
70
|
+
отключением правила.
|
|
71
|
+
- **Приоритетное объявление:** запрещено, и оформление, заданное на месте, перебить им всё
|
|
72
|
+
равно не выйдет.
|
|
73
|
+
- **Новые объявления при переносе стилей:** переносится существующее, новое появляется, только
|
|
74
|
+
когда задача — фича.
|
|
75
|
+
- **Замечание линтера, лежавшее в файле раньше, оставлено:** правятся все, и новые, и старые.
|
|
76
|
+
- **Своё наследование гарнитуры в компоненте:** оно включено глобально, и местное объявление
|
|
77
|
+
только расходится с ним.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: styling-bem-layout
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: styling-bem
|
|
5
|
+
description: Паттерн правила styling-bem. Брать при сборке экрана раздела, формы, панели или окна — блоки общего слоя, разметка директивами блока и элемента, свой элемент в чужом поддереве, признак того, что правка идёт не туда. Не брать для стилей компонента источника вида — это паттерн styling-bem-component.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Экран на общем слое раскладки
|
|
9
|
+
|
|
10
|
+
Паттерн правила `styling-bem`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/frontend-application.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Собирается экран раздела, форма, содержимое панели или модального окна.
|
|
16
|
+
- В файле стилей экрана появляется раскладка.
|
|
17
|
+
|
|
18
|
+
## Файл стилей экрана по умолчанию пустой
|
|
19
|
+
|
|
20
|
+
Раскладка объявлена один раз в общем слое приложения, а экран её только применяет. Блоков этого
|
|
21
|
+
слоя немного, и каждый отвечает за свой род экрана: экран раздела с заголовком и прокруткой,
|
|
22
|
+
форма с разделами и строками полей, содержимое панели правки, содержимое модального окна.
|
|
23
|
+
|
|
24
|
+
Блок вешается на хост, элементы получают классы от директивы блока в корне шаблона:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
host: { class: '<блок экрана>' },
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<ng-container rtBlock="<блок экрана>">
|
|
32
|
+
<header rtElem="header">
|
|
33
|
+
<div rtElem="header-main">
|
|
34
|
+
<h1 rtElem="title">{{ '<ключ заголовка>' | <перевод> }}</h1>
|
|
35
|
+
</div>
|
|
36
|
+
</header>
|
|
37
|
+
</ng-container>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Директива блока на контейнере без своего узла класса не ставит — узел это комментарий. Второго
|
|
41
|
+
носителя класса блока не нужно, и своей обёртки тоже.
|
|
42
|
+
|
|
43
|
+
## Свой элемент в чужом поддереве
|
|
44
|
+
|
|
45
|
+
Пара директив на одном элементе; класс блока при этом не ставится, только класс элемента:
|
|
46
|
+
|
|
47
|
+
```html
|
|
48
|
+
<div rtBlock="<свой блок>" rtElem="confirm"></div>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Класс получается составной, а потомки внутри считаются от того же блока.
|
|
52
|
+
|
|
53
|
+
## Своё в файле экрана
|
|
54
|
+
|
|
55
|
+
Остаётся только то, что принадлежит одному этому экрану и в общий слой не просится, — сетка
|
|
56
|
+
календаря, карта на странице записи, лента переписки. Рядом пишется, почему это не общее.
|
|
57
|
+
|
|
58
|
+
## Частые промахи
|
|
59
|
+
|
|
60
|
+
- **Раскладка на хосте в файле экрана.** Одинаковые с виду экраны от неё расходятся: заголовок
|
|
61
|
+
страницы, объявленный в каждом экране заново, живёт тремя разными кеглями.
|
|
62
|
+
- **Директива элемента без предка, объявившего блок:** отрисовка падает во время работы, сборка
|
|
63
|
+
и линтер молчат.
|
|
64
|
+
- **Класс, у которого правило сняли, а директива в шаблоне осталась:** ловит проверка «класс
|
|
65
|
+
без правила».
|
|
66
|
+
- **Элемент чужого блока в чужом поддереве:** имя блока приходит от предка, и подмешать его
|
|
67
|
+
нечем.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-e2e
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: testing
|
|
5
|
+
description: Паттерн правила testing. Брать при правке и прогоне сквозных спек — что вообще закрывается сквозной спекой, прогон одним рабочим, стенд из прод-сборки под настоящим прокси, выключатели разрушающих спек. Не брать для юнитов — это паттерн testing-unit.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Сквозные спеки
|
|
9
|
+
|
|
10
|
+
Паттерн правила `testing`. Что при этом должно быть верно — закон
|
|
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
|
+
Полный набор спек с настоящей сессией гоняется **одним рабочим**: такие тесты правят одни и те
|
|
38
|
+
же записи живого хранилища и в параллельном прогоне мешают друг другу. Одни и те же файлы дают
|
|
39
|
+
падения на многих рабочих и ноль на одном.
|
|
40
|
+
|
|
41
|
+
## Стенд из прод-сборки под настоящим прокси
|
|
42
|
+
|
|
43
|
+
Дешевле полного состава прода: один контейнер прокси с боевым конфигом, а приложения за ним —
|
|
44
|
+
процессы на хосте.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
<сборка всех приложений>
|
|
48
|
+
<адрес хранилища> <порт> node <собранный сервер> &
|
|
49
|
+
docker run -d --name <стенд> \
|
|
50
|
+
--add-host <имя апстрима>:host-gateway \
|
|
51
|
+
-p <внешний порт>:80 \
|
|
52
|
+
-v "$PWD/<конфиг прокси>:/etc/nginx/conf.d/default.conf:ro" \
|
|
53
|
+
-v "$PWD/<каталог сборки>:/usr/share/nginx/html:ro" \
|
|
54
|
+
<образ прокси>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Порты апстримов зашиты в конфиг именами — менять их нельзя, подстановка хоста заменяет только
|
|
58
|
+
адрес.
|
|
59
|
+
|
|
60
|
+
## Выключатели
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
const РАЗРЕШЕНО = окружение['<имя переменной>'] === '1';
|
|
64
|
+
тест.пропустить(!РАЗРЕШЕНО, 'меняет живой адрес записи: включается переменной');
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- Спека, необратимо меняющая данные стенда, по умолчанию пропускается и включается своей
|
|
68
|
+
переменной.
|
|
69
|
+
- Проверки, которым нужен прокси, просыпаются вместе с адресом стенда. Голый сервер отдачи
|
|
70
|
+
страниц их не проходит, и падения выглядят регрессией.
|
|
71
|
+
- Правило линтера, запрещающее выключенный тест, снимается в конфиге: выключатель здесь — приём,
|
|
72
|
+
а не забытый пропуск.
|
|
73
|
+
|
|
74
|
+
## Частые промахи
|
|
75
|
+
|
|
76
|
+
- **Порт приложения занимать осторожно:** стенд разработчика ходит по тому же имени через
|
|
77
|
+
подстановку хоста, и пока на нём висит чужой процесс, стенд отдаёт чужую сборку.
|
|
78
|
+
- **Браузер обычно установлен один.** Ошибка «Executable doesn't exist» разобрана в правиле
|
|
79
|
+
`testing`: она же приходит после смены версии прогонщика.
|
|
80
|
+
- **Спеки без учётных данных пропускаются молча** — прогон выглядит успешным, а проверено
|
|
81
|
+
меньше половины.
|
|
82
|
+
- **Конфиг прокси монтируется каталогом, а не одиночным файлом:** редактор пересоздаёт файл, и
|
|
83
|
+
контейнеру остаётся обрезанная копия.
|
|
84
|
+
- **Подменяется не только то, что запрашивает экран, но и то, что запрашивает шапка.** Общий
|
|
85
|
+
запрос идёт на каждом экране; без подмены на него отвечает настоящий сервер, поддельный вход
|
|
86
|
+
он отбивает, перехватчик сбрасывает сессию — и десятки тестов падают на пропавшей шапке, что
|
|
87
|
+
выглядит дефектом экрана.
|
|
88
|
+
- **Каталог сборки, удалённый под смонтированным томом, оставляет контейнер с пустым корнем:**
|
|
89
|
+
стенд отвечает отказом на всё, и падают сразу все тесты. Контейнер после такого удаления
|
|
90
|
+
пересоздаётся.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing-unit
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: testing
|
|
5
|
+
description: Паттерн правила testing. Брать при заведении или правке файла спеки рядом с исходником — раскладка блоков, сборщик фикстур, идентификатор сценария в заголовке, спека обработчика серверной стороны с рукописным двойником хранилища, разовый тест-доказательство. Не брать для сквозных спек — это паттерн testing-e2e.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Спека на чистую функцию и на обработчик
|
|
9
|
+
|
|
10
|
+
Паттерн правила `testing`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/verifiability.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Заводится или правится файл спеки рядом с исходником.
|
|
16
|
+
- Логику надо вынести из компонента или службы, чтобы её стало чем проверить.
|
|
17
|
+
- Пишется спека на обработчик серверной стороны.
|
|
18
|
+
|
|
19
|
+
## Импорты явные
|
|
20
|
+
|
|
21
|
+
Даже когда прогонщик кладёт свои имена в глобальную область, список пишется: файл, читаемый
|
|
22
|
+
без конфига, не должен зависеть от настройки, о которой в нём ни слова.
|
|
23
|
+
|
|
24
|
+
## Один блок на функцию
|
|
25
|
+
|
|
26
|
+
Имя блока совпадает с именем функции дословно; заголовки тестов — предложения настоящим
|
|
27
|
+
временем, о поведении, а не об устройстве:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
описание('applyDayClick', () => {
|
|
31
|
+
тест('SC-<ПРЕФИКС>-19 — нажатие по занятому дню ничего не меняет', () => {
|
|
32
|
+
ожидать(applyDayClick(day('2026-08-12'), selection)).равно(selection);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Идентификатор сценария из спека домена стоит в начале заголовка, через тире. Краевые случаи —
|
|
38
|
+
отдельные тесты в том же блоке, а не один тест с десятком проверок.
|
|
39
|
+
|
|
40
|
+
## Фикстура собирается функцией с частичной подменой
|
|
41
|
+
|
|
42
|
+
Не повторяющимся литералом: литерал, размноженный по файлу, при первой же новой обязательной
|
|
43
|
+
колонке правится в каждом месте — и в одном обязательно забывается.
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
функция day(iso, подмена = {}) {
|
|
47
|
+
вернуть { iso, dayOfMonth: число(iso.срез(8)), busyNight: занято(iso), past: ложь, ...подмена };
|
|
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
|
+
Признак временного файла — его заголовок не называет ни одного сценария.
|
|
84
|
+
|
|
85
|
+
## Частые промахи
|
|
86
|
+
|
|
87
|
+
- **Либа без своего конфига прогонщика проходит зелёной, не запустив ни одного файла.** Прежде
|
|
88
|
+
чем писать первый тест в либе, проверить, что конфиг рядом есть.
|
|
89
|
+
- **Подмена модуля скрыла бы то, ради чего тест и заводится** — какой именно вызов ушёл в
|
|
90
|
+
хранилище и в каком порядке. Двойник пишется руками.
|
|
91
|
+
- **Своих помощников для проверок не заводить** — утверждения пишутся прямо.
|
|
92
|
+
- **Заголовок с несуществующим идентификатором сценария роняет проверку спеков**, а сами тесты
|
|
93
|
+
при этом остаются зелёными.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: translations-key
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: translations
|
|
5
|
+
description: Паттерн правила translations. Брать, когда в интерфейсе появляется видимый текст — куда положить ключ, как подставить его в разметку и в класс, чем дозаполнить остальные локали и чем проверить полноту. Не брать для перевода содержимого записи — его заполняет серверная сторона.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Ключ перевода
|
|
9
|
+
|
|
10
|
+
Паттерн правила `translations`. Что при этом должно быть верно — закон `{{lawsDir}}/locales.md`.
|
|
11
|
+
|
|
12
|
+
## Когда брать
|
|
13
|
+
|
|
14
|
+
- В интерфейсе появляется любой текст, который видит человек.
|
|
15
|
+
- Правится подпись, пустое состояние, текст отказа, подпись кнопки подтверждения.
|
|
16
|
+
|
|
17
|
+
## Куда кладётся ключ
|
|
18
|
+
|
|
19
|
+
Словари разложены по разделам, и наборы ключей сверяются **внутри раздела**: общее для всех
|
|
20
|
+
приложений, публичная часть, внутренняя часть, письма. Ключ заводится во всех локалях сразу —
|
|
21
|
+
пустое значение считается пропуском, а не переводом.
|
|
22
|
+
|
|
23
|
+
## Подстановка
|
|
24
|
+
|
|
25
|
+
В разметке — преобразователем, в классе — реактивным значением:
|
|
26
|
+
|
|
27
|
+
```html
|
|
28
|
+
<h1 rtElem="title">{{ '<ключ заголовка>' | <перевод> }}</h1>
|
|
29
|
+
<table [emptyMessage]="'<ключ пустого списка>' | <перевод>"></table>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
protected readonly title: Signal<string> = translateSignal('<ключ заголовка>');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Дозаполнить и проверить
|
|
37
|
+
|
|
38
|
+
Дозаполнение недостающего делается командой, а полноту держит тест: он роняет прогон на
|
|
39
|
+
недостающем или пустом ключе. Глазами это не проверяется — ключей тысячи.
|
|
40
|
+
|
|
41
|
+
## Частые промахи
|
|
42
|
+
|
|
43
|
+
- **Текст строкой прямо в шаблоне:** он уедет в интерфейс на одном языке во всех локалях.
|
|
44
|
+
- **Ключ заведён только в паре локалей:** тест полноты падает, но замечают это уже в гейте
|
|
45
|
+
пуша.
|
|
46
|
+
- **Пустая строка вместо перевода:** на экране она выглядит как задуманная — кнопка без
|
|
47
|
+
подписи, заголовок без текста.
|
|
48
|
+
- **Ключ положен не в свой раздел:** наборы сверяются внутри раздела, и расхождение вылезет как
|
|
49
|
+
недостача в другом.
|
|
50
|
+
- **Свой ключ успеха у панели правки записи:** текст успеха принадлежит самой операции и
|
|
51
|
+
заводится во всех локалях сразу.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ts-procedure
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: typescript-conventions
|
|
5
|
+
description: Паттерн правила typescript-conventions. Брать при заведении или правке обработчика серверной стороны — класс с полем метода контракта и методом обработки, зависимости конструктором, имя файла и класса, почему форма именно такая. Не брать для объявления доступа — это паттерн permissions-procedure.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Обработчик серверной стороны
|
|
9
|
+
|
|
10
|
+
Паттерн правила `typescript-conventions`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/code-structure.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- Заводится новый обработчик серверной стороны.
|
|
16
|
+
- Правится тело существующего.
|
|
17
|
+
- Домен переезжает на вертикальную нарезку.
|
|
18
|
+
|
|
19
|
+
## Один обработчик — один класс
|
|
20
|
+
|
|
21
|
+
Файл рядом со своим доменом, класс с публичным полем метода контракта и публичным методом
|
|
22
|
+
обработки. Зависимости приходят конструктором: на серверной стороне внедрение конструкторное, и
|
|
23
|
+
функции внедрения фронта там нет.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
@Injectable()
|
|
27
|
+
@ConnectProcedure()
|
|
28
|
+
@RequiresPermission('<ресурс>:<действие>')
|
|
29
|
+
export class PingProcedure implements IConnectProcedure<typeof HealthService.method.ping> {
|
|
30
|
+
readonly #health: HealthCheckService;
|
|
31
|
+
|
|
32
|
+
public readonly method: typeof HealthService.method.ping = HealthService.method.ping;
|
|
33
|
+
|
|
34
|
+
constructor(health: HealthCheckService) {
|
|
35
|
+
this.#health = health;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public async handle(): Promise<{ status: EHealthStatus }> {
|
|
39
|
+
return { status: (await this.#health.check()).status };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Объявление доступа обязательно, и оно ровно одно — паттерн `permissions-procedure`.
|
|
45
|
+
|
|
46
|
+
## Почему форма такая
|
|
47
|
+
|
|
48
|
+
Прежняя — регистрация с телами в замыканиях внутри вызова роутера — делала обработчик
|
|
49
|
+
недостижимым для спеки: наружу торчал только метод регистрации. А регистрация сервиса целиком
|
|
50
|
+
заглушает каждый непереданный метод ответом «не реализовано», поэтому один сервис контракта не
|
|
51
|
+
мог обслуживаться двумя доменами.
|
|
52
|
+
|
|
53
|
+
Класс решает и то, и другое: метод обработки зовётся спекой напрямую, а реестр кладёт
|
|
54
|
+
обработчики поштучно.
|
|
55
|
+
|
|
56
|
+
## Частые промахи
|
|
57
|
+
|
|
58
|
+
- **Третий суффикс имени файла:** прежние имена уходят вместе с последним переехавшим доменом,
|
|
59
|
+
и новых таких файлов не заводится.
|
|
60
|
+
- **Функция внедрения фронта в классе обработчика:** зависимости идут конструктором.
|
|
61
|
+
- **Тело в замыкании внутри регистрации:** спека до него не дотянется.
|
|
62
|
+
- **Обработчик без объявления доступа:** приложение не поднимется.
|
|
63
|
+
- **Приведение к типу в переводе моделей:** на серверной стороне оно запрещено так же, как во
|
|
64
|
+
фронтовом переводчике.
|
|
65
|
+
- **Свой тип у результата группирующей выборки:** он условный, собирается из аргументов вызова
|
|
66
|
+
и с выписанным руками не сходится.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: angular-patterns
|
|
3
|
+
kind: rule
|
|
4
|
+
law: frontend-application
|
|
5
|
+
description: Правило под закон «Фронтовое приложение». Брать при правке любого класса фронтового каркаса — компонента, стора, сервиса, директивы, преобразователя, стража, перехватчика. Реактивное состояние вместо ручного пересчёта, перерисовка по требованию, место подписки и её владелец. Не действует на серверной стороне. Готовый код — в паттерне angular-patterns-state. Чем это названо здесь — в implementation.md рядом.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Реактивность экрана — каким приёмом
|
|
9
|
+
|
|
10
|
+
Правило под закон `{{lawsDir}}/frontend-application.md`. Закон говорит, что должно быть верно;
|
|
11
|
+
здесь — каким приёмом это держится. Чем названы реактивное значение, входы и выходы и как
|
|
12
|
+
зовётся гашение подписки — `implementation.md` рядом. Раскладка файла компонента —
|
|
13
|
+
`component-structure`, оформление — `styling-bem`, окружение браузера — `platform-access`, слой
|
|
14
|
+
обращения к серверу — `api-layer`. Все пять под одним законом.
|
|
15
|
+
|
|
16
|
+
Правило про фронт: на серверной стороне своя среда, и ничего из перечисленного к ней не
|
|
17
|
+
относится.
|
|
18
|
+
|
|
19
|
+
## Когда берётся
|
|
20
|
+
|
|
21
|
+
Правка любого класса фронтового каркаса: компонента, стора, сервиса, директивы,
|
|
22
|
+
преобразователя, стража, перехватчика.
|
|
23
|
+
|
|
24
|
+
## Что здесь действует
|
|
25
|
+
|
|
26
|
+
- **Подписка объявляется один раз, а не в методе действия.** Метод толкает значение в источник,
|
|
27
|
+
а долгоживущая подписка с переключением, отбрасыванием или очередью объявляется при создании
|
|
28
|
+
владельца.
|
|
29
|
+
- **Подписка гасится вместе с владельцем.** Гашение ставится в тот же поток, где объявлена
|
|
30
|
+
подписка, — иначе поток переживает экран, на котором заведён.
|
|
31
|
+
- **Источник действия носит суффикс в имени.** Иначе поток и значение в коде неотличимы, и
|
|
32
|
+
проталкивание уходит не туда.
|
|
33
|
+
- **Списочный стор наследует общую основу.** Записи, страница, порядок, условия отбора, строка
|
|
34
|
+
поиска и настройка выборки уже там, и наследнику остаются несколько строк.
|
|
35
|
+
|
|
36
|
+
## Паттерны
|
|
37
|
+
|
|
38
|
+
- `angular-patterns-state` — реактивные значения, производные, состояние сервиса, подписка.
|
|
39
|
+
|
|
40
|
+
## Ловушки
|
|
41
|
+
|
|
42
|
+
- **Производное значение объявляется вычисляемым, а не эффектом.** Эффект, кладущий значение в
|
|
43
|
+
реактивное поле, — это ручной пересчёт, и он рано или поздно отстаёт от источника.
|
|
44
|
+
- **Геттеров в компонентах нет.** Геттер пересчитывается на каждой перерисовке, и цена его не
|
|
45
|
+
видна ни в одном месте кода.
|
|
46
|
+
- **Подписка на каждый вызов метода не даёт выбрать, что делать с предыдущим запросом.**
|
|
47
|
+
Быстрые нажатия дают гонку ответов, и побеждает тот, что вернулся последним, а не тот, что
|
|
48
|
+
нажали последним.
|
|
49
|
+
- **Запрет подписки в методе идёт по имени, а не по типу.** Вызов с таким именем у чего угодно
|
|
50
|
+
считается подпиской, а взятие метода без вызова — нет.
|
|
51
|
+
- **Инициализация разметки после первой отрисовки делается своим крючком, а не крючком
|
|
52
|
+
жизненного цикла представления.** Когда страницу отдаёт сервер, разметка появляется позже.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: api-layer
|
|
3
|
+
kind: rule
|
|
4
|
+
law: frontend-application
|
|
5
|
+
description: Правило под закон «Фронтовое приложение». Брать при правке слоя обращения к серверу во фронтовом домене — пары «фасад и служба» и переводчиков моделей при ней. Один вход выборки у списка, общий конвертер страницы, поток вместо ожидания. Готовый код — в паттерне api-layer-pair. Чем это названо здесь — в implementation.md рядом.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Обращение к серверу — каким приёмом
|
|
9
|
+
|
|
10
|
+
Правило под закон `{{lawsDir}}/frontend-application.md`. Закон говорит, что должно быть верно;
|
|
11
|
+
здесь — из чего сложен слой обращения к серверу. Как названы модели выборки, где лежат
|
|
12
|
+
переводчики и чем зовётся общий конвертер — `implementation.md` рядом. Состояние —
|
|
13
|
+
`angular-patterns`, файл компонента — `component-structure`, оформление — `styling-bem`,
|
|
14
|
+
окружение браузера — `platform-access`. Все пять под одним законом.
|
|
15
|
+
|
|
16
|
+
Правило про фронтовые семьи. На серверной стороне тем же словом называют выход к чужому
|
|
17
|
+
сервису, и устроен он иначе.
|
|
18
|
+
|
|
19
|
+
## Когда берётся
|
|
20
|
+
|
|
21
|
+
Правка слоя обращения к серверу во фронтовом домене: фасада, службы, переводчиков моделей при
|
|
22
|
+
них.
|
|
23
|
+
|
|
24
|
+
## Что здесь действует
|
|
25
|
+
|
|
26
|
+
- **Домен ходит за данными парой классов: фасад зовёт обработчика, служба переводит модели.**
|
|
27
|
+
Один класс на оба дела означал бы, что подмена источника тянет за собой перевод.
|
|
28
|
+
- **Фасад знает только контракт, служба отдаёт только модель.** Тип из контракта до стора и
|
|
29
|
+
шаблона не доходит.
|
|
30
|
+
- **У списка один вход — выборка.** Объект, к которому привязан список, род ленты, состояние
|
|
31
|
+
подписки — такие же условия отбора, и лежат они в её условиях, а не отдельными входами.
|
|
32
|
+
- **Ответ списка ложится в общий конвертер целиком.** Контракт отдаёт страницу в той же форме,
|
|
33
|
+
что и модель, и промежуточного объекта в службе не остаётся.
|
|
34
|
+
- **Поля порядка и отбора — перечисления домена, а не голая строка.** Голая строка означает,
|
|
35
|
+
что имя, по которому сервер не сортирует, компилируется и падает уже запросом.
|
|
36
|
+
- **Пара отдаёт поток, а не ожидание.** Основа списочного стора работает потоками, и служба на
|
|
37
|
+
обещаниях в неё не ложится.
|
|
38
|
+
|
|
39
|
+
## Паттерны
|
|
40
|
+
|
|
41
|
+
- `api-layer-pair` — готовые фасад, служба и перевод выборки.
|
|
42
|
+
|
|
43
|
+
## Ловушки
|
|
44
|
+
|
|
45
|
+
- **Выборка в ответе — применённая, а не запрошенная.** Порядок по умолчанию и отброшенное
|
|
46
|
+
сервером условие экран иначе не увидит.
|
|
47
|
+
- **Одна пара — одна сущность.** У связанных сущностей свои пары, даже когда их обработчики
|
|
48
|
+
лежат в одном сервисе контракта.
|
|
49
|
+
- **Метод, которого у домена нет, не объявляется.** Список читают все, правят не все.
|
|
50
|
+
- **Серверный поток событий — исключение из правила про поток.** Живой срез приходит
|
|
51
|
+
асинхронным итератором, и заворачивать его некуда.
|
|
52
|
+
- **Своей копии общих переводчиков страницы, порядка и отбора домен не заводит.** Второй
|
|
53
|
+
экземпляр ловит проверка повторов — правило `shared-code`.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-verification
|
|
3
|
+
kind: rule
|
|
4
|
+
law: verifiability
|
|
5
|
+
description: Правило под закон «Проверяемость». Брать при любой проверке через браузер и при запросах к поднятому приложению из командной строки. Чему на сервере разработки верить нельзя, чем измерять вместо взгляда, почему браузер водится одним драйвером. Готовый код — в паттернах browser-verification-stand и browser-verification-measure. Чем это названо здесь — в implementation.md рядом.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Проверка работающего приложения — каким приёмом
|
|
9
|
+
|
|
10
|
+
Правило под закон `{{lawsDir}}/verifiability.md`. Закон говорит, что считается подтверждением;
|
|
11
|
+
здесь — каким приёмом приложение проверяется живьём. На каких портах оно поднято, каким
|
|
12
|
+
драйвером водится браузер и где лежит конфиг прокси — `implementation.md` рядом. Тесты под тем
|
|
13
|
+
же законом — правило `testing`.
|
|
14
|
+
|
|
15
|
+
## Когда берётся
|
|
16
|
+
|
|
17
|
+
Любая проверка через браузер, любой запрос к поднятому приложению из командной строки, любой
|
|
18
|
+
вывод о вёрстке.
|
|
19
|
+
|
|
20
|
+
## Что здесь действует
|
|
21
|
+
|
|
22
|
+
- **Свой сервер разработки не поднимается.** Приложения уже подняты владельцем; второй
|
|
23
|
+
экземпляр слушает другой порт и отвечает другой сборкой, а расхождение читается как дефект
|
|
24
|
+
правки.
|
|
25
|
+
- **Браузер водится одним драйвером на закреплённом профиле.** Остальные двери — второй
|
|
26
|
+
драйвер, открытие ссылки средствами системы, запуск бинарника — закреплённый профиль не
|
|
27
|
+
спрашивают вовсе и приходят в сеанс без входа.
|
|
28
|
+
- **Выбор браузера протухает и требует повторного вызова.** Выбор, сделанный в начале сессии,
|
|
29
|
+
не держится: после паузы следующий вызов открывает вкладку в другом профиле молча.
|
|
30
|
+
- **Профиль не выбирается из списка и не спрашивается у владельца.** Список отдаёт неустойчивые
|
|
31
|
+
имена, которые не опознают ничего, а выбор из него ведёт на профиль без входа.
|
|
32
|
+
- **Прод-конфигурация проверяется только за настоящим прокси.** Голый сервер отдачи страниц про
|
|
33
|
+
кэш, перенаправления и заголовки не знает ничего.
|
|
34
|
+
- **Вывод о вёрстке подкрепляется числом.** «Выглядит нормально» результатом проверки не
|
|
35
|
+
является; чем мерить — паттерн `browser-verification-measure`.
|
|
36
|
+
|
|
37
|
+
## Паттерны
|
|
38
|
+
|
|
39
|
+
- `browser-verification-stand` — честный стенд из прод-сборки, вход в приложение, разбор порта.
|
|
40
|
+
- `browser-verification-measure` — замер вместо взгляда, ловушки инструмента снимка экрана.
|
|
41
|
+
|
|
42
|
+
## Ловушки
|
|
43
|
+
|
|
44
|
+
- **Сначала выяснить, что отвечает на порту.** На порту регулярно висит собранный артефакт из
|
|
45
|
+
прошлой сессии: он отвечает успехом на старом коде, а заведённого в ветке обработчика у него
|
|
46
|
+
нет вовсе — и отказ читается как дефект регистрации. Таких процессов бывает несколько, и
|
|
47
|
+
завершение по имени команды не попадает ни в один: убивать по идентификатору процесса,
|
|
48
|
+
каждый.
|
|
49
|
+
- **Инкрементальная сборка протухает поштучно.** Разметка бывает уже новой, а клиентский кусок
|
|
50
|
+
— от компиляции до правки. Признак сборки для разработки — имена файлов сборки без хеша.
|
|
51
|
+
Расхождение между ответом из командной строки и страницей после оживления — повод пересобрать,
|
|
52
|
+
а не искать дефект в коде; вывод «такого маршрута нет» отсюда тоже не следует.
|
|
53
|
+
- **Кэш объясняет расхождение, но не подтверждает его.** Вывод «дефекта нет, это кэш» закрывает
|
|
54
|
+
разбор, поэтому принимается только после проверки на чистой сборке.
|
|
55
|
+
- **Сообщение об отсутствии отладочных API каркаса принадлежит расширению браузера, а не
|
|
56
|
+
приложению.** Прод-сборка их не публикует, и лечить это правкой кода не надо: опубликованные,
|
|
57
|
+
они дают карту внутренностей любому, кто откроет консоль.
|
|
58
|
+
- **Поведение маршрутизации воспроизводится нажатиями.** Подстановка адреса и заход по прямой
|
|
59
|
+
ссылке поднимают приложение заново, и накопленного состояния у него нет.
|
|
60
|
+
- **Замер отвечает только на заданный вопрос.** Отступы, кегль и скругление сходятся с
|
|
61
|
+
образцом, пока никто не спросил про фон на наведении, — а держится расхождение при верных
|
|
62
|
+
числах ровно столько, сколько его не спрашивают.
|
|
63
|
+
- **Если сменилась версия пакета, который рисует вёрстку, экраны обходят руками.** Тесты
|
|
64
|
+
нажимают по меткам и остаются зелёными, даже когда отступ съехал, размер пропал, а строка
|
|
65
|
+
стала другой высоты: они проверяют переходы, а не вид. Пары снимков тут тоже мало — смотрят
|
|
66
|
+
по очереди все экраны, которые этот пакет рисует.
|
|
67
|
+
- **Путей запуска несколько, и проверять их надо порознь.** Локальная команда, образ и состав
|
|
68
|
+
прода — разные пути; переменная, заданная в команде проверки, не говорит про образ ничего.
|
|
69
|
+
Пути перечисляются до проверки, а не после того, как один из них сошёлся.
|