agent-quality-kit 0.5.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 +53 -2
- package/README.ru.md +35 -1
- package/kit/docs/ai/agent-harness-playbook.md +1 -1
- package/kit/docs/ready-made-rules.md +65 -0
- package/kit/gates/README.md +60 -0
- package/kit/gates/ci-actually-fails/README.md +54 -0
- package/kit/gates/ci-actually-fails/check.sh +116 -0
- package/kit/gates/ci-actually-fails/gate.yml +14 -0
- package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +30 -0
- package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -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/README.md +54 -0
- package/kit/gates/gate-not-weakened/check.sh +84 -0
- package/kit/gates/gate-not-weakened/gate.yml +15 -0
- package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
- package/kit/gates/gate-not-weakened/green/payments.py +6 -0
- package/kit/gates/gate-not-weakened/green/release.sh +2 -0
- package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
- package/kit/gates/gate-not-weakened/red/payments.py +6 -0
- package/kit/gates/gate-not-weakened/red/release.sh +2 -0
- 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/promise-has-gate/README.md +50 -0
- package/kit/gates/promise-has-gate/check.sh +88 -0
- package/kit/gates/promise-has-gate/gate.yml +14 -0
- package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
- package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
- package/kit/gates/promise-has-gate/red/AGENTS.md +7 -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/README.md +47 -0
- package/kit/gates/test-has-assertion/check.sh +206 -0
- package/kit/gates/test-has-assertion/gate.yml +15 -0
- package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
- package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
- package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
- package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
- 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 +23 -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 +2 -1
- package/package.json +4 -2
- package/tool/commands/badge.mjs +7 -1
- package/tool/commands/doctor.mjs +90 -10
- package/tool/commands/gates.mjs +19 -5
- package/tool/commands/project.mjs +15 -2
- 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 +66 -54
- package/tool/i18n/ru-docs.mjs +70 -0
- package/tool/i18n/ru.mjs +66 -54
- package/tool/i18n/templates-en.mjs +9 -9
- package/tool/i18n/templates-ru.mjs +9 -9
- package/tool/lib/core.mjs +7 -1
- package/tool/lib/manifest.mjs +72 -5
- package/tool/lib/prove.mjs +160 -0
- package/tool/lib/repo.mjs +31 -2
- package/tool/lib/scope.mjs +131 -0
- package/tool/lib/templates.mjs +2 -0
- package/tool/program.mjs +6 -0
- package/tool/selfcheck/gates.sh +86 -3
- package/tool/selfcheck/lifecycle.mjs +29 -0
- package/tool/selfcheck/mutation.sh +21 -1
- package/tool/selfcheck/smoke.sh +329 -36
- package/tool/selfcheck/units-level.mjs +60 -0
- package/tool/selfcheck/units.mjs +196 -1
- 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,29 @@
|
|
|
1
|
+
// tool/selfcheck/lifecycle.mjs — зрелость записей каталога, одной таблицей.
|
|
2
|
+
//
|
|
3
|
+
// ЗАЧЕМ ОТДЕЛЬНЫМ ФАЙЛОМ, А НЕ КУСКОМ gates.sh. Правило зрелости живёт в `entryLifecycle`
|
|
4
|
+
// (tool/lib/manifest.mjs) и оттуда же читается программой. Повторить его на sh значило бы
|
|
5
|
+
// завести второй источник истины: через месяц приёмка и отчёт расходятся, и про одну и ту же
|
|
6
|
+
// запись машина говорит разное. Здесь — только печать; решение принимает та же функция.
|
|
7
|
+
//
|
|
8
|
+
// node tool/selfcheck/lifecycle.mjs → slug|состояние|замена|проблема
|
|
9
|
+
//
|
|
10
|
+
// Код возврата — число записей с проблемой объявления.
|
|
11
|
+
|
|
12
|
+
import { readCatalog } from "../lib/repo.mjs";
|
|
13
|
+
import { entryLifecycle } from "../lib/manifest.mjs";
|
|
14
|
+
import { L } from "../i18n/index.mjs";
|
|
15
|
+
|
|
16
|
+
const catalog = await readCatalog();
|
|
17
|
+
const slugs = new Set(catalog.map((r) => r.slug));
|
|
18
|
+
let bad = 0;
|
|
19
|
+
|
|
20
|
+
for (const rec of catalog) {
|
|
21
|
+
const { state, supersededBy, problem } = entryLifecycle(rec);
|
|
22
|
+
// Замена, которой нет в каталоге, — это ответ «а что теперь», ведущий в никуда. Проверяется
|
|
23
|
+
// здесь, а не в чистой функции: та не знает про остальные записи и не должна знать.
|
|
24
|
+
const why = problem || (supersededBy && !slugs.has(supersededBy) ? L.lifecycle.unknownReplacement(supersededBy) : "");
|
|
25
|
+
if (why) bad++;
|
|
26
|
+
console.log(`${rec.slug}|${state}|${supersededBy || ""}|${why}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.exit(bad);
|
|
@@ -19,11 +19,14 @@
|
|
|
19
19
|
set -uo pipefail
|
|
20
20
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
21
21
|
CAT="$ROOT/kit/gates"
|
|
22
|
-
PASS=0; FAIL=0; SKIP=0
|
|
22
|
+
PASS=0; FAIL=0; SKIP=0; UNVERIFIED=0
|
|
23
23
|
|
|
24
24
|
ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; PASS=$((PASS+1)); }
|
|
25
25
|
bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; FAIL=$((FAIL+1)); }
|
|
26
26
|
skip() { printf ' \033[2m·\033[0m %s\n' "$1"; SKIP=$((SKIP+1)); }
|
|
27
|
+
# «Нечем проверить здесь» — не то же, что «переносимого рецепта нет». Первое зависит от машины
|
|
28
|
+
# и лечится установкой; второе — свойство записи. Счётчики разные, чтобы итог не врал о причине.
|
|
29
|
+
unverified() { printf ' \033[33m~\033[0m %s\n' "$1"; UNVERIFIED=$((UNVERIFIED+1)); }
|
|
27
30
|
|
|
28
31
|
printf '\n\033[1mtool/selfcheck/mutation.sh\033[0m\n\n'
|
|
29
32
|
[ -d "$CAT" ] || { echo " каталога гейтов нет"; exit 1; }
|
|
@@ -54,6 +57,22 @@ for GATE in "$CAT"/*/; do
|
|
|
54
57
|
# машине без неё «не проверено» было бы неотличимо от «проверено»: ровно та тишина, против
|
|
55
58
|
# которой всё это написано. Записи без `any` называются вслух, а не пропускаются молча.
|
|
56
59
|
RECIPE="$(sed -n 's/^[[:space:]]*any:[[:space:]]*\(.*\)$/\1/p' "$YML" | head -1)"
|
|
60
|
+
# Запись может назвать нужную ей программу полем `requires`: по первому слову команды этого
|
|
61
|
+
# не видно, обёртка вокруг готового инструмента начинается с `bash`. Без этой поправки
|
|
62
|
+
# `test-not-adjusted` на машине без `checkwash` выглядел как «зелёный образец покраснел», хотя
|
|
63
|
+
# покраснел он от отсутствия инструмента, а не от мутации.
|
|
64
|
+
NEEDS="$(sed -n 's/^requires:[[:space:]]*\(.*\)$/\1/p' "$YML" | head -1 | tr ',' ' ')"
|
|
65
|
+
LACK=""
|
|
66
|
+
for PROG in $NEEDS; do
|
|
67
|
+
command -v "$PROG" >/dev/null 2>&1 || LACK="${LACK:+$LACK, }$PROG"
|
|
68
|
+
done
|
|
69
|
+
# Два разных состояния и два разных счётчика: «переносимого рецепта нет вовсе» и «рецепт есть,
|
|
70
|
+
# но нужного инструмента нет здесь». Один счётчик на оба означал бы, что итог врёт про причину
|
|
71
|
+
# — ровно то, что мы не прощаем гейтам. Найдено код-ревью 2026-09-07.
|
|
72
|
+
if [ -n "$LACK" ]; then
|
|
73
|
+
unverified "$SLUG: НЕ ПРОВЕРЕНА здесь — нужен «$LACK»"
|
|
74
|
+
continue
|
|
75
|
+
fi
|
|
57
76
|
if [ -z "$RECIPE" ]; then
|
|
58
77
|
skip "$SLUG: нет переносимого рецепта — мутации проверять нечем"
|
|
59
78
|
continue
|
|
@@ -91,5 +110,6 @@ else
|
|
|
91
110
|
printf ' \033[31mвердикт поплыл: %s\033[0m' "$FAIL"
|
|
92
111
|
fi
|
|
93
112
|
[ "$SKIP" -gt 0 ] && printf ' \033[2m(без переносимого рецепта: %s)\033[0m' "$SKIP"
|
|
113
|
+
[ "$UNVERIFIED" -gt 0 ] && printf ' \033[33m(не проверено здесь: %s)\033[0m' "$UNVERIFIED"
|
|
94
114
|
printf '\n\n'
|
|
95
115
|
exit "$FAIL"
|
package/tool/selfcheck/smoke.sh
CHANGED
|
@@ -164,12 +164,18 @@ done
|
|
|
164
164
|
# --- 8. храповик: старое пропускает, новое не пускает ------------------------
|
|
165
165
|
# Главный вопрос к храповику: «может ли новый код добавить нарушение и пройти?»
|
|
166
166
|
# Может — значит это советчик, а не гейт.
|
|
167
|
+
# ПОЧЕМУ ПРИМАНКА ИМЕННО gate-not-weakened. У записи должен быть ТОЛЬКО переносимый рецепт:
|
|
168
|
+
# иначе на машине, где стоит ruff или eslint, установка возьмёт рецепт под язык, и проверка
|
|
169
|
+
# станет печатать чужой формат вывода. Так и вышло — прогон был зелёным локально и красным в
|
|
170
|
+
# конвейере ровно потому, что в конвейер добавили ruff: четыре проверки искали в выводе наши
|
|
171
|
+
# «почини: …» и «путь:строка», а получали формат ruff. Прогон, чей исход зависит от того, что
|
|
172
|
+
# случайно стоит на машине, не проверяет ничего.
|
|
167
173
|
R="$WORK/ratchet"; mkdir -p "$R"; cd "$R" || exit 1
|
|
168
174
|
git init -q .
|
|
169
|
-
printf '
|
|
175
|
+
printf 'x = 1 # noqa\n' > old.py
|
|
170
176
|
node "$CLI" init >/dev/null 2>&1
|
|
171
|
-
node "$CLI" add
|
|
172
|
-
node "$CLI" ratchet
|
|
177
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
178
|
+
node "$CLI" ratchet gate-not-weakened >/dev/null 2>&1
|
|
173
179
|
|
|
174
180
|
# Судим по вердикту гейта, а не по коду возврата doctor: он ненулевой и по другим
|
|
175
181
|
# причинам (в свежей папке нет .gitignore), и проверка бы врала о храповике.
|
|
@@ -179,7 +185,7 @@ case "$OUT" in
|
|
|
179
185
|
*) ok "храповик пропустил старое нарушение" ;;
|
|
180
186
|
esac
|
|
181
187
|
|
|
182
|
-
printf '
|
|
188
|
+
printf 'y = 2 # noqa\n' > new.py
|
|
183
189
|
OUT="$(node "$CLI" doctor --run 2>&1)"
|
|
184
190
|
case "$OUT" in
|
|
185
191
|
*"новых нарушений"*) ok "храповик не пустил новое нарушение" ;;
|
|
@@ -189,7 +195,7 @@ esac
|
|
|
189
195
|
# Второй прогон с тем же новым нарушением обязан краснеть так же. Пока реестр перезаписывался
|
|
190
196
|
# всем текущим списком, одно исправленное нарушение затягивало в долг ВСЕ новые: один красный
|
|
191
197
|
# прогон — и дальше зелено навсегда. «Может ли новый код добавить нарушение и пройти?» — мог.
|
|
192
|
-
printf '
|
|
198
|
+
printf 'z = 3 # noqa\n' > another.py
|
|
193
199
|
node "$CLI" doctor --run >/dev/null 2>&1
|
|
194
200
|
rm old.py
|
|
195
201
|
OUT="$(node "$CLI" doctor --run 2>&1)"
|
|
@@ -199,12 +205,12 @@ case "$OUT2" in
|
|
|
199
205
|
*) bad "исправление одного нарушения затянуло новые в долг" "второй прогон зелёный" ;;
|
|
200
206
|
esac
|
|
201
207
|
rm -f another.py
|
|
202
|
-
printf '
|
|
203
|
-
node "$CLI" ratchet
|
|
208
|
+
printf 'x = 1 # noqa\n' > old.py
|
|
209
|
+
node "$CLI" ratchet gate-not-weakened >/dev/null 2>&1 || true
|
|
204
210
|
|
|
205
211
|
rm -f new.py old.py
|
|
206
212
|
node "$CLI" doctor --run >/dev/null 2>&1
|
|
207
|
-
if grep -q 'old.py' ratchets/
|
|
213
|
+
if grep -q 'old.py' ratchets/gate-not-weakened.txt; then
|
|
208
214
|
bad "исправленное осталось в реестре — храповик не затягивается"
|
|
209
215
|
else
|
|
210
216
|
ok "исправленное вычеркнуто из реестра"
|
|
@@ -250,27 +256,13 @@ case "$OUT" in
|
|
|
250
256
|
esac
|
|
251
257
|
cd "$WORK" || exit 1
|
|
252
258
|
|
|
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
259
|
# --- 9. разбор ошибки: три случая различаются прогоном, а не памятью ---------
|
|
268
260
|
# «Сторожа не было», «сторож был и не сработал», «сторож был и его обошли» — разные починки.
|
|
269
261
|
# Пока их различает человек по памяти, чинят обычно не то. Здесь их различает прогон.
|
|
270
262
|
Y="$WORK/why"; mkdir -p "$Y"; cd "$Y" || exit 1
|
|
271
263
|
git init -q .
|
|
272
264
|
node "$CLI" init >/dev/null 2>&1
|
|
273
|
-
printf '
|
|
265
|
+
printf 'q = 1 # noqa\n' > x.py
|
|
274
266
|
|
|
275
267
|
OUT="$(node "$CLI" why "миграция базы применена задом наперёд" 2>&1)"
|
|
276
268
|
case "$OUT" in
|
|
@@ -278,21 +270,21 @@ case "$OUT" in
|
|
|
278
270
|
*) bad "why не сказал, что сторожа не было" "$OUT" ;;
|
|
279
271
|
esac
|
|
280
272
|
|
|
281
|
-
OUT="$(node "$CLI" why "
|
|
273
|
+
OUT="$(node "$CLI" why "подавление проверки целиком без причины" 2>&1)"
|
|
282
274
|
case "$OUT" in
|
|
283
275
|
*"не поставлен"*) ok "why: запись есть в каталоге, но в проекте не поставлена" ;;
|
|
284
276
|
*) bad "why не отличил «не поставлен» от «не было»" "$OUT" ;;
|
|
285
277
|
esac
|
|
286
278
|
|
|
287
|
-
node "$CLI" add
|
|
288
|
-
OUT="$(node "$CLI" why "
|
|
279
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
280
|
+
OUT="$(node "$CLI" why "подавление проверки целиком без причины" 2>&1)"
|
|
289
281
|
case "$OUT" in
|
|
290
282
|
*"его обошли"*) ok "why: сторож стоит и ловит — значит его обошли" ;;
|
|
291
283
|
*) bad "why не отличил «обошли» от «не сработал»" "$OUT" ;;
|
|
292
284
|
esac
|
|
293
285
|
|
|
294
286
|
rm x.py
|
|
295
|
-
OUT="$(node "$CLI" why "
|
|
287
|
+
OUT="$(node "$CLI" why "подавление проверки целиком без причины" 2>&1)"
|
|
296
288
|
case "$OUT" in
|
|
297
289
|
*"этой поломки не видит"*) ok "why: сторож стоит, а поломки не видит" ;;
|
|
298
290
|
*) bad "why не отличил «не сработал» от «обошли»" "$OUT" ;;
|
|
@@ -579,11 +571,11 @@ rm -rf "$FBDIR" "$FBPROJ"
|
|
|
579
571
|
REPDIR="$(mktemp -d)"
|
|
580
572
|
(
|
|
581
573
|
cd "$REPDIR" && git init -q . && mkdir -p src &&
|
|
582
|
-
printf '
|
|
574
|
+
printf 'a = 1 # noqa\n' > src/a.py &&
|
|
583
575
|
node "$CLI" start > /tmp/aqk-start.log 2>&1
|
|
584
576
|
)
|
|
585
577
|
REP_OUT=$( cd "$REPDIR" && node "$CLI" report 2>&1 ); REP_CODE=$?
|
|
586
|
-
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌
|
|
578
|
+
if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌ gate-not-weakened'; then
|
|
587
579
|
ok "report краснеет кодом возврата и называет упавший гейт"
|
|
588
580
|
else
|
|
589
581
|
# Код возврата отчёта не говорит, ПОЧЕМУ он ноль: гейт не сработал, не установился или
|
|
@@ -591,7 +583,7 @@ else
|
|
|
591
583
|
# и «проверка не ловит на этой системе».
|
|
592
584
|
G_LS=$( cd "$REPDIR" && ls gates 2>&1 | tr '\n' ' ' )
|
|
593
585
|
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/
|
|
586
|
+
G_OUT=$( cd "$REPDIR" && bash gates/gate-not-weakened/check.sh . 2>&1 | head -2 ); G_CODE=$?
|
|
595
587
|
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
588
|
fi
|
|
597
589
|
if [ -f "$REPDIR/.aqk/report.md" ] && grep -q '^## ' "$REPDIR/.aqk/report.md"; then
|
|
@@ -631,11 +623,11 @@ rm -rf "$NOTEDIR" "$NOTEHOME"
|
|
|
631
623
|
# то есть настройка правкой чужого файла, которую затрёт следующий `aqk add`.
|
|
632
624
|
IGNDIR="$(mktemp -d)"
|
|
633
625
|
mkdir -p "$IGNDIR/third-party/inner" "$IGNDIR/src"
|
|
634
|
-
printf '
|
|
635
|
-
printf '
|
|
636
|
-
OUT_BEFORE="$(bash "$ROOT/kit/gates/
|
|
626
|
+
printf 'mine = 1 # noqa\n' > "$IGNDIR/src/mine.py"
|
|
627
|
+
printf 'theirs = 1 # noqa\n' > "$IGNDIR/third-party/inner/theirs.py"
|
|
628
|
+
OUT_BEFORE="$(bash "$ROOT/kit/gates/gate-not-weakened/check.sh" "$IGNDIR" 2>&1)"
|
|
637
629
|
printf '# принесено из другого репозитория\nthird-party/\n' > "$IGNDIR/.aqkignore"
|
|
638
|
-
OUT_AFTER="$(bash "$ROOT/kit/gates/
|
|
630
|
+
OUT_AFTER="$(bash "$ROOT/kit/gates/gate-not-weakened/check.sh" "$IGNDIR" 2>&1)"
|
|
639
631
|
if printf '%s' "$OUT_BEFORE" | grep -q 'theirs.py' &&
|
|
640
632
|
! printf '%s' "$OUT_AFTER" | grep -q 'theirs.py' &&
|
|
641
633
|
printf '%s' "$OUT_AFTER" | grep -q 'mine.py'; then
|
|
@@ -893,17 +885,318 @@ rm -rf "$BDIR2"
|
|
|
893
885
|
# Воспроизводим без Windows: урезаем PATH до одного node — инструментов не видно так же.
|
|
894
886
|
NRDIR="$(mktemp -d)"; NRBIN="$(mktemp -d)"
|
|
895
887
|
ln -sf "$(command -v node)" "$NRBIN/node"
|
|
896
|
-
( cd "$NRDIR" && git init -q . && mkdir -p src && printf '
|
|
888
|
+
( cd "$NRDIR" && git init -q . && mkdir -p src && printf 'a = 1 # noqa\n' > src/a.py )
|
|
897
889
|
NR_OUT=$( cd "$NRDIR" && PATH="$NRBIN" node "$CLI" start 2>&1 ); NR_CODE=$?
|
|
898
890
|
NR_GATES=$( ls "$NRDIR/gates" 2>/dev/null | grep -cv '^_' )
|
|
899
891
|
if [ "$NR_CODE" -eq 0 ] && [ "$NR_GATES" -ge 5 ] &&
|
|
900
|
-
[ -f "$NRDIR/gates/
|
|
892
|
+
[ -f "$NRDIR/gates/gate-not-weakened/check.sh" ]; then
|
|
901
893
|
ok "start пропускает запись без пригодного инструмента и ставит остальные ($NR_GATES)"
|
|
902
894
|
else
|
|
903
895
|
bad "start бросил установку из-за одной записи" "код $NR_CODE, поставлено $NR_GATES, хвост: $(printf '%s' "$NR_OUT" | tail -2 | tr '\n' ' ')"
|
|
904
896
|
fi
|
|
905
897
|
rm -rf "$NRDIR" "$NRBIN"
|
|
906
898
|
|
|
899
|
+
# --- 48г. подделка не берёт уровень выше первого -------------------------------
|
|
900
|
+
# ЗАЧЕМ. Ступень AQK-2 называлась «гейты доказаны» и проверяла, что существуют две папки.
|
|
901
|
+
# Проект с гейтами `true` — командой, которая всегда отвечает «ок», — проходил порог AQK-3 и
|
|
902
|
+
# получал зелёный значок в README. Проверено прогоном 2026-09-07: три «гейта», ноль защиты,
|
|
903
|
+
# высший уровень. Это `pytest || true` на уровне всего стандарта.
|
|
904
|
+
FAKEDIR="$(mktemp -d)"
|
|
905
|
+
(
|
|
906
|
+
cd "$FAKEDIR" && git init -q .
|
|
907
|
+
mkdir -p gates ratchets incidents && printf '# Журнал\n' > incidents/README.md
|
|
908
|
+
# Манифест пишется целиком, а не правится python-ом: проверка, которая на машине без python3
|
|
909
|
+
# молча собирает ДРУГОЙ проект, доказывает не то, что называет. Найдено код-ревью 2026-09-07.
|
|
910
|
+
{
|
|
911
|
+
echo 'aqk: "0.6.0"'
|
|
912
|
+
echo 'entry:'
|
|
913
|
+
echo ' - AGENTS.md'
|
|
914
|
+
echo 'rules: .aqk/rules'
|
|
915
|
+
echo 'gates:'
|
|
916
|
+
echo ' lint: "true"'
|
|
917
|
+
echo ' test: "true"'
|
|
918
|
+
echo ' security: "true"'
|
|
919
|
+
echo 'samples: gates'
|
|
920
|
+
echo 'ratchets: ratchets'
|
|
921
|
+
echo 'lessons: incidents'
|
|
922
|
+
} > .aqk.yml
|
|
923
|
+
printf '# Свод\n' > AGENTS.md
|
|
924
|
+
mkdir -p .aqk/rules && printf '# правила\n' > .aqk/rules/general.md
|
|
925
|
+
)
|
|
926
|
+
FAKE_OUT=$( cd "$FAKEDIR" && node "$CLI" doctor --run --min 3 2>&1 ); FAKE_CODE=$?
|
|
927
|
+
FAKE_PROVE=$( cd "$FAKEDIR" && node "$CLI" prove 2>&1 ); FAKE_PCODE=$?
|
|
928
|
+
# Проверяем ИМЕННО ту ступень, что назначена: без этого `grep 'AQK-1|AQK-3'` совпадал всегда,
|
|
929
|
+
# потому что doctor печатает все четыре строки в любом исходе. Пустая проверка хуже отсутствующей.
|
|
930
|
+
if [ "$FAKE_CODE" -ne 0 ] && [ "$FAKE_PCODE" -ne 0 ] &&
|
|
931
|
+
printf '%s' "$FAKE_OUT" | grep -qE 'AQK-1\.?$|AQK-1[^0-9]' &&
|
|
932
|
+
printf '%s' "$FAKE_OUT" | grep -qiE '(НЕ пройден|not passed|not reached)'; then
|
|
933
|
+
ok "гейт «true» не берёт уровень выше первого"
|
|
934
|
+
else
|
|
935
|
+
bad "подделка получила уровень" "порог: код $FAKE_CODE, доказательство: код $FAKE_PCODE, хвост: $(printf '%s' "$FAKE_OUT" | tail -2 | tr '\n' ' ')"
|
|
936
|
+
fi
|
|
937
|
+
rm -rf "$FAKEDIR"
|
|
938
|
+
|
|
939
|
+
# --- 48в. отказ установки называет, что поставить ------------------------------
|
|
940
|
+
# ЗАЧЕМ. Запись, у которой остался только рецепт под язык (`no-print-in-prod`, `swallowed-error`,
|
|
941
|
+
# `dead-code`), на машине без нужного инструмента ставиться не может — и это законно. Но отказ
|
|
942
|
+
# обязан быть действием, а не тупиком: «нет команды ни под python, ни общей» не говорит человеку
|
|
943
|
+
# ничего. Правило «находка без действия закрывает окно, а не дефект» — то же самое, что мы
|
|
944
|
+
# требуем от записей каталога, и к собственной программе оно относится так же.
|
|
945
|
+
# Найдено первым прогоном в чужом репозитории (httpx), 2026-09-07.
|
|
946
|
+
NODIR="$(mktemp -d)"; NOBIN="$(mktemp -d)"
|
|
947
|
+
ln -sf "$(command -v node)" "$NOBIN/node"
|
|
948
|
+
( cd "$NODIR" && git init -q . && mkdir -p src && printf 'x = 1\n' > src/a.py && PATH="$NOBIN" node "$CLI" init >/dev/null 2>&1 )
|
|
949
|
+
NO_OUT=$( cd "$NODIR" && PATH="$NOBIN" node "$CLI" add no-print-in-prod 2>&1 ); NO_CODE=$?
|
|
950
|
+
# Имя инструмента в отказе БЫЛО и до этой проверки — не хватало действия. Требуем оба:
|
|
951
|
+
# «ruff не установлен» — это диагноз, «поставь ruff» — это то, ради чего человек читает.
|
|
952
|
+
if [ "$NO_CODE" -ne 0 ] &&
|
|
953
|
+
printf '%s' "$NO_OUT" | grep -q 'ruff' &&
|
|
954
|
+
printf '%s' "$NO_OUT" | grep -qiE '(почини|поставь|install)'; then
|
|
955
|
+
ok "отказ установки называет инструмент И что с ним делать"
|
|
956
|
+
else
|
|
957
|
+
bad "отказ установки — тупик" "код $NO_CODE, вывод: $(printf '%s' "$NO_OUT" | tr '\n' ' ' | tail -c 200)"
|
|
958
|
+
fi
|
|
959
|
+
rm -rf "$NODIR" "$NOBIN"
|
|
960
|
+
|
|
961
|
+
# --- 48б. строгий режим приёмки: «не проверено» становится ошибкой -------------
|
|
962
|
+
# ЗАЧЕМ. «Записи не проверить, нет инструмента» — законное состояние на чужой машине и
|
|
963
|
+
# недопустимое на нашей: там инструменты ставит отдельный шаг, и жёлтая тильда вместо красного
|
|
964
|
+
# означает, что шаг не сработал. Ровно так и было: `pipx install vulture` стоял в конвейере с
|
|
965
|
+
# самого его появления, ставил vulture в каталог вне PATH, и запись dead-code не проверялась
|
|
966
|
+
# конвейером ни разу. Проверяем сам переключатель: на записи с заведомо отсутствующим
|
|
967
|
+
# инструментом обычный прогон зелёный, строгий — красный.
|
|
968
|
+
STPKG="$(mktemp -d)"
|
|
969
|
+
cp -r "$ROOT/tool" "$ROOT/kit" "$ROOT/package.json" "$STPKG/" 2>/dev/null
|
|
970
|
+
# Ломаем рецепт одной записи: программы с таким именем на машине нет ни у кого.
|
|
971
|
+
sed -i.bak 's|^ python: vulture .*| python: aqk-nesuschestvuyuschiy-instrument {dir}|' \
|
|
972
|
+
"$STPKG/kit/gates/dead-code/gate.yml" 2>/dev/null
|
|
973
|
+
# Переключатель задаётся явно в ОБЕ стороны. Без этого проверка наследовала AQK_GATES_STRICT
|
|
974
|
+
# из окружения — и в конвейере, где он поднят, «обычный» прогон был бы строгим, а проверка
|
|
975
|
+
# переключателя проверяла бы одно и то же дважды.
|
|
976
|
+
ST_SOFT_OUT=$(AQK_GATES_STRICT=0 bash "$STPKG/tool/selfcheck/gates.sh" 2>&1); ST_SOFT=$?
|
|
977
|
+
ST_HARD_OUT=$(AQK_GATES_STRICT=1 bash "$STPKG/tool/selfcheck/gates.sh" 2>&1); ST_HARD=$?
|
|
978
|
+
if [ "$ST_SOFT" -eq 0 ] && [ "$ST_HARD" -ne 0 ] &&
|
|
979
|
+
printf '%s' "$ST_HARD_OUT" | grep -q 'строгий режим'; then
|
|
980
|
+
ok "строгий режим делает «не проверено» ошибкой, обычный — нет"
|
|
981
|
+
else
|
|
982
|
+
bad "строгий режим приёмки не работает" "обычный: $ST_SOFT, строгий: $ST_HARD"
|
|
983
|
+
fi
|
|
984
|
+
rm -rf "$STPKG"
|
|
985
|
+
|
|
986
|
+
# --- 49. выведенную запись не ставят, а называют преемника --------------------
|
|
987
|
+
# ЗАЧЕМ. Зрелость записи считается по доказательству, и объявить её нельзя — кроме одного
|
|
988
|
+
# состояния: `deprecated`. Оно объявляется, и весь его смысл в отказе: запись, которую всё ещё
|
|
989
|
+
# можно поставить одной командой, не выведена, а просто помечена. Проверяем сам отказ и то, что
|
|
990
|
+
# папка гейта в проекте НЕ появилась: половина установки хуже, чем её отсутствие.
|
|
991
|
+
#
|
|
992
|
+
# Каталог мутируем в КОПИИ пакета, а не в этом репозитории: проверка, которая правит собственные
|
|
993
|
+
# исходники, однажды упадёт посередине и оставит дерево грязным.
|
|
994
|
+
DEPKG="$(mktemp -d)"; DEPRJ="$(mktemp -d)"
|
|
995
|
+
cp -r "$ROOT/tool" "$ROOT/kit" "$ROOT/package.json" "$DEPKG/" 2>/dev/null
|
|
996
|
+
printf 'lifecycle: deprecated\nsuperseded_by: no-print-in-prod\n' >> "$DEPKG/kit/gates/todo-without-task/gate.yml"
|
|
997
|
+
( cd "$DEPRJ" && git init -q . && printf 'x = 1\n' > a.py && node "$DEPKG/tool/program.mjs" init >/dev/null 2>&1 )
|
|
998
|
+
DE_OUT=$( cd "$DEPRJ" && node "$DEPKG/tool/program.mjs" add todo-without-task 2>&1 ); DE_CODE=$?
|
|
999
|
+
if [ "$DE_CODE" -ne 0 ] &&
|
|
1000
|
+
printf '%s' "$DE_OUT" | grep -q 'no-print-in-prod' &&
|
|
1001
|
+
[ ! -d "$DEPRJ/gates/todo-without-task" ]; then
|
|
1002
|
+
ok "add отказывает в выведенной записи и называет ту, что её заменяет"
|
|
1003
|
+
else
|
|
1004
|
+
bad "выведенная запись установилась или преемник не назван" "код $DE_CODE, папка: $([ -d "$DEPRJ/gates/todo-without-task" ] && echo есть || echo нет)"
|
|
1005
|
+
fi
|
|
1006
|
+
rm -rf "$DEPKG" "$DEPRJ"
|
|
1007
|
+
|
|
1008
|
+
# --- 50. --since показывает только то, что внёс диф ---------------------------
|
|
1009
|
+
# ЗАЧЕМ. Первый прогон в живом проекте показывает долг за все годы. Стену красного не разбирают
|
|
1010
|
+
# — проверку выключают целиком. Проверяем три исхода разом: старый долг молчит, новый краснеет,
|
|
1011
|
+
# а гейт, который печатает вердикт без путей, НЕ становится зелёным от того, что его нечем сузить.
|
|
1012
|
+
SCDIR="$(mktemp -d)"
|
|
1013
|
+
(
|
|
1014
|
+
cd "$SCDIR" && git init -q . && git config user.email t@t && git config user.name t
|
|
1015
|
+
mkdir -p src && printf 'old = 1 # noqa\n' > src/old.py
|
|
1016
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1017
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1018
|
+
git add -A && git commit -qm "база" >/dev/null 2>&1
|
|
1019
|
+
printf 'fresh = 1 # noqa\n' > src/fresh.py
|
|
1020
|
+
)
|
|
1021
|
+
SC_WIDE=$( cd "$SCDIR" && node "$CLI" doctor --run 2>&1 )
|
|
1022
|
+
SC_NARROW=$( cd "$SCDIR" && node "$CLI" doctor --run --since HEAD 2>&1 )
|
|
1023
|
+
if printf '%s' "$SC_WIDE" | grep -q 'old.py' &&
|
|
1024
|
+
printf '%s' "$SC_NARROW" | grep -q 'fresh.py' &&
|
|
1025
|
+
! printf '%s' "$SC_NARROW" | grep -q 'old.py'; then
|
|
1026
|
+
ok "--since прячет старый долг и показывает внесённый дифом"
|
|
1027
|
+
else
|
|
1028
|
+
bad "--since сузил не то" "широкий: $(printf '%s' "$SC_WIDE" | grep -c 'py:'), узкий: $(printf '%s' "$SC_NARROW" | grep -c 'py:')"
|
|
1029
|
+
fi
|
|
1030
|
+
# Несуществующая ссылка обязана быть отказом, а не тихим «сравнили с ничем».
|
|
1031
|
+
SC_BAD=$( cd "$SCDIR" && node "$CLI" doctor --run --since net-takoy-vetki 2>&1 ); SC_BADCODE=$?
|
|
1032
|
+
if [ "$SC_BADCODE" -ne 0 ] && printf '%s' "$SC_BAD" | grep -qi 'net-takoy-vetki'; then
|
|
1033
|
+
ok "--since с несуществующей ссылкой — отказ, а не тихое сравнение с ничем"
|
|
1034
|
+
else
|
|
1035
|
+
bad "--since проглотил неверную ссылку" "код $SC_BADCODE"
|
|
1036
|
+
fi
|
|
1037
|
+
rm -rf "$SCDIR"
|
|
1038
|
+
|
|
1039
|
+
# --- 52. совет по починке не теряется в обрезке --------------------------------
|
|
1040
|
+
# ЗАЧЕМ. Все записи каталога печатают «почини: …» последней строкой, а прогон показывал три
|
|
1041
|
+
# первые и обрезал остальное — то есть ровно ту строку, ради которой человек смотрит на красное,
|
|
1042
|
+
# он не видел никогда. Находка без действия закрывает окно, а не дефект.
|
|
1043
|
+
ADIR="$(mktemp -d)"
|
|
1044
|
+
(
|
|
1045
|
+
cd "$ADIR" && git init -q . && mkdir -p src
|
|
1046
|
+
for n in a b c d e; do printf '%s = 1 # noqa\n' "$n" > "src/$n.py"; done
|
|
1047
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1048
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1049
|
+
)
|
|
1050
|
+
A_OUT=$( cd "$ADIR" && node "$CLI" doctor --run 2>&1 )
|
|
1051
|
+
if printf '%s' "$A_OUT" | grep -qiE '(почини|fix)[[:space:]]*:' &&
|
|
1052
|
+
printf '%s' "$A_OUT" | grep -qE 'py:[0-9]+'; then
|
|
1053
|
+
ok "совет по починке виден при обрезанных находках"
|
|
1054
|
+
else
|
|
1055
|
+
bad "совет по починке потерялся" "находок в выводе: $(printf '%s' "$A_OUT" | grep -c 'py:')"
|
|
1056
|
+
fi
|
|
1057
|
+
rm -rf "$ADIR"
|
|
1058
|
+
|
|
1059
|
+
# --- 53. у долга есть цель и срок, и оба с последствием ------------------------
|
|
1060
|
+
# ЗАЧЕМ. Реестр, который может только сокращаться, всё равно не знает, когда кончится, — и
|
|
1061
|
+
# потому не кончается. Цель и срок без машинного последствия были бы украшением, поэтому
|
|
1062
|
+
# проверяем ровно последствия: срок вышел — красное; цель достигнута — сказано вслух.
|
|
1063
|
+
RDIR="$(mktemp -d)"
|
|
1064
|
+
(
|
|
1065
|
+
cd "$RDIR" && git init -q . && mkdir -p src && printf 'a = 1 # noqa\n' > src/a.py
|
|
1066
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1067
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1068
|
+
node "$CLI" ratchet gate-not-weakened >/dev/null 2>&1
|
|
1069
|
+
)
|
|
1070
|
+
R_REG="$RDIR/ratchets/gate-not-weakened.txt"
|
|
1071
|
+
if [ -f "$R_REG" ] && grep -q 'aqk-goal' "$R_REG"; then
|
|
1072
|
+
# Долг снят, новых нарушений нет — зелено.
|
|
1073
|
+
R_BASE=$( cd "$RDIR" && node "$CLI" doctor --run 2>&1 ); R_BASE_CODE=$?
|
|
1074
|
+
# Срок в прошлом — обязано покраснеть без единого нового нарушения.
|
|
1075
|
+
sed -i.bak 's/^# aqk-deadline:.*/# aqk-deadline: 2020-01-01/' "$R_REG"
|
|
1076
|
+
R_LATE=$( cd "$RDIR" && node "$CLI" doctor --run 2>&1 )
|
|
1077
|
+
if printf '%s' "$R_LATE" | grep -qi 'срок\|deadline'; then
|
|
1078
|
+
ok "срок долга вышел — храповик краснеет без новых нарушений"
|
|
1079
|
+
else
|
|
1080
|
+
bad "просроченный долг прошёл молча" "код базового прогона $R_BASE_CODE"
|
|
1081
|
+
fi
|
|
1082
|
+
# Цель заведомо достигнута — храповик обязан сказать, что обёртку пора убрать.
|
|
1083
|
+
sed -i.bak 's/^# aqk-deadline:.*/# aqk-deadline:/; s/^# aqk-goal:.*/# aqk-goal: 99/' "$R_REG"
|
|
1084
|
+
R_DONE=$( cd "$RDIR" && node "$CLI" doctor --run 2>&1 )
|
|
1085
|
+
if printf '%s' "$R_DONE" | grep -qi 'погашен\|paid off'; then
|
|
1086
|
+
ok "цель достигнута — храповик говорит убрать обёртку"
|
|
1087
|
+
else
|
|
1088
|
+
bad "погашенный долг не назван" "$(printf '%s' "$R_DONE" | grep -i ratchet | head -1)"
|
|
1089
|
+
fi
|
|
1090
|
+
else
|
|
1091
|
+
bad "реестр долга не создан или без цели" "$R_REG"
|
|
1092
|
+
fi
|
|
1093
|
+
rm -rf "$RDIR"
|
|
1094
|
+
|
|
1095
|
+
# --- 54. упавший гейт не стирает реестр долга ----------------------------------
|
|
1096
|
+
# ЗАЧЕМ. Провал без единой разобранной находки — это отказ инструмента, а не чистый прогон.
|
|
1097
|
+
# Храповик вычёркивал ВЕСЬ реестр как исправленный, возвращал ноль и — после появления цели —
|
|
1098
|
+
# предлагал снять защиту: «долг погашен, убери обёртку». Снятие защиты по итогам прогона,
|
|
1099
|
+
# которого не было. Найдено ревью 2026-09-06.
|
|
1100
|
+
WDIR="$(mktemp -d)"
|
|
1101
|
+
mkdir -p "$WDIR/ratchets"
|
|
1102
|
+
printf '# Реестр долга: проба\n# aqk-goal: 0\nsrc/a.py: печать\n' > "$WDIR/ratchets/t.txt"
|
|
1103
|
+
W_OUT=$( cd "$WDIR" && bash "$ROOT/kit/ratchet/ratchet.sh" ratchets/t.txt sh -c 'exit 3' 2>&1 ); W_CODE=$?
|
|
1104
|
+
W_LEFT=$(grep -c 'src/a.py' "$WDIR/ratchets/t.txt" || true)
|
|
1105
|
+
if [ "$W_CODE" -ne 0 ] && [ "$W_LEFT" -eq 1 ] &&
|
|
1106
|
+
! printf '%s' "$W_OUT" | grep -qi 'погашен'; then
|
|
1107
|
+
ok "упавший гейт не стирает реестр и не предлагает снять защиту"
|
|
1108
|
+
else
|
|
1109
|
+
bad "упавший гейт съел реестр" "код $W_CODE, строк долга осталось $W_LEFT"
|
|
1110
|
+
fi
|
|
1111
|
+
# Директива с опечаткой обязана быть слышной: молчаливо отключённая цель — та же тишина.
|
|
1112
|
+
printf '# Реестр\n# aqk-goal: скоро\nsrc/a.py: печать\n' > "$WDIR/ratchets/t.txt"
|
|
1113
|
+
W_BAD=$( cd "$WDIR" && bash "$ROOT/kit/ratchet/ratchet.sh" ratchets/t.txt sh -c 'echo "src/a.py: печать"' 2>&1 )
|
|
1114
|
+
if printf '%s' "$W_BAD" | grep -qi 'не число'; then
|
|
1115
|
+
ok "опечатка в директиве храповика названа, а не проглочена"
|
|
1116
|
+
else
|
|
1117
|
+
bad "нечисловая цель отключилась молча" "$(printf '%s' "$W_BAD" | head -1)"
|
|
1118
|
+
fi
|
|
1119
|
+
rm -rf "$WDIR"
|
|
1120
|
+
|
|
1121
|
+
# --- 55. просроченный долг краснеет и при сужении по дифу ----------------------
|
|
1122
|
+
# ЗАЧЕМ. Сообщение храповика про срок называет путь к реестру, а реестра в дифе нет: фильтр по
|
|
1123
|
+
# путям отбрасывал единственную строку, находок не оставалось, и гейт печатался зелёным с
|
|
1124
|
+
# пометкой «находки вне дифа». То есть `--since` отменял правило SPEC.md §7.6 ровно в том
|
|
1125
|
+
# режиме, в котором его и запускают. Найдено ревью 2026-09-06.
|
|
1126
|
+
DDIR="$(mktemp -d)"
|
|
1127
|
+
(
|
|
1128
|
+
cd "$DDIR" && git init -q . && git config user.email t@t && git config user.name t
|
|
1129
|
+
mkdir -p src && printf 'a = 1 # noqa\n' > src/a.py
|
|
1130
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1131
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1132
|
+
node "$CLI" ratchet gate-not-weakened >/dev/null 2>&1
|
|
1133
|
+
sed -i.bak 's/^# aqk-deadline:.*/# aqk-deadline: 2020-01-01/' ratchets/gate-not-weakened.txt
|
|
1134
|
+
git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1
|
|
1135
|
+
)
|
|
1136
|
+
D_WIDE=$( cd "$DDIR" && node "$CLI" doctor --run 2>&1 )
|
|
1137
|
+
D_NARROW=$( cd "$DDIR" && node "$CLI" doctor --run --since HEAD 2>&1 )
|
|
1138
|
+
if printf '%s' "$D_WIDE" | grep -q 'gate-not-weakened' &&
|
|
1139
|
+
printf '%s' "$D_NARROW" | grep -qE 'gate-not-weakened.*(код|exit)' ; then
|
|
1140
|
+
ok "просроченный долг краснеет и при --since"
|
|
1141
|
+
else
|
|
1142
|
+
bad "--since отменил срок долга" "узкий прогон: $(printf '%s' "$D_NARROW" | grep no-print | head -1 | cut -c1-90)"
|
|
1143
|
+
fi
|
|
1144
|
+
rm -rf "$DDIR"
|
|
1145
|
+
|
|
1146
|
+
# --- 56. совещательный гейт показан, но прогон не уронен -----------------------
|
|
1147
|
+
# ЗАЧЕМ. Правило вводят в проект, где старый код ему не соответствует. Без третьего пути выбор
|
|
1148
|
+
# из двух крайностей: включить и сломать сборку либо не включать вовсе. Проверяем обе стороны:
|
|
1149
|
+
# без списка — роняет; со списком — показано и НАЗВАНО, а прогон зелёный. Молчание о
|
|
1150
|
+
# совещательном гейте было бы выключенной проверкой, притворяющейся отсутствующей.
|
|
1151
|
+
VDIR="$(mktemp -d)"
|
|
1152
|
+
(
|
|
1153
|
+
cd "$VDIR" && git init -q . && mkdir -p src && printf 'a = 1 # noqa\n' > src/a.py
|
|
1154
|
+
node "$CLI" init >/dev/null 2>&1
|
|
1155
|
+
node "$CLI" add gate-not-weakened >/dev/null 2>&1
|
|
1156
|
+
)
|
|
1157
|
+
( cd "$VDIR" && node "$CLI" doctor --run --min 1 >/dev/null 2>&1 ); V_HARD=$?
|
|
1158
|
+
printf '\nadvisory:\n - gate-not-weakened\n' >> "$VDIR/.aqk.yml"
|
|
1159
|
+
V_OUT=$( cd "$VDIR" && node "$CLI" doctor --run --min 1 2>&1 ); V_SOFT=$?
|
|
1160
|
+
if [ "$V_HARD" -ne 0 ] && [ "$V_SOFT" -eq 0 ] &&
|
|
1161
|
+
printf '%s' "$V_OUT" | grep -qE 'advisory|совещательн' &&
|
|
1162
|
+
printf '%s' "$V_OUT" | grep -q 'src/a.py'; then
|
|
1163
|
+
ok "совещательный гейт показывает находки, называется и не роняет прогон"
|
|
1164
|
+
else
|
|
1165
|
+
bad "совещательный режим работает не так" "обычный код $V_HARD, совещательный $V_SOFT"
|
|
1166
|
+
fi
|
|
1167
|
+
# Опечатка в имени поля обязана быть названа: «advisery:» молча означало бы «совещательных нет»,
|
|
1168
|
+
# и правило, которое человек считал введённым, роняло бы сборку.
|
|
1169
|
+
sed -i.bak 's/^advisory:/advisery:/' "$VDIR/.aqk.yml"
|
|
1170
|
+
V_TYPO=$( cd "$VDIR" && node "$CLI" doctor 2>&1 )
|
|
1171
|
+
if printf '%s' "$V_TYPO" | grep -qi 'advisery'; then
|
|
1172
|
+
ok "опечатка в имени поля манифеста названа"
|
|
1173
|
+
else
|
|
1174
|
+
bad "опечатка в advisory проглочена" "$(printf '%s' "$V_TYPO" | tail -2 | head -1)"
|
|
1175
|
+
fi
|
|
1176
|
+
rm -rf "$VDIR"
|
|
1177
|
+
|
|
1178
|
+
# --- 81. версия в README и llms.txt та же, что в package.json ---------------
|
|
1179
|
+
# Обе строки — инструкция по установке для постороннего: `rev:` для pre-commit и `@vX.Y.Z` для
|
|
1180
|
+
# GitHub Action. Устаревшая ставит человеку не тот комплект и молчит об этом: команда проходит,
|
|
1181
|
+
# ставится прошлогодняя версия. AGENTS.md требует, чтобы README и llms.txt не расходились, —
|
|
1182
|
+
# и без прибора требование не работало: на 2026-09-08 в README стояло v0.6.0, а в llms.txt
|
|
1183
|
+
# v0.4.2, отставание на два выпуска. Проверка сравнивает обе с package.json, а не друг с другом:
|
|
1184
|
+
# совпасть друг с другом они могут и будучи одинаково устаревшими.
|
|
1185
|
+
VERS_BAD=""
|
|
1186
|
+
for VF in README.md llms.txt; do
|
|
1187
|
+
[ -f "$ROOT/$VF" ] || continue
|
|
1188
|
+
# Берём только версии AQK — «v1.2.3» в примерах чужих действий (actions/checkout@v4) не наши.
|
|
1189
|
+
for V in $(grep -oE '(rev:[[:space:]]*|Agent_Quality_Kit@)v[0-9]+\.[0-9]+\.[0-9]+' "$ROOT/$VF" \
|
|
1190
|
+
| grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | sort -u); do
|
|
1191
|
+
[ "$V" = "v$PKGVER" ] || VERS_BAD="${VERS_BAD:+$VERS_BAD, }$VF: $V"
|
|
1192
|
+
done
|
|
1193
|
+
done
|
|
1194
|
+
if [ -z "$VERS_BAD" ]; then
|
|
1195
|
+
ok "версия в README и llms.txt совпадает с package.json (v$PKGVER)"
|
|
1196
|
+
else
|
|
1197
|
+
bad "версия в документах разошлась с package.json" "package.json: v$PKGVER; найдено — $VERS_BAD"
|
|
1198
|
+
fi
|
|
1199
|
+
|
|
907
1200
|
# --- итог -------------------------------------------------------------------
|
|
908
1201
|
printf '\n'
|
|
909
1202
|
if [ "$FAIL" -eq 0 ]; then
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// tool/selfcheck/units-level.mjs — проверки уровня и доказательства гейтов.
|
|
2
|
+
//
|
|
3
|
+
// ОТДЕЛЬНЫМ ФАЙЛОМ, а не в units.mjs: тот перерос собственный предел в 500 строк, и поймал
|
|
4
|
+
// это наш же гейт `file-size-limit` на прогоне. Шов по смыслу: здесь всё про то, чем ступень
|
|
5
|
+
// отличается от заявления, — остальное осталось на месте.
|
|
6
|
+
//
|
|
7
|
+
// node --test tool/selfcheck/units-level.mjs
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { commandFor } from "../lib/prove.mjs";
|
|
11
|
+
import { assessLevel } from "../lib/manifest.mjs";
|
|
12
|
+
|
|
13
|
+
// --- доказательство гейтов ------------------------------------------------------------
|
|
14
|
+
// ЗАЧЕМ. Ступень AQK-2 называлась «гейты доказаны» и проверяла существование двух папок.
|
|
15
|
+
// Проект с тремя гейтами `true` проходил порог AQK-3 — проверено прогоном на пустой папке.
|
|
16
|
+
test("обёртка храповика снимается перед подстановкой каталога", () => {
|
|
17
|
+
// Иначе доказательство гоняет гейт вместе с реестром долга, и реестр перезаписывается
|
|
18
|
+
// находками из ОБРАЗЦА. На живом проекте это стёрло бы долг целиком.
|
|
19
|
+
const cmd = "bash ratchets/_ratchet.sh ratchets/x.txt bash gates/x/check.sh .";
|
|
20
|
+
assert.equal(commandFor(cmd, "gates/x/red"), "bash gates/x/check.sh gates/x/red");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("каталог подставляется вместо последнего слова команды", () => {
|
|
24
|
+
assert.equal(commandFor("bash gates/x/check.sh .", "gates/x/green"), "bash gates/x/check.sh gates/x/green");
|
|
25
|
+
assert.equal(commandFor("ruff check --select T20 .", "gates/x/red"), "ruff check --select T20 gates/x/red");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("ступень выше первой не берётся без доказательства", async () => {
|
|
29
|
+
const man = {
|
|
30
|
+
aqk: "0.6.0", entry: [], rules: ".", samples: ".", ratchets: ".", lessons: ".",
|
|
31
|
+
gates: { lint: "true" },
|
|
32
|
+
};
|
|
33
|
+
const withoutProof = await assessLevel(man, null);
|
|
34
|
+
const withFailedProof = await assessLevel(man, { ok: false });
|
|
35
|
+
const withProof = await assessLevel(man, { ok: true });
|
|
36
|
+
// Без доказательства ступень не выполнена, но помечена «не проверяли» — это разные состояния.
|
|
37
|
+
assert.equal(withoutProof.steps[2].ok, false);
|
|
38
|
+
assert.equal(withoutProof.steps[2].needsProof, true);
|
|
39
|
+
assert.equal(withFailedProof.steps[2].ok, false);
|
|
40
|
+
assert.equal(withFailedProof.steps[2].needsProof, false);
|
|
41
|
+
assert.equal(withProof.steps[2].ok, true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("обёртка родного инструмента едет вместе с каталогом образца", () => {
|
|
45
|
+
// `_native.sh <каталог> <команда…>` прячет пути gates/*/red|green. Оставленный «.» спрятал бы
|
|
46
|
+
// ровно то, что образец обязан показать, — красный прошёл бы зелёным.
|
|
47
|
+
const cmd = "bash gates/_native.sh . ruff check --select T20 .";
|
|
48
|
+
assert.equal(
|
|
49
|
+
commandFor(cmd, "gates/x/red"),
|
|
50
|
+
"bash gates/_native.sh gates/x/red ruff check --select T20 gates/x/red"
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("обе обёртки снимаются вместе", () => {
|
|
55
|
+
const cmd = "bash ratchets/_ratchet.sh ratchets/x.txt bash gates/_native.sh . ruff check .";
|
|
56
|
+
assert.equal(
|
|
57
|
+
commandFor(cmd, "gates/x/green"),
|
|
58
|
+
"bash gates/_native.sh gates/x/green ruff check gates/x/green"
|
|
59
|
+
);
|
|
60
|
+
});
|