agent-quality-kit 0.9.0 → 0.10.1
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.
- package/README.md +69 -10
- package/README.ru.md +68 -11
- package/kit/docs/ai/index.md +1 -0
- package/kit/docs/api-e2e.md +214 -0
- package/kit/docs/ready-made-rules.md +85 -0
- package/kit/gates/README.md +22 -0
- package/kit/gates/api-contract-has-arbiter/README.md +63 -0
- package/kit/gates/api-contract-has-arbiter/check.sh +117 -0
- package/kit/gates/api-contract-has-arbiter/gate.yml +15 -0
- package/kit/gates/api-contract-has-arbiter/green/.github/workflows/ci.yml +12 -0
- package/kit/gates/api-contract-has-arbiter/green/openapi.yaml +18 -0
- package/kit/gates/api-contract-has-arbiter/red/.github/workflows/ci.yml +11 -0
- package/kit/gates/api-contract-has-arbiter/red/openapi.yaml +18 -0
- package/kit/gates/ci-actually-fails/check.sh +9 -1
- package/kit/gates/commit-explains-itself/check.sh +15 -0
- package/kit/gates/complexity-limit/red/deep.go +17 -0
- package/kit/gates/complexity-limit/red/deep.rs +17 -0
- package/kit/gates/gate-not-weakened/red/suppress.go +5 -0
- package/kit/gates/gate-not-weakened/red/suppress.rs +3 -0
- package/kit/gates/protection-not-removed/README.md +67 -0
- package/kit/gates/protection-not-removed/check.sh +101 -0
- package/kit/gates/protection-not-removed/gate.yml +10 -0
- package/kit/gates/protection-not-removed/green/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/green/gates-declared.txt +4 -0
- package/kit/gates/protection-not-removed/red/.aqk.yml +7 -0
- package/kit/gates/protection-not-removed/red/gates-declared.txt +4 -0
- package/kit/gates/secrets-not-in-code/red/leak.go +9 -0
- package/kit/gates/secrets-not-in-code/red/leak.rs +5 -0
- package/kit/gates/todo-without-task/red/later.go +6 -0
- package/kit/gates/todo-without-task/red/later.rs +4 -0
- package/llms.txt +10 -4
- package/package.json +2 -1
- package/tool/commands/context.mjs +36 -1
- package/tool/commands/doctor.mjs +56 -2
- package/tool/commands/gates.mjs +2 -0
- package/tool/commands/probe.mjs +228 -0
- package/tool/commands/vitals.mjs +11 -3
- package/tool/i18n/en-docs.mjs +10 -0
- package/tool/i18n/en-gates.mjs +309 -0
- package/tool/i18n/en.mjs +10 -281
- package/tool/i18n/ru-docs.mjs +10 -0
- package/tool/i18n/ru-gates.mjs +311 -0
- package/tool/i18n/ru.mjs +10 -280
- package/tool/lib/cadence.mjs +57 -0
- package/tool/lib/core.mjs +1 -0
- package/tool/lib/evidence.mjs +15 -2
- package/tool/lib/execution.mjs +67 -0
- package/tool/lib/history.mjs +82 -0
- package/tool/lib/manifest.mjs +1 -1
- package/tool/lib/protection.mjs +52 -0
- package/tool/lib/prove.mjs +40 -13
- package/tool/lib/repo.mjs +12 -2
- package/tool/program.mjs +7 -0
- package/tool/selfcheck/smoke/_fixture.mjs +89 -0
- package/tool/selfcheck/smoke/api-contract.test.mjs +79 -0
- package/tool/selfcheck/smoke/commit-report.test.mjs +47 -0
- package/tool/selfcheck/smoke/protection.test.mjs +101 -0
- package/tool/selfcheck/smoke/release-tools.test.mjs +55 -0
- package/tool/selfcheck/smoke/verdict.test.mjs +40 -0
- package/tool/selfcheck/smoke.sh +194 -7
- package/tool/selfcheck/units-cadence.mjs +69 -0
- package/tool/selfcheck/units-context.mjs +24 -0
- package/tool/selfcheck/units-evidence.mjs +34 -0
- package/tool/selfcheck/units-execution.mjs +88 -0
- package/tool/selfcheck/units-level.mjs +32 -1
- package/tool/selfcheck/units-probe.mjs +100 -0
- package/tool/selfcheck/units-repo.mjs +31 -1
- 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.
|
|
343
|
+
rev: v0.10.1
|
|
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
|
[](https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
309
362
|
|
|
310
363
|
```yaml
|
|
311
|
-
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
364
|
+
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.10.1
|
|
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
|
|
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
|
-
|
|
333
|
-
there is nothing to build
|
|
334
|
-
|
|
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
|
-
###
|
|
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
|
|
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
|
-
|
|
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.
|
|
346
|
+
rev: v0.10.1
|
|
294
347
|
hooks:
|
|
295
348
|
- id: aqk # запускает объявленное; роняет коммит ниже AQK-1
|
|
296
349
|
# - id: aqk-doctor # только осмотр: уровень и чего не хватает, ничего не роняет
|
|
@@ -309,7 +362,7 @@ repos:
|
|
|
309
362
|
[](https://github.com/marketplace/actions/agent-quality-kit-aqk)
|
|
310
363
|
|
|
311
364
|
```yaml
|
|
312
|
-
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.
|
|
365
|
+
- uses: arsen-ask-lx/Agent_Quality_Kit@v0.10.1
|
|
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
|
|
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
|
-
|
|
334
|
-
|
|
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
|
-
|
|
458
|
-
две записи этот же замер **отменил**: одну — потому что
|
|
514
|
+
Первые четыре измерены на девятнадцати чужих репозиториях (~25 000 файлов) до внесения в каталог,
|
|
515
|
+
и ещё две записи этот же замер **отменил**: одну — потому что
|
|
459
516
|
[`agents-lint`](https://github.com/giacomo/agents-lint) делает это лучше, другую — потому что
|
|
460
517
|
91 находка из 120 оказалась законным приёмом.
|
|
461
518
|
|
package/kit/docs/ai/index.md
CHANGED
|
@@ -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` пишется, **что именно проверено**: какой инструмент
|