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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"deny": [
|
|
4
|
+
"Read(./.env)"
|
|
5
|
+
]
|
|
6
|
+
},
|
|
7
|
+
"hooks": {
|
|
8
|
+
"PreToolUse": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "Bash",
|
|
11
|
+
"hooks": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": ".claude/hooks/block-dangerous.sh"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"PostToolUse": [
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Write|Edit",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": ".claude/hooks/auto-format.sh"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"Stop": [
|
|
31
|
+
{
|
|
32
|
+
"hooks": [
|
|
33
|
+
{
|
|
34
|
+
"type": "command",
|
|
35
|
+
"command": ".claude/hooks/stop-gate.sh"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"UserPromptSubmit": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": ".claude/hooks/prompt.sh"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
],
|
|
51
|
+
"TaskCompleted": [
|
|
52
|
+
{
|
|
53
|
+
"matcher": "*",
|
|
54
|
+
"hooks": [
|
|
55
|
+
{
|
|
56
|
+
"type": "command",
|
|
57
|
+
"command": ".claude/hooks/done.sh"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"TeammateIdle": [
|
|
63
|
+
{
|
|
64
|
+
"matcher": null,
|
|
65
|
+
"hooks": [
|
|
66
|
+
{
|
|
67
|
+
"type": "command",
|
|
68
|
+
"command": ".claude/hooks/idle.sh"
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"deny": [
|
|
4
|
+
"Read(./.env)"
|
|
5
|
+
]
|
|
6
|
+
},
|
|
7
|
+
"hooks": {
|
|
8
|
+
"PoToolUse": [
|
|
9
|
+
{
|
|
10
|
+
"matcher": "Bash",
|
|
11
|
+
"hooks": [
|
|
12
|
+
{
|
|
13
|
+
"type": "command",
|
|
14
|
+
"command": ".claude/hooks/block-dangerous.sh"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"post_tool_use": [
|
|
20
|
+
{
|
|
21
|
+
"matcher": "Write|Edit",
|
|
22
|
+
"hooks": [
|
|
23
|
+
{
|
|
24
|
+
"type": "command",
|
|
25
|
+
"command": ".claude/hooks/auto-format.sh"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"Stop": [
|
|
31
|
+
{
|
|
32
|
+
"matcher": "Bash",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": ".claude/hooks/stop-gate.sh"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"PreToolCall": [
|
|
42
|
+
{
|
|
43
|
+
"matcher": "Bash(git commit*)",
|
|
44
|
+
"hooks": [
|
|
45
|
+
{
|
|
46
|
+
"type": "command",
|
|
47
|
+
"command": "npm run typecheck && npm test"
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Пакета с таким именем нет в реестре
|
|
2
|
+
|
|
3
|
+
**Намерение.** Агент, не знающий инструмента, придумывает правдоподобное имя: `reactCodemodHelper`,
|
|
4
|
+
`eslint-plugin-async-safe`, `@types/fetch-retry`. Имя попадает в `AGENTS.md`, в `SKILL.md`, в
|
|
5
|
+
пример из README. Дальше его читает следующий агент — человек или машина — и выполняет
|
|
6
|
+
`npm install`. Если к тому времени имя занято, в проект приезжает чужой код.
|
|
7
|
+
|
|
8
|
+
**Какой отказ это поймало.** README самого `slopcheck` открывается историей: модель написала
|
|
9
|
+
команду `npx` с именем `react-codeshift` в 47 файлах, пакета не существовало, кто-то его
|
|
10
|
+
зарегистрировал, и 237 репозиториев уже на него ссылались. Имя намеренно оторвано здесь от слова
|
|
11
|
+
`npx`: иначе эта строка стала бы находкой самой записи, а вердикт на `main` зависел бы от того,
|
|
12
|
+
не снимет ли реестр захваченный пакет с публикации. Красный образец выбирается так, чтобы его
|
|
13
|
+
нельзя было погасить снаружи, — и текст вокруг него тоже. Мы проверили этот пакет 2026-09-08:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
react-codeshift 1.0.0 создан 2026-01-14 maintainer: debugducky
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Доля: по [USENIX Security 2025](https://arxiv.org/abs/2406.10279) около 20% сгенерированного
|
|
20
|
+
кода ссылается на несуществующие пакеты, и 58% выдуманных имён повторяются от запроса к
|
|
21
|
+
запросу — то есть предсказуемы для того, кто захочет их занять.
|
|
22
|
+
|
|
23
|
+
**Почему машина, а не внимательность.** Отличить `jscodeshift` от `reactCodemodHelper` глазами
|
|
24
|
+
нельзя: оба выглядят как настоящие. Единственный арбитр — реестр.
|
|
25
|
+
|
|
26
|
+
**Готовый аналог есть, и мы его зовём.**
|
|
27
|
+
[`slopcheck`](https://github.com/mattschaller/slopcheck) (npm, MIT, ноль зависимостей) достаёт
|
|
28
|
+
имена из команд установки в `.md`, `.mdc`, `.yml`, `.yaml`, `.json`, `.cursorrules` и сверяет с
|
|
29
|
+
`registry.npmjs.org`. Своего разбора мы не писали. Обёртка отвечает за три вещи, которых он не
|
|
30
|
+
делает: какие файлы ему дать (иначе он находит наши собственные образцы), что считать браком и
|
|
31
|
+
что делать, когда реестр не ответил.
|
|
32
|
+
|
|
33
|
+
**Почему обёртка, а не прямой вызов.** Без сети `slopcheck` печатает `? имя — validation error`
|
|
34
|
+
и выходит **с нулём**. Для гейта это худший из возможных ответов: сборка зелёная, а не проверено
|
|
35
|
+
ничего — ровно та тишина, ради запрета которой существует весь стандарт. Обёртка в этом случае
|
|
36
|
+
выходит с кодом **2** и говорит «проверка не состоялась», отдельно от вердикта «брак».
|
|
37
|
+
Тот же код — если счётчик находок ненулевой, а разобрать их не вышло: значит `slopcheck` сменил
|
|
38
|
+
формат вывода, и молчать об этом нельзя. И тот же — если блока со счётчиками в ответе не нашлось
|
|
39
|
+
вовсе: так будет, если он начнёт печатать JSON одной строкой. Эту ветку нашли прогоном
|
|
40
|
+
подставного вывода **после** того, как написали «формат стабилен»: без неё все счётчики
|
|
41
|
+
оставались нулями и гейт выходил с нулём, не проверив ничего.
|
|
42
|
+
|
|
43
|
+
**Образцы.** В `red/AGENTS.md` кодмод вызван именем `reactCodemodHelper`. Имя выбрано не наугад: `npm`
|
|
44
|
+
запрещает заглавные буквы в именах новых пакетов, и это подтверждает та же библиотека, которой
|
|
45
|
+
пользуется реестр:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
$ node -e "console.log(require('validate-npm-package-name')('reactCodemodHelper'))"
|
|
49
|
+
validForNewPackages: false errors: [ 'name can no longer contain capital letters' ]
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Значит образец не сможет протухнуть: занять это имя нельзя никому, а выглядит оно как настоящая
|
|
53
|
+
галлюцинация — модели постоянно пишут camelCase. `green/AGENTS.md` — тот же файл, где кодмод
|
|
54
|
+
назван настоящим именем `jscodeshift`. Второй пакет, `prettier`, стоит в обоих: он показывает,
|
|
55
|
+
что зелёным гейт становится не от пустоты.
|
|
56
|
+
|
|
57
|
+
**Чего НЕ ловит.**
|
|
58
|
+
|
|
59
|
+
- **Пакет, который сквоттер уже зарегистрировал.** Проверка спрашивает у реестра только «есть
|
|
60
|
+
ли», и гаснет ровно в тот момент, когда становится опасно: `react-codeshift` сегодня зелёный.
|
|
61
|
+
Это предел приёма, а не недоделка — «есть ли пакет» и «чей он» разные вопросы, и второй
|
|
62
|
+
решают Socket и Snyk, которых мы не дублируем. Смысл записи в другом: до регистрации проходит
|
|
63
|
+
время, и большинство выдуманных имён так и остаются свободными.
|
|
64
|
+
- **Только npm.** PyPI, crates.io, Go-модули не проверяются: `slopcheck` их не умеет, а писать
|
|
65
|
+
своё — заводить второй разбор команд установки. Питоновский проект, где агент придумал имя
|
|
66
|
+
пакета, эта запись не прикроет.
|
|
67
|
+
- **Имя из прозы, принятое за пакет.** Разбор берёт слово после `npm i`/`npx`/`yarn add` и
|
|
68
|
+
спотыкается о текст, где эти слова стоят не как команда. Проверено на README самого
|
|
69
|
+
`slopcheck`: слово `Commands`, стоящее в прозе после имени команды, даёт находку
|
|
70
|
+
`Commands — not found on npm`. На
|
|
71
|
+
нашем репозитории — 89 файлов, 9 имён — ложных нет, но на чужом такое встретится. Первым,
|
|
72
|
+
кого эта запись покрасила, был её собственный README: имена образцов стояли там рядом с
|
|
73
|
+
`npx`. Текст переписан, проверка — нет. Ослепить её на README записей каталога значило бы
|
|
74
|
+
перестать видеть настоящие команды установки, которых там хватает.
|
|
75
|
+
- **Пакет, названный в исходнике, а не в документации.** `import` из несуществующего модуля —
|
|
76
|
+
предмет сборки, она об этом скажет сама.
|
|
77
|
+
- **Сеть.** Запись — единственная в каталоге, которой нужен интернет. В конвейере без выхода
|
|
78
|
+
наружу она честно выйдет с кодом 2, а не соврёт зелёным. Тем же кодом отвечает ответ реестра
|
|
79
|
+
«слишком часто» (HTTP 429): снаружи он неотличим от обрыва связи, и сообщение называет обе
|
|
80
|
+
причины, а не выбирает одну наугад.
|
|
81
|
+
- **Свой предел времени.** `doctor` даёт гейту 300 секунд, а здесь каждое имя — запрос наружу
|
|
82
|
+
(до трёх попыток по десять секунд, по десять имён разом). Проект с сотнями команд установки
|
|
83
|
+
при медленном реестре упрётся в предел и будет показан как `timeout` — не как «проверка не
|
|
84
|
+
состоялась». Эту разницу изнутри проверки не выразить: её съедает тот, кто её обрывает.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Имена пакетов, названные в документации, сверяются с реестром npm.
|
|
3
|
+
#
|
|
4
|
+
# ЗАЧЕМ. Агент, не знающий инструмента, придумывает правдоподобное имя. Дальше его читает
|
|
5
|
+
# другой агент и выполняет `npm install` — и если имя к тому времени кем-то занято, в проект
|
|
6
|
+
# приезжает чужой код. Обычные проверки цепочки поставок смотрят на пакеты, которые ЕСТЬ;
|
|
7
|
+
# здесь опасен как раз тот, которого пока нет.
|
|
8
|
+
#
|
|
9
|
+
# Работу делает slopcheck (MIT, ноль зависимостей). Мы отвечаем за три вещи, которых он не
|
|
10
|
+
# делает: какие файлы ему дать, что считать браком и что делать, когда ответа не было.
|
|
11
|
+
# Каждая ветка «ответа не было» кончается кодом 2 и словами «проверка не состоялась»: у
|
|
12
|
+
# инструмента, написанного против молчаливого зелёного, своего молчаливого зелёного быть не может.
|
|
13
|
+
DIR="${1:-.}"
|
|
14
|
+
|
|
15
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. В POSIX-оболочке (здесь
|
|
16
|
+
# dash) неудачный `.` завершает скрипт немедленно — привычное «. файл || запасной_вариант» не
|
|
17
|
+
# выполняется НИКОГДА, и гейт умирает с кодом 2 без единого слова о причине. Проверено прогоном
|
|
18
|
+
# 2026-09-08; та же мёртвая ветка стоит ещё в девяти проверках каталога.
|
|
19
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
20
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
21
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
22
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
23
|
+
exit 2
|
|
24
|
+
fi
|
|
25
|
+
. "$SKIP_LIB"
|
|
26
|
+
|
|
27
|
+
# Даже когда файл на месте, нужных функций в нём может не оказаться: без них конвейер ниже не
|
|
28
|
+
# напечатает ни одного пути, список окажется пуст, а пустой список — это `exit 0`. Гейт вышел бы
|
|
29
|
+
# зелёным, не посмотрев ни в один файл.
|
|
30
|
+
if ! command -v own_samples_filter >/dev/null 2>&1; then
|
|
31
|
+
echo "в _skip.sh нет обхода own_samples_filter — проверка не состоялась"
|
|
32
|
+
echo " почини: обнови kit/gates/_skip.sh до версии, идущей с этим гейтом"
|
|
33
|
+
exit 2
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
if ! command -v slopcheck >/dev/null 2>&1; then
|
|
37
|
+
echo "не найден slopcheck — эта проверка делегирована ему"
|
|
38
|
+
echo " почини: npm i -g slopcheck@0.2.0"
|
|
39
|
+
exit 2
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Свой обход, а не встроенный в slopcheck: только так работает own_samples_filter. Без него
|
|
43
|
+
# красный образец этой же записи выдаётся за находку, и гейт краснеет на самом комплекте.
|
|
44
|
+
LIST=$(mktemp) || exit 2
|
|
45
|
+
# shellcheck disable=SC2046
|
|
46
|
+
find "$DIR" $(skip_find "$DIR") -type f \
|
|
47
|
+
\( -name '*.md' -o -name '*.mdc' -o -name '*.yml' -o -name '*.yaml' \
|
|
48
|
+
-o -name '*.json' -o -name '.cursorrules' \) -print 2>/dev/null \
|
|
49
|
+
| own_samples_filter "$DIR" > "$LIST"
|
|
50
|
+
|
|
51
|
+
if [ ! -s "$LIST" ]; then rm -f "$LIST"; exit 0; fi
|
|
52
|
+
|
|
53
|
+
# Через NUL, а не через $(...): путь с пробелом иначе распадается на два аргумента, slopcheck
|
|
54
|
+
# не находит ни одного из них и молча выходит с нулём. Та же ловушка уже стоила нам зелёного
|
|
55
|
+
# вердикта в hook-actually-fires.
|
|
56
|
+
ERRF=$(mktemp) || { rm -f "$LIST"; exit 2; }
|
|
57
|
+
OUT=$(tr '\n' '\0' < "$LIST" | xargs -0 slopcheck --json 2>"$ERRF")
|
|
58
|
+
# Код xargs: 0 — все вызовы прошли, 123 — хотя бы один вернул 1..125. Именно 123 приходит и на
|
|
59
|
+
# честной находке (slopcheck выходит с 1), поэтому сам по себе он ничего не значит. А вот всё
|
|
60
|
+
# остальное — сбой обвязки, и его нельзя принимать за вердикт.
|
|
61
|
+
XS=$?
|
|
62
|
+
ERR=$(cat "$ERRF" 2>/dev/null)
|
|
63
|
+
rm -f "$ERRF" "$LIST"
|
|
64
|
+
|
|
65
|
+
# Файлов может оказаться больше, чем влезает в одну команду, и тогда xargs зовёт slopcheck
|
|
66
|
+
# несколько раз. Упавший вызов пишет в stderr, а уцелевшие всё равно печатают свой JSON —
|
|
67
|
+
# счётчики сходятся, и гейт вышел бы с нулём, не проверив целую партию файлов.
|
|
68
|
+
if [ -n "$ERR" ] || { [ "$XS" -ne 0 ] && [ "$XS" -ne 123 ]; }; then
|
|
69
|
+
echo "slopcheck не отработал (код $XS) — проверка не состоялась, это не вердикт «чисто»"
|
|
70
|
+
[ -n "$ERR" ] && printf '%s\n' "$ERR" | head -5 | sed 's/^/ /'
|
|
71
|
+
echo " почини: прогони «slopcheck --json .» руками и посмотри, на чём он споткнулся"
|
|
72
|
+
exit 2
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
[ -n "$OUT" ] || {
|
|
76
|
+
echo "slopcheck ничего не ответил — проверка не состоялась, это не вердикт «чисто»"
|
|
77
|
+
echo " почини: прогони «slopcheck --json .» руками и посмотри, на чём он споткнулся"
|
|
78
|
+
exit 2
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
printf '%s\n' "$OUT" | awk '
|
|
82
|
+
function val(s) { sub(/^[^:]*:[[:space:]]*/, "", s); sub(/,$/, "", s); gsub(/^"|"$/, "", s); return s }
|
|
83
|
+
function label(s) {
|
|
84
|
+
if (s == "not_found") return "в реестре такого пакета нет"
|
|
85
|
+
if (s == "unpublished") return "пакет был и снят с публикации — имя свободно для захвата"
|
|
86
|
+
if (s == "security_hold") return "реестр пометил пакет как вредоносный"
|
|
87
|
+
return ""
|
|
88
|
+
}
|
|
89
|
+
# xargs может позвать slopcheck несколько раз, если путей слишком много для одной команды.
|
|
90
|
+
# Тогда JSON-документов на входе будет несколько, и счётчики надо складывать, а не брать
|
|
91
|
+
# последний: иначе находки первой партии исчезнут.
|
|
92
|
+
/^ "packages"/ { inpkg = 1; sawpkg = 1; next }
|
|
93
|
+
inpkg && /^ \}/ { inpkg = 0; next }
|
|
94
|
+
inpkg && /"errors"/ { errs += val($0); next }
|
|
95
|
+
inpkg && /"notFound"/ { bad += val($0); next }
|
|
96
|
+
inpkg && /"unpublished"/ { bad += val($0); next }
|
|
97
|
+
inpkg && /"securityHold"/ { bad += val($0); next }
|
|
98
|
+
/^ "package"/ { pkg = val($0); next }
|
|
99
|
+
/^ "status"/ { st = val($0); next }
|
|
100
|
+
/^ "file"/ { f = val($0); next }
|
|
101
|
+
/^ "line"/ { ln = val($0); next }
|
|
102
|
+
# Печатаем на «command»: он идёт последним в записи о месте, значит к этому моменту известны
|
|
103
|
+
# и пакет, и состояние, и файл со строкой.
|
|
104
|
+
/^ "command"/ {
|
|
105
|
+
cmd = val($0)
|
|
106
|
+
if (st == "error") next
|
|
107
|
+
# Состояние, которого мы не знаем. Раньше здесь стояло «всё, что не error, — брак», и
|
|
108
|
+
# новое состояние в следующей версии slopcheck превратило бы КАЖДУЮ команду установки в
|
|
109
|
+
# находку с английским словом вместо объяснения. Незнакомое состояние — повод сказать
|
|
110
|
+
# «не разобрали», а не вынести вердикт.
|
|
111
|
+
if (label(st) == "") { unknown = st; next }
|
|
112
|
+
if (shown < 20) print f ":" ln ": «" pkg "» — " label(st) " · " cmd
|
|
113
|
+
shown++
|
|
114
|
+
next
|
|
115
|
+
}
|
|
116
|
+
END {
|
|
117
|
+
if (unknown != "") {
|
|
118
|
+
print "slopcheck вернул незнакомое состояние «" unknown "» — разобрать ответ не вышло"
|
|
119
|
+
print " почини: сверь версию slopcheck с той, что названа в gate.yml"
|
|
120
|
+
exit 2
|
|
121
|
+
}
|
|
122
|
+
if (shown > 20) print " … и ещё " (shown - 20)
|
|
123
|
+
if (shown > 0) {
|
|
124
|
+
# Находки и «не проверено» показываются вместе. Свернуть второе в первое значило бы
|
|
125
|
+
# потерять ровно ту разницу, ради которой написана эта обёртка.
|
|
126
|
+
if (errs > 0) print " кроме того, реестр не ответил по именам: " errs " — они НЕ проверены"
|
|
127
|
+
print " почини: проверь имя в реестре и впиши настоящее. Если пакета нет, его может"
|
|
128
|
+
print " зарегистрировать кто угодно — и следующий агент выполнит установку чужого кода."
|
|
129
|
+
exit 1
|
|
130
|
+
}
|
|
131
|
+
# Реестр не ответил. Молчать здесь нельзя: тишина неотличима от «всё хорошо», а именно
|
|
132
|
+
# это наш стандарт и запрещает. Красим отдельным кодом, чтобы вердикт не путали с браком.
|
|
133
|
+
# Причина названа обеими: slopcheck помечает так и недоступную сеть, и ответ 429 «слишком
|
|
134
|
+
# часто», и любой другой не-200 — снаружи они неразличимы, и врать про сеть мы не будем.
|
|
135
|
+
if (errs > 0) {
|
|
136
|
+
print "реестр npm не ответил: не проверено имён — " errs
|
|
137
|
+
print " проверка не состоялась. Это не вердикт «чисто»."
|
|
138
|
+
print " почини: если сети нет — прогони гейт там, где она есть. Если сеть есть, реестр"
|
|
139
|
+
print " мог ограничить частоту запросов: повтори через минуту."
|
|
140
|
+
exit 2
|
|
141
|
+
}
|
|
142
|
+
# Блока со счётчиками не нашлось вовсе. Так будет, если slopcheck начнёт печатать JSON
|
|
143
|
+
# одной строкой: ни одно правило выше не сработает, все счётчики останутся нулями — и
|
|
144
|
+
# проверка выйдет с нулём, ничего не проверив. Нашли это прогоном подставного вывода уже
|
|
145
|
+
# после того, как записали «формат стабилен»: догадка о стабильности стоила бы молчаливого
|
|
146
|
+
# зелёного на каждом прогоне.
|
|
147
|
+
if (!sawpkg) {
|
|
148
|
+
print "ответ slopcheck не разобран: блока «packages» в нём нет"
|
|
149
|
+
print " почини: сверь версию slopcheck с той, что названа в gate.yml"
|
|
150
|
+
exit 2
|
|
151
|
+
}
|
|
152
|
+
# Счётчик говорит о браке, а печатать нечего — значит мы разучились читать вывод slopcheck
|
|
153
|
+
# (сменился формат). Тоже не вердикт «чисто».
|
|
154
|
+
if (bad > 0) {
|
|
155
|
+
print "slopcheck насчитал находок: " bad ", но разобрать их не вышло — сменился формат"
|
|
156
|
+
print " почини: сверь версию slopcheck с той, что названа в gate.yml"
|
|
157
|
+
exit 2
|
|
158
|
+
}
|
|
159
|
+
exit 0
|
|
160
|
+
}
|
|
161
|
+
'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
intent: имя пакета, названное в документации, существует в реестре
|
|
2
|
+
intent_en: a package name mentioned in the documentation exists in the registry
|
|
3
|
+
|
|
4
|
+
# Там, где агента используют: предмет проверки — текст, который агент читает и по которому
|
|
5
|
+
# выполняет установку. В проекте без такого текста ставить запись незачем.
|
|
6
|
+
trigger:
|
|
7
|
+
has_agent_entry: true
|
|
8
|
+
|
|
9
|
+
recipes:
|
|
10
|
+
any: bash {gate}/check.sh {dir}
|
|
11
|
+
|
|
12
|
+
# Программа, без которой запись не работает. По первому слову команды этого не видно: обёртка
|
|
13
|
+
# начинается с `bash`, который есть всегда. Версия названа: обёртка разбирает JSON slopcheck,
|
|
14
|
+
# и смена формата обязана быть видимой, а не тихой.
|
|
15
|
+
requires: slopcheck
|
|
16
|
+
|
|
17
|
+
proof: incidents/README.md, 2026-09-08 «пакет-призрак из чужого README оказался
|
|
18
|
+
зарегистрирован» — `react-codeshift`, канонический пример slopsquatting из README самого
|
|
19
|
+
slopcheck, на 2026-09-08 существует в npm (создан 2026-01-14, maintainer debugducky);
|
|
20
|
+
доля выдуманных имён в сгенерированном коде — USENIX Security 2025, arxiv 2406.10279
|
|
@@ -3,42 +3,36 @@
|
|
|
3
3
|
**Намерение.** `print` и `console.log` не попадают в прод: они проходят мимо системы логов,
|
|
4
4
|
не имеют уровня и могут вынести наружу то, чего в выводе быть не должно.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
`
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
не
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
собранной по частям. Найдено на комментариях: печать внутри `//`, `#` и `/* */` отбрасывается,
|
|
40
|
-
потому что комментарий не выполняется.
|
|
41
|
-
|
|
42
|
-
**Образцы.** `red/` — модуль с отладочной печатью на трёх языках. `green/` — тот же модуль через
|
|
43
|
-
систему логов, плюс пример в JSDoc и закомментированная строка, на которых проверка обязана
|
|
44
|
-
молчать.
|
|
6
|
+
**Своей проверки здесь больше нет — и это результат замера, а не вкуса.** Переносимая версия
|
|
7
|
+
искала конструкции печати по тексту. Прогон по пяти чужим репозиториям (`httpx`, `fastapi`,
|
|
8
|
+
`zod`, `cobra`, `ripgrep`) дал **434 находки, настоящих — ноль**:
|
|
9
|
+
|
|
10
|
+
| Где нашлось | Что это было на самом деле |
|
|
11
|
+
|---|---|
|
|
12
|
+
| `zod/packages/bench` — 231 | бенчмарки, где печать и есть предмет |
|
|
13
|
+
| `fastapi/docs_src` | примеры кода в документации |
|
|
14
|
+
| `httpx/_exceptions.py` | печать **внутри строки документации**, между ``` |
|
|
15
|
+
| `fastapi/cli.py` | вывод программы командной строки — это интерфейс |
|
|
16
|
+
| `httpx/tests` | тесты |
|
|
17
|
+
|
|
18
|
+
Отличить это от забытой отладки можно только разбором кода, а не поиском по тексту. Разбор
|
|
19
|
+
уже написан и поддерживается: `ruff` и `eslint`. Наш поиск по тексту не приближался к ним и не
|
|
20
|
+
мог приблизиться.
|
|
21
|
+
|
|
22
|
+
**Готовый аналог — это и есть он сам.** `ruff --select T20` для Python, правило `no-console`
|
|
23
|
+
в eslint для JavaScript и TypeScript. Триггер записи сужен ровно до этих трёх языков: показывать
|
|
24
|
+
её проекту на Go, которому мы не можем дать ни инструмента, ни своей проверки, — значит показывать
|
|
25
|
+
работу, которую человек сделать не сможет.
|
|
26
|
+
|
|
27
|
+
**Чего НЕ ловит.** Печать через обёртку — `myprint(x)`, свой хелпер — не видит ни один из двоих:
|
|
28
|
+
они знают конструкции языка, а не «вывод в поток». Печать из программы командной строки оба
|
|
29
|
+
считают нарушением, хотя там она законна; такие каталоги исключают настройкой самого
|
|
30
|
+
инструмента (`per-file-ignores` в `ruff`, `overrides` в eslint), а не нашим кодом.
|
|
31
|
+
|
|
32
|
+
**Чем это оплачено.** Проект на Go, Rust, Java, Ruby по этому пункту не получает ничего. Так
|
|
33
|
+
честнее: проверка, которая на живом коде ошибается в ста процентах случаев, не «лучше, чем
|
|
34
|
+
ничего» — она хуже. Её выключают целиком, а вместе с ней и те, что работают.
|
|
35
|
+
|
|
36
|
+
**Образцы.** `red/service.py` — модуль с отладочной печатью. `green/service.py` — тот же модуль
|
|
37
|
+
через систему логов. `green/legacy.py` — закомментированная печать, на которой арбитр обязан
|
|
38
|
+
молчать. Проверяются рецептом `python` (`samples_for`), потому что переносимого рецепта нет.
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
intent: отладочная печать не доезжает до прод-кода
|
|
2
2
|
intent_en: debug printing does not reach production code
|
|
3
3
|
|
|
4
|
-
# Запись касается только языков, где есть
|
|
5
|
-
# Rust она не показывается
|
|
4
|
+
# Запись касается только языков, где под неё есть готовый инструмент. В проекте
|
|
5
|
+
# на Go или Rust она не показывается вовсе — и переносимого рецепта здесь больше нет.
|
|
6
6
|
trigger:
|
|
7
7
|
langs: python, javascript, typescript
|
|
8
8
|
|
|
9
|
+
# Переносимого рецепта нет намеренно. Он был, и замер по пяти чужим репозиториям
|
|
10
|
+
# (httpx, fastapi, zod, cobra, ripgrep) дал 434 находки, из которых настоящих — ноль:
|
|
11
|
+
# печать в примерах документации, в тестах, в бенчмарках и в выводе командной строки.
|
|
12
|
+
# Отличить их от отладки можно только разбором кода, а не поиском по тексту, — и это
|
|
13
|
+
# ровно то, что уже делают ruff и eslint.
|
|
9
14
|
recipes:
|
|
10
|
-
any: bash {gate}/check.sh {dir}
|
|
11
15
|
python: ruff check --select T20 {dir}
|
|
12
16
|
typescript: eslint --rule '{"no-console":"error"}' {dir}
|
|
17
|
+
javascript: eslint --rule '{"no-console":"error"}' {dir}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
19
|
+
# Каким рецептом написаны образцы: без переносимого рецепта проверка обязана знать это точно,
|
|
20
|
+
# иначе питоновские образцы поедут проверяться фронтовым инструментом.
|
|
21
|
+
samples_for: python
|
|
22
|
+
|
|
23
|
+
proof: incidents/README.md, 2026-09-07 «замер по пяти стекам вынес приговор пяти записям» —
|
|
24
|
+
434 находки переносимой проверки на пяти чужих репозиториях, настоящих ноль
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Личное не раздаётся всему проекту
|
|
2
|
+
|
|
3
|
+
**Намерение.** `CLAUDE.local.md` и `.claude/settings.local.json` по устройству личные. Попав в
|
|
4
|
+
git, они становятся обязательными для всех — и при этом их никто не рецензирует, потому что
|
|
5
|
+
туда их никто и не звал.
|
|
6
|
+
|
|
7
|
+
**Дело не в опрятности.** Документация Claude Code, дословно:
|
|
8
|
+
|
|
9
|
+
> Within each directory, `CLAUDE.local.md` is appended after `CLAUDE.md`, so your personal notes
|
|
10
|
+
> are the last thing Claude reads at that level.
|
|
11
|
+
|
|
12
|
+
То есть личные заметки одного человека читаются **последними** и перекрывают общие правила
|
|
13
|
+
команды — молча, у каждого. С правами то же самое: разрешения, которые человек выдал себе,
|
|
14
|
+
достаются всем, кто склонировал репозиторий.
|
|
15
|
+
|
|
16
|
+
Оба файла документация прямо велит не коммитить:
|
|
17
|
+
|
|
18
|
+
> For private per-project preferences that shouldn't be checked into version control… Add
|
|
19
|
+
> `CLAUDE.local.md` to your `.gitignore` so it isn't committed.
|
|
20
|
+
|
|
21
|
+
> Claude Code keeps it out of git when it creates the file; if you create it by hand, add it to
|
|
22
|
+
> `.gitignore` yourself.
|
|
23
|
+
|
|
24
|
+
(`code.claude.com/docs/en/memory` и `/settings`, сверено 2026-09-07.)
|
|
25
|
+
|
|
26
|
+
**Какой отказ это поймало.** Замер по тринадцати чужим репозиториям: шесть настоящих находок в
|
|
27
|
+
пяти, ноль ложных на семи контрольных. Самая наглядная — `shacker/django-todo`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
"allow": [
|
|
31
|
+
"Read(//Users/shacker/**)",
|
|
32
|
+
"Bash(/Users/shacker/dev/home/django-todo/.venv/bin/python -m pytest …)"
|
|
33
|
+
]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Домашний каталог автора и абсолютные пути с его машины раздаются всем, кто клонирует проект.
|
|
37
|
+
На чужой машине они не работают, а разрешение на чтение чужого домашнего каталога — работает.
|
|
38
|
+
|
|
39
|
+
**Готовый аналог есть, и он не роняет прогон.** У [`AgentLint`](https://github.com/0xmariowu/AgentLint)
|
|
40
|
+
есть проверка C5 «`CLAUDE.local.md` not in git». Прогнали живьём (`npx -p agentlint-ai agentlint
|
|
41
|
+
check`, 2026-09-07) на репозитории, где в git лежат и `CLAUDE.local.md`, и
|
|
42
|
+
`.claude/settings.local.json`: он выдал **61/100** и **код возврата 0**. Флага порога в его
|
|
43
|
+
README нет, `--help` вывода не дал. Это оценка, а не гейт: в конвейере такой прогон зелёный.
|
|
44
|
+
Разница ровно та, ради которой существует наш стандарт. `claudelint` этой проверки не имеет.
|
|
45
|
+
|
|
46
|
+
**Чего НЕ ловит.**
|
|
47
|
+
|
|
48
|
+
- **Файл, лежащий на диске и не отслеживаемый git.** Он и не должен ловиться: это норма.
|
|
49
|
+
- **Репозиторий-заготовку проекта.** Если в корне лежит `cookiecutter.json`, `copier.yml` или
|
|
50
|
+
`.copier-answers.yml`, все файлы в нём — рыба для будущего проекта, а не чьи-то личные.
|
|
51
|
+
Проверка молча пропускает такой репозиторий, назвав причину вслух. Найдено замером:
|
|
52
|
+
в `Frojd/Wagtail-Pipit` обе находки были ровно такими, и обе ложные.
|
|
53
|
+
- **Заготовки по имени и по месту:** `*.example`, `*.template`, каталоги `templates/`,
|
|
54
|
+
`examples/`, `samples/`, `fixtures/`, а также подстановки генератора `{{…}}` в пути.
|
|
55
|
+
- **Личный свод под чужим именем.** Файл, названный `CLAUDE.md`, но содержащий личные заметки,
|
|
56
|
+
проверка не отличит: она смотрит на имя и на git, а не на смысл текста.
|
|
57
|
+
- **Файлы других агентов.** `.cursor/rules` и инструкции copilot своего «личного» уровня не
|
|
58
|
+
имеют — проверять там нечего.
|
|
59
|
+
|
|
60
|
+
**Образцы.** Настоящего git внутри каталога комплекта взять неоткуда, поэтому образцы кладут
|
|
61
|
+
список отслеживаемых файлов в `.aqk-tracked`; в живом проекте такого файла не бывает и список
|
|
62
|
+
спрашивается у git. Имя с точкой намеренно: файл `TRACKED` в корне чужого проекта — вещь
|
|
63
|
+
возможная, и он молча подменял бы собой список git. `red/` — оба личных файла в индексе.
|
|
64
|
+
`green/` — заготовки по имени, по каталогу и с подстановкой генератора, плюс `settings.local.json`
|
|
65
|
+
в `.vscode/` и `node_modules/`, на которых арбитр обязан молчать: личным этот файл считается
|
|
66
|
+
только внутри `.claude/`.
|