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.
- package/README.md +18 -2
- package/README.ru.md +16 -0
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +29 -4
- package/kit/gates/README.md +40 -0
- package/kit/gates/ci-actually-fails/README.md +12 -0
- package/kit/gates/ci-actually-fails/check.sh +26 -3
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +16 -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/check.sh +13 -1
- 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/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/check.sh +13 -1
- 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 +9 -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 +1 -1
- package/package.json +3 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/doctor.mjs +49 -9
- package/tool/commands/gates.mjs +10 -4
- package/tool/commands/project.mjs +8 -1
- 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 +49 -54
- package/tool/i18n/ru-docs.mjs +70 -0
- package/tool/i18n/ru.mjs +48 -54
- package/tool/i18n/templates-en.mjs +1 -1
- package/tool/i18n/templates-ru.mjs +1 -1
- package/tool/lib/core.mjs +7 -1
- package/tool/lib/manifest.mjs +36 -5
- package/tool/lib/prove.mjs +160 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +37 -2
- package/tool/lib/templates.mjs +2 -0
- package/tool/program.mjs +5 -0
- package/tool/selfcheck/gates.sh +66 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +279 -39
- package/tool/selfcheck/units-level.mjs +60 -0
- package/tool/selfcheck/units.mjs +113 -2
- 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
|
```
|
|
@@ -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
|
-
| Инструмент | Что проверяет | Состояние
|
|
177
|
+
| Инструмент | Что проверяет | Состояние — дата замера в самой ячейке |
|
|
178
178
|
|---|---|---|
|
|
179
|
-
| [`agnix`](https://github.com/agent-sh/agnix) | 455 правил: структура `CLAUDE.md`/`AGENTS.md`/`SKILL.md`, синтаксис конфигов MCP и хуков, соглашения об именах, **мёртвые ссылки на файлы**. Есть автопочинка и LSP | 404 ⭐, Rust,
|
|
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` пишется, **что именно проверено**: какой инструмент
|
package/kit/gates/README.md
CHANGED
|
@@ -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
|
-
|
|
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
|
репозиторий, который сам по себе стал бы проблемой версионирования.
|
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
# оставляет след для человека, читающего историю позже.
|
|
5
5
|
DIR="${1:-.}"
|
|
6
6
|
|
|
7
|
-
# Образцы (gates.sh) читают тело коммита из
|
|
7
|
+
# Образцы (gates.sh) читают тело коммита из `.aqk-commit-msg` — реальный git log там взять неоткуда, а
|
|
8
8
|
# вложенный .git внутри каталога комплекта сам по себе создал бы embedded-репозиторий.
|
|
9
|
-
# В настоящем проекте
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,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"
|
|
@@ -12,13 +12,22 @@
|
|
|
12
12
|
**Почему агенты плодят дубли особенно охотно.** Скопировать из соседнего файла дешевле, чем
|
|
13
13
|
найти общее место и вынести туда: копия точно работает, вынесение может что-то сломать.
|
|
14
14
|
|
|
15
|
-
**Готовый аналог
|
|
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
|
решается по правилам тестирования и глазами. На живом проекте **все двадцать находок были в
|