specshift 1.2.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.
Files changed (36) hide show
  1. {specshift-1.2.0/specshift.egg-info → specshift-1.4.0}/PKG-INFO +100 -12
  2. {specshift-1.2.0 → specshift-1.4.0}/README.md +97 -9
  3. {specshift-1.2.0 → specshift-1.4.0}/pyproject.toml +5 -2
  4. {specshift-1.2.0 → specshift-1.4.0}/specshift/__init__.py +6 -1
  5. {specshift-1.2.0 → specshift-1.4.0}/specshift/ai_summary.py +80 -13
  6. specshift-1.4.0/specshift/changelog.py +164 -0
  7. {specshift-1.2.0 → specshift-1.4.0}/specshift/cli.py +73 -8
  8. {specshift-1.2.0 → specshift-1.4.0}/specshift/git_utils.py +63 -9
  9. specshift-1.4.0/specshift/linter.py +326 -0
  10. {specshift-1.2.0 → specshift-1.4.0}/specshift/protobuf_differ.py +179 -46
  11. {specshift-1.2.0 → specshift-1.4.0}/specshift/reporter.py +146 -44
  12. {specshift-1.2.0 → specshift-1.4.0/specshift.egg-info}/PKG-INFO +100 -12
  13. {specshift-1.2.0 → specshift-1.4.0}/specshift.egg-info/SOURCES.txt +4 -0
  14. specshift-1.4.0/tests/test_changelog.py +72 -0
  15. specshift-1.4.0/tests/test_cli.py +110 -0
  16. specshift-1.4.0/tests/test_linter.py +166 -0
  17. {specshift-1.2.0 → specshift-1.4.0}/tests/test_protobuf_differ.py +32 -0
  18. {specshift-1.2.0 → specshift-1.4.0}/tests/test_reporter.py +38 -0
  19. specshift-1.2.0/tests/test_cli.py +0 -46
  20. {specshift-1.2.0 → specshift-1.4.0}/LICENSE +0 -0
  21. {specshift-1.2.0 → specshift-1.4.0}/setup.cfg +0 -0
  22. {specshift-1.2.0 → specshift-1.4.0}/specshift/config.py +0 -0
  23. {specshift-1.2.0 → specshift-1.4.0}/specshift/differ.py +0 -0
  24. {specshift-1.2.0 → specshift-1.4.0}/specshift/graphql_differ.py +0 -0
  25. {specshift-1.2.0 → specshift-1.4.0}/specshift/models.py +0 -0
  26. {specshift-1.2.0 → specshift-1.4.0}/specshift/notifier.py +0 -0
  27. {specshift-1.2.0 → specshift-1.4.0}/specshift/spec_loader.py +0 -0
  28. {specshift-1.2.0 → specshift-1.4.0}/specshift/webapp.py +0 -0
  29. {specshift-1.2.0 → specshift-1.4.0}/specshift.egg-info/dependency_links.txt +0 -0
  30. {specshift-1.2.0 → specshift-1.4.0}/specshift.egg-info/entry_points.txt +0 -0
  31. {specshift-1.2.0 → specshift-1.4.0}/specshift.egg-info/requires.txt +0 -0
  32. {specshift-1.2.0 → specshift-1.4.0}/specshift.egg-info/top_level.txt +0 -0
  33. {specshift-1.2.0 → specshift-1.4.0}/tests/test_differ.py +0 -0
  34. {specshift-1.2.0 → specshift-1.4.0}/tests/test_graphql_differ.py +0 -0
  35. {specshift-1.2.0 → specshift-1.4.0}/tests/test_spec_loader.py +0 -0
  36. {specshift-1.2.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.2.0
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
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
@@ -124,9 +124,19 @@ requirement.
124
124
  for comparing specs interactively (drag in files or paste text), with
125
125
  zero extra dependencies since it runs entirely on Python's standard
126
126
  library and stays on localhost.
127
- - **Four output formats**: a colored console table, a Markdown report
128
- (ideal for PR comments), JSON (for integrating with other tools), and a
129
- standalone, filterable HTML report you can open in any browser.
127
+ - **Auto-generated changelog**: `specshift changelog` walks your spec
128
+ file's history across git tags and produces a single Markdown document
129
+ summarizing what changed (and whether it was breaking) between every
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.
136
+ - **Five output formats**: a colored console table, a Markdown report
137
+ (ideal for PR comments), JSON (for integrating with other tools), a
138
+ standalone filterable HTML report, and SARIF 2.1.0 for GitHub Code
139
+ Scanning.
130
140
 
131
141
  ## Installation
132
142
 
@@ -226,7 +236,7 @@ Useful options:
226
236
 
227
237
  | Option | Description |
228
238
  |---|---|
229
- | `--format console\|markdown\|json\|html` | Output format (default: console) |
239
+ | `--format console\|markdown\|json\|html\|sarif` | Output format (default: console) |
230
240
  | `--output <file>` | Writes the output to a file |
231
241
  | `--ai` | Adds a natural-language summary |
232
242
  | `--ai-provider groq\|gemini\|openai_compatible` | Chooses the AI provider |
@@ -290,6 +300,47 @@ specshift watch https://api.example.com/openapi.json \
290
300
  --slack-webhook "$SLACK_WEBHOOK_URL"
291
301
  ```
292
302
 
303
+ ### `specshift changelog <spec-path>`
304
+
305
+ Walks the given file's history across git tags and generates a single
306
+ Markdown changelog, diffing each consecutive pair of tagged versions. By
307
+ default it also appends an "Unreleased" section comparing the latest tag
308
+ to the current working tree, if they differ.
309
+
310
+ ```bash
311
+ specshift changelog openapi.yaml --output CHANGELOG-API.md
312
+ ```
313
+
314
+ Options: `--tag-pattern` (glob, default `*`, e.g. `v*` to only consider
315
+ tags starting with `v`), `--no-working-tree` to skip the "Unreleased"
316
+ section, and `--output` to write to a file instead of stdout.
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
+
293
344
  ### `specshift init`
294
345
 
295
346
  Creates a sample `.specshift.yml` file.
@@ -330,9 +381,32 @@ jobs:
330
381
 
331
382
  Available inputs: `spec-path`, `base-ref` (defaults to the PR base branch),
332
383
  `fail-on` (`breaking` | `warning` | `none`), `ai-summary` (`true`/`false`),
333
- `ai-provider`, `comment-on-pr` (`true`/`false`), `github-token`, and
334
- `specshift-version` to pin a specific release. Outputs: `has-breaking-changes`,
335
- `breaking-count`, `warning-count`, `info-count`, and `report-path`.
384
+ `ai-provider`, `comment-on-pr` (`true`/`false`), `upload-sarif`
385
+ (`true`/`false`, see below), `github-token`, and `specshift-version` to
386
+ pin a specific release. Outputs: `has-breaking-changes`, `breaking-count`,
387
+ `warning-count`, `info-count`, `report-path`, and `sarif-path`.
388
+
389
+ To also surface results in GitHub's Code Scanning tab, enable
390
+ `upload-sarif` and grant the extra permission it needs:
391
+
392
+ ```yaml
393
+ permissions:
394
+ pull-requests: write
395
+ security-events: write
396
+
397
+ jobs:
398
+ contract-check:
399
+ runs-on: ubuntu-latest
400
+ steps:
401
+ - uses: actions/checkout@v4
402
+ with:
403
+ fetch-depth: 0
404
+
405
+ - uses: Lethe044/SpecShift@v1
406
+ with:
407
+ spec-path: openapi.yaml
408
+ upload-sarif: 'true'
409
+ ```
336
410
 
337
411
  ### Option 2: calling the CLI directly
338
412
 
@@ -408,6 +482,16 @@ than request/response direction: adding fields or rpc methods is always
408
482
  safe, changing a field's number is always breaking, and type changes are
409
483
  judged by wire type category (see the `diff-proto` section above).
410
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
+
411
495
  ## Comparison with other tools
412
496
 
413
497
  | | SpecShift | Raw JSON/YAML diff | oasdiff / openapi-diff style tools |
@@ -415,9 +499,12 @@ judged by wire type category (see the `diff-proto` section above).
415
499
  | Context-aware classification | Yes | No | Partially |
416
500
  | GraphQL support | Yes | No | Rarely |
417
501
  | Protobuf/gRPC support (wire-aware) | Yes | No | Rarely |
502
+ | Single-document linting (not just diffing) | Yes | No | Rarely |
418
503
  | Natural-language summary | Yes (optional) | No | No |
419
504
  | Standalone HTML report | Yes | No | Rarely |
420
505
  | Local interactive dashboard | Yes | No | Rarely |
506
+ | Auto-generated changelog across git tags | Yes | No | Rarely |
507
+ | GitHub Code Scanning (SARIF) integration | Yes | No | Rarely |
421
508
  | Free to use | Fully free | Free | Usually free |
422
509
  | CI integration | Built-in (`check` + official Action) | Manual | Varies |
423
510
  | Live URL monitoring | Built-in (`watch`) | No | Rarely |
@@ -429,10 +516,11 @@ This project is under active development. Some planned areas:
429
516
  - Path parameter pattern (regex) and discriminator-level rule refinements
430
517
  - Deeper GraphQL support: directive-aware deprecation reasons, custom
431
518
  scalar compatibility hints, and federation-aware schema composition
432
- - Protobuf `reserved` statement awareness, so a properly reserved field
433
- number isn't flagged the same way as an unreserved removal
519
+ - Extending `specshift lint` to GraphQL and Protobuf schemas
434
520
  - Optional shared/hosted history for the `serve` dashboard (currently
435
521
  in-memory and per-session by design)
522
+ - Changelog generation from raw commit history (not just tags), for
523
+ repositories that don't tag every release
436
524
 
437
525
  Feel free to open an issue if you have a feature request.
438
526
 
@@ -85,9 +85,19 @@ requirement.
85
85
  for comparing specs interactively (drag in files or paste text), with
86
86
  zero extra dependencies since it runs entirely on Python's standard
87
87
  library and stays on localhost.
88
- - **Four output formats**: a colored console table, a Markdown report
89
- (ideal for PR comments), JSON (for integrating with other tools), and a
90
- standalone, filterable HTML report you can open in any browser.
88
+ - **Auto-generated changelog**: `specshift changelog` walks your spec
89
+ file's history across git tags and produces a single Markdown document
90
+ summarizing what changed (and whether it was breaking) between every
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.
97
+ - **Five output formats**: a colored console table, a Markdown report
98
+ (ideal for PR comments), JSON (for integrating with other tools), a
99
+ standalone filterable HTML report, and SARIF 2.1.0 for GitHub Code
100
+ Scanning.
91
101
 
92
102
  ## Installation
93
103
 
@@ -187,7 +197,7 @@ Useful options:
187
197
 
188
198
  | Option | Description |
189
199
  |---|---|
190
- | `--format console\|markdown\|json\|html` | Output format (default: console) |
200
+ | `--format console\|markdown\|json\|html\|sarif` | Output format (default: console) |
191
201
  | `--output <file>` | Writes the output to a file |
192
202
  | `--ai` | Adds a natural-language summary |
193
203
  | `--ai-provider groq\|gemini\|openai_compatible` | Chooses the AI provider |
@@ -251,6 +261,47 @@ specshift watch https://api.example.com/openapi.json \
251
261
  --slack-webhook "$SLACK_WEBHOOK_URL"
252
262
  ```
253
263
 
264
+ ### `specshift changelog <spec-path>`
265
+
266
+ Walks the given file's history across git tags and generates a single
267
+ Markdown changelog, diffing each consecutive pair of tagged versions. By
268
+ default it also appends an "Unreleased" section comparing the latest tag
269
+ to the current working tree, if they differ.
270
+
271
+ ```bash
272
+ specshift changelog openapi.yaml --output CHANGELOG-API.md
273
+ ```
274
+
275
+ Options: `--tag-pattern` (glob, default `*`, e.g. `v*` to only consider
276
+ tags starting with `v`), `--no-working-tree` to skip the "Unreleased"
277
+ section, and `--output` to write to a file instead of stdout.
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
+
254
305
  ### `specshift init`
255
306
 
256
307
  Creates a sample `.specshift.yml` file.
@@ -291,9 +342,32 @@ jobs:
291
342
 
292
343
  Available inputs: `spec-path`, `base-ref` (defaults to the PR base branch),
293
344
  `fail-on` (`breaking` | `warning` | `none`), `ai-summary` (`true`/`false`),
294
- `ai-provider`, `comment-on-pr` (`true`/`false`), `github-token`, and
295
- `specshift-version` to pin a specific release. Outputs: `has-breaking-changes`,
296
- `breaking-count`, `warning-count`, `info-count`, and `report-path`.
345
+ `ai-provider`, `comment-on-pr` (`true`/`false`), `upload-sarif`
346
+ (`true`/`false`, see below), `github-token`, and `specshift-version` to
347
+ pin a specific release. Outputs: `has-breaking-changes`, `breaking-count`,
348
+ `warning-count`, `info-count`, `report-path`, and `sarif-path`.
349
+
350
+ To also surface results in GitHub's Code Scanning tab, enable
351
+ `upload-sarif` and grant the extra permission it needs:
352
+
353
+ ```yaml
354
+ permissions:
355
+ pull-requests: write
356
+ security-events: write
357
+
358
+ jobs:
359
+ contract-check:
360
+ runs-on: ubuntu-latest
361
+ steps:
362
+ - uses: actions/checkout@v4
363
+ with:
364
+ fetch-depth: 0
365
+
366
+ - uses: Lethe044/SpecShift@v1
367
+ with:
368
+ spec-path: openapi.yaml
369
+ upload-sarif: 'true'
370
+ ```
297
371
 
298
372
  ### Option 2: calling the CLI directly
299
373
 
@@ -369,6 +443,16 @@ than request/response direction: adding fields or rpc methods is always
369
443
  safe, changing a field's number is always breaking, and type changes are
370
444
  judged by wire type category (see the `diff-proto` section above).
371
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
+
372
456
  ## Comparison with other tools
373
457
 
374
458
  | | SpecShift | Raw JSON/YAML diff | oasdiff / openapi-diff style tools |
@@ -376,9 +460,12 @@ judged by wire type category (see the `diff-proto` section above).
376
460
  | Context-aware classification | Yes | No | Partially |
377
461
  | GraphQL support | Yes | No | Rarely |
378
462
  | Protobuf/gRPC support (wire-aware) | Yes | No | Rarely |
463
+ | Single-document linting (not just diffing) | Yes | No | Rarely |
379
464
  | Natural-language summary | Yes (optional) | No | No |
380
465
  | Standalone HTML report | Yes | No | Rarely |
381
466
  | Local interactive dashboard | Yes | No | Rarely |
467
+ | Auto-generated changelog across git tags | Yes | No | Rarely |
468
+ | GitHub Code Scanning (SARIF) integration | Yes | No | Rarely |
382
469
  | Free to use | Fully free | Free | Usually free |
383
470
  | CI integration | Built-in (`check` + official Action) | Manual | Varies |
384
471
  | Live URL monitoring | Built-in (`watch`) | No | Rarely |
@@ -390,10 +477,11 @@ This project is under active development. Some planned areas:
390
477
  - Path parameter pattern (regex) and discriminator-level rule refinements
391
478
  - Deeper GraphQL support: directive-aware deprecation reasons, custom
392
479
  scalar compatibility hints, and federation-aware schema composition
393
- - Protobuf `reserved` statement awareness, so a properly reserved field
394
- number isn't flagged the same way as an unreserved removal
480
+ - Extending `specshift lint` to GraphQL and Protobuf schemas
395
481
  - Optional shared/hosted history for the `serve` dashboard (currently
396
482
  in-memory and per-session by design)
483
+ - Changelog generation from raw commit history (not just tags), for
484
+ repositories that don't tag every release
397
485
 
398
486
  Feel free to open an issue if you have a feature request.
399
487
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specshift"
7
- version = "1.2.0"
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,10 +20,13 @@ 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",
26
27
  "github-actions",
28
+ "sarif",
29
+ "changelog",
27
30
  ]
28
31
  classifiers = [
29
32
  "Development Status :: 5 - Production/Stable",
@@ -3,18 +3,23 @@ specshift - a CLI tool that detects, classifies, and (optionally)
3
3
  summarizes changes in OpenAPI and Swagger contracts using AI.
4
4
  """
5
5
 
6
+ from specshift.changelog import build_changelog, relevant_tags
6
7
  from specshift.differ import diff_specs
7
8
  from specshift.graphql_differ import diff_graphql, load_sdl as load_graphql_sdl
9
+ from specshift.linter import lint_spec
8
10
  from specshift.models import Change, Severity, ChangeType, DiffResult
9
11
  from specshift.protobuf_differ import diff_protos, load_proto
10
12
  from specshift.spec_loader import load_spec
11
13
 
12
- __version__ = "1.2.0"
14
+ __version__ = "1.4.0"
13
15
 
14
16
  __all__ = [
15
17
  "diff_specs",
16
18
  "diff_graphql",
17
19
  "diff_protos",
20
+ "lint_spec",
21
+ "build_changelog",
22
+ "relevant_tags",
18
23
  "load_spec",
19
24
  "load_graphql_sdl",
20
25
  "load_proto",
@@ -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
- "Below is a list of contract changes detected between two versions "
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 changes.",
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)