agent-quality-kit 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +53 -2
  2. package/README.ru.md +35 -1
  3. package/kit/docs/ai/agent-harness-playbook.md +1 -1
  4. package/kit/docs/ready-made-rules.md +65 -0
  5. package/kit/gates/README.md +60 -0
  6. package/kit/gates/ci-actually-fails/README.md +54 -0
  7. package/kit/gates/ci-actually-fails/check.sh +116 -0
  8. package/kit/gates/ci-actually-fails/gate.yml +14 -0
  9. package/kit/gates/ci-actually-fails/green/.github/workflows/ci.yml +30 -0
  10. package/kit/gates/ci-actually-fails/red/.github/workflows/ci.yml +12 -0
  11. package/kit/gates/ci-actually-fails/red/.github/workflows/soft.yml +15 -0
  12. package/kit/gates/color-from-token/check.sh +13 -1
  13. package/kit/gates/commit-explains-itself/README.md +13 -3
  14. package/kit/gates/commit-explains-itself/check.sh +8 -4
  15. package/kit/gates/complexity-limit/README.md +5 -0
  16. package/kit/gates/complexity-limit/check.sh +21 -2
  17. package/kit/gates/complexity-limit/green/test_fixtures.py +14 -0
  18. package/kit/gates/deps-are-pinned/README.md +14 -1
  19. package/kit/gates/deps-are-pinned/check.sh +6 -1
  20. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/pyproject.toml +12 -0
  21. package/kit/gates/deps-are-pinned/green/pyproject-with-requirements/requirements.txt +3 -0
  22. package/kit/gates/deps-are-pinned/red/pyproject-loose/pyproject.toml +12 -0
  23. package/kit/gates/deps-are-pinned/red/pyproject-loose/requirements.txt +3 -0
  24. package/kit/gates/duplicate-code/README.md +11 -2
  25. package/kit/gates/duplicate-code/check.sh +31 -4
  26. package/kit/gates/duplicate-code/gate.yml +8 -0
  27. package/kit/gates/duplicate-code/green/imports_a.go +20 -0
  28. package/kit/gates/duplicate-code/green/imports_b.go +19 -0
  29. package/kit/gates/entry-links-exist/README.md +5 -0
  30. package/kit/gates/entry-links-exist/check.sh +6 -0
  31. package/kit/gates/entry-links-exist/green/AGENTS.md +3 -0
  32. package/kit/gates/file-size-limit/README.md +9 -2
  33. package/kit/gates/file-size-limit/check.sh +13 -1
  34. package/kit/gates/gate-not-weakened/README.md +54 -0
  35. package/kit/gates/gate-not-weakened/check.sh +84 -0
  36. package/kit/gates/gate-not-weakened/gate.yml +15 -0
  37. package/kit/gates/gate-not-weakened/green/checkout.ts +8 -0
  38. package/kit/gates/gate-not-weakened/green/payments.py +6 -0
  39. package/kit/gates/gate-not-weakened/green/release.sh +2 -0
  40. package/kit/gates/gate-not-weakened/red/checkout.ts +9 -0
  41. package/kit/gates/gate-not-weakened/red/payments.py +6 -0
  42. package/kit/gates/gate-not-weakened/red/release.sh +2 -0
  43. package/kit/gates/hook-actually-fires/README.md +74 -0
  44. package/kit/gates/hook-actually-fires/check.sh +183 -0
  45. package/kit/gates/hook-actually-fires/gate.yml +15 -0
  46. package/kit/gates/hook-actually-fires/green/.claude/hooks/hooks.json +3 -0
  47. package/kit/gates/hook-actually-fires/green/.claude/settings.json +74 -0
  48. package/kit/gates/hook-actually-fires/green/.claude/settings.local.json +74 -0
  49. package/kit/gates/hook-actually-fires/red/.claude/hooks/hooks.json +4 -0
  50. package/kit/gates/hook-actually-fires/red/.claude/settings.json +53 -0
  51. package/kit/gates/no-phantom-package/README.md +84 -0
  52. package/kit/gates/no-phantom-package/check.sh +161 -0
  53. package/kit/gates/no-phantom-package/gate.yml +20 -0
  54. package/kit/gates/no-phantom-package/green/AGENTS.md +15 -0
  55. package/kit/gates/no-phantom-package/red/AGENTS.md +15 -0
  56. package/kit/gates/no-print-in-prod/README.md +33 -39
  57. package/kit/gates/no-print-in-prod/gate.yml +14 -6
  58. package/kit/gates/personal-config-not-shared/README.md +66 -0
  59. package/kit/gates/personal-config-not-shared/check.sh +103 -0
  60. package/kit/gates/personal-config-not-shared/gate.yml +16 -0
  61. package/kit/gates/personal-config-not-shared/green/.aqk-tracked +9 -0
  62. package/kit/gates/personal-config-not-shared/red/.aqk-tracked +6 -0
  63. package/kit/gates/promise-has-gate/README.md +50 -0
  64. package/kit/gates/promise-has-gate/check.sh +88 -0
  65. package/kit/gates/promise-has-gate/gate.yml +14 -0
  66. package/kit/gates/promise-has-gate/green/.aqk.yml +6 -0
  67. package/kit/gates/promise-has-gate/green/AGENTS.md +7 -0
  68. package/kit/gates/promise-has-gate/red/.aqk.yml +6 -0
  69. package/kit/gates/promise-has-gate/red/AGENTS.md +7 -0
  70. package/kit/gates/secrets-not-in-code/check.sh +13 -1
  71. package/kit/gates/swallowed-error/README.md +36 -18
  72. package/kit/gates/swallowed-error/gate.yml +13 -3
  73. package/kit/gates/test-has-assertion/README.md +47 -0
  74. package/kit/gates/test-has-assertion/check.sh +206 -0
  75. package/kit/gates/test-has-assertion/gate.yml +15 -0
  76. package/kit/gates/test-has-assertion/green/checkout.test.ts +9 -0
  77. package/kit/gates/test-has-assertion/green/test_billing.py +17 -0
  78. package/kit/gates/test-has-assertion/red/checkout.test.ts +8 -0
  79. package/kit/gates/test-has-assertion/red/test_billing.py +14 -0
  80. package/kit/gates/test-not-adjusted/README.md +79 -0
  81. package/kit/gates/test-not-adjusted/check.sh +136 -0
  82. package/kit/gates/test-not-adjusted/gate.yml +19 -0
  83. package/kit/gates/test-not-adjusted/green/after/calc.py +6 -0
  84. package/kit/gates/test-not-adjusted/green/after/tests/test_calc.py +9 -0
  85. package/kit/gates/test-not-adjusted/green/before/calc.py +2 -0
  86. package/kit/gates/test-not-adjusted/green/before/tests/test_calc.py +5 -0
  87. package/kit/gates/test-not-adjusted/red/after/calc.py +2 -0
  88. package/kit/gates/test-not-adjusted/red/after/tests/test_calc.py +5 -0
  89. package/kit/gates/test-not-adjusted/red/before/calc.py +2 -0
  90. package/kit/gates/test-not-adjusted/red/before/tests/test_calc.py +7 -0
  91. package/kit/gates/todo-without-task/README.md +6 -0
  92. package/kit/gates/todo-without-task/check.sh +13 -1
  93. package/kit/ratchet/ratchet.sh +70 -2
  94. package/kit/rules/general.md +23 -0
  95. package/kit/rules-en/general.md +82 -0
  96. package/kit/rules-en/security.md +33 -0
  97. package/kit/rules-en/testing.md +48 -0
  98. package/llms.txt +2 -1
  99. package/package.json +4 -2
  100. package/tool/commands/badge.mjs +7 -1
  101. package/tool/commands/doctor.mjs +90 -10
  102. package/tool/commands/gates.mjs +19 -5
  103. package/tool/commands/project.mjs +15 -2
  104. package/tool/commands/prove.mjs +67 -0
  105. package/tool/commands/report.mjs +4 -1
  106. package/tool/i18n/en-docs.mjs +70 -0
  107. package/tool/i18n/en.mjs +66 -54
  108. package/tool/i18n/ru-docs.mjs +70 -0
  109. package/tool/i18n/ru.mjs +66 -54
  110. package/tool/i18n/templates-en.mjs +9 -9
  111. package/tool/i18n/templates-ru.mjs +9 -9
  112. package/tool/lib/core.mjs +7 -1
  113. package/tool/lib/manifest.mjs +72 -5
  114. package/tool/lib/prove.mjs +160 -0
  115. package/tool/lib/repo.mjs +31 -2
  116. package/tool/lib/scope.mjs +131 -0
  117. package/tool/lib/templates.mjs +2 -0
  118. package/tool/program.mjs +6 -0
  119. package/tool/selfcheck/gates.sh +86 -3
  120. package/tool/selfcheck/lifecycle.mjs +29 -0
  121. package/tool/selfcheck/mutation.sh +21 -1
  122. package/tool/selfcheck/smoke.sh +329 -36
  123. package/tool/selfcheck/units-level.mjs +60 -0
  124. package/tool/selfcheck/units.mjs +196 -1
  125. package/kit/gates/no-print-in-prod/check.sh +0 -38
  126. package/kit/gates/no-print-in-prod/green/docs.ts +0 -15
  127. package/kit/gates/no-print-in-prod/green/main.go +0 -8
  128. package/kit/gates/no-print-in-prod/green/main.rs +0 -4
  129. package/kit/gates/no-print-in-prod/red/main.go +0 -8
  130. package/kit/gates/no-print-in-prod/red/main.rs +0 -4
  131. package/kit/gates/swallowed-error/check.sh +0 -54
  132. package/kit/gates/swallowed-error/green/run.js +0 -8
  133. package/kit/gates/swallowed-error/red/run.js +0 -3
  134. /package/kit/gates/commit-explains-itself/green/{COMMIT_MSG → .aqk-commit-msg} +0 -0
  135. /package/kit/gates/commit-explains-itself/red/{COMMIT_MSG → .aqk-commit-msg} +0 -0
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env sh
2
+ # Имена пакетов, названные в документации, сверяются с реестром npm.
3
+ #
4
+ # ЗАЧЕМ. Агент, не знающий инструмента, придумывает правдоподобное имя. Дальше его читает
5
+ # другой агент и выполняет `npm install` — и если имя к тому времени кем-то занято, в проект
6
+ # приезжает чужой код. Обычные проверки цепочки поставок смотрят на пакеты, которые ЕСТЬ;
7
+ # здесь опасен как раз тот, которого пока нет.
8
+ #
9
+ # Работу делает slopcheck (MIT, ноль зависимостей). Мы отвечаем за три вещи, которых он не
10
+ # делает: какие файлы ему дать, что считать браком и что делать, когда ответа не было.
11
+ # Каждая ветка «ответа не было» кончается кодом 2 и словами «проверка не состоялась»: у
12
+ # инструмента, написанного против молчаливого зелёного, своего молчаливого зелёного быть не может.
13
+ DIR="${1:-.}"
14
+
15
+ # Существование файла проверяется ДО `.`, а не запасной веткой после. В POSIX-оболочке (здесь
16
+ # dash) неудачный `.` завершает скрипт немедленно — привычное «. файл || запасной_вариант» не
17
+ # выполняется НИКОГДА, и гейт умирает с кодом 2 без единого слова о причине. Проверено прогоном
18
+ # 2026-09-08; та же мёртвая ветка стоит ещё в девяти проверках каталога.
19
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
20
+ if [ ! -f "$SKIP_LIB" ]; then
21
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
22
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
23
+ exit 2
24
+ fi
25
+ . "$SKIP_LIB"
26
+
27
+ # Даже когда файл на месте, нужных функций в нём может не оказаться: без них конвейер ниже не
28
+ # напечатает ни одного пути, список окажется пуст, а пустой список — это `exit 0`. Гейт вышел бы
29
+ # зелёным, не посмотрев ни в один файл.
30
+ if ! command -v own_samples_filter >/dev/null 2>&1; then
31
+ echo "в _skip.sh нет обхода own_samples_filter — проверка не состоялась"
32
+ echo " почини: обнови kit/gates/_skip.sh до версии, идущей с этим гейтом"
33
+ exit 2
34
+ fi
35
+
36
+ if ! command -v slopcheck >/dev/null 2>&1; then
37
+ echo "не найден slopcheck — эта проверка делегирована ему"
38
+ echo " почини: npm i -g slopcheck@0.2.0"
39
+ exit 2
40
+ fi
41
+
42
+ # Свой обход, а не встроенный в slopcheck: только так работает own_samples_filter. Без него
43
+ # красный образец этой же записи выдаётся за находку, и гейт краснеет на самом комплекте.
44
+ LIST=$(mktemp) || exit 2
45
+ # shellcheck disable=SC2046
46
+ find "$DIR" $(skip_find "$DIR") -type f \
47
+ \( -name '*.md' -o -name '*.mdc' -o -name '*.yml' -o -name '*.yaml' \
48
+ -o -name '*.json' -o -name '.cursorrules' \) -print 2>/dev/null \
49
+ | own_samples_filter "$DIR" > "$LIST"
50
+
51
+ if [ ! -s "$LIST" ]; then rm -f "$LIST"; exit 0; fi
52
+
53
+ # Через NUL, а не через $(...): путь с пробелом иначе распадается на два аргумента, slopcheck
54
+ # не находит ни одного из них и молча выходит с нулём. Та же ловушка уже стоила нам зелёного
55
+ # вердикта в hook-actually-fires.
56
+ ERRF=$(mktemp) || { rm -f "$LIST"; exit 2; }
57
+ OUT=$(tr '\n' '\0' < "$LIST" | xargs -0 slopcheck --json 2>"$ERRF")
58
+ # Код xargs: 0 — все вызовы прошли, 123 — хотя бы один вернул 1..125. Именно 123 приходит и на
59
+ # честной находке (slopcheck выходит с 1), поэтому сам по себе он ничего не значит. А вот всё
60
+ # остальное — сбой обвязки, и его нельзя принимать за вердикт.
61
+ XS=$?
62
+ ERR=$(cat "$ERRF" 2>/dev/null)
63
+ rm -f "$ERRF" "$LIST"
64
+
65
+ # Файлов может оказаться больше, чем влезает в одну команду, и тогда xargs зовёт slopcheck
66
+ # несколько раз. Упавший вызов пишет в stderr, а уцелевшие всё равно печатают свой JSON —
67
+ # счётчики сходятся, и гейт вышел бы с нулём, не проверив целую партию файлов.
68
+ if [ -n "$ERR" ] || { [ "$XS" -ne 0 ] && [ "$XS" -ne 123 ]; }; then
69
+ echo "slopcheck не отработал (код $XS) — проверка не состоялась, это не вердикт «чисто»"
70
+ [ -n "$ERR" ] && printf '%s\n' "$ERR" | head -5 | sed 's/^/ /'
71
+ echo " почини: прогони «slopcheck --json .» руками и посмотри, на чём он споткнулся"
72
+ exit 2
73
+ fi
74
+
75
+ [ -n "$OUT" ] || {
76
+ echo "slopcheck ничего не ответил — проверка не состоялась, это не вердикт «чисто»"
77
+ echo " почини: прогони «slopcheck --json .» руками и посмотри, на чём он споткнулся"
78
+ exit 2
79
+ }
80
+
81
+ printf '%s\n' "$OUT" | awk '
82
+ function val(s) { sub(/^[^:]*:[[:space:]]*/, "", s); sub(/,$/, "", s); gsub(/^"|"$/, "", s); return s }
83
+ function label(s) {
84
+ if (s == "not_found") return "в реестре такого пакета нет"
85
+ if (s == "unpublished") return "пакет был и снят с публикации — имя свободно для захвата"
86
+ if (s == "security_hold") return "реестр пометил пакет как вредоносный"
87
+ return ""
88
+ }
89
+ # xargs может позвать slopcheck несколько раз, если путей слишком много для одной команды.
90
+ # Тогда JSON-документов на входе будет несколько, и счётчики надо складывать, а не брать
91
+ # последний: иначе находки первой партии исчезнут.
92
+ /^ "packages"/ { inpkg = 1; sawpkg = 1; next }
93
+ inpkg && /^ \}/ { inpkg = 0; next }
94
+ inpkg && /"errors"/ { errs += val($0); next }
95
+ inpkg && /"notFound"/ { bad += val($0); next }
96
+ inpkg && /"unpublished"/ { bad += val($0); next }
97
+ inpkg && /"securityHold"/ { bad += val($0); next }
98
+ /^ "package"/ { pkg = val($0); next }
99
+ /^ "status"/ { st = val($0); next }
100
+ /^ "file"/ { f = val($0); next }
101
+ /^ "line"/ { ln = val($0); next }
102
+ # Печатаем на «command»: он идёт последним в записи о месте, значит к этому моменту известны
103
+ # и пакет, и состояние, и файл со строкой.
104
+ /^ "command"/ {
105
+ cmd = val($0)
106
+ if (st == "error") next
107
+ # Состояние, которого мы не знаем. Раньше здесь стояло «всё, что не error, — брак», и
108
+ # новое состояние в следующей версии slopcheck превратило бы КАЖДУЮ команду установки в
109
+ # находку с английским словом вместо объяснения. Незнакомое состояние — повод сказать
110
+ # «не разобрали», а не вынести вердикт.
111
+ if (label(st) == "") { unknown = st; next }
112
+ if (shown < 20) print f ":" ln ": «" pkg "» — " label(st) " · " cmd
113
+ shown++
114
+ next
115
+ }
116
+ END {
117
+ if (unknown != "") {
118
+ print "slopcheck вернул незнакомое состояние «" unknown "» — разобрать ответ не вышло"
119
+ print " почини: сверь версию slopcheck с той, что названа в gate.yml"
120
+ exit 2
121
+ }
122
+ if (shown > 20) print " … и ещё " (shown - 20)
123
+ if (shown > 0) {
124
+ # Находки и «не проверено» показываются вместе. Свернуть второе в первое значило бы
125
+ # потерять ровно ту разницу, ради которой написана эта обёртка.
126
+ if (errs > 0) print " кроме того, реестр не ответил по именам: " errs " — они НЕ проверены"
127
+ print " почини: проверь имя в реестре и впиши настоящее. Если пакета нет, его может"
128
+ print " зарегистрировать кто угодно — и следующий агент выполнит установку чужого кода."
129
+ exit 1
130
+ }
131
+ # Реестр не ответил. Молчать здесь нельзя: тишина неотличима от «всё хорошо», а именно
132
+ # это наш стандарт и запрещает. Красим отдельным кодом, чтобы вердикт не путали с браком.
133
+ # Причина названа обеими: slopcheck помечает так и недоступную сеть, и ответ 429 «слишком
134
+ # часто», и любой другой не-200 — снаружи они неразличимы, и врать про сеть мы не будем.
135
+ if (errs > 0) {
136
+ print "реестр npm не ответил: не проверено имён — " errs
137
+ print " проверка не состоялась. Это не вердикт «чисто»."
138
+ print " почини: если сети нет — прогони гейт там, где она есть. Если сеть есть, реестр"
139
+ print " мог ограничить частоту запросов: повтори через минуту."
140
+ exit 2
141
+ }
142
+ # Блока со счётчиками не нашлось вовсе. Так будет, если slopcheck начнёт печатать JSON
143
+ # одной строкой: ни одно правило выше не сработает, все счётчики останутся нулями — и
144
+ # проверка выйдет с нулём, ничего не проверив. Нашли это прогоном подставного вывода уже
145
+ # после того, как записали «формат стабилен»: догадка о стабильности стоила бы молчаливого
146
+ # зелёного на каждом прогоне.
147
+ if (!sawpkg) {
148
+ print "ответ slopcheck не разобран: блока «packages» в нём нет"
149
+ print " почини: сверь версию slopcheck с той, что названа в gate.yml"
150
+ exit 2
151
+ }
152
+ # Счётчик говорит о браке, а печатать нечего — значит мы разучились читать вывод slopcheck
153
+ # (сменился формат). Тоже не вердикт «чисто».
154
+ if (bad > 0) {
155
+ print "slopcheck насчитал находок: " bad ", но разобрать их не вышло — сменился формат"
156
+ print " почини: сверь версию slopcheck с той, что названа в gate.yml"
157
+ exit 2
158
+ }
159
+ exit 0
160
+ }
161
+ '
@@ -0,0 +1,20 @@
1
+ intent: имя пакета, названное в документации, существует в реестре
2
+ intent_en: a package name mentioned in the documentation exists in the registry
3
+
4
+ # Там, где агента используют: предмет проверки — текст, который агент читает и по которому
5
+ # выполняет установку. В проекте без такого текста ставить запись незачем.
6
+ trigger:
7
+ has_agent_entry: true
8
+
9
+ recipes:
10
+ any: bash {gate}/check.sh {dir}
11
+
12
+ # Программа, без которой запись не работает. По первому слову команды этого не видно: обёртка
13
+ # начинается с `bash`, который есть всегда. Версия названа: обёртка разбирает JSON slopcheck,
14
+ # и смена формата обязана быть видимой, а не тихой.
15
+ requires: slopcheck
16
+
17
+ proof: incidents/README.md, 2026-09-08 «пакет-призрак из чужого README оказался
18
+ зарегистрирован» — `react-codeshift`, канонический пример slopsquatting из README самого
19
+ slopcheck, на 2026-09-08 существует в npm (создан 2026-01-14, maintainer debugducky);
20
+ доля выдуманных имён в сгенерированном коде — USENIX Security 2025, arxiv 2406.10279
@@ -0,0 +1,15 @@
1
+ # Правила проекта
2
+
3
+ ## Как чинить импорты
4
+
5
+ Перед коммитом прогони кодмод:
6
+
7
+ ```bash
8
+ npx jscodeshift --fix src/
9
+ ```
10
+
11
+ ## Форматирование
12
+
13
+ ```bash
14
+ npm install prettier
15
+ ```
@@ -0,0 +1,15 @@
1
+ # Правила проекта
2
+
3
+ ## Как чинить импорты
4
+
5
+ Перед коммитом прогони кодмод:
6
+
7
+ ```bash
8
+ npx reactCodemodHelper --fix src/
9
+ ```
10
+
11
+ ## Форматирование
12
+
13
+ ```bash
14
+ npm install prettier
15
+ ```
@@ -3,42 +3,36 @@
3
3
  **Намерение.** `print` и `console.log` не попадают в прод: они проходят мимо системы логов,
4
4
  не имеют уровня и могут вынести наружу то, чего в выводе быть не должно.
5
5
 
6
- **Какой отказ это поймало.** Прогон по `audit_project`: 40 находок, из них **5 настоящих**
7
- отладочный хук `useScrollDebugger.ts`, уехавший во фронтенд и печатавший размеры окна в консоль
8
- пользователя. Остальные 35 разобраны поимённо и оказались законной печатью или шумом проверки.
9
- Запись в журнале: `incidents/README.md`, 2026-09-03.
10
-
11
- До этого прогона доказательства у записи не было, и она полтора месяца висела условной. Так и
12
- надо: каталог не выбрасывает слабое доказательство и не делает вид, что оно сильное, — он его
13
- помечает и ждёт факта.
14
-
15
- **Для программы командной строки печать — это интерфейс.** В `main` консольной утилиты
16
- `fmt.Println` или `println!` — не забытая отладка, а способ выдать результат. Такие каталоги
17
- проект называет сам, рядом с командой в манифесте:
18
-
19
- ```yaml
20
- no-print-in-prod: "AQK_PRINT_OK_DIRS=cli bash .aqk/gates/no-print-in-prod/check.sh ."
21
- ```
22
-
23
- **Не храповиком.** Сначала мы записали такие места долгом и каждая новая строка вывода красила
24
- проверку, требуя переснять реестр. Долг это то, что собираются погасить; печать из программы
25
- командной строки убирать никто не будет. Долг, который нельзя погасить работой, — не долг, а
26
- неверная мера. Храповик остаётся для настоящего долга: печати в прод-коде, которую уберут, но
27
- не сегодня.
28
-
29
- **Где печать законна.** Вспомогательные скрипты, оснастка агента, примеры, записные книжки,
30
- миграции там печать это способ говорить с человеком, а не забытая отладка. Эти каталоги
31
- проверка пропускает. Различие важное: секрет в `scripts/` — такой же секрет, а печать — нет.
32
-
33
- **Готовый аналог есть и он точнее.** В Python это `ruff --select T20`, в TypeScript — правило
34
- `no-console` в eslint. Рецепты под эти стеки берут готовое; переносимая проверка остаётся для
35
- Go, Rust и всего остального, где готового под рукой нет.
36
-
37
- **Чего НЕ ловит.** Печать через обёртку `myprint(x)`, `logger.print`, свой хелпер проверка не
38
- видит: она ищет известные конструкции языка, а не «вывод в поток». Не видит и печать в строке,
39
- собранной по частям. Найдено на комментариях: печать внутри `//`, `#` и `/* */` отбрасывается,
40
- потому что комментарий не выполняется.
41
-
42
- **Образцы.** `red/` — модуль с отладочной печатью на трёх языках. `green/` — тот же модуль через
43
- систему логов, плюс пример в JSDoc и закомментированная строка, на которых проверка обязана
44
- молчать.
6
+ **Своей проверки здесь больше нет и это результат замера, а не вкуса.** Переносимая версия
7
+ искала конструкции печати по тексту. Прогон по пяти чужим репозиториям (`httpx`, `fastapi`,
8
+ `zod`, `cobra`, `ripgrep`) дал **434 находки, настоящих ноль**:
9
+
10
+ | Где нашлось | Что это было на самом деле |
11
+ |---|---|
12
+ | `zod/packages/bench` 231 | бенчмарки, где печать и есть предмет |
13
+ | `fastapi/docs_src` | примеры кода в документации |
14
+ | `httpx/_exceptions.py` | печать **внутри строки документации**, между ``` |
15
+ | `fastapi/cli.py` | вывод программы командной строки — это интерфейс |
16
+ | `httpx/tests` | тесты |
17
+
18
+ Отличить это от забытой отладки можно только разбором кода, а не поиском по тексту. Разбор
19
+ уже написан и поддерживается: `ruff` и `eslint`. Наш поиск по тексту не приближался к ним и не
20
+ мог приблизиться.
21
+
22
+ **Готовый аналог — это и есть он сам.** `ruff --select T20` для Python, правило `no-console`
23
+ в eslint для JavaScript и TypeScript. Триггер записи сужен ровно до этих трёх языков: показывать
24
+ её проекту на Go, которому мы не можем дать ни инструмента, ни своей проверки, — значит показывать
25
+ работу, которую человек сделать не сможет.
26
+
27
+ **Чего НЕ ловит.** Печать через обёртку — `myprint(x)`, свой хелпер — не видит ни один из двоих:
28
+ они знают конструкции языка, а не «вывод в поток». Печать из программы командной строки оба
29
+ считают нарушением, хотя там она законна; такие каталоги исключают настройкой самого
30
+ инструмента (`per-file-ignores` в `ruff`, `overrides` в eslint), а не нашим кодом.
31
+
32
+ **Чем это оплачено.** Проект на Go, Rust, Java, Ruby по этому пункту не получает ничего. Так
33
+ честнее: проверка, которая на живом коде ошибается в ста процентах случаев, не «лучше, чем
34
+ ничего» она хуже. Её выключают целиком, а вместе с ней и те, что работают.
35
+
36
+ **Образцы.** `red/service.py` — модуль с отладочной печатью. `green/service.py` — тот же модуль
37
+ через систему логов. `green/legacy.py`закомментированная печать, на которой арбитр обязан
38
+ молчать. Проверяются рецептом `python` (`samples_for`), потому что переносимого рецепта нет.
@@ -1,16 +1,24 @@
1
1
  intent: отладочная печать не доезжает до прод-кода
2
2
  intent_en: debug printing does not reach production code
3
3
 
4
- # Запись касается только языков, где есть эта конструкция. В проекте на Go или
5
- # Rust она не показывается вовсе.
4
+ # Запись касается только языков, где под неё есть готовый инструмент. В проекте
5
+ # на Go или Rust она не показывается вовсе — и переносимого рецепта здесь больше нет.
6
6
  trigger:
7
7
  langs: python, javascript, typescript
8
8
 
9
+ # Переносимого рецепта нет намеренно. Он был, и замер по пяти чужим репозиториям
10
+ # (httpx, fastapi, zod, cobra, ripgrep) дал 434 находки, из которых настоящих — ноль:
11
+ # печать в примерах документации, в тестах, в бенчмарках и в выводе командной строки.
12
+ # Отличить их от отладки можно только разбором кода, а не поиском по тексту, — и это
13
+ # ровно то, что уже делают ruff и eslint.
9
14
  recipes:
10
- any: bash {gate}/check.sh {dir}
11
15
  python: ruff check --select T20 {dir}
12
16
  typescript: eslint --rule '{"no-console":"error"}' {dir}
17
+ javascript: eslint --rule '{"no-console":"error"}' {dir}
13
18
 
14
- proof: incidents/README.md, 2026-09-03 «печать внутри комментария считалась печатью»
15
- прогон по audit_project: 40 находок, из них 5 настоящих (отладочный хук во фронтенде,
16
- печатавший размеры окна в консоль пользователя)
19
+ # Каким рецептом написаны образцы: без переносимого рецепта проверка обязана знать это точно,
20
+ # иначе питоновские образцы поедут проверяться фронтовым инструментом.
21
+ samples_for: python
22
+
23
+ proof: incidents/README.md, 2026-09-07 «замер по пяти стекам вынес приговор пяти записям» —
24
+ 434 находки переносимой проверки на пяти чужих репозиториях, настоящих ноль
@@ -0,0 +1,66 @@
1
+ # Личное не раздаётся всему проекту
2
+
3
+ **Намерение.** `CLAUDE.local.md` и `.claude/settings.local.json` по устройству личные. Попав в
4
+ git, они становятся обязательными для всех — и при этом их никто не рецензирует, потому что
5
+ туда их никто и не звал.
6
+
7
+ **Дело не в опрятности.** Документация Claude Code, дословно:
8
+
9
+ > Within each directory, `CLAUDE.local.md` is appended after `CLAUDE.md`, so your personal notes
10
+ > are the last thing Claude reads at that level.
11
+
12
+ То есть личные заметки одного человека читаются **последними** и перекрывают общие правила
13
+ команды — молча, у каждого. С правами то же самое: разрешения, которые человек выдал себе,
14
+ достаются всем, кто склонировал репозиторий.
15
+
16
+ Оба файла документация прямо велит не коммитить:
17
+
18
+ > For private per-project preferences that shouldn't be checked into version control… Add
19
+ > `CLAUDE.local.md` to your `.gitignore` so it isn't committed.
20
+
21
+ > Claude Code keeps it out of git when it creates the file; if you create it by hand, add it to
22
+ > `.gitignore` yourself.
23
+
24
+ (`code.claude.com/docs/en/memory` и `/settings`, сверено 2026-09-07.)
25
+
26
+ **Какой отказ это поймало.** Замер по тринадцати чужим репозиториям: шесть настоящих находок в
27
+ пяти, ноль ложных на семи контрольных. Самая наглядная — `shacker/django-todo`:
28
+
29
+ ```json
30
+ "allow": [
31
+ "Read(//Users/shacker/**)",
32
+ "Bash(/Users/shacker/dev/home/django-todo/.venv/bin/python -m pytest …)"
33
+ ]
34
+ ```
35
+
36
+ Домашний каталог автора и абсолютные пути с его машины раздаются всем, кто клонирует проект.
37
+ На чужой машине они не работают, а разрешение на чтение чужого домашнего каталога — работает.
38
+
39
+ **Готовый аналог есть, и он не роняет прогон.** У [`AgentLint`](https://github.com/0xmariowu/AgentLint)
40
+ есть проверка C5 «`CLAUDE.local.md` not in git». Прогнали живьём (`npx -p agentlint-ai agentlint
41
+ check`, 2026-09-07) на репозитории, где в git лежат и `CLAUDE.local.md`, и
42
+ `.claude/settings.local.json`: он выдал **61/100** и **код возврата 0**. Флага порога в его
43
+ README нет, `--help` вывода не дал. Это оценка, а не гейт: в конвейере такой прогон зелёный.
44
+ Разница ровно та, ради которой существует наш стандарт. `claudelint` этой проверки не имеет.
45
+
46
+ **Чего НЕ ловит.**
47
+
48
+ - **Файл, лежащий на диске и не отслеживаемый git.** Он и не должен ловиться: это норма.
49
+ - **Репозиторий-заготовку проекта.** Если в корне лежит `cookiecutter.json`, `copier.yml` или
50
+ `.copier-answers.yml`, все файлы в нём — рыба для будущего проекта, а не чьи-то личные.
51
+ Проверка молча пропускает такой репозиторий, назвав причину вслух. Найдено замером:
52
+ в `Frojd/Wagtail-Pipit` обе находки были ровно такими, и обе ложные.
53
+ - **Заготовки по имени и по месту:** `*.example`, `*.template`, каталоги `templates/`,
54
+ `examples/`, `samples/`, `fixtures/`, а также подстановки генератора `{{…}}` в пути.
55
+ - **Личный свод под чужим именем.** Файл, названный `CLAUDE.md`, но содержащий личные заметки,
56
+ проверка не отличит: она смотрит на имя и на git, а не на смысл текста.
57
+ - **Файлы других агентов.** `.cursor/rules` и инструкции copilot своего «личного» уровня не
58
+ имеют — проверять там нечего.
59
+
60
+ **Образцы.** Настоящего git внутри каталога комплекта взять неоткуда, поэтому образцы кладут
61
+ список отслеживаемых файлов в `.aqk-tracked`; в живом проекте такого файла не бывает и список
62
+ спрашивается у git. Имя с точкой намеренно: файл `TRACKED` в корне чужого проекта — вещь
63
+ возможная, и он молча подменял бы собой список git. `red/` — оба личных файла в индексе.
64
+ `green/` — заготовки по имени, по каталогу и с подстановкой генератора, плюс `settings.local.json`
65
+ в `.vscode/` и `node_modules/`, на которых арбитр обязан молчать: личным этот файл считается
66
+ только внутри `.claude/`.
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env sh
2
+ # Личный файл агента, попавший в git: то, что человек писал для себя, становится обязательным
3
+ # для всех и никем не рецензируется.
4
+ #
5
+ # ЗАЧЕМ. Дело не в опрятности. `CLAUDE.local.md` дописывается ПОСЛЕ общего свода — дословно из
6
+ # документации: "Within each directory, `CLAUDE.local.md` is appended after `CLAUDE.md`, so your
7
+ # personal notes are the last thing Claude reads at that level"
8
+ # (code.claude.com/docs/en/memory, сверено 2026-09-07). Значит личные заметки одного человека
9
+ # читаются последними и перекрывают общие правила команды — у всех, молча.
10
+ # `.claude/settings.local.json` — то же самое для прав: разрешения, которые один человек выдал
11
+ # себе, достаются всем, кто склонировал репозиторий.
12
+ #
13
+ # Оба файла по устройству личные, и документация говорит об этом прямо: "For private
14
+ # per-project preferences that shouldn't be checked into version control… Add `CLAUDE.local.md`
15
+ # to your `.gitignore` so it isn't committed" и "Claude Code keeps it out of git when it creates
16
+ # the file; if you create it by hand, add it to `.gitignore` yourself".
17
+ DIR="${1:-.}"
18
+ # Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
19
+ # `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
20
+ # неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
21
+ # `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
22
+ # ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
23
+ # выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
24
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
25
+ if [ ! -f "$SKIP_LIB" ]; then
26
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
27
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
28
+ exit 2
29
+ fi
30
+ . "$SKIP_LIB"
31
+
32
+ # Образцы (gates.sh) читают список отслеживаемых файлов из `.aqk-tracked`: настоящего git внутри
33
+ # каталога комплекта взять неоткуда, а вложенный .git создал бы embedded-репозиторий.
34
+ # В настоящем проекте такого файла не бывает — спрашиваем сам git.
35
+ # Имя с точкой намеренно: файл `TRACKED` в корне проекта — вещь возможная, и он молча
36
+ # подменял бы собой список git. Найдено код-ревью 2026-09-07.
37
+ if [ -f "$DIR/.aqk-tracked" ]; then
38
+ LIST=$(cat "$DIR/.aqk-tracked")
39
+ else
40
+ if ! (cd "$DIR" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1); then
41
+ echo "не git-репозиторий — проверять нечего"
42
+ exit 0
43
+ fi
44
+ # core.quotePath=false ОБЯЗАТЕЛЕН. По умолчанию git заворачивает путь с не-ASCII в кавычки
45
+ # («"\320\264\320\276\320\272/CLAUDE.local.md"»), и якорь «$» в шаблоне ниже не совпадал:
46
+ # личный свод в каталоге с русским именем пропускался молча. Тот же класс, что CRLF ниже.
47
+ # Найдено код-ревью 2026-09-07.
48
+ LIST=$(cd "$DIR" && git -c core.quotePath=false ls-files 2>/dev/null)
49
+ fi
50
+ # Перевод строки Windows. Без снятия «\r» якорь «$» в шаблоне ниже не совпадал, и красный
51
+ # образец с CRLF становился зелёным. Поймано мутационной проверкой до единого прогона в чужом
52
+ # проекте — ровно то, ради чего она написана.
53
+ LIST=$(printf '%s\n' "$LIST" | tr -d '\r')
54
+ [ -z "$LIST" ] && exit 0
55
+
56
+ # Репозиторий, который целиком является заготовкой проекта, проверять нечем: все файлы в нём —
57
+ # не чьи-то личные, а рыба для будущего проекта. Признак — файл настроек генератора в корне.
58
+ # Замер: в `Frojd/Wagtail-Pipit` обе находки были такими, и обе ложные.
59
+ # `.copier-answers.yml` в этот список НЕ входит, хотя выглядит родственником: copier кладёт его
60
+ # в СГЕНЕРИРОВАННЫЙ проект, а не в шаблон. Шаблон несёт `copier.yml`. Найдено код-ревью
61
+ # 2026-09-07: обычный рабочий репозиторий, собранный из copier-шаблона, целиком выпадал из
62
+ # проверки — молчаливый отказ по причине, не имеющей отношения к предмету.
63
+ for GEN in cookiecutter.json copier.yml copier.yaml; do
64
+ if [ -f "$DIR/$GEN" ]; then
65
+ echo "репозиторий — заготовка проекта ($GEN), личных файлов в нём нет по устройству"
66
+ exit 0
67
+ fi
68
+ done
69
+
70
+ # Заготовки — не личные файлы. Замер по выдаче поиска: из двадцати совпадений по имени
71
+ # `CLAUDE.local.md` больше половины оказались `*.example`, `*.template` и файлами внутри
72
+ # `templates/` — их кладут в репозиторий намеренно, чтобы человек скопировал себе.
73
+ # Подстановки генератора (`{{cookiecutter.project_name}}/…`) — оттуда же.
74
+ # `settings.local.json` считается личным ТОЛЬКО внутри `.claude/`: это единственное место,
75
+ # откуда Claude Code его читает. Без привязки к каталогу под проверку попадали
76
+ # `.vscode/settings.local.json` и `node_modules/…/settings.local.json` — чужие файлы с
77
+ # совпавшим именем. Найдено код-ревью 2026-09-07.
78
+ HITS=$(printf '%s\n' "$LIST" \
79
+ | grep -E '(^|/)(CLAUDE\.local\.md|\.claude/settings\.local\.json)$' \
80
+ | grep -vE '(^|/)(templates?|examples?|samples?|dist|fixtures?)/' \
81
+ | grep -vF '{{' \
82
+ || true)
83
+
84
+ # Образцы гейтов — не личные файлы. В красном образце другой записи может лежать настоящий
85
+ # `settings.local.json`, и это её предмет, а не наш. Поймано на собственном репозитории:
86
+ # первой находкой стал зелёный образец `hook-actually-fires`.
87
+ if command -v own_samples_filter >/dev/null 2>&1 || type own_samples_filter >/dev/null 2>&1; then
88
+ HITS=$(printf '%s\n' "$HITS" | own_samples_filter "$DIR" | grep -v '^$' || true)
89
+ fi
90
+ # Каталог самого комплекта: у нас образцы лежат не в `gates/`, а в `kit/gates/`.
91
+ HITS=$(printf '%s\n' "$HITS" | grep -vE '(^|/)kit/gates/[^/]+/(red|green)(/|$)' | grep -v '^$' || true)
92
+
93
+ [ -z "$HITS" ] && exit 0
94
+
95
+ printf '%s\n' "$HITS" | while IFS= read -r F; do
96
+ case "$F" in
97
+ *CLAUDE.local.md) echo "$F: личный свод отслеживается git — он дописывается ПОСЛЕ общего и перекрывает его у всех" ;;
98
+ *) echo "$F: личные права отслеживаются git — разрешения одного человека достались всем" ;;
99
+ esac
100
+ done
101
+ echo " почини: убери файл из индекса — git rm --cached <файл> — и добавь его в .gitignore."
102
+ echo " личное, попавшее в общий репозиторий, никто не рецензирует: его туда никто и не звал."
103
+ exit 1
@@ -0,0 +1,16 @@
1
+ intent: личные файлы агента не раздаются всему проекту через git
2
+ intent_en: an agent's personal files are not shared with the whole project through git
3
+
4
+ # Там, где агента используют: есть свод, который он читает при запуске. Признак шире, чем
5
+ # `has_agent_config`: настройки заводят не все, а свод — почти каждый.
6
+ trigger:
7
+ has_agent_entry: true
8
+
9
+ recipes:
10
+ any: bash {gate}/check.sh {dir}
11
+
12
+ proof: incidents/README.md, 2026-09-07 «личное, попавшее в общий репозиторий» — замер по
13
+ тринадцати чужим репозиториям: шесть настоящих находок в пяти (`shacker/django-todo` раздаёт
14
+ всем `Read(//Users/shacker/**)` и абсолютные пути со своей машины; `modu-ai/moai-adk`,
15
+ `garrynewman/Control4.Jailbreak`, `AmyangXYZ/reze-mipo`, `shuigedeng/taotao-cloud-project`),
16
+ ноль ложных на семи контрольных
@@ -0,0 +1,9 @@
1
+ README.md
2
+ CLAUDE.md
3
+ CLAUDE.local.md.example
4
+ templates/CLAUDE.local.md
5
+ docs/settings.local.json.template
6
+ {{cookiecutter.project_name}}/CLAUDE.local.md
7
+ .vscode/settings.local.json
8
+ node_modules/foo/settings.local.json
9
+ src/app.py
@@ -0,0 +1,6 @@
1
+ README.md
2
+ CLAUDE.md
3
+ CLAUDE.local.md
4
+ .claude/settings.json
5
+ .claude/settings.local.json
6
+ src/app.py
@@ -0,0 +1,50 @@
1
+ # У правила виден сторож
2
+
3
+ **Намерение.** Это центральный вопрос всего комплекта. `AGENTS.md` — обычный текст: «никогда не
4
+ коммитим секреты» и «мы очень стараемся» выглядят одинаково и стоят одинаково, пока никто не
5
+ спросил, чем первое отличается от второго.
6
+
7
+ **Какой отказ это поймало.** Свой собственный. Прогон по нашему же `AGENTS.md`: тринадцать
8
+ железных правил, и **ни одно** не говорило, кто за ним следит. После разметки выяснилось, что
9
+ одиннадцать из тринадцати исполняет человек, а не машина. Это не стало хуже — стало видно.
10
+ Правило, за которым следит человек, законно; правило, о котором никто не знает, кто за ним
11
+ следит, через месяц отличается от лозунга только длиной. Запись в журнале:
12
+ `incidents/README.md`, 2026-09-06.
13
+
14
+ **Что именно проверяется.** Каждый пункт списка в точке входа, который выглядит правилом
15
+ (выделен жирным либо содержит слово долженствования или запрета), обязан нести пометку:
16
+
17
+ ```markdown
18
+ - **Секреты не в коде.** <!-- aqk: secrets-not-in-code -->
19
+ - **План до кода.** <!-- aqk: человек -->
20
+ ```
21
+
22
+ Имя гейта сверяется с блоком `gates:` манифеста: пометка, ведущая в никуда, — тоже красное.
23
+ Пометка живёт в комментарии разметки, поэтому в готовом документе её не видно.
24
+
25
+ **Почему пометка, а не угадывание.** Сопоставлять обещание с гейтом по совпадению слов — значит
26
+ выдавать вердикт по догадке; догадка, выданная за факт, и есть то, против чего построен весь
27
+ стандарт. Пометка ставится один раз и делает документ честнее: у каждого правила видно, кто его
28
+ сторожит.
29
+
30
+ **Первый прогон в зрелом проекте покрасит всё.** Так и задумано, и лечится одной командой:
31
+ `aqk ratchet promise-has-gate` — существующие правила становятся долгом, который может только
32
+ сокращаться, а новое правило без сторожа краснеет сразу.
33
+
34
+ **Готовый аналог.** Не нашли, и искали внимательно. К осени 2026 появился целый класс линтеров
35
+ агентского обвеса — [`agnix`](https://github.com/agent-sh/agnix) (455 правил, активен),
36
+ [`agents-lint`](https://github.com/giacomo/agents-lint), ctxlint, AgentLint. Все они проверяют
37
+ **документ**: формат, живые ли ссылки, существуют ли упомянутые скрипты. Ни один не спрашивает,
38
+ **исполнимо** ли обещание. Ставь `agnix` рядом — он закрывает то, чего не делаем мы, а мы
39
+ закрываем то, чего не делает он.
40
+
41
+ **Чего НЕ ловит.**
42
+
43
+ - **Не проверяет, что гейт делает то, что обещано.** Пометка `<!-- aqk: secrets-not-in-code -->`
44
+ на правиле про длину функций пройдёт. Связь объявляет человек; машина сторожит только то, что
45
+ связь есть и ведёт в существующий гейт.
46
+ - **`<!-- aqk: человек -->` не проверяется ничем** — это признание, а не проверка. Его ценность
47
+ в том, что признание сделано вслух и его видно в дифе, когда правил становится больше.
48
+ - **Правило, не оформленное пунктом списка**, не опознаётся: абзац прозы обещанием не считается.
49
+ - **Только точка входа**, объявленная в `entry:`. Правила, разложенные по десяти файлам
50
+ документации, эта запись не обойдёт.