agent-quality-kit 0.8.0 → 0.10.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 (76) hide show
  1. package/README.md +154 -12
  2. package/README.ru.md +185 -27
  3. package/kit/docs/ai/index.md +1 -0
  4. package/kit/docs/ai/project-baseline.md +14 -0
  5. package/kit/docs/api-e2e.md +214 -0
  6. package/kit/docs/ready-made-rules.md +188 -0
  7. package/kit/gates/README.md +22 -0
  8. package/kit/gates/api-contract-has-arbiter/README.md +63 -0
  9. package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
  10. package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
  11. package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
  12. package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
  13. package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
  14. package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
  15. package/kit/gates/ci-actually-fails/check.sh +9 -1
  16. package/kit/gates/color-from-token/check.sh +5 -1
  17. package/kit/gates/commit-explains-itself/check.sh +15 -0
  18. package/kit/gates/complexity-limit/red/deep.go +17 -0
  19. package/kit/gates/complexity-limit/red/deep.rs +17 -0
  20. package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
  21. package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
  22. package/kit/gates/lesson-has-outcome/check.sh +5 -1
  23. package/kit/gates/mcp-server-resolves/README.md +62 -0
  24. package/kit/gates/mcp-server-resolves/check.sh +110 -0
  25. package/kit/gates/mcp-server-resolves/gate.yml +18 -0
  26. package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
  27. package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
  28. package/kit/gates/protection-not-removed/README.md +67 -0
  29. package/kit/gates/protection-not-removed/check.sh +92 -0
  30. package/kit/gates/protection-not-removed/gate.yml +10 -0
  31. package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
  32. package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
  33. package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
  34. package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
  35. package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
  36. package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
  37. package/kit/gates/todo-without-task/red/later.go +6 -0
  38. package/kit/gates/todo-without-task/red/later.rs +4 -0
  39. package/llms.txt +25 -4
  40. package/package.json +3 -6
  41. package/tool/commands/context.mjs +37 -6
  42. package/tool/commands/doctor.mjs +139 -16
  43. package/tool/commands/probe.mjs +228 -0
  44. package/tool/commands/project.mjs +18 -2
  45. package/tool/commands/prove.mjs +1 -0
  46. package/tool/commands/vitals.mjs +167 -0
  47. package/tool/i18n/en-docs.mjs +48 -0
  48. package/tool/i18n/en-gates.mjs +309 -0
  49. package/tool/i18n/en.mjs +26 -279
  50. package/tool/i18n/index.mjs +36 -3
  51. package/tool/i18n/ru-docs.mjs +48 -0
  52. package/tool/i18n/ru-gates.mjs +311 -0
  53. package/tool/i18n/ru.mjs +26 -278
  54. package/tool/lib/banner.mjs +59 -0
  55. package/tool/lib/brief.mjs +192 -0
  56. package/tool/lib/cadence.mjs +57 -0
  57. package/tool/lib/core.mjs +3 -0
  58. package/tool/lib/history.mjs +82 -0
  59. package/tool/lib/manifest.mjs +146 -15
  60. package/tool/lib/prove.mjs +11 -1
  61. package/tool/lib/repo.mjs +43 -3
  62. package/tool/program.mjs +33 -0
  63. package/tool/selfcheck/smoke/_fixture.mjs +89 -0
  64. package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
  65. package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
  66. package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
  67. package/tool/selfcheck/smoke.sh +528 -6
  68. package/tool/selfcheck/units-banner.mjs +65 -0
  69. package/tool/selfcheck/units-brief.mjs +97 -0
  70. package/tool/selfcheck/units-cadence.mjs +69 -0
  71. package/tool/selfcheck/units-context.mjs +3 -1
  72. package/tool/selfcheck/units-level.mjs +147 -1
  73. package/tool/selfcheck/units-probe.mjs +100 -0
  74. package/tool/selfcheck/units-repo.mjs +164 -0
  75. package/tool/selfcheck/units-vitals.mjs +81 -0
  76. package/tool/selfcheck/units.mjs +3 -75
@@ -23,7 +23,15 @@ PASS=0
23
23
  FAIL=0
24
24
 
25
25
  ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; PASS=$((PASS + 1)); }
26
- bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; printf ' %s\n' "${2:-}"; FAIL=$((FAIL + 1)); }
26
+ # Упавшие называются ПОИМЁННО И С ПРИЧИНОЙ в итоге, а не только по ходу. Прогон запускают гейтом, а вывод
27
+ # упавшего гейта обрезается — и причина, напечатанная в середине двухсот строк, до глаз не
28
+ # доезжает. Час поисков в конвейере 2026-09-09 стоил ровно этого: список красных был, а увидеть
29
+ # его было нельзя. Имени мало: следующий круг конвейера показал имя и не показал причину —
30
+ # она печатается сразу под находкой, то есть в середине, которую обрезка и съедает.
31
+ FAILED_NAMES=""
32
+ bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; printf ' %s\n' "${2:-}"; FAIL=$((FAIL + 1));
33
+ FAILED_NAMES="${FAILED_NAMES:+$FAILED_NAMES
34
+ }$1 — ${2:-без пояснения}"; }
27
35
 
28
36
  # Node на Windows видит мир глазами Windows, а Git Bash — глазами POSIX: путь вида
29
37
  # /tmp/tmp.XXXX, отданный в `node -e`, там не существует, и проверка падает не на том, что
@@ -35,6 +43,26 @@ node_in() { D="$1"; shift; ( cd "$D" && node "$@" ); }
35
43
  WORK="$(mktemp -d)"
36
44
  trap 'rm -rf "$WORK"' EXIT
37
45
 
46
+ # ГЕРМЕТИЧНОСТЬ. Три строки, каждая закрывает свой класс отказов, найденный сплошным чтением
47
+ # и замерами 2026-09-09. Прогон обязан давать один и тот же ответ на любой машине и в любой
48
+ # день — иначе «116 зелёных» означает не «работает», а «сегодня совпало».
49
+ #
50
+ # 1. TMPDIR внутрь $WORK. В файле 65 вызовов `mktemp -d` и ОДИН trap — на $WORK. Прерванный
51
+ # прогон оставлял 64 каталога, а два из них несут по копии всего дерева (`cp -r tool kit`,
52
+ # ≈4000 файлов). Одна строка вместо правки шестидесяти пяти: mktemp читает TMPDIR, и вся
53
+ # временная работа попадает под уже существующую уборку.
54
+ # 2. HOME в песочницу. Комплект пишет `~/.config/aqk/feedback-shown` — просьбу про звезду
55
+ # показывают один раз НА МАШИНУ. Из 65 фикстур свой HOME задавали 8. Значит на машине
56
+ # разработчика (отметка лежит с сентября) `init` молчит, а на свежем раннере — говорит:
57
+ # один и тот же прогон получает РАЗНЫЙ ввод. Плюс прогон гадил в настоящий домашний каталог.
58
+ # 3. AQK_UPDATE=0. `doctor --brief` ходит в реестр npm по сети с таймаутом 3 с — и только вне
59
+ # конвейера (`updateWanted` выключается при CI). То есть локально тесты сетевые, а в
60
+ # конвейере нет. Сеть в тестах — отдельный класс флейков во всех разборах; здесь она ещё и
61
+ # делает две среды разными по построению.
62
+ export TMPDIR="$WORK/tmp"; mkdir -p "$TMPDIR"
63
+ export HOME="$WORK/home"; export USERPROFILE="$HOME"; mkdir -p "$HOME"
64
+ export AQK_UPDATE=0
65
+
38
66
  printf '\n\033[1mtool/selfcheck/smoke.sh\033[0m\n\n'
39
67
 
40
68
  # --- 1. синтаксис самой программы ------------------------------------------
@@ -345,10 +373,24 @@ if [ -f "$BLOBDIR/GOD_AI.md" ]; then
345
373
  bad "blob собрал не все методички" "в kit/docs $EXPECT_MD, в склейке $GOT_MD"
346
374
  fi
347
375
  # Ссылки на соседние файлы внутри склейки ведут в никуда: соседей рядом больше нет.
348
- if grep -qE '\]\((?!https?:)[^)]*\.md\)' "$BLOBDIR/GOD_AI.md" 2>/dev/null; then
376
+ #
377
+ # ДВА ПРОХОДА, А НЕ ОПЕРЕЖАЮЩАЯ ПРОВЕРКА. Здесь стояло `grep -qE '...(?!https?:)...'`, и это
378
+ # была ЛОЖЬ: `(?!` — синтаксис PCRE, в POSIX ERE его нет. GNU grep печатает предупреждение и
379
+ # не находит ничего, ugrep падает с ошибкой разбора — в обоих случаях `if` уходит в `else`, и
380
+ # проверка печатала зелёное НА ЛЮБЫХ ДАННЫХ. Ровно тот грех, ради поимки которого написан весь
381
+ # комплект, внутри прибора, который его ищет. Найдено сплошным чтением 2026-09-09.
382
+ #
383
+ # Поэтому же ниже проверяется САМА ПРОВЕРКА: подделываем склейку с относительной ссылкой и
384
+ # требуем, чтобы её нашли. Без этого следующая такая опечатка снова проедет зелёной.
385
+ rel_links() { grep -oE '\]\([^)]+\.md[^)]*\)' "$1" 2>/dev/null | grep -vE '^\]\(https?:' | grep -q .; }
386
+ printf '%s' "$(cat "$BLOBDIR/GOD_AI.md")" > "$BLOBDIR/doctored.md"
387
+ printf '\nсм. [соседний файл](other.md)\n' >> "$BLOBDIR/doctored.md"
388
+ if rel_links "$BLOBDIR/GOD_AI.md"; then
349
389
  bad "в склейке остались ссылки на соседние файлы" "внутри одного файла они ведут в никуда"
390
+ elif ! rel_links "$BLOBDIR/doctored.md"; then
391
+ bad "проверка ссылок не может покраснеть" "подделанная склейка с относительной ссылкой прошла"
350
392
  else
351
- ok "ссылки на соседние файлы в склейке сняты"
393
+ ok "ссылки на соседние файлы сняты, и проверка это умеет заметить"
352
394
  fi
353
395
  else
354
396
  bad "blob не создал GOD_AI.md" "$BLOBDIR"
@@ -579,7 +621,7 @@ REPDIR="$(mktemp -d)"
579
621
  (
580
622
  cd "$REPDIR" && git init -q . && mkdir -p src &&
581
623
  printf 'a = 1 # noqa\n' > src/a.py &&
582
- node "$CLI" start > /tmp/aqk-start.log 2>&1
624
+ node "$CLI" start > "$WORK/start.log" 2>&1
583
625
  )
584
626
  REP_OUT=$( cd "$REPDIR" && node "$CLI" report 2>&1 ); REP_CODE=$?
585
627
  if [ "$REP_CODE" -ne 0 ] && printf '%s' "$REP_OUT" | grep -q '❌ gate-not-weakened'; then
@@ -591,7 +633,7 @@ else
591
633
  G_LS=$( cd "$REPDIR" && ls gates 2>&1 | tr '\n' ' ' )
592
634
  G_DECL=$( cd "$REPDIR" && sed -n '/^gates:/,$p' .aqk.yml 2>/dev/null | grep -cE '^[[:space:]]+[A-Za-z0-9_-]+:' )
593
635
  G_OUT=$( cd "$REPDIR" && bash gates/gate-not-weakened/check.sh . 2>&1 | head -2 ); G_CODE=$?
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' ' ')»"
636
+ bad "report не отличает красное от зелёного" "код отчёта $REP_CODE; гейт напрямую: код $G_CODE, вывод «$(printf '%s' "$G_OUT" | tr '\n' ' ')»; в gates/: «$G_LS»; объявлено гейтов: $G_DECL; хвост start: «$(tail -4 "$WORK/start.log" 2>/dev/null | tr '\n' ' ')»"
595
637
  fi
596
638
  if [ -f "$REPDIR/.aqk/report.md" ] && grep -q '^## ' "$REPDIR/.aqk/report.md"; then
597
639
  ok "report сохраняет .aqk/report.md"
@@ -1437,6 +1479,481 @@ else
1437
1479
  fi
1438
1480
  rm -rf "$GENP"
1439
1481
 
1482
+ # --- 95. covers: запись закрыта другим арбитром, но только объявленным ---------
1483
+ # Просьба первого чужого пользователя, названная им первой: у него complexity-limit,
1484
+ # no-print-in-prod и swallowed-error держит biome, а doctor каждый прогон печатал «применимо,
1485
+ # но не поставлено: 5» — неправду. Неправда в СОБСТВЕННОМ выводе дороже всех прочих: весь
1486
+ # стандарт стоит на том, что вывод не врёт. Но поле не признание на слово: гейт, который
1487
+ # «закрывает», обязан быть объявлен в gates:, иначе covers становится способом объявить защиту,
1488
+ # которой нет. Проверяются оба конца сразу.
1489
+ COVP="$(mktemp -d)"
1490
+ ( cd "$COVP" && git init -q . && mkdir -p src rules &&
1491
+ printf 'const x=1;\n' > src/a.ts && printf '# вход\n' > AGENTS.md &&
1492
+ printf 'правило\n' > rules/r.md && printf '.x\n' > .gitignore &&
1493
+ 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
1494
+ COV=$( cd "$COVP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1495
+ if printf '%s' "$COV" | grep -q "закрыто другим арбитром 2" &&
1496
+ printf '%s' "$COV" | grep -q "covers называет гейт, которого нет" &&
1497
+ printf '%s' "$COV" | grep -q "biome" &&
1498
+ ! printf '%s' "$COV" | grep -q "неизвестное поле"; then
1499
+ ok "covers снимает запись с долга — и только когда закрывающий гейт объявлен"
1500
+ else
1501
+ bad "covers посчитан неверно" "$(printf '%s' "$COV" | grep -E 'Итого|covers' | head -3)"
1502
+ fi
1503
+ rm -rf "$COVP"
1504
+
1505
+ # --- 96. язык вывода — настройка проекта, а не машины -------------------------
1506
+ # Просьба первого чужого пользователя: «язык берётся из LC_ALL/LANG, а на Windows их просто нет:
1507
+ # русский проект получает английский вывод». Проверяются оба конца порядка сразу: манифест
1508
+ # сильнее локали, но переменная окружения сильнее манифеста — иначе у человека, набравшего
1509
+ # AQK_LANG=en руками, не остаётся способа получить английский.
1510
+ LNGP="$(mktemp -d)"
1511
+ ( cd "$LNGP" && git init -q . && printf '# вход\n' > AGENTS.md &&
1512
+ printf 'aqk: 1\nlang: ru\nentry: [AGENTS.md]\n' > .aqk.yml ) >/dev/null 2>&1
1513
+ 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 )
1514
+ BY_ENV=$( cd "$LNGP" && AQK_LANG=en node "$CLI" context 2>&1 | head -1 )
1515
+ if printf '%s' "$BY_MAN" | grep -q "состояние этого репозитория" &&
1516
+ printf '%s' "$BY_ENV" | grep -q "the state of this repository"; then
1517
+ ok "язык берётся из манифеста поверх локали, а переменная окружения — поверх манифеста"
1518
+ else
1519
+ bad "порядок выбора языка нарушен" "по манифесту: $BY_MAN | по окружению: $BY_ENV"
1520
+ fi
1521
+ rm -rf "$LNGP"
1522
+
1523
+ # --- 97. совет про браузер у агента — только тому, кому он нужен --------------
1524
+ # Решение владельца 2026-09-08: инструмент, дающий агенту браузер, надо рекомендовать. Но совет,
1525
+ # показанный не тому, стоит доверия всем остальным советам — та же норма, что у записей каталога.
1526
+ # Проверяются все три конца: интерфейс есть и сервера нет → совет; интерфейса нет → тишина;
1527
+ # сервер уже стоит → тишина.
1528
+ BRWP="$(mktemp -d)"
1529
+ ( cd "$BRWP" && git init -q . && mkdir -p src && printf 'body{color:#fff}\n' > src/a.css &&
1530
+ printf 'export const A=1\n' > src/a.tsx && printf '# вход\n' > AGENTS.md ) >/dev/null 2>&1
1531
+ WITH_UI=$( cd "$BRWP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1532
+ ( cd "$BRWP" && printf '{"mcpServers":{"b":{"command":"npx","args":["-y","chrome-devtools-mcp@1.9.0"]}}}\n' > .mcp.json )
1533
+ WITH_SRV=$( cd "$BRWP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1534
+ NOUIP="$(mktemp -d)"
1535
+ ( cd "$NOUIP" && git init -q . && printf 'print(1)\n' > a.py && printf '# вход\n' > AGENTS.md ) >/dev/null 2>&1
1536
+ NO_UI=$( cd "$NOUIP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1537
+ if printf '%s' "$WITH_UI" | grep -q "нет браузера" &&
1538
+ ! printf '%s' "$WITH_SRV" | grep -q "нет браузера" &&
1539
+ ! printf '%s' "$NO_UI" | grep -q "нет браузера"; then
1540
+ ok "совет про браузер даётся проекту с интерфейсом и молчит, когда сервер уже есть"
1541
+ else
1542
+ bad "совет про браузер показан не тому" "с ui: $(printf '%s' "$WITH_UI" | grep -c 'нет браузера'), с сервером: $(printf '%s' "$WITH_SRV" | grep -c 'нет браузера'), без ui: $(printf '%s' "$NO_UI" | grep -c 'нет браузера')"
1543
+ fi
1544
+ rm -rf "$BRWP" "$NOUIP"
1545
+
1546
+ # --- 98. заявка covers сверяется, а не принимается на слово -------------------
1547
+ # Поле covers завели утром того же дня, и тогда же честно записали: оно снимает запись с долга
1548
+ # ПО СЛОВУ человека. К вечеру это перестало быть теорией: запуск на настоящем ruff.toml из живого
1549
+ # проекта показал девятнадцать групп правил в extend-select и НЕ пойманный print() — группы T20
1550
+ # среди них нет. Заявка «no-print-in-prod держит наш lint» была бы ложной, а запись ушла бы из
1551
+ # долга. Поле, снимающее неправду из вывода, само стало бы способом её произвести.
1552
+ CVUP="$(mktemp -d)"
1553
+ ( cd "$CVUP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
1554
+ 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
1555
+ ( cd "$CVUP" && printf 'extend-select = ["I","B","UP","SIM"]\n' > ruff.toml )
1556
+ WITHOUT=$( cd "$CVUP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1557
+ ( cd "$CVUP" && printf 'extend-select = ["I","B","UP","SIM","T20"]\n' > ruff.toml )
1558
+ WITHT20=$( cd "$CVUP" && AQK_LANG=ru node "$CLI" doctor 2>&1 )
1559
+ if printf '%s' "$WITHOUT" | grep -q "заявка не подтверждена" &&
1560
+ printf '%s' "$WITHOUT" | grep -q "T20" &&
1561
+ ! printf '%s' "$WITHT20" | grep -q "заявка не подтверждена"; then
1562
+ ok "covers сверяется с кодами правил: без T20 говорит вслух, с T20 молчит"
1563
+ else
1564
+ bad "сверка заявки covers не работает" "без T20: $(printf '%s' "$WITHOUT" | grep -c 'не подтверждена'), с T20: $(printf '%s' "$WITHT20" | grep -c 'не подтверждена')"
1565
+ fi
1566
+ rm -rf "$CVUP"
1567
+
1568
+ # --- 99. краткий режим: присутствие видно, совет не надоедает -----------------
1569
+ # Хук pre-commit молчит на успехе — это его умолчание. Комплект, который всё держит, для
1570
+ # человека неотличим от невставленного: «скачал и че дальше» — дословная жалоба владельца.
1571
+ # Это ровно тот порок, против которого написан комплект, только у нас самих.
1572
+ # Проверяются три вещи сразу: строка есть всегда; совет приходит один раз, а не на каждый
1573
+ # прогон; переменная его выключает.
1574
+ BRFP="$(mktemp -d)"
1575
+ ( cd "$BRFP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
1576
+ printf '.x\n' > .gitignore && mkdir -p rules .aqk/docs && printf 'п\n' > rules/r.md &&
1577
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
1578
+ B1=$( cd "$BRFP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 ); B1C=$?
1579
+ B2=$( cd "$BRFP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 )
1580
+ rm -f "$BRFP/.aqk/advice-shown"
1581
+ B3=$( cd "$BRFP" && AQK_ADVICE=0 AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief 2>&1 )
1582
+ if [ "$B1C" -eq 0 ] &&
1583
+ printf '%s' "$B1" | grep -qE "^(❖ )?AQK держит" &&
1584
+ printf '%s' "$B1" | grep -q "поставить:" &&
1585
+ printf '%s' "$B2" | grep -qE "^(❖ )?AQK держит" &&
1586
+ ! printf '%s' "$B2" | grep -q "поставить:" &&
1587
+ ! printf '%s' "$B3" | grep -q "поставить:" &&
1588
+ [ "$(printf '%s\n' "$B2" | wc -l)" -le 2 ]; then
1589
+ ok "краткий режим: строка есть всегда, совет один раз в сутки и выключается"
1590
+ else
1591
+ bad "краткий режим ведёт себя не так" "первый: $(printf '%s' "$B1" | head -1) · второй строк: $(printf '%s\n' "$B2" | wc -l)"
1592
+ fi
1593
+ rm -rf "$BRFP"
1594
+
1595
+ # --- 100. в awk-программах гейтов нет повторителей {n} ------------------------
1596
+ # Часть сборок mawk не понимает повторители в регулярках. Условие с {4} не
1597
+ # совпадает НИКОГДА, и гейт молча выходит с нулём на собственном красном образце: ровно тот
1598
+ # отказ, против которого написан комплект. Поймано сборкой docker-образа 2026-09-08 — на хосте
1599
+ # gawk, и ни один прогон не краснел. Проверка статическая и потому дешёвая; настоящий прогон
1600
+ # под старым awk делает задание `image` в конвейере — там образ, в котором дефект и нашёлся.
1601
+ # ВАЖНО, ЧТО ИМЕННО СБОРКА: mawk 1.3.4 20240123 (ubuntu-latest, эта машина) повторители
1602
+ # ПОНИМАЕТ, mawk 1.3.4 20200120 (node:22-slim) — нет. Замерено 2026-09-09. Поэтому «прогнать
1603
+ # под mawk» ничего не значит без указания, под каким,
1604
+ # а прочитать исходники — можем.
1605
+ AWKBAD=""
1606
+ for G in "$ROOT"/kit/gates/*/check.sh "$ROOT"/kit/gates/_skip.sh; do
1607
+ [ -f "$G" ] || continue
1608
+ # Только строки внутри awk-программ; комментарии оболочки (#) не считаем.
1609
+ # Только регулярки AWK: `t ~ /…/` и образец-действие `/…/ {`. Шаблоны для grep, лежащие в
1610
+ # переменных оболочки, сюда не попадают намеренно — GNU grep и busybox grep повторители
1611
+ # понимают оба, и краснеть на них значит краснеть на нормальном укладе.
1612
+ HIT=$(sed 's/^[[:space:]]*#.*$//' "$G" | grep -nE '(~[[:space:]]*/|^[[:space:]]*/)' \
1613
+ | grep -E '\{[0-9]+(,[0-9]*)?\}' | head -2)
1614
+ [ -n "$HIT" ] && AWKBAD="${AWKBAD:+$AWKBAD; }$(basename "$(dirname "$G")"): $(printf '%s' "$HIT" | head -1 | cut -c1-60)"
1615
+ done
1616
+ if [ -z "$AWKBAD" ]; then
1617
+ ok "в проверках каталога нет повторителей {n} — они не работают под mawk"
1618
+ else
1619
+ bad "повторитель {n} в проверке: под mawk она молчит" "$AWKBAD"
1620
+ fi
1621
+
1622
+ # --- 101. проверка версии: раз в сутки, не в конвейере, молча при отказе ------
1623
+ # До этой строки комплект не делал ни одного исходящего запроса. Раз делает — обязан делать
1624
+ # ровно то, что о нём написано: спрашивать реестр не чаще раза в сутки и НЕ спрашивать в
1625
+ # конвейере. Само наличие новой версии здесь не проверяется — она зависит от реестра, а
1626
+ # проверка, зависящая от чужого сервера, краснеет по чужой воле. Сравнение версий проверено
1627
+ # модульно; здесь — то, что вокруг него.
1628
+ UPDP="$(mktemp -d)"
1629
+ ( cd "$UPDP" && git init -q . && printf 'x=1\n' > a.py && printf '# вход\n' > AGENTS.md &&
1630
+ printf '.x\n' > .gitignore && mkdir -p rules .aqk/docs && printf 'п\n' > rules/r.md &&
1631
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: rules\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
1632
+ ( cd "$UPDP" && env -u CI -u GITHUB_ACTIONS AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
1633
+ ASKED=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
1634
+ rm -f "$UPDP/.aqk/update-checked"
1635
+ ( cd "$UPDP" && CI=true AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
1636
+ IN_CI=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
1637
+ rm -f "$UPDP/.aqk/update-checked"
1638
+ ( cd "$UPDP" && env -u CI AQK_UPDATE=0 AQK_LANG=ru node "$CLI" doctor --run --min 1 --brief ) >/dev/null 2>&1
1639
+ OFF=$([ -f "$UPDP/.aqk/update-checked" ] && echo да || echo нет)
1640
+ # «нет сети» — тоже допустимый исход первого случая: молчание при отказе и есть требование.
1641
+ if [ "$IN_CI" = "нет" ] && [ "$OFF" = "нет" ]; then
1642
+ ok "версия спрашивается вне конвейера ($ASKED), не спрашивается в конвейере и при AQK_UPDATE=0"
1643
+ else
1644
+ bad "проверка версии спрашивает там, где не должна" "вне CI: $ASKED, в CI: $IN_CI, выключено: $OFF"
1645
+ fi
1646
+ rm -rf "$UPDP"
1647
+
1648
+ # --- 102. строка манифеста, которую разбор не понял, называется вслух ---------
1649
+ # Найдено случайно 2026-09-09: подсадили падающий гейт с именем «плохой», чтобы посмотреть на
1650
+ # вывод, — прогон вышел с НУЛЁМ. Гейт не упал: его не существовало. Разбор берёт имена только
1651
+ # латиницей, а строку, не подошедшую под это, выбрасывал без единого слова. Человек видит
1652
+ # проверку в файле, а её нет — наш класс в чистом виде, и хуже опечатки в имени поля: ту мы
1653
+ # называем с 2026-09-06, а эту не называли вовсе.
1654
+ UNPP="$(mktemp -d)"
1655
+ ( cd "$UNPP" && git init -q . && printf '# вход\n' > AGENTS.md &&
1656
+ printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n плохой: "false"\n' > .aqk.yml ) >/dev/null 2>&1
1657
+ UNP=$( cd "$UNPP" && node "$CLI" doctor 2>&1 )
1658
+ ( cd "$UNPP" && printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n bad: "false"\n' > .aqk.yml )
1659
+ OKM=$( cd "$UNPP" && node "$CLI" doctor 2>&1 )
1660
+ if printf '%s' "$UNP" | grep -q "НЕ ДЕЙСТВУЕТ" &&
1661
+ printf '%s' "$UNP" | grep -q "строка 6" &&
1662
+ ! printf '%s' "$OKM" | grep -q "НЕ ДЕЙСТВУЕТ"; then
1663
+ ok "непонятая строка манифеста называется с номером, понятая — молчит"
1664
+ else
1665
+ bad "потерянная строка манифеста не названа" "$(printf '%s' "$UNP" | grep -c 'НЕ ДЕЙСТВУЕТ') на кириллице, $(printf '%s' "$OKM" | grep -c 'НЕ ДЕЙСТВУЕТ') на латинице"
1666
+ fi
1667
+ rm -rf "$UNPP"
1668
+
1669
+ # --- 103. vitals: отказ роняет, выбор и незнание — нет ------------------------
1670
+ # `doctor` смотрит на репозиторий, `prove` — на гейты, `context` — на состояние. На саму
1671
+ # обвязку не смотрел никто: стоят ли инструменты объявленных гейтов, прописан ли хук в
1672
+ # .git/hooks НА САМОМ ДЕЛЕ. Раньше это выяснялось красным гейтом посреди коммита.
1673
+ # Первая версия ставила крест хуку, которого нет, — и на нашем же репозитории вышло два креста
1674
+ # за сознательное решение (pre-commit локально не ставим, гоняем в CI). Команда, которая кричит
1675
+ # «сломано» про выбор, перестаёт читаться вместе с настоящими отказами. Проверяются оба конца.
1676
+ VITP="$(mktemp -d)"
1677
+ ( cd "$VITP" && git init -q . && printf '# вход\n' > AGENTS.md &&
1678
+ printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n ok: "true"\n' > .aqk.yml ) >/dev/null 2>&1
1679
+ VOK=$( cd "$VITP" && AQK_LANG=ru node "$CLI" vitals 2>&1 ); VOK_C=$?
1680
+ ( cd "$VITP" && printf 'aqk: 1\nentry: [AGENTS.md]\nlang: ru\ngates:\n lint: "инструментакоторогонет ."\n' > .aqk.yml )
1681
+ VBAD=$( cd "$VITP" && AQK_LANG=ru node "$CLI" vitals 2>&1 ); VBAD_C=$?
1682
+ if [ "$VOK_C" -eq 0 ] && [ "$VBAD_C" -eq 1 ] &&
1683
+ printf '%s' "$VBAD" | grep -q "НЕ НАЙДЕНЫ" &&
1684
+ printf '%s' "$VOK" | grep -q "хук pre-commit"; then
1685
+ ok "vitals: пропавший инструмент роняет прогон, отсутствие хука — нет"
1686
+ else
1687
+ bad "vitals путает отказ с выбором" "исправный код $VOK_C, сломанный код $VBAD_C"
1688
+ fi
1689
+ rm -rf "$VITP"
1690
+
1691
+ # --- 106. совещательный гейт назван всегда и не роняет прогон никогда ---------
1692
+ # Два отказа, найденные 2026-09-09 сверкой документации с кодом. ПЕРВЫЙ: зелёный совещательный
1693
+ # печатался обычной галочкой — гейт, который уронить сборку НЕ МОЖЕТ, по выводу неотличим от
1694
+ # того, который может, и список `advisory:` был виден только в день, когда он покраснел. Ровно
1695
+ # та тишина, против которой написан стандарт, только про сам прибор. ВТОРОЙ: при `--since` гейт,
1696
+ # чей вывод не содержит путей, шёл через ветку «сузить нечем» с безусловным failed++ — то есть
1697
+ # совещательный ронял прогон. Проверено сравнением с кодом до починки: было 1, стало 0.
1698
+ # Команда гейта — ФАЙЛ, а не строка с `;`. На windows `spawnSync(shell: true)` запускает
1699
+ # cmd.exe, а не sh: там `echo x; exit 1` печатается целиком и выходит с НУЛЁМ. Первая версия
1700
+ # этой проверки так и провалилась в конвейере на windows — фикстура, а не код. Заодно урок про
1701
+ # сам тест: утверждение «зелёный помечен» искало подстроку «совещательный», которая есть и в
1702
+ # пометке КРАСНОГО совещательного, — то есть проходило бы и при полном отсутствии починки.
1703
+ # Теперь ищется текст, который бывает только на зелёной строке.
1704
+ ADVP="$(mktemp -d)"
1705
+ (
1706
+ cd "$ADVP" && git init -q . && git config user.email a@b && git config user.name a
1707
+ printf '# вход\n' > AGENTS.md && mkdir -p r && printf 'x\n' > r/a.md
1708
+ printf '#!/bin/sh\necho "вердикт без путей"\nexit 1\n' > noscope.sh
1709
+ printf '#!/bin/sh\nexit 0\n' > quiet.sh
1710
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash quiet.sh"\nadvisory:\n - adv\n' > .aqk.yml
1711
+ git add -A && git commit -qm "Сделано: основа. Не уверен: ничего"
1712
+ ) >/dev/null 2>&1
1713
+ ADVBASE=$( cd "$ADVP" && git rev-parse --abbrev-ref HEAD )
1714
+ ( cd "$ADVP" && git checkout -qb feat && printf 'y\n' > r/b.md && git add -A &&
1715
+ git commit -qm "Сделано: файл. Не уверен: ничего" ) >/dev/null 2>&1
1716
+ ADVGREEN=$( cd "$ADVP" && AQK_LANG=ru node "$CLI" doctor --run --min 1 2>&1 )
1717
+ ( cd "$ADVP" && printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash noscope.sh"\nadvisory:\n - adv\n' > .aqk.yml )
1718
+ ( cd "$ADVP" && node "$CLI" doctor --run --min 1 --since "$ADVBASE" >/dev/null 2>&1 ); ADV_C=$?
1719
+ ( cd "$ADVP" && printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\ngates:\n adv: "bash noscope.sh"\n' > .aqk.yml )
1720
+ ( cd "$ADVP" && node "$CLI" doctor --run --min 1 --since "$ADVBASE" >/dev/null 2>&1 ); BLOCK_C=$?
1721
+ if printf '%s' "$ADVGREEN" | grep -q "уронить прогон не может" && [ "$ADV_C" -eq 0 ] && [ "$BLOCK_C" -eq 1 ]; then
1722
+ ok "совещательный назван и на зелёном, и не роняет прогон даже когда сузить нечем"
1723
+ else
1724
+ bad "совещательный гейт неотличим или роняет прогон" "зелёный помечен: $(printf '%s' "$ADVGREEN" | grep -c 'уронить прогон не может'), код совещательного $ADV_C, код блокирующего $BLOCK_C"
1725
+ fi
1726
+ rm -rf "$ADVP"
1727
+
1728
+ # --- 107. prove пропускает запись без её программы, а не обвиняет её ----------
1729
+ # ТО ЖЕ, ЧТО НАШЁЛ ВТОРОЙ ПОЛЬЗОВАТЕЛЬ, ТОЛЬКО ДРУГОЙ ДВЕРЬЮ. Переносимый рецепт бывает обёрткой
1730
+ # вокруг готового инструмента: первое слово команды тогда `bash`, и по нему не видно, чего не
1731
+ # хватает. Без программы обёртка краснеет на ОБОИХ образцах, и prove объявлял исправный гейт
1732
+ # сломанным — «краснеет на исправном коде». От prove зависят ступень AQK-2 и значок, то есть
1733
+ # отсутствие чужой программы отбирало у проекта уровень. Обвинение вместо диагноза.
1734
+ # Приёмка (gates.sh) и мутационная проверка поле `requires` читают давно; prove — не читал.
1735
+ # Проверено 2026-09-09 на своём же репозитории: не стояли slopcheck и zizmor, prove дал два
1736
+ # обвинения и код 1. Проверяются ОБА конца: без поля — обвинение и код 1, с полем — пропуск и 0.
1737
+ PRVP="$(mktemp -d)"
1738
+ (
1739
+ cd "$PRVP" && git init -q . && printf '# вход\n' > AGENTS.md && mkdir -p r s/wrap/red s/wrap/green
1740
+ printf 'x\n' > s/wrap/red/a.txt && printf 'x\n' > s/wrap/green/a.txt
1741
+ # Обёртка вокруг программы, которой на машине нет: краснеет на обоих образцах.
1742
+ printf '#!/bin/sh\nпрограммыкоторойнет "$1" || exit 2\n' > wrap.sh
1743
+ # Настоящая проверка: prove требует, чтобы хоть один гейт был ДОКАЗАН, иначе проект,
1744
+ # у которого всё недоказуемо, получал бы уровень ни за что. Команда обязана кончаться
1745
+ # каталогом — иначе подставлять образец некуда, и гейт сам станет недоказуемым.
1746
+ printf '#!/bin/sh\ngrep -rq НЕЛЬЗЯ "$1" && exit 1\nexit 0\n' > real.sh
1747
+ printf 'aqk: 1\nentry: [AGENTS.md]\nrules: r\nsamples: s\ngates:\n wrap: "sh wrap.sh ."\n real: "sh real.sh ."\n' > .aqk.yml
1748
+ ) >/dev/null 2>&1
1749
+ # Второй гейт обязан быть доказуемым: prove требует хотя бы одного доказанного.
1750
+ ( cd "$PRVP" && mkdir -p s/real/red s/real/green &&
1751
+ printf 'НЕЛЬЗЯ\n' > s/real/red/a.txt && printf 'можно\n' > s/real/green/a.txt )
1752
+ PRV_NOREQ=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" prove 2>&1 ); PRV_NOREQ_C=$?
1753
+ ( cd "$PRVP" && printf 'intent: обёртка\nrequires: программыкоторойнет\nrecipes:\n any: "sh wrap.sh {dir}"\n' > s/wrap/gate.yml )
1754
+ PRV_REQ=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" prove 2>&1 ); PRV_REQ_C=$?
1755
+ # Тот же вопрос задаёт `vitals`, и до 2026-09-09 он отвечал на него иначе: печатал «все
1756
+ # инструменты на месте», потому что смотрел только на ПЕРВОЕ СЛОВО команды — `sh`, который есть
1757
+ # всегда. Две команды об одном репозитории говорили разное; читатель верил той, что зеленее.
1758
+ PRV_VIT=$( cd "$PRVP" && AQK_LANG=ru node "$CLI" vitals 2>&1 )
1759
+ if [ "$PRV_NOREQ_C" -ne 0 ] && [ "$PRV_REQ_C" -eq 0 ] &&
1760
+ printf '%s' "$PRV_REQ" | grep -q "НЕ ПРОВЕРЕНА здесь" &&
1761
+ printf '%s' "$PRV_VIT" | grep -q "программыкоторойнет"; then
1762
+ ok "prove и vitals одинаково видят программу из requires: пропуск, а не обвинение"
1763
+ else
1764
+ bad "prove путает «нечем проверить» со «сломан», либо vitals её не видит" \
1765
+ "без поля код $PRV_NOREQ_C, с полем код $PRV_REQ_C, vitals назвал: $(printf '%s' "$PRV_VIT" | grep -c 'программыкоторойнет')"
1766
+ fi
1767
+ rm -rf "$PRVP"
1768
+
1769
+ # --- 104. опись в AGENTS.md покрывает все исходники ---------------------------
1770
+ # AGENTS.md — первое, что читает агент, и раздел «Где что лежит» для него карта. Карта,
1771
+ # отставшая от дерева, хуже её отсутствия: агент уверен, что видел всё. Проверено 2026-09-09 —
1772
+ # в описи не было восьми файлов из четырнадцати добавленных за неделю (`brief.mjs`, `banner.mjs`
1773
+ # и шесть `units-*.mjs`), и заметить это можно было только сверкой руками. Тот же класс, что
1774
+ # «гейт объявлен и не существует»: расхождение молчит.
1775
+ MISSING=""
1776
+ for F in "$ROOT"/tool/lib/*.mjs "$ROOT"/tool/commands/*.mjs "$ROOT"/tool/selfcheck/* "$ROOT"/tool/selfcheck/*/*; do
1777
+ B=$(basename "$F")
1778
+ grep -qF "$B" "$ROOT/AGENTS.md" || MISSING="$MISSING $B"
1779
+ done
1780
+ if [ -z "$MISSING" ]; then
1781
+ ok "опись «Где что лежит» называет каждый исходник"
1782
+ else
1783
+ bad "AGENTS.md не называет файлы" "$MISSING"
1784
+ fi
1785
+
1786
+ # --- 105. два README не расходятся структурно --------------------------------
1787
+ # AGENTS.md требует, чтобы README и его перевод не расходились, и до сегодня требование не
1788
+ # работало: на 2026-09-09 в английском было 23 раздела, в русском 22 — «Первый прогон на
1789
+ # настоящем проекте» отсутствовал целиком, а «Как ввести правило» стояло в другом разделе.
1790
+ # Читатель одного языка получал инструкцию, которой у читателя другого не было. Сверяется
1791
+ # ПОСЛЕДОВАТЕЛЬНОСТЬ уровней, а не тексты: заголовки на разных языках сравнивать нельзя.
1792
+ # Строки внутри ``` не считаются: в примере .aqkignore есть строка, начинающаяся с #.
1793
+ levels() {
1794
+ awk '/^```/ { inf = !inf; next } !inf && /^#/ { sub(/[^#].*/, ""); print }' "$1"
1795
+ }
1796
+ if [ "$(levels "$ROOT/README.md")" = "$(levels "$ROOT/README.ru.md")" ]; then
1797
+ ok "README.md и README.ru.md несут одни и те же разделы ($(levels "$ROOT/README.md" | wc -l | tr -d ' '))"
1798
+ else
1799
+ bad "README и перевод разошлись структурно" \
1800
+ "англ. $(levels "$ROOT/README.md" | wc -l | tr -d ' '), рус. $(levels "$ROOT/README.ru.md" | wc -l | tr -d ' ')"
1801
+ fi
1802
+
1803
+ # --- 108. init доходит до конца, когда дом недоступен для записи ---------------
1804
+ # НАШЁЛ КОНВЕЙЕР, А НЕ ПРОГОН. Одноразовая пометка «просьбу про звезду уже показывали» пишется
1805
+ # в $HOME/.config/aqk. В контейнере, запущенном `--user 1001:127`, у этого uid нет записи в
1806
+ # /etc/passwd, homedir() даёт «/», и mkdir падает с EACCES на «/.config» — роняя ВЕСЬ init.
1807
+ # То есть любой, кто набрал команду из нашей же документации по docker, получал падение вместо
1808
+ # установки. Локально не воспроизводилось по случайности: uid разработчика 1000 совпадает с
1809
+ # пользователем `node` в образе, у которого дом есть; на раннере uid 1001 — не совпадает.
1810
+ # Удобство не имеет права ронять работу команды. Молчать о нём тоже нельзя — это swallowed-error.
1811
+ HOMEP="$(mktemp -d)"; HOMEH="$(mktemp -d)"
1812
+ chmod 500 "$HOMEH" 2>/dev/null
1813
+ # На windows chmod прав не отнимает: там эта проверка ничего не проверяет и говорит об этом,
1814
+ # а не зеленеет молча. Тот же приём, что «НЕ ПРОВЕРЕНА здесь» в приёмке каталога.
1815
+ if ( : > "$HOMEH/проба" ) 2>/dev/null; then
1816
+ rm -f "$HOMEH/проба"
1817
+ ok "init при недоступном доме — НЕ ПРОВЕРЕНА здесь: chmod прав не отнимает"
1818
+ else
1819
+ ( cd "$HOMEP" && git init -q . ) >/dev/null 2>&1
1820
+ HOMEOUT=$( cd "$HOMEP" && HOME="$HOMEH" AQK_LANG=ru node "$CLI" init 2>&1 ); HOME_C=$?
1821
+ if [ "$HOME_C" -eq 0 ] && [ -f "$HOMEP/.aqk.yml" ] &&
1822
+ printf '%s' "$HOMEOUT" | grep -q "запомнить не удалось"; then
1823
+ ok "init доходит до конца при недоступном доме и называет, чего не смог"
1824
+ else
1825
+ bad "init падает или молчит, когда дом недоступен для записи" \
1826
+ "код $HOME_C, манифест $([ -f "$HOMEP/.aqk.yml" ] && echo есть || echo нет)"
1827
+ fi
1828
+ fi
1829
+ chmod 700 "$HOMEH" 2>/dev/null; rm -rf "$HOMEP" "$HOMEH"
1830
+
1831
+ # --- 109. probe: находит слепоту и не выдаёт её за покрытие ---------------------
1832
+ # ЗАЧЕМ КОМАНДА. `doctor` говорит «держит машина N» — N из чего? `prove` доказывает, что гейт
1833
+ # ловит брак НА СВОЁМ образце. Ни один не отвечает: что в ЭТОМ репозитории не прикрыто ничем.
1834
+ # Замер 2026-09-09 на самом комплекте: настоящий файл, подсаженные проглоченная ошибка и печать,
1835
+ # 21 объявленный гейт — покраснело НОЛЬ. Узнать об этом было нечем.
1836
+ # Проверяются ОБА конца: класс, который объявленный гейт ловит, обязан быть зелёным, а тот,
1837
+ # которого не ловит никто, — красным. Проверка одного конца пропустила бы команду, которая
1838
+ # всегда говорит «не прикрыто», и команду, которая всегда говорит «прикрыто».
1839
+ PRBP="$(mktemp -d)"
1840
+ (
1841
+ cd "$PRBP" && git init -q . && git config user.email a@b && git config user.name a
1842
+ mkdir -p src && printf 'def send(to):\n return 1\n' > src/mailer.py
1843
+ git add -A && git commit -qm "feat: почта"
1844
+ printf '# правка\n' >> src/mailer.py && git add -A && git commit -qm "fix: письма не уходили"
1845
+ node "$CLI" init && node "$CLI" add todo-without-task
1846
+ # Коммитим ВСЁ, что положили init и add: иначе дерево грязное ещё до пробы, и проверка
1847
+ # «probe ничего не пишет» меряла бы мою же фикстуру, а не команду.
1848
+ git add -A && git commit -qm "Сделано: комплект. Не уверен: ничего"
1849
+ ) >/dev/null 2>&1
1850
+ PRB=$( cd "$PRBP" && AQK_LANG=ru node "$CLI" probe --top 1 2>&1 ); PRB_C=$?
1851
+ # Слепое: secrets-not-in-code не объявлен, образец под .py у него есть — обязан быть красным.
1852
+ # Прикрытое: todo-without-task объявлен — обязан быть зелёным и назвать, кто поймал.
1853
+ if [ "$PRB_C" -eq 0 ] &&
1854
+ printf '%s' "$PRB" | grep -q "НЕ ЛОВИТ НИКТО" &&
1855
+ printf '%s' "$PRB" | grep -q "ловит: todo-without-task" &&
1856
+ printf '%s' "$PRB" | grep -q "починок в истории: 1"; then
1857
+ ok "probe: слепые классы названы, прикрытый назван поимённо, прогон не уронен"
1858
+ else
1859
+ bad "probe не различает слепое и прикрытое" "код $PRB_C, слепых $(printf '%s' "$PRB" | grep -c 'НЕ ЛОВИТ'), пойманных $(printf '%s' "$PRB" | grep -c 'ловит:')"
1860
+ fi
1861
+ # КОД человека проба не трогает: образец живёт во временном каталоге. Если бы он попал в
1862
+ # репозиторий, команда осмотра стала бы командой правки — и её выключили бы в тот же день.
1863
+ # Своя отметка каденции — исключение, и единственное: она лежит в `.aqk/`, там же, где отчёт
1864
+ # прогона, и без неё каденция невозможна (следующий прогон не узнает, что проба была).
1865
+ # Проверяется именно это разделение: за пределами `.aqk/` — ни следа.
1866
+ DIRTY=$( cd "$PRBP" && git status --porcelain | grep -cv ' \.aqk/' )
1867
+ if [ "$DIRTY" -eq 0 ]; then
1868
+ ok "probe не трогает код: пишет только свою отметку в .aqk/"
1869
+ else
1870
+ bad "probe оставил следы вне .aqk/" "$(cd "$PRBP" && git status --porcelain | grep -v ' \.aqk/' | head -3 | tr '\n' ' ')"
1871
+ fi
1872
+ rm -rf "$PRBP"
1873
+
1874
+ # --- 111. проба случается САМА, один раз, и попадает агенту в контекст -----------
1875
+ # ГЛАВНОЕ В ЭТОЙ ПРОВЕРКЕ — не механика, а замысел. Владелец сформулировал так: «команду, о
1876
+ # которой надо вспомнить, агент не вспомнит, а человек о ней не узнает». Это тот же класс, что
1877
+ # файл, который можно не прочитать, — и весь комплект написан против него. Значит `probe`,
1878
+ # оставленная ручной, отвечает на важнейший вопрос и не задаётся никем.
1879
+ # Проверяются три вещи, и каждая — отдельный способ провалиться:
1880
+ # 1. прогон делает пробу САМ, когда её не делали;
1881
+ # 2. второй прогон подряд её НЕ повторяет — иначе каденция превращается в шум;
1882
+ # 3. агент узнаёт результат из блока состояния, не зная про команду.
1883
+ CADP="$(mktemp -d)"
1884
+ (
1885
+ cd "$CADP" && git init -q . && git config user.email a@b && git config user.name a
1886
+ mkdir -p src && printf 'def s():\n return 1\n' > src/a.py
1887
+ git add -A && git commit -qm "feat: старт"
1888
+ printf '# п\n' >> src/a.py && git add -A && git commit -qm "fix: не работало"
1889
+ node "$CLI" init && node "$CLI" add todo-without-task
1890
+ ) >/dev/null 2>&1
1891
+ CTX0=$( cd "$CADP" && AQK_LANG=ru node "$CLI" context 2>&1 )
1892
+ RUN1=$( cd "$CADP" && AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
1893
+ RUN2=$( cd "$CADP" && AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
1894
+ CTX1=$( cd "$CADP" && AQK_LANG=ru node "$CLI" context 2>&1 )
1895
+ if printf '%s' "$CTX0" | grep -q "НЕИЗВЕСТНО" &&
1896
+ printf '%s' "$RUN1" | grep -q "ещё не делали" &&
1897
+ ! printf '%s' "$RUN2" | grep -q "ещё не делали" &&
1898
+ printf '%s' "$CTX1" | grep -q "Не прикрыто ничем"; then
1899
+ ok "проба запускается прогоном сама, не повторяется и попадает агенту в контекст"
1900
+ else
1901
+ bad "каденция пробы не работает" \
1902
+ "до: $(printf '%s' "$CTX0" | grep -c 'НЕИЗВЕСТНО'), первый: $(printf '%s' "$RUN1" | grep -c 'ещё не делали'), второй: $(printf '%s' "$RUN2" | grep -c 'ещё не делали'), после: $(printf '%s' "$CTX1" | grep -c 'Не прикрыто')"
1903
+ fi
1904
+ # Выключатель обязан быть у всего, что случается само: иначе первый же, кому это помешало,
1905
+ # выключит весь прогон, а не одну пробу.
1906
+ CADP2="$(mktemp -d)"
1907
+ ( cd "$CADP2" && git init -q . && git config user.email a@b && git config user.name a
1908
+ mkdir -p src && printf 'def s():\n return 1\n' > src/a.py
1909
+ git add -A && git commit -qm "feat: старт"
1910
+ node "$CLI" init && node "$CLI" add todo-without-task ) >/dev/null 2>&1
1911
+ OFF=$( cd "$CADP2" && AQK_PROBE=0 AQK_LANG=ru node "$CLI" doctor --run 2>&1 )
1912
+ if ! printf '%s' "$OFF" | grep -q "ещё не делали"; then
1913
+ ok "AQK_PROBE=0 выключает пробу, не трогая прогон"
1914
+ else
1915
+ bad "проба игнорирует выключатель" "AQK_PROBE=0 не подействовал"
1916
+ fi
1917
+ rm -rf "$CADP" "$CADP2"
1918
+
1919
+ # --- переехавшие проверки: встроенный раннер --------------------------------
1920
+ # ПОЧЕМУ ЗДЕСЬ МОСТ, А НЕ ВТОРАЯ КОМАНДА. Проверки переезжают на `node --test` по одной, и всё
1921
+ # это время у прогона обязана оставаться ОДНА точка входа и ОДИН счётчик: два числа в двух
1922
+ # местах через месяц разойдутся, и никто не заметит, что половина не запускается.
1923
+ #
1924
+ # ЗАЧЕМ ПЕРЕЕЗД. У встроенного раннера есть то, чего нет у этого файла и не появится: свой
1925
+ # каталог и уборка на каждую проверку, таймаут на каждую, точечный перезапуск одной по имени
1926
+ # (`--test-name-pattern`). Диагностика отказа падает с сорока секунд до одной. Проверено на
1927
+ # Node 20.20.2 — той версии, что стоит в конвейере.
1928
+ NODE_SMOKE="$ROOT/tool/selfcheck/smoke"
1929
+ if [ -d "$NODE_SMOKE" ]; then
1930
+ NS_OUT=$(cd "$ROOT" && node --test --test-reporter=tap "$NODE_SMOKE"/*.test.mjs 2>&1)
1931
+ # Разбираем только верхний уровень TAP: вложенные строки идут с отступом.
1932
+ while IFS= read -r NS_LINE; do
1933
+ case "$NS_LINE" in
1934
+ "ok "*) ok "${NS_LINE#*- }" ;;
1935
+ "not ok "*)
1936
+ # ОБЪЯСНЕНИЕ ОБЯЗАНО ДОЕХАТЬ. Первая редакция моста печатала только «не прошло» и совет
1937
+ # запустить вручную — то есть отправляла гадать ровно там, где ответ уже был получен.
1938
+ # Стоило это трёх кругов конвейера: проверка падала только на раннере, а сообщение
1939
+ # утверждения оставалось в выводе, который мост выбрасывал. TAP кладёт его в блок после
1940
+ # строки `not ok`, полем `error:`.
1941
+ # TAP кладёт многострочное сообщение блочным скаляром: строка `error: |-`, а сам текст
1942
+ # идёт следующими строками с отступом. Первая редакция брала только строку `error:` и
1943
+ # печатала «|-» — то есть снова ничего.
1944
+ NS_WHY=$(printf '%s\n' "$NS_OUT" | awk '
1945
+ /^[[:space:]]+error:/ { inerr = 1; sub(/^[[:space:]]*error:[[:space:]]*/, ""); if ($0 != "|-" && $0 != "") print; next }
1946
+ inerr && /^[[:space:]]+(code|stack|failureType|type|duration_ms):/ { inerr = 0 }
1947
+ inerr && /^[[:space:]]*\.\.\.[[:space:]]*$/ { inerr = 0 }
1948
+ inerr { sub(/^[[:space:]]+/, ""); print }
1949
+ ' | head -6 | tr '\n' ' ')
1950
+ bad "${NS_LINE#*- }" "${NS_WHY:-подробности: node --test $NODE_SMOKE/*.test.mjs}" ;;
1951
+ esac
1952
+ done <<EOF
1953
+ $(printf '%s\n' "$NS_OUT" | grep -E '^(ok|not ok) ')
1954
+ EOF
1955
+ fi
1956
+
1440
1957
  # --- итог -------------------------------------------------------------------
1441
1958
  printf '\n'
1442
1959
  if [ "$FAIL" -eq 0 ]; then
@@ -1445,5 +1962,10 @@ else
1445
1962
  printf ' \033[31mпровалено: %s из %s\033[0m\n\n' "$FAIL" "$((PASS + FAIL))"
1446
1963
  fi
1447
1964
 
1448
- printf ' \033[2mне покрыто: содержание документов, установка с GitHub через npx\033[0m\n\n'
1965
+ printf ' \033[2mне покрыто: СОДЕРЖАНИЕ документов (сверяется опись и структура, не текст),\n установка с GitHub через npx\033[0m\n\n'
1966
+
1967
+ # ИМЕНА УПАВШИХ — САМОЙ ПОСЛЕДНЕЙ СТРОКОЙ, и это не косметика. Прогон запускают гейтом, а вывод
1968
+ # упавшего гейта обрезается: видно голову и хвост. Значит единственное место, где список
1969
+ # гарантированно доедет до глаз, — конец. Проверено прогоном с нарочно сломанной проверкой.
1970
+ [ "$FAIL" -eq 0 ] || printf ' \033[31mупало: %s\033[0m\n\n' "$FAILED_NAMES"
1449
1971
  exit "$FAIL"