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
@@ -4,11 +4,15 @@
4
4
  # оставляет след для человека, читающего историю позже.
5
5
  DIR="${1:-.}"
6
6
 
7
- # Образцы (gates.sh) читают тело коммита из файла — реальный git log там взять неоткуда, а
7
+ # Образцы (gates.sh) читают тело коммита из `.aqk-commit-msg` — реальный git log там взять неоткуда, а
8
8
  # вложенный .git внутри каталога комплекта сам по себе создал бы embedded-репозиторий.
9
- # В настоящем проекте COMMIT_MSG не бывает — читается последний реальный коммит.
10
- if [ -f "$DIR/COMMIT_MSG" ]; then
11
- MSG=$(cat "$DIR/COMMIT_MSG")
9
+ # В настоящем проекте такого файла не бывает — читается последний реальный коммит.
10
+ # Имя с точкой намеренно: `COMMIT_MSG` в корне проекта — вещь возможная (так зовут заготовку
11
+ # сообщения многие обёртки над git), и такой файл молча подменял бы собой настоящий коммит.
12
+ # Тот же дефект нашло ревью у `personal-config-not-shared` 2026-09-07; здесь он был с самого
13
+ # начала и не всплывал.
14
+ if [ -f "$DIR/.aqk-commit-msg" ]; then
15
+ MSG=$(cat "$DIR/.aqk-commit-msg")
12
16
  else
13
17
  if ! (cd "$DIR" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1); then
14
18
  echo "не git-репозиторий — проверять нечего"
@@ -33,5 +33,10 @@
33
33
  считает только глубину. Не различает функции внутри файла: меряется худшее место файла, а не
34
34
  каждая функция отдельно. Готовые правила умеют и то, и другое.
35
35
 
36
+ **Тесты не проверяются.** Глубокая вложенность в тесте — это обход таблицы ожиданий, а не
37
+ сложная логика. Замер по `fastapi` дал 303 находки, из которых **101 в `tests/`**; после
38
+ исключения тестов осталось 10. Гейт, который краснеет в основном на тестах, выключают целиком —
39
+ тот же довод и по той же причине, что в `duplicate-code`.
40
+
36
41
  **Образцы.** `red/` — восемь уровней вложенности. `green/` — тот же смысл, разбитый на две
37
42
  функции с ранним выходом.
@@ -6,11 +6,30 @@
6
6
  # ЗАЧЕМ ВООБЩЕ. 29 ветвлений в одной функции — это код, который никто не держит в голове
7
7
  # целиком: ни человек, ни агент. Агент в таком месте начинает переписывать вместо правки.
8
8
  DIR="${1:-.}"
9
- . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
9
+ # Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
10
+ # `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
11
+ # неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
12
+ # `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
13
+ # ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
14
+ # выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
15
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
16
+ if [ ! -f "$SKIP_LIB" ]; then
17
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
18
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
19
+ exit 2
20
+ fi
21
+ . "$SKIP_LIB"
10
22
  MAX="${AQK_MAX_DEPTH:-5}"
11
23
 
24
+ # Тесты исключены намеренно — тем же списком, что и в duplicate-code. Глубокая вложенность в
25
+ # тесте это обход таблицы ожиданий, а не сложная логика: замер по fastapi дал 303 находки, из
26
+ # которых 101 в tests/. Гейт, который краснеет в основном на тестах, выключают целиком.
27
+ TESTS="-name test -prune -o -name tests -prune -o -name spec -prune -o -name __tests__ -prune -o"
28
+
12
29
  # shellcheck disable=SC2046
13
- find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
30
+ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
31
+ ! -name 'test_*' ! -name '*_test.*' ! -name '*.test.*' ! -name '*.spec.*' \
32
+ -print 2>/dev/null | only_code | own_samples_filter "$DIR" \
14
33
  | while IFS= read -r F; do is_generated "$F" || printf '%s\n' "$F"; done \
15
34
  | xargs -r awk -v MAX="$MAX" '
16
35
  # Один обход на все файлы: процесс на каждый файл дал 19 секунд на 4000 файлов.
@@ -0,0 +1,14 @@
1
+ # Тест с глубокой вложенностью — не сложная логика, а разбор дерева ожиданий. Найдено замером
2
+ # по fastapi: 101 находка из 303 пришлась на tests/, и все они — таблицы ожидаемых ответов,
3
+ # которые никто не станет «упрощать». Гейт, который краснеет в основном на тестах, выключат.
4
+ def test_openapi_schema(client):
5
+ response = client.get("/openapi.json")
6
+ assert response.status_code == 200
7
+ schema = response.json()
8
+ for path, methods in schema["paths"].items():
9
+ for method, spec in methods.items():
10
+ for code, resp in spec["responses"].items():
11
+ if "content" in resp:
12
+ for media, body in resp["content"].items():
13
+ if "schema" in body:
14
+ assert body["schema"] is not None
@@ -26,4 +26,17 @@
26
26
  **Готового аналога нет** — ни в линтерах, ни в пакетных менеджерах: они
27
27
  умеют создать файл версий, но не умеют требовать, чтобы он существовал и лежал в репозитории.
28
28
 
29
- **Образцы.** `red/` объявление зависимостей без закрепления. `green/` с ним.
29
+ **Закрепляют не только poetry и uv.** Для `pyproject.toml` файлом закрепления считается и
30
+ `requirements.txt` — но только если он сам проходит проверку, то есть все версии в нём стоят
31
+ через `==`. Иначе «есть requirements.txt» стало бы способом обойти гейт пустым файлом.
32
+ Найдено замером по `httpx`: там инструменты закреплены до патча прямо в `requirements.txt`,
33
+ а гейт требовал ещё и `poetry.lock`, которого в этом укладе не бывает вовсе.
34
+
35
+ **Не различает библиотеку и приложение.** Библиотека намеренно оставляет
36
+ границы своих зависимостей широкими — закрепив их у себя, она ломает сборку всем, кто её
37
+ ставит. Гейт смотрит только на то, закреплено ли окружение самого проекта, и правильно молчит,
38
+ когда библиотека закрепила инструменты и не закрепила зависимости.
39
+
40
+ **Образцы.** `red/` — объявление зависимостей без закрепления, плюс `pyproject.toml` рядом с
41
+ незакреплённым `requirements.txt`. `green/` — с закреплением, включая уклад «pyproject плюс
42
+ requirements.txt с точными версиями».
@@ -30,7 +30,12 @@ need() {
30
30
  }
31
31
 
32
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
33
+ # requirements.txt считается закреплением для pyproject.toml наравне с файлами блокировки:
34
+ # закрепляют не только poetry и uv. Замер по httpx: инструменты там закреплены до патча
35
+ # прямо в requirements.txt, а гейт требовал ещё и poetry.lock, которого в этом укладе не
36
+ # бывает вовсе. Файл засчитывается только если он сам проходит проверку ниже — иначе
37
+ # «есть requirements.txt» стало бы способом обойти гейт пустым файлом.
38
+ need pyproject.toml poetry.lock uv.lock pdm.lock requirements.txt
34
39
  need go.mod go.sum
35
40
  need Cargo.toml Cargo.lock
36
41
  need Gemfile Gemfile.lock
@@ -0,0 +1,12 @@
1
+ # Питоновская библиотека, которая закрепляет версии не файлом блокировки, а точными версиями
2
+ # в requirements.txt. Так делает httpx и весь класс проектов, живущих без poetry и uv:
3
+ # инструменты закреплены до патча, а границы зависимостей самой библиотеки оставлены широкими
4
+ # намеренно — библиотека, закрепившая их у себя, ломает сборку всем, кто её ставит.
5
+ [project]
6
+ name = "sample-lib"
7
+ version = "1.0.0"
8
+ dependencies = ["httpx"]
9
+
10
+ [build-system]
11
+ requires = ["setuptools"]
12
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,3 @@
1
+ -e .
2
+ pytest==8.4.1
3
+ ruff==0.16.6
@@ -0,0 +1,12 @@
1
+ # Питоновская библиотека, которая закрепляет версии не файлом блокировки, а точными версиями
2
+ # в requirements.txt. Так делает httpx и весь класс проектов, живущих без poetry и uv:
3
+ # инструменты закреплены до патча, а границы зависимостей самой библиотеки оставлены широкими
4
+ # намеренно — библиотека, закрепившая их у себя, ломает сборку всем, кто её ставит.
5
+ [project]
6
+ name = "sample-lib"
7
+ version = "1.0.0"
8
+ dependencies = ["httpx"]
9
+
10
+ [build-system]
11
+ requires = ["setuptools"]
12
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,3 @@
1
+ -e .
2
+ pytest>=8
3
+ ruff
@@ -12,13 +12,22 @@
12
12
  **Почему агенты плодят дубли особенно охотно.** Скопировать из соседнего файла дешевле, чем
13
13
  найти общее место и вынести туда: копия точно работает, вынесение может что-то сломать.
14
14
 
15
- **Готовый аналог есть.** `jscpd` умеет и Python, и TypeScript одним прогоном и считает
16
- **похожесть**, а не только точное совпадение. Рецепты под фронтовые стеки берут его.
15
+ **Готовый аналог есть, и под каждый стек свой.** `jscpd` для фронта считает **похожесть**, а не
16
+ только точное совпадение. `pylint --enable=R0801` для Python сравнивает разобранный код, и
17
+ переименованная переменная его не обманет. Рецепт под Python появился только 2026-09-07: до
18
+ ревизии каталога его не было вовсе, и питоновский проект получал нашу побайтовую самоделку,
19
+ хотя родной инструмент стоял у него уже установленным.
17
20
 
18
21
  **Переносимая проверка грубее.** Она ищет одинаковые восемь строк подряд после снятия отступов —
19
22
  совпадение байт в байт. Копия с переименованной переменной её не насторожит. Это запасной
20
23
  вариант, а не замена.
21
24
 
25
+ **Одинаковые импорты — не дубль.** Окно, в котором нет ни одной строки кроме ввоза (`import`,
26
+ `use`, `require`, путь в кавычках внутри `import (…)` в Go) и одиноких скобок, находкой не
27
+ считается. Найдено замером по `cobra`: гейт краснел на `doc/man_docs.go` и `doc/md_docs.go`,
28
+ где совпадали восемь строк подряд из списка импортов и ни одной строки логики. В Go, Java и Rust
29
+ ввоз стоит по одной строке на строку, и в соседних файлах одного пакета он совпадает всегда.
30
+
22
31
  **Тесты не проверяются.** Повтор в тестах часто осознанный: читаемость важнее сухости, и три
23
32
  похожих теста лучше одного хитрого. Случай «три однотипных — свести в один с набором входов»
24
33
  решается по правилам тестирования и глазами. На живом проекте **все двадцать находок были в
@@ -5,7 +5,19 @@
5
5
  # ЗАЧЕМ. Дубль опаснее длины: правку вносят в одну копию из четырёх, три остаются со старым
6
6
  # поведением, и расхождение всплывает через недели в другом месте.
7
7
  DIR="${1:-.}"
8
- . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
8
+ # Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
9
+ # `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
10
+ # неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
11
+ # `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
12
+ # ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
13
+ # выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
14
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
15
+ if [ ! -f "$SKIP_LIB" ]; then
16
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
17
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
18
+ exit 2
19
+ fi
20
+ . "$SKIP_LIB"
9
21
  WIN="${AQK_DUP_LINES:-8}"
10
22
 
11
23
  # Тесты исключены намеренно. Повтор в тестах часто осознанный: читаемость там важнее сухости,
@@ -21,15 +33,30 @@ find "$DIR" $(skip_find "$DIR") $TESTS -type f \
21
33
  | while IFS= read -r F; do is_generated "$F" || printf '%s\n' "$F"; done \
22
34
  | LC_ALL=C sort \
23
35
  | xargs -r env LC_ALL=C awk -v WIN="$WIN" '
24
- FNR == 1 { n = 0; delete buf }
36
+ # Строка-объявление ввоза: `import`, `from import`, `use …;`, путь в кавычках внутри
37
+ # блока `import (…)` в Go, `require(…)`, а также одинокие скобки и точки с запятой.
38
+ # ЗАЧЕМ. Восемь одинаковых строк ввоза подряд — форма языка, а не размноженный код: в Go,
39
+ # Java и Rust список ввоза стоит по одной строке и в соседних файлах одного пакета
40
+ # совпадает целиком. Замер по cobra: обе находки в doc/ были ровно этим — ни одной
41
+ # строки логики. Окно, где нет ни одной строки кроме ввоза, дублем не считается.
42
+ function isimport(s) {
43
+ return s ~ /^(import|from|use|export|require|package|#include|using)([[:space:](]|$)/ ||
44
+ s ~ /^[A-Za-z_][A-Za-z0-9_]*[[:space:]]+"[^"]*"$/ ||
45
+ s ~ /^[_.]?[[:space:]]*"[^"]*"[,;]?$/ ||
46
+ s ~ /^(const|let|var)[[:space:]].*require\(/ ||
47
+ s ~ /^[(){}\[\];,]+$/
48
+ }
49
+ FNR == 1 { n = 0; delete buf; delete imp }
25
50
  {
26
51
  line = $0
27
52
  gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
28
53
  if (line == "" || line ~ /^([#]|\/\/)/) next # пустые и комментарии не считаем
29
54
  buf[++n] = line
55
+ imp[n] = isimport(line)
30
56
  if (n >= WIN) {
31
- key = ""
32
- for (i = n - WIN + 1; i <= n; i++) key = key buf[i] "\x1e"
57
+ key = ""; code = 0
58
+ for (i = n - WIN + 1; i <= n; i++) { key = key buf[i] "\x1e"; if (!imp[i]) code = 1 }
59
+ if (!code) next # окно целиком из ввоза — не дубль
33
60
  if (key in seen && seen[key] != FILENAME ":" (FNR - WIN + 1)) {
34
61
  print seen[key] " и " FILENAME ":" (FNR - WIN + 1) ": одинаковые " WIN " строк"
35
62
  } else if (!(key in seen)) {
@@ -16,7 +16,15 @@ recipes:
16
16
  #
17
17
  # «c#» в списке нет намеренно: разбор манифеста режет строку по «#», и рецепт обрывался бы
18
18
  # на «c». Обёртку _native.sh не пишем — её добавляет сама программа при установке гейта.
19
+ #
20
+ # Рецепт стоял только под javascript и typescript, хотя список --format покрывает и Python,
21
+ # и Go. Питоновский проект получал переносимую самоделку вместо готового инструмента —
22
+ # найдено ревизией каталога 2026-09-07, замером по пяти стекам.
19
23
  javascript: npx --yes jscpd@5 --min-lines 8 --threshold 1 --format "javascript,jsx,typescript,tsx,vue,python,go,ruby,java,php,rust,kotlin,swift,scala" {dir}
20
24
  typescript: npx --yes jscpd@5 --min-lines 8 --threshold 1 --format "javascript,jsx,typescript,tsx,vue,python,go,ruby,java,php,rust,kotlin,swift,scala" {dir}
25
+ # Родной инструмент стека, а не jscpd: питоновскому проекту незачем ставить Node ради одной
26
+ # проверки. R0801 сравнивает не байты, а разобранный код — переименованная переменная его
27
+ # не обманет, в отличие от переносимого рецепта.
28
+ python: pylint --disable=all --enable=R0801 --min-similarity-lines=8 {dir}
21
29
 
22
30
  proof: incidents/README.md — «2026-08-25 разбор 1069 коммитов»: число моделей захардкожено в четырёх местах четырьмя разными значениями
@@ -0,0 +1,20 @@
1
+ // Одинаковый блок импортов в двух файлах одного пакета — не дубль кода, а форма языка.
2
+ // Найдено замером по cobra: гейт краснел на doc/man_docs.go и doc/md_docs.go, где совпадали
3
+ // восемь строк подряд из списка импортов, и ни одной строки логики.
4
+ package doc
5
+
6
+ import (
7
+ "bytes"
8
+ "fmt"
9
+ "io"
10
+ "os"
11
+ "path/filepath"
12
+ "sort"
13
+ "strconv"
14
+ "strings"
15
+ )
16
+
17
+ func RenderMan(w io.Writer, name string) error {
18
+ _, err := fmt.Fprintf(w, "man page for %s", name)
19
+ return err
20
+ }
@@ -0,0 +1,19 @@
1
+ package doc
2
+
3
+ import (
4
+ "bytes"
5
+ "fmt"
6
+ "io"
7
+ "os"
8
+ "path/filepath"
9
+ "sort"
10
+ "strconv"
11
+ "strings"
12
+ )
13
+
14
+ func RenderMarkdown(w io.Writer, name string) error {
15
+ var buf bytes.Buffer
16
+ buf.WriteString(strings.ToUpper(name))
17
+ _, err := w.Write(buf.Bytes())
18
+ return err
19
+ }
@@ -15,6 +15,11 @@
15
15
  выбираются **по языку проекта**, а эти инструменты к языку не привязаны. Если такой инструмент у
16
16
  вас стоит — он лучше нашего.
17
17
 
18
+ **Адрес страницы сайта не считается битой ссылкой.** Путь, кончающийся косой чертой —
19
+ `[руководство](tutorial/#install)`, — это адрес на опубликованном сайте, а не файл в
20
+ репозитории. Так ссылаются mkdocs, docusaurus и jekyll. Найдено замером по `fastapi`: гейт
21
+ объявлял битой рабочую ссылку из их README.
22
+
18
23
  **Чего НЕ ловит.** Только файлы `*.md` в самом каталоге, без обхода вложенных: намерение записи —
19
24
  точка входа, а не вся документация. Не проверяет внешние адреса (сеть) и якоря внутри файла.
20
25
 
@@ -14,6 +14,12 @@ for MD in "$DIR"/*.md; do
14
14
  case "$T" in *http://*|*https://*|\#*|mailto:*) continue ;; esac
15
15
  T=${T%%#*}
16
16
  [ -z "$T" ] && continue
17
+ # Путь, кончающийся косой чертой, — адрес страницы опубликованного сайта, а не файл на
18
+ # диске. Так ссылаются mkdocs, docusaurus и jekyll: `[руководство](tutorial/#install)`
19
+ # работает у читателя и не существует в репозитории. Найдено замером по fastapi — гейт
20
+ # объявлял битой рабочую ссылку из их README. Проверять такие адреса умеют lychee и
21
+ # markdown-link-check: они ходят в сеть, а мы смотрим только на диск.
22
+ case "$T" in */) continue ;; esac
17
23
  if [ ! -e "$DIR/$T" ]; then
18
24
  echo "$MD: ссылка в никуда — $T"
19
25
  echo " почини: создай файл или убери ссылку. Документ, обещающий несуществующее, хуже отсутствующего."
@@ -3,3 +3,6 @@
3
3
  Стандарты: [общие правила](rules/general.md).
4
4
  Внешняя ссылка: [semver](https://semver.org).
5
5
  Автоссылка в скобках, как в CHANGELOG.md gin: [#1](<(https://example.com/pull/1)>).
6
+ Ссылка на страницу сайта документации: [руководство](tutorial/#install) — путь опубликованного
7
+ сайта, а не файл на диске. Найдено замером по fastapi: `[installation guide](tutorial/#install-fastapi)`
8
+ в README читалась как битая, хотя на сайте работает. Так ссылаются mkdocs, docusaurus и jekyll.
@@ -16,7 +16,14 @@
16
16
  **Чего НЕ ловит.** Длину функции и вложенность: файл на 200 строк с одной функцией в 180 строк
17
17
  проверку пройдёт. Это отдельная мера — цикломатическая сложность.
18
18
 
19
- **Готового аналога нет.** Проверено: среди 964 правил `ruff` предела на
20
- размер файла нет ни одного. Это тот случай, когда свой гейт законен.
19
+ **Готового аналога нет для Python.** Проверено дважды, второй раз 2026-09-07 по каталогу
20
+ правил `ruff`: предела на размер файла там нет ни одного (`pylint C0302 too-many-lines` в `ruff`
21
+ не перенесён). В eslint правило `max-lines` есть, но рецепта под него здесь нет намеренно: оно
22
+ знает один предел на все файлы, а запись держит два — 500 строк прод-коду и 800 тесту. Рецепт,
23
+ который меряет не то же самое, что переносимая проверка, делает запись означающей разное на
24
+ разных машинах; правило каталога это запрещает (см. `color-from-token`).
25
+
26
+ **Замер.** Пять чужих репозиториев (`httpx`, `fastapi`, `zod`, `cobra`, `ripgrep`) — 86 находок,
27
+ ложных **ноль**: мера прямая, файл либо длиннее предела, либо нет.
21
28
 
22
29
  **Образцы.** `red/` — прод-файл на 600 строк. `green/` — тот же код, разделённый надвое.
@@ -5,7 +5,19 @@
5
5
  # не по замыслу: каждую новую фичу дописывают в тот же файл, потому что агенту так ближе по
6
6
  # контексту. Числа спорные — важно, что предел существует и его считает машина.
7
7
  DIR="${1:-.}"
8
- . "$(dirname "$0")/../_skip.sh" 2>/dev/null || SKIP_NAMES=".git .aqk node_modules .venv"
8
+ # Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
9
+ # `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
10
+ # неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
11
+ # `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
12
+ # ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
13
+ # выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
14
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
15
+ if [ ! -f "$SKIP_LIB" ]; then
16
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
17
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
18
+ exit 2
19
+ fi
20
+ . "$SKIP_LIB"
9
21
 
10
22
  # Один обход и один wc на все файлы разом: на проекте в 36 тысяч файлов цикл с wc на каждый
11
23
  # не укладывался в две минуты.
@@ -0,0 +1,54 @@
1
+ # Подавление проверки — точечное и с причиной
2
+
3
+ **Намерение.** Когда проверка краснеет, у пишущего два выхода: починить код или заглушить
4
+ проверку. Второй дешевле и с виду неотличим от первого — конвейер зелёный, диф маленький.
5
+
6
+ Для агента это выход по умолчанию: задача сформулирована как «сделай, чтобы прошло», и
7
+ `@ts-ignore` решает её буквально. Дефект при этом остаётся, а сигнал о нём исчезает навсегда —
8
+ это тот же класс, что молчащий гейт, только оплаченный одной строкой.
9
+
10
+ **Какой отказ это поймало.** Замер по пятнадцати чужим репозиториям (20 774 файла): 22 находки,
11
+ 19 настоящих. Среди них — `// @ts-nocheck` первой строкой боевой страницы в `kodus-ai`
12
+ (проверка типов выключена для целого файла), `# noqa E501` без двоеточия в `pr-agent`
13
+ (flake8 такую форму читает как безадресный `# noqa`: погашено не одно правило, а все),
14
+ `/** @ts-ignore */` и `.strict().argv // eslint-disable-line` без имени правила в `repolinter`.
15
+ Запись в журнале: `incidents/README.md`, 2026-09-06.
16
+
17
+ **Что именно проверяется.** Красным делается только **безадресное** подавление:
18
+
19
+ | Красное | Зелёное |
20
+ |---|---|
21
+ | `# noqa` | `# noqa: F401 — причина` |
22
+ | `# type: ignore` | `# type: ignore[no-any-return]` |
23
+ | `# flake8: noqa`, `# ruff: noqa`, `# mypy: ignore-errors`, `# pylint: disable=all` | точечная форма с кодом |
24
+ | `/* eslint-disable */`, `// eslint-disable-next-line` без правила | `// eslint-disable-next-line no-console -- причина` |
25
+ | `// @ts-ignore`, `// @ts-nocheck` | `// @ts-expect-error -- причина` |
26
+ | `#[allow(warnings)]`, `@SuppressWarnings("all")`, `//nolint`, `rubocop:disable all`, `shellcheck disable` без `=SC…` | те же с именем правила |
27
+ | `--no-verify` в скрипте или конфиге конвейера | обычный коммит |
28
+
29
+ `@ts-ignore` красный всегда — единственное исключение из правила «безадресное». У него есть
30
+ строго лучший брат: `@ts-expect-error` краснеет сам, как только становится не нужен, то есть
31
+ не переживает починку кода. `@ts-ignore` переживает и молчит дальше.
32
+
33
+ **Готовый аналог.** Есть, и под каждый язык свой:
34
+ [`eslint-plugin-eslint-comments`](https://github.com/mysticatea/eslint-plugin-eslint-comments)
35
+ с правилом `require-description` — для JavaScript и TypeScript;
36
+ [`flake8-noqa`](https://github.com/plinss/flake8-noqa) — для Python, он же ловит сломанную форму
37
+ `# noqa E501`. Если проект на одном языке — ставь их, они точнее: разбирают код, а не текст.
38
+ Эта запись нужна там, где языков несколько или где ставить нечего: она переносима, ничего не
39
+ требует установить и покрывает то, чего нет ни у одного из двоих — `@ts-ignore` как класс,
40
+ `--no-verify` в конвейере, `@SuppressWarnings("all")`.
41
+
42
+ **Чего НЕ ловит.**
43
+
44
+ - **Подавление внутри строкового литерала считается настоящим.** Фикстуры инструментов, которые
45
+ сами обрабатывают подавления, дают ложные срабатывания: 3 из 22 на замере — тесты
46
+ `react-doctor` и `ratchets`. Отличить строку от комментария надёжно можно только разбором
47
+ языка, а его у переносимой проверки нет. Такие места — в `.aqkignore` или под храповик.
48
+ - **Точечное подавление без причины проходит.** `# noqa: E501` принимается, даже если рядом нет
49
+ ни слова о том, зачем. Требовать объяснение от каждой строки значит получить гейт, который
50
+ выключат целиком; за объяснением — к `require-description` из `eslint-plugin-eslint-comments`.
51
+ - **Не видит настройку в конфиге.** Правило, выключенное в `.eslintrc` или `pyproject.toml`, —
52
+ такое же ослабление, но это уже не подавление в коде, а решение проекта, записанное явно.
53
+ - **Не знает, было ли подавление оправдано.** Проверка отвечает на вопрос «названо ли, что
54
+ именно погашено», а не «стоило ли гасить».
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env sh
2
+ # Подавление проверки без адреса: «выключено всё» вместо «выключено вот это».
3
+ #
4
+ # ЗАЧЕМ ИМЕННО ЭТО. Когда проверка краснеет, у пишущего два выхода: починить код или заглушить
5
+ # проверку. Второй дешевле и с виду неотличим от первого — конвейер зелёный, диф маленький.
6
+ # Для агента это выход по умолчанию: ему поставлена задача «сделай, чтобы прошло».
7
+ #
8
+ # ГРАНИЦА НАМЕРЕННО УЗКАЯ. Красным делается не всякое подавление, а безадресное: `# noqa` без
9
+ # кода, `eslint-disable` без имени правила, `@SuppressWarnings("all")`. Точечное подавление с
10
+ # названным правилом — законный инструмент, и требовать объяснения от каждого значит получить
11
+ # гейт, который выключат. Одно исключение — `@ts-ignore`: у него есть строго лучший брат
12
+ # `@ts-expect-error`, который сам краснеет, когда становится не нужен.
13
+ DIR="${1:-.}"
14
+ # Существование файла проверяется ДО `.`, а не запасной веткой после. Прежняя строка
15
+ # `. файл 2>/dev/null || запасной_вариант` выглядела страховкой и ею не была: под `sh` (dash)
16
+ # неудачный `.` завершает скрипт немедленно, и ветка после `||` не выполняется никогда; под
17
+ # `bash`, которым гейты и запускаются из манифеста, она выполняется, но подставляет только
18
+ # ПЕРЕМЕННУЮ — функции обхода остаются неопределёнными, конвейер печатает пустоту, и проверка
19
+ # выходит с НУЛЁМ. Замерено 2026-09-08 на файле с настоящим нарушением: гейт сказал «чисто».
20
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
21
+ if [ ! -f "$SKIP_LIB" ]; then
22
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
23
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
24
+ exit 2
25
+ fi
26
+ . "$SKIP_LIB"
27
+
28
+ OUT=""
29
+ add() { [ -z "$1" ] || OUT="$OUT$1
30
+ "; }
31
+
32
+ # --- безадресные подавления в коде -------------------------------------------
33
+ # `# noqa` без двоеточия с кодом; `type: ignore` без [кода]; файловые выключатели целиком.
34
+ add "$(grep -rnE '(^|[^A-Za-z0-9_])#[[:space:]]*(noqa|type:[[:space:]]*ignore)([[:space:]]*$|[[:space:]]+[^:[])' \
35
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
36
+ add "$(grep -rnE '#[[:space:]]*(flake8:[[:space:]]*noqa[[:space:]]*$|ruff:[[:space:]]*noqa[[:space:]]*$|mypy:[[:space:]]*ignore-errors|pylint:[[:space:]]*(skip-file|disable=all))' \
37
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
38
+
39
+ # eslint-disable без имени правила: голая директива гасит ВСЁ в файле или на строке.
40
+ #
41
+ # ВЕДУЩИЙ КОММЕНТАРИЙ ОБЯЗАТЕЛЕН — «(//|/\*|\*)[^\"']*». Замер по пятнадцати чужим
42
+ # репозиториям показал целый класс ложных: строка ПРО подавление внутри кавычек
43
+ # («title: 'No @ts-ignore'», текст правила в наборе тестов) читалась как подавление.
44
+ # Директива живёт в комментарии; упоминание в строковом литерале — не она.
45
+ add "$(grep -rnE '(//|/\*|\*)[^\"'"'"']*eslint-disable(-next-line|-line)?[[:space:]]*(\*/|$|--)' \
46
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
47
+
48
+ # @ts-ignore и @ts-nocheck. Первый гасит ошибку молча и остаётся, когда ошибки уже нет;
49
+ # @ts-expect-error на его месте краснеет, как только становится лишним.
50
+ add "$(grep -rnE '(//|/\*|\*)[^\"'"'"']*@ts-(ignore|nocheck)' \
51
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
52
+
53
+ # Остальные экосистемы — только безадресная форма.
54
+ add "$(grep -rnE '#!?\[allow\((warnings|unused)\)\]|@SuppressWarnings\((\{[^}]*)?"all"|//[[:space:]]*nolint[[:space:]]*($|//)|rubocop:disable[[:space:]]+all|shellcheck[[:space:]]+disable[[:space:]]*($|[^=])' \
55
+ $(skip_grep) $(include_code) "$DIR" 2>/dev/null)"
56
+
57
+ # --- обход самого конвейера ---------------------------------------------------
58
+ # `--no-verify` пропускает хуки коммита. В committed-скрипте или конфиге конвейера это не
59
+ # настройка, а выключенная защита: смотрим шире кода — в оболочечные скрипты и конфиги.
60
+ # `-e`, а НЕ `--`. Двойное тире завершает разбор опций, и все флаги `--include` после него
61
+ # grep считает именами файлов: проверка читала весь репозиторий вместо оболочечных скриптов и
62
+ # конфигов. Нашлось замером — в выдаче оказались CHANGELOG чужих проектов.
63
+ add "$(grep -rnE -e '--no-verify' $(skip_grep) \
64
+ --include=*.sh --include=*.yml --include=*.yaml --include=*.json --include=*.toml \
65
+ --include=*.mk --include=Makefile "$DIR" 2>/dev/null)"
66
+
67
+ # Собственное определение записи — не подавление, а перечень того, что ищется. Без этого
68
+ # проверка находит сама себя в любом проекте, куда её поставили: `check.sh` содержит все
69
+ # образцы разом. Тот же класс, что образцы red/green, только файл другой.
70
+ SELFDIR="$(basename "$(dirname "$0")")"
71
+ LEFT="$(printf '%s' "$OUT" | grep -v '^$' | own_samples_filter "$DIR" \
72
+ | grep -vE "(^|/)$SELFDIR/(check\.sh|README\.md)")"
73
+ # Сгенерированные файлы правят не руками: подавление в них поставил инструмент.
74
+ LEFT="$(printf '%s\n' "$LEFT" | while IFS= read -r L; do
75
+ F="${L%%:*}"
76
+ [ -n "$F" ] || continue
77
+ is_generated "$F" || printf '%s\n' "$L"
78
+ done | grep -v '^$')"
79
+
80
+ [ -z "$LEFT" ] && exit 0
81
+ printf '%s\n' "$LEFT"
82
+ echo " почини: назови, ЧТО подавляешь и зачем — «# noqa: E501», «eslint-disable-next-line no-console -- причина»,"
83
+ echo " «@ts-expect-error» вместо «@ts-ignore». Безадресное подавление гасит и то, что сломается завтра."
84
+ exit 1
@@ -0,0 +1,15 @@
1
+ intent: проверка выключается точечно и с причиной, а не целиком
2
+ intent_en: a check is suppressed narrowly and with a reason, never wholesale
3
+
4
+ # Применимо везде, где есть код: подавление проверки — приём, а не язык. Список маркеров
5
+ # покрывает python, typescript, javascript, go, rust, java, ruby и оболочку сразу.
6
+ trigger:
7
+ always: true
8
+
9
+ recipes:
10
+ any: bash {gate}/check.sh {dir}
11
+
12
+ proof: incidents/README.md, 2026-09-06 «замер по пятнадцати чужим репозиториям» — 22 находки
13
+ на 20 774 файлах, из них 19 настоящих: `@ts-nocheck` на боевой странице в kodus-ai,
14
+ сломанный `# noqa E501` без двоеточия в pr-agent (flake8 читает его как безадресный),
15
+ `/** @ts-ignore */` и `eslint-disable-line` без имени правила в repolinter
@@ -0,0 +1,8 @@
1
+ // @ts-expect-error -- у gateway нет типов; строка покраснеет сама, когда они появятся
2
+ import { pay } from "./gateway";
3
+
4
+ export async function checkout(cart: unknown) {
5
+ // eslint-disable-next-line no-console -- это программа командной строки, вывод и есть интерфейс
6
+ console.log(cart);
7
+ return pay(cart as never);
8
+ }
@@ -0,0 +1,6 @@
1
+ import json # noqa: F401 — реэкспорт для обратной совместимости, убрать в 2.0
2
+ from decimal import Decimal
3
+
4
+
5
+ def total(order): # type: ignore[no-any-return] — форма заказа приходит из внешнего API
6
+ return sum(Decimal(i["price"]) for i in order["items"])
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ git commit -m "release"
@@ -0,0 +1,9 @@
1
+ /* eslint-disable */
2
+ // @ts-ignore
3
+ import { pay } from "./gateway";
4
+
5
+ export async function checkout(cart: unknown) {
6
+ // eslint-disable-next-line
7
+ console.log(cart);
8
+ return pay(cart as never);
9
+ }
@@ -0,0 +1,6 @@
1
+ import json # noqa
2
+ from decimal import Decimal
3
+
4
+
5
+ def total(order): # type: ignore
6
+ return sum(Decimal(i["price"]) for i in order["items"])
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ git commit -m "release" --no-verify