mjolnir-qa 0.4.0 → 0.5.2
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/CHANGELOG.md +1199 -28
- package/README.ar.md +667 -0
- package/README.bn.md +678 -0
- package/README.br.md +706 -0
- package/README.bs.md +690 -0
- package/README.da.md +698 -0
- package/README.de.md +711 -0
- package/README.es.md +709 -0
- package/README.fr.md +714 -0
- package/README.gr.md +705 -0
- package/README.he.md +664 -0
- package/README.it.md +709 -0
- package/README.ja.md +692 -0
- package/README.ko.md +681 -0
- package/README.md +425 -152
- package/README.no.md +696 -0
- package/README.pl.md +699 -0
- package/README.ru.md +701 -0
- package/README.th.md +672 -0
- package/README.tr.md +699 -0
- package/README.uk.md +693 -0
- package/README.vi.md +680 -0
- package/README.zh.md +652 -0
- package/README.zht.md +652 -0
- package/dist/cli.d.mts +469 -22
- package/dist/cli.mjs +9780 -5230
- package/package.json +25 -7
package/README.ru.md
ADDED
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="assets/readme/logo.png" alt="Mjölnir — Verification Trust Engine" width="800" />
|
|
4
|
+
|
|
5
|
+
### Ваши тесты вам лгут. Мы это докажем.
|
|
6
|
+
|
|
7
|
+
**Verification Trust Engine для QA.** Mjölnir проверяет тест-сьюты и
|
|
8
|
+
CI-пайплайны, выдаёт показатель достойности и показывает точно, где
|
|
9
|
+
ломается доверие.
|
|
10
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/mjolnir-qa)
|
|
12
|
+
[](https://github.com/Sergey-Bar/Mjolnir/actions/workflows/ci.yml)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
[](https://nodejs.org)
|
|
15
|
+
|
|
16
|
+
[English](README.md) | [简体中文](README.zh.md) | [繁體中文](README.zht.md) | [한국어](README.ko.md) | [Deutsch](README.de.md) | [Español](README.es.md) | [Français](README.fr.md) | [Italiano](README.it.md) | [Dansk](README.da.md) | [日本語](README.ja.md) | [Polski](README.pl.md) | Русский | [Norsk](README.no.md) | [Português (Brasil)](README.br.md) | [ไทย](README.th.md) | [Türkçe](README.tr.md) | [Українська](README.uk.md) | [বাংলা](README.bn.md) | [Ελληνικά](README.gr.md) | [Tiếng Việt](README.vi.md) | [עברית](README.he.md) | [العربية](README.ar.md) | [Bosanski](README.bs.md)
|
|
17
|
+
|
|
18
|
+
> 🤖 Machine-assisted translation. The [English README](README.md) is canonical. Last synced: 2026-09-04.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx mjolnir-qa@latest
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Достойны ли доверия ваши тесты?**
|
|
25
|
+
|
|
26
|
+
[Посмотрите в деле](#-посмотрите-в-деле) ·
|
|
27
|
+
[Быстрый старт](#-быстрый-старт) ·
|
|
28
|
+
[Что он проверяет](#-что-проверяет-mjölnir) ·
|
|
29
|
+
[Скоринг](#как-работает-скор) ·
|
|
30
|
+
[CI](#-интеграция-ci) · [Конфигурация](#конфигурация) ·
|
|
31
|
+
[Документация](#-документация)
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 🎬 Посмотрите в деле
|
|
38
|
+
|
|
39
|
+
<p align="center">
|
|
40
|
+
<img src="assets/readme/demo.svg" alt="Полный --verbose-отчёт Mjölnir по демо-репозиторию: WORTHINESS 75/100 NEEDS WORK, разбивка диагностик по категориям, список FIX THIS FIRST и каждая находка с ID правила и номером строки — CI, Playwright, тест-гигиена и Python-правила" width="900" />
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<sub>Полный вывод `npx mjolnir-qa ./examples/demo-repo --verbose`,
|
|
44
|
+
отрендеренный настоящим репортером — ничего не урезано. Перегенерируется
|
|
45
|
+
командой `npm run docs:demo`;
|
|
46
|
+
[`tests/demo-asset-reproducibility.spec.ts`](tests/demo-asset-reproducibility.spec.ts)
|
|
47
|
+
роняет CI, если артефакт разошёлся с тем, что печатает инструмент.</sub>
|
|
48
|
+
|
|
49
|
+
**Что только что произошло:**
|
|
50
|
+
|
|
51
|
+
1. Mjölnir нашёл Playwright-спеки, его конфигурацию, CI-workflow и
|
|
52
|
+
Python-тестовый файл — четыре языка/формата за один проход.
|
|
53
|
+
2. Он нашёл улики, ослабляющие доверие к сьюту — `continue-on-error`
|
|
54
|
+
в маскировке job, `|| true`, глотающий exit-код, жёсткие sleep'ы,
|
|
55
|
+
хрупкий селектор, захардкоженные staging-URL, ожидание
|
|
56
|
+
`networkidle`.
|
|
57
|
+
3. Каждую он превратил в конкретную находку с ID правила, местом и
|
|
58
|
+
фиксом — и в единый скор, по которому можно гейтить PR.
|
|
59
|
+
|
|
60
|
+
### Одна находка вблизи
|
|
61
|
+
|
|
62
|
+
Запустите `mjolnir explain QA-CI-001` на первой находке выше — и
|
|
63
|
+
получите:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
▚▞ QA-CI-001 — continue-on-error masks a failing verification gate
|
|
67
|
+
|
|
68
|
+
Severity: error
|
|
69
|
+
Confidence: high
|
|
70
|
+
Evidence: E2
|
|
71
|
+
Measured FP: not yet measured — this rule ships on assumption (see docs/FP-AUDIT.md)
|
|
72
|
+
|
|
73
|
+
WHAT WAS FOUND (real detector output, not a mockup)
|
|
74
|
+
Job `security-scan` runs a verification gate under `continue-on-error: true`.
|
|
75
|
+
|
|
76
|
+
WHY IT MATTERS
|
|
77
|
+
This job can fail every day and CI will still show green. The checkmark
|
|
78
|
+
on this workflow cannot be trusted.
|
|
79
|
+
|
|
80
|
+
HOW TO FIX
|
|
81
|
+
Remove continue-on-error, or scope it to individual non-blocking steps only.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Вот единица ценности: не придирка к стилю, а место, где ваш CI
|
|
85
|
+
сообщает, что что-то прошло, хотя это не так.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## ⚡ Быстрый старт
|
|
90
|
+
|
|
91
|
+
Запустите на репозитории — получите полный отчёт и показатель
|
|
92
|
+
достойности:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
npx mjolnir-qa@latest
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**В CI продукт — одна команда.** Она сканирует только затронутое
|
|
99
|
+
веткой и завершается с ненулевым кодом при новых проблемах:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx mjolnir-qa@latest --scope changed
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Встройте это в PR-check — `mjolnir ci install` пишет workflow — и
|
|
106
|
+
готово. Всё остальное опционально.
|
|
107
|
+
|
|
108
|
+
| Команда | Что делает |
|
|
109
|
+
| ----------------------------------- | ---------------------------------------------------- |
|
|
110
|
+
| `mjolnir` | Скан всего репо + показатель достойности |
|
|
111
|
+
| `mjolnir --scope changed` | Только то, что принесла ваша ветка — CI-режим |
|
|
112
|
+
| `mjolnir ci install` | Генерирует рекомендательный PR-workflow |
|
|
113
|
+
| `mjolnir explain QA-CI-001` | Что / почему / фикс + измеренный FP-рейт для правила |
|
|
114
|
+
| `mjolnir rules --unmeasured` | Правила, работающие по допущению, а не по измерению |
|
|
115
|
+
| `mjolnir --json` / `--format sarif` | Машинночитаемо / GitHub Code Scanning |
|
|
116
|
+
| `mjolnir --strict` | Также правила tier-а quarantine (выше риск FP) |
|
|
117
|
+
|
|
118
|
+
<details>
|
|
119
|
+
<summary><strong>Когда что-то флакует</strong></summary>
|
|
120
|
+
|
|
121
|
+
| Команда | Что делает |
|
|
122
|
+
| ----------------------------------- | ------------------------------------------------------------- |
|
|
123
|
+
| `mjolnir forensics ./test-results/` | Реальные данные прогонов → вердикты `TRUE-FLAKE`, `FLAKY.md` |
|
|
124
|
+
| `mjolnir triage ./test-results/` | Предложение карантина из истории выполнения |
|
|
125
|
+
| `mjolnir pw-report ./test-results/` | Сводка прогона Playwright — ретраи / флейки / самые медленные |
|
|
126
|
+
| `mjolnir doctor:playwright` | Глубокий скан только Playwright + Selector Health Score |
|
|
127
|
+
|
|
128
|
+
</details>
|
|
129
|
+
|
|
130
|
+
<details>
|
|
131
|
+
<summary><strong>По случаю / отчёты</strong></summary>
|
|
132
|
+
|
|
133
|
+
| Команда | Что делает |
|
|
134
|
+
| ------------------------------- | ----------------------------------------------------- |
|
|
135
|
+
| `mjolnir fix --dry-run` / `fix` | Безопасные автофиксы с доказательством |
|
|
136
|
+
| `mjolnir baseline` / `diff` | Снимок находок, затем отчёт только новых/ухудшившихся |
|
|
137
|
+
| `mjolnir impact --since <ref>` | Что изменилось с момента раннего коммита |
|
|
138
|
+
| `mjolnir debt` | Реестр тестового долга с моделью стоимости |
|
|
139
|
+
| `mjolnir handover` | Карта онбординга сьюта для нового QA |
|
|
140
|
+
| `mjolnir stats` | Локальные накопленные счётчики увиденных фиксов |
|
|
141
|
+
| `mjolnir badge` | JSON shields.io-эндпоинта + сниппет |
|
|
142
|
+
| `mjolnir rules --md` | Полный каталог правил (JSON или Markdown) |
|
|
143
|
+
| `mjolnir doctor` | Самоаудит собственной базы правил Mjölnir |
|
|
144
|
+
| `mjolnir create-rule <ID>` | Скаффолд нового правила + фикстур |
|
|
145
|
+
| `mjolnir --format mermaid` | Диаграмма тестовой архитектуры для комментария к PR |
|
|
146
|
+
|
|
147
|
+
</details>
|
|
148
|
+
|
|
149
|
+
Установите глобально вместо `npx`, если так удобнее:
|
|
150
|
+
`npm i -g mjolnir-qa`. Требуется Node.js ≥ 22.18. Работает на Windows,
|
|
151
|
+
macOS и Linux.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 👥 Для кого это?
|
|
156
|
+
|
|
157
|
+
- **QA / SDET**, владеющие e2e- или интеграционной сьютой и которым
|
|
158
|
+
нужны доказательства, что сьют действительно заслуживает зелёную
|
|
159
|
+
галочку, которую он выдаёт.
|
|
160
|
+
- **Платформенные / DevEx-команды**, отвечающие за целостность CI и
|
|
161
|
+
release-gates — те, для кого `continue-on-error` никогда не должен
|
|
162
|
+
молча перекрашивать красный пайплайн в зелёный.
|
|
163
|
+
- **OSS-мейнтейнеры**, которым нужен дешёвый, всегда включённый
|
|
164
|
+
верификационный гейт, работающий локально и в CI без сетевых
|
|
165
|
+
вызовов.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 🔨 Что проверяет Mjölnir
|
|
170
|
+
|
|
171
|
+
| | |
|
|
172
|
+
| --- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
173
|
+
| ⚖️ | **Показатель достойности** — одно число, прозрачная таблица вычетов, никакого чёрного ящика |
|
|
174
|
+
| 🎭 | **Selector Health Score** — оценивает ваши Playwright-локаторы, а не только pass-rate |
|
|
175
|
+
| 🔬 | **Runtime-криминалистика** — читает реальные данные прогонов Playwright/JUnit и ловит `TRUE-FLAKE`, а не только статические догадки |
|
|
176
|
+
| 🚨 | **Правила целостности CI** — ловит `continue-on-error`, `\|\| true` и другие трюки с ложным зелёным |
|
|
177
|
+
| 🐍 | **Все четыре Playwright-биндинга** — TypeScript, Python, Java, C#/.NET — плюс pytest, JUnit/TestNG и CI-workflows |
|
|
178
|
+
| 🔒 | **Local-first** — ноль сетевых вызовов при сканировании, ноль телеметрии, работа за секунды |
|
|
179
|
+
|
|
180
|
+
### Правила
|
|
181
|
+
|
|
182
|
+
Каждое правило поставляется с must-fire- **и** must-not-fire-фикстурами.
|
|
183
|
+
Правило, срабатывающее на собственной негативной фикстуре, не может
|
|
184
|
+
выйти — это фаервол ложных срабатываний.
|
|
185
|
+
|
|
186
|
+
<details>
|
|
187
|
+
<summary><strong>Тест-гигиена</strong></summary>
|
|
188
|
+
|
|
189
|
+
| ID | Правило | Severity |
|
|
190
|
+
| ----------- | ------------------------------------------------------ | -------- |
|
|
191
|
+
| QA-TEST-001 | Закоммичен сфокусированный тест (`.only`, `fit`) | error |
|
|
192
|
+
| QA-TEST-002 | Пропущен тест без обоснования | error |
|
|
193
|
+
| QA-TEST-002 | Пропущен тест с учтённым обоснованием | warning |
|
|
194
|
+
| QA-TEST-003 | Тест без ассертов | error |
|
|
195
|
+
| QA-TEST-004 | Жёсткий sleep (`waitForTimeout`, `sleep()`, `delay()`) | warning |
|
|
196
|
+
| QA-TEST-006 | Злоупотребление ретраями, скрывающее флакость | warning |
|
|
197
|
+
| QA-TEST-010 | Пустое тело теста | error |
|
|
198
|
+
|
|
199
|
+
</details>
|
|
200
|
+
|
|
201
|
+
<details>
|
|
202
|
+
<summary><strong>Качество тестов</strong></summary>
|
|
203
|
+
|
|
204
|
+
| ID | Правило | Severity |
|
|
205
|
+
| ------------ | ----------------------------- | -------- |
|
|
206
|
+
| QA-TQUAL-001 | Верификация только моками | info |
|
|
207
|
+
| QA-TQUAL-002 | Тавтологический ассерт | error |
|
|
208
|
+
| QA-TQUAL-009 | Ассерт не-awaitнутого promise | error |
|
|
209
|
+
| QA-TQUAL-011 | Закомментированные тесты | warning |
|
|
210
|
+
|
|
211
|
+
</details>
|
|
212
|
+
|
|
213
|
+
<details>
|
|
214
|
+
<summary><strong>Playwright 🎭</strong></summary>
|
|
215
|
+
|
|
216
|
+
| ID | Правило | Severity |
|
|
217
|
+
| --------- | ------------------------------------------- | -------- |
|
|
218
|
+
| QA-PW-002 | Ассерт локатора без await | error |
|
|
219
|
+
| QA-PW-003 | `page.pause()` / `test.only()` в коммите | error |
|
|
220
|
+
| QA-PW-004 | Хрупкие CSS/XPath-селекторы | warning |
|
|
221
|
+
| QA-PW-005 | Бизнес-логика внутри `page.evaluate()` | info |
|
|
222
|
+
| QA-PW-114 | Легаси element handles (`page.$`) | info |
|
|
223
|
+
| QA-PW-118 | Ожидания `networkidle` (флаковые by design) | info |
|
|
224
|
+
| QA-PW-123 | Захардкоженные URL окружений | warning |
|
|
225
|
+
|
|
226
|
+
</details>
|
|
227
|
+
|
|
228
|
+
<details>
|
|
229
|
+
<summary><strong>Целостность CI</strong></summary>
|
|
230
|
+
|
|
231
|
+
| ID | Правило | Severity |
|
|
232
|
+
| --------- | ----------------------------------------------------------------- | -------- |
|
|
233
|
+
| QA-CI-001 | `continue-on-error` маскирует падения | error |
|
|
234
|
+
| QA-CI-002 | `\|\| true` глотает exit-коды | error |
|
|
235
|
+
| QA-CI-005 | Отчёт потребляется, но никогда не генерируется | error |
|
|
236
|
+
| QA-CI-007 | Retry-обёртки вокруг тестов | warning |
|
|
237
|
+
| QA-CI-008 | Всегда успешный шаг маскирует падения | error |
|
|
238
|
+
| QA-CI-009 | Exit-код теста не прокидывается (`\|` без pipefail, цепочки `;`) | error |
|
|
239
|
+
| QA-CI-010 | Тесты пропускаются там, где должны блокировать (skip-on-PR-гарды) | error |
|
|
240
|
+
|
|
241
|
+
</details>
|
|
242
|
+
|
|
243
|
+
<details>
|
|
244
|
+
<summary><strong>Python / pytest 🐍</strong></summary>
|
|
245
|
+
|
|
246
|
+
| ID | Правило | Severity |
|
|
247
|
+
| --------- | --------------------------------------------- | -------- |
|
|
248
|
+
| QA-PY-002 | Пропущенный тест (`skip`, нестрогий `xfail`) | warning |
|
|
249
|
+
| QA-PY-003 | Тестовая функция без ассертов | error |
|
|
250
|
+
| QA-PY-005 | `time.sleep()` в тестах | warning |
|
|
251
|
+
| QA-PY-006 | Пустое тело теста (`pass`) | info |
|
|
252
|
+
| QA-PY-010 | Зависимость от случайности/времени без freeze | info |
|
|
253
|
+
| QA-PY-012 | Тавтологический ассерт | error |
|
|
254
|
+
|
|
255
|
+
Всего 20 Python-правил (QA-PY-001…012 гигиена pytest + QA-PY-101…108 Playwright-Python).
|
|
256
|
+
|
|
257
|
+
</details>
|
|
258
|
+
|
|
259
|
+
<details>
|
|
260
|
+
<summary><strong>Java / JUnit · TestNG ☕</strong></summary>
|
|
261
|
+
|
|
262
|
+
| ID | Правило | Severity |
|
|
263
|
+
| --------- | ------------------------------------------- | -------- |
|
|
264
|
+
| QA-JV-101 | Отключённый тест (`@Disabled`) | warning |
|
|
265
|
+
| QA-JV-102 | Жёсткий sleep (`Thread.sleep()`) | warning |
|
|
266
|
+
| QA-JV-103 | Тестовый метод без ассертов | error |
|
|
267
|
+
| QA-JV-105 | Жёсткий sleep Playwright `waitForTimeout()` | warning |
|
|
268
|
+
| QA-JV-106 | Хрупкий селектор вместо role-локатора | warning |
|
|
269
|
+
| QA-JV-108 | Захардкоженный URL окружения в тесте | info |
|
|
270
|
+
| QA-JV-111 | Бланкетный мок `page.route("**")` | info |
|
|
271
|
+
|
|
272
|
+
</details>
|
|
273
|
+
|
|
274
|
+
<details>
|
|
275
|
+
<summary><strong>C# / .NET — NUnit · xUnit · MSTest 🟣</strong></summary>
|
|
276
|
+
|
|
277
|
+
| ID | Правило | Severity |
|
|
278
|
+
| --------- | ---------------------------------------------- | -------- |
|
|
279
|
+
| QA-CS-101 | Пропущенный тест (`[Ignore]`, `[Fact(Skip=)]`) | warning |
|
|
280
|
+
| QA-CS-102 | Жёсткий sleep (`Thread.Sleep` / `Task.Delay`) | warning |
|
|
281
|
+
| QA-CS-103 | Тестовый метод без ассертов | error |
|
|
282
|
+
| QA-CS-105 | Жёсткий sleep `WaitForTimeoutAsync()` | warning |
|
|
283
|
+
| QA-CS-106 | Хрупкий селектор вместо role-локатора | warning |
|
|
284
|
+
| QA-CS-108 | Захардкоженный URL окружения в тесте | info |
|
|
285
|
+
| QA-CS-111 | Бланкетный мок `page.RouteAsync("**")` | info |
|
|
286
|
+
|
|
287
|
+
</details>
|
|
288
|
+
|
|
289
|
+
> Полный живой каталог — каждое правило с tier, confidence, риском
|
|
290
|
+
> ложных срабатываний и доступностью автофикса — генерируется из
|
|
291
|
+
> реестра:
|
|
292
|
+
>
|
|
293
|
+
> ```bash
|
|
294
|
+
> mjolnir rules --md
|
|
295
|
+
> ```
|
|
296
|
+
>
|
|
297
|
+
> Страницы по правилам лежат в [`docs/rules/`](docs/rules/).
|
|
298
|
+
|
|
299
|
+
### Сколько из этого измерено
|
|
300
|
+
|
|
301
|
+
**74 из 99 правил несут ложную положительную частоту, измеренную на
|
|
302
|
+
реальном OSS-коде** (по ≥ 10 вручную классифицированных находок на
|
|
303
|
+
правило; см. [docs/FP-AUDIT.md](docs/FP-AUDIT.md)). Остальные 19
|
|
304
|
+
выходят на оценке автора. Футер каждого скана говорит, сколько из
|
|
305
|
+
_сработавших_ правил измерены; `mjolnir rules --unmeasured` перечисляет
|
|
306
|
+
неизмеренные; страница `mjolnir explain` каждого правила указывает её
|
|
307
|
+
статус. Мы публикуем частоту, даже когда она уродлива — QA-CS-103
|
|
308
|
+
аудируется на 95 % и за это отправлен в карантин. Увеличивать эти 78 —
|
|
309
|
+
постоянная работа проекта.
|
|
310
|
+
|
|
311
|
+
### Тиры правил и зрелость языков
|
|
312
|
+
|
|
313
|
+
Каждое правило — `core`, `extended` или `quarantine`, назначенный по
|
|
314
|
+
его **измеренной** частоте ложных срабатываний:
|
|
315
|
+
|
|
316
|
+
| Tier | Значение | Скан по умолчанию | `--strict` |
|
|
317
|
+
| ------------ | -------------------------------------- | :---------------: | :--------: |
|
|
318
|
+
| `core` | ≤ 10 % измеренных FP | ✅ | ✅ |
|
|
319
|
+
| `extended` | ≤ 30 % измеренных FP | ✅ | ✅ |
|
|
320
|
+
| `quarantine` | выше 30 % или ещё не измерено (n < 10) | ❌ | ✅ |
|
|
321
|
+
|
|
322
|
+
| Язык | Адаптер | Охват сегодня |
|
|
323
|
+
| --------------- | --------------- | -------------------------------------------------------------- |
|
|
324
|
+
| TypeScript / JS | AST компилятора | самый широкий, самый измеренный — в основном `core`/`extended` |
|
|
325
|
+
| Python / pytest | Regex-слой | широкий, проверен корпусом — в основном `core`/`extended` |
|
|
326
|
+
| Java | Regex-слой | новее — в основном `extended`/`quarantine` |
|
|
327
|
+
| C# / .NET | Regex-слой | новее — в основном `extended`/`quarantine` |
|
|
328
|
+
|
|
329
|
+
У TypeScript и Python самый широкий измеренный охват. Java и C#
|
|
330
|
+
вышли, задокументированы и остаются за пределами головного числа, пока
|
|
331
|
+
реальный сьют-потребитель (не собственные тесты биндинг-библиотеки) не
|
|
332
|
+
будет проаудирован.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Как работает скор
|
|
337
|
+
|
|
338
|
+
<p align="center">
|
|
339
|
+
<img src="assets/readme/terminal-hero.svg" alt="Терминальный вывод Mjölnir — WORTHINESS 75/100 NEEDS WORK, разбивка диагностик по категориям и список FIX THIS FIRST" width="820" />
|
|
340
|
+
</p>
|
|
341
|
+
|
|
342
|
+
<sub>Перегенерируется командой `npm run docs:hero`;
|
|
343
|
+
[`tests/hero-asset-reproducibility.spec.ts`](tests/hero-asset-reproducibility.spec.ts)
|
|
344
|
+
роняет CI, если артефакт разошёлся с тем, что печатает репортер.</sub>
|
|
345
|
+
|
|
346
|
+
Скор прозрачен: **error −8, warning −3, info −1**, затем нормировка на
|
|
347
|
+
экспозицию сьюта (вычеты на объявление теста). Вычеты, взвешенные по
|
|
348
|
+
уликам, означают, что слабые сигналы стоят дешевле. Терминал показывает
|
|
349
|
+
те же со скидкой числа, что использует скор — никакого чёрного ящика.
|
|
350
|
+
Полная методика: [docs/SCORING.md](docs/SCORING.md).
|
|
351
|
+
|
|
352
|
+
**Вердикты**
|
|
353
|
+
|
|
354
|
+
| Score | Вердикт |
|
|
355
|
+
| ------- | ---------------- |
|
|
356
|
+
| ≥ 80 | ✓ **WORTHY** |
|
|
357
|
+
| 50 – 79 | ⚠ **NEEDS WORK** |
|
|
358
|
+
| < 50 | ✖ **UNWORTHY** |
|
|
359
|
+
|
|
360
|
+
**Уровни улик** — каждая находка несёт один; он задаёт вес находки в
|
|
361
|
+
скоре:
|
|
362
|
+
|
|
363
|
+
| Уровень | Значение | Влияние на скор | Пример |
|
|
364
|
+
| ------- | ------------------------ | ------------------ | ---------------------------------------------------------------- |
|
|
365
|
+
| E2 | Детерминированный дефект | Полный вычет | `.only` в коммите — структурно доказуемо |
|
|
366
|
+
| E1 | Эвристический паттерн | Половинный вычет | Найденный regex'ом `sleep()` — сильный сигнал, не доказательство |
|
|
367
|
+
| E0 | Наблюдение | Ноль (только info) | Репортится, но никогда не гейтит CI и не вычитает |
|
|
368
|
+
|
|
369
|
+
Большинство правил — **E1**. Слоган «we prove it» отсылает к этой
|
|
370
|
+
системе: находки E2 — структурное доказательство; находки E1 —
|
|
371
|
+
корректно позиционированные предупреждения, не формальные доказательства.
|
|
372
|
+
|
|
373
|
+
Пустой репозиторий получает `null`, никогда фейковую сотню — см.
|
|
374
|
+
[Модель доверия](#модель-доверия).
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## 🎭 Selector Health Score
|
|
379
|
+
|
|
380
|
+
Главная метрика для Playwright-сьютов — насколько устойчивы ваши
|
|
381
|
+
локаторы:
|
|
382
|
+
|
|
383
|
+
```text
|
|
384
|
+
▚▞ SELECTOR HEALTH — e2e/checkout.spec.ts
|
|
385
|
+
|
|
386
|
+
[█████████████████░░░] 83 / 100
|
|
387
|
+
role/text: 2 · testid: 1 · css-chains: 1 ⚠ · xpath: 0
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Локаторы на основе ролей получают полный балл. Цепочки CSS-классов и
|
|
391
|
+
XPath топят скор — они ломаются на любом DOM-рефакторе, не сообщая,
|
|
392
|
+
какое поведение регрессировало.
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## 🔬 Runtime-улики
|
|
397
|
+
|
|
398
|
+
Статическое детектирование флакости — гадание. Mjölnir читает **реальные
|
|
399
|
+
данные выполнения** — JSON-репорты Playwright и XML JUnit от любого
|
|
400
|
+
раннера:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
mjolnir forensics ./test-results/
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
```text
|
|
407
|
+
▚▞ FLAKINESS LEADERBOARD
|
|
408
|
+
|
|
409
|
+
3 tests · 1 failed · 1 flaky · 1 retried
|
|
410
|
+
|
|
411
|
+
TRUE-FLAKE completes checkout with saved card (e2e/checkout.spec.ts)
|
|
412
|
+
████████████████████ 6.0s · 2 attempts
|
|
413
|
+
FAILING declines an expired card (e2e/checkout.spec.ts)
|
|
414
|
+
████░░░░░░░░░░░░░░░░ 1.1s · 1 attempt
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
Тест, проходящий только с попытки ≥ 2, — не проходящий тест; это
|
|
418
|
+
везучий тест. Он помечается `TRUE-FLAKE` независимо от финальной
|
|
419
|
+
зелёной галочки.
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## ⚡ Mjölnir — не ещё один линтер
|
|
424
|
+
|
|
425
|
+
Линтеры говорят, соответствует ли код правилам. Mjölnir говорит,
|
|
426
|
+
можно ли доверять вашей верификации.
|
|
427
|
+
|
|
428
|
+
| | ESLint / SonarQube | Coverage-инструменты | Ручное ревью | **Mjölnir** |
|
|
429
|
+
| ------------------------------------------------------------- | :----------------: | :------------------: | :----------: | :---------: |
|
|
430
|
+
| Целостность CI-workflow (`continue-on-error`, `\|\| true`) | ❌ | ❌ | редко | ✅ |
|
|
431
|
+
| Кросс-язык (TS, Python, Java, C#) из одного инструмента | ❌ | ❌ | ❌ | ✅ |
|
|
432
|
+
| Оценивает устойчивость Playwright-локаторов (Selector Health) | ❌ | ❌ | редко | ✅ |
|
|
433
|
+
| Помечает тесты без настоящих ассертов | ✅ (плагин)\* | ❌ | иногда | ✅ |
|
|
434
|
+
| Ловит жёсткие sleep'ы (`waitForTimeout`, `time.sleep`) | ✅ (плагин)\* | ❌ | иногда | ✅ |
|
|
435
|
+
| Работает за секунды, ноль сетевых вызовов при скане | ✅ | ✅ | — | ✅ |
|
|
436
|
+
|
|
437
|
+
\*`eslint-plugin-jest` (`expect-expect`) и `eslint-plugin-playwright`
|
|
438
|
+
(`expect-expect`, `no-wait-for-timeout`) покрывают это для своих
|
|
439
|
+
фреймворков.
|
|
440
|
+
|
|
441
|
+
**Runtime-анализ** — отдельная категория рядом со статическим линтингом:
|
|
442
|
+
|
|
443
|
+
| | Playwright retry reporter | Allure / ReportPortal | **Mjölnir forensics** |
|
|
444
|
+
| ---------------------------------------------------------- | :-----------------------: | :-------------------: | :-------------------: |
|
|
445
|
+
| Читает реальные данные прогонов для вердиктов `TRUE-FLAKE` | частично\* | частично (тег) | ✅ |
|
|
446
|
+
| Отчёт флей-триажа из истории выполнения | ❌ | ✅ | ✅ |
|
|
447
|
+
| Интегрируется со статическим скором достойности | ❌ | ❌ | ✅ |
|
|
448
|
+
|
|
449
|
+
\*Playwright отслеживает ретраи внутри, но не выдаёт самостоятельного
|
|
450
|
+
отчёта о флакости с вердиктными метками.
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 🤖 Почему бы не использовать просто AI-код-ревью?
|
|
455
|
+
|
|
456
|
+
Другая проблема, другой слой. AI-ревью может заметить подозрительное
|
|
457
|
+
изменение теста в диффе; оно не доказывает, что система верификации в
|
|
458
|
+
целом заслуживает доверия — и видит только показанный ему дифф.
|
|
459
|
+
|
|
460
|
+
| | AI-код-ревью (Copilot и пр.) | **Mjölnir** |
|
|
461
|
+
| ------------------------------------------- | :------------------------------: | :-----------------------------: |
|
|
462
|
+
| Цена за скан | Токены (растут с размером диффа) | **Ноль** (локально, установлен) |
|
|
463
|
+
| Видит весь сьют + все CI-конфиги | Только PR-дифф, показанный ему | **Всё, каждый раз** |
|
|
464
|
+
| Детерминирован (тот же вход → тот же выход) | ❌ (недетерминирован) | **✅** |
|
|
465
|
+
| Ловит паттерны, дремлющие месяцами | Только если в контексте | **✅** (сканирует все файлы) |
|
|
466
|
+
| Помнит находки между запусками | ❌ (нет памяти между сессиями) | **✅** (baseline + diff) |
|
|
467
|
+
| Запускается без человека | Нужен PR или промпт | **✅** (CI-хук, 3 секунды) |
|
|
468
|
+
|
|
469
|
+
**Используйте оба.** AI ловит нюанс, замысел и дизайнерские изъяны,
|
|
470
|
+
которые не найдёт ни один regex. Mjölnir ловит структурные паттерны,
|
|
471
|
+
которые AI упускает, потому что те выглядят «намеренными» —
|
|
472
|
+
закоммиченный `.only`, проглоченный exit-код, `continue-on-error` на
|
|
473
|
+
тестовом job. Это не баги, требующие рассуждений; это факты, требующие
|
|
474
|
+
сканирования.
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## 🤖 Интеграция CI
|
|
479
|
+
|
|
480
|
+
Одна команда генерирует PR-workflow — по умолчанию рекомендательный,
|
|
481
|
+
никогда блокирующий:
|
|
482
|
+
|
|
483
|
+
```bash
|
|
484
|
+
mjolnir ci install
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Или подключите нативно к GitHub Code Scanning через SARIF:
|
|
488
|
+
|
|
489
|
+
```yaml
|
|
490
|
+
- run: npx mjolnir-qa@latest --format sarif > mjolnir.sarif
|
|
491
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
492
|
+
with:
|
|
493
|
+
sarif_file: mjolnir.sarif
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
Настройка редактора и пайплайна для SARIF:
|
|
497
|
+
[docs/SARIF-INTEGRATION.md](docs/SARIF-INTEGRATION.md).
|
|
498
|
+
|
|
499
|
+
### Охват по изменённому scope
|
|
500
|
+
|
|
501
|
+
`--scope changed` атрибутирует находки строкам, добавленным в вашей
|
|
502
|
+
ветке относительно merge-base с `main`. Он покрывает тестовые файлы
|
|
503
|
+
(`*.spec.*`, `*.test.*`), плюс workflow-файлы GitHub и конфигурации
|
|
504
|
+
Playwright в диффе. Когда merge-base не разрешается — shallow clone,
|
|
505
|
+
detached HEAD, не-git-цель, другой дефолтный ветка — он честно
|
|
506
|
+
деградирует: находки возвращаются к атрибуции на весь файл, и отчёт об
|
|
507
|
+
этом говорит. Переопределите базовую ref через `--base <ref>`.
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Конфигурация
|
|
512
|
+
|
|
513
|
+
Mjölnir — zero-config. Опциональный `mjolnir.config.json` (или
|
|
514
|
+
`.mjolnir.json`) в корне репо подстраивает severity, гейтинг и scope —
|
|
515
|
+
он никогда не меняет семантику детекции.
|
|
516
|
+
|
|
517
|
+
| Key | Тип | Действие |
|
|
518
|
+
| ------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
519
|
+
| `exclude` | `string[]` | Дополнительные ignore-глобы (подмножество gitignore), поверх встроенных дефолтов |
|
|
520
|
+
| `gate` | `"advisory" \| "error" \| "warning"` | Какие severity завершают процесс ненулевым кодом (по умолчанию `error`; `advisory` никогда не блокирует) |
|
|
521
|
+
| `severityOverrides` | `{ "<RULE-ID>": severity }` | Переранжирует находки правила для вашего репо |
|
|
522
|
+
| `ignore` | `IgnoreEntry[]` | Подавляет находки — **`reason` обязателен**; записи истекают через 90 дней (явная дата `expires`, либо время последнего изменения файла конфига для записей без неё) |
|
|
523
|
+
| `plugins` | `string[]` | Сторонние пакеты правил (см. [Модель доверия](#модель-доверия)) |
|
|
524
|
+
|
|
525
|
+
```json
|
|
526
|
+
{
|
|
527
|
+
"gate": "error",
|
|
528
|
+
"exclude": ["legacy/**"],
|
|
529
|
+
"severityOverrides": { "QA-PW-118": "warning" },
|
|
530
|
+
"ignore": [
|
|
531
|
+
{
|
|
532
|
+
"ruleId": "QA-TEST-004",
|
|
533
|
+
"files": ["e2e/legacy-login.spec.ts"],
|
|
534
|
+
"reason": "Third-party widget needs a settle delay; tracked in JIRA-4821",
|
|
535
|
+
"expires": "2026-12-31"
|
|
536
|
+
}
|
|
537
|
+
]
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
- **`.mjolnirignore`** — простой файл в стиле gitignore для исключений
|
|
542
|
+
путей, тот же диалект, что `exclude`. Используйте его для
|
|
543
|
+
машинного шума; используйте `exclude`, когда список должен жить в
|
|
544
|
+
версионном контроле вместе с остальной конфигурацией.
|
|
545
|
+
- **CLI-переопределения** — `--strict` (включить правила карантина),
|
|
546
|
+
`--width <cols>` и `--ascii` / `--no-ascii` (терминальный рендер),
|
|
547
|
+
`--tone blunt` (более резкие сообщения), `--max-duration <sec>`
|
|
548
|
+
(ограниченный частичный скан).
|
|
549
|
+
- Подавление правил и жизненный цикл депрекации:
|
|
550
|
+
[docs/RULE-LIFECYCLE.md](docs/RULE-LIFECYCLE.md).
|
|
551
|
+
|
|
552
|
+
Записи `ignore` также питают отдельную команду `mjolnir suppressions`,
|
|
553
|
+
которая перечисляет текущие подавления и время истечения каждой записи.
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
557
|
+
## 📐 Коды выхода и контракты
|
|
558
|
+
|
|
559
|
+
Заморожены — безопасно строить на них CI-логику:
|
|
560
|
+
|
|
561
|
+
| Код выхода | Значение |
|
|
562
|
+
| ---------- | --------------------------------------------------------------------------------- |
|
|
563
|
+
| `0` | Чисто — нет находок на уровне гейта или выше |
|
|
564
|
+
| `1` | Находки на уровне гейта или выше |
|
|
565
|
+
| `2` | Частичный скан (исчерпан бюджет времени, нечитаемые файлы) — никогда не блокирует |
|
|
566
|
+
| `10` | Ошибка использования (плохой флаг, отсутствие цели) |
|
|
567
|
+
| `20` | Внутренняя ошибка |
|
|
568
|
+
|
|
569
|
+
JSON/SARIF-отчёт — `schemaVersion: 1`. ID правил
|
|
570
|
+
(`QA-<FAMILY>-NNN`) неизменны после выхода и никогда не используются
|
|
571
|
+
повторно.
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Модель доверия
|
|
576
|
+
|
|
577
|
+
- **Local-first** — ноль сетевых вызовов во время сканирования. Никогда.
|
|
578
|
+
Ноль телеметрии.
|
|
579
|
+
- **Никаких ложных доказательств** — мы скорее скажем «неизвестно», чем
|
|
580
|
+
«проверено». Пустое репо получает `score: null`, никогда фейковую
|
|
581
|
+
сотню.
|
|
582
|
+
- **Частичная честность** — если анализ оборван, вывод об этом говорит.
|
|
583
|
+
Никогда «complete», когда это не так.
|
|
584
|
+
- **FP-фаервол** — детекция работает на очищенном от комментариев и
|
|
585
|
+
строк представлении кода (правила TypeScript используют AST
|
|
586
|
+
компилятора): паттерн внутри прозаического комментария или
|
|
587
|
+
док-примера-строки — это документация, а не находка.
|
|
588
|
+
- **Измерено, а не заявлено** — в головные тиры выходят только правила
|
|
589
|
+
с частотой ложных срабатываний из реального OSS-кода (см.
|
|
590
|
+
[Сколько из этого измерено](#сколько-из-этого-измерено)); футер скана
|
|
591
|
+
и `mjolnir rules --unmeasured` скажут, какие какие.
|
|
592
|
+
- **Доверие к плагинам** — плагины — это npm-пакеты, объявленные в
|
|
593
|
+
`"plugins"`. **Песочницы нет**: код плагина работает с полными
|
|
594
|
+
привилегиями Node, та же модель доверия, что у плагинов ESLint или
|
|
595
|
+
Vitest. Префиксы ID основных правил зарезервированы и отвергаются от
|
|
596
|
+
плагинов против подмены.
|
|
597
|
+
- **Workspace-локальные внешние правила** (фолдерные, ноль сети) —
|
|
598
|
+
каталог `mjolnir-rules/` рядом с целью скана загружает собственные
|
|
599
|
+
правила: JSON-файлы декларируют regex-паттерны (код не исполняется),
|
|
600
|
+
модули `.mjs`/`.js` экспортируют `rules` (полное доверие Node, как у
|
|
601
|
+
плагинов). Внешние правила несут те же trust-метаданные, что и core;
|
|
602
|
+
они никогда не могут выйти в core-тире (core требует измеренной
|
|
603
|
+
FP-частоты из corpus-сайдкара — заявленный `tier: "core"` зажимается
|
|
604
|
+
до `extended`), соблюдают тировые лимиты и проверяются на дрейф:
|
|
605
|
+
`mjolnir rules --md --external` рендерит каталог из загруженных
|
|
606
|
+
файлов (происхождение `external`), а генератор матрицы принимает
|
|
607
|
+
`--external <root>`.
|
|
608
|
+
|
|
609
|
+
---
|
|
610
|
+
|
|
611
|
+
## 🏗️ Архитектура
|
|
612
|
+
|
|
613
|
+
<details>
|
|
614
|
+
<summary>Развернуть дерево</summary>
|
|
615
|
+
|
|
616
|
+
```
|
|
617
|
+
mjolnir/
|
|
618
|
+
├── src/
|
|
619
|
+
│ ├── engine/ # LanguageAdapter interface + rule runner
|
|
620
|
+
│ ├── adapters/ # typescript · python · java · csharp · github-actions
|
|
621
|
+
│ ├── rules/ # rules across 8 families + the measured-FP table
|
|
622
|
+
│ ├── playwright/ # Selector Health Score engine
|
|
623
|
+
│ ├── discovery/ # workspace, frameworks, ignore resolution
|
|
624
|
+
│ ├── scope/ # git merge-base changed-scope engine
|
|
625
|
+
│ ├── scorer/ # transparent deduction table + prioritization
|
|
626
|
+
│ ├── reporter/ # terminal · JSON · SARIF 2.1 · Mermaid
|
|
627
|
+
│ ├── forensics/ # run-data ingestion · flake verdicts · triage
|
|
628
|
+
│ ├── config/ # mjolnir.config.json + suppressions
|
|
629
|
+
│ ├── plugins/ # third-party rule loading (no sandbox)
|
|
630
|
+
│ └── commands/ # every subcommand
|
|
631
|
+
└── tests/
|
|
632
|
+
├── fixtures/ # must-fire / must-not-fire per rule
|
|
633
|
+
└── golden/ # frozen score regression locks
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
</details>
|
|
637
|
+
|
|
638
|
+
- **Правила — чистые функции** — `(SourceFileContext) → Finding[]`, без
|
|
639
|
+
I/O, без глобалов. Новый экосистем = один адаптер + его правила.
|
|
640
|
+
- **TypeScript/Playwright использует AST компилятора** (ts-morph).
|
|
641
|
+
Python, Java и C# работают на общем regex-слое с маскированием
|
|
642
|
+
комментариев и строк.
|
|
643
|
+
- Слой tree-sitter WASM AST для Java и C# существует и является
|
|
644
|
+
следующим шагом точности — он ещё не подключён к синхронному
|
|
645
|
+
скан-пайплайну.
|
|
646
|
+
|
|
647
|
+
---
|
|
648
|
+
|
|
649
|
+
## 📚 Документация
|
|
650
|
+
|
|
651
|
+
| Документ | Что внутри |
|
|
652
|
+
| ------------------------------------------------------ | ------------------------------------------------- |
|
|
653
|
+
| [docs/SCORING.md](docs/SCORING.md) | Нормировка скора + взвешивание по уликам |
|
|
654
|
+
| [docs/FP-AUDIT.md](docs/FP-AUDIT.md) | Измеренные частоты ложных срабатываний + методика |
|
|
655
|
+
| [docs/RULE-LIFECYCLE.md](docs/RULE-LIFECYCLE.md) | Состояния правил, подавление, депрекация |
|
|
656
|
+
| [docs/SARIF-INTEGRATION.md](docs/SARIF-INTEGRATION.md) | SARIF-вывод + настройка редактора/CI |
|
|
657
|
+
| [docs/rules/](docs/rules/) | Сгенерированный каталог по правилам |
|
|
658
|
+
| [CONTRIBUTING.md](CONTRIBUTING.md) | Dev-сетап + процесс контрибуции |
|
|
659
|
+
| [CHANGELOG.md](CHANGELOG.md) | История релизов |
|
|
660
|
+
| [SECURITY.md](SECURITY.md) | Сообщение об уязвимостях |
|
|
661
|
+
|
|
662
|
+
---
|
|
663
|
+
|
|
664
|
+
## 📈 Статус
|
|
665
|
+
|
|
666
|
+
**v0.5.x · открытая бета.** JSON-схема и коды выхода — замороженные
|
|
667
|
+
контракты. TypeScript и Python имеют самый широкий измеренный охват;
|
|
668
|
+
Java и C# новее — читайте о них в
|
|
669
|
+
[таблице тиров](#тиры-правил-и-зрелость-языков).
|
|
670
|
+
|
|
671
|
+
---
|
|
672
|
+
|
|
673
|
+
## 🤝 Участие в проекте
|
|
674
|
+
|
|
675
|
+
Новые правила — самый простой первый вклад: одна команда скаффолдит
|
|
676
|
+
правило и его must-fire- **и** must-not-fire-фикстуры (сгенерированное
|
|
677
|
+
правление намеренно падает на фикстурах, пока вы не реализуете реальную
|
|
678
|
+
детекцию — стаб не может выйти):
|
|
679
|
+
|
|
680
|
+
```bash
|
|
681
|
+
mjolnir create-rule QA-PW-140 --title "Screenshot without diff bound"
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
Полный dev-сетап, команды постоянного гейта и законы anti-creep /
|
|
685
|
+
фикстурного фаервола — в [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
<div align="center">
|
|
690
|
+
|
|
691
|
+
**Перестаньте выкатывать тесты, которым нельзя доверять.**
|
|
692
|
+
|
|
693
|
+
```bash
|
|
694
|
+
npx mjolnir-qa@latest
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**Star ⭐ · Watch 👀 · Contribute 🤝**
|
|
698
|
+
|
|
699
|
+
Создано [Сергеем Баром](https://www.linkedin.com/in/sergeybar/)
|
|
700
|
+
|
|
701
|
+
</div>
|