agent-quality-kit 0.2.2

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 (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +155 -0
  3. package/kit/docs/ai/agent-harness-playbook.md +596 -0
  4. package/kit/docs/ai/ai-native-development.md +371 -0
  5. package/kit/docs/ai/ai-sdlc.md +221 -0
  6. package/kit/docs/ai/anthropic-ai-native-sdlc-2026-08.md +294 -0
  7. package/kit/docs/ai/app-owner-strategy.md +921 -0
  8. package/kit/docs/ai/deep-research-2026-07.md +161 -0
  9. package/kit/docs/ai/harness-best-practices.md +385 -0
  10. package/kit/docs/ai/index.md +64 -0
  11. package/kit/docs/ai/project-baseline.md +261 -0
  12. package/kit/docs/ai/quality-gates-checklist.md +322 -0
  13. package/kit/docs/ai/sources-building-with-agents.md +111 -0
  14. package/kit/docs/ai/stream-2026-08-ai-coding-panel.md +304 -0
  15. package/kit/docs/ready-made-rules.md +170 -0
  16. package/kit/gates/README.md +231 -0
  17. package/kit/gates/_skip.sh +75 -0
  18. package/kit/gates/commit-explains-itself/README.md +45 -0
  19. package/kit/gates/commit-explains-itself/check.sh +63 -0
  20. package/kit/gates/commit-explains-itself/gate.yml +10 -0
  21. package/kit/gates/commit-explains-itself/green/COMMIT_MSG +6 -0
  22. package/kit/gates/commit-explains-itself/red/COMMIT_MSG +3 -0
  23. package/kit/gates/complexity-limit/README.md +37 -0
  24. package/kit/gates/complexity-limit/check.sh +44 -0
  25. package/kit/gates/complexity-limit/gate.yml +13 -0
  26. package/kit/gates/complexity-limit/green/flat.py +10 -0
  27. package/kit/gates/complexity-limit/red/deep.py +9 -0
  28. package/kit/gates/dead-code/README.md +30 -0
  29. package/kit/gates/dead-code/gate.yml +23 -0
  30. package/kit/gates/dead-code/green/mod.py +9 -0
  31. package/kit/gates/dead-code/red/mod.py +9 -0
  32. package/kit/gates/deps-are-pinned/README.md +29 -0
  33. package/kit/gates/deps-are-pinned/check.sh +49 -0
  34. package/kit/gates/deps-are-pinned/gate.yml +9 -0
  35. package/kit/gates/deps-are-pinned/green/nodep-go/go.mod +3 -0
  36. package/kit/gates/deps-are-pinned/green/package-lock.json +3 -0
  37. package/kit/gates/deps-are-pinned/green/package.json +4 -0
  38. package/kit/gates/deps-are-pinned/green/requirements.txt +2 -0
  39. package/kit/gates/deps-are-pinned/red/package.json +4 -0
  40. package/kit/gates/deps-are-pinned/red/requirements.txt +2 -0
  41. package/kit/gates/deps-are-pinned/red/withdep-go/go.mod +5 -0
  42. package/kit/gates/duplicate-code/README.md +40 -0
  43. package/kit/gates/duplicate-code/check.sh +58 -0
  44. package/kit/gates/duplicate-code/gate.yml +12 -0
  45. package/kit/gates/duplicate-code/green/common.py +9 -0
  46. package/kit/gates/duplicate-code/green/use.py +9 -0
  47. package/kit/gates/duplicate-code/red/a.py +12 -0
  48. package/kit/gates/duplicate-code/red/b.py +12 -0
  49. package/kit/gates/entry-links-exist/README.md +22 -0
  50. package/kit/gates/entry-links-exist/check.sh +24 -0
  51. package/kit/gates/entry-links-exist/gate.yml +16 -0
  52. package/kit/gates/entry-links-exist/green/AGENTS.md +5 -0
  53. package/kit/gates/entry-links-exist/green/rules/general.md +3 -0
  54. package/kit/gates/entry-links-exist/red/AGENTS.md +3 -0
  55. package/kit/gates/file-size-limit/README.md +22 -0
  56. package/kit/gates/file-size-limit/check.sh +34 -0
  57. package/kit/gates/file-size-limit/gate.yml +9 -0
  58. package/kit/gates/file-size-limit/green/a.py +251 -0
  59. package/kit/gates/file-size-limit/green/b.py +251 -0
  60. package/kit/gates/file-size-limit/red/big.py +601 -0
  61. package/kit/gates/gate-has-samples/README.md +29 -0
  62. package/kit/gates/gate-has-samples/check.sh +48 -0
  63. package/kit/gates/gate-has-samples/gate.yml +9 -0
  64. package/kit/gates/gate-has-samples/green/.aqk.yml +10 -0
  65. package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/check.sh +2 -0
  66. package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/green/good.py +2 -0
  67. package/kit/gates/gate-has-samples/green/gates/no-print-in-prod/red/bad.py +1 -0
  68. package/kit/gates/gate-has-samples/red/.aqk.yml +10 -0
  69. package/kit/gates/gate-has-samples/red/gates/no-print-in-prod/check.sh +2 -0
  70. package/kit/gates/gates-are-runnable/README.md +23 -0
  71. package/kit/gates/gates-are-runnable/check.sh +35 -0
  72. package/kit/gates/gates-are-runnable/gate.yml +9 -0
  73. package/kit/gates/gates-are-runnable/green/.aqk.yml +9 -0
  74. package/kit/gates/gates-are-runnable/green/checks/lint.sh +2 -0
  75. package/kit/gates/gates-are-runnable/red/.aqk.yml +6 -0
  76. package/kit/gates/gates-run-in-ci/README.md +29 -0
  77. package/kit/gates/gates-run-in-ci/check.sh +42 -0
  78. package/kit/gates/gates-run-in-ci/gate.yml +12 -0
  79. package/kit/gates/gates-run-in-ci/green/.aqk.yml +6 -0
  80. package/kit/gates/gates-run-in-ci/green/.github/workflows/ci.yml +7 -0
  81. package/kit/gates/gates-run-in-ci/green/checks/lint.sh +2 -0
  82. package/kit/gates/gates-run-in-ci/red/.aqk.yml +6 -0
  83. package/kit/gates/gates-run-in-ci/red/.github/workflows/ci.yml +7 -0
  84. package/kit/gates/gates-run-in-ci/red/checks/lint.sh +2 -0
  85. package/kit/gates/lesson-has-outcome/README.md +37 -0
  86. package/kit/gates/lesson-has-outcome/check.sh +50 -0
  87. package/kit/gates/lesson-has-outcome/gate.yml +11 -0
  88. package/kit/gates/lesson-has-outcome/green/.aqk.yml +2 -0
  89. package/kit/gates/lesson-has-outcome/green/incidents/README.md +32 -0
  90. package/kit/gates/lesson-has-outcome/red/.aqk.yml +2 -0
  91. package/kit/gates/lesson-has-outcome/red/incidents/README.md +13 -0
  92. package/kit/gates/no-print-in-prod/README.md +44 -0
  93. package/kit/gates/no-print-in-prod/check.sh +36 -0
  94. package/kit/gates/no-print-in-prod/gate.yml +15 -0
  95. package/kit/gates/no-print-in-prod/green/docs.ts +15 -0
  96. package/kit/gates/no-print-in-prod/green/legacy.py +9 -0
  97. package/kit/gates/no-print-in-prod/green/main.go +8 -0
  98. package/kit/gates/no-print-in-prod/green/main.rs +4 -0
  99. package/kit/gates/no-print-in-prod/green/service.py +8 -0
  100. package/kit/gates/no-print-in-prod/red/main.go +8 -0
  101. package/kit/gates/no-print-in-prod/red/main.rs +4 -0
  102. package/kit/gates/no-print-in-prod/red/service.py +3 -0
  103. package/kit/gates/secrets-not-in-code/README.md +29 -0
  104. package/kit/gates/secrets-not-in-code/check.sh +18 -0
  105. package/kit/gates/secrets-not-in-code/gate.yml +9 -0
  106. package/kit/gates/secrets-not-in-code/green/settings.py +4 -0
  107. package/kit/gates/secrets-not-in-code/red/settings.py +2 -0
  108. package/kit/gates/swallowed-error/README.md +26 -0
  109. package/kit/gates/swallowed-error/check.sh +54 -0
  110. package/kit/gates/swallowed-error/gate.yml +12 -0
  111. package/kit/gates/swallowed-error/green/loader.py +11 -0
  112. package/kit/gates/swallowed-error/green/run.js +8 -0
  113. package/kit/gates/swallowed-error/red/loader.py +5 -0
  114. package/kit/gates/swallowed-error/red/run.js +3 -0
  115. package/kit/gates/todo-without-task/README.md +26 -0
  116. package/kit/gates/todo-without-task/check.sh +19 -0
  117. package/kit/gates/todo-without-task/gate.yml +12 -0
  118. package/kit/gates/todo-without-task/green/order.py +9 -0
  119. package/kit/gates/todo-without-task/red/order.py +8 -0
  120. package/kit/ratchet/ratchet.sh +62 -0
  121. package/kit/rules/general.md +55 -0
  122. package/kit/rules/security.md +33 -0
  123. package/kit/rules/testing.md +46 -0
  124. package/package.json +41 -0
  125. package/tool/commands/doctor.mjs +230 -0
  126. package/tool/commands/gates.mjs +445 -0
  127. package/tool/commands/project.mjs +316 -0
  128. package/tool/lib/core.mjs +98 -0
  129. package/tool/lib/manifest.mjs +140 -0
  130. package/tool/lib/repo.mjs +270 -0
  131. package/tool/lib/templates.mjs +187 -0
  132. package/tool/program.mjs +81 -0
  133. package/tool/selfcheck/conditional.sh +24 -0
  134. package/tool/selfcheck/gates.sh +127 -0
  135. package/tool/selfcheck/smoke.sh +539 -0
  136. package/tool/selfcheck/syntax.sh +23 -0
  137. package/tool/selfcheck/units.mjs +105 -0
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env bash
2
+ # tool/selfcheck/smoke.sh — проверка комплекта на чистой папке, как у постороннего человека.
3
+ #
4
+ # ЗАЧЕМ. До этого правильность проверялась глазами и памятью: «вроде работало». Инструмент,
5
+ # который ставят одной командой в чужие проекты, так проверять нельзя — сломанный `init`
6
+ # обнаружится у пользователя, а не у автора.
7
+ #
8
+ # ЧЕГО ЭТА ПРОВЕРКА НЕ ДЕЛАЕТ. Не проверяет содержание разложенных документов и не ходит в сеть
9
+ # (установку с GitHub проверяет человек перед выпуском). Инструмент, молчащий о своих пробелах,
10
+ # хуже отсутствующего.
11
+ #
12
+ # bash tool/selfcheck/smoke.sh
13
+
14
+ set -uo pipefail
15
+
16
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
17
+ CLI="$ROOT/tool/program.mjs"
18
+ PASS=0
19
+ FAIL=0
20
+
21
+ ok() { printf ' \033[32m✔\033[0m %s\n' "$1"; PASS=$((PASS + 1)); }
22
+ bad() { printf ' \033[31m✘\033[0m %s\n' "$1"; printf ' %s\n' "${2:-}"; FAIL=$((FAIL + 1)); }
23
+
24
+ WORK="$(mktemp -d)"
25
+ trap 'rm -rf "$WORK"' EXIT
26
+
27
+ printf '\n\033[1mtool/selfcheck/smoke.sh\033[0m\n\n'
28
+
29
+ # --- 1. синтаксис самой программы ------------------------------------------
30
+ if node --check "$CLI" 2>/dev/null; then ok "программа разбирается"; else bad "программа не разбирается"; fi
31
+
32
+ cd "$WORK" || exit 1
33
+ git init -q .
34
+
35
+ # --- 2. init раскладывает комплект -----------------------------------------
36
+ node "$CLI" init >/dev/null 2>&1
37
+ # Считаем не «сколько-то файлов», а ровно то, что лежит в комплекте: жёсткое число
38
+ # устаревает при первой же правке состава и роняет проверку на пустом месте.
39
+ EXPECT=$(find "$ROOT/kit/docs" "$ROOT/kit/rules" -type f | wc -l)
40
+ GOT=$(find .aqk -type f 2>/dev/null | wc -l)
41
+ if [ "$GOT" -eq "$EXPECT" ]; then
42
+ ok "init разложил весь комплект: $GOT файлов"
43
+ else
44
+ bad "разложено $GOT из $EXPECT файлов комплекта"
45
+ fi
46
+
47
+ # Ищем по имени на любой глубине: раскладка внутри .aqk/docs — дело владельца комплекта,
48
+ # а обязательность файла от неё не зависит.
49
+ for f in .aqk.yml AGENTS.md CLAUDE.md; do
50
+ [ -f "$f" ] || bad "не создан обязательный файл" "$f"
51
+ done
52
+ for n in general.md index.md project-baseline.md; do
53
+ find .aqk -type f -name "$n" | grep -q . || bad "не создан обязательный файл" "$n"
54
+ done
55
+ ok "обязательные файлы на месте"
56
+
57
+ # --- 3. ЗЕЛЁНЫЙ ОБРАЗЕЦ: повтор ничего не портит ---------------------------
58
+ # Именно этого свойства ждут от команды, которую запускают вслепую по инструкции.
59
+ echo "правка человека" >> AGENTS.md
60
+ node "$CLI" init >/dev/null 2>&1
61
+ if grep -q "правка человека" AGENTS.md; then ok "повторный init не перезаписал чужие правки"; else bad "повторный init затёр правки человека"; fi
62
+
63
+ # --- 4. init --force перезаписывает, раз его просили явно -------------------
64
+ node "$CLI" init --force >/dev/null 2>&1
65
+ if grep -q "правка человека" AGENTS.md; then bad "init --force не перезаписал, хотя обязан"; else ok "init --force перезаписал, как просили"; fi
66
+
67
+ # --- 5. doctor считает ступени ---------------------------------------------
68
+ OUT="$(node "$CLI" doctor 2>&1)"
69
+ case "$OUT" in
70
+ *"AQK-0"*) ok "doctor печатает ступени" ;;
71
+ *) bad "doctor не напечатал ступени" "$OUT" ;;
72
+ esac
73
+
74
+ node "$CLI" doctor --min 0 >/dev/null 2>&1 && ok "порог AQK-0 пройден" || bad "порог AQK-0 не пройден на свежем проекте"
75
+ node "$CLI" doctor --min 3 >/dev/null 2>&1 && bad "порог AQK-3 пройден на пустом проекте — проверка врёт" || ok "порог AQK-3 честно не пройден"
76
+
77
+ # --- 6. КРАСНЫЙ ОБРАЗЕЦ: журнал не принимает запись без вывода --------------
78
+ if echo "просто сломалось" | node "$CLI" note "проверка" >/dev/null 2>&1; then
79
+ bad "журнал принял запись без раздела «Вывод»"
80
+ else
81
+ ok "журнал отклонил запись без вывода"
82
+ fi
83
+
84
+ # --- 7. сверка по намерению: находит своё и не выдумывает чужого -------------
85
+ # ЗЕЛЁНЫЙ: описание чужими словами обязано найти существующую запись.
86
+ OUT="$(node "$CLI" find "отладочная печать в проде" 2>&1)"
87
+ case "$OUT" in
88
+ *"no-print-in-prod"*) ok "find нашёл запись по описанию другими словами" ;;
89
+ *) bad "find не нашёл no-print-in-prod" "$OUT" ;;
90
+ esac
91
+
92
+ # КРАСНЫЙ: намерения, которого нет, находить нельзя. Ложное «такое уже есть» хуже
93
+ # отсутствия поиска: человек не заведёт нужный гейт, решив, что он есть.
94
+ OUT="$(node "$CLI" find "проверять что миграции базы обратимы" 2>&1)"
95
+ case "$OUT" in
96
+ *"Такого намерения в каталоге нет"*) ok "find не выдумал совпадение" ;;
97
+ *) bad "find нашёл несуществующее намерение" "$OUT" ;;
98
+ esac
99
+
100
+ # --- 5а. подсказки печатают вызов, который сработает ------------------------
101
+ # Через npx команды `aqk` в системе нет. Подсказка «aqk doctor» отправляет человека
102
+ # в «команда не найдена» на первом же шаге — проверено на живой установке с GitHub.
103
+ OUT="$(node "$CLI" 2>&1)"
104
+ case "$OUT" in
105
+ *"node "*"doctor"*) ok "справка печатает тот вызов, которым программу запустили" ;;
106
+ *) bad "справка советует команду, которой у человека нет" "$OUT" ;;
107
+ esac
108
+
109
+ # --- 5б. без манифеста уровень не выносит приговор проекту ------------------
110
+ # «Уровень не достигнут» на зрелом проекте читается как «проект плохой». Сообщение обязано
111
+ # объяснять, что мерится машиночитаемость, а не практика.
112
+ N="$WORK/nomanifest"; mkdir -p "$N"; cd "$N" || exit 1
113
+ git init -q .
114
+ OUT="$(node "$CLI" doctor 2>&1)"
115
+ case "$OUT" in
116
+ *"стандарт в этом репозитории не заведён"*"не оценка проекта"*)
117
+ ok "без манифеста сказано, что это не оценка проекта" ;;
118
+ *) bad "без манифеста доктор выносит приговор проекту" "$OUT" ;;
119
+ esac
120
+ cd "$WORK" || exit 1
121
+
122
+ # --- 7б. составной триггер: скрыто и названа причина ------------------------
123
+ # Запись про конвейер касается тех, у кого есть И гейты, И конвейер. В свежем проекте
124
+ # конвейера нет — она обязана быть скрыта, и обязана сказать почему.
125
+ # Гейт объявляем — значит первое условие выполнено; конвейера нет — второе нет.
126
+ # Так проверяется именно СЛОЖЕНИЕ условий, а не первое попавшееся.
127
+ C="$WORK/trigger"; mkdir -p "$C"; cd "$C" || exit 1
128
+ git init -q .
129
+ node "$CLI" init >/dev/null 2>&1
130
+ node "$CLI" add secrets-not-in-code >/dev/null 2>&1
131
+ OUT="$(node "$CLI" doctor 2>&1)"
132
+ case "$OUT" in
133
+ *"gates-run-in-ci"*"нет конвейера"*) ok "составной триггер скрыл запись и назвал причину" ;;
134
+ *) bad "запись про конвейер показана там, где конвейера нет" "$OUT" ;;
135
+ esac
136
+ cd "$WORK" || exit 1
137
+
138
+ # --- 7в. запись без переносимого рецепта не выдаётся за проверенную --------
139
+ # «Нечем проверить здесь» обязано отличаться и от «принято», и от «отклонено»: иначе
140
+ # непроверенная запись едет в каталог как рабочая.
141
+ OUT="$(bash "$ROOT/tool/selfcheck/gates.sh" 2>&1)"
142
+ case "$OUT" in
143
+ *"НЕ ПРОВЕРЕНА здесь"*|*"принято"*) ok "непроверенные записи считаются отдельно" ;;
144
+ *) bad "проверка каталога не различает непроверенные записи" "$OUT" ;;
145
+ esac
146
+
147
+ # --- 7г. проверка не зависает на проекте не на своём языке -------------------
148
+ # awk без файловых аргументов читает поток ввода и ждёт вечно. В терминале незаметно, в
149
+ # конвейере и в хуке коммита — намертво. Поток ввода здесь держим открытым нарочно.
150
+ E="$WORK/чужой-язык"; mkdir -p "$E"; printf 'fn main() {}\n' > "$E/main.rs"
151
+ # Поток ввода из /dev/zero: он никогда не кончается, но и ждать его не надо — здоровый гейт
152
+ # в него не заглядывает и выходит сразу. Спящий процесс в конвейере заставлял ждать себя.
153
+ STUCK=0
154
+ for G in "$ROOT"/kit/gates/*/check.sh; do
155
+ timeout 5 bash "$G" "$E" >/dev/null 2>&1 < /dev/zero
156
+ [ $? -eq 124 ] && { bad "гейт зависает на чужом языке" "$(basename "$(dirname "$G")")"; STUCK=1; }
157
+ done
158
+ [ $STUCK -eq 0 ] && ok "ни один гейт не зависает там, где его языка нет"
159
+
160
+ # --- 8. храповик: старое пропускает, новое не пускает ------------------------
161
+ # Главный вопрос к храповику: «может ли новый код добавить нарушение и пройти?»
162
+ # Может — значит это советчик, а не гейт.
163
+ R="$WORK/ratchet"; mkdir -p "$R"; cd "$R" || exit 1
164
+ git init -q .
165
+ printf 'def a():\n print("старое")\n' > old.py
166
+ node "$CLI" init >/dev/null 2>&1
167
+ node "$CLI" add no-print-in-prod >/dev/null 2>&1
168
+ node "$CLI" ratchet no-print-in-prod >/dev/null 2>&1
169
+
170
+ # Судим по вердикту гейта, а не по коду возврата doctor: он ненулевой и по другим
171
+ # причинам (в свежей папке нет .gitignore), и проверка бы врала о храповике.
172
+ OUT="$(node "$CLI" doctor --run 2>&1)"
173
+ case "$OUT" in
174
+ *"новых нарушений"*) bad "храповик не пропустил старое — правило нельзя ввести в живой проект" ;;
175
+ *) ok "храповик пропустил старое нарушение" ;;
176
+ esac
177
+
178
+ printf 'def b():\n print("новое")\n' > new.py
179
+ OUT="$(node "$CLI" doctor --run 2>&1)"
180
+ case "$OUT" in
181
+ *"новых нарушений"*) ok "храповик не пустил новое нарушение" ;;
182
+ *) bad "храповик пропустил НОВОЕ нарушение — это советчик, а не гейт" ;;
183
+ esac
184
+
185
+ # Второй прогон с тем же новым нарушением обязан краснеть так же. Пока реестр перезаписывался
186
+ # всем текущим списком, одно исправленное нарушение затягивало в долг ВСЕ новые: один красный
187
+ # прогон — и дальше зелено навсегда. «Может ли новый код добавить нарушение и пройти?» — мог.
188
+ printf 'def c():\n print("ещё одно")\n' > another.py
189
+ node "$CLI" doctor --run >/dev/null 2>&1
190
+ rm old.py
191
+ OUT="$(node "$CLI" doctor --run 2>&1)"
192
+ OUT2="$(node "$CLI" doctor --run 2>&1)"
193
+ case "$OUT2" in
194
+ *"новых нарушений"*) ok "новое нарушение не попадает в реестр вслед за исправленным" ;;
195
+ *) bad "исправление одного нарушения затянуло новые в долг" "второй прогон зелёный" ;;
196
+ esac
197
+ rm -f another.py
198
+ printf 'def a():\n print("старое")\n' > old.py
199
+ node "$CLI" ratchet no-print-in-prod >/dev/null 2>&1 || true
200
+
201
+ rm -f new.py old.py
202
+ node "$CLI" doctor --run >/dev/null 2>&1
203
+ if grep -q 'old.py' ratchets/no-print-in-prod.txt; then
204
+ bad "исправленное осталось в реестре — храповик не затягивается"
205
+ else
206
+ ok "исправленное вычеркнуто из реестра"
207
+ fi
208
+ cd "$WORK" || exit 1
209
+
210
+ # --- 8а. порядок «с нуля»: сторожа дня 0 ставятся до первой строки кода -------
211
+ # Сторож, поставленный на пустой проект, долга не создаёт. Он же, поставленный через полгода,
212
+ # краснеет на всём старом коде — и его выключают. Поэтому проверяем ровно это: после start
213
+ # гейты стоят И все зелёные.
214
+ S0="$WORK/start"; mkdir -p "$S0"; cd "$S0" || exit 1
215
+ git init -q .
216
+ node "$CLI" start >/dev/null 2>&1
217
+ N=$(awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:[[:space:]]*"/' .aqk.yml | wc -l)
218
+ if [ "$N" -ge 5 ]; then ok "start поставил сторожей дня 0 ($N)"; else bad "start почти ничего не поставил" "объявлено $N"; fi
219
+
220
+ OUT="$(node "$CLI" doctor --run 2>&1)"
221
+ case "$OUT" in
222
+ *"✘"*"код "*) bad "сторожа дня 0 краснеют на пустом проекте" "$(printf '%s' "$OUT" | grep -A2 '✘' | head -6)" ;;
223
+ *) ok "все сторожа дня 0 зелёные — долга нет" ;;
224
+ esac
225
+
226
+ # Образцы, скопированные в проект, — не код проекта. Пока они им считались, пустой репозиторий
227
+ # «становился» проектом на Python, и ему показывались записи про мёртвый код.
228
+ # Признак — не отсутствие слова, а причина рядом с ним: запись обязана быть СКРЫТА с
229
+ # пояснением «нет языков». Первая редакция искала само слово и падала на строке про скрытое.
230
+ case "$OUT" in
231
+ *"dead-code"*"нет языков"*) ok "образцы гейтов не считаются кодом проекта" ;;
232
+ *) bad "образцы гейтов посчитаны кодом проекта" "$(printf '%s' "$OUT" | grep 'dead-code')" ;;
233
+ esac
234
+ cd "$WORK" || exit 1
235
+
236
+ # На проекте, где код уже есть, это другой сценарий — и start обязан сказать об этом, а не
237
+ # красить весь старый код разом.
238
+ S1="$WORK/start-big"; mkdir -p "$S1"; cd "$S1" || exit 1
239
+ git init -q .
240
+ i=0; while [ $i -lt 40 ]; do printf 'x = %s\n' "$i" > "m$i.py"; i=$((i + 1)); done
241
+ node "$CLI" init >/dev/null 2>&1
242
+ OUT="$(node "$CLI" start 2>&1)"
243
+ case "$OUT" in
244
+ *"другой сценарий"*) ok "start на живом коде отправляет в doctor, а не красит всё" ;;
245
+ *) bad "start не отличил пустой проект от живого" "$OUT" ;;
246
+ esac
247
+ cd "$WORK" || exit 1
248
+
249
+ # --- 8б. проект называет свои каталоги, где печать — интерфейс ---------------
250
+ # Исключение объявляется В МАНИФЕСТЕ и потому видно глазами. Проверяем обе стороны: названный
251
+ # каталог пропускается, все остальные — нет. Исключение, которое прячет всё, бесполезно.
252
+ P="$WORK/printok"; mkdir -p "$P/cli" "$P/src"; cd "$P" || exit 1
253
+ printf 'def a():\n print("вывод программы")\n' > cli/main.py
254
+ printf 'def b():\n print("забытая отладка")\n' > src/service.py
255
+ OUT="$(AQK_PRINT_OK_DIRS=cli bash "$ROOT/kit/gates/no-print-in-prod/check.sh" . 2>&1)"
256
+ case "$OUT" in
257
+ *"cli/main.py"*) bad "названный каталог не исключён" "$OUT" ;;
258
+ *"src/service.py"*) ok "названный каталог пропущен, остальные проверяются" ;;
259
+ *) bad "проверка печати не нашла отладку вне названного каталога" "$OUT" ;;
260
+ esac
261
+ cd "$WORK" || exit 1
262
+
263
+ # --- 9. разбор ошибки: три случая различаются прогоном, а не памятью ---------
264
+ # «Сторожа не было», «сторож был и не сработал», «сторож был и его обошли» — разные починки.
265
+ # Пока их различает человек по памяти, чинят обычно не то. Здесь их различает прогон.
266
+ Y="$WORK/why"; mkdir -p "$Y"; cd "$Y" || exit 1
267
+ git init -q .
268
+ node "$CLI" init >/dev/null 2>&1
269
+ printf 'def a():\n print("отладка")\n' > x.py
270
+
271
+ OUT="$(node "$CLI" why "миграция базы применена задом наперёд" 2>&1)"
272
+ case "$OUT" in
273
+ *"Сторожа не было"*) ok "why: намерения нет в каталоге — сторожа не было" ;;
274
+ *) bad "why не сказал, что сторожа не было" "$OUT" ;;
275
+ esac
276
+
277
+ OUT="$(node "$CLI" why "отладочная печать уехала в прод" 2>&1)"
278
+ case "$OUT" in
279
+ *"не поставлен"*) ok "why: запись есть в каталоге, но в проекте не поставлена" ;;
280
+ *) bad "why не отличил «не поставлен» от «не было»" "$OUT" ;;
281
+ esac
282
+
283
+ node "$CLI" add no-print-in-prod >/dev/null 2>&1
284
+ OUT="$(node "$CLI" why "отладочная печать уехала в прод" 2>&1)"
285
+ case "$OUT" in
286
+ *"его обошли"*) ok "why: сторож стоит и ловит — значит его обошли" ;;
287
+ *) bad "why не отличил «обошли» от «не сработал»" "$OUT" ;;
288
+ esac
289
+
290
+ rm x.py
291
+ OUT="$(node "$CLI" why "отладочная печать уехала в прод" 2>&1)"
292
+ case "$OUT" in
293
+ *"этой поломки не видит"*) ok "why: сторож стоит, а поломки не видит" ;;
294
+ *) bad "why не отличил «не сработал» от «обошли»" "$OUT" ;;
295
+ esac
296
+
297
+ # Неуверенное совпадение не выбирается за человека: неверно названный случай отправляет
298
+ # чинить не то, а это дороже лишнего вопроса.
299
+ OUT="$(node "$CLI" why "файл вырос до девяти тысяч строк" 2>&1)"
300
+ case "$OUT" in
301
+ *"Уверенного совпадения нет"*) ok "why не гадает при слабом совпадении" ;;
302
+ *) bad "why выбрал запись при слабом совпадении" "$OUT" ;;
303
+ esac
304
+ cd "$WORK" || exit 1
305
+
306
+ # --- 20. new заводит заготовку в ПРОЕКТЕ, а не в комплекте -------------------
307
+ # Заготовка, созданная в каталоге пакета, для человека не существует: через npx пакет лежит во
308
+ # временной папке и исчезает вместе с ней. Работа сделана, результата нет.
309
+ NEWDIR="$(mktemp -d)"
310
+ ( cd "$NEWDIR" && git init -q . && node "$CLI" new proba-obraztsa >/dev/null 2>&1 )
311
+ if [ -f "$NEWDIR/gates/proba-obraztsa/gate.yml" ]; then
312
+ ok "new заводит заготовку в проекте"
313
+ else
314
+ bad "new завёл заготовку не в проекте" "$(ls -d "$NEWDIR"/gates/* 2>/dev/null || echo 'в проекте пусто')"
315
+ fi
316
+ if [ -d "$ROOT/kit/gates/proba-obraztsa" ]; then
317
+ bad "new написал в каталог комплекта из чужого проекта" "$ROOT/kit/gates/proba-obraztsa"
318
+ rm -rf "$ROOT/kit/gates/proba-obraztsa"
319
+ else
320
+ ok "new не трогает каталог комплекта из чужого проекта"
321
+ fi
322
+ # Заготовка обязана нести метки ЗАПОЛНИ: незаполненная запись не должна проехать как готовая.
323
+ if grep -rq 'ЗАПОЛНИ' "$NEWDIR/gates/proba-obraztsa" 2>/dev/null; then
324
+ ok "заготовка помечена как незаполненная"
325
+ else
326
+ bad "в заготовке нет меток ЗАПОЛНИ" "фильтр каталога не отличит её от готовой записи"
327
+ fi
328
+ rm -rf "$NEWDIR"
329
+
330
+ # --- 21. blob собирает все методички в один файл ------------------------------
331
+ BLOBDIR="$(mktemp -d)"
332
+ ( cd "$BLOBDIR" && node "$CLI" blob >/dev/null 2>&1 )
333
+ EXPECT_MD=$(find "$ROOT/kit/docs" -name '*.md' | wc -l)
334
+ if [ -f "$BLOBDIR/GOD_AI.md" ]; then
335
+ # Считаем только подписи, которые ставит сама склейка: путь в них начинается с kit/docs.
336
+ # Первая редакция считала все строки «источник» и намеряла 20 при 13 файлах — сами методички
337
+ # несут такие подписи внутри, указывая на проект, откуда перенесены.
338
+ GOT_MD=$(grep -c '^<!-- источник: kit/docs' "$BLOBDIR/GOD_AI.md")
339
+ if [ "$GOT_MD" -eq "$EXPECT_MD" ]; then
340
+ ok "blob собрал все методички ($GOT_MD)"
341
+ else
342
+ bad "blob собрал не все методички" "в kit/docs $EXPECT_MD, в склейке $GOT_MD"
343
+ fi
344
+ # Ссылки на соседние файлы внутри склейки ведут в никуда: соседей рядом больше нет.
345
+ if grep -qE '\]\((?!https?:)[^)]*\.md\)' "$BLOBDIR/GOD_AI.md" 2>/dev/null; then
346
+ bad "в склейке остались ссылки на соседние файлы" "внутри одного файла они ведут в никуда"
347
+ else
348
+ ok "ссылки на соседние файлы в склейке сняты"
349
+ fi
350
+ else
351
+ bad "blob не создал GOD_AI.md" "$BLOBDIR"
352
+ fi
353
+ rm -rf "$BLOBDIR"
354
+
355
+ # --- 22. doctor на самом комплекте ------------------------------------------
356
+ # В исходнике комплекта разложенной копии `.aqk/` нет и быть не должно. Пока это не различалось,
357
+ # `doctor` краснел на собственном репозитории и требовал разложить комплект в комплект — то есть
358
+ # врал ровно там, где его показывают первым делом.
359
+ if ( cd "$ROOT" && node "$CLI" doctor >/dev/null 2>&1 ); then
360
+ ok "doctor не краснеет на самом комплекте"
361
+ else
362
+ bad "doctor краснеет на самом комплекте" "оригиналы лежат в kit/, разложенной копии здесь не бывает"
363
+ fi
364
+
365
+ # --- 23. Go-конвенция *_test.go опознаётся как тесты ------------------------
366
+ # Найдено прогоном на gin-gonic/gin: тесты лежат рядом с кодом как foo_test.go, без
367
+ # отдельной папки tests/ — признак has_tests молчал на полностью протестированном репозитории.
368
+ GODIR="$(mktemp -d)"
369
+ mkdir -p "$GODIR/pkg"
370
+ printf 'package pkg\nfunc Foo() {}\n' > "$GODIR/pkg/foo.go"
371
+ printf 'package pkg\nfunc TestFoo(t *testing.T) {}\n' > "$GODIR/pkg/foo_test.go"
372
+ ( cd "$GODIR" && node "$CLI" init >/dev/null 2>&1 )
373
+ GOOUT=$( cd "$GODIR" && node "$CLI" doctor 2>&1 )
374
+ if printf '%s' "$GOOUT" | grep -E 'есть:.*\btests\b' >/dev/null; then
375
+ ok "*_test.go опознаётся как тесты (Go)"
376
+ else
377
+ bad "*_test.go не опознан как тесты" "признак has_tests молчит на репозитории gin-типа"
378
+ fi
379
+ rm -rf "$GODIR"
380
+
381
+ # --- 24. просьба про звезду и обратную связь — один раз на машину -----------
382
+ # Печатается один раз на установку (не на проект): второй init на этой же HOME её не повторяет.
383
+ # Отметка живёт вне репозитория — внутри .aqk/ она либо закоммитится в чужой проект, либо
384
+ # потеряется при повторном init --force.
385
+ FAKEHOME="$(mktemp -d)"
386
+ D1="$(mktemp -d)"
387
+ OUT1=$( cd "$D1" && HOME="$FAKEHOME" node "$CLI" init 2>&1 )
388
+ if printf '%s' "$OUT1" | grep -qi 'звезд'; then
389
+ ok "первый init на новой машине зовёт поставить звезду"
390
+ else
391
+ bad "первый init не упомянул звезду/обратную связь" "$OUT1"
392
+ fi
393
+ D2="$(mktemp -d)"
394
+ OUT2=$( cd "$D2" && HOME="$FAKEHOME" node "$CLI" init 2>&1 )
395
+ if printf '%s' "$OUT2" | grep -qi 'звезд'; then
396
+ bad "init повторил просьбу про звезду на той же машине" "второй проект, та же HOME"
397
+ else
398
+ ok "просьба про звезду не повторяется на той же машине"
399
+ fi
400
+ rm -rf "$FAKEHOME" "$D1" "$D2"
401
+
402
+ # --- 25. ссылки в kit/docs/ не битые -----------------------------------------
403
+ # entry-links-exist проверяет только корень проекта-получателя, а не kit/docs/ комплекта —
404
+ # методички туда не попадают вовсе. Нашли переносом файла: ../app-owner-strategy.md указывал
405
+ # мимо после того, как файл переехал в тот же каталог, что и index.md, — .md-ссылка молчала,
406
+ # доктор комплекта не краснел, потому что не туда смотрит.
407
+ BROKEN=0
408
+ while IFS= read -r -d '' MD; do
409
+ DIR=$(dirname "$MD")
410
+ grep -oE '\]\([^)]+\.md[^)]*\)' "$MD" 2>/dev/null | sed -E 's/^\]\(//; s/\)$//' | while IFS= read -r LINK; do
411
+ TARGET="${LINK%%#*}"
412
+ [ -z "$TARGET" ] && continue
413
+ case "$TARGET" in http*) continue ;; esac
414
+ [ -e "$DIR/$TARGET" ] || echo "$MD -> $LINK"
415
+ done
416
+ done < <(find "$ROOT/kit/docs" -name '*.md' -print0) > /tmp/aqk-broken-doc-links.$$
417
+ if [ -s /tmp/aqk-broken-doc-links.$$ ]; then
418
+ bad "в kit/docs/ есть битые ссылки на .md" "$(cat /tmp/aqk-broken-doc-links.$$)"
419
+ else
420
+ ok "ссылки в kit/docs/ не битые"
421
+ fi
422
+ rm -f /tmp/aqk-broken-doc-links.$$
423
+
424
+ # --- 26. doctor печатает версию комплекта ------------------------------------
425
+ # Баг-репорт без версии нечем привязать к коммиту — заметили, заполняя .github/ISSUE_TEMPLATE/,
426
+ # где просили версию из шапки doctor, а шапка её не печатала вовсе.
427
+ PKGVER=$(node -e "console.log(require('$ROOT/package.json').version)")
428
+ DOCVER=$( cd "$ROOT" && node "$CLI" doctor 2>&1 | head -3)
429
+ if printf '%s' "$DOCVER" | grep -qF "$PKGVER"; then
430
+ ok "doctor печатает версию комплекта ($PKGVER)"
431
+ else
432
+ bad "doctor не печатает версию" "package.json: $PKGVER; шапка doctor: $(printf '%s' "$DOCVER" | tr '\n' ' ')"
433
+ fi
434
+
435
+ # --- 27. пользовательская red/green не путается с образцами каталога -------
436
+ # `--exclude-dir=red` смотрит только на имя папки, не на путь — реальный секрет в чужой red/
437
+ # (red-team тесты, что угодно) был невидим во всех проектах, куда ставили гейт. Проверка на
438
+ # самом опасном случае: secrets-not-in-code.
439
+ REDDIR="$(mktemp -d)"
440
+ mkdir -p "$REDDIR/red"
441
+ # Собран из частей, а не написан буквально: иначе secrets-not-in-code находит эту строку
442
+ # в собственном исходнике smoke.sh — гейт теперь смотрит по всему дереву, включая себя.
443
+ printf 'AKIA%s\n' 'ABCDEFGHIJKLMNOP' > "$REDDIR/red/config.py"
444
+ if bash "$ROOT/kit/gates/secrets-not-in-code/check.sh" "$REDDIR" >/dev/null 2>&1; then
445
+ bad "секрет в пользовательской red/ невидим" "own_samples_filter смотрит по имени, не по пути"
446
+ else
447
+ ok "секрет в пользовательской red/ виден (не путается с образцами каталога)"
448
+ fi
449
+ rm -rf "$REDDIR"
450
+
451
+ # --- 28. note требует ту же отметку, что потом проверяет lesson-has-outcome -
452
+ # Раньше note принимал любой текст со словом "вывод" — запись проходила note и тут же
453
+ # краснела на doctor --run, потому что гейт требует одну из трёх настоящих отметок.
454
+ NOTEJ="$(mktemp -d)"
455
+ ( cd "$NOTEJ" && git init -q . && git config user.email t@t.com && git config user.name t \
456
+ && mkdir -p incidents && echo "# Журнал" > incidents/README.md && git add -A && git commit -q -m init )
457
+ NOWORD=$(cd /tmp && AQK_HOME="$NOTEJ" sh -c 'echo "слово вывод здесь есть, но отметки нет" | node "'"$CLI"'" note "без отметки"' 2>&1; echo "EXIT:$?")
458
+ if printf '%s' "$NOWORD" | grep -q "EXIT:0"; then
459
+ bad "note принял запись без настоящей отметки решения" "$(printf '%s' "$NOWORD" | head -3)"
460
+ else
461
+ ok "note требует настоящую отметку (✅🔧📜👤), не просто слово «вывод»"
462
+ fi
463
+ rm -rf "$NOTEJ"
464
+
465
+ # --- 29. заготовка aqk new использует own_samples_filter, а не голое имя red/green -
466
+ # CHECK_SH_TEMPLATE нёс тот же баг, что чинили в семи существующих гейтах: --exclude-dir=red
467
+ # по имени, а не по пути. Каждый новый гейт, заведённый через `aqk new`, наследовал бы дыру.
468
+ NEWDIR2="$(mktemp -d)"
469
+ ( cd "$NEWDIR2" && node "$CLI" init >/dev/null 2>&1 && node "$CLI" new probe-template >/dev/null 2>&1 )
470
+ if [ -f "$NEWDIR2/gates/_skip.sh" ] && grep -q 'own_samples_filter' "$NEWDIR2/gates/probe-template/check.sh"; then
471
+ ok "заготовка aqk new использует own_samples_filter и несёт _skip.sh"
472
+ else
473
+ bad "заготовка aqk new не подключает own_samples_filter или не копирует _skip.sh" "$NEWDIR2"
474
+ fi
475
+ rm -rf "$NEWDIR2"
476
+
477
+ # --- 30. doctor --run пишет короткий отчёт .aqk/last-run.md -----------------
478
+ # Список объявленных гейтов молчит о том, сколько из них реально работают именно сейчас —
479
+ # короткий отчёт после каждого прогона нужен и агенту в следующей сессии, и владельцу.
480
+ RUNDIR="$(mktemp -d)"
481
+ ( cd "$RUNDIR" && node "$CLI" start >/dev/null 2>&1 && node "$CLI" doctor --run >/dev/null 2>&1 )
482
+ if [ -f "$RUNDIR/.aqk/last-run.md" ] && grep -q 'итого:' "$RUNDIR/.aqk/last-run.md"; then
483
+ ok "doctor --run пишет .aqk/last-run.md"
484
+ else
485
+ bad "doctor --run не написал отчёт" "$RUNDIR/.aqk/last-run.md"
486
+ fi
487
+ rm -rf "$RUNDIR"
488
+
489
+ # --- 31. commit-explains-itself и note не спорят друг с другом ---------------
490
+ # Собственная команда `note` делает коммит сама, и её тело — заголовок записи, без разделов
491
+ # «Сделано:»/«Не уверен:». Пока гейт этого не различал, каждая запись в журнал красила
492
+ # репозиторий, где стоят оба, — то есть инструмент воевал сам с собой.
493
+ CEDIR="$(mktemp -d)"
494
+ (
495
+ cd "$CEDIR" && git init -q . && git config user.email t@t && git config user.name t
496
+ mkdir -p incidents && echo "# журнал" > incidents/README.md
497
+ # С комментарием в той же строке намеренно: программа режет «#…» при разборе манифеста
498
+ # (tool/lib/manifest.mjs), и гейт обязан читать тот же файл по тем же правилам.
499
+ printf 'lessons: incidents # где копятся уроки\n' > .aqk.yml
500
+ echo "код" > a.js
501
+ git add -A && git commit -q -m "первый"
502
+ echo "## запись" >> incidents/README.md && git add -A && git commit -q -m "lesson(aqk): шишка"
503
+ )
504
+ if bash "$ROOT/kit/gates/commit-explains-itself/check.sh" "$CEDIR" >/dev/null 2>&1; then
505
+ ok "коммит только в журнал не требует мини-отчёта: запись и есть отчёт"
506
+ else
507
+ bad "гейт краснеет на записи в журнал — спорит с собственной командой note"
508
+ fi
509
+ ( cd "$CEDIR" && echo "ещё" >> a.js && git add -A && git commit -q -m "fix: без отчёта" )
510
+ if bash "$ROOT/kit/gates/commit-explains-itself/check.sh" "$CEDIR" >/dev/null 2>&1; then
511
+ bad "гейт молчит на коммите в код без мини-отчёта"
512
+ else
513
+ ok "коммит, трогающий код, мини-отчёт всё так же обязан нести"
514
+ fi
515
+
516
+ # В мелком клоне (actions/checkout берёт один коммит) git считает коммит корневым и выдаёт
517
+ # всё дерево как изменённое — состав коммита узнать нельзя. Молча требовать отчёт в этом
518
+ # случае нельзя: конвейер краснел бы на каждой записи журнала. Пропускаем, назвав причину
519
+ # и способ починки, — так же, как doctor называет причину в списке «не применимо».
520
+ SHDIR="$(mktemp -d)"
521
+ git clone -q --depth 1 "file://$CEDIR" "$SHDIR/r" 2>/dev/null
522
+ OUT=$(bash "$ROOT/kit/gates/commit-explains-itself/check.sh" "$SHDIR/r" 2>&1)
523
+ if [ $? -eq 0 ] && printf '%s' "$OUT" | grep -q "fetch-depth"; then
524
+ ok "мелкий клон: гейт пропускает, назвав причину и способ починки"
525
+ else
526
+ bad "в мелком клоне гейт врёт про состав коммита" "$OUT"
527
+ fi
528
+ rm -rf "$SHDIR" "$CEDIR"
529
+
530
+ # --- итог -------------------------------------------------------------------
531
+ printf '\n'
532
+ if [ "$FAIL" -eq 0 ]; then
533
+ printf ' \033[32mвсё зелёное: %s проверок\033[0m\n\n' "$PASS"
534
+ else
535
+ printf ' \033[31mпровалено: %s из %s\033[0m\n\n' "$FAIL" "$((PASS + FAIL))"
536
+ fi
537
+
538
+ printf ' \033[2mне покрыто: содержание документов, установка с GitHub через npx\033[0m\n\n'
539
+ exit "$FAIL"
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env sh
2
+ # Все исходники комплекта разбираются: программа на нескольких файлах и скрипты гейтов.
3
+ #
4
+ # ЗАЧЕМ ОТДЕЛЬНЫМ ФАЙЛОМ. `node --check` берёт ровно один файл. Пока программа была одним
5
+ # файлом, этого хватало; после разделения проверка точки входа означала бы «семь из восьми не
6
+ # проверены» — ровно тот класс, где отсутствие сигнала читается как успех.
7
+ #
8
+ # Цикл в манифесте вместо скрипта не годится: проверка «объявленный гейт запускается» разбирает
9
+ # команду на слова и ищет пути — кусок `for f in $(...)` она читает как несуществующий файл.
10
+ set -u
11
+ ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
12
+ cd "$ROOT" || exit 1
13
+ BAD=0
14
+
15
+ for F in $(git ls-files 'tool/*.mjs' 'tool/**/*.mjs' 2>/dev/null); do
16
+ node --check "$F" || BAD=1
17
+ done
18
+ for F in $(git ls-files '*.sh' 2>/dev/null); do
19
+ bash -n "$F" || BAD=1
20
+ done
21
+
22
+ [ "$BAD" -eq 0 ] || echo " почини: файл не разбирается — до него не доедет ни одна другая проверка."
23
+ exit $BAD