specshift 1.3.0__tar.gz → 1.4.0__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.
- {specshift-1.3.0/specshift.egg-info → specshift-1.4.0}/PKG-INFO +46 -3
- {specshift-1.3.0 → specshift-1.4.0}/README.md +43 -0
- {specshift-1.3.0 → specshift-1.4.0}/pyproject.toml +3 -2
- {specshift-1.3.0 → specshift-1.4.0}/specshift/__init__.py +3 -1
- {specshift-1.3.0 → specshift-1.4.0}/specshift/ai_summary.py +80 -13
- {specshift-1.3.0 → specshift-1.4.0}/specshift/cli.py +23 -7
- specshift-1.4.0/specshift/linter.py +326 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/reporter.py +61 -46
- {specshift-1.3.0 → specshift-1.4.0/specshift.egg-info}/PKG-INFO +46 -3
- {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/SOURCES.txt +2 -0
- {specshift-1.3.0 → specshift-1.4.0}/tests/test_cli.py +26 -0
- specshift-1.4.0/tests/test_linter.py +166 -0
- {specshift-1.3.0 → specshift-1.4.0}/LICENSE +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/setup.cfg +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/changelog.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/config.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/differ.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/git_utils.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/graphql_differ.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/models.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/notifier.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/protobuf_differ.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/spec_loader.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift/webapp.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/dependency_links.txt +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/entry_points.txt +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/requires.txt +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/top_level.txt +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/tests/test_changelog.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/tests/test_differ.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/tests/test_graphql_differ.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/tests/test_protobuf_differ.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/tests/test_reporter.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/tests/test_spec_loader.py +0 -0
- {specshift-1.3.0 → specshift-1.4.0}/tests/test_webapp.py +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: specshift
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: Detects, classifies, and optionally summarizes breaking changes in OpenAPI/Swagger, GraphQL, and Protobuf/gRPC contracts using AI
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Detects, classifies, and optionally summarizes breaking changes and best-practice issues in OpenAPI/Swagger, GraphQL, and Protobuf/gRPC contracts using AI
|
|
5
5
|
Author: Lethe044
|
|
6
6
|
License: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Lethe044/specshift
|
|
8
8
|
Project-URL: Repository, https://github.com/Lethe044/specshift
|
|
9
9
|
Project-URL: Issues, https://github.com/Lethe044/specshift/issues
|
|
10
10
|
Project-URL: Changelog, https://github.com/Lethe044/specshift/blob/main/CHANGELOG.md
|
|
11
|
-
Keywords: openapi,swagger,graphql,grpc,protobuf,api,breaking-changes,contract-testing,api-diff,devtools,cli,ci-cd,github-actions,sarif,changelog
|
|
11
|
+
Keywords: openapi,swagger,graphql,grpc,protobuf,api,breaking-changes,contract-testing,api-diff,api-linter,devtools,cli,ci-cd,github-actions,sarif,changelog
|
|
12
12
|
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Environment :: Console
|
|
14
14
|
Classifier: Intended Audience :: Developers
|
|
@@ -128,6 +128,11 @@ requirement.
|
|
|
128
128
|
file's history across git tags and produces a single Markdown document
|
|
129
129
|
summarizing what changed (and whether it was breaking) between every
|
|
130
130
|
release, plus an "Unreleased" section for the current working tree.
|
|
131
|
+
- **Spec linting**: `specshift lint` reviews a single OpenAPI document for
|
|
132
|
+
spec-validity problems (undeclared path parameters, missing response
|
|
133
|
+
descriptions, undefined security schemes, duplicate operationIds) and
|
|
134
|
+
best-practice gaps (missing summaries, missing error responses,
|
|
135
|
+
inconsistent path naming, unused schemas), independent of any diff.
|
|
131
136
|
- **Five output formats**: a colored console table, a Markdown report
|
|
132
137
|
(ideal for PR comments), JSON (for integrating with other tools), a
|
|
133
138
|
standalone filterable HTML report, and SARIF 2.1.0 for GitHub Code
|
|
@@ -310,6 +315,32 @@ Options: `--tag-pattern` (glob, default `*`, e.g. `v*` to only consider
|
|
|
310
315
|
tags starting with `v`), `--no-working-tree` to skip the "Unreleased"
|
|
311
316
|
section, and `--output` to write to a file instead of stdout.
|
|
312
317
|
|
|
318
|
+
### `specshift lint <spec>`
|
|
319
|
+
|
|
320
|
+
Reviews a single OpenAPI/Swagger document for spec-validity problems and
|
|
321
|
+
best-practice gaps, independent of comparing it to any other version.
|
|
322
|
+
Supports the same `--format`, `--output`, `--ai`, `--fail-on`, and
|
|
323
|
+
`--artifact-path` options as `diff`.
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
specshift lint openapi.yaml
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
Bookstore API : 1.0.0
|
|
331
|
+
2 breaking, 3 warning, 1 info issues found.
|
|
332
|
+
|
|
333
|
+
[BREAKING] GET /books/{bookId} :: Path template parameter '{bookId}' is used in the path but not declared as a path parameter for this operation.
|
|
334
|
+
[BREAKING] GET /books/{bookId} > response 200 :: Response has no description. The OpenAPI spec requires every response to have one.
|
|
335
|
+
[WARNING] POST /books :: Operation has no operationId; many code generators rely on this to name functions.
|
|
336
|
+
...
|
|
337
|
+
|
|
338
|
+
Result: 2 issue(s) should be fixed for spec validity.
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
See [When SpecShift calls something breaking](#when-specshift-calls-something-breaking)
|
|
342
|
+
below for what each severity means in lint results specifically.
|
|
343
|
+
|
|
313
344
|
### `specshift init`
|
|
314
345
|
|
|
315
346
|
Creates a sample `.specshift.yml` file.
|
|
@@ -451,6 +482,16 @@ than request/response direction: adding fields or rpc methods is always
|
|
|
451
482
|
safe, changing a field's number is always breaking, and type changes are
|
|
452
483
|
judged by wire type category (see the `diff-proto` section above).
|
|
453
484
|
|
|
485
|
+
`specshift lint` follows a different model too, since it reviews one
|
|
486
|
+
document rather than comparing two: **breaking** means the document
|
|
487
|
+
violates something the OpenAPI spec itself requires (undeclared path
|
|
488
|
+
parameters, a response with no description, an undefined security
|
|
489
|
+
scheme), which can cause strict tooling or code generators to reject the
|
|
490
|
+
document outright; **warning** means a common best-practice gap (missing
|
|
491
|
+
summaries, missing operationIds, undocumented error responses,
|
|
492
|
+
inconsistent naming); **info** means a minor, low-priority suggestion
|
|
493
|
+
(an apparently unused schema).
|
|
494
|
+
|
|
454
495
|
## Comparison with other tools
|
|
455
496
|
|
|
456
497
|
| | SpecShift | Raw JSON/YAML diff | oasdiff / openapi-diff style tools |
|
|
@@ -458,6 +499,7 @@ judged by wire type category (see the `diff-proto` section above).
|
|
|
458
499
|
| Context-aware classification | Yes | No | Partially |
|
|
459
500
|
| GraphQL support | Yes | No | Rarely |
|
|
460
501
|
| Protobuf/gRPC support (wire-aware) | Yes | No | Rarely |
|
|
502
|
+
| Single-document linting (not just diffing) | Yes | No | Rarely |
|
|
461
503
|
| Natural-language summary | Yes (optional) | No | No |
|
|
462
504
|
| Standalone HTML report | Yes | No | Rarely |
|
|
463
505
|
| Local interactive dashboard | Yes | No | Rarely |
|
|
@@ -474,6 +516,7 @@ This project is under active development. Some planned areas:
|
|
|
474
516
|
- Path parameter pattern (regex) and discriminator-level rule refinements
|
|
475
517
|
- Deeper GraphQL support: directive-aware deprecation reasons, custom
|
|
476
518
|
scalar compatibility hints, and federation-aware schema composition
|
|
519
|
+
- Extending `specshift lint` to GraphQL and Protobuf schemas
|
|
477
520
|
- Optional shared/hosted history for the `serve` dashboard (currently
|
|
478
521
|
in-memory and per-session by design)
|
|
479
522
|
- Changelog generation from raw commit history (not just tags), for
|
|
@@ -89,6 +89,11 @@ requirement.
|
|
|
89
89
|
file's history across git tags and produces a single Markdown document
|
|
90
90
|
summarizing what changed (and whether it was breaking) between every
|
|
91
91
|
release, plus an "Unreleased" section for the current working tree.
|
|
92
|
+
- **Spec linting**: `specshift lint` reviews a single OpenAPI document for
|
|
93
|
+
spec-validity problems (undeclared path parameters, missing response
|
|
94
|
+
descriptions, undefined security schemes, duplicate operationIds) and
|
|
95
|
+
best-practice gaps (missing summaries, missing error responses,
|
|
96
|
+
inconsistent path naming, unused schemas), independent of any diff.
|
|
92
97
|
- **Five output formats**: a colored console table, a Markdown report
|
|
93
98
|
(ideal for PR comments), JSON (for integrating with other tools), a
|
|
94
99
|
standalone filterable HTML report, and SARIF 2.1.0 for GitHub Code
|
|
@@ -271,6 +276,32 @@ Options: `--tag-pattern` (glob, default `*`, e.g. `v*` to only consider
|
|
|
271
276
|
tags starting with `v`), `--no-working-tree` to skip the "Unreleased"
|
|
272
277
|
section, and `--output` to write to a file instead of stdout.
|
|
273
278
|
|
|
279
|
+
### `specshift lint <spec>`
|
|
280
|
+
|
|
281
|
+
Reviews a single OpenAPI/Swagger document for spec-validity problems and
|
|
282
|
+
best-practice gaps, independent of comparing it to any other version.
|
|
283
|
+
Supports the same `--format`, `--output`, `--ai`, `--fail-on`, and
|
|
284
|
+
`--artifact-path` options as `diff`.
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
specshift lint openapi.yaml
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
Bookstore API : 1.0.0
|
|
292
|
+
2 breaking, 3 warning, 1 info issues found.
|
|
293
|
+
|
|
294
|
+
[BREAKING] GET /books/{bookId} :: Path template parameter '{bookId}' is used in the path but not declared as a path parameter for this operation.
|
|
295
|
+
[BREAKING] GET /books/{bookId} > response 200 :: Response has no description. The OpenAPI spec requires every response to have one.
|
|
296
|
+
[WARNING] POST /books :: Operation has no operationId; many code generators rely on this to name functions.
|
|
297
|
+
...
|
|
298
|
+
|
|
299
|
+
Result: 2 issue(s) should be fixed for spec validity.
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
See [When SpecShift calls something breaking](#when-specshift-calls-something-breaking)
|
|
303
|
+
below for what each severity means in lint results specifically.
|
|
304
|
+
|
|
274
305
|
### `specshift init`
|
|
275
306
|
|
|
276
307
|
Creates a sample `.specshift.yml` file.
|
|
@@ -412,6 +443,16 @@ than request/response direction: adding fields or rpc methods is always
|
|
|
412
443
|
safe, changing a field's number is always breaking, and type changes are
|
|
413
444
|
judged by wire type category (see the `diff-proto` section above).
|
|
414
445
|
|
|
446
|
+
`specshift lint` follows a different model too, since it reviews one
|
|
447
|
+
document rather than comparing two: **breaking** means the document
|
|
448
|
+
violates something the OpenAPI spec itself requires (undeclared path
|
|
449
|
+
parameters, a response with no description, an undefined security
|
|
450
|
+
scheme), which can cause strict tooling or code generators to reject the
|
|
451
|
+
document outright; **warning** means a common best-practice gap (missing
|
|
452
|
+
summaries, missing operationIds, undocumented error responses,
|
|
453
|
+
inconsistent naming); **info** means a minor, low-priority suggestion
|
|
454
|
+
(an apparently unused schema).
|
|
455
|
+
|
|
415
456
|
## Comparison with other tools
|
|
416
457
|
|
|
417
458
|
| | SpecShift | Raw JSON/YAML diff | oasdiff / openapi-diff style tools |
|
|
@@ -419,6 +460,7 @@ judged by wire type category (see the `diff-proto` section above).
|
|
|
419
460
|
| Context-aware classification | Yes | No | Partially |
|
|
420
461
|
| GraphQL support | Yes | No | Rarely |
|
|
421
462
|
| Protobuf/gRPC support (wire-aware) | Yes | No | Rarely |
|
|
463
|
+
| Single-document linting (not just diffing) | Yes | No | Rarely |
|
|
422
464
|
| Natural-language summary | Yes (optional) | No | No |
|
|
423
465
|
| Standalone HTML report | Yes | No | Rarely |
|
|
424
466
|
| Local interactive dashboard | Yes | No | Rarely |
|
|
@@ -435,6 +477,7 @@ This project is under active development. Some planned areas:
|
|
|
435
477
|
- Path parameter pattern (regex) and discriminator-level rule refinements
|
|
436
478
|
- Deeper GraphQL support: directive-aware deprecation reasons, custom
|
|
437
479
|
scalar compatibility hints, and federation-aware schema composition
|
|
480
|
+
- Extending `specshift lint` to GraphQL and Protobuf schemas
|
|
438
481
|
- Optional shared/hosted history for the `serve` dashboard (currently
|
|
439
482
|
in-memory and per-session by design)
|
|
440
483
|
- Changelog generation from raw commit history (not just tags), for
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specshift"
|
|
7
|
-
version = "1.
|
|
8
|
-
description = "Detects, classifies, and optionally summarizes breaking changes in OpenAPI/Swagger, GraphQL, and Protobuf/gRPC contracts using AI"
|
|
7
|
+
version = "1.4.0"
|
|
8
|
+
description = "Detects, classifies, and optionally summarizes breaking changes and best-practice issues in OpenAPI/Swagger, GraphQL, and Protobuf/gRPC contracts using AI"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
11
11
|
license = { text = "MIT" }
|
|
@@ -20,6 +20,7 @@ keywords = [
|
|
|
20
20
|
"breaking-changes",
|
|
21
21
|
"contract-testing",
|
|
22
22
|
"api-diff",
|
|
23
|
+
"api-linter",
|
|
23
24
|
"devtools",
|
|
24
25
|
"cli",
|
|
25
26
|
"ci-cd",
|
|
@@ -6,16 +6,18 @@ summarizes changes in OpenAPI and Swagger contracts using AI.
|
|
|
6
6
|
from specshift.changelog import build_changelog, relevant_tags
|
|
7
7
|
from specshift.differ import diff_specs
|
|
8
8
|
from specshift.graphql_differ import diff_graphql, load_sdl as load_graphql_sdl
|
|
9
|
+
from specshift.linter import lint_spec
|
|
9
10
|
from specshift.models import Change, Severity, ChangeType, DiffResult
|
|
10
11
|
from specshift.protobuf_differ import diff_protos, load_proto
|
|
11
12
|
from specshift.spec_loader import load_spec
|
|
12
13
|
|
|
13
|
-
__version__ = "1.
|
|
14
|
+
__version__ = "1.4.0"
|
|
14
15
|
|
|
15
16
|
__all__ = [
|
|
16
17
|
"diff_specs",
|
|
17
18
|
"diff_graphql",
|
|
18
19
|
"diff_protos",
|
|
20
|
+
"lint_spec",
|
|
19
21
|
"build_changelog",
|
|
20
22
|
"relevant_tags",
|
|
21
23
|
"load_spec",
|
|
@@ -36,13 +36,18 @@ def generate_summary(
|
|
|
36
36
|
provider: Optional[str] = None,
|
|
37
37
|
model: Optional[str] = None,
|
|
38
38
|
timeout: int = 30,
|
|
39
|
+
context: str = "diff",
|
|
39
40
|
) -> str:
|
|
40
41
|
"""
|
|
41
42
|
Returns a natural-language summary using a configured AI provider when
|
|
42
43
|
possible, otherwise falls back to a rule-based summary. Never raises an
|
|
43
44
|
exception that would halt execution; failures fall back silently.
|
|
45
|
+
|
|
46
|
+
context is either "diff" (comparing two versions, the default) or
|
|
47
|
+
"lint" (findings about a single specification), and selects which
|
|
48
|
+
prompt and fallback wording is used.
|
|
44
49
|
"""
|
|
45
|
-
prompt = _build_prompt(result)
|
|
50
|
+
prompt = _build_prompt(result, context=context)
|
|
46
51
|
|
|
47
52
|
chosen_provider = provider or _detect_provider()
|
|
48
53
|
|
|
@@ -64,7 +69,7 @@ def generate_summary(
|
|
|
64
69
|
except AISummaryError:
|
|
65
70
|
pass
|
|
66
71
|
|
|
67
|
-
return build_template_summary(result)
|
|
72
|
+
return build_template_summary(result, context=context)
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
def _detect_provider() -> Optional[str]:
|
|
@@ -77,19 +82,40 @@ def _detect_provider() -> Optional[str]:
|
|
|
77
82
|
return None
|
|
78
83
|
|
|
79
84
|
|
|
80
|
-
def _build_prompt(result: DiffResult) -> str:
|
|
85
|
+
def _build_prompt(result: DiffResult, context: str = "diff") -> str:
|
|
81
86
|
counts = result.summary_counts()
|
|
87
|
+
|
|
88
|
+
if context == "lint":
|
|
89
|
+
intro = (
|
|
90
|
+
"Below is a list of issues found while linting a single OpenAPI "
|
|
91
|
+
"specification for correctness and best practices. This is NOT a "
|
|
92
|
+
"comparison between two versions, it is a quality review of one "
|
|
93
|
+
"document. Read this list and write a short, clear, professional "
|
|
94
|
+
"summary in English that a developer maintaining this API would "
|
|
95
|
+
"understand. First highlight any spec-validity issues (things that "
|
|
96
|
+
"could break tooling or fail parsing), then best-practice gaps, "
|
|
97
|
+
"then minor style suggestions. Do not use lists or headings, write "
|
|
98
|
+
"in flowing paragraphs. Do not invent information, base the "
|
|
99
|
+
"summary only on the findings given.",
|
|
100
|
+
)
|
|
101
|
+
total_label = "issues"
|
|
102
|
+
else:
|
|
103
|
+
intro = (
|
|
104
|
+
"Below is a list of contract changes detected between two versions "
|
|
105
|
+
"of an API. Read this list and write a short, clear, professional "
|
|
106
|
+
"summary in English that a developer consuming this API would "
|
|
107
|
+
"understand. First highlight the most important breaking changes, "
|
|
108
|
+
"then anything worth paying attention to, and finally briefly "
|
|
109
|
+
"mention minor additions. Do not use lists or headings, write in "
|
|
110
|
+
"flowing paragraphs. Do not invent information, base the summary "
|
|
111
|
+
"only on the changes given.",
|
|
112
|
+
)
|
|
113
|
+
total_label = "changes"
|
|
114
|
+
|
|
82
115
|
lines = [
|
|
83
|
-
|
|
84
|
-
"of an API. Read this list and write a short, clear, professional "
|
|
85
|
-
"summary in English that a developer consuming this API would "
|
|
86
|
-
"understand. First highlight the most important breaking changes, "
|
|
87
|
-
"then anything worth paying attention to, and finally briefly "
|
|
88
|
-
"mention minor additions. Do not use lists or headings, write in "
|
|
89
|
-
"flowing paragraphs. Do not invent information, base the summary "
|
|
90
|
-
"only on the changes given.",
|
|
116
|
+
intro[0],
|
|
91
117
|
"",
|
|
92
|
-
f"Total: {counts['breaking']} breaking, {counts['warning']} warning, {counts['info']} informational
|
|
118
|
+
f"Total: {counts['breaking']} breaking, {counts['warning']} warning, {counts['info']} informational {total_label}.",
|
|
93
119
|
"",
|
|
94
120
|
]
|
|
95
121
|
for change in result.sorted_changes()[:60]:
|
|
@@ -160,8 +186,14 @@ def _call_openai_compatible(prompt: str, model: Optional[str], timeout: int) ->
|
|
|
160
186
|
raise AISummaryError(f"Custom endpoint request failed: {exc}") from exc
|
|
161
187
|
|
|
162
188
|
|
|
163
|
-
def build_template_summary(result: DiffResult) -> str:
|
|
189
|
+
def build_template_summary(result: DiffResult, context: str = "diff") -> str:
|
|
164
190
|
"""A deterministic, rule-based summary that works without any API key."""
|
|
191
|
+
if context == "lint":
|
|
192
|
+
return _build_lint_template_summary(result)
|
|
193
|
+
return _build_diff_template_summary(result)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _build_diff_template_summary(result: DiffResult) -> str:
|
|
165
197
|
counts = result.summary_counts()
|
|
166
198
|
|
|
167
199
|
if result.is_empty:
|
|
@@ -199,3 +231,38 @@ def build_template_summary(result: DiffResult) -> str:
|
|
|
199
231
|
)
|
|
200
232
|
|
|
201
233
|
return " ".join(parts)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _build_lint_template_summary(result: DiffResult) -> str:
|
|
237
|
+
counts = result.summary_counts()
|
|
238
|
+
|
|
239
|
+
if result.is_empty:
|
|
240
|
+
return "No issues were found; this specification looks clean."
|
|
241
|
+
|
|
242
|
+
parts: list[str] = []
|
|
243
|
+
|
|
244
|
+
if counts["breaking"] > 0:
|
|
245
|
+
top_issues = result.breaking_changes[:5]
|
|
246
|
+
issue_desc = "; ".join(f"{c.location} ({c.message.rstrip('.')})" for c in top_issues)
|
|
247
|
+
extra = counts["breaking"] - len(top_issues)
|
|
248
|
+
extra_note = f" and {extra} more" if extra > 0 else ""
|
|
249
|
+
parts.append(
|
|
250
|
+
f"This specification has {counts['breaking']} spec-validity issue(s) that should be "
|
|
251
|
+
f"fixed first, since they can break tooling or code generation. Notably: {issue_desc}{extra_note}."
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
parts.append("This specification has no spec-validity issues.")
|
|
255
|
+
|
|
256
|
+
if counts["warning"] > 0:
|
|
257
|
+
parts.append(
|
|
258
|
+
f"There are {counts['warning']} best-practice recommendation(s), such as missing "
|
|
259
|
+
"descriptions, operation IDs, or documented error responses, that would improve the "
|
|
260
|
+
"developer experience for anyone consuming this API."
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
if counts["info"] > 0:
|
|
264
|
+
parts.append(
|
|
265
|
+
f"The remaining {counts['info']} finding(s) are minor style or cleanup suggestions."
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
return " ".join(parts)
|
|
@@ -17,6 +17,7 @@ from specshift.config import SpecShiftConfig, write_default_config
|
|
|
17
17
|
from specshift.differ import diff_specs
|
|
18
18
|
from specshift.git_utils import GitError, load_spec_from_git
|
|
19
19
|
from specshift.graphql_differ import GraphQLLoadError, diff_graphql, load_sdl
|
|
20
|
+
from specshift.linter import lint_spec
|
|
20
21
|
from specshift.models import DiffResult
|
|
21
22
|
from specshift.notifier import NotifyError, notify_discord, notify_slack
|
|
22
23
|
from specshift.protobuf_differ import ProtoLoadError, diff_protos, load_proto
|
|
@@ -35,7 +36,8 @@ CACHE_DIR = Path(".specshift_cache")
|
|
|
35
36
|
def build_parser() -> argparse.ArgumentParser:
|
|
36
37
|
parser = argparse.ArgumentParser(
|
|
37
38
|
prog="specshift",
|
|
38
|
-
description="Detects and classifies changes in OpenAPI/Swagger, GraphQL, and Protobuf/gRPC contracts
|
|
39
|
+
description="Detects and classifies changes in OpenAPI/Swagger, GraphQL, and Protobuf/gRPC contracts, "
|
|
40
|
+
"and lints a single OpenAPI specification for best practices.",
|
|
39
41
|
)
|
|
40
42
|
parser.add_argument("--version", action="version", version=f"specshift {__version__}")
|
|
41
43
|
|
|
@@ -56,6 +58,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
56
58
|
proto_parser.add_argument("new", help="New .proto schema: file path or URL")
|
|
57
59
|
_add_common_diff_args(proto_parser)
|
|
58
60
|
|
|
61
|
+
lint_parser = subparsers.add_parser(
|
|
62
|
+
"lint", help="Checks a single OpenAPI/Swagger specification for validity and best-practice issues"
|
|
63
|
+
)
|
|
64
|
+
lint_parser.add_argument("spec", help="Specification to lint: file path or URL")
|
|
65
|
+
_add_common_diff_args(lint_parser)
|
|
66
|
+
|
|
59
67
|
check_parser = subparsers.add_parser(
|
|
60
68
|
"check", help="For CI: compares the current specification against a git reference"
|
|
61
69
|
)
|
|
@@ -138,6 +146,8 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|
|
138
146
|
return _run_diff_graphql(args)
|
|
139
147
|
if args.command == "diff-proto":
|
|
140
148
|
return _run_diff_proto(args)
|
|
149
|
+
if args.command == "lint":
|
|
150
|
+
return _run_lint(args)
|
|
141
151
|
if args.command == "check":
|
|
142
152
|
return _run_check(args)
|
|
143
153
|
if args.command == "watch":
|
|
@@ -183,6 +193,12 @@ def _run_diff_proto(args: argparse.Namespace) -> int:
|
|
|
183
193
|
return _emit_result(result, args)
|
|
184
194
|
|
|
185
195
|
|
|
196
|
+
def _run_lint(args: argparse.Namespace) -> int:
|
|
197
|
+
spec = load_spec(args.spec)
|
|
198
|
+
result = lint_spec(spec)
|
|
199
|
+
return _emit_result(result, args, context="lint")
|
|
200
|
+
|
|
201
|
+
|
|
186
202
|
def _run_check(args: argparse.Namespace) -> int:
|
|
187
203
|
config = SpecShiftConfig.load(args.config)
|
|
188
204
|
spec_path = args.spec or config.spec_path
|
|
@@ -212,18 +228,18 @@ def _run_check(args: argparse.Namespace) -> int:
|
|
|
212
228
|
return exit_code
|
|
213
229
|
|
|
214
230
|
|
|
215
|
-
def _emit_result(result: DiffResult, args: argparse.Namespace) -> int:
|
|
231
|
+
def _emit_result(result: DiffResult, args: argparse.Namespace, context: str = "diff") -> int:
|
|
216
232
|
ai_summary = None
|
|
217
233
|
if args.ai:
|
|
218
|
-
ai_summary = generate_summary(result, provider=args.ai_provider, model=args.ai_model)
|
|
234
|
+
ai_summary = generate_summary(result, provider=args.ai_provider, model=args.ai_model, context=context)
|
|
219
235
|
|
|
220
236
|
output_text: str
|
|
221
237
|
if args.format == "json":
|
|
222
238
|
output_text = render_json_report(result, ai_summary=ai_summary)
|
|
223
239
|
elif args.format == "markdown":
|
|
224
|
-
output_text = render_markdown_report(result, ai_summary=ai_summary)
|
|
240
|
+
output_text = render_markdown_report(result, ai_summary=ai_summary, context=context)
|
|
225
241
|
elif args.format == "html":
|
|
226
|
-
output_text = render_html_report(result, ai_summary=ai_summary)
|
|
242
|
+
output_text = render_html_report(result, ai_summary=ai_summary, context=context)
|
|
227
243
|
elif args.format == "sarif":
|
|
228
244
|
artifact_path = args.artifact_path or getattr(args, "new", None) or getattr(args, "spec", None) or "specification"
|
|
229
245
|
output_text = render_sarif_report(result, artifact_path=artifact_path)
|
|
@@ -234,11 +250,11 @@ def _emit_result(result: DiffResult, args: argparse.Namespace) -> int:
|
|
|
234
250
|
if args.output:
|
|
235
251
|
from specshift.reporter import render_plain_text_report
|
|
236
252
|
|
|
237
|
-
Path(args.output).write_text(render_plain_text_report(result), encoding="utf-8")
|
|
253
|
+
Path(args.output).write_text(render_plain_text_report(result, context=context), encoding="utf-8")
|
|
238
254
|
if not args.quiet:
|
|
239
255
|
print(f"Report written to: {args.output}")
|
|
240
256
|
else:
|
|
241
|
-
print_console_report(result, verbose=not args.quiet)
|
|
257
|
+
print_console_report(result, verbose=not args.quiet, context=context)
|
|
242
258
|
if ai_summary:
|
|
243
259
|
print(f"\nAI Summary:\n{ai_summary}")
|
|
244
260
|
else:
|