@rt-tools/agent-kit 0.5.3 → 0.7.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 +15 -1
- package/assets/checks/check-file-size.mjs +127 -0
- package/assets/checks/check-push-gate.mjs +139 -0
- package/assets/checks/rt-kit-checks.config.mjs +21 -0
- package/assets/defaults/gate-map.sh +90 -34
- package/assets/defaults/project.sh +26 -3
- package/assets/hooks/skill-gate-layers.sh +156 -0
- package/assets/hooks/skill-gate.sh +11 -2
- package/assets/laws/code-structure.md +3 -0
- package/assets/laws/delivery.md +9 -0
- package/assets/laws/observability.md +46 -0
- package/assets/laws/project-documentation.md +4 -0
- package/assets/laws/reuse-first.md +2 -0
- package/assets/laws/verifiability.md +5 -0
- package/assets/patterns/browser-verification-stand.md +22 -2
- package/assets/patterns/doc-style-trace.md +111 -0
- package/assets/patterns/git-workflow-commit.azure.md +18 -0
- package/assets/patterns/git-workflow-commit.github.md +19 -1
- package/assets/patterns/git-workflow-commit.gitlab.md +18 -0
- package/assets/patterns/git-workflow-docker.md +203 -0
- package/assets/patterns/git-workflow-secrets.md +93 -0
- package/assets/patterns/observability-record.md +114 -0
- package/assets/patterns/seo-verify.md +1 -1
- package/assets/patterns/spec-driven-domain.md +2 -2
- package/assets/patterns/spec-driven-rule.md +5 -0
- package/assets/patterns/styling-bem-sheet.md +178 -0
- package/assets/patterns/task-flow-close.md +20 -0
- package/assets/patterns/task-flow-resume.md +5 -0
- package/assets/patterns/translations-content.md +107 -0
- package/assets/patterns/translations-key.md +1 -1
- package/assets/rules/angular-patterns.md +5 -0
- package/assets/rules/browser-verification.md +17 -12
- package/assets/rules/component-structure.md +6 -2
- package/assets/rules/doc-style.md +16 -0
- package/assets/rules/git-workflow.azure.md +60 -1
- package/assets/rules/git-workflow.github.md +67 -1
- package/assets/rules/git-workflow.gitlab.md +61 -1
- package/assets/rules/lists.md +13 -0
- package/assets/rules/observability.md +147 -0
- package/assets/rules/permissions.md +23 -0
- package/assets/rules/reuse-first.md +9 -0
- package/assets/rules/seo.md +57 -9
- package/assets/rules/shared-code.md +6 -0
- package/assets/rules/spec-driven.md +9 -0
- package/assets/rules/styling-bem.md +34 -1
- package/assets/rules/task-flow.md +5 -0
- package/assets/rules/testing.md +46 -8
- package/assets/rules/translations.md +11 -5
- package/assets/rules/typescript-conventions.md +5 -0
- package/lib/assets.d.ts +1 -1
- package/lib/assets.d.ts.map +1 -1
- package/lib/assets.js +2 -4
- package/lib/assets.js.map +1 -1
- package/lib/catalog.d.ts +73 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +121 -0
- package/lib/catalog.js.map +1 -1
- package/lib/commands.d.ts.map +1 -1
- package/lib/commands.js +84 -5
- package/lib/commands.js.map +1 -1
- package/lib/integrity.d.ts +25 -12
- package/lib/integrity.d.ts.map +1 -1
- package/lib/integrity.js +40 -18
- package/lib/integrity.js.map +1 -1
- package/lib/proposals.d.ts +9 -1
- package/lib/proposals.d.ts.map +1 -1
- package/lib/proposals.js +11 -2
- package/lib/proposals.js.map +1 -1
- package/lib/retired.d.ts +30 -0
- package/lib/retired.d.ts.map +1 -0
- package/lib/retired.js +19 -0
- package/lib/retired.js.map +1 -0
- package/lib/sync.d.ts +45 -1
- package/lib/sync.d.ts.map +1 -1
- package/lib/sync.js +44 -10
- package/lib/sync.js.map +1 -1
- package/package.json +1 -1
- package/rt-tools-agent-kit-0.7.0.tgz +0 -0
- package/assets/laws/application/money.md +0 -41
- package/assets/laws/application/ownership.md +0 -32
- package/assets/patterns/ownership-scope-resolve.md +0 -69
- package/assets/patterns/pricing-quote.md +0 -71
- package/assets/rules/ownership-scope.md +0 -63
- package/assets/rules/pricing.md +0 -64
- package/rt-tools-agent-kit-0.5.3.tgz +0 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-workflow-docker
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: git-workflow
|
|
5
|
+
description: Паттерн правила git-workflow. Брать при работе с образами на своей машине — подъём и перезапуск демона, диагностика «висящей» команды, сборка под платформу прод-сервера, одноразовый контейнер рядом с чужими, вход в реестр из службы. Не брать для команд прод-сервера и выбора образа — это паттерн git-workflow-restart, и не для наката миграций — это git-workflow-migration.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Образы на своей машине
|
|
9
|
+
|
|
10
|
+
Паттерн правила `git-workflow`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/delivery.md`.
|
|
12
|
+
|
|
13
|
+
Демон здесь чужой: на машине владельца в нём живут его хранилище разработки, его стенд и
|
|
14
|
+
контейнеры других его работ. Любая команда пишется так, чтобы её отменяли, не спрашивая
|
|
15
|
+
владельца, и чтобы она не задела ничего, кроме заведённого ею самой.
|
|
16
|
+
|
|
17
|
+
## Когда брать
|
|
18
|
+
|
|
19
|
+
- Команда демона не отвечает, и надо понять почему.
|
|
20
|
+
- Нужен одноразовый контейнер рядом с уже работающими.
|
|
21
|
+
- Собирается образ, который поедет на прод-сервер.
|
|
22
|
+
- Раннер конвейера на этой машине не может войти в реестр.
|
|
23
|
+
|
|
24
|
+
## Чужое не трогается
|
|
25
|
+
|
|
26
|
+
Перед любым действием, которое задевает демон целиком, читается, что в нём живёт и переживёт
|
|
27
|
+
ли оно перезапуск:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
docker ps --format '{{.Names}} | {{.Image}} | {{.Status}}'
|
|
31
|
+
docker inspect <контейнер> --format '{{.Name}} restart={{.HostConfig.RestartPolicy.Name}}'
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`unless-stopped` поднимется сам, `no` — нет, и его возвращают руками сразу после подъёма
|
|
35
|
+
демона. Стенд владельца обычно заведён с `no`: после перезапуска он остаётся лежать, а по его
|
|
36
|
+
порту отвечает пустота — это выглядит поломкой стенда, а не следом перезапуска.
|
|
37
|
+
|
|
38
|
+
Свои контейнеры именуются приставкой и снимаются по имени. `docker system prune`, `docker rm`
|
|
39
|
+
по маске и `docker volume prune` не пишутся никогда: они уносят чужое молча.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
docker rm -f <приставка>-ci-db >/dev/null 2>&1 || true # снять прошлый прогон
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Порт своего контейнера выбирается свободным: порт хранилища разработки занят, и попасть в него
|
|
46
|
+
чужой миграцией нельзя.
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
lsof -nP -iTCP:<свободный порт> -sTCP:LISTEN # пусто — порт свободен
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Место спрашивается до прогона
|
|
53
|
+
|
|
54
|
+
Диск виртуальной машины отдельный от диска хоста: на хосте свободно, внутри пусто, и видно это
|
|
55
|
+
только изнутри.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
docker system df # образы, тома и кэш сборки с долей многоразового
|
|
59
|
+
docker run --rm alpine:3 df -h / # сколько осталось у самой виртуальной машины
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Наружу нехватка выходит чужим лицом: контейнер поднимается и сразу гаснет, сверка схемы
|
|
63
|
+
отвечает про ненакатываемую цепочку, гейт пуша краснеет целиком. По этим признакам чинят
|
|
64
|
+
репозиторий, а причина в машине, — поэтому первое при любом из них `docker system df`.
|
|
65
|
+
|
|
66
|
+
Освобождают отбором, а не общей чисткой: у сценария чистки образов сперва спрашивают, что он
|
|
67
|
+
снял бы, и только потом дают снимать.
|
|
68
|
+
|
|
69
|
+
## Демон поднимается своим CLI
|
|
70
|
+
|
|
71
|
+
Открытие приложения виртуальную машину не поднимает: приложение считается запущенным, а демон
|
|
72
|
+
не отвечает часами. Поднимает только собственная команда клиента, а готовность проверяется
|
|
73
|
+
самим демоном:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
docker desktop restart
|
|
77
|
+
until docker info >/dev/null 2>&1; do sleep 5; done
|
|
78
|
+
docker version --format 'демон: {{.Server.Version}}'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Состояние, которое печатает приложение, говорит про приложение: оно отвечает «работает» и
|
|
82
|
+
тогда, когда демон не принимает ни одной команды. Единственный признак живого демона — ответ
|
|
83
|
+
`docker info`. Первые полминуты после подъёма он отвечает ошибкой и пишет в лог, что маршрута
|
|
84
|
+
до виртуальной машины нет: это нормальный старт, а не поломка.
|
|
85
|
+
|
|
86
|
+
## «Команда висит» — сначала проверяется, вправду ли висит
|
|
87
|
+
|
|
88
|
+
Вывод не заворачивается в `tail`, `head` и не глушится тихим режимом: они держат его в буфере
|
|
89
|
+
до конца команды, и идущая работа выглядит зависшей. Читается прямой вывод:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
docker pull alpine:3 # прогресс виден построчно
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Скачивания не запускаются параллельно. Несколько одновременных забивают канал друг другу:
|
|
96
|
+
образ, который тянется за три секунды, шёл полчаса — и это выглядело сломанным демоном, а было
|
|
97
|
+
очередью, устроенной проверяющим.
|
|
98
|
+
|
|
99
|
+
## Где рвётся: три яруса
|
|
100
|
+
|
|
101
|
+
Ярусы проверяются по отдельности, иначе чинится не то. Каждый отвечает секундами:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# 1. Сеть машины: реестр отдаёт манифест
|
|
105
|
+
curl -s -m 30 -w '%{http_code} за %{time_total}s\n' -o /dev/null '<адрес токена реестра>'
|
|
106
|
+
|
|
107
|
+
# 2. Сеть контейнеров: объём проходит внутрь
|
|
108
|
+
docker run --rm alpine:3 sh -c 'time wget -q -O /dev/null <адрес пробы канала>'
|
|
109
|
+
|
|
110
|
+
# 3. Демон: тянет ли он сам
|
|
111
|
+
docker pull busybox:latest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Хост тянет быстро, контейнер тянет быстро, а скачивание стоит — дело в демоне, и его
|
|
115
|
+
перезапускают. Стоят все три — дело в сети машины, и демон ни при чём. Что делал сам демон,
|
|
116
|
+
отвечают его логи: ходы к реестру, подъём и состояние, консоль виртуальной машины — три разных
|
|
117
|
+
файла в каталоге данных клиента.
|
|
118
|
+
|
|
119
|
+
## Команда из службы: свой каталог настроек и явный адрес демона
|
|
120
|
+
|
|
121
|
+
Раннер конвейера запущен службой, и вход в реестр из неё отказывает: пароль сохраняет
|
|
122
|
+
системный помощник хранения ключей, а сеанса пользователя у службы нет. Свой каталог настроек
|
|
123
|
+
с пустым помощником от этого не спасает — клиент подставляет помощника сам и переписывает
|
|
124
|
+
пустое значение молча. Поэтому вход не зовётся вовсе, а пароль пишется в файл настроек прямо:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
export DOCKER_CONFIG="$(mktemp -d)"
|
|
128
|
+
export DOCKER_HOST="unix://${HOME}/.docker/run/docker.sock"
|
|
129
|
+
auth=$(printf '%s:%s' "${REGISTRY_USER}" "${REGISTRY_TOKEN}" | base64)
|
|
130
|
+
printf '{"auths":{"<реестр>":{"auth":"%s"}}}' "${auth}" > "${DOCKER_CONFIG}/config.json"
|
|
131
|
+
chmod 600 "${DOCKER_CONFIG}/config.json"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`DOCKER_HOST` здесь не для красоты: свой каталог уносит с собой и текущий контекст, а без него
|
|
135
|
+
клиент идёт в общесистемный сокет, которого на машине с настольным клиентом нет вовсе, и
|
|
136
|
+
отвечает «нет такого файла» на что угодно. Связь проверяется до сборки:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
docker info --format 'демон: {{.ServerVersion}}'
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Поле у `docker info` называется иначе, чем у `docker version`: имя из второй команды первая не
|
|
143
|
+
понимает, и связь выглядит непроверенной, хотя демон отвечает.
|
|
144
|
+
|
|
145
|
+
Что пароль принят, видно по ответу реестра: анонимному он отвечает `401`, авторизованному —
|
|
146
|
+
содержимым или `403`, но не `401`. Каталог снимается в конце — раннер живёт между прогонами, и
|
|
147
|
+
пароль реестра остался бы лежать на диске владельца.
|
|
148
|
+
|
|
149
|
+
## Образ собирается под платформу прод-сервера
|
|
150
|
+
|
|
151
|
+
Машина владельца и прод-сервер бывают разной архитектуры. Без явной платформы собирается образ
|
|
152
|
+
под сборщика: он уходит в реестр, оттуда на сервер и не стартует там вовсе.
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
docker buildx build --platform <платформа сервера> -f <файл сборки> --output type=cacheonly .
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`--output type=cacheonly` считает сборку, ничего не сохраняя, — этим замеряют время, не
|
|
159
|
+
засоряя машину образом. Чужая платформа идёт эмуляцией, поэтому время сборки на машине и в
|
|
160
|
+
облаке сравнивают числом, а не ожиданием.
|
|
161
|
+
|
|
162
|
+
Сборщик с драйвером `docker-container` нужен для чужой платформы и заводится отдельно; его
|
|
163
|
+
первый подъём тянет свой образ из реестра:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
docker buildx create --name <приставка>-ci-builder --driver docker-container
|
|
167
|
+
docker buildx inspect <приставка>-ci-builder --bootstrap # покажет платформы и состояние
|
|
168
|
+
docker buildx build --builder <приставка>-ci-builder … # сборщик владельца не переключается
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Сборщик не сносится после прогона: в его кэше живут слои и хранилище пакетов, а без них
|
|
172
|
+
следующая сборка тянет все зависимости заново. Флаг `--use` тоже не пишется — он переключает
|
|
173
|
+
сборщик владельца; вместо него `--builder` у самой сборки. Объявления сборщиков лежат в
|
|
174
|
+
каталоге настроек, поэтому под своим каталогом их не видно вовсе — оставить их на месте
|
|
175
|
+
помогает `BUILDX_CONFIG` с адресом каталога владельца.
|
|
176
|
+
|
|
177
|
+
Установка зависимостей внутри сборки ограничивается по числу запросов, иначе она роняет сборку
|
|
178
|
+
целиком — и не потому, что канал медленный, а потому, что сотни запросов забивают его сами
|
|
179
|
+
себе.
|
|
180
|
+
|
|
181
|
+
## Частые промахи
|
|
182
|
+
|
|
183
|
+
- **Перезапуск демона объявлен сделанным по состоянию приложения.** Приложение говорит
|
|
184
|
+
«работает», а `docker info` в это же время отвечает ошибкой.
|
|
185
|
+
- **Стенд не возвращён после перезапуска.** У него политика `no`, и владелец находит мёртвый
|
|
186
|
+
порт вместо стенда.
|
|
187
|
+
- **Вывод команды заведён в `tail`** — и работающая команда объявлена зависшей.
|
|
188
|
+
- **Несколько скачиваний разом** — и медленной объявлена машина, а не собственная очередь.
|
|
189
|
+
- **Образ собран без указания платформы** — прод получает образ чужой архитектуры, и видно это
|
|
190
|
+
только на перезапуске контейнеров.
|
|
191
|
+
- **Свой контейнер занял порт хранилища разработки** — конвейер пишет в данные владельца.
|
|
192
|
+
- **Вход в реестр из службы сделан командой входа** — пароль уходит в помощник хранения ключей
|
|
193
|
+
даже из своего каталога настроек, и задание падает до сборки.
|
|
194
|
+
- **Каталог настроек подменён, а адрес демона не задан** — и починка входа в реестр выглядит
|
|
195
|
+
как упавший демон.
|
|
196
|
+
- **Сборщик снесён после прогона** — вместе с кэшем, и следующая сборка идёт как первая.
|
|
197
|
+
- **Общая чистка ради места** — уносит чужие образы и тома, и восстановить их нечем.
|
|
198
|
+
- **Место освобождено сценарием выкатки целиком.** Сценарий писан для прод-сервера: там
|
|
199
|
+
собирает конвейер, а сервер только тянет готовое, поэтому последним шагом сценарий сносит
|
|
200
|
+
кэш сборщика. На машине владельца собирает раннер, и тот же шаг оставляет сборщика без кэша.
|
|
201
|
+
Отбор самих образов у сценария годится и здесь — он идёт по имени своего реестра, оставляет
|
|
202
|
+
три последних sha и обходит поднятые контейнеры, — а вот его хвост на этой машине запускают,
|
|
203
|
+
только когда согласились ждать полную сборку.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: git-workflow-secrets
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: git-workflow
|
|
5
|
+
description: Паттерн правила git-workflow. Брать при работе с ключами внешних служб — где они лежат, чем ключ, заводимый владельцем, отличается от ключа окружения, что означает каждое состояние строки интеграции и почему зелёная проба не обещает работающей возможности. Не брать для перезапуска прода и выбора образа — это паттерн git-workflow-restart.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Ключи внешних служб
|
|
9
|
+
|
|
10
|
+
Паттерн правила `git-workflow`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/delivery.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
|
+
строкой лога и сводкой старта — что в ней стоит, описывает правило `observability`.
|
|
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
|
+
## Частые промахи
|
|
84
|
+
|
|
85
|
+
- **Искать ключ поиском по составу прода и делать вывод.** Значение там лежит, приложение его
|
|
86
|
+
не читает, а хвост из последних знаков совпадает с записанным в хранилище — совпадение
|
|
87
|
+
подсказки и переменной ничего не доказывает, кроме того, что владелец завёл тот же ключ.
|
|
88
|
+
- **Идти в логи прежде экрана.** Логи скажут про отказ внешней службы, а экран — «не задан»;
|
|
89
|
+
второе точнее и стоит одного нажатия.
|
|
90
|
+
- **Считать отсутствие ошибок признаком работы.** Ни почта, ни перевод не роняют запрос:
|
|
91
|
+
обращение сохранится, запись сохранится, а наружу ничего не уйдёт.
|
|
92
|
+
- **Заводить ключ заново при «расшифровать нечем».** Это не про ключ, а про ключ шифрования;
|
|
93
|
+
новый ляжет рядом и тоже не прочитается.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: observability-record
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: observability
|
|
5
|
+
description: Паттерн правила observability. Брать, когда в коде заводится новая строка лога — готовый вызов логгера, выбор уровня, имя строки, поля объектом, отказ внешней службы и предел ожидания. Не брать для правки самого логгера и контекста запроса — это правило observability.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Новая строка лога
|
|
9
|
+
|
|
10
|
+
Паттерн правила `observability`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/observability.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- В домене появилось место, о котором владелец должен узнать: не сработала автоматика,
|
|
16
|
+
отказала внешняя служба, упала процедура.
|
|
17
|
+
- Хочется поставить `console.log` — вместо него ставится строка лога.
|
|
18
|
+
|
|
19
|
+
## Логгер домена берётся один раз
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const LOG_CONTEXT: string = 'PageCache';
|
|
23
|
+
|
|
24
|
+
readonly #log: ScopedLogger;
|
|
25
|
+
|
|
26
|
+
constructor(logger: AppLoggerService) {
|
|
27
|
+
this.#log = logger.scope(LOG_CONTEXT);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Контекст задаётся один раз и константой модуля, а не литералом в вызове: соседние классы одного
|
|
32
|
+
домена пишут под тем же источником, а отбор отказов судит именно по нему. Номер обращения,
|
|
33
|
+
процедуру и того, кто в системе, логгер подставляет сам — передавать их полями не надо.
|
|
34
|
+
|
|
35
|
+
## Имя строки постоянное, всё изменяемое — в поля
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
✓ this.#log.warn('cache.refresh.rejected', { url, attempt, status, durationMs });
|
|
39
|
+
✗ this.#log.warn(`cache refresh rejected for ${url} after ${attempt} attempts`);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Первый аргумент — имя строки, а не предложение. По нему строки об одном и том же собираются
|
|
43
|
+
вместе, и по нему же они схлопываются в одну строку ленты отказов. Подставленное в имя значение
|
|
44
|
+
делает каждую строку уникальной.
|
|
45
|
+
|
|
46
|
+
Имя пишется точками от общего к частному: `cache.refresh.rejected`, `mail.owner.skipped`.
|
|
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
|
+
```ts
|
|
76
|
+
} catch (error: unknown) {
|
|
77
|
+
this.#log.error('mail.send.failed', { to: maskEmail(to), error: describeError(error) });
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Разбор причины кладёт класс, текст, код и срезанный стек одной формой — той же, что у остальных
|
|
82
|
+
отказов. Подставлять ошибку в текст (`` `failed: ${String(error)}` ``) нельзя: так в лог уедет
|
|
83
|
+
чужой текст целиком, вместе с адресом, ключом или почтой гостя.
|
|
84
|
+
|
|
85
|
+
Почту маскируют на месте вызова, если она приходит отдельным значением: вычистка узнаёт её по
|
|
86
|
+
имени поля, а имя вроде `to` на почту не похоже.
|
|
87
|
+
|
|
88
|
+
## Отказ наступает только тогда, когда у обращения есть предел ожидания
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
/**
|
|
92
|
+
* Ожидание источника курсов. Курс справочный и обновляется раз в сутки: пять
|
|
93
|
+
* секунд ожидания и прежний курс на месте лучше, чем пять минут занятого
|
|
94
|
+
* расписания.
|
|
95
|
+
*/
|
|
96
|
+
const RATES_TIMEOUT_MS: number = 5_000;
|
|
97
|
+
|
|
98
|
+
const response: Response = await fetch(RATES_URL, { signal: AbortSignal.timeout(RATES_TIMEOUT_MS) });
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Без предела соединение с молчащей службой живёт до умолчания среды — минуты, — и всё это время
|
|
102
|
+
`catch` не наступает: писать нечего. Число стоит рядом с клиентом со своим доводом, потому что
|
|
103
|
+
цена ожидания у каждой службы своя: гость внутри запроса ждёт иначе, чем ночное расписание.
|
|
104
|
+
|
|
105
|
+
## Частые промахи
|
|
106
|
+
|
|
107
|
+
- **Одно и то же имя строки стоит в двух местах.** Тогда две разные ситуации читаются как одна.
|
|
108
|
+
Либо имена разные, либо место одно.
|
|
109
|
+
- **`console.log` вместо строки лога.** У него нет ни уровня, ни номера обращения, и в отбор он
|
|
110
|
+
не попадает. На проде это строка, которую никто не увидит.
|
|
111
|
+
- **Уровень выбран по тому, насколько неприятно.** Отказ гостя пройти проверку неприятен, но
|
|
112
|
+
приложение при этом работает.
|
|
113
|
+
- **Ошибка положена в поле как есть, без разбора причины.** Объект ошибки сериализуется в
|
|
114
|
+
пустой `{}`, и в логе не останется ни текста, ни класса.
|
|
@@ -32,7 +32,7 @@ PORT={{prodSitePort}} node dist/apps/site/server/server.mjs &
|
|
|
32
32
|
## Что смотреть в отданном HTML
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
for locale in ""
|
|
35
|
+
for locale in "" <префиксы локалей>; do
|
|
36
36
|
printf '%-10s ' "${locale:-en}"
|
|
37
37
|
curl -s -H "Host: localhost" "http://localhost:{{prodSitePort}}/${locale}<адрес страницы>" \
|
|
38
38
|
| grep -c -E '<title>|name="description"|property="og:|rel="canonical"|hreflang=|application/ld\+json'
|
|
@@ -48,8 +48,8 @@ docs/specs/<домен>/
|
|
|
48
48
|
процедуры домен обслуживает.
|
|
49
49
|
|
|
50
50
|
```markdown
|
|
51
|
-
**Зависимости:** `
|
|
52
|
-
**Законы:** `access`, `locales`, `
|
|
51
|
+
**Зависимости:** `catalog` (состав заявки), `availability` (занятость дат)
|
|
52
|
+
**Законы:** `access`, `locales`, `lists`
|
|
53
53
|
**Процедуры:** `libs/api/<домен>`
|
|
54
54
|
```
|
|
55
55
|
|
|
@@ -125,3 +125,8 @@ description: Паттерн правила git-workflow. Брать … Не б
|
|
|
125
125
|
проверка перестанет её находить.
|
|
126
126
|
- В `description` не сказано, когда паттерн **не** брать, — соседний паттерн того же правила
|
|
127
127
|
становится неотличимым.
|
|
128
|
+
- Блок готового кода принят по виду, а не сверкой с объявлением. Вызов в примере повторяет имя,
|
|
129
|
+
число и порядок параметров живой функции: двухпараметрный вызов выглядел правдоподобно ровно
|
|
130
|
+
до того, как рядом открыли четырёхпараметрное объявление. Тем же проходом сверяется форма
|
|
131
|
+
кода — пример, объявляющий поля не так, как их объявляет дерево, учит нарушать правило о
|
|
132
|
+
языке, и запрещающее правило про это не узнает.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: styling-bem-sheet
|
|
3
|
+
kind: pattern
|
|
4
|
+
rule: styling-bem
|
|
5
|
+
description: Паттерн правила styling-bem. Брать при заведении или правке шторки, окна и полноэкранного просмотра поверх страницы — чем открывается, что передаётся внутрь, как приезжает снизу, чем проверяется. Не брать для панели правки записи в админке — это паттерн entity-aside.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Шторка и окно поверх страницы
|
|
9
|
+
|
|
10
|
+
Паттерн правила `styling-bem`. Что при этом должно быть верно — закон
|
|
11
|
+
`docs/constitution/frontend-application.md`.
|
|
12
|
+
|
|
13
|
+
## Когда брать
|
|
14
|
+
|
|
15
|
+
- На телефоне заводится шторка: выбор языка, валюты, дат.
|
|
16
|
+
- Заводится окно или полноэкранный просмотр поверх страницы.
|
|
17
|
+
- Шторку или окно что-то перекрывает, и хочется поднять им `z-index`.
|
|
18
|
+
|
|
19
|
+
Панель правки записи в админке — это другое: там маршрут в своём аутлете, паттерн
|
|
20
|
+
`entity-aside`.
|
|
21
|
+
|
|
22
|
+
## Открывает служба, а не разметка рядом с кнопкой
|
|
23
|
+
|
|
24
|
+
Компонент шторки — обычный элемент разметки. Он рисуется там, где написан, поэтому его
|
|
25
|
+
`z-index` сравнивается только с соседями по этому месту. Липкая шапка размывает фон под собой —
|
|
26
|
+
и всё, что написано внутри шапки, замкнуто в её слой.
|
|
27
|
+
|
|
28
|
+
Открывает шторку служба окон кита — тем же вызовом открывается полноэкранный просмотр.
|
|
29
|
+
Разметка уезжает к `<body>`, и сравнивать её становится не с чем.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
readonly #dialog: RtDialogService = inject(RtDialogService);
|
|
33
|
+
|
|
34
|
+
readonly #sheetOpenedSource: Subject<RtDialogRef<string>> = new Subject<RtDialogRef<string>>();
|
|
35
|
+
#sheetRef: RtDialogRef<string> | null = null;
|
|
36
|
+
|
|
37
|
+
protected openLocalePicker(): void {
|
|
38
|
+
if (this.#sheetRef) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data: ILocalePickerData = { locales: this.locales, value: this.currentLocale };
|
|
43
|
+
const ref: RtDialogRef<string> = this.#dialog.open<LocalePickerComponent, ILocalePickerData, string>(
|
|
44
|
+
LocalePickerComponent,
|
|
45
|
+
{ data, panelClass: 'locale-picker-panel', backdropClass: 'locale-picker-backdrop' }
|
|
46
|
+
);
|
|
47
|
+
this.#sheetRef = ref;
|
|
48
|
+
this.#sheetOpenedSource.next(ref);
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Ответ шторки ждут подпиской из конструктора — подписываться в методе запрещает
|
|
53
|
+
`angular-patterns`:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
this.#sheetOpenedSource
|
|
57
|
+
.pipe(
|
|
58
|
+
mergeMap((ref: RtDialogRef<string>): Observable<string | undefined> => ref.afterClosed()),
|
|
59
|
+
takeUntilDestroyed(this.#destroyRef)
|
|
60
|
+
)
|
|
61
|
+
.subscribe((code: string | undefined): void => {
|
|
62
|
+
this.#sheetRef = null;
|
|
63
|
+
if (code) {
|
|
64
|
+
this.onLocaleChange(code);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Ссылку обнуляют здесь, а не в обработчике кнопки: шторку закрывают ещё жестом вниз и нажатием
|
|
70
|
+
мимо, и оба пути идут мимо кнопки.
|
|
71
|
+
|
|
72
|
+
## Внутри шторки — только она сама
|
|
73
|
+
|
|
74
|
+
Что показать, приходит токеном. Что выбрали, уходит вместе с закрытием. Своей кнопки у шторки
|
|
75
|
+
нет: кнопку держит тот, кто шторку открывает.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
export interface ILocalePickerData {
|
|
79
|
+
readonly locales: readonly ISiteLocale[];
|
|
80
|
+
readonly value: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
readonly #data: ILocalePickerData = inject<ILocalePickerData>(RT_DIALOG_DATA);
|
|
84
|
+
readonly #dialogRef: RtDialogRef<string> = inject<RtDialogRef<string>>(RtDialogRef);
|
|
85
|
+
|
|
86
|
+
protected confirm(): void {
|
|
87
|
+
this.#dialogRef.close(this.centeredLocale());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Закрыли, не подтвердив: значит, передумали — значение не меняем */
|
|
91
|
+
protected onOpenChange(open: boolean): void {
|
|
92
|
+
this.open.set(open);
|
|
93
|
+
if (!open) {
|
|
94
|
+
this.#dialogRef.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Шторка приезжает снизу
|
|
100
|
+
|
|
101
|
+
Служба поднимает её сразу открытой, и анимировать киту нечего: вместо движения гость видит
|
|
102
|
+
подмену экрана. Поэтому открывают её на кадр позже:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
protected readonly open: WritableSignal<boolean> = signal<boolean>(false);
|
|
106
|
+
|
|
107
|
+
constructor() {
|
|
108
|
+
afterNextRender((): void => {
|
|
109
|
+
this.open.set(true);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`afterNextRender`, а не `ngAfterViewInit`: страницу отдаёт сервер, и разметка появляется позже.
|
|
115
|
+
|
|
116
|
+
## Подложка
|
|
117
|
+
|
|
118
|
+
Шторка сама затемняет фон и сама ловит нажатие мимо. Подложка окна поверх неё дала бы второе
|
|
119
|
+
затемнение — фон стал бы вдвое темнее, чем у соседней шторки. Поэтому её делают прозрачной.
|
|
120
|
+
Правило пишут в общий слой приложения: шторка уехала к `<body>`, и файл стилей компонента до
|
|
121
|
+
неё не достаёт.
|
|
122
|
+
|
|
123
|
+
```scss
|
|
124
|
+
/* общий слой приложения; имя класса несёт приставку дерева */
|
|
125
|
+
.locale-picker-backdrop {
|
|
126
|
+
background: transparent;
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Нажатие она ловит по-прежнему — закрытие по ней делает само окно.
|
|
131
|
+
|
|
132
|
+
## Чем проверяется
|
|
133
|
+
|
|
134
|
+
Скриншот тут не поможет: перекрытая шторка выглядит целой, а сборка и линт молчат. Спрашивают
|
|
135
|
+
точку — кому достанется нажатие:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
const rect = button.getBoundingClientRect();
|
|
139
|
+
document.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Ответом должна быть сама кнопка. Мерить надо на стенде из прод-сборки и на узком экране —
|
|
143
|
+
паттерн `browser-verification-measure`.
|
|
144
|
+
|
|
145
|
+
В сквозном тесте перед замером дожидаются, пока шторка доедет: видимая кнопка ещё может
|
|
146
|
+
двигаться, и координаты через кадр будут другими.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
await page.locator('[qa-dataid="locale-picker-sheet"] [qa-dataid="bottom-sheet-panel"]').evaluate(
|
|
150
|
+
(panel: Element): Promise<void> =>
|
|
151
|
+
new Promise<void>((resolve: () => void): void => {
|
|
152
|
+
if (panel.getBoundingClientRect().bottom <= window.innerHeight) {
|
|
153
|
+
resolve();
|
|
154
|
+
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
panel.addEventListener('transitionend', (): void => resolve(), { once: true });
|
|
158
|
+
})
|
|
159
|
+
);
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Частые промахи
|
|
163
|
+
|
|
164
|
+
- **Шторка написана рядом со своей кнопкой в шапке.** Шапка размывает фон, и шторка замкнута в
|
|
165
|
+
её слой. Так нижняя панель накрыла шторку вместе с кнопкой подтверждения, и значение на
|
|
166
|
+
телефоне стало не сменить.
|
|
167
|
+
- **Шапке подняли номер слоя.** Дефект уходит, шторка остаётся замкнутой в чужой слой:
|
|
168
|
+
следующий сосед с номером повыше накроет её снова.
|
|
169
|
+
- **Шторку перенесли в другое место разметки.** То же самое: заработает, пока над новым местом
|
|
170
|
+
никто не поставит `transform`, `filter` или свой `z-index`.
|
|
171
|
+
- **Правило подложки положили в стили компонента.** Подложка уехала к `<body>`, и правило до
|
|
172
|
+
неё не достаёт — писать надо в общий слой приложения.
|
|
173
|
+
- **Ссылку на открытую шторку обнулили в обработчике кнопки.** Закрытие жестом и нажатием мимо
|
|
174
|
+
идёт мимо него, и второй раз шторка уже не откроется.
|
|
175
|
+
- **Тест померил сразу после проверки видимости.** Шторка ещё едет, `elementFromPoint`
|
|
176
|
+
возвращает `null`, и тест краснеет на исправном коде.
|
|
177
|
+
- **Образец искали по именам библиотеки, поверх которой собран кит.** Она лежит внутри кита, её
|
|
178
|
+
имён в дереве нет — искать надо по именам кита.
|