agent-quality-kit 0.6.0 → 0.8.0

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