agent-quality-kit 0.5.0 → 0.6.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.
Files changed (49) hide show
  1. package/README.md +36 -1
  2. package/README.ru.md +19 -1
  3. package/kit/docs/ready-made-rules.md +40 -0
  4. package/kit/gates/README.md +20 -0
  5. package/kit/gates/ci-actually-fails/README.md +42 -0
  6. package/kit/gates/ci-actually-fails/check.sh +93 -0
  7. package/kit/gates/ci-actually-fails/gate.yml +14 -0
  8. package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +14 -0
  9. package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
  10. package/kit/gates/gate-not-weakened/README.md +54 -0
  11. package/kit/gates/gate-not-weakened/check.sh +72 -0
  12. package/kit/gates/gate-not-weakened/gate.yml +15 -0
  13. package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
  14. package/kit/gates/gate-not-weakened/green/payments.py +6 -0
  15. package/kit/gates/gate-not-weakened/green/release.sh +2 -0
  16. package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
  17. package/kit/gates/gate-not-weakened/red/payments.py +6 -0
  18. package/kit/gates/gate-not-weakened/red/release.sh +2 -0
  19. package/kit/gates/promise-has-gate/README.md +50 -0
  20. package/kit/gates/promise-has-gate/check.sh +88 -0
  21. package/kit/gates/promise-has-gate/gate.yml +14 -0
  22. package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
  23. package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
  24. package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
  25. package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
  26. package/kit/gates/test-has-assertion/README.md +47 -0
  27. package/kit/gates/test-has-assertion/check.sh +194 -0
  28. package/kit/gates/test-has-assertion/gate.yml +15 -0
  29. package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
  30. package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
  31. package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
  32. package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
  33. package/kit/rules/general.md +14 -0
  34. package/llms.txt +1 -0
  35. package/package.json +3 -2
  36. package/tool/commands/doctor.mjs +44 -4
  37. package/tool/commands/gates.mjs +10 -2
  38. package/tool/commands/project.mjs +7 -1
  39. package/tool/i18n/en.mjs +17 -0
  40. package/tool/i18n/ru.mjs +18 -0
  41. package/tool/i18n/templates-en.mjs +9 -9
  42. package/tool/i18n/templates-ru.mjs +9 -9
  43. package/tool/lib/manifest.mjs +37 -1
  44. package/tool/lib/scope.mjs +96 -0
  45. package/tool/program.mjs +1 -0
  46. package/tool/selfcheck/gates.sh +20 -3
  47. package/tool/selfcheck/lifecycle.mjs +29 -0
  48. package/tool/selfcheck/smoke.sh +53 -0
  49. package/tool/selfcheck/units.mjs +85 -1
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.5.0
199
+ rev: v0.6.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
@@ -236,9 +236,26 @@ 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`.
@@ -289,6 +306,24 @@ catalogue may grow to hundreds of entries; a given project still sees about a do
289
306
  An entry is accepted only if its arbiter goes red on the red sample, stays quiet on the green
290
307
  one, and names a real failure it caught. A machine checks this: `bash tool/selfcheck/gates.sh`.
291
308
 
309
+ ### Four entries that watch the agent, not the code
310
+
311
+ Ruff, ESLint and gitleaks already find bad code, and AQK calls them where it can rather than
312
+ reinventing them. These four look elsewhere — at the moment the **signal** about bad code is
313
+ switched off, which is what a coding agent does when the task is phrased as "make it pass":
314
+
315
+ | Entry | What it catches |
316
+ |---|---|
317
+ | `gate-not-weakened` | the fix was a suppression, not a fix: bare `# noqa`, `eslint-disable` with no rule named, `@ts-ignore`, `--no-verify` |
318
+ | `ci-actually-fails` | a pipeline step that renders a verdict but cannot fail — `run: pytest \|\| true`, `continue-on-error: true` |
319
+ | `test-has-assertion` | a test that cannot fail: empty body, `assert True`, a skip with no reason given |
320
+ | `promise-has-gate` | a rule in `AGENTS.md` with no enforcer named — neither a gate nor, honestly, a human |
321
+
322
+ Each was measured on nineteen third-party repositories (~25 000 files) before it entered the
323
+ catalogue, and two further entries were **cancelled by that measurement**: one because
324
+ [`agents-lint`](https://github.com/giacomo/agents-lint) already does it better, one because
325
+ 91 of its 120 findings turned out to be a legitimate pattern.
326
+
292
327
  ## The guides as a single file
293
328
 
294
329
  ```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.5.0
201
+ rev: v0.6.0
202
202
  hooks:
203
203
  - id: aqk # запускает объявленное; роняет коммит ниже AQK-1
204
204
  # - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
@@ -286,6 +286,24 @@ 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
+
289
307
  ## Методички одним файлом
290
308
 
291
309
  ```bash
@@ -149,6 +149,46 @@ 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
+ | Инструмент | Что проверяет | Состояние на 2026-09-06 |
178
+ |---|---|---|
179
+ | [`agnix`](https://github.com/agent-sh/agnix) | 455 правил: структура `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, синтаксис конфигов MCP и хуков, соглашения об именах, **мёртвые ссылки на файлы**. Есть автопочинка и LSP | 404 ⭐, Rust, активен. `npm i -g agnix`, `brew`, `pip`, `cargo` |
180
+ | [`agents-lint`](https://github.com/giacomo/agents-lint) | мёртвые npm-скрипты, упомянутые в `AGENTS.md`, устаревшие рамки, деревья каталогов в контексте | 13 ⭐, TypeScript, последний коммит март 2026 |
181
+
182
+ ```bash
183
+ npm install -g agnix && agnix --strict .
184
+ ```
185
+
186
+ **Чего они НЕ делают — и почему у AQK остаётся своя половина.** Все они проверяют документ:
187
+ формат, существование путей, наличие скриптов. Ни один не спрашивает, **подкреплено ли обещание
188
+ командой с кодом возврата**. «Мы никогда не коммитим секреты» — грамматически безупречная
189
+ строка, на которую ни один из них ничего не скажет. Разделение простое: обвес агента проверяет
190
+ `agnix`, исполнимость обещаний — `promise-has-gate`.
191
+
152
192
  ## А если проект не на Python?
153
193
 
154
194
  Ничего не меняется. Запись каталога держит **одно намерение и несколько исполнителей**, и
@@ -70,6 +70,26 @@
70
70
  Плюс шестое, без которого запись не принимается: **доказательство** — реальный отказ, который
71
71
  она поймала. «Это хорошая практика» не принимается.
72
72
 
73
+ ## Зрелость записи не пишут руками
74
+
75
+ Седьмого поля нет: зрелость **считается** из доказательства. Ссылается `proof` на журнал шишек —
76
+ запись зрелая; не ссылается — условная, и это видно в приёмке каталога. Написать себе
77
+ `lifecycle: stable` нельзя, приёмка такую запись отклонит.
78
+
79
+ Это не придирка к форме. У всех трёх соседей, чей каталог мы разбирали, поле зрелости есть, и
80
+ у всех троих его заполняет автор: `lifecycle` у зондов Scorecard, `future`/`obsolete` у
81
+ критериев значка OpenSSF. Значение, написанное автором, означает доверие к автору. Каталог, где
82
+ зрелость объявляют, к сотне записей превращается в список, в котором нельзя выбрать.
83
+
84
+ Объявляется ровно одно состояние — **`deprecated`**, потому что «эту запись больше не ставят»
85
+ из её файлов не выводится никак. Вместе с ним обязателен `superseded_by` с именем существующей
86
+ записи, и `aqk add` тогда отказывает в установке, назвав преемника:
87
+
88
+ ```yaml
89
+ lifecycle: deprecated
90
+ superseded_by: no-print-in-prod
91
+ ```
92
+
73
93
  ## Запись без переносимого рецепта
74
94
 
75
95
  Иногда переносимой проверки быть не может: чтобы понять, вызывают ли функцию, нужен граф
@@ -0,0 +1,42 @@
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
+ **Чего НЕ ловит.**
33
+
34
+ - **Проверку, которую не по чему опознать.** Задача `mutation-diff` с именем «Mutation score on
35
+ changed files» под `continue-on-error` (нашлась в `kodus-ai`, автор сам пометил её «advisory —
36
+ does not block») не опознаётся: в имени нет слова-приметы, а команда не из списка. Объяви такую
37
+ проверку гейтом в `.aqk.yml` — тогда она станет видна точно, а не по догадке.
38
+ - **Провал, погашенный внутри скрипта.** `set +e`, `trap`, `exit 0` в конце `run: |` — это уже
39
+ логика скрипта, а не конфиг конвейера.
40
+ - **Шаг, который проходит по другой причине.** Тест, всегда возвращающий 0, — не эта запись,
41
+ а `test-has-assertion`.
42
+ - **`if: always()`** маскировкой не считается: он про порядок выполнения, а не про вердикт.
@@ -0,0 +1,93 @@
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
+ # Разбор ПО ШАГАМ, а не по строкам. Построчно проверка врала в обе стороны: законный
37
+ # `continue-on-error` на шаге выгрузки отчёта красил соседний шаг с тестами, а слово «test»
38
+ # внутри перечисления типов коммита («feat|fix|test|chore») делало проверкой строку, которая
39
+ # ничего не проверяет. Замер по чужим конвейерам дал 4 ложных из 10 — переписано на блоки.
40
+ #
41
+ # Шаг начинается элементом списка («- ») или ключом верхнего уровня: так устроен и github,
42
+ # и gitlab, где `allow_failure` живёт на уровне задачи.
43
+ RES=$(tr -d '\r' < "$F" | awk -v runners="$RUNNERS" -v words="$WORDS" -v keys="$KEYS" -v file="$F" '
44
+ function isComment(l) { return l ~ /^[[:space:]]*#/ }
45
+ function looksLikeCheck(l, j, nk) {
46
+ if (isComment(l)) return 0
47
+ # `uses:` — чужое действие. Его провал бывает законно необязательным: выгрузка отчёта,
48
+ # комментарий в пул-реквест, уведомление. Вердикт выносит то, что ЗАПУСКАЮТ.
49
+ if (l ~ /^[[:space:]]*(-[[:space:]]+)?uses[[:space:]]*:/) return 0
50
+ if (l ~ runners) return 1
51
+ # Проверка, объявленная в манифесте ЭТОГО проекта, — тоже проверка, как бы она ни
52
+ # называлась. Это самая точная примета из трёх: не догадка по имени, а список, который
53
+ # проект написал сам.
54
+ nk = split(keys, K, "\n")
55
+ for (j = 1; j <= nk; j++) if (K[j] != "" && index(l, K[j])) return 1
56
+ # Слово-примета — ТОЛЬКО в имени шага. В теле команды оно ловит своё же упоминание:
57
+ # «grep -oE (feat|fix|test|chore)» — это разбор заголовка коммита, а не проверка.
58
+ if (l ~ /^[[:space:]]*(-[[:space:]]+)?name[[:space:]]*:/ && l ~ words) return 1
59
+ return 0
60
+ }
61
+ function isMask(l) {
62
+ return !isComment(l) && l ~ /^[[:space:]]*(continue-on-error|allow_failure|ignore_failure)[[:space:]]*:[[:space:]]*(true|yes)/
63
+ }
64
+ function isBoundary(l) { return l ~ /^[[:space:]]*-[[:space:]]/ || l ~ /^[A-Za-z_.-]+[[:space:]]*:/ }
65
+ function flush( ) {
66
+ if (blockStart && blockCheck && blockMask)
67
+ printf "%s:%d: проверка не может провалиться — шаг под %s\n", file, blockCheckLine, blockMaskText
68
+ blockStart = 0; blockCheck = 0; blockMask = 0
69
+ }
70
+ {
71
+ # Гашение прямо в команде красится только для ЗАКРЫТОГО списка запускалок: «|| true» на
72
+ # вспомогательной команде внутри скрипта (`docker network create … || true`) — это
73
+ # идемпотентность, а не выключенная проверка.
74
+ if (!isComment($0) && $0 ~ runners && $0 ~ /\|\|[[:space:]]*(true|:|exit[[:space:]]+0)/) {
75
+ line = $0; sub(/^[[:space:]]+/, "", line)
76
+ printf "%s:%d: провал погашен прямо в команде: %s\n", file, NR, substr(line, 1, 90)
77
+ }
78
+ if (isBoundary($0)) flush()
79
+ if (!blockStart) blockStart = NR
80
+ if (!blockCheck && looksLikeCheck($0)) { blockCheck = 1; blockCheckLine = NR }
81
+ if (isMask($0)) { blockMask = 1; blockMaskText = $0; sub(/^[[:space:]]+/, "", blockMaskText) }
82
+ }
83
+ END { flush() }' 2>/dev/null)
84
+ [ -z "$RES" ] || BAD="$BAD$RES
85
+ "
86
+ done
87
+
88
+ LEFT="$(printf '%s' "$BAD" | grep -v '^$')"
89
+ [ -z "$LEFT" ] && exit 0
90
+ printf '%s\n' "$LEFT"
91
+ echo " почини: убери «|| true» и «continue-on-error» с шага, который выносит вердикт."
92
+ echo " шаг, который не может провалиться, — это не проверка, а строка в логе."
93
+ 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,14 @@
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
@@ -0,0 +1,12 @@
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 || true
10
+ - name: типы
11
+ run: tsc --noEmit
12
+ continue-on-error: true
@@ -0,0 +1,54 @@
1
+ # Подавление проверки — точечное и с причиной
2
+
3
+ **Намерение.** Когда проверка краснеет, у пишущего два выхода: починить код или заглушить
4
+ проверку. Второй дешевле и с виду неотличим от первого — конвейер зелёный, диф маленький.
5
+
6
+ Для агента это выход по умолчанию: задача сформулирована как «сделай, чтобы прошло», и
7
+ `@ts-ignore` решает её буквально. Дефект при этом остаётся, а сигнал о нём исчезает навсегда —
8
+ это тот же класс, что молчащий гейт, только оплаченный одной строкой.
9
+
10
+ **Какой отказ это поймало.** Замер по пятнадцати чужим репозиториям (20 774 файла): 22 находки,
11
+ 19 настоящих. Среди них — `// @ts-nocheck` первой строкой боевой страницы в `kodus-ai`
12
+ (проверка типов выключена для целого файла), `# noqa E501` без двоеточия в `pr-agent`
13
+ (flake8 такую форму читает как безадресный `# noqa`: погашено не одно правило, а все),
14
+ `/** @ts-ignore */` и `.strict().argv // eslint-disable-line` без имени правила в `repolinter`.
15
+ Запись в журнале: `incidents/README.md`, 2026-09-06.
16
+
17
+ **Что именно проверяется.** Красным делается только **безадресное** подавление:
18
+
19
+ | Красное | Зелёное |
20
+ |---|---|
21
+ | `# noqa` | `# noqa: F401 — причина` |
22
+ | `# type: ignore` | `# type: ignore[no-any-return]` |
23
+ | `# flake8: noqa`, `# ruff: noqa`, `# mypy: ignore-errors`, `# pylint: disable=all` | точечная форма с кодом |
24
+ | `/* eslint-disable */`, `// eslint-disable-next-line` без правила | `// eslint-disable-next-line no-console -- причина` |
25
+ | `// @ts-ignore`, `// @ts-nocheck` | `// @ts-expect-error -- причина` |
26
+ | `#[allow(warnings)]`, `@SuppressWarnings("all")`, `//nolint`, `rubocop:disable all`, `shellcheck disable` без `=SC…` | те же с именем правила |
27
+ | `--no-verify` в скрипте или конфиге конвейера | обычный коммит |
28
+
29
+ `@ts-ignore` красный всегда — единственное исключение из правила «безадресное». У него есть
30
+ строго лучший брат: `@ts-expect-error` краснеет сам, как только становится не нужен, то есть
31
+ не переживает починку кода. `@ts-ignore` переживает и молчит дальше.
32
+
33
+ **Готовый аналог.** Есть, и под каждый язык свой:
34
+ [`eslint-plugin-eslint-comments`](https://github.com/mysticatea/eslint-plugin-eslint-comments)
35
+ с правилом `require-description` — для JavaScript и TypeScript;
36
+ [`flake8-noqa`](https://github.com/plinss/flake8-noqa) — для Python, он же ловит сломанную форму
37
+ `# noqa E501`. Если проект на одном языке — ставь их, они точнее: разбирают код, а не текст.
38
+ Эта запись нужна там, где языков несколько или где ставить нечего: она переносима, ничего не
39
+ требует установить и покрывает то, чего нет ни у одного из двоих — `@ts-ignore` как класс,
40
+ `--no-verify` в конвейере, `@SuppressWarnings("all")`.
41
+
42
+ **Чего НЕ ловит.**
43
+
44
+ - **Подавление внутри строкового литерала считается настоящим.** Фикстуры инструментов, которые
45
+ сами обрабатывают подавления, дают ложные срабатывания: 3 из 22 на замере — тесты
46
+ `react-doctor` и `ratchets`. Отличить строку от комментария надёжно можно только разбором
47
+ языка, а его у переносимой проверки нет. Такие места — в `.aqkignore` или под храповик.
48
+ - **Точечное подавление без причины проходит.** `# noqa: E501` принимается, даже если рядом нет
49
+ ни слова о том, зачем. Требовать объяснение от каждой строки значит получить гейт, который
50
+ выключат целиком; за объяснением — к `require-description` из `eslint-plugin-eslint-comments`.
51
+ - **Не видит настройку в конфиге.** Правило, выключенное в `.eslintrc` или `pyproject.toml`, —
52
+ такое же ослабление, но это уже не подавление в коде, а решение проекта, записанное явно.
53
+ - **Не знает, было ли подавление оправдано.** Проверка отвечает на вопрос «названо ли, что
54
+ именно погашено», а не «стоило ли гасить».
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env sh
2
+ # Подавление проверки без адреса: «выключено всё» вместо «выключено вот это».
3
+ #
4
+ # ЗАЧЕМ ИМЕННО ЭТО. Когда проверка краснеет, у пишущего два выхода: починить код или заглушить
5
+ # проверку. Второй дешевле и с виду неотличим от первого — конвейер зелёный, диф маленький.
6
+ # Для агента это выход по умолчанию: ему поставлена задача «сделай, чтобы прошло».
7
+ #
8
+ # ГРАНИЦА НАМЕРЕННО УЗКАЯ. Красным делается не всякое подавление, а безадресное: `# noqa` без
9
+ # кода, `eslint-disable` без имени правила, `@SuppressWarnings("all")`. Точечное подавление с
10
+ # названным правилом — законный инструмент, и требовать объяснения от каждого значит получить
11
+ # гейт, который выключат. Одно исключение — `@ts-ignore`: у него есть строго лучший брат
12
+ # `@ts-expect-error`, который сам краснеет, когда становится не нужен.
13
+ DIR="${1:-.}"
14
+ . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
15
+
16
+ OUT=""
17
+ add() { [ -z "$1" ] || OUT="$OUT$1
18
+ "; }
19
+
20
+ # --- безадресные подавления в коде -------------------------------------------
21
+ # `# noqa` без двоеточия с кодом; `type: ignore` без [кода]; файловые выключатели целиком.
22
+ add "$(grep -rnE '(^|[^A-Za-z0-9_])#[[:space:]]*(noqa|type:[[:space:]]*ignore)([[:space:]]*$|[[:space:]]+[^:[])' \
23
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
24
+ add "$(grep -rnE '#[[:space:]]*(flake8:[[:space:]]*noqa[[:space:]]*$|ruff:[[:space:]]*noqa[[:space:]]*$|mypy:[[:space:]]*ignore-errors|pylint:[[:space:]]*(skip-file|disable=all))' \
25
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
26
+
27
+ # eslint-disable без имени правила: голая директива гасит ВСЁ в файле или на строке.
28
+ #
29
+ # ВЕДУЩИЙ КОММЕНТАРИЙ ОБЯЗАТЕЛЕН — «(//|/\*|\*)[^\"']*». Замер по пятнадцати чужим
30
+ # репозиториям показал целый класс ложных: строка ПРО подавление внутри кавычек
31
+ # («title: 'No @ts-ignore'», текст правила в наборе тестов) читалась как подавление.
32
+ # Директива живёт в комментарии; упоминание в строковом литерале — не она.
33
+ add "$(grep -rnE '(//|/\*|\*)[^\"'"'"']*eslint-disable(-next-line|-line)?[[:space:]]*(\*/|$|--)' \
34
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
35
+
36
+ # @ts-ignore и @ts-nocheck. Первый гасит ошибку молча и остаётся, когда ошибки уже нет;
37
+ # @ts-expect-error на его месте краснеет, как только становится лишним.
38
+ add "$(grep -rnE '(//|/\*|\*)[^\"'"'"']*@ts-(ignore|nocheck)' \
39
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
40
+
41
+ # Остальные экосистемы — только безадресная форма.
42
+ add "$(grep -rnE '#!?\[allow\((warnings|unused)\)\]|@SuppressWarnings\((\{[^}]*)?"all"|//[[:space:]]*nolint[[:space:]]*($|//)|rubocop:disable[[:space:]]+all|shellcheck[[:space:]]+disable[[:space:]]*($|[^=])' \
43
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
44
+
45
+ # --- обход самого конвейера ---------------------------------------------------
46
+ # `--no-verify` пропускает хуки коммита. В committed-скрипте или конфиге конвейера это не
47
+ # настройка, а выключенная защита: смотрим шире кода — в оболочечные скрипты и конфиги.
48
+ # `-e`, а НЕ `--`. Двойное тире завершает разбор опций, и все флаги `--include` после него
49
+ # grep считает именами файлов: проверка читала весь репозиторий вместо оболочечных скриптов и
50
+ # конфигов. Нашлось замером — в выдаче оказались CHANGELOG чужих проектов.
51
+ add "$(grep -rnE -e '--no-verify' $(skip_grep) \
52
+ --include=*.sh --include=*.yml --include=*.yaml --include=*.json --include=*.toml \
53
+ --include=*.mk --include=Makefile "$DIR" 2>/dev/null)"
54
+
55
+ # Собственное определение записи — не подавление, а перечень того, что ищется. Без этого
56
+ # проверка находит сама себя в любом проекте, куда её поставили: `check.sh` содержит все
57
+ # образцы разом. Тот же класс, что образцы red/green, только файл другой.
58
+ SELFDIR="$(basename "$(dirname "$0")")"
59
+ LEFT="$(printf '%s' "$OUT" | grep -v '^$' | own_samples_filter "$DIR" \
60
+ | grep -vE "(^|/)$SELFDIR/(check\.sh|README\.md)")"
61
+ # Сгенерированные файлы правят не руками: подавление в них поставил инструмент.
62
+ LEFT="$(printf '%s\n' "$LEFT" | while IFS= read -r L; do
63
+ F="${L%%:*}"
64
+ [ -n "$F" ] || continue
65
+ is_generated "$F" || printf '%s\n' "$L"
66
+ done | grep -v '^$')"
67
+
68
+ [ -z "$LEFT" ] && exit 0
69
+ printf '%s\n' "$LEFT"
70
+ echo " почини: назови, ЧТО подавляешь и зачем — «# noqa: E501», «eslint-disable-next-line no-console -- причина»,"
71
+ echo " «@ts-expect-error» вместо «@ts-ignore». Безадресное подавление гасит и то, что сломается завтра."
72
+ exit 1
@@ -0,0 +1,15 @@
1
+ intent: проверка выключается точечно и с причиной, а не целиком
2
+ intent_en: a check is suppressed narrowly and with a reason, never wholesale
3
+
4
+ # Применимо везде, где есть код: подавление проверки — приём, а не язык. Список маркеров
5
+ # покрывает python, typescript, javascript, go, rust, java, ruby и оболочку сразу.
6
+ trigger:
7
+ always: true
8
+
9
+ recipes:
10
+ any: bash {gate}/check.sh {dir}
11
+
12
+ proof: incidents/README.md, 2026-09-06 «замер по пятнадцати чужим репозиториям» — 22 находки
13
+ на 20 774 файлах, из них 19 настоящих: `@ts-nocheck` на боевой странице в kodus-ai,
14
+ сломанный `# noqa E501` без двоеточия в pr-agent (flake8 читает его как безадресный),
15
+ `/** @ts-ignore */` и `eslint-disable-line` без имени правила в repolinter
@@ -0,0 +1,8 @@
1
+ // @ts-expect-error -- у gateway нет типов; строка покраснеет сама, когда они появятся
2
+ import { pay } from "./gateway";
3
+
4
+ export async function checkout(cart: unknown) {
5
+ // eslint-disable-next-line no-console -- это программа командной строки, вывод и есть интерфейс
6
+ console.log(cart);
7
+ return pay(cart as never);
8
+ }
@@ -0,0 +1,6 @@
1
+ import json # noqa: F401 — реэкспорт для обратной совместимости, убрать в 2.0
2
+ from decimal import Decimal
3
+
4
+
5
+ def total(order): # type: ignore[no-any-return] — форма заказа приходит из внешнего API
6
+ return sum(Decimal(i["price"]) for i in order["items"])
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ git commit -m "release"
@@ -0,0 +1,9 @@
1
+ /* eslint-disable */
2
+ // @ts-ignore
3
+ import { pay } from "./gateway";
4
+
5
+ export async function checkout(cart: unknown) {
6
+ // eslint-disable-next-line
7
+ console.log(cart);
8
+ return pay(cart as never);
9
+ }
@@ -0,0 +1,6 @@
1
+ import json # noqa
2
+ from decimal import Decimal
3
+
4
+
5
+ def total(order): # type: ignore
6
+ return sum(Decimal(i["price"]) for i in order["items"])
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ git commit -m "release" --no-verify
@@ -0,0 +1,50 @@
1
+ # У правила виден сторож
2
+
3
+ **Намерение.** Это центральный вопрос всего комплекта. `AGENTS.md` — обычный текст: «никогда не
4
+ коммитим секреты» и «мы очень стараемся» выглядят одинаково и стоят одинаково, пока никто не
5
+ спросил, чем первое отличается от второго.
6
+
7
+ **Какой отказ это поймало.** Свой собственный. Прогон по нашему же `AGENTS.md`: тринадцать
8
+ железных правил, и **ни одно** не говорило, кто за ним следит. После разметки выяснилось, что
9
+ одиннадцать из тринадцати исполняет человек, а не машина. Это не стало хуже — стало видно.
10
+ Правило, за которым следит человек, законно; правило, о котором никто не знает, кто за ним
11
+ следит, через месяц отличается от лозунга только длиной. Запись в журнале:
12
+ `incidents/README.md`, 2026-09-06.
13
+
14
+ **Что именно проверяется.** Каждый пункт списка в точке входа, который выглядит правилом
15
+ (выделен жирным либо содержит слово долженствования или запрета), обязан нести пометку:
16
+
17
+ ```markdown
18
+ - **Секреты не в коде.** <!-- aqk: secrets-not-in-code -->
19
+ - **План до кода.** <!-- aqk: человек -->
20
+ ```
21
+
22
+ Имя гейта сверяется с блоком `gates:` манифеста: пометка, ведущая в никуда, — тоже красное.
23
+ Пометка живёт в комментарии разметки, поэтому в готовом документе её не видно.
24
+
25
+ **Почему пометка, а не угадывание.** Сопоставлять обещание с гейтом по совпадению слов — значит
26
+ выдавать вердикт по догадке; догадка, выданная за факт, и есть то, против чего построен весь
27
+ стандарт. Пометка ставится один раз и делает документ честнее: у каждого правила видно, кто его
28
+ сторожит.
29
+
30
+ **Первый прогон в зрелом проекте покрасит всё.** Так и задумано, и лечится одной командой:
31
+ `aqk ratchet promise-has-gate` — существующие правила становятся долгом, который может только
32
+ сокращаться, а новое правило без сторожа краснеет сразу.
33
+
34
+ **Готовый аналог.** Не нашли, и искали внимательно. К осени 2026 появился целый класс линтеров
35
+ агентского обвеса — [`agnix`](https://github.com/agent-sh/agnix) (455 правил, активен),
36
+ [`agents-lint`](https://github.com/giacomo/agents-lint), ctxlint, AgentLint. Все они проверяют
37
+ **документ**: формат, живые ли ссылки, существуют ли упомянутые скрипты. Ни один не спрашивает,
38
+ **исполнимо** ли обещание. Ставь `agnix` рядом — он закрывает то, чего не делаем мы, а мы
39
+ закрываем то, чего не делает он.
40
+
41
+ **Чего НЕ ловит.**
42
+
43
+ - **Не проверяет, что гейт делает то, что обещано.** Пометка `<!-- aqk: secrets-not-in-code -->`
44
+ на правиле про длину функций пройдёт. Связь объявляет человек; машина сторожит только то, что
45
+ связь есть и ведёт в существующий гейт.
46
+ - **`<!-- aqk: человек -->` не проверяется ничем** — это признание, а не проверка. Его ценность
47
+ в том, что признание сделано вслух и его видно в дифе, когда правил становится больше.
48
+ - **Правило, не оформленное пунктом списка**, не опознаётся: абзац прозы обещанием не считается.
49
+ - **Только точка входа**, объявленная в `entry:`. Правила, разложенные по десяти файлам
50
+ документации, эта запись не обойдёт.