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
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -25,6 +25,13 @@ FAIL=0
|
|
|
25
25
|
ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; PASS=$((PASS + 1)); }
|
|
26
26
|
bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; printf ' %s\n' "${2:-}"; FAIL=$((FAIL + 1)); }
|
|
27
27
|
|
|
28
|
+
# Node на Windows видит мир глазами Windows, а Git Bash — глазами POSIX: путь вида
|
|
29
|
+
# /tmp/tmp.XXXX, отданный в `node -e`, там не существует, и проверка падает не на том, что
|
|
30
|
+
# проверяет. Поймано дважды на windows-прогоне — сперва на `learn`, потом на установке хука.
|
|
31
|
+
# Поэтому node запускается ИЗ каталога и получает относительный путь: помощник, а не памятка,
|
|
32
|
+
# потому что памятку третий раз забудут ровно так же, как забыли второй.
|
|
33
|
+
node_in() { D="$1"; shift; ( cd "$D" && node "$@" ); }
|
|
34
|
+
|
|
28
35
|
WORK="$(mktemp -d)"
|
|
29
36
|
trap 'rm -rf "$WORK"' EXIT
|
|
30
37
|
|
|
@@ -164,12 +171,18 @@ done
|
|
|
164
171
|
# --- 8. храповик: старое пропускает, новое не пускает ------------------------
|
|
165
172
|
# Главный вопрос к храповику: «может ли новый код добавить нарушение и пройти?»
|
|
166
173
|
# Может — значит это советчик, а не гейт.
|
|
174
|
+
# ПОЧЕМУ ПРИМАНКА ИМЕННО gate-not-weakened. У записи должен быть ТОЛЬКО переносимый рецепт:
|
|
175
|
+
# иначе на машине, где стоит ruff или eslint, установка возьмёт рецепт под язык, и проверка
|
|
176
|
+
# станет печатать чужой формат вывода. Так и вышло — прогон был зелёным локально и красным в
|
|
177
|
+
# конвейере ровно потому, что в конвейер добавили ruff: четыре проверки искали в выводе наши
|
|
178
|
+
# «почини: …» и «путь:строка», а получали формат ruff. Прогон, чей исход зависит от того, что
|
|
179
|
+
# случайно стоит на машине, не проверяет ничего.
|
|
167
180
|
R="$WORK/ratchet"; mkdir -p "$R"; cd "$R" || exit 1
|
|
168
181
|
git init -q .
|
|
169
|
-
printf '
|
|
182
|
+
printf 'x = 1 # noqa\n' > old.py
|
|
170
183
|
node "$CLI" init >/dev/null 2>&1
|
|
171
|
-
node "$CLI" add
|
|
172
|
-
node "$CLI" ratchet
|
|
184
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
185
|
+
node "$CLI" ratchet gate-not-weakened >/dev/null 2>&1
|
|
173
186
|
|
|
174
187
|
# Судим по вердикту гейта, а не по коду возврата doctor: он ненулевой и по другим
|
|
175
188
|
# причинам (в свежей папке нет .gitignore), и проверка бы врала о храповике.
|
|
@@ -179,7 +192,7 @@ case "$OUT" in
|
|
|
179
192
|
*) ok "храповик пропустил старое нарушение" ;;
|
|
180
193
|
esac
|
|
181
194
|
|
|
182
|
-
printf '
|
|
195
|
+
printf 'y = 2 # noqa\n' > new.py
|
|
183
196
|
OUT="$(node "$CLI" doctor --run 2>&1)"
|
|
184
197
|
case "$OUT" in
|
|
185
198
|
*"новых нарушений"*) ok "храповик не пустил новое нарушение" ;;
|
|
@@ -189,7 +202,7 @@ esac
|
|
|
189
202
|
# Второй прогон с тем же новым нарушением обязан краснеть так же. Пока реестр перезаписывался
|
|
190
203
|
# всем текущим списком, одно исправленное нарушение затягивало в долг ВСЕ новые: один красный
|
|
191
204
|
# прогон — и дальше зелено навсегда. «Может ли новый код добавить нарушение и пройти?» — мог.
|
|
192
|
-
printf '
|
|
205
|
+
printf 'z = 3 # noqa\n' > another.py
|
|
193
206
|
node "$CLI" doctor --run >/dev/null 2>&1
|
|
194
207
|
rm old.py
|
|
195
208
|
OUT="$(node "$CLI" doctor --run 2>&1)"
|
|
@@ -199,12 +212,12 @@ case "$OUT2" in
|
|
|
199
212
|
*) bad "исправление одного нарушения затянуло новые в долг" "второй прогон зелёный" ;;
|
|
200
213
|
esac
|
|
201
214
|
rm -f another.py
|
|
202
|
-
printf '
|
|
203
|
-
node "$CLI" ratchet
|
|
215
|
+
printf 'x = 1 # noqa\n' > old.py
|
|
216
|
+
node "$CLI" ratchet gate-not-weakened >/dev/null 2>&1 || true
|
|
204
217
|
|
|
205
218
|
rm -f new.py old.py
|
|
206
219
|
node "$CLI" doctor --run >/dev/null 2>&1
|
|
207
|
-
if grep -q 'old.py' ratchets/
|
|
220
|
+
if grep -q 'old.py' ratchets/gate-not-weakened.txt; then
|
|
208
221
|
bad "исправленное осталось в реестре — храповик не затягивается"
|
|
209
222
|
else
|
|
210
223
|
ok "исправленное вычеркнуто из реестра"
|
|
@@ -250,27 +263,13 @@ case "$OUT" in
|
|
|
250
263
|
esac
|
|
251
264
|
cd "$WORK" || exit 1
|
|
252
265
|
|
|
253
|
-
# --- 8б. проект называет свои каталоги, где печать — интерфейс ---------------
|
|
254
|
-
# Исключение объявляется В МАНИФЕСТЕ и потому видно глазами. Проверяем обе стороны: названный
|
|
255
|
-
# каталог пропускается, все остальные — нет. Исключение, которое прячет всё, бесполезно.
|
|
256
|
-
P="$WORK/printok"; mkdir -p "$P/cli" "$P/src"; cd "$P" || exit 1
|
|
257
|
-
printf 'def a():\n print("вывод программы")\n' > cli/main.py
|
|
258
|
-
printf 'def b():\n print("забытая отладка")\n' > src/service.py
|
|
259
|
-
OUT="$(AQK_PRINT_OK_DIRS=cli bash "$ROOT/kit/gates/no-print-in-prod/check.sh" . 2>&1)"
|
|
260
|
-
case "$OUT" in
|
|
261
|
-
*"cli/main.py"*) bad "названный каталог не исключён" "$OUT" ;;
|
|
262
|
-
*"src/service.py"*) ok "названный каталог пропущен, остальные проверяются" ;;
|
|
263
|
-
*) bad "проверка печати не нашла отладку вне названного каталога" "$OUT" ;;
|
|
264
|
-
esac
|
|
265
|
-
cd "$WORK" || exit 1
|
|
266
|
-
|
|
267
266
|
# --- 9. разбор ошибки: три случая различаются прогоном, а не памятью ---------
|
|
268
267
|
# «Сторожа не было», «сторож был и не сработал», «сторож был и его обошли» — разные починки.
|
|
269
268
|
# Пока их различает человек по памяти, чинят обычно не то. Здесь их различает прогон.
|
|
270
269
|
Y="$WORK/why"; mkdir -p "$Y"; cd "$Y" || exit 1
|
|
271
270
|
git init -q .
|
|
272
271
|
node "$CLI" init >/dev/null 2>&1
|
|
273
|
-
printf '
|
|
272
|
+
printf 'q = 1 # noqa\n' > x.py
|
|
274
273
|
|
|
275
274
|
OUT="$(node "$CLI" why "миграция базы применена задом наперёд" 2>&1)"
|
|
276
275
|
case "$OUT" in
|
|
@@ -278,21 +277,21 @@ case "$OUT" in
|
|
|
278
277
|
*) bad "why не сказал, что сторожа не было" "$OUT" ;;
|
|
279
278
|
esac
|
|
280
279
|
|
|
281
|
-
OUT="$(node "$CLI" why "
|
|
280
|
+
OUT="$(node "$CLI" why "подавление проверки целиком без причины" 2>&1)"
|
|
282
281
|
case "$OUT" in
|
|
283
282
|
*"не поставлен"*) ok "why: запись есть в каталоге, но в проекте не поставлена" ;;
|
|
284
283
|
*) bad "why не отличил «не поставлен» от «не было»" "$OUT" ;;
|
|
285
284
|
esac
|
|
286
285
|
|
|
287
|
-
node "$CLI" add
|
|
288
|
-
OUT="$(node "$CLI" why "
|
|
286
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
287
|
+
OUT="$(node "$CLI" why "подавление проверки целиком без причины" 2>&1)"
|
|
289
288
|
case "$OUT" in
|
|
290
289
|
*"его обошли"*) ok "why: сторож стоит и ловит — значит его обошли" ;;
|
|
291
290
|
*) bad "why не отличил «обошли» от «не сработал»" "$OUT" ;;
|
|
292
291
|
esac
|
|
293
292
|
|
|
294
293
|
rm x.py
|
|
295
|
-
OUT="$(node "$CLI" why "
|
|
294
|
+
OUT="$(node "$CLI" why "подавление проверки целиком без причины" 2>&1)"
|
|
296
295
|
case "$OUT" in
|
|
297
296
|
*"этой поломки не видит"*) ok "why: сторож стоит, а поломки не видит" ;;
|
|
298
297
|
*) bad "why не отличил «не сработал» от «обошли»" "$OUT" ;;
|
|
@@ -579,11 +578,11 @@ rm -rf "$FBDIR" "$FBPROJ"
|
|
|
579
578
|
REPDIR="$(mktemp -d)"
|
|
580
579
|
(
|
|
581
580
|
cd "$REPDIR" && git init -q . && mkdir -p src &&
|
|
582
|
-
printf '
|
|
581
|
+
printf 'a = 1 # noqa\n' > src/a.py &&
|
|
583
582
|
node "$CLI" start > /tmp/aqk-start.log 2>&1
|
|
584
583
|
)
|
|
585
584
|
REP_OUT=$( cd "$REPDIR" && node "$CLI" report 2>&1 ); REP_CODE=$?
|
|
586
|
-
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌
|
|
585
|
+
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌ gate-not-weakened'; then
|
|
587
586
|
ok "report краснеет кодом возврата и называет упавший гейт"
|
|
588
587
|
else
|
|
589
588
|
# Код возврата отчёта не говорит, ПОЧЕМУ он ноль: гейт не сработал, не установился или
|
|
@@ -591,7 +590,7 @@ else
|
|
|
591
590
|
# и «проверка не ловит на этой системе».
|
|
592
591
|
G_LS=$( cd "$REPDIR" && ls gates 2>&1 | tr '\n' ' ' )
|
|
593
592
|
G_DECL=$( cd "$REPDIR" && sed -n '/^gates:/,$p' .aqk.yml 2>/dev/null | grep -cE '^[[:space:]]+[A-Za-z0-9_-]+:' )
|
|
594
|
-
G_OUT=$( cd "$REPDIR" && bash gates/
|
|
593
|
+
G_OUT=$( cd "$REPDIR" && bash gates/gate-not-weakened/check.sh . 2>&1 | head -2 ); G_CODE=$?
|
|
595
594
|
bad "report не отличает красное от зелёного" "код отчёта $REP_CODE; гейт напрямую: код $G_CODE, вывод «$(printf '%s' "$G_OUT" | tr '\n' ' ')»; в gates/: «$G_LS»; объявлено гейтов: $G_DECL; хвост start: «$(tail -4 /tmp/aqk-start.log 2>/dev/null | tr '\n' ' ')»"
|
|
596
595
|
fi
|
|
597
596
|
if [ -f "$REPDIR/.aqk/report.md" ] && grep -q '^## ' "$REPDIR/.aqk/report.md"; then
|
|
@@ -631,11 +630,11 @@ rm -rf "$NOTEDIR" "$NOTEHOME"
|
|
|
631
630
|
# то есть настройка правкой чужого файла, которую затрёт следующий `aqk add`.
|
|
632
631
|
IGNDIR="$(mktemp -d)"
|
|
633
632
|
mkdir -p "$IGNDIR/third-party/inner" "$IGNDIR/src"
|
|
634
|
-
printf '
|
|
635
|
-
printf '
|
|
636
|
-
OUT_BEFORE="$(bash "$ROOT/kit/gates/
|
|
633
|
+
printf 'mine = 1 # noqa\n' > "$IGNDIR/src/mine.py"
|
|
634
|
+
printf 'theirs = 1 # noqa\n' > "$IGNDIR/third-party/inner/theirs.py"
|
|
635
|
+
OUT_BEFORE="$(bash "$ROOT/kit/gates/gate-not-weakened/check.sh" "$IGNDIR" 2>&1)"
|
|
637
636
|
printf '# принесено из другого репозитория\nthird-party/\n' > "$IGNDIR/.aqkignore"
|
|
638
|
-
OUT_AFTER="$(bash "$ROOT/kit/gates/
|
|
637
|
+
OUT_AFTER="$(bash "$ROOT/kit/gates/gate-not-weakened/check.sh" "$IGNDIR" 2>&1)"
|
|
639
638
|
if printf '%s' "$OUT_BEFORE" | grep -q 'theirs.py' &&
|
|
640
639
|
! printf '%s' "$OUT_AFTER" | grep -q 'theirs.py' &&
|
|
641
640
|
printf '%s' "$OUT_AFTER" | grep -q 'mine.py'; then
|
|
@@ -893,17 +892,104 @@ rm -rf "$BDIR2"
|
|
|
893
892
|
# Воспроизводим без Windows: урезаем PATH до одного node — инструментов не видно так же.
|
|
894
893
|
NRDIR="$(mktemp -d)"; NRBIN="$(mktemp -d)"
|
|
895
894
|
ln -sf "$(command -v node)" "$NRBIN/node"
|
|
896
|
-
( cd "$NRDIR" && git init -q . && mkdir -p src && printf '
|
|
895
|
+
( cd "$NRDIR" && git init -q . && mkdir -p src && printf 'a = 1 # noqa\n' > src/a.py )
|
|
897
896
|
NR_OUT=$( cd "$NRDIR" && PATH="$NRBIN" node "$CLI" start 2>&1 ); NR_CODE=$?
|
|
898
897
|
NR_GATES=$( ls "$NRDIR/gates" 2>/dev/null | grep -cv '^_' )
|
|
899
898
|
if [ "$NR_CODE" -eq 0 ] && [ "$NR_GATES" -ge 5 ] &&
|
|
900
|
-
[ -f "$NRDIR/gates/
|
|
899
|
+
[ -f "$NRDIR/gates/gate-not-weakened/check.sh" ]; then
|
|
901
900
|
ok "start пропускает запись без пригодного инструмента и ставит остальные ($NR_GATES)"
|
|
902
901
|
else
|
|
903
902
|
bad "start бросил установку из-за одной записи" "код $NR_CODE, поставлено $NR_GATES, хвост: $(printf '%s' "$NR_OUT" | tail -2 | tr '\n' ' ')"
|
|
904
903
|
fi
|
|
905
904
|
rm -rf "$NRDIR" "$NRBIN"
|
|
906
905
|
|
|
906
|
+
# --- 48г. подделка не берёт уровень выше первого -------------------------------
|
|
907
|
+
# ЗАЧЕМ. Ступень AQK-2 называлась «гейты доказаны» и проверяла, что существуют две папки.
|
|
908
|
+
# Проект с гейтами `true` — командой, которая всегда отвечает «ок», — проходил порог AQK-3 и
|
|
909
|
+
# получал зелёный значок в README. Проверено прогоном 2026-09-07: три «гейта», ноль защиты,
|
|
910
|
+
# высший уровень. Это `pytest || true` на уровне всего стандарта.
|
|
911
|
+
FAKEDIR="$(mktemp -d)"
|
|
912
|
+
(
|
|
913
|
+
cd "$FAKEDIR" && git init -q .
|
|
914
|
+
mkdir -p gates ratchets incidents && printf '# Журнал\n' > incidents/README.md
|
|
915
|
+
# Манифест пишется целиком, а не правится python-ом: проверка, которая на машине без python3
|
|
916
|
+
# молча собирает ДРУГОЙ проект, доказывает не то, что называет. Найдено код-ревью 2026-09-07.
|
|
917
|
+
{
|
|
918
|
+
echo 'aqk: "0.6.0"'
|
|
919
|
+
echo 'entry:'
|
|
920
|
+
echo ' - AGENTS.md'
|
|
921
|
+
echo 'rules: .aqk/rules'
|
|
922
|
+
echo 'gates:'
|
|
923
|
+
echo ' lint: "true"'
|
|
924
|
+
echo ' test: "true"'
|
|
925
|
+
echo ' security: "true"'
|
|
926
|
+
echo 'samples: gates'
|
|
927
|
+
echo 'ratchets: ratchets'
|
|
928
|
+
echo 'lessons: incidents'
|
|
929
|
+
} > .aqk.yml
|
|
930
|
+
printf '# Свод\n' > AGENTS.md
|
|
931
|
+
mkdir -p .aqk/rules && printf '# правила\n' > .aqk/rules/general.md
|
|
932
|
+
)
|
|
933
|
+
FAKE_OUT=$( cd "$FAKEDIR" && node "$CLI" doctor --run --min 3 2>&1 ); FAKE_CODE=$?
|
|
934
|
+
FAKE_PROVE=$( cd "$FAKEDIR" && node "$CLI" prove 2>&1 ); FAKE_PCODE=$?
|
|
935
|
+
# Проверяем ИМЕННО ту ступень, что назначена: без этого `grep 'AQK-1|AQK-3'` совпадал всегда,
|
|
936
|
+
# потому что doctor печатает все четыре строки в любом исходе. Пустая проверка хуже отсутствующей.
|
|
937
|
+
if [ "$FAKE_CODE" -ne 0 ] && [ "$FAKE_PCODE" -ne 0 ] &&
|
|
938
|
+
printf '%s' "$FAKE_OUT" | grep -qE 'AQK-1\.?$|AQK-1[^0-9]' &&
|
|
939
|
+
printf '%s' "$FAKE_OUT" | grep -qiE '(НЕ пройден|not passed|not reached)'; then
|
|
940
|
+
ok "гейт «true» не берёт уровень выше первого"
|
|
941
|
+
else
|
|
942
|
+
bad "подделка получила уровень" "порог: код $FAKE_CODE, доказательство: код $FAKE_PCODE, хвост: $(printf '%s' "$FAKE_OUT" | tail -2 | tr '\n' ' ')"
|
|
943
|
+
fi
|
|
944
|
+
rm -rf "$FAKEDIR"
|
|
945
|
+
|
|
946
|
+
# --- 48в. отказ установки называет, что поставить ------------------------------
|
|
947
|
+
# ЗАЧЕМ. Запись, у которой остался только рецепт под язык (`no-print-in-prod`, `swallowed-error`,
|
|
948
|
+
# `dead-code`), на машине без нужного инструмента ставиться не может — и это законно. Но отказ
|
|
949
|
+
# обязан быть действием, а не тупиком: «нет команды ни под python, ни общей» не говорит человеку
|
|
950
|
+
# ничего. Правило «находка без действия закрывает окно, а не дефект» — то же самое, что мы
|
|
951
|
+
# требуем от записей каталога, и к собственной программе оно относится так же.
|
|
952
|
+
# Найдено первым прогоном в чужом репозитории (httpx), 2026-09-07.
|
|
953
|
+
NODIR="$(mktemp -d)"; NOBIN="$(mktemp -d)"
|
|
954
|
+
ln -sf "$(command -v node)" "$NOBIN/node"
|
|
955
|
+
( cd "$NODIR" && git init -q . && mkdir -p src && printf 'x = 1\n' > src/a.py && PATH="$NOBIN" node "$CLI" init >/dev/null 2>&1 )
|
|
956
|
+
NO_OUT=$( cd "$NODIR" && PATH="$NOBIN" node "$CLI" add no-print-in-prod 2>&1 ); NO_CODE=$?
|
|
957
|
+
# Имя инструмента в отказе БЫЛО и до этой проверки — не хватало действия. Требуем оба:
|
|
958
|
+
# «ruff не установлен» — это диагноз, «поставь ruff» — это то, ради чего человек читает.
|
|
959
|
+
if [ "$NO_CODE" -ne 0 ] &&
|
|
960
|
+
printf '%s' "$NO_OUT" | grep -q 'ruff' &&
|
|
961
|
+
printf '%s' "$NO_OUT" | grep -qiE '(почини|поставь|install)'; then
|
|
962
|
+
ok "отказ установки называет инструмент И что с ним делать"
|
|
963
|
+
else
|
|
964
|
+
bad "отказ установки — тупик" "код $NO_CODE, вывод: $(printf '%s' "$NO_OUT" | tr '\n' ' ' | tail -c 200)"
|
|
965
|
+
fi
|
|
966
|
+
rm -rf "$NODIR" "$NOBIN"
|
|
967
|
+
|
|
968
|
+
# --- 48б. строгий режим приёмки: «не проверено» становится ошибкой -------------
|
|
969
|
+
# ЗАЧЕМ. «Записи не проверить, нет инструмента» — законное состояние на чужой машине и
|
|
970
|
+
# недопустимое на нашей: там инструменты ставит отдельный шаг, и жёлтая тильда вместо красного
|
|
971
|
+
# означает, что шаг не сработал. Ровно так и было: `pipx install vulture` стоял в конвейере с
|
|
972
|
+
# самого его появления, ставил vulture в каталог вне PATH, и запись dead-code не проверялась
|
|
973
|
+
# конвейером ни разу. Проверяем сам переключатель: на записи с заведомо отсутствующим
|
|
974
|
+
# инструментом обычный прогон зелёный, строгий — красный.
|
|
975
|
+
STPKG="$(mktemp -d)"
|
|
976
|
+
cp -r "$ROOT/tool" "$ROOT/kit" "$ROOT/package.json" "$STPKG/" 2>/dev/null
|
|
977
|
+
# Ломаем рецепт одной записи: программы с таким именем на машине нет ни у кого.
|
|
978
|
+
sed -i.bak 's|^ python: vulture .*| python: aqk-nesuschestvuyuschiy-instrument {dir}|' \
|
|
979
|
+
"$STPKG/kit/gates/dead-code/gate.yml" 2>/dev/null
|
|
980
|
+
# Переключатель задаётся явно в ОБЕ стороны. Без этого проверка наследовала AQK_GATES_STRICT
|
|
981
|
+
# из окружения — и в конвейере, где он поднят, «обычный» прогон был бы строгим, а проверка
|
|
982
|
+
# переключателя проверяла бы одно и то же дважды.
|
|
983
|
+
ST_SOFT_OUT=$(AQK_GATES_STRICT=0 bash "$STPKG/tool/selfcheck/gates.sh" 2>&1); ST_SOFT=$?
|
|
984
|
+
ST_HARD_OUT=$(AQK_GATES_STRICT=1 bash "$STPKG/tool/selfcheck/gates.sh" 2>&1); ST_HARD=$?
|
|
985
|
+
if [ "$ST_SOFT" -eq 0 ] && [ "$ST_HARD" -ne 0 ] &&
|
|
986
|
+
printf '%s' "$ST_HARD_OUT" | grep -q 'строгий режим'; then
|
|
987
|
+
ok "строгий режим делает «не проверено» ошибкой, обычный — нет"
|
|
988
|
+
else
|
|
989
|
+
bad "строгий режим приёмки не работает" "обычный: $ST_SOFT, строгий: $ST_HARD"
|
|
990
|
+
fi
|
|
991
|
+
rm -rf "$STPKG"
|
|
992
|
+
|
|
907
993
|
# --- 49. выведенную запись не ставят, а называют преемника --------------------
|
|
908
994
|
# ЗАЧЕМ. Зрелость записи считается по доказательству, и объявить её нельзя — кроме одного
|
|
909
995
|
# состояния: `deprecated`. Оно объявляется, и весь его смысл в отказе: запись, которую всё ещё
|
|
@@ -933,11 +1019,11 @@ rm -rf "$DEPKG" "$DEPRJ"
|
|
|
933
1019
|
SCDIR="$(mktemp -d)"
|
|
934
1020
|
(
|
|
935
1021
|
cd "$SCDIR" && git init -q . && git config user.email t@t && git config user.name t
|
|
936
|
-
mkdir -p src && printf '
|
|
1022
|
+
mkdir -p src && printf 'old = 1 # noqa\n' > src/old.py
|
|
937
1023
|
node "$CLI" init >/dev/null 2>&1
|
|
938
|
-
node "$CLI" add
|
|
1024
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
939
1025
|
git add -A && git commit -qm "база" >/dev/null 2>&1
|
|
940
|
-
printf '
|
|
1026
|
+
printf 'fresh = 1 # noqa\n' > src/fresh.py
|
|
941
1027
|
)
|
|
942
1028
|
SC_WIDE=$( cd "$SCDIR" && node "$CLI" doctor --run 2>&1 )
|
|
943
1029
|
SC_NARROW=$( cd "$SCDIR" && node "$CLI" doctor --run --since HEAD 2>&1 )
|
|
@@ -957,6 +1043,400 @@ else
|
|
|
957
1043
|
fi
|
|
958
1044
|
rm -rf "$SCDIR"
|
|
959
1045
|
|
|
1046
|
+
# --- 52. совет по починке не теряется в обрезке --------------------------------
|
|
1047
|
+
# ЗАЧЕМ. Все записи каталога печатают «почини: …» последней строкой, а прогон показывал три
|
|
1048
|
+
# первые и обрезал остальное — то есть ровно ту строку, ради которой человек смотрит на красное,
|
|
1049
|
+
# он не видел никогда. Находка без действия закрывает окно, а не дефект.
|
|
1050
|
+
ADIR="$(mktemp -d)"
|
|
1051
|
+
(
|
|
1052
|
+
cd "$ADIR" && git init -q . && mkdir -p src
|
|
1053
|
+
for n in a b c d e; do printf '%s = 1 # noqa\n' "$n" > "src/$n.py"; done
|
|
1054
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1055
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1056
|
+
)
|
|
1057
|
+
A_OUT=$( cd "$ADIR" && node "$CLI" doctor --run 2>&1 )
|
|
1058
|
+
if printf '%s' "$A_OUT" | grep -qiE '(почини|fix)[[:space:]]*:' &&
|
|
1059
|
+
printf '%s' "$A_OUT" | grep -qE 'py:[0-9]+'; then
|
|
1060
|
+
ok "совет по починке виден при обрезанных находках"
|
|
1061
|
+
else
|
|
1062
|
+
bad "совет по починке потерялся" "находок в выводе: $(printf '%s' "$A_OUT" | grep -c 'py:')"
|
|
1063
|
+
fi
|
|
1064
|
+
rm -rf "$ADIR"
|
|
1065
|
+
|
|
1066
|
+
# --- 53. у долга есть цель и срок, и оба с последствием ------------------------
|
|
1067
|
+
# ЗАЧЕМ. Реестр, который может только сокращаться, всё равно не знает, когда кончится, — и
|
|
1068
|
+
# потому не кончается. Цель и срок без машинного последствия были бы украшением, поэтому
|
|
1069
|
+
# проверяем ровно последствия: срок вышел — красное; цель достигнута — сказано вслух.
|
|
1070
|
+
RDIR="$(mktemp -d)"
|
|
1071
|
+
(
|
|
1072
|
+
cd "$RDIR" && git init -q . && mkdir -p src && printf 'a = 1 # noqa\n' > src/a.py
|
|
1073
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1074
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1075
|
+
node "$CLI" ratchet gate-not-weakened >/dev/null 2>&1
|
|
1076
|
+
)
|
|
1077
|
+
R_REG="$RDIR/ratchets/gate-not-weakened.txt"
|
|
1078
|
+
if [ -f "$R_REG" ] && grep -q 'aqk-goal' "$R_REG"; then
|
|
1079
|
+
# Долг снят, новых нарушений нет — зелено.
|
|
1080
|
+
R_BASE=$( cd "$RDIR" && node "$CLI" doctor --run 2>&1 ); R_BASE_CODE=$?
|
|
1081
|
+
# Срок в прошлом — обязано покраснеть без единого нового нарушения.
|
|
1082
|
+
sed -i.bak 's/^# aqk-deadline:.*/# aqk-deadline: 2020-01-01/' "$R_REG"
|
|
1083
|
+
R_LATE=$( cd "$RDIR" && node "$CLI" doctor --run 2>&1 )
|
|
1084
|
+
if printf '%s' "$R_LATE" | grep -qi 'срок\|deadline'; then
|
|
1085
|
+
ok "срок долга вышел — храповик краснеет без новых нарушений"
|
|
1086
|
+
else
|
|
1087
|
+
bad "просроченный долг прошёл молча" "код базового прогона $R_BASE_CODE"
|
|
1088
|
+
fi
|
|
1089
|
+
# Цель заведомо достигнута — храповик обязан сказать, что обёртку пора убрать.
|
|
1090
|
+
sed -i.bak 's/^# aqk-deadline:.*/# aqk-deadline:/; s/^# aqk-goal:.*/# aqk-goal: 99/' "$R_REG"
|
|
1091
|
+
R_DONE=$( cd "$RDIR" && node "$CLI" doctor --run 2>&1 )
|
|
1092
|
+
if printf '%s' "$R_DONE" | grep -qi 'погашен\|paid off'; then
|
|
1093
|
+
ok "цель достигнута — храповик говорит убрать обёртку"
|
|
1094
|
+
else
|
|
1095
|
+
bad "погашенный долг не назван" "$(printf '%s' "$R_DONE" | grep -i ratchet | head -1)"
|
|
1096
|
+
fi
|
|
1097
|
+
else
|
|
1098
|
+
bad "реестр долга не создан или без цели" "$R_REG"
|
|
1099
|
+
fi
|
|
1100
|
+
rm -rf "$RDIR"
|
|
1101
|
+
|
|
1102
|
+
# --- 54. упавший гейт не стирает реестр долга ----------------------------------
|
|
1103
|
+
# ЗАЧЕМ. Провал без единой разобранной находки — это отказ инструмента, а не чистый прогон.
|
|
1104
|
+
# Храповик вычёркивал ВЕСЬ реестр как исправленный, возвращал ноль и — после появления цели —
|
|
1105
|
+
# предлагал снять защиту: «долг погашен, убери обёртку». Снятие защиты по итогам прогона,
|
|
1106
|
+
# которого не было. Найдено ревью 2026-09-06.
|
|
1107
|
+
WDIR="$(mktemp -d)"
|
|
1108
|
+
mkdir -p "$WDIR/ratchets"
|
|
1109
|
+
printf '# Реестр долга: проба\n# aqk-goal: 0\nsrc/a.py: печать\n' > "$WDIR/ratchets/t.txt"
|
|
1110
|
+
W_OUT=$( cd "$WDIR" && bash "$ROOT/kit/ratchet/ratchet.sh" ratchets/t.txt sh -c 'exit 3' 2>&1 ); W_CODE=$?
|
|
1111
|
+
W_LEFT=$(grep -c 'src/a.py' "$WDIR/ratchets/t.txt" || true)
|
|
1112
|
+
if [ "$W_CODE" -ne 0 ] && [ "$W_LEFT" -eq 1 ] &&
|
|
1113
|
+
! printf '%s' "$W_OUT" | grep -qi 'погашен'; then
|
|
1114
|
+
ok "упавший гейт не стирает реестр и не предлагает снять защиту"
|
|
1115
|
+
else
|
|
1116
|
+
bad "упавший гейт съел реестр" "код $W_CODE, строк долга осталось $W_LEFT"
|
|
1117
|
+
fi
|
|
1118
|
+
# Директива с опечаткой обязана быть слышной: молчаливо отключённая цель — та же тишина.
|
|
1119
|
+
printf '# Реестр\n# aqk-goal: скоро\nsrc/a.py: печать\n' > "$WDIR/ratchets/t.txt"
|
|
1120
|
+
W_BAD=$( cd "$WDIR" && bash "$ROOT/kit/ratchet/ratchet.sh" ratchets/t.txt sh -c 'echo "src/a.py: печать"' 2>&1 )
|
|
1121
|
+
if printf '%s' "$W_BAD" | grep -qi 'не число'; then
|
|
1122
|
+
ok "опечатка в директиве храповика названа, а не проглочена"
|
|
1123
|
+
else
|
|
1124
|
+
bad "нечисловая цель отключилась молча" "$(printf '%s' "$W_BAD" | head -1)"
|
|
1125
|
+
fi
|
|
1126
|
+
rm -rf "$WDIR"
|
|
1127
|
+
|
|
1128
|
+
# --- 55. просроченный долг краснеет и при сужении по дифу ----------------------
|
|
1129
|
+
# ЗАЧЕМ. Сообщение храповика про срок называет путь к реестру, а реестра в дифе нет: фильтр по
|
|
1130
|
+
# путям отбрасывал единственную строку, находок не оставалось, и гейт печатался зелёным с
|
|
1131
|
+
# пометкой «находки вне дифа». То есть `--since` отменял правило SPEC.md §7.6 ровно в том
|
|
1132
|
+
# режиме, в котором его и запускают. Найдено ревью 2026-09-06.
|
|
1133
|
+
DDIR="$(mktemp -d)"
|
|
1134
|
+
(
|
|
1135
|
+
cd "$DDIR" && git init -q . && git config user.email t@t && git config user.name t
|
|
1136
|
+
mkdir -p src && printf 'a = 1 # noqa\n' > src/a.py
|
|
1137
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1138
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1139
|
+
node "$CLI" ratchet gate-not-weakened >/dev/null 2>&1
|
|
1140
|
+
sed -i.bak 's/^# aqk-deadline:.*/# aqk-deadline: 2020-01-01/' ratchets/gate-not-weakened.txt
|
|
1141
|
+
git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1
|
|
1142
|
+
)
|
|
1143
|
+
D_WIDE=$( cd "$DDIR" && node "$CLI" doctor --run 2>&1 )
|
|
1144
|
+
D_NARROW=$( cd "$DDIR" && node "$CLI" doctor --run --since HEAD 2>&1 )
|
|
1145
|
+
if printf '%s' "$D_WIDE" | grep -q 'gate-not-weakened' &&
|
|
1146
|
+
printf '%s' "$D_NARROW" | grep -qE 'gate-not-weakened.*(код|exit)' ; then
|
|
1147
|
+
ok "просроченный долг краснеет и при --since"
|
|
1148
|
+
else
|
|
1149
|
+
bad "--since отменил срок долга" "узкий прогон: $(printf '%s' "$D_NARROW" | grep no-print | head -1 | cut -c1-90)"
|
|
1150
|
+
fi
|
|
1151
|
+
rm -rf "$DDIR"
|
|
1152
|
+
|
|
1153
|
+
# --- 56. совещательный гейт показан, но прогон не уронен -----------------------
|
|
1154
|
+
# ЗАЧЕМ. Правило вводят в проект, где старый код ему не соответствует. Без третьего пути выбор
|
|
1155
|
+
# из двух крайностей: включить и сломать сборку либо не включать вовсе. Проверяем обе стороны:
|
|
1156
|
+
# без списка — роняет; со списком — показано и НАЗВАНО, а прогон зелёный. Молчание о
|
|
1157
|
+
# совещательном гейте было бы выключенной проверкой, притворяющейся отсутствующей.
|
|
1158
|
+
VDIR="$(mktemp -d)"
|
|
1159
|
+
(
|
|
1160
|
+
cd "$VDIR" && git init -q . && mkdir -p src && printf 'a = 1 # noqa\n' > src/a.py
|
|
1161
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1162
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1163
|
+
)
|
|
1164
|
+
( cd "$VDIR" && node "$CLI" doctor --run --min 1 >/dev/null 2>&1 ); V_HARD=$?
|
|
1165
|
+
printf '\nadvisory:\n - gate-not-weakened\n' >> "$VDIR/.aqk.yml"
|
|
1166
|
+
V_OUT=$( cd "$VDIR" && node "$CLI" doctor --run --min 1 2>&1 ); V_SOFT=$?
|
|
1167
|
+
if [ "$V_HARD" -ne 0 ] && [ "$V_SOFT" -eq 0 ] &&
|
|
1168
|
+
printf '%s' "$V_OUT" | grep -qE 'advisory|совещательн' &&
|
|
1169
|
+
printf '%s' "$V_OUT" | grep -q 'src/a.py'; then
|
|
1170
|
+
ok "совещательный гейт показывает находки, называется и не роняет прогон"
|
|
1171
|
+
else
|
|
1172
|
+
bad "совещательный режим работает не так" "обычный код $V_HARD, совещательный $V_SOFT"
|
|
1173
|
+
fi
|
|
1174
|
+
# Опечатка в имени поля обязана быть названа: «advisery:» молча означало бы «совещательных нет»,
|
|
1175
|
+
# и правило, которое человек считал введённым, роняло бы сборку.
|
|
1176
|
+
sed -i.bak 's/^advisory:/advisery:/' "$VDIR/.aqk.yml"
|
|
1177
|
+
V_TYPO=$( cd "$VDIR" && node "$CLI" doctor 2>&1 )
|
|
1178
|
+
if printf '%s' "$V_TYPO" | grep -qi 'advisery'; then
|
|
1179
|
+
ok "опечатка в имени поля манифеста названа"
|
|
1180
|
+
else
|
|
1181
|
+
bad "опечатка в advisory проглочена" "$(printf '%s' "$V_TYPO" | tail -2 | head -1)"
|
|
1182
|
+
fi
|
|
1183
|
+
rm -rf "$VDIR"
|
|
1184
|
+
|
|
1185
|
+
# --- 81. версия в README и llms.txt та же, что в package.json ---------------
|
|
1186
|
+
# Обе строки — инструкция по установке для постороннего: `rev:` для pre-commit и `@vX.Y.Z` для
|
|
1187
|
+
# GitHub Action. Устаревшая ставит человеку не тот комплект и молчит об этом: команда проходит,
|
|
1188
|
+
# ставится прошлогодняя версия. AGENTS.md требует, чтобы README и llms.txt не расходились, —
|
|
1189
|
+
# и без прибора требование не работало: на 2026-09-08 в README стояло v0.6.0, а в llms.txt
|
|
1190
|
+
# v0.4.2, отставание на два выпуска. Проверка сравнивает обе с package.json, а не друг с другом:
|
|
1191
|
+
# совпасть друг с другом они могут и будучи одинаково устаревшими.
|
|
1192
|
+
VERS_BAD=""
|
|
1193
|
+
# README.ru.md попал сюда 2026-09-08, при выпуске 0.8.0: в нём стояли v0.6.0 и v0.5.0 —
|
|
1194
|
+
# отставание на два и на три выпуска. Проверка его не смотрела, и русский читатель ставил
|
|
1195
|
+
# прошлогодний комплект молча. Ровно та же дыра, ради которой проверку и заводили, — просто
|
|
1196
|
+
# в файле, который забыли перечислить.
|
|
1197
|
+
for VF in README.md README.ru.md llms.txt; do
|
|
1198
|
+
[ -f "$ROOT/$VF" ] || continue
|
|
1199
|
+
# Берём только версии AQK — «v1.2.3» в примерах чужих действий (actions/checkout@v4) не наши.
|
|
1200
|
+
for V in $(grep -oE '(rev:[[:space:]]*|Agent_Quality_Kit@)v[0-9]+\.[0-9]+\.[0-9]+' "$ROOT/$VF" \
|
|
1201
|
+
| grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | sort -u); do
|
|
1202
|
+
[ "$V" = "v$PKGVER" ] || VERS_BAD="${VERS_BAD:+$VERS_BAD, }$VF: $V"
|
|
1203
|
+
done
|
|
1204
|
+
done
|
|
1205
|
+
if [ -z "$VERS_BAD" ]; then
|
|
1206
|
+
ok "версия в обоих README и llms.txt совпадает с package.json (v$PKGVER)"
|
|
1207
|
+
else
|
|
1208
|
+
bad "версия в документах разошлась с package.json" "package.json: v$PKGVER; найдено — $VERS_BAD"
|
|
1209
|
+
fi
|
|
1210
|
+
|
|
1211
|
+
# --- 82. report --since называет, чем доказан диф ---------------------------
|
|
1212
|
+
# Три состояния у файла, и разница между вторым и третьим — весь смысл раздела: «проверка
|
|
1213
|
+
# обошла и промолчала» не то же самое, что «никто не смотрел». Двух состояний хватило ровно до
|
|
1214
|
+
# первого прогона — `tool/commands/doctor.mjs` попал в «никем не проверен», хотя его обходят
|
|
1215
|
+
# пять проверок; они молчали, потому что нашли чисто.
|
|
1216
|
+
EVDIR="$(mktemp -d)"
|
|
1217
|
+
(
|
|
1218
|
+
cd "$EVDIR" || exit 1
|
|
1219
|
+
git init -q . && git config user.email a@b.c && git config user.name t
|
|
1220
|
+
mkdir -p src gates/noisy
|
|
1221
|
+
printf 'x = 1\n' > src/kept.py
|
|
1222
|
+
# Гейт, который печатает путь: по нему файл становится «назван».
|
|
1223
|
+
printf '#!/usr/bin/env sh\necho "src/kept.py:1: нашёл"\nexit 0\n' > gates/noisy/check.sh
|
|
1224
|
+
printf 'aqk: "1"\nentry: [AGENTS.md]\ngates:\n noisy: "sh gates/noisy/check.sh ."\n' > .aqk.yml
|
|
1225
|
+
printf '# правила\n' > AGENTS.md
|
|
1226
|
+
git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1
|
|
1227
|
+
printf 'y = 2\n' > src/quiet.py
|
|
1228
|
+
git add -A >/dev/null 2>&1 && git commit -qm second >/dev/null 2>&1
|
|
1229
|
+
) >/dev/null 2>&1
|
|
1230
|
+
EV_OUT=$( cd "$EVDIR" && AQK_LANG=ru node "$CLI" report --since HEAD~1 2>&1 )
|
|
1231
|
+
EV_FILE="$EVDIR/.aqk/report.md"
|
|
1232
|
+
if [ -f "$EV_FILE" ] && grep -q "src/quiet.py" "$EV_FILE" && grep -q "Чем доказан" "$EV_FILE"; then
|
|
1233
|
+
ok "report --since называет, чем доказан диф"
|
|
1234
|
+
else
|
|
1235
|
+
bad "report --since не отчитался о покрытии" "$(printf '%s' "$EV_OUT" | tail -2 | head -1)"
|
|
1236
|
+
fi
|
|
1237
|
+
# Ссылка, которой нет, обязана быть названа: «сравнили не с тем» не должно читаться как «чисто».
|
|
1238
|
+
EV_BAD=$( cd "$EVDIR" && AQK_LANG=ru node "$CLI" report --since no-such-ref 2>&1 )
|
|
1239
|
+
if printf '%s' "$EV_BAD" | grep -q "no-such-ref"; then
|
|
1240
|
+
ok "report --since называет неразобранную ссылку"
|
|
1241
|
+
else
|
|
1242
|
+
bad "report --since проглотил неверную ссылку" "$(printf '%s' "$EV_BAD" | tail -2 | head -1)"
|
|
1243
|
+
fi
|
|
1244
|
+
rm -rf "$EVDIR"
|
|
1245
|
+
|
|
1246
|
+
# --- 84. learn читает только напечатанное человеком --------------------------
|
|
1247
|
+
# Поле promptSource отделяет реплику от результата инструмента. Без него первая версия отбора
|
|
1248
|
+
# выдавала вставленные пути и ссылки вместо правил — «agent quality kit» 44 раза.
|
|
1249
|
+
# Каталог логов кладём ВНУТРЬ проекта и передаём относительным путём. Абсолютный сюда не
|
|
1250
|
+
# годится: Git Bash на Windows отдаёт «/tmp/…», а Node в том же окружении читает это как
|
|
1251
|
+
# «C:\tmp\…» — переменная указывает в никуда, и проверка падает не по делу.
|
|
1252
|
+
LRNP="$(mktemp -d)"; LRN=".cfg"
|
|
1253
|
+
# Имя каталога логов спрашиваем у самой программы, а не считаем здесь. На Windows оболочка
|
|
1254
|
+
# отдаёт «/tmp/…», а Node видит «C:\Users\…» — две разные строки, и тест падал не по делу.
|
|
1255
|
+
# Правило перевода пути в имя сторожит модульная проверка logSlug, здесь проверяется конвейер.
|
|
1256
|
+
SLUG=$( cd "$LRNP" && node -e "const {pathToFileURL}=require('node:url');import(pathToFileURL(process.argv[1]).href).then(m=>console.log(m.logSlug(process.cwd())))" "$ROOT/tool/commands/learn.mjs" )
|
|
1257
|
+
mkdir -p "$LRNP/$LRN/projects/$SLUG"
|
|
1258
|
+
{
|
|
1259
|
+
printf '{"type":"user","promptSource":"typed","timestamp":"2026-09-08T10:00:00Z","message":{"role":"user","content":"никогда не коммить прямо в основную ветку"}}\n'
|
|
1260
|
+
printf '{"type":"user","promptSource":"typed","timestamp":"2026-09-08T10:01:00Z","message":{"role":"user","content":"ок го дальше"}}\n'
|
|
1261
|
+
printf '{"type":"user","timestamp":"2026-09-08T10:02:00Z","message":{"role":"user","content":[{"type":"tool_result","content":"нельзя обязательно всегда"}]}}\n'
|
|
1262
|
+
} > "$LRNP/$LRN/projects/$SLUG/s1.jsonl"
|
|
1263
|
+
printf '# правила\n- Ничего особенного.\n' > "$LRNP/AGENTS.md"
|
|
1264
|
+
printf 'aqk: "1"\nentry: [AGENTS.md]\n' > "$LRNP/.aqk.yml"
|
|
1265
|
+
LRN_OUT=$( cd "$LRNP" && CLAUDE_CONFIG_DIR="$LRN" AQK_LANG=ru node "$CLI" learn 2>&1 )
|
|
1266
|
+
if printf '%s' "$LRN_OUT" | grep -q "основную ветку" &&
|
|
1267
|
+
! printf '%s' "$LRN_OUT" | grep -q "го дальше" &&
|
|
1268
|
+
printf '%s' "$LRN_OUT" | grep -q "напечатано человеком: 2"; then
|
|
1269
|
+
ok "learn берёт напечатанное человеком и не берёт вывод инструментов"
|
|
1270
|
+
else
|
|
1271
|
+
bad "learn отобрал не то" "$(printf '%s' "$LRN_OUT" | tr '\n' ' ' | cut -c1-150)"
|
|
1272
|
+
fi
|
|
1273
|
+
# Правило, уже стоящее в точке входа, показывать незачем: команда не пересказывает свод.
|
|
1274
|
+
printf '# правила\n- Никогда не коммить прямо в основную ветку.\n' > "$LRNP/AGENTS.md"
|
|
1275
|
+
LRN_W=$( cd "$LRNP" && CLAUDE_CONFIG_DIR="$LRN" AQK_LANG=ru node "$CLI" learn 2>&1 )
|
|
1276
|
+
if printf '%s' "$LRN_W" | grep -q "уже стоит в точке входа"; then
|
|
1277
|
+
ok "learn молчит о правиле, которое уже записано"
|
|
1278
|
+
else
|
|
1279
|
+
bad "learn повторил записанное правило" "$(printf '%s' "$LRN_W" | tr '\n' ' ' | cut -c1-150)"
|
|
1280
|
+
fi
|
|
1281
|
+
# Читает переписку — значит на диск не пишет ничего. Проверяем буквально.
|
|
1282
|
+
if [ ! -d "$LRNP/.aqk" ] || [ -z "$(ls -A "$LRNP/.aqk" 2>/dev/null)" ]; then
|
|
1283
|
+
ok "learn ничего не записал на диск"
|
|
1284
|
+
else
|
|
1285
|
+
bad "learn создал файлы" "$(ls -A "$LRNP/.aqk" | tr '\n' ' ')"
|
|
1286
|
+
fi
|
|
1287
|
+
rm -rf "$LRNP"
|
|
1288
|
+
|
|
1289
|
+
# --- 87. --baseline не совмещается с --run и --min ---------------------------
|
|
1290
|
+
# `--baseline` выходит с нулём всегда: это осмотр, а не прогон. Совмещённый с порогом он давал
|
|
1291
|
+
# конвейер, который НЕ МОЖЕТ покраснеть — порог назван, гейты не запущены, код нулевой. Найдено
|
|
1292
|
+
# ревью 2026-09-08. Отказ должен быть громким: молчаливое зелёное здесь дороже сломанной команды.
|
|
1293
|
+
BLP="$(mktemp -d)"
|
|
1294
|
+
( cd "$BLP" && git init -q . && printf 'aqk: "1"\nentry: [AGENTS.md]\n' > .aqk.yml && printf '# правила\n' > AGENTS.md ) >/dev/null 2>&1
|
|
1295
|
+
BL_OK=$( cd "$BLP" && AQK_LANG=ru node "$CLI" doctor --baseline 2>&1 ); BL_OK_C=$?
|
|
1296
|
+
BL_BAD=$( cd "$BLP" && AQK_LANG=ru node "$CLI" doctor --baseline --min 1 2>&1 ); BL_BAD_C=$?
|
|
1297
|
+
BL_RUN=$( cd "$BLP" && AQK_LANG=ru node "$CLI" doctor --baseline --run 2>&1 ); BL_RUN_C=$?
|
|
1298
|
+
if [ "$BL_OK_C" -eq 0 ] && [ "$BL_BAD_C" -ne 0 ] && [ "$BL_RUN_C" -ne 0 ] &&
|
|
1299
|
+
printf '%s' "$BL_BAD" | grep -q "не может покраснеть"; then
|
|
1300
|
+
ok "--baseline с --min и --run отказывает вслух, сам по себе работает"
|
|
1301
|
+
else
|
|
1302
|
+
bad "--baseline не отказал на пороге" "коды: сам $BL_OK_C, с --min $BL_BAD_C, с --run $BL_RUN_C"
|
|
1303
|
+
fi
|
|
1304
|
+
rm -rf "$BLP"
|
|
1305
|
+
|
|
1306
|
+
# --- 88. записи каталога не ломаются от цвета в выводе арбитра ---------------
|
|
1307
|
+
# Внутри GitHub Actions чужие инструменты КРАСЯТ вывод: там цвет поддержан, вне конвейера они
|
|
1308
|
+
# его выключают сами. Разбор по началу строки тогда промахивается — строка начинается с
|
|
1309
|
+
# escape-последовательности. Поймано конвейером 2026-09-08 на записи ci-not-hijackable: локально
|
|
1310
|
+
# зелёная, в конвейере «формат сменился». Тот же класс уже записан в scope.mjs.
|
|
1311
|
+
CLR_BAD=""
|
|
1312
|
+
for CG in kit/gates/*/check.sh; do
|
|
1313
|
+
CS=$(basename "$(dirname "$CG")")
|
|
1314
|
+
# Только записи с готовым арбитром: у них вывод чужой, и красит его не наш код.
|
|
1315
|
+
grep -q "^requires:" "$(dirname "$CG")/gate.yml" 2>/dev/null || continue
|
|
1316
|
+
command -v "$(sed -n 's/^requires:[[:space:]]*//p' "$(dirname "$CG")/gate.yml" | head -1 | awk '{print $1}')" >/dev/null 2>&1 || continue
|
|
1317
|
+
PLAIN=$(bash "$CG" "$(dirname "$CG")/green" >/dev/null 2>&1; echo $?)
|
|
1318
|
+
COLOR=$(GITHUB_ACTIONS=true CI=true bash "$CG" "$(dirname "$CG")/green" >/dev/null 2>&1; echo $?)
|
|
1319
|
+
[ "$PLAIN" = "$COLOR" ] || CLR_BAD="${CLR_BAD:+$CLR_BAD, }$CS ($PLAIN vs $COLOR)"
|
|
1320
|
+
done
|
|
1321
|
+
if [ -z "$CLR_BAD" ]; then
|
|
1322
|
+
ok "вердикт записи не зависит от того, красит ли арбитр вывод"
|
|
1323
|
+
else
|
|
1324
|
+
bad "цвет в выводе арбитра меняет вердикт" "$CLR_BAD"
|
|
1325
|
+
fi
|
|
1326
|
+
|
|
1327
|
+
# --- 89. шапка doctor читает манифест, а не литеральные пути ------------------
|
|
1328
|
+
# Отзыв второго пользователя 2026-09-08: у проекта `rules: .temper/rules`, правила на месте,
|
|
1329
|
+
# СТУПЕНЬ по манифесту берётся — а шапка рисовала кресты за `.aqk/rules` и `.aqk/docs` и
|
|
1330
|
+
# советовала сделать сделанное. Вывод расходился с собственным вердиктом программы, напечатанным
|
|
1331
|
+
# на десять строк ниже. Модульная проверка сторожит функцию; эта — то, что её кто-то зовёт.
|
|
1332
|
+
LYP="$(mktemp -d)"
|
|
1333
|
+
( cd "$LYP" && git init -q . && mkdir -p .temper/rules .temper/docs &&
|
|
1334
|
+
printf 'правило\n' > .temper/rules/r.md && printf 'методичка\n' > .temper/docs/d.md &&
|
|
1335
|
+
printf '# вход\n' > CLAUDE.md && printf '.x\n' > .gitignore &&
|
|
1336
|
+
printf 'aqk: 1\nentry:\n - CLAUDE.md\nrules: .temper/rules\ndocs: .temper/docs\ngates:\n smoke: "true"\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1337
|
+
LY=$( cd "$LYP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
|
|
1338
|
+
if printf '%s' "$LY" | grep -q "\.temper/rules" &&
|
|
1339
|
+
printf '%s' "$LY" | grep -q "\.temper/docs" &&
|
|
1340
|
+
printf '%s' "$LY" | grep -q "CLAUDE\.md" &&
|
|
1341
|
+
! printf '%s' "$LY" | grep -q "\.aqk/rules" &&
|
|
1342
|
+
! printf '%s' "$LY" | grep -q "неизвестное поле"; then
|
|
1343
|
+
ok "шапка doctor берёт правила, методички и вход из манифеста"
|
|
1344
|
+
else
|
|
1345
|
+
bad "doctor проверил не то, что объявлено в манифесте" "$(printf '%s' "$LY" | head -8)"
|
|
1346
|
+
fi
|
|
1347
|
+
rm -rf "$LYP"
|
|
1348
|
+
|
|
1349
|
+
# --- 90. блок состояния: тишина не выдаётся за «чисто» -----------------------
|
|
1350
|
+
# Читатель этого блока — машина. Человек, увидев пустое место, переспросит; агент примет его
|
|
1351
|
+
# за утверждение и пойдёт писать код по несуществующему разрешению. Поэтому главное здесь одно:
|
|
1352
|
+
# без прогона блок обязан сказать «неизвестно» СЛОВОМ.
|
|
1353
|
+
CTXP="$(mktemp -d)"
|
|
1354
|
+
( cd "$CTXP" && git init -q . ) >/dev/null 2>&1
|
|
1355
|
+
CTX=$( cd "$CTXP" && AQK_LANG=ru node "$CLI" context 2>&1 ); CTX_C=$?
|
|
1356
|
+
if [ "$CTX_C" -eq 0 ] &&
|
|
1357
|
+
printf '%s' "$CTX" | grep -q "НЕИЗВЕСТНО" &&
|
|
1358
|
+
printf '%s' "$CTX" | grep -q "не вычислен" &&
|
|
1359
|
+
! printf '%s' "$CTX" | grep -q "AGENTS.md"; then
|
|
1360
|
+
ok "context без прогона говорит «неизвестно» и не называет несуществующий свод"
|
|
1361
|
+
else
|
|
1362
|
+
bad "context выдал незнание за чистоту" "код $CTX_C: $(printf '%s' "$CTX" | head -5)"
|
|
1363
|
+
fi
|
|
1364
|
+
|
|
1365
|
+
# --- 91. хук ставится в общий файл и не затирает чужие настройки --------------
|
|
1366
|
+
mkdir -p "$CTXP/.claude"
|
|
1367
|
+
printf '{ "permissions": { "deny": ["Read(./.env)"] } }\n' > "$CTXP/.claude/settings.json"
|
|
1368
|
+
( cd "$CTXP" && AQK_LANG=ru node "$CLI" context --install ) >/dev/null 2>&1
|
|
1369
|
+
AGAIN=$( cd "$CTXP" && AQK_LANG=ru node "$CLI" context --install 2>&1 )
|
|
1370
|
+
HOOKS=$(node_in "$CTXP" -e 'const s=require("./.claude/settings.json");
|
|
1371
|
+
console.log([s.hooks?.SessionStart?.length, s.permissions?.deny?.length,
|
|
1372
|
+
/[/\\]program\.mjs/.test(JSON.stringify(s.hooks?.SessionStart||[]))].join(" "))' 2>&1)
|
|
1373
|
+
if [ "$HOOKS" = "1 1 false" ] && printf '%s' "$AGAIN" | grep -q "уже стоит"; then
|
|
1374
|
+
ok "хук ставится один раз, переносимой командой, чужие настройки целы"
|
|
1375
|
+
else
|
|
1376
|
+
bad "установка хука испортила настройки или задвоилась" "разбор: $HOOKS"
|
|
1377
|
+
fi
|
|
1378
|
+
rm -rf "$CTXP"
|
|
1379
|
+
|
|
1380
|
+
# --- 92. полный блок: карта и свод дословно ----------------------------------
|
|
1381
|
+
# Решение владельца 2026-09-08, принятое ПОСЛЕ возражения про длину входа: агент читает файлы
|
|
1382
|
+
# плохо, и лишние токены — плата за то, чтобы он не ошибался. Раз плата внесена, товар обязан
|
|
1383
|
+
# быть доставлен: свод дословно, а не пересказ, и карта команд, а не половина карты.
|
|
1384
|
+
FULP="$(mktemp -d)"
|
|
1385
|
+
( cd "$FULP" && git init -q . &&
|
|
1386
|
+
printf '# Свод\n\n- Правило-маячок-для-проверки. <!-- aqk: человек -->\n' > AGENTS.md &&
|
|
1387
|
+
printf 'aqk: 1\nentry:\n - AGENTS.md\n' > .aqk.yml ) >/dev/null 2>&1
|
|
1388
|
+
FUL=$( cd "$FULP" && AQK_LANG=ru node "$CLI" context --full 2>&1 )
|
|
1389
|
+
SHORT=$( cd "$FULP" && AQK_LANG=ru node "$CLI" context 2>&1 )
|
|
1390
|
+
if printf '%s' "$FUL" | grep -q "Правило-маячок-для-проверки" &&
|
|
1391
|
+
printf '%s' "$FUL" | grep -q "doctor --run --since main" &&
|
|
1392
|
+
printf '%s' "$FUL" | grep -q "ЧТО УМЕЕТ ЭТОТ ИНСТРУМЕНТ" &&
|
|
1393
|
+
! printf '%s' "$SHORT" | grep -q "Правило-маячок-для-проверки"; then
|
|
1394
|
+
ok "context --full несёт карту и свод дословно, обычный — нет"
|
|
1395
|
+
else
|
|
1396
|
+
bad "полный блок не донёс свод или карту" "$(printf '%s' "$FUL" | head -4)"
|
|
1397
|
+
fi
|
|
1398
|
+
|
|
1399
|
+
# --- 93. хук с --full ставится именно с флагом -------------------------------
|
|
1400
|
+
( cd "$FULP" && AQK_LANG=ru node "$CLI" context --full --install ) >/dev/null 2>&1
|
|
1401
|
+
FLAG=$(node_in "$FULP" -e 'const s=require("./.claude/settings.json");
|
|
1402
|
+
console.log(JSON.stringify(s.hooks?.SessionStart||[]).includes("--full"))' 2>&1)
|
|
1403
|
+
if [ "$FLAG" = "true" ]; then
|
|
1404
|
+
ok "хук, поставленный с --full, зовёт полный блок, а не короткий"
|
|
1405
|
+
else
|
|
1406
|
+
bad "хук потерял --full — вливался бы короткий блок" "разбор: $FLAG"
|
|
1407
|
+
fi
|
|
1408
|
+
rm -rf "$FULP"
|
|
1409
|
+
|
|
1410
|
+
# --- 94. отсев сгенерированных файлов: края, а не середина -------------------
|
|
1411
|
+
# `drop_generated` заменил цикл `while read; do is_generated; done` — два процесса на файл.
|
|
1412
|
+
# Замер на шести чужих проектах дал ноль расхождений, но отсеяно там было 5 файлов из 1242:
|
|
1413
|
+
# такая сверка почти ничего не доказывает. Набор ниже нарочный, и он сразу нашёл ДВА
|
|
1414
|
+
# расхождения, которых на живых проектах не было, — пробел и двоеточие в имени файла.
|
|
1415
|
+
GENP="$(mktemp -d)"
|
|
1416
|
+
mkg() { printf '%b' "$2" > "$GENP/$1"; }
|
|
1417
|
+
mkg 'l1.js' '// @generated\ncode\n'
|
|
1418
|
+
mkg 'l5.js' 'a\nb\nc\nd\n// @generated\n'
|
|
1419
|
+
mkg 'l6.js' 'a\nb\nc\nd\ne\n// @generated\n'
|
|
1420
|
+
mkg 'upper.js' '// @GENERATED\n'
|
|
1421
|
+
mkg 'donotedit.js' '// Do Not Edit\n'
|
|
1422
|
+
mkg 'autogen.js' '// auto-generated by tool\n'
|
|
1423
|
+
mkg 'genby.js' '// Generated by protoc\n'
|
|
1424
|
+
mkg 'cyr.js' '// сгенерирован автоматически\n'
|
|
1425
|
+
mkg 'cyrup.js' '// СГЕНЕРИРОВАН\n'
|
|
1426
|
+
mkg 'with space.js' '// @generated\n'
|
|
1427
|
+
mkg 'with:colon.js' '// @generated\n'
|
|
1428
|
+
mkg 'clean.js' 'const x = 1;\n'
|
|
1429
|
+
: > "$GENP/empty.js"
|
|
1430
|
+
KEPT=$( . "$ROOT/kit/gates/_skip.sh"; find "$GENP" -type f | sort | drop_generated | sed "s#$GENP/##" | sort | tr '\n' ' ' )
|
|
1431
|
+
# Ожидания абсолютные, а не «как было раньше»: старая функция однажды уйдёт, а края останутся.
|
|
1432
|
+
# l6 остаётся намеренно: маркер на шестой строке — это уже не шапка файла.
|
|
1433
|
+
if [ "$KEPT" = "clean.js empty.js l6.js " ]; then
|
|
1434
|
+
ok "сгенерированные отсеяны по всем маркерам; пробел, двоеточие и граница 5-й строки учтены"
|
|
1435
|
+
else
|
|
1436
|
+
bad "отсев сгенерированных изменился" "осталось: $KEPT"
|
|
1437
|
+
fi
|
|
1438
|
+
rm -rf "$GENP"
|
|
1439
|
+
|
|
960
1440
|
# --- итог -------------------------------------------------------------------
|
|
961
1441
|
printf '\n'
|
|
962
1442
|
if [ "$FAIL" -eq 0 ]; then
|