agent-quality-kit 0.5.0 → 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 +53 -2
- package/README.ru.md +35 -1
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +65 -0
- package/kit/gates/README.md +60 -0
- package/kit/gates/ci-actually-fails/README.md +54 -0
- package/kit/gates/ci-actually-fails/check.sh +116 -0
- package/kit/gates/ci-actually-fails/gate.yml +14 -0
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +30 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
- package/kit/gates/color-from-token/check.sh +13 -1
- package/kit/gates/commit-explains-itself/README.md +13 -3
- package/kit/gates/commit-explains-itself/check.sh +8 -4
- package/kit/gates/complexity-limit/README.md +5 -0
- package/kit/gates/complexity-limit/check.sh +21 -2
- package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
- package/kit/gates/deps-are-pinned/README.md +14 -1
- package/kit/gates/deps-are-pinned/check.sh +6 -1
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
- package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
- package/kit/gates/duplicate-code/README.md +11 -2
- package/kit/gates/duplicate-code/check.sh +31 -4
- package/kit/gates/duplicate-code/gate.yml +8 -0
- package/kit/gates/duplicate-code/green/imports_a.go +20 -0
- package/kit/gates/duplicate-code/green/imports_b.go +19 -0
- package/kit/gates/entry-links-exist/README.md +5 -0
- package/kit/gates/entry-links-exist/check.sh +6 -0
- package/kit/gates/entry-links-exist/green/AGENTS.md +3 -0
- package/kit/gates/file-size-limit/README.md +9 -2
- package/kit/gates/file-size-limit/check.sh +13 -1
- package/kit/gates/gate-not-weakened/README.md +54 -0
- package/kit/gates/gate-not-weakened/check.sh +84 -0
- package/kit/gates/gate-not-weakened/gate.yml +15 -0
- package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
- package/kit/gates/gate-not-weakened/green/payments.py +6 -0
- package/kit/gates/gate-not-weakened/green/release.sh +2 -0
- package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
- package/kit/gates/gate-not-weakened/red/payments.py +6 -0
- package/kit/gates/gate-not-weakened/red/release.sh +2 -0
- package/kit/gates/hook-actually-fires/README.md +74 -0
- package/kit/gates/hook-actually-fires/check.sh +183 -0
- package/kit/gates/hook-actually-fires/gate.yml +15 -0
- package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
- package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
- package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
- package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
- package/kit/gates/no-phantom-package/README.md +84 -0
- package/kit/gates/no-phantom-package/check.sh +161 -0
- package/kit/gates/no-phantom-package/gate.yml +20 -0
- package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
- package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
- package/kit/gates/no-print-in-prod/README.md +33 -39
- package/kit/gates/no-print-in-prod/gate.yml +14 -6
- package/kit/gates/personal-config-not-shared/README.md +66 -0
- package/kit/gates/personal-config-not-shared/check.sh +103 -0
- package/kit/gates/personal-config-not-shared/gate.yml +16 -0
- package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
- package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
- package/kit/gates/promise-has-gate/README.md +50 -0
- package/kit/gates/promise-has-gate/check.sh +88 -0
- package/kit/gates/promise-has-gate/gate.yml +14 -0
- package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
- package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
- package/kit/gates/secrets-not-in-code/check.sh +13 -1
- package/kit/gates/swallowed-error/README.md +36 -18
- package/kit/gates/swallowed-error/gate.yml +13 -3
- package/kit/gates/test-has-assertion/README.md +47 -0
- package/kit/gates/test-has-assertion/check.sh +206 -0
- package/kit/gates/test-has-assertion/gate.yml +15 -0
- package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
- package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
- package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
- package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
- package/kit/gates/test-not-adjusted/README.md +79 -0
- package/kit/gates/test-not-adjusted/check.sh +136 -0
- package/kit/gates/test-not-adjusted/gate.yml +19 -0
- package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
- package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
- package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
- package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
- package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
- package/kit/gates/todo-without-task/README.md +6 -0
- package/kit/gates/todo-without-task/check.sh +13 -1
- package/kit/ratchet/ratchet.sh +70 -2
- package/kit/rules/general.md +23 -0
- package/kit/rules-en/general.md +82 -0
- package/kit/rules-en/security.md +33 -0
- package/kit/rules-en/testing.md +48 -0
- package/llms.txt +2 -1
- package/package.json +4 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/doctor.mjs +90 -10
- package/tool/commands/gates.mjs +19 -5
- package/tool/commands/project.mjs +15 -2
- package/tool/commands/prove.mjs +67 -0
- package/tool/commands/report.mjs +4 -1
- package/tool/i18n/en-docs.mjs +70 -0
- package/tool/i18n/en.mjs +66 -54
- package/tool/i18n/ru-docs.mjs +70 -0
- package/tool/i18n/ru.mjs +66 -54
- package/tool/i18n/templates-en.mjs +9 -9
- package/tool/i18n/templates-ru.mjs +9 -9
- package/tool/lib/core.mjs +7 -1
- package/tool/lib/manifest.mjs +72 -5
- package/tool/lib/prove.mjs +160 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +131 -0
- package/tool/lib/templates.mjs +2 -0
- package/tool/program.mjs +6 -0
- package/tool/selfcheck/gates.sh +86 -3
- package/tool/selfcheck/lifecycle.mjs +29 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +329 -36
- package/tool/selfcheck/units-level.mjs +60 -0
- package/tool/selfcheck/units.mjs +196 -1
- package/kit/gates/no-print-in-prod/check.sh +0 -38
- package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
- package/kit/gates/no-print-in-prod/green/main.go +0 -8
- package/kit/gates/no-print-in-prod/green/main.rs +0 -4
- package/kit/gates/no-print-in-prod/red/main.go +0 -8
- package/kit/gates/no-print-in-prod/red/main.rs +0 -4
- package/kit/gates/swallowed-error/check.sh +0 -54
- package/kit/gates/swallowed-error/green/run.js +0 -8
- package/kit/gates/swallowed-error/red/run.js +0 -3
- /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
- /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
package/README.md
CHANGED
|
@@ -196,7 +196,7 @@ Already using [pre-commit](https://pre-commit.com)? Three lines in the file you
|
|
|
196
196
|
```yaml
|
|
197
197
|
repos:
|
|
198
198
|
- repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
|
|
199
|
-
rev: v0.
|
|
199
|
+
rev: v0.7.0
|
|
200
200
|
hooks:
|
|
201
201
|
- id: aqk # runs what the repository declares; blocks below AQK-1
|
|
202
202
|
# - id: aqk-doctor # read-only: the level and what is missing, blocks nothing
|
|
@@ -217,7 +217,7 @@ layer AQK adds.
|
|
|
217
217
|
[](https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
218
218
|
|
|
219
219
|
```yaml
|
|
220
|
-
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
220
|
+
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.7.0
|
|
221
221
|
with:
|
|
222
222
|
min: 1 # the build fails below AQK-1, or if any declared gate failed
|
|
223
223
|
```
|
|
@@ -236,13 +236,46 @@ aqk find "print statements in production" # is there already such a gate — m
|
|
|
236
236
|
aqk doctor # what applies to this repository and what is missing
|
|
237
237
|
aqk add secrets-not-in-code # copies the check and its samples in, declares it
|
|
238
238
|
aqk doctor --run # runs the declared gates and shows the result
|
|
239
|
+
aqk doctor --run --since main # ... but only what the diff introduced
|
|
239
240
|
aqk ratchet no-print-in-prod # existing violations become debt, new ones are blocked
|
|
240
241
|
```
|
|
241
242
|
|
|
243
|
+
### The first run on a real project
|
|
244
|
+
|
|
245
|
+
An established repository carries years of debt. Run every gate over all of it and you get a wall
|
|
246
|
+
of red that nobody reads — so the tool gets switched off. `--since <ref>` narrows the output to
|
|
247
|
+
files the diff touched:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
aqk doctor --run --since main # only what this branch introduced
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Three outcomes, all of them said out loud. Findings inside the diff — red, as usual. Findings only
|
|
254
|
+
outside it — green, with the number that was hidden, never a silent "all clear". And a gate whose
|
|
255
|
+
output carries no paths at all (a commit-message check, a CI-config check) **cannot** be narrowed:
|
|
256
|
+
it stays red, and says why. Calling it green because there was nothing to narrow would be exactly
|
|
257
|
+
the silence this tool exists to remove.
|
|
258
|
+
|
|
242
259
|
Every `doctor --run` rewrites `.aqk/last-run.md` — a short report of what actually ran and how
|
|
243
260
|
long it took. The list of gates in the manifest says nothing about how many of them are alive
|
|
244
261
|
right now; the report does. The file is ephemeral — keep it in your own `.gitignore`.
|
|
245
262
|
|
|
263
|
+
### Introducing a rule into a live project
|
|
264
|
+
|
|
265
|
+
Three ways, and each has a price. A big clean-up is put off forever because it is big. The
|
|
266
|
+
ratchet turns existing violations into debt and blocks new ones — right once the rule is agreed.
|
|
267
|
+
And while it is still being argued about, an advisory gate shows findings without failing the run:
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
advisory:
|
|
271
|
+
- complexity-limit
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Declared in the manifest, not passed as a flag. A flag that says "fail nothing" downgrades every
|
|
275
|
+
check at once, is invisible in the diff, and is never named in the summary — that is
|
|
276
|
+
`continue-on-error`, which this tool marks red elsewhere. The list is printed on **every** run:
|
|
277
|
+
an advisory gate everyone forgot about is a switched-off check.
|
|
278
|
+
|
|
246
279
|
## When a bug slips past the guards
|
|
247
280
|
|
|
248
281
|
```bash
|
|
@@ -289,6 +322,24 @@ catalogue may grow to hundreds of entries; a given project still sees about a do
|
|
|
289
322
|
An entry is accepted only if its arbiter goes red on the red sample, stays quiet on the green
|
|
290
323
|
one, and names a real failure it caught. A machine checks this: `bash tool/selfcheck/gates.sh`.
|
|
291
324
|
|
|
325
|
+
### Four entries that watch the agent, not the code
|
|
326
|
+
|
|
327
|
+
Ruff, ESLint and gitleaks already find bad code, and AQK calls them where it can rather than
|
|
328
|
+
reinventing them. These four look elsewhere — at the moment the **signal** about bad code is
|
|
329
|
+
switched off, which is what a coding agent does when the task is phrased as "make it pass":
|
|
330
|
+
|
|
331
|
+
| Entry | What it catches |
|
|
332
|
+
|---|---|
|
|
333
|
+
| `gate-not-weakened` | the fix was a suppression, not a fix: bare `# noqa`, `eslint-disable` with no rule named, `@ts-ignore`, `--no-verify` |
|
|
334
|
+
| `ci-actually-fails` | a pipeline step that renders a verdict but cannot fail — `run: pytest \|\| true`, `continue-on-error: true` |
|
|
335
|
+
| `test-has-assertion` | a test that cannot fail: empty body, `assert True`, a skip with no reason given |
|
|
336
|
+
| `promise-has-gate` | a rule in `AGENTS.md` with no enforcer named — neither a gate nor, honestly, a human |
|
|
337
|
+
|
|
338
|
+
Each was measured on nineteen third-party repositories (~25 000 files) before it entered the
|
|
339
|
+
catalogue, and two further entries were **cancelled by that measurement**: one because
|
|
340
|
+
[`agents-lint`](https://github.com/giacomo/agents-lint) already does it better, one because
|
|
341
|
+
91 of its 120 findings turned out to be a legitimate pattern.
|
|
342
|
+
|
|
292
343
|
## The guides as a single file
|
|
293
344
|
|
|
294
345
|
```bash
|
package/README.ru.md
CHANGED
|
@@ -198,7 +198,7 @@ aqk badge --check # в конвейере: код 1 в тот день, ког
|
|
|
198
198
|
```yaml
|
|
199
199
|
repos:
|
|
200
200
|
- repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
|
|
201
|
-
rev: v0.
|
|
201
|
+
rev: v0.6.0
|
|
202
202
|
hooks:
|
|
203
203
|
- id: aqk # запускает объявленное; роняет коммит ниже AQK-1
|
|
204
204
|
# - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
|
|
@@ -286,6 +286,40 @@ vendor/
|
|
|
286
286
|
Запись принимается, только если её арбитр краснеет на красном образце, молчит на зелёном и
|
|
287
287
|
назван реальный отказ, который она поймала. Проверяет это машина: `bash tool/selfcheck/gates.sh`.
|
|
288
288
|
|
|
289
|
+
### Четыре записи, которые смотрят на агента, а не на код
|
|
290
|
+
|
|
291
|
+
Ruff, ESLint и gitleaks и так находят плохой код — AQK зовёт их, где может, вместо того чтобы
|
|
292
|
+
писать своё. Эти четыре смотрят в другое место: на момент, когда **сигнал** о плохом коде
|
|
293
|
+
выключают. Именно это делает агент, когда задача сформулирована как «сделай, чтобы прошло»:
|
|
294
|
+
|
|
295
|
+
| Запись | Что ловит |
|
|
296
|
+
|---|---|
|
|
297
|
+
| `gate-not-weakened` | починкой было подавление: голый `# noqa`, `eslint-disable` без имени правила, `@ts-ignore`, `--no-verify` |
|
|
298
|
+
| `ci-actually-fails` | шаг конвейера, который выносит вердикт, но не может провалиться — `run: pytest \|\| true`, `continue-on-error: true` |
|
|
299
|
+
| `test-has-assertion` | тест, который не может провалиться: пустое тело, `assert True`, пропуск без причины |
|
|
300
|
+
| `promise-has-gate` | правило в `AGENTS.md`, у которого не назван сторож — ни гейт, ни, честно, человек |
|
|
301
|
+
|
|
302
|
+
Каждая измерена на девятнадцати чужих репозиториях (~25 000 файлов) до внесения в каталог, и ещё
|
|
303
|
+
две записи этот же замер **отменил**: одну — потому что
|
|
304
|
+
[`agents-lint`](https://github.com/giacomo/agents-lint) делает это лучше, другую — потому что
|
|
305
|
+
91 находка из 120 оказалась законным приёмом.
|
|
306
|
+
|
|
307
|
+
### Как ввести правило в живой проект
|
|
308
|
+
|
|
309
|
+
Три способа, и у каждого своя цена. Большая чистка откладывается навсегда, потому что она
|
|
310
|
+
большая. Храповик превращает старые нарушения в долг и блокирует новые — верно, когда правило
|
|
311
|
+
уже принято. А пока о правиле спорят, совещательный гейт показывает находки, не роняя прогон:
|
|
312
|
+
|
|
313
|
+
```yaml
|
|
314
|
+
advisory:
|
|
315
|
+
- complexity-limit
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Объявлением в манифесте, а не флагом. Флаг «не роняй ничего» понижает все проверки разом, не
|
|
319
|
+
виден в дифе и не назван в итоге — это тот самый `continue-on-error`, который мы сами красим.
|
|
320
|
+
Список печатается **каждый** прогон: совещательный гейт, о котором забыли, — это выключенная
|
|
321
|
+
проверка.
|
|
322
|
+
|
|
289
323
|
## Методички одним файлом
|
|
290
324
|
|
|
291
325
|
```bash
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
|---|---|---|---|
|
|
194
194
|
| `block-dangerous-commands.sh` | PreToolUse (Bash **и** PowerShell) | exit 2 + причина в stderr на необратимое: force-push, reset --hard, `DROP DATABASE`, `TRUNCATE`, `docker volume rm`, `rm -rf /` | fail-safe: не распарсил JSON → грепай сырой ввод. Штатный сброс дев-БД (`compose down -v`) — НЕ блокировать |
|
|
195
195
|
| `auto-format.sh` | PostToolUse (Write\|Edit) | `ruff format` + `ruff check --fix` на изменённый `.py` | агент физически не оставляет неотформатированный код, контекст не тратится |
|
|
196
|
-
| `stop-gate.sh` | Stop | красный `ruff` по прод-путям → exit 2 + хвост ошибок → агент чинит, а не «сдаёт» | обязателен гард `stop_hook_active
|
|
196
|
+
| `stop-gate.sh` | Stop | красный `ruff` по прод-путям → exit 2 + хвост ошибок → агент чинит, а не «сдаёт» | обязателен гард `stop_hook_active`; проверять только СВОЙ домен, не параллельную работу человека. Поле настоящее — Stop и SubagentStop получают его на вход; но вечного цикла не будет и без гарда: «Claude Code overrides the hook and ends the turn after 8 consecutive blocks» (code.claude.com/docs/en/hooks, раздел Stop input, сверено 2026-09-07). Цена ошибки — восемь ходов, а не вечность |
|
|
197
197
|
|
|
198
198
|
- [ ] ⚠️ **Грабля №1 (Windows): `jq` нет в Git Bash.** Хук с `command -v jq || exit 0` молча
|
|
199
199
|
превращается в no-op — защита «есть», но не работает. Парсить JSON через
|
|
@@ -149,6 +149,54 @@ ruff check --select TRY400 --statistics . # сколько находок У
|
|
|
149
149
|
|
|
150
150
|
---
|
|
151
151
|
|
|
152
|
+
## Заглушка вместо реализации: почти всё уже покрыто
|
|
153
|
+
|
|
154
|
+
Самый ожидаемый способ, которым агент «заканчивает» задачу, — подпись без работы. Мы собирались
|
|
155
|
+
писать на это запись каталога и не стали: замер по девятнадцати репозиториям (~25 000 файлов)
|
|
156
|
+
показал, что своей доли почти не остаётся.
|
|
157
|
+
|
|
158
|
+
| Что | Чем ловится | Не забыть |
|
|
159
|
+
|---|---|---|
|
|
160
|
+
| пустое тело функции в JS и TS | `eslint` [`no-empty-function`](https://eslint.org/docs/latest/rules/no-empty-function) | функция с комментарием внутри не считается пустой |
|
|
161
|
+
| абстрактный метод, не переопределённый в конкретном классе | `pylint` [`W0223`](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/abstract-method.html) | `--disable=all --enable=W0223` — остальное берёт ruff |
|
|
162
|
+
| `raise NotImplemented` вместо `NotImplementedError` | `ruff` [`F901`](https://docs.astral.sh/ruff/rules/raise-not-implemented/) | входит в группу `F` |
|
|
163
|
+
| лишний `pass` рядом с настоящим кодом | `ruff` `PIE790` | |
|
|
164
|
+
|
|
165
|
+
**Чего мы НЕ стали делать и почему.** Пустое тело (`pass`) — не признак недоделки: из 120 находок
|
|
166
|
+
первой версии 91 оказалась законной. Это null-объекты (`NoOpSpan` в sentry-python), безопасные
|
|
167
|
+
заглушки провайдера в pr-agent — там прямо стоит комментарий «safe no-op stubs», —
|
|
168
|
+
необязательные обработчики. А `raise NotImplementedError` в методе класса и есть питоновский
|
|
169
|
+
способ объявить абстракцию, даже без `abc`: так написаны `ContentDecoder` в httpx и интерфейс
|
|
170
|
+
плагина в pre-commit. За вычетом этих двух классов на 25 000 файлах не осталось ни одной находки.
|
|
171
|
+
|
|
172
|
+
## Обвес самого агента: тоже есть готовое
|
|
173
|
+
|
|
174
|
+
К осени 2026 появился отдельный класс инструментов — линтеры не кода, а того, что читает агент:
|
|
175
|
+
`AGENTS.md`, `CLAUDE.md`, файлы навыков, конфиги хуков и MCP. Писать своё здесь незачем.
|
|
176
|
+
|
|
177
|
+
| Инструмент | Что проверяет | Состояние — дата замера в самой ячейке |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| [`agnix`](https://github.com/agent-sh/agnix) | 455 правил: структура `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, синтаксис конфигов MCP и хуков, соглашения об именах, **мёртвые ссылки на файлы**. Есть автопочинка и LSP | 404 ⭐, Rust, активен на 2026-09-06. `npm i -g agnix`, `brew`, `pip`, `cargo` |
|
|
180
|
+
| [`agents-lint`](https://github.com/giacomo/agents-lint) | мёртвые npm-скрипты, упомянутые в `AGENTS.md`, устаревшие рамки, деревья каталогов в контексте | 13 ⭐, TypeScript, последний коммит март 2026 — снято 2026-09-06 |
|
|
181
|
+
| [`claudelint`](https://github.com/pdugan20/claudelint) | 116 правил: схема `.claude/settings.json`, синтаксис правил доступа, имена переменных окружения, ссылки на несуществующие файлы, разбор навыков, плагинов, MCP и LSP. Ловит `"allow": ["*"]` | TypeScript, MIT, коммит 2026-09-08. `npx claude-code-lint`. **Код возврата 0 даже на находке**, если её строгость — `warn`: блокирует только `--strict`. Проверено прогоном 0.8.0 |
|
|
182
|
+
| [`slopcheck`](https://github.com/mattschaller/slopcheck) — **не путать с `0xToxSec/slopcheck`, это разные проекты** | имена npm-пакетов из команд установки в `.md`, `.mdc`, `.yml`, `.yaml`, `.json`, `.cursorrules` сверяет с реестром: пакет, которого не существует, — приманка для захвата имени | MIT, ноль зависимостей, TypeScript, пуш 2026-09-06. `npx slopcheck .`, код возврата 1 на находке — проверено прогоном 0.2.0 от 2026-09-08. Без сети выходит с нулём: «не проверено» у него неотличимо от «чисто» |
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
npm install -g agnix && agnix --strict .
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Чего они НЕ делают — и почему у AQK остаётся своя половина.** Все они проверяют документ:
|
|
189
|
+
формат, существование путей, наличие скриптов. Ни один не спрашивает, **подкреплено ли обещание
|
|
190
|
+
командой с кодом возврата**. «Мы никогда не коммитим секреты» — грамматически безупречная
|
|
191
|
+
строка, на которую ни один из них ничего не скажет. Разделение простое: обвес агента проверяет
|
|
192
|
+
`agnix`, права и схему настроек — `claudelint`, исполнимость обещаний — `promise-has-gate`.
|
|
193
|
+
|
|
194
|
+
**Одну дыру в обвесе пришлось закрыть самим.** Имя события хука: ошибка в нём не показывается,
|
|
195
|
+
хук просто не вызывается. У `claudelint` правило `hooks-invalid-event` есть в исходниках, но
|
|
196
|
+
живой прогон версии 0.8.0 на файле с `"PoToolUse"` даёт «No problems found». Замер по 48 чужим
|
|
197
|
+
настройкам нашёл шесть таких хуков в четырёх репозиториях — среди них `typecheck && test` перед
|
|
198
|
+
коммитом, не запускавшийся ни разу. Это запись `hook-actually-fires`.
|
|
199
|
+
|
|
152
200
|
## А если проект не на Python?
|
|
153
201
|
|
|
154
202
|
Ничего не меняется. Запись каталога держит **одно намерение и несколько исполнителей**, и
|
|
@@ -161,6 +209,23 @@ ruff check --select TRY400 --statistics . # сколько находок У
|
|
|
161
209
|
**Одна строка в существующей записи — самый дешёвый и самый ценный вклад.** Намерение уже
|
|
162
210
|
доказано отказом, образцы уже лежат, проверять нечего кроме самой команды.
|
|
163
211
|
|
|
212
|
+
## Приёмы агента, названные практиками: чем закрыт каждый
|
|
213
|
+
|
|
214
|
+
Список взят из разбора 1154 обсуждений с r/programming, r/learnprogramming, r/ExperiencedDevs
|
|
215
|
+
и Hacker News (Baltes, Cheong, Treude, «An Endless Stream of AI Slop», arxiv 2603.27249,
|
|
216
|
+
январь–сентябрь 2025). Это не подборка мнений, а размеченный корпус.
|
|
217
|
+
|
|
218
|
+
| Приём | Чем закрыт |
|
|
219
|
+
|---|---|
|
|
220
|
+
| «test subversion»: правка теста, чтобы прошёл сломанный код | [`checkwash`](https://github.com/taipei49314/checkwash) — запись `test-not-adjusted` делегирует ему целиком |
|
|
221
|
+
| «deleting methods instead of fixing them» | он же, детектор `TEST_DISABLED` |
|
|
222
|
+
| «casting to `any` to silence type errors» | `@typescript-eslint/no-explicit-any`. Своей записи нет намеренно: замер по `zod` — 769 вхождений на 501 файл, первый прогон даёт стену |
|
|
223
|
+
| «using `setTimeout` as a band-aid fix» | ничем. Замера нет, риск ложных высок: `setTimeout` законен сплошь и рядом |
|
|
224
|
+
| «hallucinating external services, then mocking» | ничем. Отличить выдуманную службу от настоящей статически нечем |
|
|
225
|
+
| выдуманная зависимость (slopsquatting) | [`slopcheck`](https://github.com/mattschaller/slopcheck) — тот, что лежит в npm под этим именем; на нём стоит запись `no-phantom-package`. **Проектов с именем `slopcheck` два**: [`0xToxSec/slopcheck`](https://github.com/0xToxSec/slopcheck) тоже MIT, но последний пуш апрель 2026 и в npm его нет. Проверено 2026-09-08 |
|
|
226
|
+
| подавление проверки без адреса | наш `gate-not-weakened` плюс `eslint-plugin-eslint-comments`, `flake8-noqa` |
|
|
227
|
+
| шаг конвейера, который не может провалиться | наш `ci-actually-fails`; у `checkwash` есть смежный `CI_WORKFLOW_TOUCHED` |
|
|
228
|
+
|
|
164
229
|
## Если готового нет
|
|
165
230
|
|
|
166
231
|
Тогда свой гейт — и в его `README.md` пишется, **что именно проверено**: какой инструмент
|
package/kit/gates/README.md
CHANGED
|
@@ -70,6 +70,64 @@
|
|
|
70
70
|
Плюс шестое, без которого запись не принимается: **доказательство** — реальный отказ, который
|
|
71
71
|
она поймала. «Это хорошая практика» не принимается.
|
|
72
72
|
|
|
73
|
+
## Находка обязана кончаться действием
|
|
74
|
+
|
|
75
|
+
Проверка, покрасневшая молча, ничем не лучше молчащей. Проверка, сказавшая «плохо» и не
|
|
76
|
+
сказавшая «делай так», — немногим лучше: человек, открывший тридцать находок, хочет команду,
|
|
77
|
+
а не оценку.
|
|
78
|
+
|
|
79
|
+
Поэтому последняя строка вывода — совет, и он начинается с метки:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
почини: замени на вызов системы логов — тогда запись попадёт в общий журнал
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Приёмка требует эту строку на красном образце, а `doctor --run` печатает её **всегда**: находки
|
|
86
|
+
обрезаются до трёх, совет не обрезается никогда. До этой правки строка существовала для приёмки
|
|
87
|
+
и не существовала для человека — обрезка съедала ровно её.
|
|
88
|
+
|
|
89
|
+
Требование действует для записей с переносимым рецептом. Запись, целиком делегирующая готовому
|
|
90
|
+
инструменту, печатает вывод этого инструмента, и требовать от чужого вывода нашу строку значит
|
|
91
|
+
требовать невозможного.
|
|
92
|
+
|
|
93
|
+
## Зрелость записи не пишут руками
|
|
94
|
+
|
|
95
|
+
Седьмого поля нет: зрелость **считается** из доказательства. Ссылается `proof` на журнал шишек —
|
|
96
|
+
запись зрелая; не ссылается — условная, и это видно в приёмке каталога. Написать себе
|
|
97
|
+
`lifecycle: stable` нельзя, приёмка такую запись отклонит.
|
|
98
|
+
|
|
99
|
+
Это не придирка к форме. У всех трёх соседей, чей каталог мы разбирали, поле зрелости есть, и
|
|
100
|
+
у всех троих его заполняет автор: `lifecycle` у зондов Scorecard, `future`/`obsolete` у
|
|
101
|
+
критериев значка OpenSSF. Значение, написанное автором, означает доверие к автору. Каталог, где
|
|
102
|
+
зрелость объявляют, к сотне записей превращается в список, в котором нельзя выбрать.
|
|
103
|
+
|
|
104
|
+
Объявляется ровно одно состояние — **`deprecated`**, потому что «эту запись больше не ставят»
|
|
105
|
+
из её файлов не выводится никак. Вместе с ним обязателен `superseded_by` с именем существующей
|
|
106
|
+
записи, и `aqk add` тогда отказывает в установке, назвав преемника:
|
|
107
|
+
|
|
108
|
+
```yaml
|
|
109
|
+
lifecycle: deprecated
|
|
110
|
+
superseded_by: no-print-in-prod
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Программа, без которой запись не работает
|
|
114
|
+
|
|
115
|
+
**`requires:`** называет её явно. Нужно там, где переносимый рецепт — обёртка вокруг готового
|
|
116
|
+
инструмента: первое слово команды тогда `bash`, который есть всегда, и по нему не видно, чего
|
|
117
|
+
не хватает. Без этого поля гейт ставился бы и вставал при первом же запуске с «not found» —
|
|
118
|
+
отсутствие сигнала неотличимо от успеха.
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
recipes:
|
|
122
|
+
any: bash {gate}/check.sh {dir}
|
|
123
|
+
requires: checkwash
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`add` отказывает с названной причиной и говорит, что поставить. Приёмка и мутационная проверка
|
|
127
|
+
пропускают запись со словами «НЕ ПРОВЕРЕНА здесь — нужен «…»». Строгий режим
|
|
128
|
+
(`AQK_GATES_STRICT=1`, поднят в нашем конвейере) делает такой пропуск ошибкой: на машине,
|
|
129
|
+
которая инструменты сама и ставит, «нечем проверить» обязано быть красным.
|
|
130
|
+
|
|
73
131
|
## Запись без переносимого рецепта
|
|
74
132
|
|
|
75
133
|
Иногда переносимой проверки быть не может: чтобы понять, вызывают ли функцию, нужен граф
|
|
@@ -173,6 +231,8 @@ samples_for: python
|
|
|
173
231
|
| `has_deps: true` | есть файл зависимостей |
|
|
174
232
|
| `has_tests: true` | есть каталог тестов или файлы вида `*_test.*` |
|
|
175
233
|
| `has_env: true` | есть файл окружения |
|
|
234
|
+
| `has_agent_entry: true` | агента здесь используют: есть `CLAUDE.md`, `AGENTS.md`, каталог `.claude`, `.cursor/rules` или инструкции copilot |
|
|
235
|
+
| `has_agent_config: true` | есть настройки самого агента: `.claude/settings.json`, `.claude/settings.local.json`, `.claude/hooks.json`, `.claude/hooks/hooks.json`. Второй файл лежит в `.gitignore` — признак сработает у человека и не сработает в конвейере |
|
|
176
236
|
| `has_ui: true` | есть стили или однофайловые компоненты (`.css`, `.scss`, `.vue`, `.svelte`, `.astro`) |
|
|
177
237
|
|
|
178
238
|
Любое из `has_*` принимает и `false` — «показывать тем, у кого этого нет». Условие, которого
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Проверка в конвейере может провалиться
|
|
2
|
+
|
|
3
|
+
**Намерение.** Шаг выполнен, круг зелёный, проверка не сработала. `run: pytest || true` —
|
|
4
|
+
это строка в логе, а не проверка.
|
|
5
|
+
|
|
6
|
+
**Какой отказ это поймало.** Дыру нашли у себя. Запись `gates-run-in-ci` отвечает на вопрос
|
|
7
|
+
«упомянут ли гейт в конфиге конвейера» и на этом останавливается — то есть конфиг с
|
|
8
|
+
`run: pytest || true` проходил её зелёным. «Упомянут» и «работает» — разные утверждения, и весь
|
|
9
|
+
этот стандарт стоит на том, чтобы их не путать; у себя мы их спутали.
|
|
10
|
+
|
|
11
|
+
Замер по пятнадцати чужим репозиториям подтвердил, что класс живой: у `reviewdog` два шага с
|
|
12
|
+
его собственными линтерами идут под `continue-on-error: true`. Запись в журнале:
|
|
13
|
+
`incidents/README.md`, 2026-09-06.
|
|
14
|
+
|
|
15
|
+
**Что именно проверяется.** Конфиг разбирается по шагам. Шаг красный, если он **и** выносит
|
|
16
|
+
вердикт, **и** не может провалиться.
|
|
17
|
+
|
|
18
|
+
| Выносит вердикт, если | Не может провалиться, если |
|
|
19
|
+
|---|---|
|
|
20
|
+
| команда есть в списке запускалок (`pytest`, `eslint`, `go test`, `golangci-lint`, `mypy`, `cargo clippy`, …) | шаг помечен `continue-on-error: true` |
|
|
21
|
+
| команда объявлена гейтом в `.aqk.yml` **этого** проекта | задача помечена `allow_failure: true` (gitlab) |
|
|
22
|
+
| в **имени шага** стоит слово `lint`, `test`, `check`, `verify`, `audit`, `scan`, `coverage` | провал погашен в самой команде: `\|\| true`, `\|\| :`, `\|\| exit 0` |
|
|
23
|
+
|
|
24
|
+
Гашение прямо в команде красится только для закрытого списка запускалок: `docker network create … || true` — это идемпотентность, а не выключенная проверка.
|
|
25
|
+
|
|
26
|
+
**Готовый аналог.** Не нашли. [`actionlint`](https://github.com/rhysd/actionlint) разбирает
|
|
27
|
+
синтаксис workflow, [`zizmor`](https://github.com/woodruffw/zizmor) ищет в них дыры
|
|
28
|
+
безопасности — ни тот, ни другой не спрашивает, может ли шаг провалиться. `continue-on-error`
|
|
29
|
+
для них — законная настройка, каковой она и является: незаконной её делает то, ЧТО под ней
|
|
30
|
+
стоит, а это знает только проект.
|
|
31
|
+
|
|
32
|
+
**Исход переспрашивают — маска законна.** `continue-on-error` иногда стоит не ради прощения
|
|
33
|
+
провала, а чтобы дать выполниться шагам ПОСЛЕ проверки; вердикт выносится отдельным шагом
|
|
34
|
+
`if: steps.<id>.outcome == 'failure'` → `exit 1`. Так устроен `pre-commit.yml` в `fastapi`:
|
|
35
|
+
проверка идёт под маской, потом чинит файлы и пушит их в ветку, и только в конце роняет
|
|
36
|
+
сборку. Такой шаг больше не находка. Условие узкое: в файле должна быть И ссылка на исход,
|
|
37
|
+
И падение — переспросить исход и ничего с ним не сделать значит простить провал длиннее на
|
|
38
|
+
три строки, и это по-прежнему красное (образец `red/.github/workflows/soft.yml`).
|
|
39
|
+
|
|
40
|
+
Замер 2026-09-07 по пяти чужим репозиториям: молчал на всех пяти. На `fastapi` — по
|
|
41
|
+
случайности, слово-примета не совпало с именем шага «Run prek - pre-commit»; назови они его
|
|
42
|
+
«Run lint», гейт покрасил бы законный уклад. Дефект найден разбором молчания, а не находки.
|
|
43
|
+
|
|
44
|
+
**Чего НЕ ловит.**
|
|
45
|
+
|
|
46
|
+
- **Проверку, которую не по чему опознать.** Задача `mutation-diff` с именем «Mutation score on
|
|
47
|
+
changed files» под `continue-on-error` (нашлась в `kodus-ai`, автор сам пометил её «advisory —
|
|
48
|
+
does not block») не опознаётся: в имени нет слова-приметы, а команда не из списка. Объяви такую
|
|
49
|
+
проверку гейтом в `.aqk.yml` — тогда она станет видна точно, а не по догадке.
|
|
50
|
+
- **Провал, погашенный внутри скрипта.** `set +e`, `trap`, `exit 0` в конце `run: |` — это уже
|
|
51
|
+
логика скрипта, а не конфиг конвейера.
|
|
52
|
+
- **Шаг, который проходит по другой причине.** Тест, всегда возвращающий 0, — не эта запись,
|
|
53
|
+
а `test-has-assertion`.
|
|
54
|
+
- **`if: always()`** маскировкой не считается: он про порядок выполнения, а не про вердикт.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Конвейер, который гасит провал проверки: шаг выполнен, круг зелёный, проверка не сработала.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ ОТДЕЛЬНО ОТ «гейт запускается конвейером». Та проверка отвечает на вопрос «упомянут ли»,
|
|
5
|
+
# и на этом останавливается. Мы нашли дыру в собственной оснастке: конфиг с `run: pytest || true`
|
|
6
|
+
# проходил её зелёным — команда упомянута, а провалиться не может никогда. «Упомянут» и
|
|
7
|
+
# «работает» — разные утверждения, и весь этот стандарт стоит на том, чтобы их не путать.
|
|
8
|
+
DIR="${1:-.}"
|
|
9
|
+
|
|
10
|
+
CI=$(find "$DIR/.github/workflows" "$DIR/.gitlab-ci.yml" "$DIR/.circleci" "$DIR/Jenkinsfile" \
|
|
11
|
+
-type f 2>/dev/null)
|
|
12
|
+
[ -z "$CI" ] && { echo "конвейера нет — эта проверка не про тебя"; exit 0; }
|
|
13
|
+
|
|
14
|
+
# Что считается ПРОВЕРКОЙ. Список намеренно закрытый: маскировка бывает законной — необязательная
|
|
15
|
+
# выгрузка отчёта, публикация артефакта, уведомление. Красить всякий `continue-on-error` значит
|
|
16
|
+
# получить гейт, который выключат первым. Красим только гашение того, что выносит вердикт.
|
|
17
|
+
RUNNERS='aqk|doctor --run|pytest|tox|nox|unittest|jest|vitest|mocha|jasmine|karma|playwright|cypress|eslint|tsc|ruff|flake8|pylint|mypy|pyright|bandit|semgrep|gitleaks|trivy|rubocop|golangci-lint|golint|govet|go vet|go test|staticcheck|shellcheck|hadolint|actionlint|codespell|reviewdog|cargo test|cargo clippy|mvn|gradle|phpstan|psalm|npm test|npm run (test|lint|check|typecheck)|yarn (test|lint)|pnpm (test|lint)|make (test|lint|check)'
|
|
18
|
+
|
|
19
|
+
# Закрытый список не поспевает: замер по чужим репозиториям нашёл шаг «Run reviewdog
|
|
20
|
+
# (github-pr-check)» под `continue-on-error: true`, и ни одно имя из списка в нём не звучало.
|
|
21
|
+
# Поэтому вторая примета — СЛОВО в имени шага или в команде. Целым словом: «checkout» не
|
|
22
|
+
# «check», иначе первый же `actions/checkout` красил бы каждый конвейер на свете.
|
|
23
|
+
WORDS='([Ll]int|[Tt]est|[Cc]heck|[Vv]erify|[Aa]udit|[Ss]can|[Tt]ypecheck|[Cc]overage)([^A-Za-z]|$)'
|
|
24
|
+
|
|
25
|
+
# Проверка из манифеста — тоже проверка, как бы она ни называлась в этом проекте.
|
|
26
|
+
MAN="$DIR/.aqk.yml"
|
|
27
|
+
if [ -f "$MAN" ]; then
|
|
28
|
+
KEYS=$(tr -d '\r' < "$MAN" | awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:/{
|
|
29
|
+
sub(/^[[:space:]]*[A-Za-z0-9_-]*:[[:space:]]*/,""); gsub(/^"|"$/,"");
|
|
30
|
+
n=split($0,w," "); for(i=1;i<=n;i++) if (index(w[i],"/")) { print w[i]; break }
|
|
31
|
+
}')
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
BAD=""
|
|
35
|
+
for F in $CI; do
|
|
36
|
+
# Шаги, чей исход ПЕРЕСПРАШИВАЮТ ниже: `continue-on-error` на них стоит не ради прощения
|
|
37
|
+
# провала, а чтобы дали выполниться шагам после — а вердикт выносится отдельным шагом
|
|
38
|
+
# `if: steps.<id>.outcome == 'failure'` → `exit 1`. Найдено замером по fastapi
|
|
39
|
+
# (`.github/workflows/pre-commit.yml`): проверка идёт под маской, потом чинит файлы и пушит
|
|
40
|
+
# их в ветку, и только в конце роняет сборку. Гейт молчал там по случайности — слово-примета
|
|
41
|
+
# не совпало; назови они шаг «lint», он покрасил бы законный уклад.
|
|
42
|
+
#
|
|
43
|
+
# Засчитывается только когда в файле есть И ссылка на исход, И падение: переспросить исход и
|
|
44
|
+
# ничего с ним не сделать — то же самое прощение, только длиннее.
|
|
45
|
+
REDEEMED=""
|
|
46
|
+
if tr -d '\r' < "$F" | grep -qE '^[[:space:]]*(-[[:space:]]+)?run[[:space:]]*:.*(exit[[:space:]]+1|^[[:space:]]*false[[:space:]]*$)'; then
|
|
47
|
+
REDEEMED=$(tr -d '\r' < "$F" | sed -n "s/.*steps\.\([A-Za-z0-9_-]*\)\.\(outcome\|conclusion\|result\).*/\1/p" | sort -u)
|
|
48
|
+
fi
|
|
49
|
+
# Разбор ПО ШАГАМ, а не по строкам. Построчно проверка врала в обе стороны: законный
|
|
50
|
+
# `continue-on-error` на шаге выгрузки отчёта красил соседний шаг с тестами, а слово «test»
|
|
51
|
+
# внутри перечисления типов коммита («feat|fix|test|chore») делало проверкой строку, которая
|
|
52
|
+
# ничего не проверяет. Замер по чужим конвейерам дал 4 ложных из 10 — переписано на блоки.
|
|
53
|
+
#
|
|
54
|
+
# Шаг начинается элементом списка («- ») или ключом верхнего уровня: так устроен и github,
|
|
55
|
+
# и gitlab, где `allow_failure` живёт на уровне задачи.
|
|
56
|
+
RES=$(tr -d '\r' < "$F" | awk -v runners="$RUNNERS" -v words="$WORDS" -v keys="$KEYS" -v file="$F" -v redeemed="$REDEEMED" '
|
|
57
|
+
function isComment(l) { return l ~ /^[[:space:]]*#/ }
|
|
58
|
+
function looksLikeCheck(l, j, nk) {
|
|
59
|
+
if (isComment(l)) return 0
|
|
60
|
+
# `uses:` — чужое действие. Его провал бывает законно необязательным: выгрузка отчёта,
|
|
61
|
+
# комментарий в пул-реквест, уведомление. Вердикт выносит то, что ЗАПУСКАЮТ.
|
|
62
|
+
if (l ~ /^[[:space:]]*(-[[:space:]]+)?uses[[:space:]]*:/) return 0
|
|
63
|
+
if (l ~ runners) return 1
|
|
64
|
+
# Проверка, объявленная в манифесте ЭТОГО проекта, — тоже проверка, как бы она ни
|
|
65
|
+
# называлась. Это самая точная примета из трёх: не догадка по имени, а список, который
|
|
66
|
+
# проект написал сам.
|
|
67
|
+
nk = split(keys, K, "\n")
|
|
68
|
+
for (j = 1; j <= nk; j++) if (K[j] != "" && index(l, K[j])) return 1
|
|
69
|
+
# Слово-примета — ТОЛЬКО в имени шага. В теле команды оно ловит своё же упоминание:
|
|
70
|
+
# «grep -oE (feat|fix|test|chore)» — это разбор заголовка коммита, а не проверка.
|
|
71
|
+
if (l ~ /^[[:space:]]*(-[[:space:]]+)?name[[:space:]]*:/ && l ~ words) return 1
|
|
72
|
+
return 0
|
|
73
|
+
}
|
|
74
|
+
function isMask(l) {
|
|
75
|
+
return !isComment(l) && l ~ /^[[:space:]]*(continue-on-error|allow_failure|ignore_failure)[[:space:]]*:[[:space:]]*(true|yes)/
|
|
76
|
+
}
|
|
77
|
+
function isBoundary(l) { return l ~ /^[[:space:]]*-[[:space:]]/ || l ~ /^[A-Za-z_.-]+[[:space:]]*:/ }
|
|
78
|
+
# Исход этого шага переспрашивают ниже — маска на нём законна.
|
|
79
|
+
function isRedeemed(id, j, nr) {
|
|
80
|
+
if (id == "") return 0
|
|
81
|
+
nr = split(redeemed, R, "\n")
|
|
82
|
+
for (j = 1; j <= nr; j++) if (R[j] != "" && R[j] == id) return 1
|
|
83
|
+
return 0
|
|
84
|
+
}
|
|
85
|
+
function flush( ) {
|
|
86
|
+
if (blockStart && blockCheck && blockMask && !isRedeemed(blockId))
|
|
87
|
+
printf "%s:%d: проверка не может провалиться — шаг под %s\n", file, blockCheckLine, blockMaskText
|
|
88
|
+
blockStart = 0; blockCheck = 0; blockMask = 0; blockId = ""
|
|
89
|
+
}
|
|
90
|
+
{
|
|
91
|
+
# Гашение прямо в команде красится только для ЗАКРЫТОГО списка запускалок: «|| true» на
|
|
92
|
+
# вспомогательной команде внутри скрипта (`docker network create … || true`) — это
|
|
93
|
+
# идемпотентность, а не выключенная проверка.
|
|
94
|
+
if (!isComment($0) && $0 ~ runners && $0 ~ /\|\|[[:space:]]*(true|:|exit[[:space:]]+0)/) {
|
|
95
|
+
line = $0; sub(/^[[:space:]]+/, "", line)
|
|
96
|
+
printf "%s:%d: провал погашен прямо в команде: %s\n", file, NR, substr(line, 1, 90)
|
|
97
|
+
}
|
|
98
|
+
if (isBoundary($0)) flush()
|
|
99
|
+
if (!blockStart) blockStart = NR
|
|
100
|
+
if (!blockCheck && looksLikeCheck($0)) { blockCheck = 1; blockCheckLine = NR }
|
|
101
|
+
if (isMask($0)) { blockMask = 1; blockMaskText = $0; sub(/^[[:space:]]+/, "", blockMaskText) }
|
|
102
|
+
if (!isComment($0) && $0 ~ /^[[:space:]]*(-[[:space:]]+)?id[[:space:]]*:/) {
|
|
103
|
+
blockId = $0; sub(/^[^:]*:[[:space:]]*/, "", blockId); gsub(/[[:space:]"'"'"']/, "", blockId)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
END { flush() }' 2>/dev/null)
|
|
107
|
+
[ -z "$RES" ] || BAD="$BAD$RES
|
|
108
|
+
"
|
|
109
|
+
done
|
|
110
|
+
|
|
111
|
+
LEFT="$(printf '%s' "$BAD" | grep -v '^$')"
|
|
112
|
+
[ -z "$LEFT" ] && exit 0
|
|
113
|
+
printf '%s\n' "$LEFT"
|
|
114
|
+
echo " почини: убери «|| true» и «continue-on-error» с шага, который выносит вердикт."
|
|
115
|
+
echo " шаг, который не может провалиться, — это не проверка, а строка в логе."
|
|
116
|
+
exit 1
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
intent: шаг конвейера, выносящий вердикт, может провалиться — а не только выполниться
|
|
2
|
+
intent_en: a pipeline step that renders a verdict can actually fail, not merely run
|
|
3
|
+
|
|
4
|
+
# Только там, где конвейер есть. Проверять его отсутствие — дело другой записи.
|
|
5
|
+
trigger:
|
|
6
|
+
has_ci: true
|
|
7
|
+
|
|
8
|
+
recipes:
|
|
9
|
+
any: bash {gate}/check.sh {dir}
|
|
10
|
+
|
|
11
|
+
proof: incidents/README.md, 2026-09-06 «упомянут и работает — разные утверждения» — дыра
|
|
12
|
+
найдена в собственной оснастке (`gates-run-in-ci` пропускал `pytest || true` зелёным),
|
|
13
|
+
замер по пятнадцати чужим репозиториям подтвердил класс: у `reviewdog` два шага
|
|
14
|
+
собственных линтеров идут под `continue-on-error: true`
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push]
|
|
3
|
+
jobs:
|
|
4
|
+
build:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
steps:
|
|
7
|
+
- run: npm ci
|
|
8
|
+
- name: тесты
|
|
9
|
+
run: pytest
|
|
10
|
+
- name: типы
|
|
11
|
+
run: tsc --noEmit
|
|
12
|
+
- name: выгрузить отчёт
|
|
13
|
+
run: bash scripts/upload-report.sh
|
|
14
|
+
continue-on-error: true
|
|
15
|
+
|
|
16
|
+
# Законный уклад: шаг помечен continue-on-error не для того, чтобы простить провал, а
|
|
17
|
+
# чтобы дать выполниться шагам после него; вердикт выносится ниже, по его исходу.
|
|
18
|
+
# Найдено замером по fastapi (.github/workflows/pre-commit.yml): проверка прогоняется
|
|
19
|
+
# под continue-on-error, потом чинит файлы и пушит их в ветку, а в конце падает, если
|
|
20
|
+
# проверка была красной. Гейт, красящий такое, требует убрать то, без чего уклад не
|
|
21
|
+
# работает, — и его выключат целиком.
|
|
22
|
+
- name: линтер
|
|
23
|
+
id: lint
|
|
24
|
+
run: ruff check .
|
|
25
|
+
continue-on-error: true
|
|
26
|
+
- name: применить починки и запушить
|
|
27
|
+
run: bash scripts/push-fixes.sh
|
|
28
|
+
- name: провалить сборку, если линтер был красным
|
|
29
|
+
if: steps.lint.outcome == 'failure'
|
|
30
|
+
run: exit 1
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Исход проверки переспрашивают — и ничего с ним не делают. Ровно то же прощение, что и
|
|
2
|
+
# голый continue-on-error, только длиннее на три строки и убедительнее на вид.
|
|
3
|
+
name: soft
|
|
4
|
+
on: [push]
|
|
5
|
+
jobs:
|
|
6
|
+
build:
|
|
7
|
+
runs-on: ubuntu-latest
|
|
8
|
+
steps:
|
|
9
|
+
- name: линтер
|
|
10
|
+
id: lint
|
|
11
|
+
run: ruff check .
|
|
12
|
+
continue-on-error: true
|
|
13
|
+
- name: сказать вслух
|
|
14
|
+
if: steps.lint.outcome == 'failure'
|
|
15
|
+
run: echo "линтер был красным"
|
|
@@ -5,7 +5,19 @@
|
|
|
5
5
|
# который в тёмной теме остаётся светлым пятном, — и это видно не автору, а пользователю,
|
|
6
6
|
# который переключил тему. Дефект тихий: в теме автора всё выглядит правильно.
|
|
7
7
|
DIR="${1:-.}"
|
|
8
|
-
|
|
8
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
9
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
10
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
11
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
12
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
13
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
14
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
15
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
16
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
17
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
18
|
+
exit 2
|
|
19
|
+
fi
|
|
20
|
+
. "$SKIP_LIB"
|
|
9
21
|
|
|
10
22
|
# Расширения, где живёт ТЕМИЗИРУЕМЫЙ интерфейс. Своё, а не общий CODE_EXT: там нет ни css, ни
|
|
11
23
|
# vue — они не код в смысле «отладочная печать», но именно в них живёт цвет.
|
|
@@ -12,8 +12,18 @@
|
|
|
12
12
|
**Почему машина, а не внимательность.** Написать отчёт «на будущее» — первое, что пропускают под
|
|
13
13
|
давлением дедлайна. Гейт делает это ценой, а не пожеланием.
|
|
14
14
|
|
|
15
|
-
**Готовый
|
|
16
|
-
|
|
15
|
+
**Готовый аналог есть, и раньше здесь было написано «не искал».** Искали 2026-09-07, при ревизии
|
|
16
|
+
каталога. Историю коммитов держат [`gitlint`](https://jorisroovers.com/gitlint/) и
|
|
17
|
+
[`commitlint`](https://commitlint.js.org/): проверяют форму заголовка, тип по Conventional
|
|
18
|
+
Commits, длину строк, пустую строку между заголовком и телом; у `gitlint` есть даже
|
|
19
|
+
`body-min-length` и возможность дописать своё правило на Python.
|
|
20
|
+
|
|
21
|
+
**Почему рецепта под них здесь нет.** Оба проверяют, что тело **есть** и как оно оформлено. Эта
|
|
22
|
+
запись требует другого: чтобы в теле стояли два названных раздела — что сделано и **в чём агент
|
|
23
|
+
не уверен**. Второго нет ни в Conventional Commits, ни во встроенных правилах обоих. Написать
|
|
24
|
+
своё правило `gitlint` можно, но это код на Python в проекте, который может быть не на Python, —
|
|
25
|
+
и он всё равно наш, только в чужой обёртке. Если `commitlint` у вас уже стоит — он закрывает
|
|
26
|
+
форму заголовка, чего не делаем мы; записи это не отменяет.
|
|
17
27
|
|
|
18
28
|
**Чего НЕ ловит.** Не проверяет качество отчёта, только его наличие — «Сделано: починил» и «Не
|
|
19
29
|
уверен: не знаю» формально пройдут. Не проверяет, что отчёт правдив. Это ограничение того же
|
|
@@ -39,7 +49,7 @@
|
|
|
39
49
|
|
|
40
50
|
Гейт всё-таки местный: подсказка «допиши в тело коммита» выполнима до пуша, а не после.
|
|
41
51
|
|
|
42
|
-
**Образцы.** `red
|
|
52
|
+
**Образцы.** `red/.aqk-commit-msg` — обычное тело коммита без отчёта. `green/.aqk-commit-msg` — то же самое
|
|
43
53
|
плюс `Сделано:` и `Не уверен:`. Образцы — текстовые файлы, а не настоящий git: арбитр в реальном
|
|
44
54
|
проекте читает `git log -1`, а вложенный `.git` внутри каталога комплекта создал бы embedded-
|
|
45
55
|
репозиторий, который сам по себе стал бы проблемой версионирования.
|