openapi-agent-ready 0.3.0__tar.gz → 0.4.1__tar.gz

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 (22) hide show
  1. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/CHANGELOG.md +25 -0
  2. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/PKG-INFO +66 -35
  3. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/README.md +64 -33
  4. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/pyproject.toml +1 -1
  5. openapi_agent_ready-0.4.1/src/agent_ready/baseline.py +137 -0
  6. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/cli.py +95 -4
  7. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/rubric.py +4 -3
  8. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/tests/test_audit.py +7 -0
  9. openapi_agent_ready-0.4.1/tests/test_baseline.py +306 -0
  10. openapi_agent_ready-0.3.0/openapi.yaml +0 -11890
  11. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/.github/workflows/ci.yml +0 -0
  12. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/.github/workflows/publish.yml +0 -0
  13. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/.gitignore +0 -0
  14. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/CONTRIBUTING.md +0 -0
  15. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/LICENSE +0 -0
  16. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/examples/agent_ready_booking_api.yaml +0 -0
  17. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/examples/sample_booking_api.yaml +0 -0
  18. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/__init__.py +0 -0
  19. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/auditor.py +0 -0
  20. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/mcp_generator.py +0 -0
  21. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/src/agent_ready/report_generator.py +0 -0
  22. {openapi_agent_ready-0.3.0 → openapi_agent_ready-0.4.1}/tests/test_mcp_conformance.py +0 -0
@@ -5,6 +5,31 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.4.1] - 2026-10-24
9
+
10
+ ### Fixed
11
+ - **Swagger 2.0 specs are now rejected (exit 2)** instead of being audited.
12
+ They have `paths`, so they were scored, but auth lives in
13
+ `securityDefinitions`, so a spec with scoped OAuth2 was reported as having no
14
+ security schemes at all.
15
+ - **Progress bar counts operations, not paths.** A spec with 100 paths × 3
16
+ methods (300 operations) previously never showed progress.
17
+
18
+ ## [0.4.0] - 2026-08-08
19
+
20
+ ### Added
21
+ - **Baseline mode** (`--write-baseline`, `--baseline`, `--max-new`). A mature
22
+ API audited for the first time produces hundreds of findings, so a score gate
23
+ is unusable on day one and gets switched off. Recording the current state lets
24
+ CI block only regressions while the backlog stays visible.
25
+
26
+ Findings are matched on category, endpoint, and severity rather than message
27
+ text — messages embed counts and parameter names ("3/3 parameters have no
28
+ description (a, b, c)") that shift when unrelated things change, which would
29
+ report unfixed problems as new. Severity is part of the identity so a warning
30
+ escalating to a fail is caught; message is excluded so rewording a finding in
31
+ a future release doesn't invalidate stored baselines.
32
+
8
33
  ## [0.3.0] - 2026-07-31
9
34
 
10
35
  ### Added
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: openapi-agent-ready
3
- Version: 0.3.0
3
+ Version: 0.4.1
4
4
  Summary: Audit any OpenAPI spec for AI-agent and MCP readiness, and generate an MCP server scaffold.
5
5
  Project-URL: Homepage, https://github.com/sfaisal/agent-ready
6
6
  Project-URL: Issues, https://github.com/sfaisal/agent-ready/issues
@@ -64,7 +64,7 @@ Description-Content-Type: text/markdown
64
64
  **Audit any OpenAPI spec for AI-agent readiness — then generate an MCP server scaffold for the endpoints that pass.**
65
65
 
66
66
  Your API works fine for human developers. That doesn't mean an AI agent can use
67
- it reliably. `agent-ready` scores a spec against seven categories of
67
+ it reliably. `agent-ready` scores a spec against eight categories of
68
68
  agent-specific failure, tells you exactly which endpoints will cause trouble and
69
69
  why, and can gate a CI pipeline so specs don't regress.
70
70
 
@@ -75,17 +75,18 @@ agent-ready https://example.com/openapi.yaml
75
75
 
76
76
  ```
77
77
  Generic Booking Platform API (v1.0)
78
- AI-readiness score: 54.4/100
79
- Endpoints: 4 Fails: 6 Warnings: 12
78
+ AI-readiness score: 58.4/100
79
+ Endpoints: 4 Fails: 7 Warnings: 14
80
80
 
81
81
  Category scores:
82
- Description Clarity 37.5/100
83
- Side Effects 70.0/100
84
- Error Responses 62.5/100
85
- Auth Clarity 50.0/100
86
- Ambiguity 100.0/100
87
- Parameter Explanation 16.7/100
88
- Usage Guidelines 50.0/100
82
+ Description Clarity 37.5/100
83
+ Side Effects 70.0/100
84
+ Error Responses 62.5/100
85
+ Auth Clarity 50.0/100
86
+ Ambiguity 100.0/100
87
+ Parameter Explanation 10.0/100
88
+ Usage Guidelines 50.0/100
89
+ Tool Surface 100.0/100
89
90
  ```
90
91
 
91
92
  ## Why this exists
@@ -111,11 +112,8 @@ directly onto the rubric:
111
112
  | **Frictionless authentication** | 4 (auth/scope clarity) |
112
113
  | **Structured error handling** | 3 (actionable errors) |
113
114
 
114
- Categories 2 (side effects) and 5 (ambiguity) have no counterpart in the
115
- article — they were added during implementation, once it became clear that
116
- unsignalled write operations and confusable endpoint descriptions cause agent
117
- failures as often as the original three. Category 8 came later still, from
118
- OpenAI's function-calling guidance.
115
+ Categories 2 (side effects) and 5 (ambiguity) weren't in the article. They came
116
+ up once I started building, and they matter as much as the original three.
119
117
 
120
118
  The gap is measurable, and enormous. A 2026 study of 856 tools across 103 MCP
121
119
  servers found that **97.1% of tool descriptions contained at least one quality
@@ -185,9 +183,55 @@ Exit codes:
185
183
  | Code | Meaning |
186
184
  |---|---|
187
185
  | `0` | All configured gates passed |
188
- | `1` | A gate failed (`--min-score` / `--max-fails`) |
186
+ | `1` | A gate failed (`--min-score` / `--max-fails` / `--max-new`) |
189
187
  | `2` | The spec could not be loaded or parsed |
190
188
 
189
+ ### Adopting on an existing API
190
+
191
+ A mature API audited for the first time will produce hundreds of findings. No
192
+ team can fix all of them before merging anything, so a score gate is unusable
193
+ on day one and gets switched off within a week.
194
+
195
+ Record the current state instead, and block only what gets worse:
196
+
197
+ ```bash
198
+ # once, when you adopt the tool — commit this file
199
+ agent-ready openapi.yaml --write-baseline .agent-ready-baseline.json
200
+
201
+ # in CI, from then on
202
+ agent-ready openapi.yaml --baseline .agent-ready-baseline.json --max-new 0
203
+ ```
204
+
205
+ Existing findings stay visible in the report but don't fail the build. A pull
206
+ request that introduces a new one does, and the output names exactly which
207
+ findings it added:
208
+
209
+ ```
210
+ Compared against .agent-ready-baseline.json: 5 new, 0 fixed, 11 unchanged
211
+ NEW FAIL GET /invoices — Description is a bare label (12 chars) ...
212
+ NEW FAIL GET /invoices — No error responses documented ...
213
+ ```
214
+
215
+ Findings are matched on category, endpoint, and severity — not on message text,
216
+ which contains counts and parameter names that shift when unrelated things
217
+ change. A warning escalating to a fail counts as new; rewording a finding in a
218
+ future release does not invalidate your baseline.
219
+
220
+ As the team fixes the backlog, regenerate the baseline to lock in the progress.
221
+
222
+ ### Python API
223
+
224
+ ```python
225
+ from agent_ready import load_spec, run_audit
226
+
227
+ spec = load_spec("openapi.yaml")
228
+ result = run_audit(spec)
229
+
230
+ print(result["overall_score"])
231
+ for finding in result["gaps"]:
232
+ print(finding.severity, finding.endpoint, finding.message)
233
+ ```
234
+
191
235
  ## Real-world results
192
236
 
193
237
  Running it against Stripe's production spec (587 endpoints), as of v0.1.0.
@@ -213,8 +257,8 @@ Two things worth drawing out.
213
257
  in this tool, not in Stripe.** Stripe documents errors via OpenAPI's `default`
214
258
  response rather than enumerating explicit 4xx/5xx codes. Perfectly valid, and
215
259
  arguably cleaner. The check only looked for `4`/`5` prefixes, so it failed all
216
- 587 endpoints. Fixed, regression-tested, and the reason the README says to run
217
- this against a real spec before trusting it.
260
+ 587 endpoints. Fixed and regression-tested — and a good reason to run this
261
+ against your own real spec before trusting the score.
218
262
 
219
263
  **Stripe is widely and correctly regarded as agent-ready, yet its raw OpenAPI
220
264
  spec scores 62.9.** That isn't a contradiction — it's the whole point. Stripe's
@@ -229,19 +273,6 @@ Genuine findings that survived scrutiny: `GET /v1/account` and
229
273
  uses bearer/basic API keys rather than scoped OAuth2, so there's no way to
230
274
  express "this agent may only read."
231
275
 
232
- ### Python API
233
-
234
- ```python
235
- from agent_ready import load_spec, run_audit
236
-
237
- spec = load_spec("openapi.yaml")
238
- result = run_audit(spec)
239
-
240
- print(result["overall_score"])
241
- for finding in result["gaps"]:
242
- print(finding.severity, finding.endpoint, finding.message)
243
- ```
244
-
245
276
  ## The eight categories
246
277
 
247
278
  | # | Category | What it catches | Published anchor |
@@ -305,7 +336,7 @@ doesn't:
305
336
 
306
337
  - **The ambiguity check uses string similarity**, so it misses endpoints that
307
338
  are semantically similar but differently worded. Embedding-based similarity
308
- would be stronger. ([#1](https://github.com/sfaisal/agent-ready/issues))
339
+ would be stronger. ([#1](https://github.com/sfaisal/agent-ready/issues/1))
309
340
  - **Category weights are judgement calls, not measured effect sizes.**
310
341
  Description clarity is weighted highest on the prior that wrong tool selection
311
342
  is the costliest failure — that prior is untested here.
@@ -328,7 +359,7 @@ doesn't:
328
359
  - [ ] LLM-assisted rewriting of flagged descriptions (`--fix`)
329
360
  - [ ] Live tool-selection testing against a generated MCP server
330
361
  - [ ] GitHub Action wrapper
331
- - [ ] Baseline mode (`--baseline`) so CI only fails on *new* regressions
362
+ - [x] Baseline mode (`--baseline`) so CI only fails on *new* regressions
332
363
 
333
364
  ## Contributing
334
365
 
@@ -9,7 +9,7 @@
9
9
  **Audit any OpenAPI spec for AI-agent readiness — then generate an MCP server scaffold for the endpoints that pass.**
10
10
 
11
11
  Your API works fine for human developers. That doesn't mean an AI agent can use
12
- it reliably. `agent-ready` scores a spec against seven categories of
12
+ it reliably. `agent-ready` scores a spec against eight categories of
13
13
  agent-specific failure, tells you exactly which endpoints will cause trouble and
14
14
  why, and can gate a CI pipeline so specs don't regress.
15
15
 
@@ -20,17 +20,18 @@ agent-ready https://example.com/openapi.yaml
20
20
 
21
21
  ```
22
22
  Generic Booking Platform API (v1.0)
23
- AI-readiness score: 54.4/100
24
- Endpoints: 4 Fails: 6 Warnings: 12
23
+ AI-readiness score: 58.4/100
24
+ Endpoints: 4 Fails: 7 Warnings: 14
25
25
 
26
26
  Category scores:
27
- Description Clarity 37.5/100
28
- Side Effects 70.0/100
29
- Error Responses 62.5/100
30
- Auth Clarity 50.0/100
31
- Ambiguity 100.0/100
32
- Parameter Explanation 16.7/100
33
- Usage Guidelines 50.0/100
27
+ Description Clarity 37.5/100
28
+ Side Effects 70.0/100
29
+ Error Responses 62.5/100
30
+ Auth Clarity 50.0/100
31
+ Ambiguity 100.0/100
32
+ Parameter Explanation 10.0/100
33
+ Usage Guidelines 50.0/100
34
+ Tool Surface 100.0/100
34
35
  ```
35
36
 
36
37
  ## Why this exists
@@ -56,11 +57,8 @@ directly onto the rubric:
56
57
  | **Frictionless authentication** | 4 (auth/scope clarity) |
57
58
  | **Structured error handling** | 3 (actionable errors) |
58
59
 
59
- Categories 2 (side effects) and 5 (ambiguity) have no counterpart in the
60
- article — they were added during implementation, once it became clear that
61
- unsignalled write operations and confusable endpoint descriptions cause agent
62
- failures as often as the original three. Category 8 came later still, from
63
- OpenAI's function-calling guidance.
60
+ Categories 2 (side effects) and 5 (ambiguity) weren't in the article. They came
61
+ up once I started building, and they matter as much as the original three.
64
62
 
65
63
  The gap is measurable, and enormous. A 2026 study of 856 tools across 103 MCP
66
64
  servers found that **97.1% of tool descriptions contained at least one quality
@@ -130,9 +128,55 @@ Exit codes:
130
128
  | Code | Meaning |
131
129
  |---|---|
132
130
  | `0` | All configured gates passed |
133
- | `1` | A gate failed (`--min-score` / `--max-fails`) |
131
+ | `1` | A gate failed (`--min-score` / `--max-fails` / `--max-new`) |
134
132
  | `2` | The spec could not be loaded or parsed |
135
133
 
134
+ ### Adopting on an existing API
135
+
136
+ A mature API audited for the first time will produce hundreds of findings. No
137
+ team can fix all of them before merging anything, so a score gate is unusable
138
+ on day one and gets switched off within a week.
139
+
140
+ Record the current state instead, and block only what gets worse:
141
+
142
+ ```bash
143
+ # once, when you adopt the tool — commit this file
144
+ agent-ready openapi.yaml --write-baseline .agent-ready-baseline.json
145
+
146
+ # in CI, from then on
147
+ agent-ready openapi.yaml --baseline .agent-ready-baseline.json --max-new 0
148
+ ```
149
+
150
+ Existing findings stay visible in the report but don't fail the build. A pull
151
+ request that introduces a new one does, and the output names exactly which
152
+ findings it added:
153
+
154
+ ```
155
+ Compared against .agent-ready-baseline.json: 5 new, 0 fixed, 11 unchanged
156
+ NEW FAIL GET /invoices — Description is a bare label (12 chars) ...
157
+ NEW FAIL GET /invoices — No error responses documented ...
158
+ ```
159
+
160
+ Findings are matched on category, endpoint, and severity — not on message text,
161
+ which contains counts and parameter names that shift when unrelated things
162
+ change. A warning escalating to a fail counts as new; rewording a finding in a
163
+ future release does not invalidate your baseline.
164
+
165
+ As the team fixes the backlog, regenerate the baseline to lock in the progress.
166
+
167
+ ### Python API
168
+
169
+ ```python
170
+ from agent_ready import load_spec, run_audit
171
+
172
+ spec = load_spec("openapi.yaml")
173
+ result = run_audit(spec)
174
+
175
+ print(result["overall_score"])
176
+ for finding in result["gaps"]:
177
+ print(finding.severity, finding.endpoint, finding.message)
178
+ ```
179
+
136
180
  ## Real-world results
137
181
 
138
182
  Running it against Stripe's production spec (587 endpoints), as of v0.1.0.
@@ -158,8 +202,8 @@ Two things worth drawing out.
158
202
  in this tool, not in Stripe.** Stripe documents errors via OpenAPI's `default`
159
203
  response rather than enumerating explicit 4xx/5xx codes. Perfectly valid, and
160
204
  arguably cleaner. The check only looked for `4`/`5` prefixes, so it failed all
161
- 587 endpoints. Fixed, regression-tested, and the reason the README says to run
162
- this against a real spec before trusting it.
205
+ 587 endpoints. Fixed and regression-tested — and a good reason to run this
206
+ against your own real spec before trusting the score.
163
207
 
164
208
  **Stripe is widely and correctly regarded as agent-ready, yet its raw OpenAPI
165
209
  spec scores 62.9.** That isn't a contradiction — it's the whole point. Stripe's
@@ -174,19 +218,6 @@ Genuine findings that survived scrutiny: `GET /v1/account` and
174
218
  uses bearer/basic API keys rather than scoped OAuth2, so there's no way to
175
219
  express "this agent may only read."
176
220
 
177
- ### Python API
178
-
179
- ```python
180
- from agent_ready import load_spec, run_audit
181
-
182
- spec = load_spec("openapi.yaml")
183
- result = run_audit(spec)
184
-
185
- print(result["overall_score"])
186
- for finding in result["gaps"]:
187
- print(finding.severity, finding.endpoint, finding.message)
188
- ```
189
-
190
221
  ## The eight categories
191
222
 
192
223
  | # | Category | What it catches | Published anchor |
@@ -250,7 +281,7 @@ doesn't:
250
281
 
251
282
  - **The ambiguity check uses string similarity**, so it misses endpoints that
252
283
  are semantically similar but differently worded. Embedding-based similarity
253
- would be stronger. ([#1](https://github.com/sfaisal/agent-ready/issues))
284
+ would be stronger. ([#1](https://github.com/sfaisal/agent-ready/issues/1))
254
285
  - **Category weights are judgement calls, not measured effect sizes.**
255
286
  Description clarity is weighted highest on the prior that wrong tool selection
256
287
  is the costliest failure — that prior is untested here.
@@ -273,7 +304,7 @@ doesn't:
273
304
  - [ ] LLM-assisted rewriting of flagged descriptions (`--fix`)
274
305
  - [ ] Live tool-selection testing against a generated MCP server
275
306
  - [ ] GitHub Action wrapper
276
- - [ ] Baseline mode (`--baseline`) so CI only fails on *new* regressions
307
+ - [x] Baseline mode (`--baseline`) so CI only fails on *new* regressions
277
308
 
278
309
  ## Contributing
279
310
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openapi-agent-ready"
7
- version = "0.3.0"
7
+ version = "0.4.1"
8
8
  description = "Audit any OpenAPI spec for AI-agent and MCP readiness, and generate an MCP server scaffold."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,137 @@
1
+ """
2
+ Baseline support: fail CI only on *new* findings.
3
+
4
+ The problem this solves: a mature API audited for the first time produces
5
+ hundreds of findings. A team cannot fix them all before merging anything, so
6
+ `--min-score` and `--max-fails` are unusable as gates and get switched off
7
+ within a week. A baseline records the current state and lets the gate fail only
8
+ when something gets worse.
9
+
10
+ The whole design rests on one question: what makes a finding "the same finding"
11
+ across two runs? Message text is the obvious answer and the wrong one — it
12
+ embeds counts and parameter names ("3/3 parameters have no description
13
+ (date_from, date_to, location)") that change when an unrelated parameter is
14
+ added, which would report a fixed finding as new.
15
+
16
+ So findings are fingerprinted on (category, endpoint, severity) instead:
17
+
18
+ - **category** — which rule fired.
19
+ - **endpoint** — where. Stable across edits to the endpoint's content.
20
+ - **severity** — a warning becoming a fail is a regression worth catching, so
21
+ it belongs in the identity rather than being treated as the same finding.
22
+
23
+ Message is deliberately excluded. Rewording a finding's text in a future
24
+ release must not invalidate every stored baseline.
25
+ """
26
+
27
+ import json
28
+ from collections import Counter
29
+ from dataclasses import asdict
30
+ from pathlib import Path
31
+
32
+ BASELINE_VERSION = 1
33
+
34
+
35
+ class BaselineError(Exception):
36
+ """Raised when a baseline file can't be read or is malformed."""
37
+
38
+
39
+ def fingerprint(finding) -> str:
40
+ """A stable identity for a finding across runs. See module docstring."""
41
+ return f"{finding.category}|{finding.endpoint}|{finding.severity}"
42
+
43
+
44
+ def _counted(findings) -> Counter:
45
+ """
46
+ Fingerprints with multiplicity.
47
+
48
+ A single endpoint can legitimately produce two findings in one category —
49
+ for example a parameter-explanation fail for undocumented parameters plus a
50
+ warning for a missing date format. Counting rather than set-membership means
51
+ going from one such finding to two is correctly reported as new.
52
+ """
53
+ return Counter(fingerprint(f) for f in findings)
54
+
55
+
56
+ def write_baseline(path: str, spec: dict, result: dict) -> dict:
57
+ """Record the current findings so future runs can compare against them."""
58
+ info = spec.get("info") or {}
59
+ payload = {
60
+ "baseline_version": BASELINE_VERSION,
61
+ "api": {
62
+ "title": info.get("title", "Untitled API"),
63
+ "version": str(info.get("version", "unknown")),
64
+ },
65
+ "overall_score": result["overall_score"],
66
+ "endpoint_count": result["endpoint_count"],
67
+ "fingerprints": sorted(_counted(result["gaps"]).elements()),
68
+ # Stored for human readability when reviewing a baseline diff; never
69
+ # read back during comparison.
70
+ "findings": [asdict(f) for f in result["gaps"]],
71
+ }
72
+ Path(path).write_text(json.dumps(payload, indent=2), encoding="utf-8")
73
+ return payload
74
+
75
+
76
+ def load_baseline(path: str) -> dict:
77
+ p = Path(path)
78
+ if not p.exists():
79
+ raise BaselineError(
80
+ f"Baseline file not found: {path}\n"
81
+ f"Create one from the current state with: --write-baseline {path}"
82
+ )
83
+ try:
84
+ data = json.loads(p.read_text(encoding="utf-8"))
85
+ except json.JSONDecodeError as exc:
86
+ raise BaselineError(f"Baseline file is not valid JSON: {exc}") from exc
87
+
88
+ if not isinstance(data, dict) or "fingerprints" not in data:
89
+ raise BaselineError(
90
+ f"{path} does not look like an agent-ready baseline "
91
+ "(no 'fingerprints' key)."
92
+ )
93
+
94
+ version = data.get("baseline_version", 0)
95
+ if version > BASELINE_VERSION:
96
+ raise BaselineError(
97
+ f"Baseline was written by a newer version of agent-ready "
98
+ f"(format v{version}, this build understands v{BASELINE_VERSION}). "
99
+ "Upgrade agent-ready or regenerate the baseline."
100
+ )
101
+ return data
102
+
103
+
104
+ def compare_to_baseline(result: dict, baseline: dict) -> dict:
105
+ """
106
+ Classify current findings against a baseline.
107
+
108
+ Returns counts plus the actual new findings, so the CLI can show a team
109
+ exactly what their change introduced rather than the whole backlog.
110
+ """
111
+ baseline_counts = Counter(baseline.get("fingerprints", []))
112
+ current_counts = _counted(result["gaps"])
113
+
114
+ # A fingerprint appearing more often than the baseline recorded is new,
115
+ # by the excess amount.
116
+ new_counts = current_counts - baseline_counts
117
+ fixed_counts = baseline_counts - current_counts
118
+
119
+ # Map fingerprints back to findings so the report can name them. Consume
120
+ # the allowance so duplicates beyond it are still reported.
121
+ remaining = Counter(new_counts)
122
+ new_findings = []
123
+ for f in result["gaps"]:
124
+ fp = fingerprint(f)
125
+ if remaining.get(fp, 0) > 0:
126
+ new_findings.append(f)
127
+ remaining[fp] -= 1
128
+
129
+ return {
130
+ "new_findings": new_findings,
131
+ "new_count": sum(new_counts.values()),
132
+ "fixed_count": sum(fixed_counts.values()),
133
+ "unchanged_count": sum((current_counts & baseline_counts).values()),
134
+ "new_fails": sum(1 for f in new_findings if f.severity == "fail"),
135
+ "baseline_score": baseline.get("overall_score"),
136
+ "current_score": result["overall_score"],
137
+ }
@@ -3,8 +3,8 @@ Command-line interface for agent-ready.
3
3
 
4
4
  Exit codes (designed for CI use):
5
5
  0 audit passed all configured gates
6
- 1 audit ran, but a gate failed (score below --min-score, or
7
- --max-fails exceeded)
6
+ 1 audit ran, but a gate failed (score below --min-score,
7
+ --max-fails exceeded, or --max-new exceeded against --baseline)
8
8
  2 the spec could not be loaded or parsed
9
9
  """
10
10
 
@@ -17,10 +17,17 @@ from . import __version__
17
17
  from .auditor import (
18
18
  SpecLoadError,
19
19
  endpoints_ready_for_mcp,
20
+ extract_endpoints,
20
21
  load_spec,
21
22
  result_to_dict,
22
23
  run_audit,
23
24
  )
25
+ from .baseline import (
26
+ BaselineError,
27
+ compare_to_baseline,
28
+ load_baseline,
29
+ write_baseline,
30
+ )
24
31
  from .mcp_generator import generate_mcp_scaffold
25
32
  from .report_generator import generate_report
26
33
 
@@ -38,7 +45,12 @@ def build_parser() -> argparse.ArgumentParser:
38
45
  ),
39
46
  epilog=(
40
47
  "CI example:\n"
41
- " agent-ready openapi.yaml --min-score 70 --max-fails 0 --quiet\n"
48
+ " agent-ready openapi.yaml --min-score 60 --max-fails 0 --quiet\n"
49
+ "\n"
50
+ "Adopting on an existing API, where fixing everything first is not\n"
51
+ "realistic — record today's findings, then block only regressions:\n"
52
+ " agent-ready openapi.yaml --write-baseline .agent-ready-baseline.json\n"
53
+ " agent-ready openapi.yaml --baseline .agent-ready-baseline.json --max-new 0\n"
42
54
  ),
43
55
  formatter_class=argparse.RawDescriptionHelpFormatter,
44
56
  )
@@ -60,6 +72,31 @@ def build_parser() -> argparse.ArgumentParser:
60
72
  type=int,
61
73
  help="Exit 1 if the number of FAIL findings exceeds this value (CI gate)",
62
74
  )
75
+ parser.add_argument(
76
+ "--write-baseline",
77
+ metavar="PATH",
78
+ help=(
79
+ "Record current findings to PATH as an accepted baseline. Run this "
80
+ "once when adopting the tool on an existing API."
81
+ ),
82
+ )
83
+ parser.add_argument(
84
+ "--baseline",
85
+ metavar="PATH",
86
+ help=(
87
+ "Compare against a baseline file and report what changed. Combine "
88
+ "with --max-new to fail only on regressions."
89
+ ),
90
+ )
91
+ parser.add_argument(
92
+ "--max-new",
93
+ type=int,
94
+ metavar="N",
95
+ help=(
96
+ "Exit 1 if more than N findings are new since --baseline. "
97
+ "Use --max-new 0 to block any regression."
98
+ ),
99
+ )
63
100
  parser.add_argument(
64
101
  "--generate-mcp",
65
102
  metavar="PATH",
@@ -147,6 +184,16 @@ def _run(argv=None) -> int:
147
184
  print(f"error: {exc}", file=sys.stderr)
148
185
  return EXIT_SPEC_ERROR
149
186
 
187
+ # Swagger 2.0 has `paths` too, but keeps auth in `securityDefinitions` and
188
+ # bodies in `in: body` params, so auditing it yields confidently wrong scores.
189
+ if isinstance(spec, dict) and "swagger" in spec:
190
+ print(
191
+ f"error: Swagger {spec['swagger']} is not supported — only OpenAPI 3.x. "
192
+ "Convert it first (e.g. with swagger2openapi).",
193
+ file=sys.stderr,
194
+ )
195
+ return EXIT_SPEC_ERROR
196
+
150
197
  if not isinstance(spec, dict) or not spec.get("paths"):
151
198
  print(
152
199
  "error: no 'paths' found — this does not look like an OpenAPI spec.",
@@ -154,7 +201,8 @@ def _run(argv=None) -> int:
154
201
  )
155
202
  return EXIT_SPEC_ERROR
156
203
 
157
- endpoint_count = len(spec.get("paths") or {})
204
+ # Operations, not paths — the audit (and the ambiguity check) is per operation.
205
+ endpoint_count = len(extract_endpoints(spec))
158
206
  result = run_audit(spec, progress=_make_progress(endpoint_count, args.quiet))
159
207
 
160
208
  if result["endpoint_count"] == 0:
@@ -185,6 +233,37 @@ def _run(argv=None) -> int:
185
233
  f"({len(ready)}/{result['endpoint_count']} endpoints included)"
186
234
  )
187
235
 
236
+ # Baseline: write, or compare and gate on regressions only.
237
+ if args.write_baseline:
238
+ write_baseline(args.write_baseline, spec, result)
239
+ if not args.quiet:
240
+ print(
241
+ f"Baseline written to {args.write_baseline} "
242
+ f"({len(result['gaps'])} finding(s) recorded as accepted)"
243
+ )
244
+
245
+ comparison = None
246
+ if args.baseline:
247
+ try:
248
+ comparison = compare_to_baseline(result, load_baseline(args.baseline))
249
+ except BaselineError as exc:
250
+ print(f"error: {exc}", file=sys.stderr)
251
+ return EXIT_SPEC_ERROR
252
+
253
+ if not args.quiet:
254
+ print()
255
+ print(
256
+ f"Compared against {args.baseline}: "
257
+ f"{comparison['new_count']} new, "
258
+ f"{comparison['fixed_count']} fixed, "
259
+ f"{comparison['unchanged_count']} unchanged"
260
+ )
261
+ for f in comparison["new_findings"][:20]:
262
+ marker = "FAIL" if f.severity == "fail" else "WARN"
263
+ print(f" NEW {marker} {f.endpoint} — {f.message}")
264
+ if len(comparison["new_findings"]) > 20:
265
+ print(f" ... and {len(comparison['new_findings']) - 20} more")
266
+
188
267
  # CI gates
189
268
  gate_failed = False
190
269
  if args.min_score is not None and result["overall_score"] < args.min_score:
@@ -202,6 +281,18 @@ def _run(argv=None) -> int:
202
281
  )
203
282
  gate_failed = True
204
283
 
284
+ if (
285
+ comparison is not None
286
+ and args.max_new is not None
287
+ and comparison["new_count"] > args.max_new
288
+ ):
289
+ print(
290
+ f"GATE FAILED: {comparison['new_count']} new finding(s) since "
291
+ f"baseline exceeds maximum {args.max_new}",
292
+ file=sys.stderr,
293
+ )
294
+ gate_failed = True
295
+
205
296
  return EXIT_GATE_FAILED if gate_failed else EXIT_OK
206
297
 
207
298