agent-quality-kit 0.6.0 → 0.8.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 +61 -2
- package/README.ru.md +61 -2
- 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/_skip.sh +61 -1
- 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/ci-not-hijackable/README.md +56 -0
- package/kit/gates/ci-not-hijackable/check.sh +73 -0
- package/kit/gates/ci-not-hijackable/gate.yml +19 -0
- package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
- package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
- package/kit/gates/color-from-token/check.sh +19 -3
- package/kit/gates/color-from-token/green/Button.tsx +2 -0
- 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 +27 -9
- 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 +36 -5
- 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 +10 -1
- package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
- package/kit/gates/file-size-limit/README.md +9 -2
- package/kit/gates/file-size-limit/check.sh +14 -2
- 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 +29 -4
- package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
- 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 +14 -2
- package/kit/gates/todo-without-task/green/app.py +1 -0
- 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 +22 -1
- package/package.json +6 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/context.mjs +260 -0
- package/tool/commands/doctor.mjs +72 -25
- package/tool/commands/gates.mjs +10 -4
- package/tool/commands/learn.mjs +159 -0
- package/tool/commands/project.mjs +9 -1
- package/tool/commands/prove.mjs +67 -0
- package/tool/commands/report.mjs +37 -2
- package/tool/i18n/en-docs.mjs +154 -0
- package/tool/i18n/en.mjs +70 -90
- package/tool/i18n/ru-docs.mjs +156 -0
- package/tool/i18n/ru.mjs +69 -90
- package/tool/i18n/templates-en.mjs +1 -1
- package/tool/i18n/templates-ru.mjs +1 -1
- package/tool/lib/core.mjs +37 -2
- package/tool/lib/evidence.mjs +124 -0
- package/tool/lib/manifest.mjs +63 -5
- package/tool/lib/prove.mjs +172 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +46 -2
- package/tool/lib/templates.mjs +3 -0
- package/tool/program.mjs +23 -22
- package/tool/selfcheck/gates.sh +66 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +519 -39
- package/tool/selfcheck/units-context.mjs +186 -0
- package/tool/selfcheck/units-evidence.mjs +83 -0
- package/tool/selfcheck/units-learn.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +122 -0
- package/tool/selfcheck/units.mjs +114 -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
|
@@ -2,13 +2,38 @@
|
|
|
2
2
|
# Секрет, попавший в историю, из неё уже не убрать: ветку перепишут, а копии останутся у всех,
|
|
3
3
|
# кто её тянул. Поэтому проверка стоит до коммита, а не «иногда руками».
|
|
4
4
|
DIR="${1:-.}"
|
|
5
|
-
|
|
5
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
6
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
7
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
8
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
9
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
10
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
11
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
12
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
13
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
14
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
15
|
+
exit 2
|
|
16
|
+
fi
|
|
17
|
+
. "$SKIP_LIB"
|
|
6
18
|
# Красный образец — намеренно сломанный код в репозитории. Сканирующий гейт обязан его
|
|
7
19
|
# пропускать, иначе будет вечно краснеть на том, что сам же и положил. Исключение снимается,
|
|
8
20
|
# когда проверяют сам образец: тогда каталог red и есть цель проверки.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
# Два прохода, а не один, и разница между ними измерена. Живой облачный ключ — находка везде,
|
|
22
|
+
# включая тестовые каталоги: он открывает настоящий счёт. Приватный КЛЮЧ в тестовых данных —
|
|
23
|
+
# норма: любой проект с проверками TLS кладёт туда сертификат, и Go так делает по соглашению.
|
|
24
|
+
# Замер 2026-09-08 по шести чужим репозиториям: gin краснел на testdata/certificate/key.pem,
|
|
25
|
+
# положенном туда намеренно. Гейт, краснеющий на нормальном укладе, выключают в первый день —
|
|
26
|
+
# и тогда он не ловит уже НИЧЕГО, включая настоящий ключ.
|
|
27
|
+
TOKENS='(sk|pk)_(live|test)_[A-Za-z0-9]{16,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{30,}|xox[baprs]-[A-Za-z0-9-]{10,}'
|
|
28
|
+
PRIVKEY='-----BEGIN [A-Z ]*PRIVATE KEY-----'
|
|
29
|
+
# Каталоги образцов: только те, чьё имя не оставляет сомнений. `data` или `assets` сюда не
|
|
30
|
+
# входят — там ключ вполне может оказаться настоящим.
|
|
31
|
+
FIXTURES='(^|/)(testdata|fixtures|__fixtures__|test|tests|spec|__tests__)/'
|
|
32
|
+
|
|
33
|
+
HITS=$(
|
|
34
|
+
{ grep -rInE $(skip_grep "$DIR") -e "$TOKENS" "$DIR" 2>/dev/null
|
|
35
|
+
grep -rInE $(skip_grep "$DIR") -e "$PRIVKEY" "$DIR" 2>/dev/null | grep -vE "$FIXTURES"
|
|
36
|
+
} | grep -v '/\.git/' | own_samples_filter "$DIR" | sort -u)
|
|
12
37
|
if [ -n "$HITS" ]; then
|
|
13
38
|
echo "$HITS" | cut -c1-160
|
|
14
39
|
echo " почини: убери значение из файла, положи его в переменную окружения и отзови старый ключ."
|
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
# Ошибка не глушится молча
|
|
2
2
|
|
|
3
|
-
**Намерение.**
|
|
3
|
+
**Намерение.** Перехваченная и выброшенная ошибка — это отказ, о котором никто не узнал.
|
|
4
|
+
Система продолжает работать «как будто всё хорошо», а причина всплывает через недели и в другом
|
|
5
|
+
месте. Ошибка либо обработана и записана, либо проброшена дальше.
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
Запись в журнале: `incidents/README.md`, 2026-08-25.
|
|
7
|
+
**Своей проверки здесь больше нет — так решил замер.** Переносимая версия искала перехват по
|
|
8
|
+
тексту. Прогон по пяти чужим репозиториям (`httpx`, `fastapi`, `zod`, `cobra`, `ripgrep`) дал
|
|
9
|
+
46 находок, и настоящим оказался ровно один класс — пустые `catch (_) {}` в `zod`. Остальное:
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
```python
|
|
12
|
+
try:
|
|
13
|
+
if sniffio.current_async_library() == "trio":
|
|
14
|
+
return True
|
|
15
|
+
except ImportError: # библиотеки нет — значит не trio. Это не глушение, это ветвление.
|
|
16
|
+
pass
|
|
17
|
+
```
|
|
12
18
|
|
|
13
|
-
|
|
14
|
-
|
|
19
|
+
```rust
|
|
20
|
+
let _ = self.wtr.write(b"\n")?; // «?» пробрасывает ошибку. Отброшено только число байт.
|
|
21
|
+
```
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
Питоновский `except <узкое исключение>: pass` — законный приём: ловят не ошибку, а её отсутствие.
|
|
24
|
+
Растовый `let _ = …?` вообще ничего не глушит. Отличить это от настоящего глушения можно только
|
|
25
|
+
разбором кода — а разбор уже написан.
|
|
18
26
|
|
|
19
|
-
**Готовый аналог
|
|
20
|
-
покрывают разные оттенки: `BLE` — ловля голого исключения, `TRY400` — запись в лог без трейса,
|
|
21
|
-
`SIM105` — перехват ради тишины. В TypeScript — `no-empty` с запретом пустого перехвата.
|
|
22
|
-
Измерено на живом проекте: `TRY400` нашёл 47 мест, которые самописный разбор искал час.
|
|
23
|
-
Рецепты под эти стеки берут готовое; своя проверка — запасная.
|
|
27
|
+
**Готовый аналог — это и есть он сам.**
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
| Стек | Чем |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Python | `ruff --select BLE,TRY400,SIM105` |
|
|
32
|
+
| JavaScript, TypeScript | `eslint no-empty` с `allowEmptyCatch: false` |
|
|
33
|
+
| Go | [`errcheck`](https://github.com/kisielk/errcheck) — канонический инструмент под этот предмет |
|
|
34
|
+
|
|
35
|
+
**Чего НЕ ловит.** Перехват, который что-то делает, но делает не то: `except: return None`,
|
|
36
|
+
`catch { return [] }`. Формально обработка есть, по сути ошибка потеряна так же. Это видно
|
|
37
|
+
только глазами и на ревью. Для Rust рецепта нет: у `clippy` есть близкие правила
|
|
38
|
+
(`let_underscore_must_use`), но мы их не замеряли, а рецепт без замера — та же догадка,
|
|
39
|
+
из-за которой эта запись и лишилась своей проверки.
|
|
40
|
+
|
|
41
|
+
**Чем это оплачено.** Проект на Rust, Java, Ruby, PHP по этому пункту не получает ничего.
|
|
42
|
+
|
|
43
|
+
**Образцы.** `red/loader.py` — `except Exception: pass`. `green/loader.py` — узкое исключение,
|
|
44
|
+
запись в журнал и проброс. Проверяются рецептом `python` (`samples_for`).
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
intent: ошибка не глушится молча — она обработана и записана либо проброшена
|
|
2
2
|
intent_en: an error is not silently swallowed — it is handled and logged, or re-raised
|
|
3
3
|
|
|
4
|
+
# Только языки, под которые есть готовый разбор кода. Переносимой проверки здесь больше нет:
|
|
5
|
+
# отличить «перехват ради тишины» от законного `except ImportError: pass` можно разбором,
|
|
6
|
+
# а не поиском по тексту.
|
|
4
7
|
trigger:
|
|
5
|
-
|
|
8
|
+
langs: python, javascript, typescript, go
|
|
6
9
|
|
|
7
10
|
recipes:
|
|
8
|
-
any: bash {gate}/check.sh {dir}
|
|
9
11
|
# BLE — ловля голого исключения, TRY400 — запись без трейса, SIM105 — перехват ради тишины.
|
|
10
12
|
python: ruff check --select BLE,TRY400,SIM105 {dir}
|
|
11
13
|
typescript: eslint --rule '{"no-empty":["error",{"allowEmptyCatch":false}]}' {dir}
|
|
14
|
+
javascript: eslint --rule '{"no-empty":["error",{"allowEmptyCatch":false}]}' {dir}
|
|
15
|
+
# errcheck — канонический инструмент Go под этот же предмет: ошибка присвоена и не проверена.
|
|
16
|
+
go: errcheck -blank {dir}/...
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
samples_for: python
|
|
19
|
+
|
|
20
|
+
proof: incidents/README.md, 2026-09-07 «замер по пяти стекам вынес приговор пяти записям» —
|
|
21
|
+
переносимая версия дала 46 находок на пяти чужих репозиториях, настоящими оказались только
|
|
22
|
+
пустые `catch {}` в zod; весь питоновский класс `except ImportError: pass` и растовый
|
|
23
|
+
`let _ = write(..)?` были ложными
|
|
@@ -12,7 +12,19 @@
|
|
|
12
12
|
# исключения. Гейт, спорящий о стиле, выключают целиком, поэтому здесь остались только
|
|
13
13
|
# безусловные случаи — те, где тест не может провалиться ни при каких данных.
|
|
14
14
|
DIR="${1:-.}"
|
|
15
|
-
|
|
15
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
16
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
17
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
18
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
19
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
20
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
21
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
22
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
23
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
24
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
25
|
+
exit 2
|
|
26
|
+
fi
|
|
27
|
+
. "$SKIP_LIB"
|
|
16
28
|
|
|
17
29
|
# Только файлы тестов. Слово «assert» в обычном коде — не тест, а проверка входа.
|
|
18
30
|
FILES=$(find "$DIR" $(skip_find) -type f \( \
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Арбитра не правит тот, кто чинит код
|
|
2
|
+
|
|
3
|
+
**Намерение.** Когда проверка краснеет, у пишущего два выхода: починить код или подогнать
|
|
4
|
+
проверку. Второй дешевле и с виду неотличим от первого — прогон зелёный, диф маленький.
|
|
5
|
+
Для агента, которому поставлена задача «сделай, чтобы прошло», это выход по умолчанию.
|
|
6
|
+
|
|
7
|
+
**Откуда взято.** Практики называют этот приём поимённо. В разборе 1154 обсуждений с
|
|
8
|
+
r/programming, r/learnprogramming, **r/ExperiencedDevs** и Hacker News (Baltes, Cheong, Treude,
|
|
9
|
+
[«An Endless Stream of AI Slop»](https://arxiv.org/html/2603.27249v3), январь–сентябрь 2025)
|
|
10
|
+
он идёт в списке конкретных технических приёмов рядом с «casting to `any` to silence type
|
|
11
|
+
errors» и «deleting methods instead of fixing them»:
|
|
12
|
+
|
|
13
|
+
> «Test subversion»: changing tests to pass broken code rather than fixing underlying issues
|
|
14
|
+
|
|
15
|
+
Наше собственное правило `kit/rules/testing.md` требует того же с самого начала — и сторожа не
|
|
16
|
+
имело: «Тот, кто чинит код, не правит тест, который этот код проверяет».
|
|
17
|
+
|
|
18
|
+
**Готовый аналог есть, и он лучше того, что написали бы мы.**
|
|
19
|
+
[`checkwash`](https://github.com/taipei49314/checkwash) (Apache-2.0, чистый stdlib Python, без
|
|
20
|
+
зависимостей, 21 детектор) разбирает диф и находит ослабленные утверждения, снятые проверки,
|
|
21
|
+
подменённые ожидания, изменённые эталонные файлы. Своей проверки здесь нет и не будет —
|
|
22
|
+
это обвязка вокруг него.
|
|
23
|
+
|
|
24
|
+
Инструмент вышел 1 сентября 2026, последняя версия 5 сентября; его собственная документация
|
|
25
|
+
говорит ровно то, что говорим мы: *«checkwash cannot block when it does not run. A change that
|
|
26
|
+
deletes or disables the checkwash job disarms it in the same diff»* — это как раз то, что
|
|
27
|
+
стерегут наши `gates-run-in-ci`, `ci-actually-fails` и `gate-not-weakened`. Мы дополняем друг
|
|
28
|
+
друга, а не соперничаем.
|
|
29
|
+
|
|
30
|
+
**Что добавляет запись — и почему это не мелочь.** Замерено 2026-09-07 на образце: код изменён
|
|
31
|
+
на неверный, три точных утверждения заменены на `is not None`. `checkwash` находит все три —
|
|
32
|
+
и **завершается кодом 0**: он понижает их до `warn` по признаку `REPAIR_EVIDENCE` («в дифе есть
|
|
33
|
+
и правка кода, значит похоже на настоящую починку»). То есть ровно тот случай, ради которого
|
|
34
|
+
запись нужна, по умолчанию проходит зелёным.
|
|
35
|
+
|
|
36
|
+
Поднять порог целиком нельзя — замерено на живой истории `httpx`, последние 40 коммитов:
|
|
37
|
+
|
|
38
|
+
| Порог | Заблокировано | Чем |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| по умолчанию (`high`) | 2.5% | одна настоящая находка, но предмет записи пропускается |
|
|
41
|
+
| `--fail-on warn` | 15% | восемь находок из одиннадцати — `CI_WORKFLOW_TOUCHED`, то есть «диф трогает конвейер» |
|
|
42
|
+
| **отбор по правилу (наш)** | **2.5%** | та же настоящая находка **плюс** подгонка арбитра уровня `warn` |
|
|
43
|
+
|
|
44
|
+
Отбор идёт по правилу: всё уровня `high` и выше, плюс семейство `ASSERT_*` и подмена предмета
|
|
45
|
+
уровня `warn`. Конвейер не наш предмет здесь — его стерегут другие записи.
|
|
46
|
+
|
|
47
|
+
**Чего НЕ ловит.**
|
|
48
|
+
|
|
49
|
+
- **`TEST_DISABLED` уровня `warn` — намеренно.** Инструмент сам различает: тест исчез вместе с
|
|
50
|
+
правкой кода — `warn`, тест исчез без неё — `high` («NO_PROD_CHANGE_IN_DIFF»). Первое обычно
|
|
51
|
+
законная уборка при снятии возможности: в `httpx` коммит «Graceful upgrade path for 0.28»
|
|
52
|
+
удалил два теста вместе с самой возможностью. Поднимать это целиком значит красить каждую
|
|
53
|
+
депрекацию — замер давал 5% вместо 2.5%, и лишняя половина была именно такой. Второе и так
|
|
54
|
+
`high` и потому попадает под общее правило.
|
|
55
|
+
- **Тест, который был слабым с самого начала.** Запись смотрит на изменение, а не на
|
|
56
|
+
качество: тест, родившийся тавтологией, — предмет `test-has-assertion`.
|
|
57
|
+
- **Подгонку через данные.** Правку фикстуры или эталонного файла `checkwash` умеет, но наша
|
|
58
|
+
обвязка её не поднимает выше порога инструмента.
|
|
59
|
+
- **Правку свода правил — намеренно.** `checkwash` поднимает `GUARDRAIL_TOUCHED` до `critical`
|
|
60
|
+
на любое изменение `AGENTS.md`, `CLAUDE.md`, `.claude/**`. Для нас это худшее из возможных
|
|
61
|
+
ложных срабатываний: свод — ровно тот файл, ради правки которого комплект существует, его
|
|
62
|
+
пишет `aqk init`. Отбор идёт **по имени правила**, а не по уровню, и это правило в список не
|
|
63
|
+
входит. Найдено код-ревью, а не замером: в `httpx` файлов инструкций агенту нет вовсе.
|
|
64
|
+
- **Файл, который инструмент не смог разобрать.** `TEST_FILE_UNPARSEABLE` остаётся `warn` и в
|
|
65
|
+
наш список не входит: часть дифа окажется неразобранной, а гейт промолчит. Ошибки настройки
|
|
66
|
+
самого инструмента мы, наоборот, поднимаем в отказ (код 2).
|
|
67
|
+
- **Разрешённое исключение.** Находку, записанную командой `checkwash allow` и попавшую в
|
|
68
|
+
реестр на базовой стороне дифа, мы не красим. Иначе у команды, разобравшей случай глазами,
|
|
69
|
+
не остаётся выхода, кроме как выключить проверку целиком. Проверено сквозным прогоном:
|
|
70
|
+
из трёх находок красного образца после записи исключения остаётся одна.
|
|
71
|
+
- **Проекты без Python.** `checkwash` ставится через `pip`. Запись объявляет это полем
|
|
72
|
+
`requires`, и `add` отказывает с названной причиной, а не ставит гейт, который встанет
|
|
73
|
+
с «not found».
|
|
74
|
+
|
|
75
|
+
**Образцы.** Настоящей истории git внутри каталога комплекта взять неоткуда, поэтому образец —
|
|
76
|
+
это две папки, `before/` и `after/`; проверка собирает из них одноразовый репозиторий с двумя
|
|
77
|
+
коммитами. `red/` — код изменён на неверный, три утверждения заменены на `is not None`.
|
|
78
|
+
`green/` — код и тест выросли вместе: добавлена функция и тест к ней, ни одно утверждение не
|
|
79
|
+
убрано и не ослаблено.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Арбитра правит не тот, кто чинит код. Проверку выполняет `checkwash` — готовый инструмент,
|
|
3
|
+
# который это умеет лучше, чем сумели бы мы. Здесь только обвязка: выбор диапазона, режим
|
|
4
|
+
# образцов и строка совета.
|
|
5
|
+
#
|
|
6
|
+
# ЗАЧЕМ ЭТО ВООБЩЕ. «Test subversion» — правка теста, чтобы прошёл сломанный код, — один из
|
|
7
|
+
# приёмов, которые практики называют поимённо в разборе 1154 обсуждений с Reddit и Hacker News
|
|
8
|
+
# (Baltes, Cheong, Treude, «An Endless Stream of AI Slop», arxiv 2603.27249). Наше собственное
|
|
9
|
+
# правило `kit/rules/testing.md` требует того же и сторожа не имело.
|
|
10
|
+
#
|
|
11
|
+
# ПОЧЕМУ НЕ `--fail-on warn` И НЕ ПОРОГ ПО УМОЛЧАНИЮ. Измерено 2026-09-07 на живой истории
|
|
12
|
+
# `httpx`, последние 40 коммитов:
|
|
13
|
+
# порог по умолчанию (high) — блокирует 2.5% коммитов, и та единственная блокировка настоящая;
|
|
14
|
+
# `--fail-on warn` — блокирует 15%, и восемь из одиннадцати находок уровня warn это
|
|
15
|
+
# `CI_WORKFLOW_TOUCHED`, то есть «диф трогает конвейер» вообще.
|
|
16
|
+
# Поднять порог целиком значит красить любую правку конвейера. Поэтому отбор идёт ПО ПРАВИЛУ:
|
|
17
|
+
# всё, что high и выше, плюс подгонка арбитра уровня warn. Именно её инструмент понижает по
|
|
18
|
+
# признаку REPAIR_EVIDENCE — «в дифе есть и правка кода, значит похоже на настоящую починку», —
|
|
19
|
+
# а это ровно тот случай, ради которого запись и заведена: код сломали, тест подогнали.
|
|
20
|
+
# Конвейер здесь не наш предмет: его стерегут `ci-actually-fails` и `gates-run-in-ci`.
|
|
21
|
+
#
|
|
22
|
+
# ОТБОР ИДЁТ ПО ИМЕНИ ПРАВИЛА, А НЕ ПО УРОВНЮ. Первая версия блокировала всё уровня high и выше
|
|
23
|
+
# «на всякий случай» — и ловила `GUARDRAIL_TOUCHED`, который инструмент поднимает до critical на
|
|
24
|
+
# ЛЮБУЮ правку `AGENTS.md`, `CLAUDE.md`, `.claude/**`. Для нас это худшее из возможных ложных
|
|
25
|
+
# срабатываний: свод правил — ровно тот файл, ради правки которого комплект и существует, его
|
|
26
|
+
# пишет `aqk init`. Найдено код-ревью 2026-09-07; замер по `httpx` этого показать не мог —
|
|
27
|
+
# там нет файлов инструкций агенту вовсе.
|
|
28
|
+
#
|
|
29
|
+
# ALWAYS — подгонка арбитра: красим на любом уровне. Именно её инструмент понижает до `warn`
|
|
30
|
+
# по признаку REPAIR_EVIDENCE, и именно она предмет этой записи.
|
|
31
|
+
ALWAYS='ASSERT_REMOVED|ASSERT_WEAKENED|ASSERT_SUBSTITUTED|TEST_PATCHES_SUBJECT|SUBJECT_NORMALIZED|CONFTEST_PATCHES_PROD'
|
|
32
|
+
#
|
|
33
|
+
# HIGH_ONLY — тест исчез. Инструмент сам различает: вместе с правкой кода — `warn` (обычно
|
|
34
|
+
# законная уборка при снятии возможности, так было в коммите `httpx` «Graceful upgrade path for
|
|
35
|
+
# 0.28»); без правки кода — `high` («NO_PROD_CHANGE_IN_DIFF»), и это чистое снятие сигнала.
|
|
36
|
+
# Поднимать первое значит красить каждую депрекацию: замер давал 5% заблокированных коммитов
|
|
37
|
+
# вместо 2.5%, и лишняя половина была именно такой.
|
|
38
|
+
HIGH_ONLY='TEST_DISABLED'
|
|
39
|
+
#
|
|
40
|
+
# Всё остальное — не наш предмет. `CI_WORKFLOW_TOUCHED` стерегут `ci-actually-fails` и
|
|
41
|
+
# `gates-run-in-ci`; `GUARDRAIL_TOUCHED` не стережёт никто, и это правильно: правка свода
|
|
42
|
+
# правил — обычная работа, а не подгонка арбитра.
|
|
43
|
+
|
|
44
|
+
DIR="${1:-.}"
|
|
45
|
+
|
|
46
|
+
# Режим образца: рядом лежат before/ и after/. Настоящей истории git внутри каталога комплекта
|
|
47
|
+
# взять неоткуда, а вложенный .git создал бы embedded-репозиторий. Собираем одноразовый.
|
|
48
|
+
if [ -d "$DIR/before" ] && [ -d "$DIR/after" ]; then
|
|
49
|
+
T=$(mktemp -d) || exit 2
|
|
50
|
+
cp -R "$DIR/before/." "$T/" 2>/dev/null
|
|
51
|
+
( cd "$T" && git init -q . && git config user.email aqk@example && git config user.name aqk &&
|
|
52
|
+
git add -A && git commit -qm "before" ) >/dev/null 2>&1
|
|
53
|
+
find "$T" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} + 2>/dev/null
|
|
54
|
+
cp -R "$DIR/after/." "$T/" 2>/dev/null
|
|
55
|
+
( cd "$T" && git add -A && git commit -qm "after" ) >/dev/null 2>&1
|
|
56
|
+
REPO="$T"; RANGE="HEAD~1..HEAD"
|
|
57
|
+
else
|
|
58
|
+
if ! (cd "$DIR" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1); then
|
|
59
|
+
echo "не git-репозиторий — проверять нечего"
|
|
60
|
+
exit 0
|
|
61
|
+
fi
|
|
62
|
+
# Диапазон: по умолчанию последний коммит. Проект может назвать свой — так же, как это
|
|
63
|
+
# делает `doctor --since`.
|
|
64
|
+
RANGE="${AQK_TEST_RANGE:-HEAD~1..HEAD}"
|
|
65
|
+
# Проверяем ТОТ диапазон, который сейчас используется, а не всегда `HEAD~1`. Проект, назвавший
|
|
66
|
+
# `AQK_TEST_RANGE=origin/main..HEAD`, получал бы «меньше двух коммитов» на клоне без `HEAD~1`,
|
|
67
|
+
# хотя его диапазон полностью разрешим. Найдено код-ревью 2026-09-07.
|
|
68
|
+
BASE="${RANGE%%..*}"
|
|
69
|
+
if ! (cd "$DIR" && git rev-parse -q --verify "$BASE" >/dev/null 2>&1); then
|
|
70
|
+
echo "$BASE не разрешается — сравнивать не с чем, проверка пропущена"
|
|
71
|
+
echo " почини: дай конвейеру историю глубже одного коммита —"
|
|
72
|
+
echo " actions/checkout@v4 с fetch-depth: 2, либо назови свой диапазон в AQK_TEST_RANGE."
|
|
73
|
+
exit 0
|
|
74
|
+
fi
|
|
75
|
+
REPO="$DIR"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
if ! command -v checkwash >/dev/null 2>&1; then
|
|
79
|
+
[ -n "${T:-}" ] && rm -rf "$T"
|
|
80
|
+
echo "checkwash не установлен — проверять нечем"
|
|
81
|
+
echo " почини: pip install checkwash — запись делегирует ему целиком, своей проверки у неё нет."
|
|
82
|
+
exit 2
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
# `--fail-on info` — чтобы инструмент отдал ВСЁ, что нашёл; решение принимаем сами, ниже.
|
|
86
|
+
JSON=$(checkwash check "$RANGE" --repo "$REPO" --format json --fail-on info 2>/dev/null)
|
|
87
|
+
[ -n "${T:-}" ] && rm -rf "$T"
|
|
88
|
+
|
|
89
|
+
if [ -z "$JSON" ]; then
|
|
90
|
+
echo "checkwash не дал разбора для $RANGE"
|
|
91
|
+
echo " почини: запусти команду руками и посмотри, почему она молчит."
|
|
92
|
+
echo " «не смогли проверить» и «нарушений нет» дают одинаково пустой список и разные выводы."
|
|
93
|
+
exit 2
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# Разбор вывода: json печатается по полю на строку, поэтому пары «rule/severity» собираются
|
|
97
|
+
# построчно. Своего разбора json мы не пишем — нужны два поля, а не структура.
|
|
98
|
+
# Ошибки разбора самого инструмента — это «не смогли проверить», а не «нарушений нет».
|
|
99
|
+
if printf '%s\n' "$JSON" | grep -q '"config_errors": \[$' &&
|
|
100
|
+
printf '%s\n' "$JSON" | sed -n '/"config_errors": \[/,/\]/p' | grep -q '"'; then
|
|
101
|
+
printf '%s\n' "$JSON" | sed -n '/"config_errors": \[/,/\]/p'
|
|
102
|
+
echo " почини: у checkwash ошибка в настройке — запусти его руками и прочти вывод."
|
|
103
|
+
exit 2
|
|
104
|
+
fi
|
|
105
|
+
|
|
106
|
+
# Разбор вывода: json печатается по полю на строку, поэтому поля находки собираются построчно.
|
|
107
|
+
# Своего разбора json мы не пишем — нужны пять полей, а не структура.
|
|
108
|
+
FOUND=$(printf '%s\n' "$JSON" | awk -v always="$ALWAYS" -v highonly="$HIGH_ONLY" '
|
|
109
|
+
/"allowlisted":/ { al = ($0 ~ /true/) }
|
|
110
|
+
/"rule":/ { r = $0; sub(/.*"rule":[[:space:]]*"/, "", r); sub(/".*/, "", r) }
|
|
111
|
+
/"severity":/ { s = $0; sub(/.*"severity":[[:space:]]*"/, "", s); sub(/".*/, "", s) }
|
|
112
|
+
/"path":/ { p = $0; sub(/.*"path":[[:space:]]*"/, "", p); sub(/".*/, "", p) }
|
|
113
|
+
/"message":/ { m = $0; sub(/.*"message":[[:space:]]*"/, "", m); sub(/".*/, "", m) }
|
|
114
|
+
# Печатаем на `unit` — ПОСЛЕДНЕМ поле находки. Поля идут по алфавиту, и `message` стоит
|
|
115
|
+
# РАНЬШЕ `rule` и `severity`: печать по сообщению подписывала правило от предыдущей находки.
|
|
116
|
+
/"unit":/ {
|
|
117
|
+
u = $0
|
|
118
|
+
sub(/.*"unit":[[:space:]]*/, "", u) # значение бывает и `null` без кавычек
|
|
119
|
+
gsub(/^[",[:space:]]+|[",[:space:]]+$/, "", u)
|
|
120
|
+
if (u == "null") u = ""
|
|
121
|
+
if (r != "") {
|
|
122
|
+
# Разрешённое исключение уже разобрано человеком и записано в реестр инструмента со
|
|
123
|
+
# сроком. Красить его значит не оставить выхода, кроме как выключить проверку целиком.
|
|
124
|
+
blocking = !al && (r ~ ("^(" always ")$") ||
|
|
125
|
+
((s == "high" || s == "critical") && r ~ ("^(" highonly ")$")))
|
|
126
|
+
if (blocking) printf "%s:%s: %s — %s [%s]\n", p, (u == "" ? "0" : u), r, m, s
|
|
127
|
+
}
|
|
128
|
+
r = ""; s = ""; p = ""; u = ""; m = ""; al = 0
|
|
129
|
+
}
|
|
130
|
+
')
|
|
131
|
+
|
|
132
|
+
[ -z "$FOUND" ] && exit 0
|
|
133
|
+
printf '%s\n' "$FOUND"
|
|
134
|
+
echo " почини: верни утверждение на место, а код приведи в соответствие с ним."
|
|
135
|
+
echo " тест, подогнанный под сломанный код, — это зелёный прогон без основания."
|
|
136
|
+
exit 1
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
intent: арбитра не правит тот, кто чинит код — утверждение не ослабляют, чтобы прошло
|
|
2
|
+
intent_en: whoever fixes the code does not adjust the arbiter — assertions are not weakened to pass
|
|
3
|
+
|
|
4
|
+
# Там, где есть тесты. Без них подгонять нечего.
|
|
5
|
+
trigger:
|
|
6
|
+
has_tests: true
|
|
7
|
+
|
|
8
|
+
recipes:
|
|
9
|
+
any: bash {gate}/check.sh {dir}
|
|
10
|
+
|
|
11
|
+
# Программа, без которой запись не работает. По первому слову команды этого не видно: обёртка
|
|
12
|
+
# начинается с `bash`, который есть всегда, и гейт ставился бы, а вставал при первом запуске.
|
|
13
|
+
requires: checkwash
|
|
14
|
+
|
|
15
|
+
proof: incidents/README.md, 2026-09-07 «арбитра правит тот, кто чинит код» — приём назван
|
|
16
|
+
практиками в разборе 1154 обсуждений с Reddit и Hacker News (Baltes, Cheong, Treude,
|
|
17
|
+
arxiv 2603.27249, «test subversion»); замер обвязки по 40 коммитам `httpx` — одна блокировка,
|
|
18
|
+
настоящая, при 15% на пороге `--fail-on warn` и 2.5% на пороге по умолчанию, который пропускает
|
|
19
|
+
сам предмет записи
|
|
@@ -23,4 +23,10 @@
|
|
|
23
23
|
поэтому рецепт под эти стеки берёт их. Своя проверка остаётся как запасная — для языков, где
|
|
24
24
|
готового правила нет.
|
|
25
25
|
|
|
26
|
+
**Замер оправдал переносимую проверку.** Пять чужих репозиториев (`httpx`, `fastapi`, `zod`,
|
|
27
|
+
`cobra`, `ripgrep`) — 51 находка, ложных ноль: маркер `TODO` либо стоит в коде, либо нет,
|
|
28
|
+
двусмысленности здесь нет. На той же ревизии 2026-09-07 две соседние записи лишились своих
|
|
29
|
+
переносимых проверок именно из-за замера — эта его прошла, и потому осталась. В `cobra` и
|
|
30
|
+
`ripgrep`, где `ruff` и eslint неприменимы, она единственная, кто эти маркеры видит.
|
|
31
|
+
|
|
26
32
|
**Образцы.** `red/` — код с двумя маркерами. `green/` — тот же код, задача заведена, маркера нет.
|
|
@@ -2,14 +2,26 @@
|
|
|
2
2
|
# Маркер «доделать потом» — это задача, спрятанная от очереди работ. Её не видно при
|
|
3
3
|
# планировании, о ней не знает никто, кроме того, кто её оставил, и она переживает автора.
|
|
4
4
|
DIR="${1:-.}"
|
|
5
|
-
|
|
5
|
+
# Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
|
|
6
|
+
# `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
|
|
7
|
+
# неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
|
|
8
|
+
# `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
|
|
9
|
+
# ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
|
|
10
|
+
# выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
|
|
11
|
+
SKIP_LIB="$(dirname "$0")/../_skip.sh"
|
|
12
|
+
if [ ! -f "$SKIP_LIB" ]; then
|
|
13
|
+
echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
|
|
14
|
+
echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
|
|
15
|
+
exit 2
|
|
16
|
+
fi
|
|
17
|
+
. "$SKIP_LIB"
|
|
6
18
|
|
|
7
19
|
# shellcheck disable=SC2086
|
|
8
20
|
# Маркер обязан стоять В КОММЕНТАРИИ. Иначе гейт краснеет на имени переменной с таким же
|
|
9
21
|
# названием и на собственном шаблоне поиска — на том, что дефектом не является.
|
|
10
22
|
# Сами эти слова здесь не пишем: гейт нашёл бы себя. Проверено — находил.
|
|
11
23
|
HITS=$(grep -rnE $(skip_grep "$DIR") $(include_code) \
|
|
12
|
-
'(#|//|/\*|--|<!--)[^"'"'"']*(^|[^A-Za-
|
|
24
|
+
'(#|//|/\*|--|<!--)[^"'"'"']*(^|[^A-Za-z0-9_])(TODO|FIXME|HACK|XXX)([^A-Za-z0-9_]|$)' "$DIR" 2>/dev/null | own_samples_filter "$DIR")
|
|
13
25
|
if [ -n "$HITS" ]; then
|
|
14
26
|
echo "$HITS"
|
|
15
27
|
echo " почини: заведи задачу в очереди работ, маркер убери."
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
UV_TEST_XXX = "имя переменной, а не маркер долга"
|