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,214 @@
1
+ # Контракт API и e2e: как сделать проверку, которая может провалиться
2
+
3
+ > **Кому.** Агенту, который собирается «написать e2e для API», и человеку, который будет решать,
4
+ > доказывает ли этот e2e хоть что-нибудь. Файл отвечает на один вопрос: **что должно получиться,
5
+ > чтобы это была проверка, а не строка в логе.**
6
+ >
7
+ > Всё, что здесь названо числами, замерено на стенде 2026-09-09, а не пересказано.
8
+
9
+ ## Один стенд, три ответа
10
+
11
+ Стенд: спецификация OpenAPI и сервер, который **врёт в каждом поле ответа** — `id` строкой
12
+ вместо числа, обязательного `email` нет вовсе, `created_at` не дата. При этом пятисоток нет,
13
+ коды ответа честные. Ровно так выглядит настоящее расхождение: не падение, а тихое расхождение.
14
+
15
+ | Инструмент | Код возврата | Что сказал |
16
+ |---|---|---|
17
+ | `spectral` (набор `spectral:oas`) | **0** | нет контакта, нет описания операции, нет тегов |
18
+ | `schemathesis`, умолчания | **1** | три нарушения схемы в ответах + принят невалидный запрос |
19
+ | `schemathesis -c not_a_server_error` | **0** | «18 из 18 прошли» |
20
+
21
+ Три вывода, и каждый стоит своей строки.
22
+
23
+ 1. **Линтер спецификации — не проверка сервера.** Он читает документ. На сервере, который врёт
24
+ в каждом поле, он остаётся зелёным и жалуется на отсутствие тегов. Это не недостаток
25
+ инструмента: он отвечает на другой вопрос.
26
+ 2. **`schemathesis` с умолчаниями — настоящий арбитр.** У него по умолчанию включены **все**
27
+ проверки, включая `response_schema_conformance`.
28
+ 3. **Сужение до `not_a_server_error` — не настройка, а отключение.** Тот же сервер, та же
29
+ спецификация, восемнадцать из восемнадцати прошли.
30
+
31
+ ## Семь вопросов о договоре — и кто на какой отвечает
32
+
33
+ Подменять один вопрос другим нельзя: зелёный ответ на первый ничего не говорит о втором.
34
+
35
+ | Вопрос | Чем отвечают | Чем краснеет |
36
+ |---|---|---|
37
+ | Документ хорошо написан? | `spectral`, `redocly`, `vacuum`, `openapi-spec-validator` | нарушение правил оформления |
38
+ | **Сервер не ушёл от документа?** | `schemathesis`, `dredd`, `portman`+`newman` | ответ не сходится со схемой |
39
+ | Вчерашний клиент переживёт выпуск? | `oasdiff breaking --fail-on ERR` | убрали поле, сузили тип, добавили обязательный параметр |
40
+ | Потребитель порождён договором? | `openapi-typescript`, `orval`, `oapi-codegen`, `kubb` | сборка клиента ломается на расхождении |
41
+ | Документ не отстал от кода? | `manage.py spectacular --fail-on-warn`, `tsoa spec` + `git diff --exit-code` | схема в репозитории не совпала с порождённой |
42
+ | Объявленная охрана и правда стоит? | `schemathesis` (проверка `ignored_auth`, включена по умолчанию) | путь объявлен под охраной, а пускает без токена или с мусорным |
43
+ | **Чужое не отдаётся?** | никто из перечисленных — только свой прогон от ДВУХ пользователей | второму отдали объект первого |
44
+
45
+ Второй вопрос — главный и он же чаще всего не задан. Первый задают почти все: линтер ставится
46
+ одной строкой и красиво выглядит в отчёте. Последний не задаёт ни один инструмент из списка, и
47
+ это не их недоработка: схема описывает форму, а не право (замер — ниже).
48
+
49
+ ## Порядок для агента: как построить e2e, который что-то доказывает
50
+
51
+ **1. Арбитр — до кода, и он обязан покраснеть.** Проверка, которую написали после кода и
52
+ которая сразу зелёная, не проверена ни разу. Убедись, что она краснеет: сломай ответ нарочно
53
+ (`id` строкой вместо числа) и посмотри на код возврата. Не покраснела — её нет.
54
+
55
+ **2. Через настоящий порт, а не через тестовый клиент.** И вот точная граница, потому что здесь
56
+ обычно врут в обе стороны. Тестовый клиент (`TestClient`, `APIClient`, `supertest` без сервера)
57
+ **проходит через приложение целиком** — промежуточные слои, обработчики ошибок, внедрение
58
+ зависимостей. Он не проходит через **дорогу к приложению**:
59
+
60
+ - обратный прокси и его правила: срезанные заголовки, лимит тела запроса, таймаут, сжатие;
61
+ - TLS и то, что за ним: настоящий `Host`, схема, порт;
62
+ - сервер приложений и его настройка: число процессов, keep-alive, размер очереди;
63
+ - настоящая сериализация по проводу: то, что клиент увидит байтами, а не объектом в памяти;
64
+ - окружение: переменные, которых в тестах нет, и наоборот.
65
+
66
+ Мок-тесты слепы на швах ровно этого списка. Поэтому: **хотя бы один прогон — `curl` по
67
+ настоящему адресу**, а не только тестовый клиент.
68
+
69
+ **3. Проверяй состояние, а не текст ответа.** «Ответ 200» и «заказ создан» — разные утверждения.
70
+ Арбитр смотрит на внешний признак: строка в базе, файл на диске, событие в очереди. Ответ
71
+ сервера — то, что подделать проще всего.
72
+
73
+ **4. «Не 500» — не проверка.** Это самая частая подмена. Сервер, который врёт в каждом поле,
74
+ отвечает двумястами. Смотри `response_schema_conformance`, а не отсутствие падений.
75
+
76
+ **5. Проверяй от ДВУХ пользователей, а не от одного.** Это не про полноту, это про целый класс
77
+ дыр, невидимый по построению. Замер ниже показывает: сервер, который отдаёт любому чужой заказ,
78
+ проходит сверку контракта целиком — 44 из 44, код 0. Один пользователь не может обнаружить, что
79
+ ему отдали чужое: чтобы это увидеть, нужен второй, чьи данные попробуют забрать. Самая частая
80
+ дыра в API (`BOLA`, около 40% атак) видна только так.
81
+
82
+ **6. Данные теста не общие.** Самый частый отказ в e2e — общие изменяемые данные: один прогон
83
+ создал запись, другой ждал, что её нет. Три рабочих уклада: сброс базы к известному состоянию
84
+ перед набором; свой счёт на каждый прогон, создаваемый и удаляемый через само API; свой счёт на
85
+ каждого параллельного исполнителя. Плохо — один вечный тестовый пользователь на всех.
86
+
87
+ **7. Тест не правится ради зелёного.** Когда арбитр краснеет, у пишущего два выхода: починить
88
+ код или ослабить арбитра. Второй дешевле и с виду неотличим от первого. Этот класс сторожат
89
+ записи `test-not-adjusted` и `gate-not-weakened`.
90
+
91
+ ## Что сверка контракта ловит, а что не увидит никогда
92
+
93
+ Второй стенд. Сервер **безупречен по контракту**: токен требует, мусорный токен отвергает, кривой
94
+ ввод отвергает, на неописанный метод отвечает `405` с заголовком `Allow`, ответы точно по схеме.
95
+ Дыра ровно одна и настоящая: **любой заказ отдаётся любому, кто спросил** — чужой в том числе.
96
+
97
+ | Что подсадили | Что сказал `schemathesis` |
98
+ |---|---|
99
+ | схема объявляет охрану, сервер токен не спрашивает | **поймал**: «API accepts requests without authentication» |
100
+ | сервер принимает мусорный токен | **поймал**: «API accepts invalid authentication» |
101
+ | сервер отдаёт любому чужой заказ | **44 из 44 прошли, код 0** |
102
+
103
+ Первые две строки — приятная неожиданность: сверка контракта нашла настоящую дыру в охране,
104
+ причём с умолчаниями и без единой настройки. Обещание «этот путь под охраной» — такое же
105
+ обещание, как тип поля, и у него теперь есть сторож.
106
+
107
+ Третья строка — граница, и её надо знать наизусть. **Схема описывает форму, а не право.**
108
+ «Заказ существует и выглядит как заказ» и «этот заказ можно показать этому человеку» —
109
+ утверждения из разных миров, и второе машина из документа не выведет. Отсюда правило 5 выше:
110
+ проверка от одного пользователя этот класс не видит вовсе, сколько её ни гоняй.
111
+
112
+ ## Моки расходятся с сервером молча
113
+
114
+ Мок, написанный руками, расходится с сервером в тот день, когда сервер меняют, — и тест
115
+ продолжает проходить. Это тот же класс, что вся эта методичка: зелёное, которое ничего не значит.
116
+
117
+ - мок, **порождённый из спецификации** (`prism`, `microcks`), расходится меньше по построению:
118
+ он врёт ровно настолько, насколько врёт спецификация;
119
+ - мок, **написанный руками** (`wiremock`, `mockserver` с ручными заглушками), не связан с
120
+ реальностью ничем, кроме памяти того, кто его писал;
121
+ - дешёвый сторож расхождения: ночной прогон небольшого набора **против настоящего сервера**.
122
+ Прошёл на моках и упал на сервере — моки разошлись, и это видно в тот же день, а не в проде.
123
+
124
+ ## Договор об ошибках — тоже договор
125
+
126
+ Спецификации почти всегда описывают успех и молчат про отказ. А клиент живёт отказами: он должен
127
+ отличить «повтори позже» от «так нельзя никогда». Есть готовый формат — **RFC 9457
128
+ (`application/problem+json`)**, поля `type`, `title`, `status`, `detail`, `instance`; его отдают
129
+ из коробки Spring Boot 6+ и ASP.NET Core 7+. В сам стандарт OpenAPI он не входит — его описывают
130
+ как обычную схему ответа.
131
+
132
+ Практический смысл прямой: **описанный код ответа проверяется, неописанный — нет.**
133
+ `status_code_conformance` краснеет на коде, которого нет в схеме, — то есть документируя только
134
+ `200`, вы выводите из-под проверки всё остальное поведение сервера.
135
+
136
+ ## Pact: когда договор нужен со стороны потребителя
137
+
138
+ `OpenAPI` — обещание сервера: «вот всё, что я умею». `Pact` — заявление потребителя: «вот то, чем
139
+ я на самом деле пользуюсь». Второе обычно составляет малую долю первого, и ломается на практике
140
+ именно оно.
141
+
142
+ - **публичный API с неизвестными потребителями** — только OpenAPI: опереться могли на любое поле;
143
+ - **несколько своих потребителей** — `Pact` точнее и дешевле в проверке;
144
+ - **двусторонний вариант** (провайдер публикует свою спецификацию и сам доказывает, что ей
145
+ соответствует; потребители публикуют свои куски; сверяет их брокер, без общего прогона)
146
+ существует только в платном `PactFlow` — в открытом брокере его нет. Это стоит знать до, а не
147
+ после выбора;
148
+ - цена настоящая: брокер, `can-i-deploy` в выкатке, дисциплина. Для трёх сервисов дороже пользы,
149
+ для сорока — не обсуждается.
150
+
151
+ ## Пять способов, которыми проверка контракта врёт
152
+
153
+ Все пять — с замером или с чужим отказом, а не «бывает и такое».
154
+
155
+ **1. Линтер вместо сверки.** Стоит `spectral`, галочка «контракт проверяется» есть, сервер с
156
+ документом никто не сравнивал. Замер выше: код 0 при сервере, который врёт везде.
157
+
158
+ **2. Арбитр сужен.** `--checks not_a_server_error`, один метод, три примера. Замер: «18 из 18
159
+ прошли». Сужение выглядит как настройка и работает как выключатель.
160
+
161
+ **3. Совещательный режим.** `continue-on-error: true`, `allow_failure: true`, `|| true`. Шаг
162
+ выполняется, вердикт не выносится. Это `ci-actually-fails`, и до 2026-09-09 он не знал ни одного
163
+ инструмента про API: три шага — фаззер, `oasdiff` и `pact`, все три обезврежены, — и он говорил
164
+ «чисто».
165
+
166
+ **4. Громкий вывод и нулевой код.** Самый коварный. Замер: `oasdiff breaking` на паре
167
+ спецификаций, где из ответа убрано обязательное поле, печатает
168
+ `1 changes: 1 error … removed the required property` — и **выходит с нулём**. С `--fail-on ERR`
169
+ на той же паре код 1. Человек, читающий лог, видит красное; конвейер видит зелёное.
170
+
171
+ **5. Договор описывает четверть кода.** Спецификация порождается из кода, генератор ругается,
172
+ часть обработчиков в неё не попадает — и фаззер честно проверяет то, что до него дошло. Отказ
173
+ чужой и с числами: 202 ошибки генерации, 37 обработчиков выброшено целиком, при этом в отчёте
174
+ стоит «фаззинг API ✅». Лечится там же, где возникает: `--fail-on-warn` у генератора.
175
+
176
+ ## Спецификация из кода и спецификация руками — разные риски
177
+
178
+ **Порождается из кода** (`drf-spectacular`, FastAPI, `tsoa`). Расхождение с кодом невозможно по
179
+ построению — зато возможна **дыра**: обработчик, который генератор не понял и молча выбросил.
180
+ Сторож — `--fail-on-warn` при генерации и коммит порождённого файла с `git diff --exit-code` в
181
+ конвейере: тогда «схема отстала» становится красным, а не разговором.
182
+
183
+ **Пишется руками** (спецификация впереди кода). Дыр нет — есть расхождение: документ живёт своей
184
+ жизнью. Сторож — `schemathesis` против настоящего сервера.
185
+
186
+ Уклад выбирается проектом; беззащитны оба, если не назвать сторожа.
187
+
188
+ ## Чего машина не проверит
189
+
190
+ - **Право, а не форму.** Кому можно показывать этот объект — не выводится из схемы.
191
+ Замерено: 44 из 44 прошли на сервере, отдающем чужие заказы. Лечится не инструментом, а
192
+ прогоном от двух пользователей.
193
+ - **Что договор описывает ЭТОТ сервер.** Файл может описывать чужой API.
194
+ - **Что арбитр смотрит на то, что важно.** Схема сходится, а поле означает не то — это разбор
195
+ человеком, а не проверка.
196
+ - **Что e2e прошёл по важному пути.** Покрытие путей арбитром не считает никто из перечисленных.
197
+ - **Смысл ломающей правки.** `oasdiff` скажет «поле убрано»; нужно ли его убирать — решение.
198
+
199
+ ## Что из этого сторожит комплект
200
+
201
+ | Запись | Что ловит |
202
+ |---|---|
203
+ | `api-contract-has-arbiter` | спецификацию не держит ни одна команда; арбитр не может провалиться |
204
+ | `ci-actually-fails` | шаг с проверкой контракта под `continue-on-error` или `\|\| true` |
205
+ | `test-not-adjusted` | арбитра ослабили, чтобы стало зелёным |
206
+ | `promise-has-gate` | обещание в своде без названного сторожа |
207
+
208
+ Остальное из этого файла машиной не проверяется — и написано именно поэтому.
209
+
210
+ **Почему записи каталога нет на «две учётки» и на «моки разошлись».** Проверить это машиной
211
+ можно только догадкой: по коду теста не видно, два ли в нём пользователя и настоящий ли за ним
212
+ сервер. Запись, которая красит по догадке, ошибается на законном укладе — а гейт, ошибающийся на
213
+ законном, выключают целиком, вместе с тем, что он ловил верно. Здесь дешевле текст, который
214
+ человек прочтёт один раз, чем сторож, которому перестанут верить.
@@ -197,6 +197,58 @@ npm install -g agnix && agnix --strict .
197
197
  настройкам нашёл шесть таких хуков в четырёх репозиториях — среди них `typecheck && test` перед
198
198
  коммитом, не запускавшийся ни разу. Это запись `hook-actually-fires`.
199
199
 
200
+ Тот же класс у MCP: объявленный сервер, который не поднимается, агенту не виден никак — у него просто нет этих инструментов. Запись `mcp-server-resolves` ловит две самые частые смерти без запуска: команды по абсолютному пути, которой больше нет, и версии, которая не закреплена и приезжает новая при каждом запуске.
201
+
202
+ ## Поиск на большом репозитории: `tgrep`, и как им пользоваться правильно
203
+
204
+ Переносимые проверки каталога ищут через `grep`. На проекте до нескольких тысяч файлов это
205
+ десятки миллисекунд, и менять там нечего. **От десятка тысяч файлов картина другая.**
206
+
207
+ [`tgrep`](https://github.com/microsoft/tgrep) — Microsoft, MIT, Rust: строит триграммный индекс
208
+ и держит фоновый сервер, следящий за изменениями. Замер 2026-09-09 на настоящем проекте, один
209
+ и тот же набор файлов, одна и та же регулярка:
210
+
211
+ | файлов в проекте | `grep` | `tgrep` |
212
+ |---|---|---|
213
+ | 277 | **6 мс** | индекс строится дольше, чем идёт поиск |
214
+ | 1 737 | **39 мс** | 77 мс |
215
+ | 37 760 | 96 мс | **16 мс**, полнота полная |
216
+
217
+ Перелом между двумя и десятью тысячами файлов. Их собственные замеры — на 96 000, 388 000 и
218
+ 504 000 файлов, где выигрыш доходит до пятидесяти раз; заявка честная и подтверждается.
219
+
220
+ ### Две грабли, на которые мы наступили сами
221
+
222
+ **Первая: по умолчанию он не видит точечные каталоги.** Индекс строится по видимым файлам, и
223
+ `.claude/`, `.github/`, `.env.example` для него не существуют. На нашем замере это было 32
224
+ найденных файла вместо 55 — и узнали мы это только сверкой со списком `grep`, а не из вывода.
225
+ Лечится флагом **при индексации**, а не при поиске:
226
+
227
+ ```bash
228
+ tgrep index . --hidden # без --hidden точечные каталоги не попадут в индекс
229
+ tgrep serve . # демон следит за изменениями
230
+ tgrep "что ищем" . # мгновенно, полнота как у grep
231
+ ```
232
+
233
+ **Вторая: `--hidden` при ПОИСКЕ отключает индекс.** Это написано в их документации, раздел
234
+ «Flags that bypass the index», и мы это пропустили: первый замер дал «7 секунд, медленнее
235
+ grep» — потому что мерили обход дерева, а не индекс. Флаг ставится один раз, на `index`.
236
+
237
+ ### Почему он не годится арбитром гейта
238
+
239
+ Проверено опытом: секрет, записанный **после** индексации, при запросе к устаревшему индексу
240
+ не находится, и код возврата — «совпадений нет». Для гейта секретов это молчаливое зелёное на
241
+ самом опасном месте. С работающим `tgrep serve` находится сразу — но тогда условие правильности
242
+ гейта звучит как «на машине поднят фоновый процесс», а гейт обязан быть прав без предварительных
243
+ условий.
244
+
245
+ Плюс два расхождения, о которых стоит знать: **коды возврата как у ripgrep** (`0` — совпадение
246
+ найдено, у гейтов наоборот) и **файлы больше 64 МиБ пропускаются** по умолчанию.
247
+
248
+ Вывод простой: **инструмент для человека, а не для ворот.** Ищете руками по большому
249
+ репозиторию — ставьте. Проверяете код перед коммитом — `grep` честнее, потому что не зависит ни
250
+ от чего.
251
+
200
252
  ## А если проект не на Python?
201
253
 
202
254
  Ничего не меняется. Запись каталога держит **одно намерение и несколько исполнителей**, и
@@ -226,6 +278,142 @@ npm install -g agnix && agnix --strict .
226
278
  | подавление проверки без адреса | наш `gate-not-weakened` плюс `eslint-plugin-eslint-comments`, `flake8-noqa` |
227
279
  | шаг конвейера, который не может провалиться | наш `ci-actually-fails`; у `checkwash` есть смежный `CI_WORKFLOW_TOUCHED` |
228
280
 
281
+ ## Три сверки под три предложенные записи — 2026-09-09
282
+
283
+ Второй пользователь предложил три записи каталога (issue #46). Прежде чем писать код, проверено
284
+ снаружи, есть ли готовое. Ответ вышел **разный по всем трём** — записано здесь, чтобы поиск не
285
+ повторяли.
286
+
287
+ ### Миграция, не попавшая в журнал инструмента
288
+
289
+ **Похоже, готовое есть, и запись остановлена до замера.**
290
+
291
+ | Инструмент | Что делает | Закрывает ли класс |
292
+ |---|---|---|
293
+ | `drizzle-kit check` | согласованность истории миграций: коллизии снимков, дубли `idx` в `_journal`, непоследовательные номера | **неизвестно** — про написанный руками `.sql` без записи в журнале в документации не сказано |
294
+ | `alembic check` | сравнивает МОДЕЛИ с миграциями: «есть ли изменения без ревизии» | нет, это другой вопрос |
295
+
296
+ Замер, который решает: положить руками `migrations/9999_test.sql`, в `_journal.json` не вписывать,
297
+ запустить `npx drizzle-kit check`. Красный — записи не надо вовсе, надо строка «поставь эту
298
+ команду в конвейер». Зелёный — запись законна и сразу с доказательством.
299
+
300
+ **Это тот случай, ради которого правило и написано:** одна команда решает, будет папка или строка.
301
+
302
+ ### Команда сборки, заглушившая свой отказ
303
+
304
+ **Готового нет, и это проверено — причём показательно.**
305
+
306
+ Ближайшее — `shellcheck SC2312`, и оно про другое: маскировку кода возврата в подстановке
307
+ команды. Мало того, что не ловит `>/dev/null 2>&1` у сборки, — в собственном тексте правила
308
+ `|| true` предложен **как способ подавления**:
309
+
310
+ > Consider invoking this command separately to avoid masking its return value (or use `|| true`
311
+ > to ignore).
312
+
313
+ Источник: <https://www.shellcheck.net/wiki/SC2312>
314
+
315
+ То есть общепринятый линтер оболочки считает `|| true` намеренным приёмом — и он прав в своей
316
+ области. Наша запись про **место**: команда, выносящая вердикт, не имеет права молчать о своём
317
+ отказе. `Makefile`, `package.json`, скрипты развёртывания — там `|| true` частый гость, а сторожа
318
+ нет ни одного.
319
+
320
+ ### Починка без арбитра
321
+
322
+ **Готовое есть, и его обязательно называть.** `danger-js` делает этот класс типовым правилом
323
+ («app changes without tests»), есть и плагины. Источник: <https://danger.systems/js/>
324
+
325
+ Отличия настоящие, но их всего два: `danger-js` работает на уровне PR и требует GitHub плюс
326
+ `Dangerfile`. Запись каталога смотрела бы **коммит** и не была бы привязана к площадке —
327
+ прецедент есть, `commit-explains-itself` читает историю, а не дерево файлов.
328
+
329
+ Это ровно тот случай, когда своя запись законна, но раздел «готовый аналог» обязан назвать
330
+ чужой инструмент и сказать, **почему не он**.
331
+
332
+ ## Разбор истории git: кто это уже делает — 2026-09-09
333
+
334
+ Прежде чем писать `aqk probe`, проверено снаружи. **Поле не пустое, и это надо знать.**
335
+
336
+ | Инструмент | Что меряет | Лицензия |
337
+ |---|---|---|
338
+ | [CodeScene](https://codescene.com/product/behavioral-code-analysis) | горячие точки = **частота изменений × сложность** (строки кода как приближение). Плюс здоровье кода и временную связность файлов | коммерческий |
339
+ | [code-maat](https://github.com/adamtornhill/code-maat) | то же из командной строки: горячие точки, связность авторов, возраст кода. Git, hg, svn, p4, tfs | открытый, тот же автор |
340
+ | Hercules, git-quick-stats, git-fame, CodeCharta | статистика по истории в разных срезах | открытые |
341
+
342
+ **Все они отвечают «этот код рискованный».** Ни один не отвечает на следующий вопрос: **поймает
343
+ ли там что-нибудь брак.** Наш угол именно в этом — история скрещивается с **подсадкой**: мы
344
+ говорим не «здесь сложно», а «здесь брак не увидит ни одна ваша проверка, вот доказательство».
345
+
346
+ Второе отличие тоньше и важнее. Они берут **частоту изменений**, мы — **частоту починок**.
347
+ Часто меняют и то, что активно пишут: растущий модуль наберёт изменений больше всех и окажется
348
+ наверху рейтинга, не будучи ломким. **Возвращаются с починкой** — туда, где ломается.
349
+
350
+ Своё писать законно ровно потому, что назван вопрос, на который готовое не отвечает. Если
351
+ понадобится связность файлов или возраст кода — брать `code-maat`, а не писать второй.
352
+
353
+ ## Контракт API: самая дорогая дыра, и готовое здесь сильное — 2026-09-09
354
+
355
+ **Спецификация, разошедшаяся с кодом, — это наш центральный класс, только на уровне API.**
356
+ Документ обещает, машина не держит: `openapi.yaml` говорит, что поле обязательно, сервер отдаёт
357
+ без него, и узнают об этом у клиента. Ровно «тишина неотличима от успеха».
358
+
359
+ Своего гейта здесь быть не должно: готовое сильное и его несколько. Разница между инструментами
360
+ — в том, на какой вопрос они отвечают.
361
+
362
+ ### Спецификация правильно устроена
363
+
364
+ | Инструмент | Чем берёт | Цена |
365
+ |---|---|---|
366
+ | [`spectral`](https://github.com/stoplightio/spectral) | сложившаяся экосистема правил, привычен | тянет Node; на больших спецификациях медленный |
367
+ | [`redocly cli`](https://github.com/Redocly/redocly-cli) | набор правил «из коробки», настроен под документацию | мнение автора зашито сильнее |
368
+ | [`vacuum`](https://github.com/daveshanley/vacuum) | Go, самый быстрый (втрое против redocly), отчёты в формате spectral | ест больше памяти |
369
+
370
+ **Важное для выбора, и это замер, а не мнение:** при ОДИНАКОВО настроенных правилах все трое
371
+ нашли одни и те же 12 ошибок. Различаются они умолчаниями, а не способностями: на одной и той же
372
+ спецификации `vacuum` дал 2 ошибки и 20 предупреждений, `redocly` — 6 и 5, `spectral` — 0 и 11.
373
+
374
+ Отсюда практический вывод: **сравнивать их по числу находок «из коробки» бессмысленно.** Мягкое
375
+ умолчание `spectral` — не слабость инструмента, а его настройка, и именно она делает его тем,
376
+ что молча пропускает. Выбирается инструмент по цене (скорость, зависимости), правила — руками.
377
+
378
+ ### Спецификация соответствует КОДУ
379
+
380
+ Это другой вопрос, и линтеры на него не отвечают вовсе: они читают документ, а не сервер.
381
+
382
+ [`schemathesis`](https://schemathesis.io/) генерирует запросы из самой схемы и сообщает о
383
+ нарушениях схемы в ОТВЕТАХ — то есть о том, что сервер ушёл от собственного контракта. Для
384
+ спецификаций, которые давно не обновляли (а это большинство), расхождение вылезает быстро.
385
+
386
+ **Это и есть проверка, которую стоит ставить порогом конвейера.** Линтер отвечает «документ
387
+ хорошо написан», `schemathesis` — «документ не врёт».
388
+
389
+ ### Ломающие изменения и потребитель договора
390
+
391
+ [`oasdiff`](https://github.com/oasdiff/oasdiff) сравнивает две версии спецификации и отвечает на
392
+ третий вопрос: переживёт ли вчерашний клиент сегодняшний выпуск. **С одной оговоркой, которая
393
+ дороже самого инструмента:** без `--fail-on ERR` он печатает находки и выходит с НУЛЁМ. Замер
394
+ 2026-09-09 на паре, где из ответа убрано обязательное поле: вывод
395
+ `1 changes: 1 error … removed the required property`, код возврата **0**; с `--fail-on ERR` — 1.
396
+
397
+ Четвёртый вопрос — есть ли у договора потребитель. `openapi-typescript`, `orval`, `oapi-codegen`,
398
+ `kubb` порождают клиентские типы ИЗ спецификации: тогда расхождение ломает сборку, а не
399
+ обнаруживается у пользователя. Типы, написанные руками рядом со спецификацией, — это два
400
+ документа об одном, и расходятся они молча.
401
+
402
+ Пятый — не отстал ли документ от кода там, где он из кода порождается:
403
+ `manage.py spectacular --fail-on-warn` (флаг так и описан: «Intended for CI/CD»), `tsoa spec`
404
+ плюс `git diff --exit-code` на закоммиченной схеме.
405
+
406
+ ### Чего у них нет — и наша запись
407
+
408
+ Все перечисленные предполагают, что их УЖЕ запускают. Ни один не отвечает на вопрос:
409
+ **в репозитории лежит `openapi.yaml`, а держит ли его кто-нибудь вообще?** Это ровно форма нашей
410
+ записи `promise-has-gate`: обещание, у которого не назван сторож.
411
+
412
+ Запись есть: `api-contract-has-arbiter` (2026-09-09). Условной она была ровно до того дня, когда
413
+ нашёлся настоящий отказ — и нашёлся сразу в трёх видах: спецификацию не держит никто; арбитр
414
+ сужен до «не пятисотка» (замер: «18 из 18 прошли» на сервере, который врёт в каждом поле);
415
+ `oasdiff` печатает ломающие изменения и выходит с нулём. Разбор целиком — `kit/docs/api-e2e.md`.
416
+
229
417
  ## Если готового нет
230
418
 
231
419
  Тогда свой гейт — и в его `README.md` пишется, **что именно проверено**: какой инструмент
@@ -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
+ их машина не умеет.