agent-quality-kit 0.9.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 (57) hide show
  1. package/README.md +69 -10
  2. package/README.ru.md +68 -11
  3. package/kit/docs/ai/index.md +1 -0
  4. package/kit/docs/api-e2e.md +214 -0
  5. package/kit/docs/ready-made-rules.md +85 -0
  6. package/kit/gates/README.md +22 -0
  7. package/kit/gates/api-contract-has-arbiter/README.md +63 -0
  8. package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
  9. package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
  10. package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
  11. package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
  12. package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
  13. package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
  14. package/kit/gates/ci-actually-fails/check.sh +9 -1
  15. package/kit/gates/commit-explains-itself/check.sh +15 -0
  16. package/kit/gates/complexity-limit/red/deep.go +17 -0
  17. package/kit/gates/complexity-limit/red/deep.rs +17 -0
  18. package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
  19. package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
  20. package/kit/gates/protection-not-removed/README.md +67 -0
  21. package/kit/gates/protection-not-removed/check.sh +92 -0
  22. package/kit/gates/protection-not-removed/gate.yml +10 -0
  23. package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
  24. package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
  25. package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
  26. package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
  27. package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
  28. package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
  29. package/kit/gates/todo-without-task/red/later.go +6 -0
  30. package/kit/gates/todo-without-task/red/later.rs +4 -0
  31. package/llms.txt +10 -4
  32. package/package.json +2 -1
  33. package/tool/commands/context.mjs +28 -1
  34. package/tool/commands/doctor.mjs +56 -2
  35. package/tool/commands/probe.mjs +228 -0
  36. package/tool/commands/vitals.mjs +11 -3
  37. package/tool/i18n/en-docs.mjs +8 -0
  38. package/tool/i18n/en-gates.mjs +309 -0
  39. package/tool/i18n/en.mjs +10 -281
  40. package/tool/i18n/ru-docs.mjs +8 -0
  41. package/tool/i18n/ru-gates.mjs +311 -0
  42. package/tool/i18n/ru.mjs +10 -280
  43. package/tool/lib/cadence.mjs +57 -0
  44. package/tool/lib/core.mjs +1 -0
  45. package/tool/lib/history.mjs +82 -0
  46. package/tool/lib/manifest.mjs +1 -1
  47. package/tool/lib/repo.mjs +12 -2
  48. package/tool/program.mjs +7 -0
  49. package/tool/selfcheck/smoke/_fixture.mjs +89 -0
  50. package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
  51. package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
  52. package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
  53. package/tool/selfcheck/smoke.sh +179 -6
  54. package/tool/selfcheck/units-cadence.mjs +69 -0
  55. package/tool/selfcheck/units-probe.mjs +100 -0
  56. package/tool/selfcheck/units-repo.mjs +31 -1
  57. package/tool/selfcheck/units-vitals.mjs +19 -0
@@ -128,6 +128,28 @@ requires: checkwash
128
128
  (`AQK_GATES_STRICT=1`, поднят в нашем конвейере) делает такой пропуск ошибкой: на машине,
129
129
  которая инструменты сама и ставит, «нечем проверить» обязано быть красным.
130
130
 
131
+ ## Образец про секреты обязан быть узнаваем НАМИ и не узнаваем сканерами
132
+
133
+ Красный образец для записи про секреты — правдоподобный ключ. Слишком правдоподобный отправить
134
+ нельзя: защита GitHub от секретов отклоняет push целиком.
135
+
136
+ ```
137
+ remote: —— Stripe API Key ——
138
+ remote: path: kit/gates/secrets-not-in-code/red/leak.go:4
139
+ remote: Push cannot contain secrets
140
+ ```
141
+
142
+ Проверено 2026-09-09: `sk_live_` плюс тридцать четыре знака совпало с настоящим форматом Stripe,
143
+ и ветка не ушла вовсе. Работающий образец короче настоящего ключа: наш гейт узнаёт его по
144
+ префиксу, а чужой сканер по длине и форме — нет.
145
+
146
+ **Правило: образец подбирается так, чтобы краснел НАШ гейт и молчал чужой сканер.** Иначе запись
147
+ нельзя ни отправить, ни принять — и автор узнаёт об этом только на push, потратив работу.
148
+
149
+ **И не цитируй значение в документации.** Запись журнала, процитировавшая образец дословно,
150
+ уронила наш же `secrets-not-in-code` — текст о секрете попал под правило о секретах. В прозе
151
+ нужен рассказ о форме («тот же префикс, вдвое меньше знаков»), а не сама форма.
152
+
131
153
  ## Запись без переносимого рецепта
132
154
 
133
155
  Иногда переносимой проверки быть не может: чтобы понять, вызывают ли функцию, нужен граф
@@ -0,0 +1,63 @@
1
+ # У спецификации API есть арбитр
2
+
3
+ **Намерение.** `openapi.yaml` — это обещание чужому коду: вот поля, вот типы, вот что
4
+ обязательно. Обещание, которое никто не сверяет с сервером, расходится с ним молча и
5
+ обнаруживается у потребителя. Наш центральный класс, только на уровне API.
6
+
7
+ **Какой отказ это поймало.** Стенд 2026-09-09: сервер отдаёт `id` строкой вместо числа,
8
+ обязательного `email` не отдаёт вовсе, `created_at` — не дата. Пятисоток при этом нет, коды
9
+ ответа честные. Кто что сказал на одной и той же паре «спецификация + сервер»:
10
+
11
+ | Инструмент | Код возврата | Что сказал |
12
+ |---|---|---|
13
+ | `spectral` (набор `spectral:oas`) | **0** | нет контакта, нет описания, нет тегов |
14
+ | `schemathesis`, умолчания | **1** | три нарушения схемы в ответах |
15
+ | `schemathesis -c not_a_server_error` | **0** | «18 из 18 прошли» — ни одной находки |
16
+
17
+ Отсюда обе красные ветки. Замер целиком — `incidents/README.md`, 2026-09-09.
18
+
19
+ **Что именно проверяется.** Записи две, и вторая тоньше первой.
20
+
21
+ 1. **Спецификацию не держит ни одна команда.** Файл `openapi*.{yaml,yml,json}` (и `swagger*`,
22
+ `asyncapi*`) в репозитории есть, а ни в конвейере, ни в сборочных файлах, ни в скриптах, ни
23
+ в объявлении пакета нет ни одного инструмента из четырёх семей ниже.
24
+ 2. **Арбитр сужен до «не пятисотка».** `schemathesis --checks not_a_server_error` (или `-c`).
25
+ У `schemathesis` по умолчанию включены **все** проверки, включая
26
+ `response_schema_conformance`; сужение до одной — не настройка, а отключение.
27
+
28
+ Четыре семьи держателей отвечают на **разные** вопросы, и подменять один другим нельзя:
29
+
30
+ | Семья | Инструменты | Вопрос |
31
+ |---|---|---|
32
+ | сверка с сервером | `schemathesis`, `dredd`, `portman`, `newman`, `pact` | документ не врёт? |
33
+ | линт документа | `spectral`, `redocly`, `vacuum`, `openapi-spec-validator`, `swagger-cli` | документ хорошо написан? |
34
+ | ломающие правки | `oasdiff` | вчерашний клиент переживёт сегодняшний выпуск? |
35
+ | потребитель | `openapi-typescript`, `orval`, `oapi-codegen`, `openapi-generator`, `kubb` | типы порождены договором, а не переписаны руками? |
36
+
37
+ **Готовый аналог.** Не нашли, и это проверено. Все четыре семьи предполагают, что их **уже
38
+ запускают**: `spectral` читает документ, `schemathesis` — сервер, `oasdiff` — две версии
39
+ документа. Ни один не задаёт вопрос «а меня вообще кто-нибудь запускает». Это ровно форма
40
+ записи `promise-has-gate`: обещание, у которого не назван сторож.
41
+
42
+ **Чего НЕ ловит.**
43
+
44
+ - **Что арбитр проверяет ПРАВИЛЬНЫЕ вещи.** Один только линтер спецификации считается
45
+ держателем, хотя на сервере, который врёт в каждом поле, он остаётся зелёным. Красить за это
46
+ нельзя: репозиторий, который публикует чужую спецификацию (клиентский SDK, описание чужого
47
+ API), сервером не владеет вовсе. Разбор — `kit/docs/api-e2e.md`.
48
+ - **Провал, погашенный `|| true` или `continue-on-error`.** Это `ci-actually-fails` — там же
49
+ теперь опознаются и инструменты про API.
50
+ - **Обезвреженного арбитра внутри оболочечного скрипта.** Держателя ищем и в `*.sh`, а вопрос
51
+ «может ли он провалиться» задаём только объявлениям запуска: конвейеру, `Makefile`,
52
+ `justfile`, `package.json`. Внутри скрипта вердикт решают `set -e`, `trap` и явный `exit`, и
53
+ по одной строке о нём судить нельзя — ту же границу проводит `ci-actually-fails`. Поймано на
54
+ себе: наш `smoke.sh` держит образцы в heredoc, и запись покраснела на собственном наборе
55
+ проверок, причём одна находка была в комментарии.
56
+ - **Спецификацию с нестандартным именем.** `docs/api/spec.yaml` не опознаётся: имя файла —
57
+ единственный надёжный признак, а `spec.yaml` встречается у чего угодно. Переименуй в
58
+ `openapi.yaml` — так его найдут и чужие инструменты, а не только этот.
59
+ - **Сужение другими способами.** Ограничение одним методом (`--include-method GET`), крошечное
60
+ число примеров, выборка путей. Это законные настройки: у проекта бывает причина не трогать
61
+ запись. Красным сделано только то, у чего законного применения нет.
62
+ - **Что спецификация вообще описывает этот сервер.** Файл может описывать чужой API; сверять
63
+ их машина не умеет.
@@ -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`, и ни одно имя из списка в нём не звучало.
@@ -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() {}
@@ -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
+ давали.
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env sh
2
+ # Набор объявленных гейтов может только РАСТИ. Убрать гейт можно, но не молча.
3
+ #
4
+ # ЗАЧЕМ. Комплект ловит агента, когда тот выключает сигнал: `# noqa`, `pytest || true`,
5
+ # ослабленный тест. А главный рубильник — сам манифест — не сторожил никто. Замер 2026-09-09:
6
+ # четыре гейта и настоящий секрет в коде дают код 1; убираешь ОДНУ строку из `.aqk.yml` —
7
+ # `Порог AQK-1 пройден`, код 0, секрет на месте, файлы гейта на диске. Ни `doctor`, ни `report`,
8
+ # ни `vitals`, ни блок состояния для агента этого не заметили.
9
+ #
10
+ # Это `pytest || true` этажом выше. И бьёт сильнее обычного обхода: `# noqa` виден в коде, а
11
+ # удалённая строка — это ОТСУТСТВИЕ, а отсутствие не рецензируют.
12
+ #
13
+ # ЧТО ЭТО НЕ ЗАПРЕЩАЕТ. Снять гейт по-прежнему можно — но названно. Причина пишется в реестре
14
+ # после `#`, как `deprecated` обязан нести `superseded_by`: решение остаётся за человеком и
15
+ # перестаёт быть невидимым.
16
+ DIR="${1:-.}"
17
+ MAN="$DIR/.aqk.yml"
18
+ [ -f "$MAN" ] || { echo "нет .aqk.yml — проверять нечего"; exit 0; }
19
+
20
+ MANTEXT="$(tr -d '\r' < "$MAN")"
21
+
22
+ # Имена гейтов из блока gates:. Пустая команда защиты не даёт — такие не считаем объявленными.
23
+ DECLARED="$(printf '%s\n' "$MANTEXT" \
24
+ | awk '/^gates:/{g=1;next} /^[A-Za-z]/{g=0} g && /^[[:space:]]+[A-Za-z0-9_-]+:[[:space:]]*[^[:space:]]/{print}' \
25
+ | sed 's/^[[:space:]]*\([A-Za-z0-9_-]*\):.*/\1/' | LC_ALL=C sort -u)"
26
+ [ -z "$DECLARED" ] && { echo "гейтов не объявлено — сторожить нечего"; exit 0; }
27
+
28
+ # Кавычки снимаются, и ПУСТОЕ значение считается отсутствующим. `init` кладёт `ratchets: ""`
29
+ # намеренно — пустое поле честнее заглушки, — и без снятия кавычек путь получался «""/…»:
30
+ # сообщение с таким путём человек не может выполнить. Найдено первым же прогоном в чужом
31
+ # проекте, а не на нашем репозитории, где поле заполнено.
32
+ RDIR="$(printf '%s\n' "$MANTEXT" | sed -n 's/^ratchets:[[:space:]]*//p' | head -1 \
33
+ | sed 's/^"//; s/"$//; s/^'"'"'//; s/'"'"'$//; s/[[:space:]]*$//')"
34
+ [ -z "$RDIR" ] && RDIR="ratchets"
35
+ REG="$DIR/$RDIR/gates-declared.txt"
36
+ REL="$RDIR/gates-declared.txt"
37
+
38
+ if [ ! -f "$REG" ]; then
39
+ # Реестра нет. Два разных случая, и путать их нельзя: снимка ещё не снимали — или его
40
+ # УДАЛИЛИ, что и есть тот самый обход, только другим файлом. Свидетель — git: если у пути
41
+ # есть история, файл существовал.
42
+ if git -C "$DIR" log -1 --format=%H -- "$REL" 2>/dev/null | grep -q .; then
43
+ echo "$REL был в истории и удалён — снимок объявленной защиты уничтожен"
44
+ echo " почини: верни файл (git checkout -- $REL). Удаление снимка снимает и саму проверку —"
45
+ echo " почини: это тот же обход, что удаление гейта, только через соседний файл."
46
+ exit 1
47
+ fi
48
+ mkdir -p "$DIR/$RDIR" || { echo "не создать $RDIR"; exit 2; }
49
+ printf '# Снимок объявленной защиты. Набор может только РАСТИ.\n' > "$REG"
50
+ printf '# Убрал гейт — напиши причину после # в его строке, иначе проверка краснеет.\n' >> "$REG"
51
+ printf '%s\n' "$DECLARED" >> "$REG"
52
+ echo "снят снимок объявленной защиты: $(printf '%s\n' "$DECLARED" | wc -l | tr -d ' ') гейтов → $REL"
53
+ echo " почини: закоммить этот файл — без него проверка не знает, что защита была."
54
+ exit 0
55
+ fi
56
+
57
+ # Имя без причины — строка реестра, где после имени НЕТ решётки. Имя с причиной — снятое
58
+ # осознанно; печатаем его отдельно, но прогон не роняем.
59
+ GONE=""
60
+ NAMED=""
61
+ while IFS= read -r LINE; do
62
+ case "$LINE" in ''|'#'*) continue ;; esac
63
+ NAME=$(printf '%s' "$LINE" | sed 's/[[:space:]]*#.*//; s/[[:space:]]*$//')
64
+ [ -z "$NAME" ] && continue
65
+ printf '%s\n' "$DECLARED" | grep -qx "$NAME" && continue
66
+ case "$LINE" in
67
+ *'#'*) NAMED="$NAMED $NAME" ;;
68
+ *) GONE="$GONE $NAME" ;;
69
+ esac
70
+ done < "$REG"
71
+
72
+ [ -n "$NAMED" ] && echo "совет: снято осознанно и названо в $REL:$NAMED"
73
+
74
+ if [ -n "$GONE" ]; then
75
+ for N in $GONE; do
76
+ echo "$REL: гейт «$N» был объявлен и исчез из .aqk.yml"
77
+ done
78
+ echo " почини: верни строку в gates: — либо, если снял намеренно, напиши причину в $REL"
79
+ echo " почини: после имени через #, например «$N # снят: закрыт гейтом lint»."
80
+ exit 1
81
+ fi
82
+
83
+ # Всё на месте — дописываем новые. Набор растёт сам; укоротить его молча нельзя.
84
+ NEW=""
85
+ for N in $DECLARED; do
86
+ sed 's/[[:space:]]*#.*//; s/[[:space:]]*$//' "$REG" | grep -qx "$N" || NEW="$NEW $N"
87
+ done
88
+ if [ -n "$NEW" ]; then
89
+ for N in $NEW; do printf '%s\n' "$N" >> "$REG"; done
90
+ echo "совет: в снимок дописаны новые гейты:$NEW"
91
+ fi
92
+ exit 0
@@ -0,0 +1,10 @@
1
+ intent: объявленная защита не исчезает молча — набор гейтов в манифесте может только расти
2
+ intent_en: declared protection does not vanish silently — the set of gates in the manifest may only grow
3
+
4
+ trigger:
5
+ has_gates: true
6
+
7
+ recipes:
8
+ any: bash {gate}/check.sh {dir}
9
+
10
+ proof: incidents/README.md — «2026-09-09 сигнализация построена, выключатель оставлен снаружи без пломбы»
@@ -0,0 +1,7 @@
1
+ aqk: 1
2
+ entry:
3
+ - AGENTS.md
4
+ rules: .aqk/rules
5
+ ratchets: .
6
+ gates:
7
+ lint: "true"
@@ -0,0 +1,4 @@
1
+ # Снимок объявленной защиты. Набор может только РАСТИ.
2
+ # Убрал гейт — напиши причину после # в его строке, иначе проверка краснеет.
3
+ lint
4
+ secrets-not-in-code # снят 2026-09-09: закрыт гейтом lint, см. covers в манифесте
@@ -0,0 +1,7 @@
1
+ aqk: 1
2
+ entry:
3
+ - AGENTS.md
4
+ rules: .aqk/rules
5
+ ratchets: .
6
+ gates:
7
+ lint: "true"
@@ -0,0 +1,4 @@
1
+ # Снимок объявленной защиты. Набор может только РАСТИ.
2
+ # Убрал гейт — напиши причину после # в его строке, иначе проверка краснеет.
3
+ lint
4
+ secrets-not-in-code
@@ -0,0 +1,9 @@
1
+ package config
2
+
3
+ // Ключ в коде: он уедет в историю git и останется там навсегда.
4
+ //
5
+ // Строка намеренно КОРОЧЕ настоящего ключа Stripe и повторяет ту, что лежит в settings.py:
6
+ // правдоподобный ключ блокирует защита GitHub от секретов, и образец нельзя отправить в
7
+ // репозиторий вовсе. Красный образец обязан быть узнаваем НАШИМ гейтом и не узнаваем чужими
8
+ // сканерами — проверено 2026-09-09: push отклонён на «Stripe API Key».
9
+ const StripeKey = "sk_live_51HxxQwErTyUiOpAsDfGh"
@@ -0,0 +1,5 @@
1
+ // Ключ в коде: он уедет в историю git и останется там навсегда.
2
+ //
3
+ // Строка намеренно короче настоящего ключа Stripe — см. комментарий в leak.go: правдоподобный
4
+ // ключ блокирует защита GitHub от секретов, и образец не отправить.
5
+ pub const STRIPE_KEY: &str = "sk_live_51HxxQwErTyUiOpAsDfGh";
@@ -0,0 +1,6 @@
1
+ package svc
2
+
3
+ func Send(to string) error {
4
+ // TODO: переписать на очередь
5
+ return deliver(to)
6
+ }