agent-quality-kit 0.8.0 → 0.10.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 (76) hide show
  1. package/README.md +154 -12
  2. package/README.ru.md +185 -27
  3. package/kit/docs/ai/index.md +1 -0
  4. package/kit/docs/ai/project-baseline.md +14 -0
  5. package/kit/docs/api-e2e.md +214 -0
  6. package/kit/docs/ready-made-rules.md +188 -0
  7. package/kit/gates/README.md +22 -0
  8. package/kit/gates/api-contract-has-arbiter/README.md +63 -0
  9. package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
  10. package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
  11. package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
  12. package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
  13. package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
  14. package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
  15. package/kit/gates/ci-actually-fails/check.sh +9 -1
  16. package/kit/gates/color-from-token/check.sh +5 -1
  17. package/kit/gates/commit-explains-itself/check.sh +15 -0
  18. package/kit/gates/complexity-limit/red/deep.go +17 -0
  19. package/kit/gates/complexity-limit/red/deep.rs +17 -0
  20. package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
  21. package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
  22. package/kit/gates/lesson-has-outcome/check.sh +5 -1
  23. package/kit/gates/mcp-server-resolves/README.md +62 -0
  24. package/kit/gates/mcp-server-resolves/check.sh +110 -0
  25. package/kit/gates/mcp-server-resolves/gate.yml +18 -0
  26. package/kit/gates/mcp-server-resolves/green/.mcp.json +20 -0
  27. package/kit/gates/mcp-server-resolves/red/.mcp.json +16 -0
  28. package/kit/gates/protection-not-removed/README.md +67 -0
  29. package/kit/gates/protection-not-removed/check.sh +92 -0
  30. package/kit/gates/protection-not-removed/gate.yml +10 -0
  31. package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
  32. package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
  33. package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
  34. package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
  35. package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
  36. package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
  37. package/kit/gates/todo-without-task/red/later.go +6 -0
  38. package/kit/gates/todo-without-task/red/later.rs +4 -0
  39. package/llms.txt +25 -4
  40. package/package.json +3 -6
  41. package/tool/commands/context.mjs +37 -6
  42. package/tool/commands/doctor.mjs +139 -16
  43. package/tool/commands/probe.mjs +228 -0
  44. package/tool/commands/project.mjs +18 -2
  45. package/tool/commands/prove.mjs +1 -0
  46. package/tool/commands/vitals.mjs +167 -0
  47. package/tool/i18n/en-docs.mjs +48 -0
  48. package/tool/i18n/en-gates.mjs +309 -0
  49. package/tool/i18n/en.mjs +26 -279
  50. package/tool/i18n/index.mjs +36 -3
  51. package/tool/i18n/ru-docs.mjs +48 -0
  52. package/tool/i18n/ru-gates.mjs +311 -0
  53. package/tool/i18n/ru.mjs +26 -278
  54. package/tool/lib/banner.mjs +59 -0
  55. package/tool/lib/brief.mjs +192 -0
  56. package/tool/lib/cadence.mjs +57 -0
  57. package/tool/lib/core.mjs +3 -0
  58. package/tool/lib/history.mjs +82 -0
  59. package/tool/lib/manifest.mjs +146 -15
  60. package/tool/lib/prove.mjs +11 -1
  61. package/tool/lib/repo.mjs +43 -3
  62. package/tool/program.mjs +33 -0
  63. package/tool/selfcheck/smoke/_fixture.mjs +89 -0
  64. package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
  65. package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
  66. package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
  67. package/tool/selfcheck/smoke.sh +528 -6
  68. package/tool/selfcheck/units-banner.mjs +65 -0
  69. package/tool/selfcheck/units-brief.mjs +97 -0
  70. package/tool/selfcheck/units-cadence.mjs +69 -0
  71. package/tool/selfcheck/units-context.mjs +3 -1
  72. package/tool/selfcheck/units-level.mjs +147 -1
  73. package/tool/selfcheck/units-probe.mjs +100 -0
  74. package/tool/selfcheck/units-repo.mjs +164 -0
  75. package/tool/selfcheck/units-vitals.mjs +81 -0
  76. package/tool/selfcheck/units.mjs +3 -75
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env sh
2
+ # Спецификация API лежит — а держит ли её кто-нибудь.
3
+ #
4
+ # ЗАЧЕМ ИМЕННО ЭТО. `openapi.yaml` — обещание чужому коду: вот поля, вот типы, вот что
5
+ # обязательно. Обещание, которое никто не сверяет с сервером, расходится с ним молча и
6
+ # обнаруживается у потребителя. Это наш центральный класс, только на уровне API.
7
+ #
8
+ # ЗАМЕР, ИЗ КОТОРОГО ВЗЯЛАСЬ ЗАПИСЬ (2026-09-09). Стенд: сервер врёт в каждом поле ответа —
9
+ # `id` строкой вместо числа, обязательного `email` нет вовсе, `created_at` не дата. Пятисоток
10
+ # при этом нет, коды ответа честные. Кто что сказал:
11
+ # spectral (spectral:oas) -> код 0, три замечания: нет контакта, описания, тегов
12
+ # schemathesis, умолчания -> код 1, три нарушения схемы в ответах
13
+ # schemathesis -c not_a_server_error -> код 0, «18 из 18 прошли»
14
+ # Отсюда обе красные ветки этой проверки: держать спецификацию некому — и держать поручено
15
+ # тому, кто по построению не может покраснеть.
16
+ DIR="${1:-.}"
17
+ SKIP_LIB="$(dirname "$0")/../_skip.sh"
18
+ if [ ! -f "$SKIP_LIB" ]; then
19
+ echo "рядом с проверкой нет _skip.sh — обход не собран, проверка не состоялась"
20
+ echo " почини: скопируй гейт вместе с файлом kit/gates/_skip.sh, он общий на весь каталог"
21
+ exit 2
22
+ fi
23
+ . "$SKIP_LIB"
24
+
25
+ # --- есть ли договор ----------------------------------------------------------
26
+ # По имени файла, а не по расположению: спецификацию кладут в корень, в `docs/`, рядом с
27
+ # приложением. Расширение обязательно разбираемое — `openapi.md` это рассказ о договоре.
28
+ # `-print` в конце ОБЯЗАТЕЛЕН. Без него действие по умолчанию применяется ко всему выражению,
29
+ # и ветка `-prune -o` печатает сами обойдённые каталоги: в списке спецификаций оказывались
30
+ # `./.git` и `./.aqk`. Найдено аудитом фич 2026-09-09, образцами не ловилось.
31
+ SPECS=$(find "$DIR" $(skip_find) -type f \
32
+ \( -iname 'openapi*.yaml' -o -iname 'openapi*.yml' -o -iname 'openapi*.json' \
33
+ -o -iname 'swagger*.yaml' -o -iname 'swagger*.yml' -o -iname 'swagger*.json' \
34
+ -o -iname 'asyncapi*.yaml' -o -iname 'asyncapi*.yml' -o -iname 'asyncapi*.json' \) \
35
+ -print 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$')
36
+ [ -z "$SPECS" ] && { echo "спецификации API здесь нет — эта проверка не про тебя"; exit 0; }
37
+
38
+ # --- кто её держит ------------------------------------------------------------
39
+ # Четыре семьи держателей, и они отвечают на РАЗНЫЕ вопросы — подробности в README:
40
+ # сверка с сервером schemathesis, dredd, portman, newman, pact — «документ не врёт»
41
+ # линт документа spectral, redocly, vacuum, openapi-spec-validator, swagger-cli
42
+ # ломающие правки oasdiff — «вчерашний клиент переживёт сегодняшний выпуск»
43
+ # потребитель openapi-typescript, orval, oapi-codegen, openapi-generator, kubb —
44
+ # типы порождены договором, и расхождение ломает сборку
45
+ HOLDERS='schemathesis|dredd|portman|newman[[:space:]]+run|pact-broker|pact-verifier|can-i-deploy|spectral[[:space:]]+lint|redocly[[:space:]]+(lint|bundle)|vacuum[[:space:]]+(lint|report|html-report)|openapi-spec-validator|swagger-cli|oasdiff|openapi-typescript|orval|oapi-codegen|openapi-generator|kubb'
46
+ # Ищем в том, что ЗАПУСКАЮТ: конвейер, оболочечные скрипты, сборочные файлы, объявления пакета
47
+ # и манифест самого комплекта. Не в коде: упоминание инструмента в исходнике — не его запуск.
48
+ FOUND=$(grep -rnE "$HOLDERS" $(skip_grep) \
49
+ --include=*.yml --include=*.yaml --include=*.sh --include=*.json --include=*.toml \
50
+ --include=*.ini --include=*.cfg --include=*.mk --include=Makefile --include=Justfile \
51
+ --include=justfile --include=Taskfile.yml --include=*.gradle --include=Jenkinsfile \
52
+ "$DIR" 2>/dev/null | own_samples_filter "$DIR" | grep -v '^$')
53
+
54
+ # ОПРЕДЕЛЕНИЕ ЗАПИСИ КАТАЛОГА — НЕ НАХОДКА, и это касается не только своей записи. Соседняя
55
+ # запись `ci-actually-fails` держит в своём `check.sh` строку со списком запускалок, где
56
+ # перечислены все инструменты про API разом. Проверка находила её и решала, что спецификацию
57
+ # кто-то держит: договор не держал никто, а гейт был ЗЕЛЁНЫМ.
58
+ #
59
+ # Найдено аудитом фич 2026-09-09 — прогоном на настоящем проекте, а не образцами: красный и
60
+ # зелёный образцы лежат по одному, а в проекте записи стоят рядом. Признак определения взят
61
+ # самый надёжный: в той же папке лежит `gate.yml`.
62
+ FOUND=$(printf '%s\n' "$FOUND" | while IFS= read -r L; do
63
+ F="${L%%:*}"
64
+ [ -n "$F" ] || continue
65
+ [ -f "$(dirname "$F")/gate.yml" ] && continue
66
+ printf '%s\n' "$L"
67
+ done | grep -v '^$')
68
+
69
+ if [ -z "$FOUND" ]; then
70
+ printf '%s\n' "$SPECS" | sed 's/$/: спецификацию не держит ни одна команда/'
71
+ echo " почини: заведи арбитра, который краснеет, когда сервер ушёл от договора —"
72
+ echo " «schemathesis run openapi.yaml --url <адрес>» в конвейере. Линтер спецификации"
73
+ echo " (spectral, redocly, vacuum) этого НЕ заменяет: он читает документ, а не сервер,"
74
+ echo " и на сервере, который врёт в каждом поле, остаётся зелёным."
75
+ exit 1
76
+ fi
77
+
78
+ # --- держит, но провалиться не может -------------------------------------------
79
+ # `--checks not_a_server_error` (и короткое `-c`) оставляет от арбитра одну проверку: «не
80
+ # пятисотка». На замере выше это ровно код 0 при сервере, который врёт в каждом поле. Умолчание
81
+ # у schemathesis — ВСЕ проверки разом, поэтому такое сужение это не настройка, а отключение.
82
+ # КАК ОБЕЗВРЕЖЕН АРБИТР — вопрос к ОБЪЯВЛЕНИЮ запуска, а не к любому файлу, где эта строка
83
+ # встретилась. Граница взята не отсюда: `ci-actually-fails` уже проводит её теми же словами —
84
+ # «провал, погашенный внутри скрипта, это логика скрипта, а не конфиг конвейера». Внутри
85
+ # оболочечного скрипта вердикт решают `set -e`, `trap` и явный `exit`, и по одной строке о нём
86
+ # судить нельзя.
87
+ #
88
+ # Поймано на себе в тот же день: наш `smoke.sh` держит образцы нарушений в heredoc — и новая
89
+ # запись покраснела на собственном наборе проверок. Одна из трёх находок была вообще в
90
+ # КОММЕНТАРИИ, поясняющем замер; отсюда же `drop_comments`.
91
+ RUNS=$(printf '%s\n' "$FOUND" | grep -E '\.(ya?ml|toml|json|mk|gradle):|/(Makefile|Justfile|justfile|Jenkinsfile):' | drop_comments)
92
+
93
+ NARROW=$(printf '%s\n' "$RUNS" | grep -E '(-c|--checks)[[:space:]=]+not_a_server_error([[:space:]]|$|")')
94
+
95
+ # Вторая форма того же: `oasdiff breaking` БЕЗ `--fail-on`. Замер 2026-09-09 на паре
96
+ # спецификаций, где из ответа убрано обязательное поле: инструмент печатает
97
+ # «1 changes: 1 error … removed the required property `email`» — и выходит с НУЛЁМ. С
98
+ # `--fail-on ERR` на той же паре код 1, а на паре без ломающих изменений снова 0.
99
+ # Вывод громкий и красный на вид, конвейер зелёный — самая коварная форма молчания.
100
+ #
101
+ # Форма `uses: oasdiff/oasdiff-action/...` не считается находкой: действие роняет прогон само.
102
+ # Подкоманда `changelog` тоже: она затем и нужна, чтобы напечатать, а не уронить.
103
+ LOUD=$(printf '%s\n' "$RUNS" | grep -E 'oasdiff[[:space:]]+breaking' | grep -v 'oasdiff-action' | grep -v -- '--fail-on')
104
+
105
+ if [ -n "$NARROW$LOUD" ]; then
106
+ [ -z "$NARROW" ] || printf '%s\n' "$NARROW" | sed 's/$/ <- арбитр сужен до «не пятисотка»/'
107
+ [ -z "$LOUD" ] || printf '%s\n' "$LOUD" | sed 's/$/ <- печатает ломающие изменения и выходит с нулём/'
108
+ [ -z "$NARROW" ] || {
109
+ echo " почини: убери «--checks not_a_server_error» — у schemathesis по умолчанию включены"
110
+ echo " ВСЕ проверки, включая response_schema_conformance. Сужение до одной оставляет"
111
+ echo " зелёный прогон на сервере, который врёт в каждом поле ответа."
112
+ }
113
+ [ -z "$LOUD" ] || echo " почини: добавь «--fail-on ERR» — без него oasdiff печатает находки и выходит с нулём."
114
+ echo " провал, погашенный «|| true» или «continue-on-error», — это ci-actually-fails."
115
+ exit 1
116
+ fi
117
+ exit 0
@@ -0,0 +1,15 @@
1
+ intent: у спецификации API есть арбитр, и он может провалиться
2
+ intent_en: the API specification has an arbiter, and that arbiter can actually fail
3
+
4
+ # Только там, где договор с чужим кодом уже есть. Библиотеке, консольной программе и монолиту
5
+ # без внешнего интерфейса эта запись не нужна: показанной не тому записи не верят.
6
+ trigger:
7
+ has_api_spec: true
8
+
9
+ recipes:
10
+ any: bash {gate}/check.sh {dir}
11
+
12
+ proof: incidents/README.md, 2026-09-09 «сузили арбитра до самой слабой проверки» — на стенде,
13
+ где сервер врёт в каждом поле ответа, schemathesis с умолчаниями даёт код 1 и три нарушения
14
+ схемы, он же с `-c not_a_server_error` — код 0 и «18 из 18 прошли»; spectral на той же
15
+ спецификации тоже код 0 и жалобы на отсутствие тегов
@@ -0,0 +1,12 @@
1
+ # Арбитр назван и может провалиться. Оба слова важны:
2
+ # schemathesis с умолчаниями сверяет ОТВЕТЫ сервера со схемой, а не только отсутствие пятисоток;
3
+ # oasdiff без «--fail-on ERR» печатает ломающие изменения и выходит с нулём — замерено.
4
+ name: ci
5
+ on: [push]
6
+ jobs:
7
+ contract:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - run: schemathesis run openapi.yaml --url http://localhost:8000
12
+ - run: oasdiff breaking base.yaml openapi.yaml --fail-on ERR
@@ -0,0 +1,18 @@
1
+ openapi: 3.0.3
2
+ info: { title: Заказы, version: 1.0.0 }
3
+ paths:
4
+ /users/{id}:
5
+ get:
6
+ parameters:
7
+ - { name: id, in: path, required: true, schema: { type: integer } }
8
+ responses:
9
+ '200':
10
+ description: Пользователь
11
+ content:
12
+ application/json:
13
+ schema:
14
+ type: object
15
+ required: [id, email]
16
+ properties:
17
+ id: { type: integer }
18
+ email: { type: string }
@@ -0,0 +1,11 @@
1
+ # Договор лежит, конвейер есть — и ни одна команда договора не касается. Спецификация здесь
2
+ # документация, а не контракт: сервер уходит от неё молча.
3
+ name: ci
4
+ on: [push]
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - run: pytest
11
+ - run: ruff check .
@@ -0,0 +1,18 @@
1
+ openapi: 3.0.3
2
+ info: { title: Заказы, version: 1.0.0 }
3
+ paths:
4
+ /users/{id}:
5
+ get:
6
+ parameters:
7
+ - { name: id, in: path, required: true, schema: { type: integer } }
8
+ responses:
9
+ '200':
10
+ description: Пользователь
11
+ content:
12
+ application/json:
13
+ schema:
14
+ type: object
15
+ required: [id, email]
16
+ properties:
17
+ id: { type: integer }
18
+ email: { type: string }
@@ -14,7 +14,15 @@ CI=$(find "$DIR/.github/workflows" "$DIR/.gitlab-ci.yml" "$DIR/.circleci" "$DIR/
14
14
  # Что считается ПРОВЕРКОЙ. Список намеренно закрытый: маскировка бывает законной — необязательная
15
15
  # выгрузка отчёта, публикация артефакта, уведомление. Красить всякий `continue-on-error` значит
16
16
  # получить гейт, который выключат первым. Красим только гашение того, что выносит вердикт.
17
- RUNNERS='aqk|doctor --run|pytest|tox|nox|unittest|jest|vitest|mocha|jasmine|karma|playwright|cypress|eslint|tsc|ruff|flake8|pylint|mypy|pyright|bandit|semgrep|gitleaks|trivy|rubocop|golangci-lint|golint|govet|go vet|go test|staticcheck|shellcheck|hadolint|actionlint|codespell|reviewdog|cargo test|cargo clippy|mvn|gradle|phpstan|psalm|npm test|npm run (test|lint|check|typecheck)|yarn (test|lint)|pnpm (test|lint)|make (test|lint|check)'
17
+ # ПОЧЕМУ ЗДЕСЬ ЕСТЬ ИНСТРУМЕНТЫ ПРО API И ПОЧЕМУ НЕ ВСЕ ИХ ИМЕНА ЦЕЛИКОМ.
18
+ # Замер 2026-09-09: три шага — фаззер спецификации, детектор ломающих изменений и сверка с
19
+ # ожиданиями потребителей, — все три под `continue-on-error: true`, и эта проверка сказала
20
+ # «чисто», код 0. Список знал `pytest` и `eslint` и не знал ни одного инструмента про API,
21
+ # то есть самый дорогой класс проверок проходил как строка в логе.
22
+ # Имена сокращены до подкоманды там, где слово обиходное: `vacuum` без `lint` совпадает с
23
+ # обслуживанием базы (`psql -c 'VACUUM ANALYZE'`), а шаг обслуживания имеет полное право быть
24
+ # прощающим. Ложный красный дороже пропуска: гейт, который врёт, выключают целиком.
25
+ RUNNERS='aqk|doctor --run|pytest|tox|nox|unittest|jest|vitest|mocha|jasmine|karma|playwright|cypress|eslint|tsc|ruff|flake8|pylint|mypy|pyright|bandit|semgrep|gitleaks|trivy|rubocop|golangci-lint|golint|govet|go vet|go test|staticcheck|shellcheck|hadolint|actionlint|codespell|reviewdog|cargo test|cargo clippy|mvn|gradle|phpstan|psalm|npm test|npm run (test|lint|check|typecheck)|yarn (test|lint)|pnpm (test|lint)|make (test|lint|check)|schemathesis|dredd|oasdiff|spectral lint|redocly (lint|bundle)|vacuum (lint|report|html-report)|pact-broker|pact-verifier|can-i-deploy|portman|newman run|manage.py spectacular'
18
26
 
19
27
  # Закрытый список не поспевает: замер по чужим репозиториям нашёл шаг «Run reviewdog
20
28
  # (github-pr-check)» под `continue-on-error: true`, и ни одно имя из списка в нём не звучало.
@@ -67,7 +67,11 @@ HITS=$(find "$DIR" $(skip_find "$DIR") -type f -print 2>/dev/null \
67
67
  # Якорь ссылки «#defining-entry-points» начинается с «#def», за которым идёт «i»: по
68
68
  # прежнему правилу это был цвет. Замер 2026-09-08 по шести чужим репозиториям: в uv
69
69
  # ложным оказалось именно это, в docs/js/extra.js.
70
- if (line ~ /#[0-9a-fA-F]{8}([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F]{6}([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F]{3}([^0-9a-zA-Z_-]|$)/)
70
+ # Цвет выписан поразрядно: часть сборок mawk повторители не понимает (в node:22-slim
71
+ # это mawk 1.3.4 20200120), а какая сборка у читателя — мы не знаем
72
+ # {3},{6},{8}, и это условие не срабатывало никогда — гейт молчал на красном образце.
73
+ # Поймано сборкой docker-образа 2026-09-08, а не прогоном: на хосте gawk.
74
+ if (line ~ /#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)|#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]([^0-9a-zA-Z_-]|$)/)
71
75
  print FILENAME ":" FNR ":" $0
72
76
  }
73
77
  ' 2>/dev/null \
@@ -65,6 +65,21 @@ else
65
65
  [ -z "$LESSONS" ] && LESSONS="incidents"
66
66
  case "$LESSONS" in http*) LESSONS="" ;; esac # journal по адресу, а не путём — не применимо
67
67
  if [ -n "$LESSONS" ]; then
68
+ # СОСТАВ КОММИТА СЧИТАЕТСЯ ДИФФОМ С РОДИТЕЛЕМ, а родителя может не быть видно. При разборе
69
+ # предложения изменений GitHub выкладывает синтетический коммит слияния мелким клоном: сам
70
+ # коммит автора (`HEAD^2`) в клоне есть, а его родителя нет. `git show --name-only` в этом
71
+ # случае считает коммит КОРНЕВЫМ и выдаёт всё дерево — освобождение «трогает только журнал»
72
+ # переставало срабатывать МОЛЧА, и гейт требовал отчёт там, где не должен.
73
+ #
74
+ # Поймано конвейером на записи в собственный журнал 2026-09-09; опыт показал границу точно:
75
+ # глубина 2 — красный, глубина 3 — чисто. Молчать здесь нельзя: пропуск называет себя, иначе
76
+ # выключенная проверка неотличима от работающей.
77
+ if ! (cd "$DIR" && git rev-parse -q --verify "$REF^" >/dev/null 2>&1); then
78
+ echo "мелкий клон: у коммита не видно родителя — состав не определить, проверка пропущена"
79
+ echo " чтобы она работала на предложении изменений:"
80
+ echo " actions/checkout@v4 с fetch-depth: 3 (слияние, его родитель и родитель родителя)"
81
+ exit 0
82
+ fi
68
83
  FILES=$(cd "$DIR" && git show --pretty=format: --name-only "$REF" 2>/dev/null | grep -v '^$')
69
84
  if [ -n "$FILES" ]; then
70
85
  OUTSIDE=$(printf '%s\n' "$FILES" | grep -v "^$LESSONS/" | grep -v "^$LESSONS\$")
@@ -0,0 +1,17 @@
1
+ package svc
2
+
3
+ func Route(a, b, c, d int) int {
4
+ if a > 0 {
5
+ if b > 0 {
6
+ for i := 0; i < c; i++ {
7
+ switch d {
8
+ case 1:
9
+ if a > b {
10
+ return 1
11
+ }
12
+ }
13
+ }
14
+ }
15
+ }
16
+ return 0
17
+ }
@@ -0,0 +1,17 @@
1
+ pub fn route(a: i32, b: i32, c: i32, d: i32) -> i32 {
2
+ if a > 0 {
3
+ if b > 0 {
4
+ for i in 0..c {
5
+ match d {
6
+ 1 => {
7
+ if a > b {
8
+ return 1;
9
+ }
10
+ }
11
+ _ => {}
12
+ }
13
+ }
14
+ }
15
+ }
16
+ 0
17
+ }
@@ -0,0 +1,5 @@
1
+ package svc
2
+
3
+ // Голый nolint гасит ВСЁ на строке: адреса у подавления нет.
4
+ //nolint
5
+ func Risky() error { return nil }
@@ -0,0 +1,3 @@
1
+ // allow(warnings) гасит всё разом: это «выключено всё», а не «выключено вот это».
2
+ #[allow(warnings)]
3
+ pub fn risky() {}
@@ -30,7 +30,11 @@ FILES=$(find "$DIR/$LESSONS" -type f -name '*.md' 2>/dev/null)
30
30
  # старые записи несут её строкой ">", новые — абзацем "**Вывод.**"; оба варианта законны.
31
31
  printf '%s\n' $FILES | while read -r F; do
32
32
  awk -v FILE="$F" '
33
- /^## [0-9]{4}-[0-9]{2}-[0-9]{2}/ { flush(); title = $0; sub(/^## /, "", title); is_entry = 1; has_mark = 0; next }
33
+ # Дата выписана поразрядно, а не как [0-9]{4}: часть сборок mawk повторители не понимает
34
+ # повторители в регулярках, и такая строка не совпадала НИКОГДА. Гейт молча выходил с нулём
35
+ # на собственном красном образце. Поймано сборкой образа 2026-09-08: на хосте gawk, в образе
36
+ # mawk, и приёмка каталога внутри образа отклонила две записи из двадцати.
37
+ /^## [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ { flush(); title = $0; sub(/^## /, "", title); is_entry = 1; has_mark = 0; next }
34
38
  /^## / { flush(); is_entry = 0; next }
35
39
  (/✅/ || /🔧/ || /📜/ || /👤/) { has_mark = 1 }
36
40
  END { flush() }
@@ -0,0 +1,62 @@
1
+ # mcp-server-resolves
2
+
3
+ **Что ловит.** Объявленный MCP-сервер, которого на деле нет: команда указывает на несуществующий
4
+ файл, либо версия не закреплена и завтра приедет другая.
5
+
6
+ **Почему это отдельная запись.** Сервер, который не поднялся, агенту **никак не виден**: у него
7
+ просто нет этих инструментов, и он молча работает без них. Ни ошибки, ни строки в журнале.
8
+ Это тот же класс, что хук с опечаткой в имени события: настройка выглядит как возможность и
9
+ возможностью не является.
10
+
11
+ ## Шишка, из которой она выросла
12
+
13
+ 2026-09-08. В проект записали настройку `chrome-devtools-mcp` — выглядела безупречно:
14
+
15
+ ```json
16
+ "chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@1.9.0"] }
17
+ ```
18
+
19
+ Сервер был мёртв. Puppeteer подхватывал Windows-овый Chrome из `/mnt/c/` и падал с
20
+ `Target closed`. Узнали только потому, что запустили руками и прочитали журнал — обычно так не
21
+ делают: записали конфиг, увидели «сохранено», пошли дальше.
22
+
23
+ ## Замер
24
+
25
+ Двадцать настоящих настроек `.mcp.json`, взятых поиском по GitHub 2026-09-08.
26
+
27
+ | | |
28
+ |---|---|
29
+ | конфигов проверено | 20 |
30
+ | покраснело | 9 |
31
+ | находок | 14 |
32
+ | ложных срабатываний | **0** |
33
+
34
+ Все находки — чужие серверы, которые тянутся незакреплёнными при каждом запуске:
35
+ `@modelcontextprotocol/server-github`, `firecrawl-mcp`, `mcp-server-postgres`, `@upstash/context7-mcp`.
36
+ Серверу с таким доступом «сегодня одна версия, завтра другая» — открытая дверь в цепочке поставок.
37
+
38
+ Замер тут же нашёл и два ложных срабатывания, оба починены до объявления записи рабочей:
39
+
40
+ 1. `npx tsx rag/src/index.ts` — сервер **свой**, лежит в репозитории и уже под версией. Требовать
41
+ закрепить `tsx` значит краснеть на нормальном укладе, а такой гейт выключают в первый день.
42
+ 2. Починка первого съела настоящую находку: `@meridian/docs-mcp` — имя пакета со слэшем, а слэш
43
+ считался признаком локального файла. Видно было только построчным сравнением до и после.
44
+
45
+ ## Готовый аналог
46
+
47
+ Готового аналога нет. Проверено 2026-09-08 поиском: линтеры настроек агента существуют
48
+ (`claudelint`, `agent-lint`), но смотрят на права и хуки, а не на MCP. Ближайшее по смыслу —
49
+ наш собственный `deps-are-pinned`: там то же правило про версии, но для зависимостей сборки,
50
+ и файла `.mcp.json` он не видит.
51
+
52
+ ## Чего НЕ ловит
53
+
54
+ - **Не запускает серверы.** Мёртвый по любой другой причине сервер — как в шишке выше, где путь
55
+ и версия были верны, а падал сам браузер, — эта проверка не увидит. Запуск чужих команд на
56
+ каждый коммит означает скачивание пакетов и минуты ожидания; такую проверку выключают сразу.
57
+ Живое рукопожатие — то, что делают руками при подключении сервера, и это записано в методичке,
58
+ а не в гейте.
59
+ - **Не судит об относительных путях.** `"command": "./bin/server"` не проверяется: рабочий
60
+ каталог сервера задаётся полем `cwd`, и проверить существование без запуска нельзя.
61
+ - **Не знает, что пакет существует в реестре.** Это делает `no-phantom-package`, но только для
62
+ того, что упомянуто в документации.
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env sh
2
+ # Объявленный MCP-сервер, которого на деле нет: команда указывает на файл, которого не
3
+ # существует, либо версия не закреплена и завтра приедет другая.
4
+ #
5
+ # ЗАЧЕМ. Сервер, который не поднялся, агенту НИКАК не виден: у него просто нет этих
6
+ # инструментов, и он молча работает без них. Ни ошибки, ни строки в журнале — та же тишина,
7
+ # что у хука с опечаткой в имени события. Настройка выглядит как возможность и возможностью
8
+ # не является.
9
+ #
10
+ # ПРОВЕРЕНО НА СЕБЕ 2026-09-08. Записали в проект настройку chrome-devtools-mcp, выглядела
11
+ # безупречно. Сервер был мёртв: puppeteer подхватывал Windows-овый Chrome из /mnt/c/ и падал
12
+ # с «Target closed». Узнали только потому, что запустили руками и прочитали журнал; обычно
13
+ # так не делают — записали конфиг, увидели «сохранено», пошли дальше.
14
+ #
15
+ # ЧТО ЭТА ПРОВЕРКА НЕ ДЕЛАЕТ. Она НЕ запускает серверы. Запуск — это исполнение чужих команд
16
+ # на каждый коммит, скачивание пакетов и минуты ожидания; такую проверку выключают в первый
17
+ # день. Здесь ловится только то, что видно без запуска, и этого достаточно для двух самых
18
+ # частых смертей: файла нет и версия плавает.
19
+ DIR="${1:-.}"
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
+ for F in "$DIR/.mcp.json" "$DIR/.cursor/mcp.json" "$DIR/.vscode/mcp.json" "$DIR/.claude/mcp.json"; do
30
+ [ -f "$F" ] || continue
31
+ RES=$(awk -v file="$F" -v dir="$DIR" '
32
+ # Номера строк с командой копятся отдельно от текста: разбор идёт в END по всему файлу
33
+ # одной строкой (иначе минифицированный JSON не разобрать), а находку надо привязать к
34
+ # строке — без неё `--since` не сверит путь с дифом и гейт зазеленеет на чужом дифе.
35
+ /"command"[ \t]*:/ { cline[++ci] = NR }
36
+ { buf = buf $0 " " }
37
+ END {
38
+ n = split(buf, part, /"command"[ \t]*:/)
39
+ for (i = 2; i <= n; i++) {
40
+ chunk = part[i]
41
+ if (!match(chunk, /"[^"]*"/)) continue
42
+ cmd = substr(chunk, RSTART + 1, RLENGTH - 2)
43
+ line = cline[i - 1] ? cline[i - 1] : 1
44
+
45
+ # 1. Абсолютный путь, которого нет. Самая частая смерть у пакетов, чья версия вшита
46
+ # в имя файла: обновились — старый файл удалён, сервер умер, никто не заметил.
47
+ if (substr(cmd, 1, 1) == "/") {
48
+ if (system("test -e \"" cmd "\"") != 0)
49
+ print file ":" line ": команда сервера не существует: " cmd
50
+ continue
51
+ }
52
+
53
+ # 2. Скачивающий запуск без точной версии. `@latest` и голое имя означают «сегодня
54
+ # одно, завтра другое», а для сервера, которому доверен браузер и файлы, это дверь
55
+ # в цепочке поставок. Правило то же, что у нас для зависимостей.
56
+ if (cmd == "npx" || cmd == "uvx" || cmd == "pipx" || cmd == "bunx" || cmd == "pnpm") {
57
+ args = ""
58
+ if (match(chunk, /"args"[ \t]*:[ \t]*\[[^]]*\]/)) args = substr(chunk, RSTART, RLENGTH)
59
+ if (args == "") continue
60
+ if (args ~ /@latest/) {
61
+ print file ":" line ": версия сервера не закреплена (@latest): " cmd
62
+ continue
63
+ }
64
+ # Имя пакета — первый довод, не начинающийся с дефиса. Точная версия пишется как
65
+ # name@1.2.3 (npm) или name==1.2.3 (python); без неё запуск невоспроизводим.
66
+ m = args
67
+ gsub(/"args"[ \t]*:[ \t]*\[/, "", m); gsub(/\]/, "", m)
68
+ k = split(m, tok, ",")
69
+
70
+ # СЕРВЕР ИЗ СВОЕГО РЕПОЗИТОРИЯ — не находка. `npx tsx rag/src/index.ts` запускает
71
+ # СВОЙ файл, а npx здесь только запускалка: код сервера лежит в репозитории и уже
72
+ # под версией — той же, что и весь проект. Требовать закрепить `tsx` значит краснеть
73
+ # на нормальном укладе, а такой гейт выключают в первый день. Найдено замером по
74
+ # двадцати чужим настройкам с GitHub 2026-09-08: две из десяти находок были ровно
75
+ # этим. Тот же класс, что четыре ложных срабатывания на чужом коде до этого.
76
+ local_entry = 0
77
+ for (j = 1; j <= k; j++) {
78
+ t = tok[j]; gsub(/^[ \t"]+|[ \t"]+$/, "", t)
79
+ # Признак локального входа — расширение файла или явно относительный путь.
80
+ # Просто «есть слэш» не годится: `@scope/name` — это имя пакета в npm, и первая
81
+ # версия этой проверки съела настоящую находку `npx @meridian/docs-mcp`. Поймано
82
+ # тем же замером по двадцати чужим настройкам: находок стало восемь вместо девяти,
83
+ # и пропажу было видно только построчным сравнением до и после.
84
+ if (t ~ /\.(ts|js|mjs|cjs|py|rb|sh)$/ || t ~ /^\// || t ~ /^\.\// || t ~ /^\.\.\//) { local_entry = 1; break }
85
+ }
86
+ if (local_entry) continue
87
+
88
+ for (j = 1; j <= k; j++) {
89
+ t = tok[j]; gsub(/^[ \t"]+|[ \t"]+$/, "", t)
90
+ if (t == "" || substr(t, 1, 1) == "-") continue
91
+ if (t ~ /@[0-9]/ || t ~ /==[0-9]/) break
92
+ print file ":" line ": версия сервера не закреплена: " cmd " " t
93
+ break
94
+ }
95
+ }
96
+ }
97
+ }
98
+ ' "$F")
99
+ [ -n "$RES" ] && OUT="${OUT}${RES}
100
+ "
101
+ done
102
+
103
+ OUT=$(printf '%s' "$OUT" | own_samples_filter "$DIR" | sed '/^$/d')
104
+ if [ -n "$OUT" ]; then
105
+ printf '%s\n' "$OUT"
106
+ echo " почини: закрепи точную версию (пакет@1.2.3) и убедись, что файл команды существует."
107
+ echo " мёртвый сервер агенту не виден: он просто работает без этих инструментов и молчит."
108
+ exit 1
109
+ fi
110
+ exit 0
@@ -0,0 +1,18 @@
1
+ intent: объявленный MCP-сервер существует и закреплён по версии — иначе агент молча работает без него
2
+ intent_en: a declared MCP server exists and is version-pinned — otherwise the agent silently works without it
3
+
4
+ # Только там, где агенту подключали внешние инструменты. В проекте без `.mcp.json` проверять
5
+ # нечего, а запись, показанная не тому, стоит доверия всему каталогу.
6
+ trigger:
7
+ has_mcp: true
8
+
9
+ recipes:
10
+ any: bash {gate}/check.sh {dir}
11
+
12
+ samples_for: any
13
+
14
+ proof: incidents/README.md, 2026-09-08 «конфиг был написан верно, а сервер мёртв» — в проект
15
+ записали настройку `chrome-devtools-mcp`, выглядела безупречно; сервер падал с «Target closed»,
16
+ потому что puppeteer подхватывал Windows-овый Chrome из `/mnt/c/`. Узнали только запуском
17
+ руками и чтением журнала: агенту мёртвый сервер не виден вовсе — у него просто нет этих
18
+ инструментов, и он молча работает без них
@@ -0,0 +1,20 @@
1
+ {
2
+ "mcpServers": {
3
+ "browser": {
4
+ "command": "npx",
5
+ "args": ["-y", "chrome-devtools-mcp@1.9.0"]
6
+ },
7
+ "search": {
8
+ "command": "uvx",
9
+ "args": ["some-search-mcp==0.4.1"]
10
+ },
11
+ "local": {
12
+ "command": "node",
13
+ "args": ["scripts/mcp-server.mjs"]
14
+ },
15
+ "own-code": {
16
+ "command": "npx",
17
+ "args": ["tsx", "rag/src/index.ts"]
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "mcpServers": {
3
+ "browser": {
4
+ "command": "npx",
5
+ "args": ["-y", "chrome-devtools-mcp@latest"]
6
+ },
7
+ "memory": {
8
+ "command": "/opt/tools/memory-mcp-v0.10.8/bin/memory",
9
+ "args": []
10
+ },
11
+ "search": {
12
+ "command": "npx",
13
+ "args": ["-y", "some-search-mcp"]
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,67 @@
1
+ # protection-not-removed
2
+
3
+ **Набор объявленных гейтов может только расти.** Убрать гейт из `.aqk.yml` можно — но не молча.
4
+
5
+ ## Зачем
6
+
7
+ Комплект ловит агента, когда тот выключает **сигнал**: `# noqa` ловит `gate-not-weakened`,
8
+ `pytest || true` — `ci-actually-fails`, тест, зелёный при любых данных, — `test-has-assertion`.
9
+
10
+ А главный рубильник — сам манифест — не сторожил никто. Замер 2026-09-09 на живой фикстуре:
11
+
12
+ ```console
13
+ $ aqk doctor --run --min 1 # четыре гейта, настоящий секрет в коде
14
+ ✘ secrets-not-in-code код 1
15
+ код: 1
16
+
17
+ $ grep -v "secrets-not-in-code:" .aqk.yml > tmp && mv tmp .aqk.yml
18
+ $ aqk doctor --run --min 1
19
+ Порог AQK-1 пройден.
20
+ код: 0 # секрет на месте, файлы гейта на диске
21
+ ```
22
+
23
+ Ни `doctor`, ни `report --since`, ни `vitals`, ни блок состояния для агента этого не заметили.
24
+ Это `pytest || true` этажом выше. И бьёт сильнее обычного обхода: `# noqa` виден в коде, глаз за
25
+ него цепляется, а удалённая строка — это **отсутствие**, а отсутствие не рецензируют.
26
+
27
+ ## Как устроено
28
+
29
+ Рядом с реестрами долга лежит `gates-declared.txt` — снимок объявленной защиты. Проверка сверяет
30
+ его с `gates:` манифеста:
31
+
32
+ - имя из снимка пропало из манифеста → **красный**;
33
+ - имя в снимке несёт причину после `#` → снятие названо, прогон зелёный, причина печатается;
34
+ - новые гейты дописываются в снимок сами: набор растёт, укоротить его молча нельзя;
35
+ - снимка нет, но он **был в истории git** → красный: удаление снимка снимает и саму проверку.
36
+
37
+ Снять гейт по-прежнему можно. Требуется одно: назвать причину — как `deprecated` обязан нести
38
+ `superseded_by`. Решение остаётся за человеком и перестаёт быть невидимым.
39
+
40
+ ## Готовый аналог
41
+
42
+ **Есть похожие, и они про другое.** [`imbue-ai/ratchets`](https://github.com/imbue-ai/ratchets)
43
+ и [`eslint-seatbelt`](https://www.notion.com/blog/how-we-evolved-our-code-notions-ratcheting-system-using-custom-eslint-rules)
44
+ сторожат **число нарушений**: счётчик по правилу может только уменьшаться.
45
+
46
+ Удаление самого правила они не ловят — и это не догадка, а их документация, проверено
47
+ 2026-09-09: «Counts for rules no longer in the resolved enabled set are kept dormant (no
48
+ cleanup). `ratchets tighten` emits a stderr warning naming each orphan». То есть правило,
49
+ убранное из конфига, оставляет счётчик дремать, печатается предупреждение в stderr — **удаление
50
+ не блокируется и объяснения не требует**.
51
+
52
+ Разница принципиальная: у счётчикового храповика удаление правила выглядит как **снижение до
53
+ нуля**, то есть как успех. Ровно та подмена, против которой написан весь комплект.
54
+
55
+ ## Чего НЕ ловит
56
+
57
+ - **Гейт, оставленный в манифесте, но выхолощенный до `true`.** Имя на месте, снимок доволен.
58
+ Это ловит `gates-are-runnable` (пустая команда) и `prove` (гейт не краснеет на красном
59
+ образце) — но `lint: "true"` пройдёт обе. Отдельной защиты от подмены команды здесь нет.
60
+ - **Причину, написанную для отвода глаз.** «# снят: не нужен» — законная строка. Проверка
61
+ требует, чтобы решение было НАЗВАНО, а не чтобы оно было верным: верность оценивает человек
62
+ в дифе. Это сознательная граница, та же, что у `deprecated`.
63
+ - **Удаление вместе со снимком в одном коммите на свежем клоне.** Свидетель — история git;
64
+ в репозитории без истории (или при `--depth 1` без этого файла) проверка скажет «снимок
65
+ снят заново», а не «удалён».
66
+ - **Гейты, объявленные пустой командой.** Они не считаются объявленными — защиты они и не
67
+ давали.