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
package/README.md CHANGED
@@ -87,6 +87,8 @@ aqk why <name> what failure this guard was written for
87
87
 
88
88
  aqk prove run every declared gate against its own samples:
89
89
  red on the red one, quiet on the green one
90
+ aqk probe what the declared checks CANNOT see: plant a defect
91
+ into files the fix history calls hot
90
92
  aqk report the report form, assembled by a run
91
93
  aqk report --since main ...plus what proves this diff, file by file
92
94
  aqk badge write the level badge into the README
@@ -182,6 +184,7 @@ covers: # what a declared gate already holds — not counted
182
184
  lint: [no-print-in-prod, swallowed-error]
183
185
  samples: gates # a red and a green sample for every entry
184
186
  ratchets: ratchets # debt registries: the list may only get shorter
187
+ probe: 100 # run the probe itself every N commits; 0 turns it off
185
188
  lessons: incidents # where lessons accumulate
186
189
  ```
187
190
 
@@ -268,6 +271,56 @@ The level is granted when **no provable gate is broken AND at least one is prove
268
271
  condition is not optional: a project where everything is unprovable has proven nothing — that is
269
272
  exactly what the forgery with three `true` gates looks like.
270
273
 
274
+ ### What your checks cannot see — `probe`
275
+
276
+ `doctor` says "held by a machine 21". **Twenty-one out of what?** There is no denominator: 21 is
277
+ what we happened to write into the catalogue, not what matters in your project. `prove` shows a
278
+ gate catches a defect **on its own** sample. Neither answers the owner's question: what here is
279
+ covered by nothing.
280
+
281
+ ```bash
282
+ aqk probe
283
+ ```
284
+
285
+ Two sources, both facts rather than our taste: **your repository's history** (where defects come
286
+ back — fix commits) and the **red samples of the catalogue** (each one proven by a run). The
287
+ sample is placed in a temporary directory at the hot file's path, and **your declared** gates are
288
+ run against it.
289
+
290
+ ```
291
+ src/mailer.py fixes in history: 3
292
+ ✘ keys and passwords do not end up in the code NOTHING CATCHES IT
293
+ close it: aqk add secrets-not-in-code
294
+ ✘ an error is not silently swallowed NOTHING CATCHES IT
295
+ close it: aqk add swallowed-error
296
+ ✔ no "fix later" markers in finished code caught by: todo-without-task
297
+ ```
298
+
299
+ **Coverage is not declared, it is proven by planting.** This is the kit's own principle turned on
300
+ the whole repository: a check that cannot go red is indistinguishable from an absent one. We
301
+ demand that of every catalogue entry — and until `probe` never demanded it of a project.
302
+
303
+ The measurement the command grew from, taken on the kit itself: a real source file, a swallowed
304
+ error and a debug print planted into a copy, 21 declared gates. **Red: none.**
305
+
306
+ Three states, and they do not merge: caught · **nothing catches it** · nothing to check with (the
307
+ gate did not run, or the catalogue has no sample for that extension). The working tree is not
308
+ touched and the exit code is always 0 — this is a look, not a threshold.
309
+
310
+ **The command does not need to be remembered — that is half the design.** Once every hundred
311
+ commits — or whatever `probe:` in the manifest says, `0` turning it off — `doctor --run` runs the
312
+ probe **itself**, unprompted. A command you have to remember is
313
+ the same class as a file you can fail to read: the agent will not recall it, and the human will
314
+ never learn it exists. The unit is commits, not days: a repository nobody touched for a month
315
+ needs no re-probe, a hundred commits in a day does. Turn it off with `AQK_PROBE=0`.
316
+
317
+ The result also lands in the state block the agent reads by construction, without knowing the
318
+ command. If no probe has ever run, it says **UNKNOWN** rather than staying silent: silence would
319
+ read as "everything is covered".
320
+
321
+ Your code is not touched — the sample lives in a temporary directory. The probe's own mark goes
322
+ to `.aqk/last-probe.md`, next to the run report; it is ephemeral, keep it in your `.gitignore`.
323
+
271
324
  ## The badge
272
325
 
273
326
  ```bash
@@ -287,7 +340,7 @@ Already using [pre-commit](https://pre-commit.com)? Three lines in the file you
287
340
  ```yaml
288
341
  repos:
289
342
  - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
290
- rev: v0.9.0
343
+ rev: v0.10.0
291
344
  hooks:
292
345
  - id: aqk # runs what the repository declares; blocks below AQK-1
293
346
  # - id: aqk-doctor # read-only: the level and what is missing, blocks nothing
@@ -308,7 +361,7 @@ layer AQK adds.
308
361
  [![on the GitHub Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-Agent%20Quality%20Kit-2ea44f?logo=github)](https://github.com/marketplace/actions/agent-quality-kit-aqk)
309
362
 
310
363
  ```yaml
311
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.9.0
364
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.10.0
312
365
  with:
313
366
  min: 1 # the build fails below AQK-1, or if any declared gate failed
314
367
  ```
@@ -325,13 +378,12 @@ the same thing in a single line:
325
378
  A Python, Go or Rust project where nobody installed Node and nobody will:
326
379
 
327
380
  ```bash
328
- docker build -t aqk . # from this repository
329
- docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/work" aqk doctor
381
+ docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/work" ghcr.io/arsen-ask-lx/aqk doctor
330
382
  ```
331
383
 
332
- From v0.9.0 the image is pushed to the registry by the same run that publishes the package, so
333
- there is nothing to build: `ghcr.io/arsen-ask-lx/aqk`. Before that tag the registry holds no
334
- image, and this says so plainly: a command that points at nothing is worse than no command.
384
+ The image is pushed to the registry by the same run, from the same tag, that publishes the
385
+ package — there is nothing to build. You can still build it yourself: `docker build -t aqk .`
386
+ from this repository.
335
387
 
336
388
  **The `--user` flag is not decoration.** Without it the container runs as root and the files
337
389
  `init` writes end up owned by root — you cannot edit your own manifest. Measured 2026-09-09:
@@ -443,10 +495,10 @@ catalogue may grow to hundreds of entries; a given project still sees about a do
443
495
  An entry is accepted only if its arbiter goes red on the red sample, stays quiet on the green
444
496
  one, and names a real failure it caught. A machine checks this: `bash tool/selfcheck/gates.sh`.
445
497
 
446
- ### Four entries that watch the agent, not the code
498
+ ### Five entries that watch the agent, not the code
447
499
 
448
500
  Ruff, ESLint and gitleaks already find bad code, and AQK calls them where it can rather than
449
- reinventing them. These four look elsewhere — at the moment the **signal** about bad code is
501
+ reinventing them. These five look elsewhere — at the moment the **signal** about bad code is
450
502
  switched off, which is what a coding agent does when the task is phrased as "make it pass":
451
503
 
452
504
  | Entry | What it catches |
@@ -455,8 +507,15 @@ switched off, which is what a coding agent does when the task is phrased as "mak
455
507
  | `ci-actually-fails` | a pipeline step that renders a verdict but cannot fail — `run: pytest \|\| true`, `continue-on-error: true` |
456
508
  | `test-has-assertion` | a test that cannot fail: empty body, `assert True`, a skip with no reason given |
457
509
  | `promise-has-gate` | a rule in `AGENTS.md` with no enforcer named — neither a gate nor, honestly, a human |
510
+ | `protection-not-removed` | **the instrument itself was switched off**: a gate vanished from the manifest. The declared set may only grow; removing one is allowed, but must be named |
511
+
512
+ The fifth was added later, and from a bruise of our own. The first four catch the agent switching
513
+ off a **signal**. The master switch — the manifest itself — was guarded by nothing: a measurement
514
+ on 2026-09-09 showed that deleting one line from `.aqk.yml` turns the run green while a real
515
+ secret sits in the code, and neither `doctor`, `report --since`, `vitals` nor the state block the
516
+ agent reads noticed anything.
458
517
 
459
- Each was measured on nineteen third-party repositories (~25 000 files) before it entered the
518
+ The first four were measured on nineteen third-party repositories (~25 000 files) before entering the
460
519
  catalogue, and two further entries were **cancelled by that measurement**: one because
461
520
  [`agents-lint`](https://github.com/giacomo/agents-lint) already does it better, one because
462
521
  91 of its 120 findings turned out to be a legitimate pattern.
package/README.ru.md CHANGED
@@ -89,6 +89,8 @@ aqk why <имя> какой отказ этот гейт поймал
89
89
 
90
90
  aqk prove прогнать каждый объявленный гейт по его образцам:
91
91
  красный на красном, тишина на зелёном
92
+ aqk probe чего объявленные проверки НЕ видят: подсадка брака
93
+ в файлы, горячие по истории починок
92
94
  aqk report форма отчёта, собранная прогоном
93
95
  aqk report --since main ...и чем доказан этот диф, файл за файлом
94
96
  aqk badge вписать значок уровня в README
@@ -184,6 +186,7 @@ covers: # что уже держит объявленный
184
186
  lint: [no-print-in-prod, swallowed-error]
185
187
  samples: gates # красный и зелёный образец каждой записи
186
188
  ratchets: ratchets # реестры долга: список может только укорачиваться
189
+ probe: 100 # раз во столько коммитов проба делается сама; 0 — не делать
187
190
  lessons: incidents # где копятся уроки
188
191
  ```
189
192
 
@@ -270,6 +273,56 @@ aqk prove
270
273
  условие обязательно: проект, у которого все гейты недоказуемы, не доказал ничего — именно так
271
274
  выглядит подделка с тремя гейтами `true`.
272
275
 
276
+ ### Чего ваши проверки не видят — `probe`
277
+
278
+ `doctor` говорит «держит машина 21». **Двадцать один из чего?** Знаменателя нет: 21 — это то,
279
+ что мы успели написать в каталог, а не то, что важно в вашем проекте. `prove` доказывает, что
280
+ гейт ловит брак **на своём** образце. Ни один из них не отвечает на вопрос владельца: что здесь
281
+ не прикрыто ничем.
282
+
283
+ ```bash
284
+ aqk probe
285
+ ```
286
+
287
+ Два источника, и оба — факты, а не наш вкус: **история вашего репозитория** (где брак
288
+ возвращается — коммиты-починки) и **красные образцы каталога** (каждый доказан прогоном).
289
+ Образец кладётся во временный каталог по пути горячего файла, и по нему прогоняются
290
+ **объявленные вами** гейты.
291
+
292
+ ```
293
+ src/mailer.py починок в истории: 3
294
+ ✘ ключи и пароли не попадают в код НЕ ЛОВИТ НИКТО
295
+ закрыть: aqk add secrets-not-in-code
296
+ ✘ ошибка не глушится молча НЕ ЛОВИТ НИКТО
297
+ закрыть: aqk add swallowed-error
298
+ ✔ маркеров «доделать потом» нет в готовом коде ловит: todo-without-task
299
+ ```
300
+
301
+ **Покрытие не заявлено, а доказано подсадкой.** Это наш собственный принцип, наведённый на весь
302
+ репозиторий: проверка, которая не может покраснеть, неотличима от отсутствующей. Мы требуем
303
+ этого от каждой записи каталога — и до `probe` ни разу не потребовали от проекта целиком.
304
+
305
+ Замер, с которого команда началась, — на самом комплекте: настоящий файл, подсаженные
306
+ проглоченная ошибка и отладочная печать, 21 объявленный гейт. **Покраснело: ноль.**
307
+
308
+ Три состояния, и они не сливаются: ловит · **не ловит никто** · нечем проверить (гейт не
309
+ состоялся, или в каталоге нет образца под это расширение). Рабочее дерево не трогается, код
310
+ возврата всегда 0 — это осмотр, а не порог.
311
+
312
+ **Команду не нужно помнить — и это половина замысла.** Раз в сто коммитов (`probe:` в манифесте — своё число, `0` — не делать) `doctor --run`
313
+ запускает пробу **сам**, без напоминания. Команда, о которой надо вспомнить, — тот же класс,
314
+ что файл, который можно не прочитать: агент не вспомнит, а человек не узнает, что она есть.
315
+ Единица — коммиты, а не сутки: репозиторий, в котором месяц не работали, перепроверять незачем,
316
+ а сто коммитов за день — надо. Выключается `AQK_PROBE=0`.
317
+
318
+ Результат попадает и в блок состояния для агента: он читает его по построению, не зная про
319
+ команду. Пробы не было — там сказано **НЕИЗВЕСТНО**, а не пропущено: молчание прочиталось бы
320
+ как «всё прикрыто».
321
+
322
+ Ваш код проба не трогает: образец живёт во временном каталоге. Своя отметка ложится в
323
+ `.aqk/last-probe.md` — там же, где отчёт прогона; файл эфемерный, в `.gitignore` его стоит
324
+ держать самому.
325
+
273
326
  ## Значок
274
327
 
275
328
  ```bash
@@ -290,7 +343,7 @@ aqk badge --check # в конвейере: код 1 в тот день, ког
290
343
  ```yaml
291
344
  repos:
292
345
  - repo: https://github.com/arsen-ask-lx/Agent_Quality_Kit
293
- rev: v0.9.0
346
+ rev: v0.10.0
294
347
  hooks:
295
348
  - id: aqk # запускает объявленное; роняет коммит ниже AQK-1
296
349
  # - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
@@ -309,7 +362,7 @@ repos:
309
362
  [![в GitHub Marketplace](https://img.shields.io/badge/GitHub%20Marketplace-Agent%20Quality%20Kit-2ea44f?logo=github)](https://github.com/marketplace/actions/agent-quality-kit-aqk)
310
363
 
311
364
  ```yaml
312
- - uses: arsen-ask-lx/Agent_Quality_Kit@v0.9.0
365
+ - uses: arsen-ask-lx/Agent_Quality_Kit@v0.10.0
313
366
  with:
314
367
  min: 1 # сборка падает ниже AQK-1 или если упал любой объявленный гейт
315
368
  ```
@@ -326,13 +379,11 @@ repos:
326
379
  Проект на Python, Go или Rust, где Node никто не ставил и не поставит:
327
380
 
328
381
  ```bash
329
- docker build -t aqk . # из этого репозитория
330
- docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/work" aqk doctor
382
+ docker run --rm -u "$(id -u):$(id -g)" -v "$PWD:/work" ghcr.io/arsen-ask-lx/aqk doctor
331
383
  ```
332
384
 
333
- С версии 0.9.0 образ выкладывается в реестр тем же прогоном, что и пакет, — тогда собирать не
334
- нужно: `ghcr.io/arsen-ask-lx/aqk`. До этой метки образа в реестре нет, и здесь это сказано
335
- прямо: команда, указывающая в никуда, хуже её отсутствия.
385
+ Образ выкладывается в реестр тем же прогоном и из того же тега, что и пакет, — собирать ничего
386
+ не нужно. Собрать у себя тоже можно: `docker build -t aqk .` из этого репозитория.
336
387
 
337
388
  **Ключ `--user` не украшение.** Без него контейнер работает от root, и файлы, которые пишет
338
389
  `init`, достаются root — человек не может править собственный манифест. Проверено 2026-09-09:
@@ -441,10 +492,10 @@ vendor/
441
492
  Запись принимается, только если её арбитр краснеет на красном образце, молчит на зелёном и
442
493
  назван реальный отказ, который она поймала. Проверяет это машина: `bash tool/selfcheck/gates.sh`.
443
494
 
444
- ### Четыре записи, которые смотрят на агента, а не на код
495
+ ### Пять записей, которые смотрят на агента, а не на код
445
496
 
446
497
  Ruff, ESLint и gitleaks и так находят плохой код — AQK зовёт их, где может, вместо того чтобы
447
- писать своё. Эти четыре смотрят в другое место: на момент, когда **сигнал** о плохом коде
498
+ писать своё. Эти пять смотрят в другое место: на момент, когда **сигнал** о плохом коде
448
499
  выключают. Именно это делает агент, когда задача сформулирована как «сделай, чтобы прошло»:
449
500
 
450
501
  | Запись | Что ловит |
@@ -453,9 +504,15 @@ Ruff, ESLint и gitleaks и так находят плохой код — AQK з
453
504
  | `ci-actually-fails` | шаг конвейера, который выносит вердикт, но не может провалиться — `run: pytest \|\| true`, `continue-on-error: true` |
454
505
  | `test-has-assertion` | тест, который не может провалиться: пустое тело, `assert True`, пропуск без причины |
455
506
  | `promise-has-gate` | правило в `AGENTS.md`, у которого не назван сторож — ни гейт, ни, честно, человек |
507
+ | `protection-not-removed` | **сам прибор выключили**: гейт исчез из манифеста. Набор объявленной защиты может только расти; снять можно, но названно |
508
+
509
+ Пятая заведена позже остальных и по собственной шишке. Четыре первых ловят агента, когда он
510
+ выключает **сигнал**. А главный рубильник — сам манифест — не сторожил никто: замер 2026-09-09
511
+ показал, что удаление одной строки из `.aqk.yml` даёт зелёный прогон при секрете в коде, и этого
512
+ не заметили ни `doctor`, ни `report`, ни `vitals`, ни блок состояния для агента.
456
513
 
457
- Каждая измерена на девятнадцати чужих репозиториях (~25 000 файлов) до внесения в каталог, и ещё
458
- две записи этот же замер **отменил**: одну — потому что
514
+ Первые четыре измерены на девятнадцати чужих репозиториях (~25 000 файлов) до внесения в каталог,
515
+ и ещё две записи этот же замер **отменил**: одну — потому что
459
516
  [`agents-lint`](https://github.com/giacomo/agents-lint) делает это лучше, другую — потому что
460
517
  91 находка из 120 оказалась законным приёмом.
461
518
 
@@ -19,6 +19,7 @@
19
19
  | хотите понять, **как работать каждый день** | [Плейбук харнеса](agent-harness-playbook.md), раздел 18 |
20
20
  | хотите увидеть **процесс по этапам** | [SDLC эпохи агентов](ai-sdlc.md) |
21
21
  | ищете **готовое правило**, прежде чем писать своё | [Готовые правила](../ready-made-rules.md) |
22
+ | делаете **e2e для API** и хотите, чтобы он что-то доказывал | [Контракт API и e2e](../api-e2e.md) |
22
23
  | ставите **гейты** и хотите отличить работающий от мёртвого | `kit/gates/README.md` — норма записи, храповик, четыре способа вранья |
23
24
 
24
25
  ## 1. Наше — применяется в работе
@@ -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
+ человек прочтёт один раз, чем сторож, которому перестанут верить.
@@ -329,6 +329,91 @@ grep» — потому что мерили обход дерева, а не и
329
329
  Это ровно тот случай, когда своя запись законна, но раздел «готовый аналог» обязан назвать
330
330
  чужой инструмент и сказать, **почему не он**.
331
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
+
332
417
  ## Если готового нет
333
418
 
334
419
  Тогда свой гейт — и в его `README.md` пишется, **что именно проверено**: какой инструмент