agent-quality-kit 0.6.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.
Files changed (111) hide show
  1. package/README.md +18 -2
  2. package/README.ru.md +16 -0
  3. package/kit/docs/ai/agent-harness-playbook.md +1 -1
  4. package/kit/docs/ready-made-rules.md +29 -4
  5. package/kit/gates/README.md +40 -0
  6. package/kit/gates/ci-actually-fails/README.md +12 -0
  7. package/kit/gates/ci-actually-fails/check.sh +26 -3
  8. package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +16 -0
  9. package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
  10. package/kit/gates/color-from-token/check.sh +13 -1
  11. package/kit/gates/commit-explains-itself/README.md +13 -3
  12. package/kit/gates/commit-explains-itself/check.sh +8 -4
  13. package/kit/gates/complexity-limit/README.md +5 -0
  14. package/kit/gates/complexity-limit/check.sh +21 -2
  15. package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
  16. package/kit/gates/deps-are-pinned/README.md +14 -1
  17. package/kit/gates/deps-are-pinned/check.sh +6 -1
  18. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
  19. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
  20. package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
  21. package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
  22. package/kit/gates/duplicate-code/README.md +11 -2
  23. package/kit/gates/duplicate-code/check.sh +31 -4
  24. package/kit/gates/duplicate-code/gate.yml +8 -0
  25. package/kit/gates/duplicate-code/green/imports_a.go +20 -0
  26. package/kit/gates/duplicate-code/green/imports_b.go +19 -0
  27. package/kit/gates/entry-links-exist/README.md +5 -0
  28. package/kit/gates/entry-links-exist/check.sh +6 -0
  29. package/kit/gates/entry-links-exist/green/AGENTS.md +3 -0
  30. package/kit/gates/file-size-limit/README.md +9 -2
  31. package/kit/gates/file-size-limit/check.sh +13 -1
  32. package/kit/gates/gate-not-weakened/check.sh +13 -1
  33. package/kit/gates/hook-actually-fires/README.md +74 -0
  34. package/kit/gates/hook-actually-fires/check.sh +183 -0
  35. package/kit/gates/hook-actually-fires/gate.yml +15 -0
  36. package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
  37. package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
  38. package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
  39. package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
  40. package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
  41. package/kit/gates/no-phantom-package/README.md +84 -0
  42. package/kit/gates/no-phantom-package/check.sh +161 -0
  43. package/kit/gates/no-phantom-package/gate.yml +20 -0
  44. package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
  45. package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
  46. package/kit/gates/no-print-in-prod/README.md +33 -39
  47. package/kit/gates/no-print-in-prod/gate.yml +14 -6
  48. package/kit/gates/personal-config-not-shared/README.md +66 -0
  49. package/kit/gates/personal-config-not-shared/check.sh +103 -0
  50. package/kit/gates/personal-config-not-shared/gate.yml +16 -0
  51. package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
  52. package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
  53. package/kit/gates/secrets-not-in-code/check.sh +13 -1
  54. package/kit/gates/swallowed-error/README.md +36 -18
  55. package/kit/gates/swallowed-error/gate.yml +13 -3
  56. package/kit/gates/test-has-assertion/check.sh +13 -1
  57. package/kit/gates/test-not-adjusted/README.md +79 -0
  58. package/kit/gates/test-not-adjusted/check.sh +136 -0
  59. package/kit/gates/test-not-adjusted/gate.yml +19 -0
  60. package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
  61. package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
  62. package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
  63. package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
  64. package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
  65. package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
  66. package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
  67. package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
  68. package/kit/gates/todo-without-task/README.md +6 -0
  69. package/kit/gates/todo-without-task/check.sh +13 -1
  70. package/kit/ratchet/ratchet.sh +70 -2
  71. package/kit/rules/general.md +9 -0
  72. package/kit/rules-en/general.md +82 -0
  73. package/kit/rules-en/security.md +33 -0
  74. package/kit/rules-en/testing.md +48 -0
  75. package/llms.txt +1 -1
  76. package/package.json +3 -2
  77. package/tool/commands/badge.mjs +7 -1
  78. package/tool/commands/doctor.mjs +49 -9
  79. package/tool/commands/gates.mjs +10 -4
  80. package/tool/commands/project.mjs +8 -1
  81. package/tool/commands/prove.mjs +67 -0
  82. package/tool/commands/report.mjs +4 -1
  83. package/tool/i18n/en-docs.mjs +70 -0
  84. package/tool/i18n/en.mjs +49 -54
  85. package/tool/i18n/ru-docs.mjs +70 -0
  86. package/tool/i18n/ru.mjs +48 -54
  87. package/tool/i18n/templates-en.mjs +1 -1
  88. package/tool/i18n/templates-ru.mjs +1 -1
  89. package/tool/lib/core.mjs +7 -1
  90. package/tool/lib/manifest.mjs +36 -5
  91. package/tool/lib/prove.mjs +160 -0
  92. package/tool/lib/repo.mjs +31 -2
  93. package/tool/lib/scope.mjs +37 -2
  94. package/tool/lib/templates.mjs +2 -0
  95. package/tool/program.mjs +5 -0
  96. package/tool/selfcheck/gates.sh +66 -0
  97. package/tool/selfcheck/mutation.sh +21 -1
  98. package/tool/selfcheck/smoke.sh +279 -39
  99. package/tool/selfcheck/units-level.mjs +60 -0
  100. package/tool/selfcheck/units.mjs +113 -2
  101. package/kit/gates/no-print-in-prod/check.sh +0 -38
  102. package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
  103. package/kit/gates/no-print-in-prod/green/main.go +0 -8
  104. package/kit/gates/no-print-in-prod/green/main.rs +0 -4
  105. package/kit/gates/no-print-in-prod/red/main.go +0 -8
  106. package/kit/gates/no-print-in-prod/red/main.rs +0 -4
  107. package/kit/gates/swallowed-error/check.sh +0 -54
  108. package/kit/gates/swallowed-error/green/run.js +0 -8
  109. package/kit/gates/swallowed-error/red/run.js +0 -3
  110. /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
  111. /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.6.0
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
  [![on the GitHub Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-Agent%20Quality%20Kit-2ea44f?logo=github)](https://github.com/marketplace/actions/agent-quality-kit-aqk)
218
218
 
219
219
  ```yaml
220
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.5.0
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
  ```
@@ -260,6 +260,22 @@ Every `doctor --run` rewrites `.aqk/last-run.md` — a short report of what actu
260
260
  long it took. The list of gates in the manifest says nothing about how many of them are alive
261
261
  right now; the report does. The file is ephemeral — keep it in your own `.gitignore`.
262
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
+
263
279
  ## When a bug slips past the guards
264
280
 
265
281
  ```bash
package/README.ru.md CHANGED
@@ -304,6 +304,22 @@ Ruff, ESLint и gitleaks и так находят плохой код — AQK з
304
304
  [`agents-lint`](https://github.com/giacomo/agents-lint) делает это лучше, другую — потому что
305
305
  91 находка из 120 оказалась законным приёмом.
306
306
 
307
+ ### Как ввести правило в живой проект
308
+
309
+ Три способа, и у каждого своя цена. Большая чистка откладывается навсегда, потому что она
310
+ большая. Храповик превращает старые нарушения в долг и блокирует новые — верно, когда правило
311
+ уже принято. А пока о правиле спорят, совещательный гейт показывает находки, не роняя прогон:
312
+
313
+ ```yaml
314
+ advisory:
315
+ - complexity-limit
316
+ ```
317
+
318
+ Объявлением в манифесте, а не флагом. Флаг «не роняй ничего» понижает все проверки разом, не
319
+ виден в дифе и не назван в итоге — это тот самый `continue-on-error`, который мы сами красим.
320
+ Список печатается **каждый** прогон: совещательный гейт, о котором забыли, — это выключенная
321
+ проверка.
322
+
307
323
  ## Методички одним файлом
308
324
 
309
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 через
@@ -174,10 +174,12 @@ ruff check --select TRY400 --statistics . # сколько находок У
174
174
  К осени 2026 появился отдельный класс инструментов — линтеры не кода, а того, что читает агент:
175
175
  `AGENTS.md`, `CLAUDE.md`, файлы навыков, конфиги хуков и MCP. Писать своё здесь незачем.
176
176
 
177
- | Инструмент | Что проверяет | Состояние на 2026-09-06 |
177
+ | Инструмент | Что проверяет | Состояние дата замера в самой ячейке |
178
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 |
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. Без сети выходит с нулём: «не проверено» у него неотличимо от «чисто» |
181
183
 
182
184
  ```bash
183
185
  npm install -g agnix && agnix --strict .
@@ -187,7 +189,13 @@ npm install -g agnix && agnix --strict .
187
189
  формат, существование путей, наличие скриптов. Ни один не спрашивает, **подкреплено ли обещание
188
190
  командой с кодом возврата**. «Мы никогда не коммитим секреты» — грамматически безупречная
189
191
  строка, на которую ни один из них ничего не скажет. Разделение простое: обвес агента проверяет
190
- `agnix`, исполнимость обещаний — `promise-has-gate`.
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`.
191
199
 
192
200
  ## А если проект не на Python?
193
201
 
@@ -201,6 +209,23 @@ npm install -g agnix && agnix --strict .
201
209
  **Одна строка в существующей записи — самый дешёвый и самый ценный вклад.** Намерение уже
202
210
  доказано отказом, образцы уже лежат, проверять нечего кроме самой команды.
203
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
+
204
229
  ## Если готового нет
205
230
 
206
231
  Тогда свой гейт — и в его `README.md` пишется, **что именно проверено**: какой инструмент
@@ -70,6 +70,26 @@
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
+
73
93
  ## Зрелость записи не пишут руками
74
94
 
75
95
  Седьмого поля нет: зрелость **считается** из доказательства. Ссылается `proof` на журнал шишек —
@@ -90,6 +110,24 @@ lifecycle: deprecated
90
110
  superseded_by: no-print-in-prod
91
111
  ```
92
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
+
93
131
  ## Запись без переносимого рецепта
94
132
 
95
133
  Иногда переносимой проверки быть не может: чтобы понять, вызывают ли функцию, нужен граф
@@ -193,6 +231,8 @@ samples_for: python
193
231
  | `has_deps: true` | есть файл зависимостей |
194
232
  | `has_tests: true` | есть каталог тестов или файлы вида `*_test.*` |
195
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` — признак сработает у человека и не сработает в конвейере |
196
236
  | `has_ui: true` | есть стили или однофайловые компоненты (`.css`, `.scss`, `.vue`, `.svelte`, `.astro`) |
197
237
 
198
238
  Любое из `has_*` принимает и `false` — «показывать тем, у кого этого нет». Условие, которого
@@ -29,6 +29,18 @@
29
29
  для них — законная настройка, каковой она и является: незаконной её делает то, ЧТО под ней
30
30
  стоит, а это знает только проект.
31
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
+
32
44
  **Чего НЕ ловит.**
33
45
 
34
46
  - **Проверку, которую не по чему опознать.** Задача `mutation-diff` с именем «Mutation score on
@@ -33,6 +33,19 @@ fi
33
33
 
34
34
  BAD=""
35
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
36
49
  # Разбор ПО ШАГАМ, а не по строкам. Построчно проверка врала в обе стороны: законный
37
50
  # `continue-on-error` на шаге выгрузки отчёта красил соседний шаг с тестами, а слово «test»
38
51
  # внутри перечисления типов коммита («feat|fix|test|chore») делало проверкой строку, которая
@@ -40,7 +53,7 @@ for F in $CI; do
40
53
  #
41
54
  # Шаг начинается элементом списка («- ») или ключом верхнего уровня: так устроен и github,
42
55
  # и gitlab, где `allow_failure` живёт на уровне задачи.
43
- RES=$(tr -d '\r' < "$F" | awk -v runners="$RUNNERS" -v words="$WORDS" -v keys="$KEYS" -v file="$F" '
56
+ RES=$(tr -d '\r' < "$F" | awk -v runners="$RUNNERS" -v words="$WORDS" -v keys="$KEYS" -v file="$F" -v redeemed="$REDEEMED" '
44
57
  function isComment(l) { return l ~ /^[[:space:]]*#/ }
45
58
  function looksLikeCheck(l, j, nk) {
46
59
  if (isComment(l)) return 0
@@ -62,10 +75,17 @@ for F in $CI; do
62
75
  return !isComment(l) && l ~ /^[[:space:]]*(continue-on-error|allow_failure|ignore_failure)[[:space:]]*:[[:space:]]*(true|yes)/
63
76
  }
64
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
+ }
65
85
  function flush( ) {
66
- if (blockStart && blockCheck && blockMask)
86
+ if (blockStart && blockCheck && blockMask && !isRedeemed(blockId))
67
87
  printf "%s:%d: проверка не может провалиться — шаг под %s\n", file, blockCheckLine, blockMaskText
68
- blockStart = 0; blockCheck = 0; blockMask = 0
88
+ blockStart = 0; blockCheck = 0; blockMask = 0; blockId = ""
69
89
  }
70
90
  {
71
91
  # Гашение прямо в команде красится только для ЗАКРЫТОГО списка запускалок: «|| true» на
@@ -79,6 +99,9 @@ for F in $CI; do
79
99
  if (!blockStart) blockStart = NR
80
100
  if (!blockCheck && looksLikeCheck($0)) { blockCheck = 1; blockCheckLine = NR }
81
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
+ }
82
105
  }
83
106
  END { flush() }' 2>/dev/null)
84
107
  [ -z "$RES" ] || BAD="$BAD$RES
@@ -12,3 +12,19 @@ jobs:
12
12
  - name: выгрузить отчёт
13
13
  run: bash scripts/upload-report.sh
14
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
- . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
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/COMMIT_MSG` — обычное тело коммита без отчёта. `green/COMMIT_MSG` — то же самое
52
+ **Образцы.** `red/.aqk-commit-msg` — обычное тело коммита без отчёта. `green/.aqk-commit-msg` — то же самое
43
53
  плюс `Сделано:` и `Не уверен:`. Образцы — текстовые файлы, а не настоящий git: арбитр в реальном
44
54
  проекте читает `git log -1`, а вложенный `.git` внутри каталога комплекта создал бы embedded-
45
55
  репозиторий, который сам по себе стал бы проблемой версионирования.
@@ -4,11 +4,15 @@
4
4
  # оставляет след для человека, читающего историю позже.
5
5
  DIR="${1:-.}"
6
6
 
7
- # Образцы (gates.sh) читают тело коммита из файла — реальный git log там взять неоткуда, а
7
+ # Образцы (gates.sh) читают тело коммита из `.aqk-commit-msg` — реальный git log там взять неоткуда, а
8
8
  # вложенный .git внутри каталога комплекта сам по себе создал бы embedded-репозиторий.
9
- # В настоящем проекте COMMIT_MSG не бывает — читается последний реальный коммит.
10
- if [ -f "$DIR/COMMIT_MSG" ]; then
11
- MSG=$(cat "$DIR/COMMIT_MSG")
9
+ # В настоящем проекте такого файла не бывает — читается последний реальный коммит.
10
+ # Имя с точкой намеренно: `COMMIT_MSG` в корне проекта — вещь возможная (так зовут заготовку
11
+ # сообщения многие обёртки над git), и такой файл молча подменял бы собой настоящий коммит.
12
+ # Тот же дефект нашло ревью у `personal-config-not-shared` 2026-09-07; здесь он был с самого
13
+ # начала и не всплывал.
14
+ if [ -f "$DIR/.aqk-commit-msg" ]; then
15
+ MSG=$(cat "$DIR/.aqk-commit-msg")
12
16
  else
13
17
  if ! (cd "$DIR" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1); then
14
18
  echo "не git-репозиторий — проверять нечего"
@@ -33,5 +33,10 @@
33
33
  считает только глубину. Не различает функции внутри файла: меряется худшее место файла, а не
34
34
  каждая функция отдельно. Готовые правила умеют и то, и другое.
35
35
 
36
+ **Тесты не проверяются.** Глубокая вложенность в тесте — это обход таблицы ожиданий, а не
37
+ сложная логика. Замер по `fastapi` дал 303 находки, из которых **101 в `tests/`**; после
38
+ исключения тестов осталось 10. Гейт, который краснеет в основном на тестах, выключают целиком —
39
+ тот же довод и по той же причине, что в `duplicate-code`.
40
+
36
41
  **Образцы.** `red/` — восемь уровней вложенности. `green/` — тот же смысл, разбитый на две
37
42
  функции с ранним выходом.
@@ -6,11 +6,30 @@
6
6
  # ЗАЧЕМ ВООБЩЕ. 29 ветвлений в одной функции — это код, который никто не держит в голове
7
7
  # целиком: ни человек, ни агент. Агент в таком месте начинает переписывать вместо правки.
8
8
  DIR="${1:-.}"
9
- . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
9
+ # Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
10
+ # `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
11
+ # неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
12
+ # `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
13
+ # ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
14
+ # выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
15
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
16
+ if [ ! -f "$SKIP_LIB" ]; then
17
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
18
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
19
+ exit 2
20
+ fi
21
+ . "$SKIP_LIB"
10
22
  MAX="${AQK_MAX_DEPTH:-5}"
11
23
 
24
+ # Тесты исключены намеренно — тем же списком, что и в duplicate-code. Глубокая вложенность в
25
+ # тесте это обход таблицы ожиданий, а не сложная логика: замер по fastapi дал 303 находки, из
26
+ # которых 101 в tests/. Гейт, который краснеет в основном на тестах, выключают целиком.
27
+ TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __tests__ -prune -o"
28
+
12
29
  # shellcheck disable=SC2046
13
- find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
30
+ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
31
+ ! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
32
+ -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
14
33
  | while IFS= read -r F; do is_generated "$F" || printf '%s\n' "$F"; done \
15
34
  | xargs -r awk -v MAX="$MAX" '
16
35
  # Один обход на все файлы: процесс на каждый файл дал 19 секунд на 4000 файлов.
@@ -0,0 +1,14 @@
1
+ # Тест с глубокой вложенностью — не сложная логика, а разбор дерева ожиданий. Найдено замером
2
+ # по fastapi: 101 находка из 303 пришлась на tests/, и все они — таблицы ожидаемых ответов,
3
+ # которые никто не станет «упрощать». Гейт, который краснеет в основном на тестах, выключат.
4
+ def test_openapi_schema(client):
5
+ response = client.get("/openapi.json")
6
+ assert response.status_code == 200
7
+ schema = response.json()
8
+ for path, methods in schema["paths"].items():
9
+ for method, spec in methods.items():
10
+ for code, resp in spec["responses"].items():
11
+ if "content" in resp:
12
+ for media, body in resp["content"].items():
13
+ if "schema" in body:
14
+ assert body["schema"] is not None
@@ -26,4 +26,17 @@
26
26
  **Готового аналога нет** — ни в линтерах, ни в пакетных менеджерах: они
27
27
  умеют создать файл версий, но не умеют требовать, чтобы он существовал и лежал в репозитории.
28
28
 
29
- **Образцы.** `red/` объявление зависимостей без закрепления. `green/` с ним.
29
+ **Закрепляют не только poetry и uv.** Для `pyproject.toml` файлом закрепления считается и
30
+ `requirements.txt` — но только если он сам проходит проверку, то есть все версии в нём стоят
31
+ через `==`. Иначе «есть requirements.txt» стало бы способом обойти гейт пустым файлом.
32
+ Найдено замером по `httpx`: там инструменты закреплены до патча прямо в `requirements.txt`,
33
+ а гейт требовал ещё и `poetry.lock`, которого в этом укладе не бывает вовсе.
34
+
35
+ **Не различает библиотеку и приложение.** Библиотека намеренно оставляет
36
+ границы своих зависимостей широкими — закрепив их у себя, она ломает сборку всем, кто её
37
+ ставит. Гейт смотрит только на то, закреплено ли окружение самого проекта, и правильно молчит,
38
+ когда библиотека закрепила инструменты и не закрепила зависимости.
39
+
40
+ **Образцы.** `red/` — объявление зависимостей без закрепления, плюс `pyproject.toml` рядом с
41
+ незакреплённым `requirements.txt`. `green/` — с закреплением, включая уклад «pyproject плюс
42
+ requirements.txt с точными версиями».
@@ -30,7 +30,12 @@ need() {
30
30
  }
31
31
 
32
32
  need package.json package-lock.json yarn.lock pnpm-lock.yaml npm-shrinkwrap.json
33
- need pyproject.toml poetry.lock uv.lock pdm.lock
33
+ # requirements.txt считается закреплением для pyproject.toml наравне с файлами блокировки:
34
+ # закрепляют не только poetry и uv. Замер по httpx: инструменты там закреплены до патча
35
+ # прямо в requirements.txt, а гейт требовал ещё и poetry.lock, которого в этом укладе не
36
+ # бывает вовсе. Файл засчитывается только если он сам проходит проверку ниже — иначе
37
+ # «есть requirements.txt» стало бы способом обойти гейт пустым файлом.
38
+ need pyproject.toml poetry.lock uv.lock pdm.lock requirements.txt
34
39
  need go.mod go.sum
35
40
  need Cargo.toml Cargo.lock
36
41
  need Gemfile Gemfile.lock
@@ -0,0 +1,12 @@
1
+ # Питоновская библиотека, которая закрепляет версии не файлом блокировки, а точными версиями
2
+ # в requirements.txt. Так делает httpx и весь класс проектов, живущих без poetry и uv:
3
+ # инструменты закреплены до патча, а границы зависимостей самой библиотеки оставлены широкими
4
+ # намеренно — библиотека, закрепившая их у себя, ломает сборку всем, кто её ставит.
5
+ [project]
6
+ name = "sample-lib"
7
+ version = "1.0.0"
8
+ dependencies = ["httpx"]
9
+
10
+ [build-system]
11
+ requires = ["setuptools"]
12
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,3 @@
1
+ -e .
2
+ pytest==8.4.1
3
+ ruff==0.16.6
@@ -0,0 +1,12 @@
1
+ # Питоновская библиотека, которая закрепляет версии не файлом блокировки, а точными версиями
2
+ # в requirements.txt. Так делает httpx и весь класс проектов, живущих без poetry и uv:
3
+ # инструменты закреплены до патча, а границы зависимостей самой библиотеки оставлены широкими
4
+ # намеренно — библиотека, закрепившая их у себя, ломает сборку всем, кто её ставит.
5
+ [project]
6
+ name = "sample-lib"
7
+ version = "1.0.0"
8
+ dependencies = ["httpx"]
9
+
10
+ [build-system]
11
+ requires = ["setuptools"]
12
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,3 @@
1
+ -e .
2
+ pytest>=8
3
+ ruff
@@ -12,13 +12,22 @@
12
12
  **Почему агенты плодят дубли особенно охотно.** Скопировать из соседнего файла дешевле, чем
13
13
  найти общее место и вынести туда: копия точно работает, вынесение может что-то сломать.
14
14
 
15
- **Готовый аналог есть.** `jscpd` умеет и Python, и TypeScript одним прогоном и считает
16
- **похожесть**, а не только точное совпадение. Рецепты под фронтовые стеки берут его.
15
+ **Готовый аналог есть, и под каждый стек свой.** `jscpd` для фронта считает **похожесть**, а не
16
+ только точное совпадение. `pylint --enable=R0801` для Python сравнивает разобранный код, и
17
+ переименованная переменная его не обманет. Рецепт под Python появился только 2026-09-07: до
18
+ ревизии каталога его не было вовсе, и питоновский проект получал нашу побайтовую самоделку,
19
+ хотя родной инструмент стоял у него уже установленным.
17
20
 
18
21
  **Переносимая проверка грубее.** Она ищет одинаковые восемь строк подряд после снятия отступов —
19
22
  совпадение байт в байт. Копия с переименованной переменной её не насторожит. Это запасной
20
23
  вариант, а не замена.
21
24
 
25
+ **Одинаковые импорты — не дубль.** Окно, в котором нет ни одной строки кроме ввоза (`import`,
26
+ `use`, `require`, путь в кавычках внутри `import (…)` в Go) и одиноких скобок, находкой не
27
+ считается. Найдено замером по `cobra`: гейт краснел на `doc/man_docs.go` и `doc/md_docs.go`,
28
+ где совпадали восемь строк подряд из списка импортов и ни одной строки логики. В Go, Java и Rust
29
+ ввоз стоит по одной строке на строку, и в соседних файлах одного пакета он совпадает всегда.
30
+
22
31
  **Тесты не проверяются.** Повтор в тестах часто осознанный: читаемость важнее сухости, и три
23
32
  похожих теста лучше одного хитрого. Случай «три однотипных — свести в один с набором входов»
24
33
  решается по правилам тестирования и глазами. На живом проекте **все двадцать находок были в