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,9 @@
1
+ def used():
2
+ return 1
3
+
4
+
5
+ def also_used():
6
+ return 2
7
+
8
+
9
+ print(used(), also_used())
@@ -0,0 +1,9 @@
1
+ def used():
2
+ return 1
3
+
4
+
5
+ def never_called():
6
+ return 2
7
+
8
+
9
+ print(used())
@@ -0,0 +1,29 @@
1
+ # Версии зависимостей закреплены
2
+
3
+ **Намерение.** Рядом с объявлением зависимостей лежит файл с точными версиями, и он в
4
+ репозитории.
5
+
6
+ **Какой отказ это поймало.** Обязательный минимум проекта требует точных версий всего, что
7
+ участвует в сборке. Проверки на это не стояло нигде.
8
+ Запись в журнале: `incidents/README.md`, 2026-08-27.
9
+
10
+ **Почему это важнее, чем кажется.** Без закреплённых версий среда разъезжается между машиной
11
+ разработчика, конвейером и сервером. Это класс ошибок, который **невозможно поймать тестами**:
12
+ тесты зелёные у всех, а падает у одного. «У меня работает» становится неопровержимым, потому что
13
+ сравнить нечего.
14
+
15
+ **Что именно проверяется.** Для каждого найденного объявления зависимостей — наличие
16
+ соответствующего файла с закреплёнными версиями. Для `requirements.txt` иначе: там закрепление
17
+ живёт в самом файле, поэтому проверяется, что версии указаны через `==`, а не «не ниже такой-то».
18
+
19
+ **Пустое объявление пропускается.** Проект без единой зависимости закреплять нечем: пакетный
20
+ менеджер не создаст файл версий там, где версий нет. Требовать его значило бы красить гейт на
21
+ пустом месте — а такой гейт выключают.
22
+
23
+ **Чего НЕ ловит.** Что файл свежий: он может быть от прошлого года и не совпадать с объявлением.
24
+ Это ловится командой пакетного менеджера, а не наличием файла.
25
+
26
+ **Готового аналога нет** — ни в линтерах, ни в пакетных менеджерах: они
27
+ умеют создать файл версий, но не умеют требовать, чтобы он существовал и лежал в репозитории.
28
+
29
+ **Образцы.** `red/` — объявление зависимостей без закрепления. `green/` — с ним.
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env sh
2
+ # Рядом с объявлением зависимостей обязан лежать файл с закреплёнными версиями. Без него
3
+ # завтрашняя сборка соберёт другое, и «у меня работает» становится неопровержимым: сравнить
4
+ # нечего.
5
+ DIR="${1:-.}"
6
+ BAD=0
7
+
8
+ # Объявление без единой зависимости закреплять нечем и незачем: пакетный менеджер не создаст
9
+ # файл версий там, где версий нет. Требовать его — красить гейт на пустом месте.
10
+ has_deps_declared() {
11
+ case "$1" in
12
+ package.json) grep -qE '"(dependencies|devDependencies|peerDependencies)"[[:space:]]*:[[:space:]]*\{[[:space:]]*"' "$DIR/$1" ;;
13
+ # go.sum вообще не создаётся, если модуль использует только стандартную библиотеку —
14
+ # требовать его там означает красить гейт на пустом месте, а не ловить нарушение.
15
+ go.mod) grep -qE '^require\b' "$DIR/$1" ;;
16
+ *) return 0 ;;
17
+ esac
18
+ }
19
+
20
+ need() {
21
+ MANIFEST="$1"; shift
22
+ [ -f "$DIR/$MANIFEST" ] || return 0
23
+ has_deps_declared "$MANIFEST" || return 0
24
+ for LOCK in "$@"; do
25
+ [ -f "$DIR/$LOCK" ] && return 0
26
+ done
27
+ echo "$MANIFEST: нет файла с закреплёнными версиями (ожидался один из: $*)"
28
+ echo " почини: создай его командой пакетного менеджера и положи в репозиторий."
29
+ BAD=1
30
+ }
31
+
32
+ need package.json package-lock.json yarn.lock pnpm-lock.yaml npm-shrinkwrap.json
33
+ need pyproject.toml poetry.lock uv.lock pdm.lock
34
+ need go.mod go.sum
35
+ need Cargo.toml Cargo.lock
36
+ need Gemfile Gemfile.lock
37
+ need composer.json composer.lock
38
+
39
+ # requirements.txt закрепляют не отдельным файлом, а точными версиями в самом файле
40
+ if [ -f "$DIR/requirements.txt" ]; then
41
+ LOOSE=$(grep -nE '^[A-Za-z0-9_.-]+([[:space:]]*(>=|<=|>|<|~=|\^)|[[:space:]]*$)' "$DIR/requirements.txt" 2>/dev/null)
42
+ if [ -n "$LOOSE" ]; then
43
+ echo "$LOOSE" | sed 's|^|requirements.txt:|'
44
+ echo " почини: закрепи точные версии через ==, иначе сборка завтра соберёт другое."
45
+ BAD=1
46
+ fi
47
+ fi
48
+
49
+ exit $BAD
@@ -0,0 +1,9 @@
1
+ intent: версии зависимостей закреплены — сборка воспроизводима
2
+
3
+ trigger:
4
+ has_deps: true
5
+
6
+ recipes:
7
+ any: bash {gate}/check.sh {dir}
8
+
9
+ proof: incidents/README.md — «2026-08-27 сборка невоспроизводима: версии не закреплены»
@@ -0,0 +1,3 @@
1
+ module example.com/nodep
2
+
3
+ go 1.22
@@ -0,0 +1,3 @@
1
+ {
2
+ "lockfileVersion": 3
3
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "x",
3
+ "dependencies": { "left-pad": "1.3.0" }
4
+ }
@@ -0,0 +1,2 @@
1
+ requests==2.31.0
2
+ flask==3.0.0
@@ -0,0 +1,4 @@
1
+ {
2
+ "name": "x",
3
+ "dependencies": { "left-pad": "^1.0.0" }
4
+ }
@@ -0,0 +1,2 @@
1
+ requests>=2
2
+ flask
@@ -0,0 +1,5 @@
1
+ module example.com/withdep
2
+
3
+ go 1.22
4
+
5
+ require github.com/example/x v1.0.0
@@ -0,0 +1,40 @@
1
+ # Один и тот же код не размножается копиями
2
+
3
+ **Намерение.** Одинаковые куски не лежат в нескольких местах.
4
+
5
+ **Какой отказ это поймало.** В разборе 1069 коммитов: **число моделей захардкожено в четырёх
6
+ местах четырьмя разными значениями** — 10, 13, 14 и 30. Копии разъехались, и никто не заметил.
7
+ Запись в журнале: `incidents/README.md`, 2026-08-25.
8
+
9
+ **Почему это опаснее длины.** Правку вносят в одну копию из четырёх. Три остаются со старым
10
+ поведением, и расхождение всплывает не сразу и не там, где его сделали.
11
+
12
+ **Почему агенты плодят дубли особенно охотно.** Скопировать из соседнего файла дешевле, чем
13
+ найти общее место и вынести туда: копия точно работает, вынесение может что-то сломать.
14
+
15
+ **Готовый аналог есть.** `jscpd` умеет и Python, и TypeScript одним прогоном и считает
16
+ **похожесть**, а не только точное совпадение. Рецепты под фронтовые стеки берут его.
17
+
18
+ **Переносимая проверка грубее.** Она ищет одинаковые восемь строк подряд после снятия отступов —
19
+ совпадение байт в байт. Копия с переименованной переменной её не насторожит. Это запасной
20
+ вариант, а не замена.
21
+
22
+ **Тесты не проверяются.** Повтор в тестах часто осознанный: читаемость важнее сухости, и три
23
+ похожих теста лучше одного хитрого. Случай «три однотипных — свести в один с набором входов»
24
+ решается по правилам тестирования и глазами. На живом проекте **все двадцать находок были в
25
+ тестах** — гейт, который краснеет только на них, выключат целиком.
26
+
27
+ **Предел настраивается** переменной `AQK_DUP_LINES`, по умолчанию восемь строк.
28
+
29
+ **Триггер — от двадцати файлов.** В проекте на пять файлов дубли ищут глазами, и проверка была бы
30
+ шумом.
31
+
32
+ **Самая тяжёлая из наших.** На проекте в четыре тысячи файлов — порядка пятнадцати секунд:
33
+ она держит в памяти все восьмистрочные окна. Место такой проверки — перед пушем, в конвейере
34
+ или по расписанию, но не на каждый коммит.
35
+
36
+ **Чего НЕ ловит.** Копию с переименованной переменной, изменённым порядком строк или другим
37
+ форматированием — сравнение идёт байт в байт после снятия отступов. Не смотрит в тесты и в
38
+ сгенерированные файлы. Куски короче восьми строк не считает вовсе.
39
+
40
+ **Образцы.** `red/` — один блок в двух файлах. `green/` — блок вынесен в общую функцию.
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env sh
2
+ # Ищет одинаковые блоки по восемь строк. Мера грубая — совпадение байт в байт после снятия
3
+ # отступов, — но именно так размножается код, который агент копирует из соседнего файла.
4
+ #
5
+ # ЗАЧЕМ. Дубль опаснее длины: правку вносят в одну копию из четырёх, три остаются со старым
6
+ # поведением, и расхождение всплывает через недели в другом месте.
7
+ DIR="${1:-.}"
8
+ . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
9
+ WIN="${AQK_DUP_LINES:-8}"
10
+
11
+ # Тесты исключены намеренно. Повтор в тестах часто осознанный: читаемость там важнее сухости,
12
+ # и три похожих теста лучше одного хитрого. Случай «три однотипных — свести в один с набором
13
+ # входов» решается глазами по правилам тестирования, а не этим гейтом. На живом проекте все
14
+ # двадцать находок были в тестах — гейт, который краснеет только на них, выключат.
15
+ TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __tests__ -prune -o"
16
+
17
+ # shellcheck disable=SC2046
18
+ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
19
+ ! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
20
+ -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
21
+ | while IFS= read -r F; do is_generated "$F" || printf '%s\n' "$F"; done \
22
+ | xargs -r awk -v WIN="$WIN" '
23
+ FNR == 1 { n = 0; delete buf }
24
+ {
25
+ line = $0
26
+ gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
27
+ if (line == "" || line ~ /^([#]|\/\/)/) next # пустые и комментарии не считаем
28
+ buf[++n] = line
29
+ if (n >= WIN) {
30
+ key = ""
31
+ for (i = n - WIN + 1; i <= n; i++) key = key buf[i] "\x1e"
32
+ if (key in seen && seen[key] != FILENAME ":" (FNR - WIN + 1)) {
33
+ print seen[key] " и " FILENAME ":" (FNR - WIN + 1) ": одинаковые " WIN " строк"
34
+ } else if (!(key in seen)) {
35
+ seen[key] = FILENAME ":" (FNR - WIN + 1)
36
+ }
37
+ }
38
+ }
39
+ ' 2>/dev/null | sort -u \
40
+ | awk -F' и |: ' '
41
+ # Один повторённый кусок даёт столько сообщений, на сколько окон он делится: восемь
42
+ # строк — восемь почти одинаковых строк отчёта. Схлопываем в одну на пару файлов.
43
+ { split($1, a, ":"); split($2, b, ":"); pair = a[1] " и " b[1]
44
+ if (!(pair in seen)) { seen[pair] = $1 " и " $2 }
45
+ cnt[pair]++ }
46
+ END { for (p in seen) print seen[p] ": одинаковый кусок" (cnt[p] > 1 ? " (окон: " cnt[p] ")" : "") }
47
+ ' | sort > /tmp/.dup.$$
48
+
49
+ if [ -s /tmp/.dup.$$ ]; then
50
+ head -20 /tmp/.dup.$$
51
+ N=$(wc -l < /tmp/.dup.$$); [ "$N" -gt 20 ] && echo " … и ещё $((N - 20))"
52
+ rm -f /tmp/.dup.$$
53
+ echo " почини: вынеси общее в одно место. Правку вносят в одну копию из четырёх —"
54
+ echo " остальные остаются со старым поведением, и это всплывает не сразу и не здесь."
55
+ exit 1
56
+ fi
57
+ rm -f /tmp/.dup.$$
58
+ exit 0
@@ -0,0 +1,12 @@
1
+ intent: один и тот же код не размножается по проекту копиями
2
+
3
+ trigger:
4
+ files_gt: 20
5
+
6
+ recipes:
7
+ any: bash {gate}/check.sh {dir}
8
+ # jscpd умеет и Python, и TypeScript одним прогоном и считает похожесть, а не совпадение.
9
+ javascript: npx --yes jscpd@5 --min-lines 8 --threshold 1 {dir}
10
+ typescript: npx --yes jscpd@5 --min-lines 8 --threshold 1 {dir}
11
+
12
+ proof: incidents/README.md — «2026-08-25 разбор 1069 коммитов»: число моделей захардкожено в четырёх местах четырьмя разными значениями
@@ -0,0 +1,9 @@
1
+ def accumulate(rows, count):
2
+ total = 0
3
+ for row in rows:
4
+ for i in range(count):
5
+ value = compute(row[i])
6
+ if value is None:
7
+ continue
8
+ total += value
9
+ return total
@@ -0,0 +1,9 @@
1
+ from common import accumulate
2
+
3
+
4
+ def sum_a(rows):
5
+ return accumulate(rows, 2)
6
+
7
+
8
+ def sum_b(rows):
9
+ return accumulate(rows, 2)
@@ -0,0 +1,12 @@
1
+ def sum_a(rows):
2
+ total = 0
3
+ for row in rows:
4
+ value = compute(row[0])
5
+ if value is None:
6
+ continue
7
+ total += value
8
+ value = compute(row[1])
9
+ if value is None:
10
+ continue
11
+ total += value
12
+ return total
@@ -0,0 +1,12 @@
1
+ def sum_b(rows):
2
+ total = 0
3
+ for row in rows:
4
+ value = compute(row[0])
5
+ if value is None:
6
+ continue
7
+ total += value
8
+ value = compute(row[1])
9
+ if value is None:
10
+ continue
11
+ total += value
12
+ return total
@@ -0,0 +1,22 @@
1
+ # Ссылки точки входа ведут на существующие файлы
2
+
3
+ **Намерение.** Свод правил, который ссылается на несуществующий файл, утверждает то, чего нет.
4
+
5
+ **Какой отказ это поймало.** В `audit_project` чек-лист гейтов четыре недели числил работающими
6
+ изоляцию исполнителя и генерацию контрактов. Обе были отключены владельцем за три недели до
7
+ этого — инструмент ломал файлы. Планирование опиралось на защиту, которой не существовало.
8
+ Запись в журнале: `incidents/README.md`, 2026-08-24.
9
+
10
+ **Почему машина, а не внимательность.** Расхождение появляется не в момент написания документа, а
11
+ через недели, когда файл удалили в другой задаче. Человек в этот момент смотрит не сюда.
12
+
13
+ **Готовый аналог есть и он сильнее.** `lychee` и `markdown-link-check` проверяют ещё и внешние
14
+ адреса, и якоря внутри страницы. Рецепта под них в записи нет по устройству формата: рецепты
15
+ выбираются **по языку проекта**, а эти инструменты к языку не привязаны. Если такой инструмент у
16
+ вас стоит — он лучше нашего.
17
+
18
+ **Чего НЕ ловит.** Только файлы `*.md` в самом каталоге, без обхода вложенных: намерение записи —
19
+ точка входа, а не вся документация. Не проверяет внешние адреса (сеть) и якоря внутри файла.
20
+
21
+ **Образцы.** `red/` — свод ссылается на `rules/nope.md`, которого нет: гейт обязан краснеть.
22
+ `green/` — ссылается на существующий `rules/general.md`: гейт обязан молчать.
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env sh
2
+ # Каждая ссылка на локальный файл из markdown-файлов каталога обязана вести на
3
+ # существующий файл. Ссылка в никуда — это документ, утверждающий защиту,
4
+ # которой нет: планирование опирается на неё и ломается молча.
5
+ DIR="${1:-.}"
6
+ MISS=0
7
+ for MD in "$DIR"/*.md; do
8
+ [ -f "$MD" ] || continue
9
+ # вытащить цели ссылок вида [текст](путь)
10
+ TARGETS=$(sed -n 's/.*](\([^)]*\)).*/\1/p' "$MD")
11
+ for T in $TARGETS; do
12
+ # автоссылки бывают обёрнуты как <(https://...)> — искать http где угодно внутри,
13
+ # не только в начале строки.
14
+ case "$T" in *http://*|*https://*|\#*|mailto:*) continue ;; esac
15
+ T=${T%%#*}
16
+ [ -z "$T" ] && continue
17
+ if [ ! -e "$DIR/$T" ]; then
18
+ echo "$MD: ссылка в никуда — $T"
19
+ echo " почини: создай файл или убери ссылку. Документ, обещающий несуществующее, хуже отсутствующего."
20
+ MISS=1
21
+ fi
22
+ done
23
+ done
24
+ exit $MISS
@@ -0,0 +1,16 @@
1
+ # Запись каталога AQK. Норма и все поля — kit/gates/README.md.
2
+ # Читается программой; всё, что нельзя выполнить, живёт в README.md рядом.
3
+
4
+ intent: файлы, на которые ссылается точка входа, существуют на диске
5
+
6
+ # Когда запись показывается человеку. Отсутствие триггера сделало бы её шумом
7
+ # для тех, кого она не касается.
8
+ trigger:
9
+ always: true
10
+
11
+ # Команда-арбитр под каждый стек. {dir} — каталог, который проверяют.
12
+ # `any` — команда без зависимостей, работает везде, где есть sh и grep.
13
+ recipes:
14
+ any: bash {gate}/check.sh {dir}
15
+
16
+ proof: incidents/README.md — «2026-08-24 документ четыре недели утверждал защиту, которой не было»
@@ -0,0 +1,5 @@
1
+ # Свод правил
2
+
3
+ Стандарты: [общие правила](rules/general.md).
4
+ Внешняя ссылка: [semver](https://semver.org).
5
+ Автоссылка в скобках, как в CHANGELOG.md gin: [#1](<(https://example.com/pull/1)>).
@@ -0,0 +1,3 @@
1
+ # Общие правила
2
+
3
+ Пусто, но файл существует.
@@ -0,0 +1,3 @@
1
+ # Свод правил
2
+
3
+ Стандарты: [общие правила](rules/nope.md).
@@ -0,0 +1,22 @@
1
+ # Файл не вырастает до размера, в котором агент теряется
2
+
3
+ **Намерение.** Прод-код не длиннее 500 строк, компонент интерфейса — 300, тест — 800.
4
+
5
+ **Какой отказ это поймало.** В разборе 1069 коммитов за 90 дней работы с агентами нашёлся
6
+ **один файл маршрутов на 6305 строк**. Дословно: «не потому что так задумано, а потому что каждую
7
+ новую фичу дописывали туда же — агенту так ближе по контексту».
8
+ Запись в журнале: `incidents/README.md`, 2026-08-25.
9
+
10
+ **Почему машина, а не внимательность.** Файл растёт по одной строке за раз, и ни одна правка не
11
+ выглядит как «пора делить». Момент, когда стало поздно, не наступает — он проходит незаметно.
12
+
13
+ **Числа спорные, предел — нет.** Можно спорить, 500 или 700; нельзя работать без предела вообще.
14
+ Свои числа правятся в `check.sh` одной строкой.
15
+
16
+ **Чего НЕ ловит.** Длину функции и вложенность: файл на 200 строк с одной функцией в 180 строк
17
+ проверку пройдёт. Это отдельная мера — цикломатическая сложность.
18
+
19
+ **Готового аналога нет.** Проверено: среди 964 правил `ruff` предела на
20
+ размер файла нет ни одного. Это тот случай, когда свой гейт законен.
21
+
22
+ **Образцы.** `red/` — прод-файл на 600 строк. `green/` — тот же код, разделённый надвое.
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env sh
2
+ # Пределы: прод-код 500 строк, компонент интерфейса 300, тест 800.
3
+ #
4
+ # ЗАЧЕМ. Агент теряется в больших файлах и начинает переписывать вместо правки. И растут они
5
+ # не по замыслу: каждую новую фичу дописывают в тот же файл, потому что агенту так ближе по
6
+ # контексту. Числа спорные — важно, что предел существует и его считает машина.
7
+ DIR="${1:-.}"
8
+ . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
9
+
10
+ # Один обход и один wc на все файлы разом: на проекте в 36 тысяч файлов цикл с wc на каждый
11
+ # не укладывался в две минуты.
12
+ # shellcheck disable=SC2046
13
+ find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
14
+ | while IFS= read -r F; do is_generated "$F" || printf '%s\n' "$F"; done \
15
+ | xargs -r wc -l 2>/dev/null \
16
+ | awk '
17
+ $2 == "total" { next }
18
+ {
19
+ n = $1; f = $2
20
+ limit = 500; kind = "прод-код"
21
+ if (f ~ /(test|spec)/) { limit = 800; kind = "тест" }
22
+ else if (f ~ /\.(jsx|tsx|vue)$/) { limit = 300; kind = "компонент" }
23
+ # Число строк стоит в позиции номера строки нарочно: храповик вырезает «:N:» из ключа,
24
+ # и запись о нарушении не меняется от каждой добавленной строки. Иначе рост файла на
25
+ # строку читался бы как новое нарушение, а сокращение — тоже как новое.
26
+ if (n > limit) { print f ":" n ": длиннее предела для «" kind "» — " limit " строк"; bad = 1 }
27
+ }
28
+ END { exit bad ? 1 : 0 }
29
+ ' || {
30
+ echo " почини: раздели по смыслу, а не пополам. Файл растёт не по замыслу — в него"
31
+ echo " дописывают каждую новую правку, потому что так ближе по контексту."
32
+ exit 1
33
+ }
34
+ exit 0
@@ -0,0 +1,9 @@
1
+ intent: файл не вырастает до размера, в котором агент теряется
2
+
3
+ trigger:
4
+ always: true
5
+
6
+ recipes:
7
+ any: bash {gate}/check.sh {dir}
8
+
9
+ proof: incidents/README.md — «2026-08-25 разбор 1069 коммитов»: один файл маршрутов на 6305 строк