agent-quality-kit 0.7.0 → 0.9.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.
Files changed (61) hide show
  1. package/README.md +135 -9
  2. package/README.ru.md +167 -23
  3. package/kit/docs/ai/project-baseline.md +14 -0
  4. package/kit/docs/ready-made-rules.md +103 -0
  5. package/kit/gates/_skip.sh +61 -1
  6. package/kit/gates/ci-not-hijackable/README.md +56 -0
  7. package/kit/gates/ci-not-hijackable/check.sh +73 -0
  8. package/kit/gates/ci-not-hijackable/gate.yml +19 -0
  9. package/kit/gates/ci-not-hijackable/green/.github/workflows/triage.yml +19 -0
  10. package/kit/gates/ci-not-hijackable/red/.github/workflows/triage.yml +18 -0
  11. package/kit/gates/color-from-token/check.sh +10 -2
  12. package/kit/gates/color-from-token/green/Button.tsx +2 -0
  13. package/kit/gates/complexity-limit/check.sh +6 -7
  14. package/kit/gates/duplicate-code/check.sh +5 -1
  15. package/kit/gates/entry-links-exist/check.sh +4 -1
  16. package/kit/gates/entry-links-exist/green/AGENTS.md +2 -0
  17. package/kit/gates/file-size-limit/check.sh +1 -1
  18. package/kit/gates/lesson-has-outcome/check.sh +5 -1
  19. package/kit/gates/mcp-server-resolves/README.md +62 -0
  20. package/kit/gates/mcp-server-resolves/check.sh +110 -0
  21. package/kit/gates/mcp-server-resolves/gate.yml +18 -0
  22. package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
  23. package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
  24. package/kit/gates/secrets-not-in-code/check.sh +16 -3
  25. package/kit/gates/secrets-not-in-code/green/testdata/certificate/key.pem +3 -0
  26. package/kit/gates/todo-without-task/check.sh +1 -1
  27. package/kit/gates/todo-without-task/green/app.py +1 -0
  28. package/llms.txt +38 -2
  29. package/package.json +2 -3
  30. package/tool/commands/context.mjs +264 -0
  31. package/tool/commands/doctor.mjs +104 -28
  32. package/tool/commands/learn.mjs +159 -0
  33. package/tool/commands/project.mjs +19 -2
  34. package/tool/commands/prove.mjs +1 -0
  35. package/tool/commands/report.mjs +33 -1
  36. package/tool/commands/vitals.mjs +159 -0
  37. package/tool/i18n/en-docs.mjs +125 -1
  38. package/tool/i18n/en.mjs +39 -36
  39. package/tool/i18n/index.mjs +36 -3
  40. package/tool/i18n/ru-docs.mjs +127 -1
  41. package/tool/i18n/ru.mjs +39 -36
  42. package/tool/lib/banner.mjs +59 -0
  43. package/tool/lib/brief.mjs +192 -0
  44. package/tool/lib/core.mjs +32 -1
  45. package/tool/lib/evidence.mjs +124 -0
  46. package/tool/lib/manifest.mjs +173 -15
  47. package/tool/lib/prove.mjs +24 -2
  48. package/tool/lib/repo.mjs +31 -1
  49. package/tool/lib/scope.mjs +10 -1
  50. package/tool/lib/templates.mjs +1 -0
  51. package/tool/program.mjs +45 -23
  52. package/tool/selfcheck/smoke.sh +592 -3
  53. package/tool/selfcheck/units-banner.mjs +65 -0
  54. package/tool/selfcheck/units-brief.mjs +97 -0
  55. package/tool/selfcheck/units-context.mjs +188 -0
  56. package/tool/selfcheck/units-evidence.mjs +83 -0
  57. package/tool/selfcheck/units-learn.mjs +88 -0
  58. package/tool/selfcheck/units-level.mjs +211 -3
  59. package/tool/selfcheck/units-repo.mjs +134 -0
  60. package/tool/selfcheck/units-vitals.mjs +62 -0
  61. package/tool/selfcheck/units.mjs +4 -75
@@ -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
 
@@ -1183,7 +1190,11 @@ rm -rf "$VDIR"
1183
1190
  # v0.4.2, отставание на два выпуска. Проверка сравнивает обе с package.json, а не друг с другом:
1184
1191
  # совпасть друг с другом они могут и будучи одинаково устаревшими.
1185
1192
  VERS_BAD=""
1186
- for VF in README.md llms.txt; do
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
1187
1198
  [ -f "$ROOT/$VF" ] || continue
1188
1199
  # Берём только версии AQK — «v1.2.3» в примерах чужих действий (actions/checkout@v4) не наши.
1189
1200
  for V in $(grep -oE '(rev:[[:space:]]*|Agent_Quality_Kit@)v[0-9]+\.[0-9]+\.[0-9]+' "$ROOT/$VF" \
@@ -1192,11 +1203,589 @@ for VF in README.md llms.txt; do
1192
1203
  done
1193
1204
  done
1194
1205
  if [ -z "$VERS_BAD" ]; then
1195
- ok "версия в README и llms.txt совпадает с package.json (v$PKGVER)"
1206
+ ok "версия в обоих README и llms.txt совпадает с package.json (v$PKGVER)"
1196
1207
  else
1197
1208
  bad "версия в документах разошлась с package.json" "package.json: v$PKGVER; найдено — $VERS_BAD"
1198
1209
  fi
1199
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
+
1440
+ # --- 95. covers: запись закрыта другим арбитром, но только объявленным ---------
1441
+ # Просьба первого чужого пользователя, названная им первой: у него complexity-limit,
1442
+ # no-print-in-prod и swallowed-error держит biome, а doctor каждый прогон печатал «применимо,
1443
+ # но не поставлено: 5» — неправду. Неправда в СОБСТВЕННОМ выводе дороже всех прочих: весь
1444
+ # стандарт стоит на том, что вывод не врёт. Но поле не признание на слово: гейт, который
1445
+ # «закрывает», обязан быть объявлен в gates:, иначе covers становится способом объявить защиту,
1446
+ # которой нет. Проверяются оба конца сразу.
1447
+ COVP="$(mktemp -d)"
1448
+ ( cd "$COVP" && git init -q . && mkdir -p src rules &&
1449
+ printf 'const x=1;\n' > src/a.ts && printf '# вход\n' > AGENTS.md &&
1450
+ printf 'правило\n' > rules/r.md && printf '.x\n' > .gitignore &&
1451
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n lint: "true"\ncovers:\n lint: [no-print-in-prod, swallowed-error]\n biome: [duplicate-code]\n' > .aqk.yml ) >/dev/null 2>&1
1452
+ COV=$( cd "$COVP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1453
+ if printf '%s' "$COV" | grep -q "закрыто другим арбитром 2" &&
1454
+ printf '%s' "$COV" | grep -q "covers называет гейт, которого нет" &&
1455
+ printf '%s' "$COV" | grep -q "biome" &&
1456
+ ! printf '%s' "$COV" | grep -q "неизвестное поле"; then
1457
+ ok "covers снимает запись с долга — и только когда закрывающий гейт объявлен"
1458
+ else
1459
+ bad "covers посчитан неверно" "$(printf '%s' "$COV" | grep -E 'Итого|covers' | head -3)"
1460
+ fi
1461
+ rm -rf "$COVP"
1462
+
1463
+ # --- 96. язык вывода — настройка проекта, а не машины -------------------------
1464
+ # Просьба первого чужого пользователя: «язык берётся из LC_ALL/LANG, а на Windows их просто нет:
1465
+ # русский проект получает английский вывод». Проверяются оба конца порядка сразу: манифест
1466
+ # сильнее локали, но переменная окружения сильнее манифеста — иначе у человека, набравшего
1467
+ # AQK_LANG=en руками, не остаётся способа получить английский.
1468
+ LNGP="$(mktemp -d)"
1469
+ ( cd "$LNGP" && git init -q . && printf '# вход\n' > AGENTS.md &&
1470
+ printf 'aqk: 1\nlang: ru\nentry: [AGENTS.md]\n' > .aqk.yml ) >/dev/null 2>&1
1471
+ BY_MAN=$( cd "$LNGP" && env -u AQK_LANG LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 node "$CLI" context 2>&1 | head -1 )
1472
+ BY_ENV=$( cd "$LNGP" && AQK_LANG=en node "$CLI" context 2>&1 | head -1 )
1473
+ if printf '%s' "$BY_MAN" | grep -q "состояние этого репозитория" &&
1474
+ printf '%s' "$BY_ENV" | grep -q "the state of this repository"; then
1475
+ ok "язык берётся из манифеста поверх локали, а переменная окружения — поверх манифеста"
1476
+ else
1477
+ bad "порядок выбора языка нарушен" "по манифесту: $BY_MAN | по окружению: $BY_ENV"
1478
+ fi
1479
+ rm -rf "$LNGP"
1480
+
1481
+ # --- 97. совет про браузер у агента — только тому, кому он нужен --------------
1482
+ # Решение владельца 2026-09-08: инструмент, дающий агенту браузер, надо рекомендовать. Но совет,
1483
+ # показанный не тому, стоит доверия всем остальным советам — та же норма, что у записей каталога.
1484
+ # Проверяются все три конца: интерфейс есть и сервера нет → совет; интерфейса нет → тишина;
1485
+ # сервер уже стоит → тишина.
1486
+ BRWP="$(mktemp -d)"
1487
+ ( cd "$BRWP" && git init -q . && mkdir -p src && printf 'body{color:#fff}\n' > src/a.css &&
1488
+ printf 'export const A=1\n' > src/a.tsx && printf '# вход\n' > AGENTS.md ) >/dev/null 2>&1
1489
+ WITH_UI=$( cd "$BRWP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1490
+ ( cd "$BRWP" && printf '{"mcpServers":{"b":{"command":"npx","args":["-y","chrome-devtools-mcp@1.9.0"]}}}\n' > .mcp.json )
1491
+ WITH_SRV=$( cd "$BRWP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1492
+ NOUIP="$(mktemp -d)"
1493
+ ( cd "$NOUIP" && git init -q . && printf 'print(1)\n' > a.py && printf '# вход\n' > AGENTS.md ) >/dev/null 2>&1
1494
+ NO_UI=$( cd "$NOUIP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1495
+ if printf '%s' "$WITH_UI" | grep -q "нет браузера" &&
1496
+ ! printf '%s' "$WITH_SRV" | grep -q "нет браузера" &&
1497
+ ! printf '%s' "$NO_UI" | grep -q "нет браузера"; then
1498
+ ok "совет про браузер даётся проекту с интерфейсом и молчит, когда сервер уже есть"
1499
+ else
1500
+ bad "совет про браузер показан не тому" "с ui: $(printf '%s' "$WITH_UI" | grep -c 'нет браузера'), с сервером: $(printf '%s' "$WITH_SRV" | grep -c 'нет браузера'), без ui: $(printf '%s' "$NO_UI" | grep -c 'нет браузера')"
1501
+ fi
1502
+ rm -rf "$BRWP" "$NOUIP"
1503
+
1504
+ # --- 98. заявка covers сверяется, а не принимается на слово -------------------
1505
+ # Поле covers завели утром того же дня, и тогда же честно записали: оно снимает запись с долга
1506
+ # ПО СЛОВУ человека. К вечеру это перестало быть теорией: запуск на настоящем ruff.toml из живого
1507
+ # проекта показал девятнадцать групп правил в extend-select и НЕ пойманный print() — группы T20
1508
+ # среди них нет. Заявка «no-print-in-prod держит наш lint» была бы ложной, а запись ушла бы из
1509
+ # долга. Поле, снимающее неправду из вывода, само стало бы способом её произвести.
1510
+ CVUP="$(mktemp -d)"
1511
+ ( cd "$CVUP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
1512
+ printf 'aqk: 1\nentry: [AGENTS.md]\ngates:\n lint: "ruff check ."\ncovers:\n lint: [no-print-in-prod]\n' > .aqk.yml ) >/dev/null 2>&1
1513
+ ( cd "$CVUP" && printf 'extend-select = ["I","B","UP","SIM"]\n' > ruff.toml )
1514
+ WITHOUT=$( cd "$CVUP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1515
+ ( cd "$CVUP" && printf 'extend-select = ["I","B","UP","SIM","T20"]\n' > ruff.toml )
1516
+ WITHT20=$( cd "$CVUP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1517
+ if printf '%s' "$WITHOUT" | grep -q "заявка не подтверждена" &&
1518
+ printf '%s' "$WITHOUT" | grep -q "T20" &&
1519
+ ! printf '%s' "$WITHT20" | grep -q "заявка не подтверждена"; then
1520
+ ok "covers сверяется с кодами правил: без T20 говорит вслух, с T20 молчит"
1521
+ else
1522
+ bad "сверка заявки covers не работает" "без T20: $(printf '%s' "$WITHOUT" | grep -c 'не подтверждена'), с T20: $(printf '%s' "$WITHT20" | grep -c 'не подтверждена')"
1523
+ fi
1524
+ rm -rf "$CVUP"
1525
+
1526
+ # --- 99. краткий режим: присутствие видно, совет не надоедает -----------------
1527
+ # Хук pre-commit молчит на успехе — это его умолчание. Комплект, который всё держит, для
1528
+ # человека неотличим от невставленного: «скачал и че дальше» — дословная жалоба владельца.
1529
+ # Это ровно тот порок, против которого написан комплект, только у нас самих.
1530
+ # Проверяются три вещи сразу: строка есть всегда; совет приходит один раз, а не на каждый
1531
+ # прогон; переменная его выключает.
1532
+ BRFP="$(mktemp -d)"
1533
+ ( cd "$BRFP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
1534
+ printf '.x\n' > .gitignore && mkdir -p rules .aqk/docs && printf 'п\n' > rules/r.md &&
1535
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
1536
+ B1=$( cd "$BRFP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 ); B1C=$?
1537
+ B2=$( cd "$BRFP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 )
1538
+ rm -f "$BRFP/.aqk/advice-shown"
1539
+ B3=$( cd "$BRFP" && AQK_ADVICE=0 AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 )
1540
+ if [ "$B1C" -eq 0 ] &&
1541
+ printf '%s' "$B1" | grep -qE "^(❖ )?AQK держит" &&
1542
+ printf '%s' "$B1" | grep -q "поставить:" &&
1543
+ printf '%s' "$B2" | grep -qE "^(❖ )?AQK держит" &&
1544
+ ! printf '%s' "$B2" | grep -q "поставить:" &&
1545
+ ! printf '%s' "$B3" | grep -q "поставить:" &&
1546
+ [ "$(printf '%s\n' "$B2" | wc -l)" -le 2 ]; then
1547
+ ok "краткий режим: строка есть всегда, совет один раз в сутки и выключается"
1548
+ else
1549
+ bad "краткий режим ведёт себя не так" "первый: $(printf '%s' "$B1" | head -1) · второй строк: $(printf '%s\n' "$B2" | wc -l)"
1550
+ fi
1551
+ rm -rf "$BRFP"
1552
+
1553
+ # --- 100. в awk-программах гейтов нет повторителей {n} ------------------------
1554
+ # Часть сборок mawk не понимает повторители в регулярках. Условие с {4} не
1555
+ # совпадает НИКОГДА, и гейт молча выходит с нулём на собственном красном образце: ровно тот
1556
+ # отказ, против которого написан комплект. Поймано сборкой docker-образа 2026-09-08 — на хосте
1557
+ # gawk, и ни один прогон не краснел. Проверка статическая и потому дешёвая; настоящий прогон
1558
+ # под старым awk делает задание `image` в конвейере — там образ, в котором дефект и нашёлся.
1559
+ # ВАЖНО, ЧТО ИМЕННО СБОРКА: mawk 1.3.4 20240123 (ubuntu-latest, эта машина) повторители
1560
+ # ПОНИМАЕТ, mawk 1.3.4 20200120 (node:22-slim) — нет. Замерено 2026-09-09. Поэтому «прогнать
1561
+ # под mawk» ничего не значит без указания, под каким,
1562
+ # а прочитать исходники — можем.
1563
+ AWKBAD=""
1564
+ for G in "$ROOT"/kit/gates/*/check.sh "$ROOT"/kit/gates/_skip.sh; do
1565
+ [ -f "$G" ] || continue
1566
+ # Только строки внутри awk-программ; комментарии оболочки (#) не считаем.
1567
+ # Только регулярки AWK: `t ~ /…/` и образец-действие `/…/ {`. Шаблоны для grep, лежащие в
1568
+ # переменных оболочки, сюда не попадают намеренно — GNU grep и busybox grep повторители
1569
+ # понимают оба, и краснеть на них значит краснеть на нормальном укладе.
1570
+ HIT=$(sed 's/^[[:space:]]*#.*$//' "$G" | grep -nE '(~[[:space:]]*/|^[[:space:]]*/)' \
1571
+ | grep -E '\{[0-9]+(,[0-9]*)?\}' | head -2)
1572
+ [ -n "$HIT" ] && AWKBAD="${AWKBAD:+$AWKBAD; }$(basename "$(dirname "$G")"): $(printf '%s' "$HIT" | head -1 | cut -c1-60)"
1573
+ done
1574
+ if [ -z "$AWKBAD" ]; then
1575
+ ok "в проверках каталога нет повторителей {n} — они не работают под mawk"
1576
+ else
1577
+ bad "повторитель {n} в проверке: под mawk она молчит" "$AWKBAD"
1578
+ fi
1579
+
1580
+ # --- 101. проверка версии: раз в сутки, не в конвейере, молча при отказе ------
1581
+ # До этой строки комплект не делал ни одного исходящего запроса. Раз делает — обязан делать
1582
+ # ровно то, что о нём написано: спрашивать реестр не чаще раза в сутки и НЕ спрашивать в
1583
+ # конвейере. Само наличие новой версии здесь не проверяется — она зависит от реестра, а
1584
+ # проверка, зависящая от чужого сервера, краснеет по чужой воле. Сравнение версий проверено
1585
+ # модульно; здесь — то, что вокруг него.
1586
+ UPDP="$(mktemp -d)"
1587
+ ( cd "$UPDP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
1588
+ printf '.x\n' > .gitignore && mkdir -p rules .aqk/docs && printf 'п\n' > rules/r.md &&
1589
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
1590
+ ( cd "$UPDP" && env -u CI -u GITHUB_ACTIONS AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
1591
+ ASKED=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
1592
+ rm -f "$UPDP/.aqk/update-checked"
1593
+ ( cd "$UPDP" && CI=true AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
1594
+ IN_CI=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
1595
+ rm -f "$UPDP/.aqk/update-checked"
1596
+ ( cd "$UPDP" && env -u CI AQK_UPDATE=0 AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
1597
+ OFF=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
1598
+ # «нет сети» — тоже допустимый исход первого случая: молчание при отказе и есть требование.
1599
+ if [ "$IN_CI" = "нет" ] && [ "$OFF" = "нет" ]; then
1600
+ ok "версия спрашивается вне конвейера ($ASKED), не спрашивается в конвейере и при AQK_UPDATE=0"
1601
+ else
1602
+ bad "проверка версии спрашивает там, где не должна" "вне CI: $ASKED, в CI: $IN_CI, выключено: $OFF"
1603
+ fi
1604
+ rm -rf "$UPDP"
1605
+
1606
+ # --- 102. строка манифеста, которую разбор не понял, называется вслух ---------
1607
+ # Найдено случайно 2026-09-09: подсадили падающий гейт с именем «плохой», чтобы посмотреть на
1608
+ # вывод, — прогон вышел с НУЛЁМ. Гейт не упал: его не существовало. Разбор берёт имена только
1609
+ # латиницей, а строку, не подошедшую под это, выбрасывал без единого слова. Человек видит
1610
+ # проверку в файле, а её нет — наш класс в чистом виде, и хуже опечатки в имени поля: ту мы
1611
+ # называем с 2026-09-06, а эту не называли вовсе.
1612
+ UNPP="$(mktemp -d)"
1613
+ ( cd "$UNPP" && git init -q . && printf '# вход\n' > AGENTS.md &&
1614
+ printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n плохой: "false"\n' > .aqk.yml ) >/dev/null 2>&1
1615
+ UNP=$( cd "$UNPP" && node "$CLI" doctor 2>&1 )
1616
+ ( cd "$UNPP" && printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n bad: "false"\n' > .aqk.yml )
1617
+ OKM=$( cd "$UNPP" && node "$CLI" doctor 2>&1 )
1618
+ if printf '%s' "$UNP" | grep -q "НЕ ДЕЙСТВУЕТ" &&
1619
+ printf '%s' "$UNP" | grep -q "строка 6" &&
1620
+ ! printf '%s' "$OKM" | grep -q "НЕ ДЕЙСТВУЕТ"; then
1621
+ ok "непонятая строка манифеста называется с номером, понятая — молчит"
1622
+ else
1623
+ bad "потерянная строка манифеста не названа" "$(printf '%s' "$UNP" | grep -c 'НЕ ДЕЙСТВУЕТ') на кириллице, $(printf '%s' "$OKM" | grep -c 'НЕ ДЕЙСТВУЕТ') на латинице"
1624
+ fi
1625
+ rm -rf "$UNPP"
1626
+
1627
+ # --- 103. vitals: отказ роняет, выбор и незнание — нет ------------------------
1628
+ # `doctor` смотрит на репозиторий, `prove` — на гейты, `context` — на состояние. На саму
1629
+ # обвязку не смотрел никто: стоят ли инструменты объявленных гейтов, прописан ли хук в
1630
+ # .git/hooks НА САМОМ ДЕЛЕ. Раньше это выяснялось красным гейтом посреди коммита.
1631
+ # Первая версия ставила крест хуку, которого нет, — и на нашем же репозитории вышло два креста
1632
+ # за сознательное решение (pre-commit локально не ставим, гоняем в CI). Команда, которая кричит
1633
+ # «сломано» про выбор, перестаёт читаться вместе с настоящими отказами. Проверяются оба конца.
1634
+ VITP="$(mktemp -d)"
1635
+ ( cd "$VITP" && git init -q . && printf '# вход\n' > AGENTS.md &&
1636
+ printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
1637
+ VOK=$( cd "$VITP" && AQK_LANG=ru node "$CLI" vitals 2>&1 ); VOK_C=$?
1638
+ ( cd "$VITP" && printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n lint: "инструментакоторогонет ."\n' > .aqk.yml )
1639
+ VBAD=$( cd "$VITP" && AQK_LANG=ru node "$CLI" vitals 2>&1 ); VBAD_C=$?
1640
+ if [ "$VOK_C" -eq 0 ] && [ "$VBAD_C" -eq 1 ] &&
1641
+ printf '%s' "$VBAD" | grep -q "НЕ НАЙДЕНЫ" &&
1642
+ printf '%s' "$VOK" | grep -q "хук pre-commit"; then
1643
+ ok "vitals: пропавший инструмент роняет прогон, отсутствие хука — нет"
1644
+ else
1645
+ bad "vitals путает отказ с выбором" "исправный код $VOK_C, сломанный код $VBAD_C"
1646
+ fi
1647
+ rm -rf "$VITP"
1648
+
1649
+ # --- 106. совещательный гейт назван всегда и не роняет прогон никогда ---------
1650
+ # Два отказа, найденные 2026-09-09 сверкой документации с кодом. ПЕРВЫЙ: зелёный совещательный
1651
+ # печатался обычной галочкой — гейт, который уронить сборку НЕ МОЖЕТ, по выводу неотличим от
1652
+ # того, который может, и список `advisory:` был виден только в день, когда он покраснел. Ровно
1653
+ # та тишина, против которой написан стандарт, только про сам прибор. ВТОРОЙ: при `--since` гейт,
1654
+ # чей вывод не содержит путей, шёл через ветку «сузить нечем» с безусловным failed++ — то есть
1655
+ # совещательный ронял прогон. Проверено сравнением с кодом до починки: было 1, стало 0.
1656
+ # Команда гейта — ФАЙЛ, а не строка с `;`. На windows `spawnSync(shell: true)` запускает
1657
+ # cmd.exe, а не sh: там `echo x; exit 1` печатается целиком и выходит с НУЛЁМ. Первая версия
1658
+ # этой проверки так и провалилась в конвейере на windows — фикстура, а не код. Заодно урок про
1659
+ # сам тест: утверждение «зелёный помечен» искало подстроку «совещательный», которая есть и в
1660
+ # пометке КРАСНОГО совещательного, — то есть проходило бы и при полном отсутствии починки.
1661
+ # Теперь ищется текст, который бывает только на зелёной строке.
1662
+ ADVP="$(mktemp -d)"
1663
+ (
1664
+ cd "$ADVP" && git init -q . && git config user.email a@b && git config user.name a
1665
+ printf '# вход\n' > AGENTS.md && mkdir -p r && printf 'x\n' > r/a.md
1666
+ printf '#!/bin/sh\necho "вердикт без путей"\nexit 1\n' > noscope.sh
1667
+ printf '#!/bin/sh\nexit 0\n' > quiet.sh
1668
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash quiet.sh"\nadvisory:\n - adv\n' > .aqk.yml
1669
+ git add -A && git commit -qm "Сделано: основа. Не уверен: ничего"
1670
+ ) >/dev/null 2>&1
1671
+ ADVBASE=$( cd "$ADVP" && git rev-parse --abbrev-ref HEAD )
1672
+ ( cd "$ADVP" && git checkout -qb feat && printf 'y\n' > r/b.md && git add -A &&
1673
+ git commit -qm "Сделано: файл. Не уверен: ничего" ) >/dev/null 2>&1
1674
+ ADVGREEN=$( cd "$ADVP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 2>&1 )
1675
+ ( cd "$ADVP" && printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash noscope.sh"\nadvisory:\n - adv\n' > .aqk.yml )
1676
+ ( cd "$ADVP" && node "$CLI" doctor --run --min 1 --since "$ADVBASE" >/dev/null 2>&1 ); ADV_C=$?
1677
+ ( cd "$ADVP" && printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash noscope.sh"\n' > .aqk.yml )
1678
+ ( cd "$ADVP" && node "$CLI" doctor --run --min 1 --since "$ADVBASE" >/dev/null 2>&1 ); BLOCK_C=$?
1679
+ if printf '%s' "$ADVGREEN" | grep -q "уронить прогон не может" && [ "$ADV_C" -eq 0 ] && [ "$BLOCK_C" -eq 1 ]; then
1680
+ ok "совещательный назван и на зелёном, и не роняет прогон даже когда сузить нечем"
1681
+ else
1682
+ bad "совещательный гейт неотличим или роняет прогон" "зелёный помечен: $(printf '%s' "$ADVGREEN" | grep -c 'уронить прогон не может'), код совещательного $ADV_C, код блокирующего $BLOCK_C"
1683
+ fi
1684
+ rm -rf "$ADVP"
1685
+
1686
+ # --- 107. prove пропускает запись без её программы, а не обвиняет её ----------
1687
+ # ТО ЖЕ, ЧТО НАШЁЛ ВТОРОЙ ПОЛЬЗОВАТЕЛЬ, ТОЛЬКО ДРУГОЙ ДВЕРЬЮ. Переносимый рецепт бывает обёрткой
1688
+ # вокруг готового инструмента: первое слово команды тогда `bash`, и по нему не видно, чего не
1689
+ # хватает. Без программы обёртка краснеет на ОБОИХ образцах, и prove объявлял исправный гейт
1690
+ # сломанным — «краснеет на исправном коде». От prove зависят ступень AQK-2 и значок, то есть
1691
+ # отсутствие чужой программы отбирало у проекта уровень. Обвинение вместо диагноза.
1692
+ # Приёмка (gates.sh) и мутационная проверка поле `requires` читают давно; prove — не читал.
1693
+ # Проверено 2026-09-09 на своём же репозитории: не стояли slopcheck и zizmor, prove дал два
1694
+ # обвинения и код 1. Проверяются ОБА конца: без поля — обвинение и код 1, с полем — пропуск и 0.
1695
+ PRVP="$(mktemp -d)"
1696
+ (
1697
+ cd "$PRVP" && git init -q . && printf '# вход\n' > AGENTS.md && mkdir -p r s/wrap/red s/wrap/green
1698
+ printf 'x\n' > s/wrap/red/a.txt && printf 'x\n' > s/wrap/green/a.txt
1699
+ # Обёртка вокруг программы, которой на машине нет: краснеет на обоих образцах.
1700
+ printf '#!/bin/sh\nпрограммыкоторойнет "$1" || exit 2\n' > wrap.sh
1701
+ # Настоящая проверка: prove требует, чтобы хоть один гейт был ДОКАЗАН, иначе проект,
1702
+ # у которого всё недоказуемо, получал бы уровень ни за что. Команда обязана кончаться
1703
+ # каталогом — иначе подставлять образец некуда, и гейт сам станет недоказуемым.
1704
+ printf '#!/bin/sh\ngrep -rq НЕЛЬЗЯ "$1" && exit 1\nexit 0\n' > real.sh
1705
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\nsamples: s\ngates:\n wrap: "sh wrap.sh ."\n real: "sh real.sh ."\n' > .aqk.yml
1706
+ ) >/dev/null 2>&1
1707
+ # Второй гейт обязан быть доказуемым: prove требует хотя бы одного доказанного.
1708
+ ( cd "$PRVP" && mkdir -p s/real/red s/real/green &&
1709
+ printf 'НЕЛЬЗЯ\n' > s/real/red/a.txt && printf 'можно\n' > s/real/green/a.txt )
1710
+ PRV_NOREQ=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" prove 2>&1 ); PRV_NOREQ_C=$?
1711
+ ( cd "$PRVP" && printf 'intent: обёртка\nrequires: программыкоторойнет\nrecipes:\n any: "sh wrap.sh {dir}"\n' > s/wrap/gate.yml )
1712
+ PRV_REQ=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" prove 2>&1 ); PRV_REQ_C=$?
1713
+ # Тот же вопрос задаёт `vitals`, и до 2026-09-09 он отвечал на него иначе: печатал «все
1714
+ # инструменты на месте», потому что смотрел только на ПЕРВОЕ СЛОВО команды — `sh`, который есть
1715
+ # всегда. Две команды об одном репозитории говорили разное; читатель верил той, что зеленее.
1716
+ PRV_VIT=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" vitals 2>&1 )
1717
+ if [ "$PRV_NOREQ_C" -ne 0 ] && [ "$PRV_REQ_C" -eq 0 ] &&
1718
+ printf '%s' "$PRV_REQ" | grep -q "НЕ ПРОВЕРЕНА здесь" &&
1719
+ printf '%s' "$PRV_VIT" | grep -q "программыкоторойнет"; then
1720
+ ok "prove и vitals одинаково видят программу из requires: пропуск, а не обвинение"
1721
+ else
1722
+ bad "prove путает «нечем проверить» со «сломан», либо vitals её не видит" \
1723
+ "без поля код $PRV_NOREQ_C, с полем код $PRV_REQ_C, vitals назвал: $(printf '%s' "$PRV_VIT" | grep -c 'программыкоторойнет')"
1724
+ fi
1725
+ rm -rf "$PRVP"
1726
+
1727
+ # --- 104. опись в AGENTS.md покрывает все исходники ---------------------------
1728
+ # AGENTS.md — первое, что читает агент, и раздел «Где что лежит» для него карта. Карта,
1729
+ # отставшая от дерева, хуже её отсутствия: агент уверен, что видел всё. Проверено 2026-09-09 —
1730
+ # в описи не было восьми файлов из четырнадцати добавленных за неделю (`brief.mjs`, `banner.mjs`
1731
+ # и шесть `units-*.mjs`), и заметить это можно было только сверкой руками. Тот же класс, что
1732
+ # «гейт объявлен и не существует»: расхождение молчит.
1733
+ MISSING=""
1734
+ for F in "$ROOT"/tool/lib/*.mjs "$ROOT"/tool/commands/*.mjs "$ROOT"/tool/selfcheck/*; do
1735
+ B=$(basename "$F")
1736
+ grep -qF "$B" "$ROOT/AGENTS.md" || MISSING="$MISSING $B"
1737
+ done
1738
+ if [ -z "$MISSING" ]; then
1739
+ ok "опись «Где что лежит» называет каждый исходник"
1740
+ else
1741
+ bad "AGENTS.md не называет файлы" "$MISSING"
1742
+ fi
1743
+
1744
+ # --- 105. два README не расходятся структурно --------------------------------
1745
+ # AGENTS.md требует, чтобы README и его перевод не расходились, и до сегодня требование не
1746
+ # работало: на 2026-09-09 в английском было 23 раздела, в русском 22 — «Первый прогон на
1747
+ # настоящем проекте» отсутствовал целиком, а «Как ввести правило» стояло в другом разделе.
1748
+ # Читатель одного языка получал инструкцию, которой у читателя другого не было. Сверяется
1749
+ # ПОСЛЕДОВАТЕЛЬНОСТЬ уровней, а не тексты: заголовки на разных языках сравнивать нельзя.
1750
+ # Строки внутри ``` не считаются: в примере .aqkignore есть строка, начинающаяся с #.
1751
+ levels() {
1752
+ awk '/^```/ { inf = !inf; next } !inf && /^#/ { sub(/[^#].*/, ""); print }' "$1"
1753
+ }
1754
+ if [ "$(levels "$ROOT/README.md")" = "$(levels "$ROOT/README.ru.md")" ]; then
1755
+ ok "README.md и README.ru.md несут одни и те же разделы ($(levels "$ROOT/README.md" | wc -l | tr -d ' '))"
1756
+ else
1757
+ bad "README и перевод разошлись структурно" \
1758
+ "англ. $(levels "$ROOT/README.md" | wc -l | tr -d ' '), рус. $(levels "$ROOT/README.ru.md" | wc -l | tr -d ' ')"
1759
+ fi
1760
+
1761
+ # --- 108. init доходит до конца, когда дом недоступен для записи ---------------
1762
+ # НАШЁЛ КОНВЕЙЕР, А НЕ ПРОГОН. Одноразовая пометка «просьбу про звезду уже показывали» пишется
1763
+ # в $HOME/.config/aqk. В контейнере, запущенном `--user 1001:127`, у этого uid нет записи в
1764
+ # /etc/passwd, homedir() даёт «/», и mkdir падает с EACCES на «/.config» — роняя ВЕСЬ init.
1765
+ # То есть любой, кто набрал команду из нашей же документации по docker, получал падение вместо
1766
+ # установки. Локально не воспроизводилось по случайности: uid разработчика 1000 совпадает с
1767
+ # пользователем `node` в образе, у которого дом есть; на раннере uid 1001 — не совпадает.
1768
+ # Удобство не имеет права ронять работу команды. Молчать о нём тоже нельзя — это swallowed-error.
1769
+ HOMEP="$(mktemp -d)"; HOMEH="$(mktemp -d)"
1770
+ chmod 500 "$HOMEH" 2>/dev/null
1771
+ # На windows chmod прав не отнимает: там эта проверка ничего не проверяет и говорит об этом,
1772
+ # а не зеленеет молча. Тот же приём, что «НЕ ПРОВЕРЕНА здесь» в приёмке каталога.
1773
+ if ( : > "$HOMEH/проба" ) 2>/dev/null; then
1774
+ rm -f "$HOMEH/проба"
1775
+ ok "init при недоступном доме — НЕ ПРОВЕРЕНА здесь: chmod прав не отнимает"
1776
+ else
1777
+ ( cd "$HOMEP" && git init -q . ) >/dev/null 2>&1
1778
+ HOMEOUT=$( cd "$HOMEP" && HOME="$HOMEH" AQK_LANG=ru node "$CLI" init 2>&1 ); HOME_C=$?
1779
+ if [ "$HOME_C" -eq 0 ] && [ -f "$HOMEP/.aqk.yml" ] &&
1780
+ printf '%s' "$HOMEOUT" | grep -q "запомнить не удалось"; then
1781
+ ok "init доходит до конца при недоступном доме и называет, чего не смог"
1782
+ else
1783
+ bad "init падает или молчит, когда дом недоступен для записи" \
1784
+ "код $HOME_C, манифест $([ -f "$HOMEP/.aqk.yml" ] && echo есть || echo нет)"
1785
+ fi
1786
+ fi
1787
+ chmod 700 "$HOMEH" 2>/dev/null; rm -rf "$HOMEP" "$HOMEH"
1788
+
1200
1789
  # --- итог -------------------------------------------------------------------
1201
1790
  printf '\n'
1202
1791
  if [ "$FAIL" -eq 0 ]; then
@@ -1205,5 +1794,5 @@ else
1205
1794
  printf ' \033[31mпровалено: %s из %s\033[0m\n\n' "$FAIL" "$((PASS + FAIL))"
1206
1795
  fi
1207
1796
 
1208
- printf ' \033[2mне покрыто: содержание документов, установка с GitHub через npx\033[0m\n\n'
1797
+ printf ' \033[2mне покрыто: СОДЕРЖАНИЕ документов (сверяется опись и структура, не текст),\n установка с GitHub через npx\033[0m\n\n'
1209
1798
  exit "$FAIL"