@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,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lib-layers-move
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: lib-layers
|
|
5
|
+
description: Паттерн правила lib-layers. Брать при переносе кода или символа между либами — с чего начинать, в каком порядке двигать домены, куда кладётся общее, что делать с границами, импортами и README обеих либ, чем проверять. Заведение и удаление самой либы — паттерн lib-layers-new.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Перенести код между либами
|
|
9
|
+
|
|
10
|
+
Паттерн правила `lib-layers`. Что при этом должно быть верно — закон `{{lawsDir}}/lib-imports.md`.
|
|
11
|
+
|
|
12
|
+
## Когда брать
|
|
13
|
+
|
|
14
|
+
- Символ переезжает из одной либы в другую.
|
|
15
|
+
- Домен переносится в новую раскладку.
|
|
16
|
+
- Общий код собирается из копий в одно место.
|
|
17
|
+
|
|
18
|
+
## Начинать с планов
|
|
19
|
+
|
|
20
|
+
Решение о том, куда переезжает код, часто уже принято и записано, а принятое заново с ним
|
|
21
|
+
расходится — и откатывать приходится целиком. Поиск по документам делается до первой правки:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
grep -rn "<имя либы>" <каталог планов>/
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Порядок задаёт граф зависимостей, а не список в плане
|
|
28
|
+
|
|
29
|
+
Домен переносится после всех, от кого он зависит. Списки доменов в планах отсортированы по
|
|
30
|
+
важности, и следование им в лоб заставляет временно расширять границы — а каждая временная
|
|
31
|
+
строка в границах и есть та механическая проверка, ради которой нарезка затевалась.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
grep -rn "<алиас домена>" <каталоги кода> | sed 's/:.*//' | sort -u
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Рёбра выписываются поиском по алиасам домена и сортируются топологически.
|
|
38
|
+
|
|
39
|
+
## Куда именно кладётся общее
|
|
40
|
+
|
|
41
|
+
Своя либа заводится тогда, когда ни одна существующая код не видит.
|
|
42
|
+
|
|
43
|
+
| Кому нужно | Куда |
|
|
44
|
+
| ---------------------------------------------- | ------------------------------------------------------------- |
|
|
45
|
+
| серверной стороне и фронту, без каркаса фронта | общая либа утилит |
|
|
46
|
+
| только фронтам, тянет каркас | либа платформы — служба и токен, либа общих компонентов — вид |
|
|
47
|
+
| предмету, у которого уже есть либа | в неё |
|
|
48
|
+
| всем доменам одной семьи | основание семейства |
|
|
49
|
+
| всей серверной стороне | тот слой утилит, что уже перечислен у каждого домена |
|
|
50
|
+
|
|
51
|
+
Новых строк в границах при таком переезде не появляется — кроме права видеть контракт, если код
|
|
52
|
+
его читает.
|
|
53
|
+
|
|
54
|
+
## После переезда
|
|
55
|
+
|
|
56
|
+
1. **README обеих либ.** У той, откуда файл ушёл, и у той, куда пришёл: README перечисляет, что
|
|
57
|
+
в либе лежит и кто её зовёт. Ни одна проверка эти тексты не читает.
|
|
58
|
+
2. **Порядок импортов.** Переезд алиаса его ломает, и приходит это ошибкой линтера, а не
|
|
59
|
+
сборки. Автоправка есть только у линтера — в общий прогон с тестами и сборкой её флаг
|
|
60
|
+
передавать нельзя, падает весь вызов.
|
|
61
|
+
3. **Линтер по всем затронутым проектам, а не по одному приложению.** Скрипт ошибается молча и
|
|
62
|
+
не так, как человек: строка импорта не переписывается, а исчезает целиком. Сборка одного
|
|
63
|
+
приложения до таких файлов не доходит — их находит только прогон по списку проектов.
|
|
64
|
+
|
|
65
|
+
## Проверить
|
|
66
|
+
|
|
67
|
+
Проверка раскладки — и обязательно проверка повторов: перенос и есть тот момент, когда копия
|
|
68
|
+
остаётся на старом месте.
|
|
69
|
+
|
|
70
|
+
## Частые промахи
|
|
71
|
+
|
|
72
|
+
- Новый адрес выбран без чтения планов — расходится с уже принятым решением.
|
|
73
|
+
- Порядок переноса взят из списка в плане — приходится временно расширять границы.
|
|
74
|
+
- README поправлен только у одной либы.
|
|
75
|
+
- Линтер прогнан по приложению, а не по списку затронутых проектов — пропавшие импорты не
|
|
76
|
+
видно.
|
|
77
|
+
- Копия осталась на старом месте, а проверка повторов не гонялась.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: lib-layers-new
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: lib-layers
|
|
5
|
+
description: Паттерн правила lib-layers. Брать при заведении, переименовании или удалении либы — нужна ли либа вообще, генератор вместо голого вызова каркаса, тег, алиас, барель, README, чем добивать удаление. Перенос кода между уже существующими либами — паттерн lib-layers-move.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Завести или удалить либу
|
|
9
|
+
|
|
10
|
+
Паттерн правила `lib-layers`. Что при этом должно быть верно — закон `{{lawsDir}}/lib-imports.md`.
|
|
11
|
+
|
|
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
|
+
1. Тег в конфиге границ домена — один на либу, равный имени и пути.
|
|
41
|
+
2. Алиас в конфиге путей.
|
|
42
|
+
3. README либы: что в ней лежит и кто её зовёт.
|
|
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
|
+
после сборки и живого прогона сценария, а не по зелёному линтеру с тестами.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: permissions-procedure
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: permissions
|
|
5
|
+
description: Паттерн правила permissions. Брать при заведении обработчика серверной стороны и при закрытии раздела интерфейса — метки доступа, отбивка без входа и без права, декларация пункта меню с правом и признаком незавершённости.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Объявление доступа
|
|
9
|
+
|
|
10
|
+
Паттерн правила `permissions`. Что при этом должно быть верно — закон `{{lawsDir}}/access.md`.
|
|
11
|
+
|
|
12
|
+
## Когда брать
|
|
13
|
+
|
|
14
|
+
- Заводится обработчик серверной стороны.
|
|
15
|
+
- Раздел интерфейса закрывается правом.
|
|
16
|
+
- Обработчик должен отвечать гостю.
|
|
17
|
+
|
|
18
|
+
## Метка на классе обработчика
|
|
19
|
+
|
|
20
|
+
Объявление ровно одно; без него приложение не поднимается:
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
@Injectable()
|
|
24
|
+
@ConnectProcedure()
|
|
25
|
+
@RequiresPermission('<ресурс>:<действие>')
|
|
26
|
+
export class LinkEntityProcedure implements IConnectProcedure<typeof DomainService.method.linkEntity> {
|
|
27
|
+
public readonly method: typeof DomainService.method.linkEntity = DomainService.method.linkEntity;
|
|
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
|
+
ничего, и пустая шапка выхода владельцу не оставляет.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: platform-access-di
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: platform-access
|
|
5
|
+
description: Паттерн правила platform-access. Брать, когда в код заходит окно, документ или проверка среды — внедрение токенов, приведение к глобальной области, окно параметром в чистой функции, работа с разметкой после первой отрисовки. Не брать на серверной стороне.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Окно, документ и проверка среды
|
|
9
|
+
|
|
10
|
+
Паттерн правила `platform-access`. Что при этом должно быть верно — закон
|
|
11
|
+
`{{lawsDir}}/frontend-application.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- В компонент, службу или директиву заходит окно, документ или проверка среды.
|
|
16
|
+
- Появляется работа с разметкой, которой нельзя случиться до первой отрисовки.
|
|
17
|
+
- Чистой функции нужен доступ к окну.
|
|
18
|
+
|
|
19
|
+
## Внедрение
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
@Injectable({ providedIn: 'root' })
|
|
23
|
+
export class SomeService {
|
|
24
|
+
readonly #document: Document = inject(DOCUMENT);
|
|
25
|
+
readonly #platform: PlatformService = inject(PlatformService);
|
|
26
|
+
readonly #window: Window = inject(WINDOW);
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Когда нужен тип глобальной области
|
|
31
|
+
|
|
32
|
+
Интерфейс окна не описывает глобальные конструкторы и пространства имён — наблюдателей
|
|
33
|
+
пересечения и размера, объекты внешних карт. Токен отдаёт тот же самый объект, поэтому тип
|
|
34
|
+
уточняется приведением, и рядом ставится комментарий с причиной:
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Конструкторы наблюдателей объявлены на глобальной области, а не на интерфейсе окна —
|
|
38
|
+
// токен отдаёт тот же объект, тип лишь уточняется.
|
|
39
|
+
readonly #window: Window & typeof globalThis = inject(WINDOW) as Window & typeof globalThis;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Чистая функция принимает окно параметром
|
|
43
|
+
|
|
44
|
+
В файлах чистой логики внедрения нет, и глобал внутрь не тянется:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
export function mapsReady(windowRef: Window & typeof globalThis): boolean {
|
|
48
|
+
return typeof windowRef.maps?.importLibrary === 'function';
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Внедряет его вызывающий компонент.
|
|
53
|
+
|
|
54
|
+
## Проверка среды и первая отрисовка
|
|
55
|
+
|
|
56
|
+
Среда спрашивается у службы платформы, а работа с разметкой уходит в крючок первой отрисовки.
|
|
57
|
+
Проверка «глобал определён» не годится: она верна случайно и ломается на первой же среде, где
|
|
58
|
+
глобал подставлен.
|
|
59
|
+
|
|
60
|
+
## Частые промахи
|
|
61
|
+
|
|
62
|
+
- **Проверка среды прямым обращением к признаку платформы** вместо службы.
|
|
63
|
+
- **Проверка среды вокруг чтения и записи в хранилище:** служба хранилища и так уходит в память
|
|
64
|
+
вне браузера, и такое условие — мёртвый код.
|
|
65
|
+
- **Окно полем класса в службе, обязанной работать без разметки вовсе:** там оно берётся внутри
|
|
66
|
+
метода под проверкой среды.
|
|
67
|
+
- **Правка добавила окно в службу, создающуюся на подъёме, а проверили одной сборкой:** падение
|
|
68
|
+
видно только на поднятом сервере отдачи страниц.
|
|
69
|
+
- **Приведение без комментария:** в переводчиках моделей приведение запрещено, и строка
|
|
70
|
+
читается как нарушение.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reuse-first-extend
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: reuse-first
|
|
5
|
+
description: Паттерн правила reuse-first. Брать, когда готового в источнике вида или в базовом классе не хватило — что проверить перед тем, как писать своё, как расширить готовое, как объявить разовое отступление маркером и когда его снимать.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Готового не хватило
|
|
9
|
+
|
|
10
|
+
Паттерн правила `reuse-first`. Что при этом должно быть верно — закон `{{lawsDir}}/reuse-first.md`.
|
|
11
|
+
|
|
12
|
+
## Когда брать
|
|
13
|
+
|
|
14
|
+
Готовый компонент или базовый класс не покрывает случай, и рука тянется написать своё рядом.
|
|
15
|
+
|
|
16
|
+
## Сначала — проверить, что действительно не хватает
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
grep -c "export" <барель источника вида>
|
|
20
|
+
grep -rn "<похожий приём>" <каталоги кода> --include='*.html' | head
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Источник вида большой, и большинства его компонентов нет ни в одной таблице. Соседний домен
|
|
24
|
+
читается целиком: приём, который кажется новым, обычно уже написан — и переименованный при
|
|
25
|
+
переносе он перестаёт узнаваться.
|
|
26
|
+
|
|
27
|
+
Частые подмены, которые находятся при чтении: готовый мерцающий заполнитель вместо своего,
|
|
28
|
+
готовый диалог вместо своей вуали, готовое сообщение вместо своей области оповещения.
|
|
29
|
+
|
|
30
|
+
## Расширять, а не клонировать
|
|
31
|
+
|
|
32
|
+
Недостающий вариант заводится **в источнике вида или в базовом классе**, и его видят остальные
|
|
33
|
+
экраны.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
✗ <домен>/ui/my-dialog/ клон «почти как готовый»
|
|
37
|
+
✓ <источник вида>/dialog/ новый вариант у готового
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Клон, написанный рядом, забирает правки на себя и расходится с оригиналом с первой же.
|
|
41
|
+
Переименованный файл и «похожий, но свой» компонент — то же отступление, только необъявленное.
|
|
42
|
+
|
|
43
|
+
## Свой примитив — только с одобрения владельца
|
|
44
|
+
|
|
45
|
+
Спрашивается до того, как написан первый файл. То же относится к своей основе и к своему
|
|
46
|
+
оформлению на месте.
|
|
47
|
+
|
|
48
|
+
## Разовое отступление объявляется маркером
|
|
49
|
+
|
|
50
|
+
```html
|
|
51
|
+
<!-- <маркер>: в источнике вида нет поля с маской телефона, заведено задачей -->
|
|
52
|
+
<input type="tel" qa-dataid="phone-input" />
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Маркер ставится в той же строке и объясняет, **чего именно нет в готовом**. «Эти строки были
|
|
56
|
+
здесь раньше» причиной не считается: гард вычёркивает из проверяемого текста то, что уже лежит
|
|
57
|
+
в файле, поэтому отказ означает новый текст.
|
|
58
|
+
|
|
59
|
+
Сверка идёт без отступов — при переезде блок меняет отступ, оставаясь тем же кодом.
|
|
60
|
+
|
|
61
|
+
## Снятие отступления
|
|
62
|
+
|
|
63
|
+
Когда недостающее появилось в готовом, маркер снимается вместе с обходом. Комментарий,
|
|
64
|
+
оправдывающий отклонение, держит это отклонение на себе: пока объяснение выглядит убедительно,
|
|
65
|
+
его не трогают.
|
|
66
|
+
|
|
67
|
+
## Частые промахи
|
|
68
|
+
|
|
69
|
+
- Своё написано до чтения источника вида и соседнего домена.
|
|
70
|
+
- Клон рядом вместо нового варианта у готового.
|
|
71
|
+
- Маркер поставлен без объяснения, чего не хватает.
|
|
72
|
+
- Маркер оставлен после того, как готовое появилось.
|
|
73
|
+
- Своя основа спроектирована заново вместо переноса образца владельца дословно.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: seo-page
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: seo
|
|
5
|
+
description: Паттерн правила seo. Брать, когда правится разметка публичной страницы, заводится новый маршрут или новая страница должна попасть в карту сайта — вызов общей службы тегов, ветки локалей в маршрутах, запись в карте сайта. Не брать для проверки отданной разметки — это паттерн seo-verify.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Разметка публичной страницы
|
|
9
|
+
|
|
10
|
+
Паттерн правила `seo`. Что при этом должно быть верно — закон `{{lawsDir}}/search-visibility.md`.
|
|
11
|
+
|
|
12
|
+
## Когда брать
|
|
13
|
+
|
|
14
|
+
- Правится шаблон публичной страницы, её заголовок, описание или картинка для соцсетей.
|
|
15
|
+
- Заводится новый публичный маршрут.
|
|
16
|
+
- Появилась страница, которая должна попасть в карту сайта.
|
|
17
|
+
|
|
18
|
+
## Разметку ставит служба, а не шаблон
|
|
19
|
+
|
|
20
|
+
Страница собирает данные и одним вызовом отдаёт их общей службе тегов. Своих тегов в шаблоне не
|
|
21
|
+
заводить: они не помечены и потому не переписываются при переходе — тег от прошлой страницы
|
|
22
|
+
переживёт следующую.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
readonly #seo: PageSeoService = inject(PageSeoService);
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
effect((): void => this.#applySeo());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#applySeo(): void {
|
|
32
|
+
const entity: IEntity.State | null = this.entity();
|
|
33
|
+
if (!entity) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.#seo.apply({
|
|
38
|
+
entity,
|
|
39
|
+
pageTitle: this.title(),
|
|
40
|
+
description: entity.shortDescription || this.#descriptionFallback(),
|
|
41
|
+
ogImageUrl: this.cover() ? ogUrl(this.cover()) : this.#brandImage(),
|
|
42
|
+
locale: this.#localeId,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Вызов идёт из эффекта, а не из конструктора напрямую: запись приходит реактивным значением, и
|
|
48
|
+
на первом кадре её ещё нет. Ранний выход по пустой записи обязателен — без него разметка встала
|
|
49
|
+
бы на пустых значениях и второй раз уже не переписалась бы.
|
|
50
|
+
|
|
51
|
+
У картинки есть запасной вариант: запись без обложки отдаёт брендовый кадр того же формата,
|
|
52
|
+
иначе превью в соцсети пустует.
|
|
53
|
+
|
|
54
|
+
## Новый маршрут заводится веткой на каждую локаль
|
|
55
|
+
|
|
56
|
+
Ветки собираются из списка локалей; локаль по умолчанию своей ветки не имеет — она отдаётся из
|
|
57
|
+
корня.
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
export const appRoutes: Route[] = [
|
|
61
|
+
...LOCALE_CODES.filter((code: ELocale): boolean => code !== DEFAULT_LOCALE).map((code: ELocale): Route => ({
|
|
62
|
+
path: code,
|
|
63
|
+
children: publicRoutes,
|
|
64
|
+
})),
|
|
65
|
+
...publicRoutes,
|
|
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
|
+
путь. Проверка описана паттерном `seo-verify`.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: seo-verify
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: seo
|
|
5
|
+
description: Паттерн правила seo. Брать после любой правки, задевающей публичную разметку, теги головы документа или маршруты — сборка, поднятие сервера отдачи страниц, проверка отданной разметки по всем локалям, карта сайта, кэшируемость перенаправления. Не брать для самой правки разметки — это паттерн seo-page.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Проверка разметки на прод-сборке
|
|
9
|
+
|
|
10
|
+
Паттерн правила `seo`. Что при этом должно быть верно — закон `{{lawsDir}}/search-visibility.md`.
|
|
11
|
+
|
|
12
|
+
## Когда брать
|
|
13
|
+
|
|
14
|
+
После любой правки, задевающей публичную разметку, теги головы документа, маршруты, серверную
|
|
15
|
+
точку входа, правила обхода или конфиг прокси.
|
|
16
|
+
|
|
17
|
+
## Сервер разработки здесь не показатель
|
|
18
|
+
|
|
19
|
+
Теги ставятся при отдаче страницы сервером. В режиме разработки этот путь другой, поэтому
|
|
20
|
+
проверка идёт на собранном приложении с поднятым сервером отдачи страниц.
|
|
21
|
+
|
|
22
|
+
Сервер отдачи страниц отвечает отказом на чужое имя хоста — запросы идут с явным заголовком
|
|
23
|
+
хоста.
|
|
24
|
+
|
|
25
|
+
## Что смотреть в отданной разметке
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
for locale in "" <остальные локали>; do
|
|
29
|
+
printf '%-10s ' "${locale:-<по умолчанию>}"
|
|
30
|
+
curl -s -H "Host: localhost" "http://localhost:<порт>/${locale}<путь>" \
|
|
31
|
+
| grep -c -E '<title>|name="description"|property="og:|rel="canonical"|hreflang=|application/ld\+json'
|
|
32
|
+
done
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
В ответе каждой локали должны быть заголовок, описание, набор тегов соцсетей, канонический
|
|
36
|
+
адрес, полный набор языковых ссылок вместе со ссылкой по умолчанию и блок структурированных
|
|
37
|
+
данных.
|
|
38
|
+
|
|
39
|
+
Отдельно проверяется, что канонический адрес ведёт на **свой** язык, а не на локаль по
|
|
40
|
+
умолчанию.
|
|
41
|
+
|
|
42
|
+
## Карта сайта
|
|
43
|
+
|
|
44
|
+
Карта строится из живых данных, а не из файла. Пустой ответ означает, что не поднялся запрос за
|
|
45
|
+
записями, — это отказ, а не «записей нет».
|
|
46
|
+
|
|
47
|
+
## Кэшируемость перенаправления
|
|
48
|
+
|
|
49
|
+
Проверяется только на стенде с настоящим конфигом прокси: голый сервер отдачи страниц отдаёт
|
|
50
|
+
заголовки, но не показывает, попадёт ли ответ в кэш.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
curl -sI http://<стенд>/<прежний адрес> | grep -i -E 'HTTP/|location|cache-control|x-cache'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Ответ обязан быть постоянным перенаправлением, нести срок жизни и попадать в кэш.
|
|
57
|
+
|
|
58
|
+
## Частые промахи
|
|
59
|
+
|
|
60
|
+
- **Проверка на сервере разработки:** теги ставит другой путь, и результат ничего не говорит о
|
|
61
|
+
проде.
|
|
62
|
+
- **Проверена одна локаль:** ветка маршрутов, забытая для остальных, отдаёт отказ и поисковику,
|
|
63
|
+
и читателю.
|
|
64
|
+
- **Заголовки смотрели на голом сервере:** кэш и перенаправления живут в прокси.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shared-code-new
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: shared-code
|
|
5
|
+
description: Паттерн правила shared-code. Брать, когда заводится новое число-настройка, общая функция или общий тип, который должны одинаково понимать все приложения — куда класть, как объявить, как сверить строку с набором и как убедиться, что копия не осталась на старом месте.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Новое общее заводится так
|
|
9
|
+
|
|
10
|
+
Паттерн правила `shared-code`. Что при этом должно быть верно — закон `{{lawsDir}}/shared-code.md`.
|
|
11
|
+
|
|
12
|
+
## Когда брать
|
|
13
|
+
|
|
14
|
+
- Появилось число-настройка: предел, размер, длительность.
|
|
15
|
+
- Появилась функция без каркаса, нужная обеим сторонам.
|
|
16
|
+
- Значение приходит строкой и должно быть сверено с конечным набором.
|
|
17
|
+
|
|
18
|
+
## Куда класть
|
|
19
|
+
|
|
20
|
+
| Что | Куда |
|
|
21
|
+
| -------------------------------------------- | ------------------------------------------- |
|
|
22
|
+
| число или функция без каркаса | общая либа утилит, файл по предмету |
|
|
23
|
+
| готовый тип или набор значений | берётся из общего пакета, не переписывается |
|
|
24
|
+
| токен внедрения, общий двум фронтовым семьям | либа платформы |
|
|
25
|
+
|
|
26
|
+
Файл выбирается по предмету, а не по роду («константы», «функции»): предметный файл читается
|
|
27
|
+
целиком, свалка по роду — никогда. Дальше — строка в барель.
|
|
28
|
+
|
|
29
|
+
## Число объявляется один раз и без довода
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
export const DEFAULT_PAGE_SIZE: number = 20;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
✗ export function listPageOf(query: IListQuery | undefined, defaultPageSize: number): IListPage
|
|
37
|
+
✓ export function listPageOf(query: IListQuery | undefined): IListPage
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Пока умолчание передаётся доводом, домен вправе назвать своё число — и называет, расходясь с
|
|
41
|
+
соседним на единицу, которую никто не заметит.
|
|
42
|
+
|
|
43
|
+
## Строка сверяется с набором, а не приводится к типу
|
|
44
|
+
|
|
45
|
+
Приведение принимает любую строку. Сверку делает общая функция, а что делать с промахом, решает
|
|
46
|
+
вызывающий:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
const operator: TFilterOperator | null = listFilterOperatorOf(filter.operatorType);
|
|
50
|
+
if (!operator) {
|
|
51
|
+
throw new RequestError(`filter operator is required: ${filter.propertyName}`);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
const direction: TListSortOrder = listSortOrderOf(rawDirection) ?? LIST_SORT_ORDER_ENUM.ASC;
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Сервер отбивает запрос, экран берёт умолчание. Общий переводчик здесь не годится: он подал бы
|
|
60
|
+
промах умолчанием, и клиент получил бы отбор, которого не просил.
|
|
61
|
+
|
|
62
|
+
Помощник приведения типов для этого тоже не годится — значение вне набора он пишет в журнал и
|
|
63
|
+
возвращает строкой.
|
|
64
|
+
|
|
65
|
+
## Проверить, что копия не осталась
|
|
66
|
+
|
|
67
|
+
Проверка повторов падает на четырёх признаках: одно имя из двух либ, два перечисления с
|
|
68
|
+
одинаковым набором членов, число-настройка под одним именем в двух либах, перечисление,
|
|
69
|
+
повторяющее набор из общего пакета.
|
|
70
|
+
|
|
71
|
+
Накопленное лежит в списке исключений и отказом не считается. Список только сокращается: новая
|
|
72
|
+
строка в нём означает, что повтор завели уже после проверки.
|
|
73
|
+
|
|
74
|
+
## Частые промахи
|
|
75
|
+
|
|
76
|
+
- Своё перечисление с теми же членами, что уже есть в общем пакете, — копия, даже если имена
|
|
77
|
+
разошлись.
|
|
78
|
+
- Умолчание, переданное доводом, — домен назовёт своё число, и разъезд будет молчаливым.
|
|
79
|
+
- Ту же логику, написанную заново под другим именем, проверка не ловит.
|
|
80
|
+
- Строковая настройка и таблица соответствий не учитываются вовсе.
|