specshift 1.0.0__tar.gz → 1.2.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 (32) hide show
  1. {specshift-1.0.0/specshift.egg-info → specshift-1.2.0}/PKG-INFO +124 -18
  2. {specshift-1.0.0 → specshift-1.2.0}/README.md +121 -15
  3. {specshift-1.0.0 → specshift-1.2.0}/pyproject.toml +6 -2
  4. {specshift-1.0.0 → specshift-1.2.0}/specshift/__init__.py +7 -1
  5. {specshift-1.0.0 → specshift-1.2.0}/specshift/cli.py +64 -8
  6. {specshift-1.0.0 → specshift-1.2.0}/specshift/differ.py +283 -0
  7. specshift-1.2.0/specshift/graphql_differ.py +527 -0
  8. specshift-1.2.0/specshift/protobuf_differ.py +622 -0
  9. specshift-1.2.0/specshift/reporter.py +369 -0
  10. specshift-1.2.0/specshift/webapp.py +335 -0
  11. {specshift-1.0.0 → specshift-1.2.0/specshift.egg-info}/PKG-INFO +124 -18
  12. {specshift-1.0.0 → specshift-1.2.0}/specshift.egg-info/SOURCES.txt +7 -1
  13. {specshift-1.0.0 → specshift-1.2.0}/tests/test_differ.py +145 -0
  14. specshift-1.2.0/tests/test_graphql_differ.py +119 -0
  15. specshift-1.2.0/tests/test_protobuf_differ.py +168 -0
  16. {specshift-1.0.0 → specshift-1.2.0}/tests/test_reporter.py +19 -0
  17. specshift-1.2.0/tests/test_webapp.py +62 -0
  18. specshift-1.0.0/specshift/reporter.py +0 -167
  19. {specshift-1.0.0 → specshift-1.2.0}/LICENSE +0 -0
  20. {specshift-1.0.0 → specshift-1.2.0}/setup.cfg +0 -0
  21. {specshift-1.0.0 → specshift-1.2.0}/specshift/ai_summary.py +0 -0
  22. {specshift-1.0.0 → specshift-1.2.0}/specshift/config.py +0 -0
  23. {specshift-1.0.0 → specshift-1.2.0}/specshift/git_utils.py +0 -0
  24. {specshift-1.0.0 → specshift-1.2.0}/specshift/models.py +0 -0
  25. {specshift-1.0.0 → specshift-1.2.0}/specshift/notifier.py +0 -0
  26. {specshift-1.0.0 → specshift-1.2.0}/specshift/spec_loader.py +0 -0
  27. {specshift-1.0.0 → specshift-1.2.0}/specshift.egg-info/dependency_links.txt +0 -0
  28. {specshift-1.0.0 → specshift-1.2.0}/specshift.egg-info/entry_points.txt +0 -0
  29. {specshift-1.0.0 → specshift-1.2.0}/specshift.egg-info/requires.txt +0 -0
  30. {specshift-1.0.0 → specshift-1.2.0}/specshift.egg-info/top_level.txt +0 -0
  31. {specshift-1.0.0 → specshift-1.2.0}/tests/test_cli.py +0 -0
  32. {specshift-1.0.0 → specshift-1.2.0}/tests/test_spec_loader.py +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specshift
3
- Version: 1.0.0
4
- Summary: Detects, classifies, and optionally summarizes breaking changes in OpenAPI/Swagger contracts using AI
3
+ Version: 1.2.0
4
+ Summary: Detects, classifies, and optionally summarizes breaking changes 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,api,breaking-changes,contract-testing,api-diff,devtools,cli,ci-cd
11
+ Keywords: openapi,swagger,graphql,grpc,protobuf,api,breaking-changes,contract-testing,api-diff,devtools,cli,ci-cd,github-actions
12
12
  Classifier: Development Status :: 5 - Production/Stable
13
13
  Classifier: Environment :: Console
14
14
  Classifier: Intended Audience :: Developers
@@ -93,11 +93,21 @@ requirement.
93
93
 
94
94
  - **Comprehensive structural diff**: deep comparison at the path, HTTP
95
95
  method, parameter, request body, response, and schema level.
96
+ - **GraphQL support**: diff GraphQL SDL schemas with `specshift diff-graphql`,
97
+ covering types, fields, arguments, enum values, interfaces, unions, and
98
+ deprecations, no external GraphQL library required.
99
+ - **Protobuf/gRPC support**: diff `.proto` files with `specshift diff-proto`,
100
+ using protobuf's own documented wire-compatibility rules (field numbers,
101
+ wire type categories, zigzag encoding) rather than generic JSON-shape
102
+ heuristics, so severity reflects what will actually break on the wire.
96
103
  - **Context-aware classification**: the same change is weighted
97
- differently depending on whether it occurs in a request or a response.
104
+ differently depending on whether it occurs in a request or a response
105
+ (or, for GraphQL, an input type versus an object type).
98
106
  - **`$ref` resolution and `allOf` merging**: correctly follows the
99
107
  reference and composition patterns common in real-world specifications.
100
- - **Detects enum, format, nullable, and security scheme changes**.
108
+ - **Detects enum, format, nullable, security scheme, content-type,
109
+ `additionalProperties`, `readOnly`/`writeOnly`, validation constraint,
110
+ default value, and `oneOf`/`anyOf` changes**.
101
111
  - **Works entirely for free**: no API key or paid service is required.
102
112
  - **Optional AI summary**: can generate a natural-language summary using
103
113
  Groq, the Gemini free tier, or any OpenAI-compatible endpoint. If no key
@@ -105,12 +115,18 @@ requirement.
105
115
  stops working.
106
116
  - **CI/CD integration**: the `specshift check` command compares the
107
117
  current specification against a branch and fails the build if a
108
- breaking change is found.
118
+ breaking change is found. An official GitHub Action is also available
119
+ for one-line setup with automatic PR comments (see below).
109
120
  - **Live monitoring**: the `specshift watch` command periodically checks
110
121
  a remote API's specification and sends a Slack or Discord notification
111
122
  when it changes.
112
- - **Three output formats**: a colored console table, a Markdown report
113
- (ideal for PR comments), and JSON (for integrating with other tools).
123
+ - **Local web dashboard**: `specshift serve` opens a browser-based tool
124
+ for comparing specs interactively (drag in files or paste text), with
125
+ zero extra dependencies since it runs entirely on Python's standard
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.
114
130
 
115
131
  ## Installation
116
132
 
@@ -210,13 +226,50 @@ Useful options:
210
226
 
211
227
  | Option | Description |
212
228
  |---|---|
213
- | `--format console\|markdown\|json` | Output format (default: console) |
229
+ | `--format console\|markdown\|json\|html` | Output format (default: console) |
214
230
  | `--output <file>` | Writes the output to a file |
215
231
  | `--ai` | Adds a natural-language summary |
216
232
  | `--ai-provider groq\|gemini\|openai_compatible` | Chooses the AI provider |
217
233
  | `--fail-on breaking\|warning\|none` | Determines at which level exit code 1 is returned |
218
234
  | `--quiet` | Only prints the summary line |
219
235
 
236
+ ### `specshift diff-graphql <old> <new>`
237
+
238
+ Compares two GraphQL SDL schemas. `<old>` and `<new>` can be a file path,
239
+ an http(s) URL, or raw SDL text. Supports the same `--format`, `--output`,
240
+ `--ai`, and `--fail-on` options as `diff`.
241
+
242
+ ```bash
243
+ specshift diff-graphql old_schema.graphql new_schema.graphql
244
+ ```
245
+
246
+ ### `specshift diff-proto <old> <new>`
247
+
248
+ Compares two Protobuf/gRPC `.proto` schemas using protobuf's own
249
+ wire-compatibility rules (field numbers, wire type categories) rather
250
+ than generic JSON-shape heuristics. Supports the same `--format`,
251
+ `--output`, `--ai`, and `--fail-on` options as `diff`.
252
+
253
+ ```bash
254
+ specshift diff-proto old_service.proto new_service.proto
255
+ ```
256
+
257
+ ### `specshift serve`
258
+
259
+ Starts a local, dependency-free web dashboard for comparing specs
260
+ interactively. Opens a browser tab where you can drag in or paste two
261
+ files (OpenAPI, GraphQL, or Protobuf, auto-detected), and view the same
262
+ styled HTML report the CLI produces, with a short history of past
263
+ comparisons in the current session.
264
+
265
+ ```bash
266
+ specshift serve
267
+ # or: specshift serve --port 9000 --no-browser
268
+ ```
269
+
270
+ Everything runs in-process on your machine; nothing is uploaded anywhere,
271
+ and the server binds to `127.0.0.1` by default.
272
+
220
273
  ### `specshift check`
221
274
 
222
275
  Designed for CI/CD. Compares the current specification file against a git
@@ -243,8 +296,48 @@ Creates a sample `.specshift.yml` file.
243
296
 
244
297
  ## Using it with GitHub Actions
245
298
 
246
- The workflow below checks your API contract against the `main` branch on
247
- every pull request and fails the build if a breaking change is found:
299
+ ### Option 1: the official SpecShift action (recommended)
300
+
301
+ SpecShift ships its own composite GitHub Action that runs the contract
302
+ check and automatically posts (and keeps updated) a Markdown report as a
303
+ pull request comment:
304
+
305
+ ```yaml
306
+ name: API Contract Check
307
+
308
+ on:
309
+ pull_request:
310
+ paths:
311
+ - "openapi.yaml"
312
+
313
+ permissions:
314
+ pull-requests: write
315
+
316
+ jobs:
317
+ contract-check:
318
+ runs-on: ubuntu-latest
319
+ steps:
320
+ - uses: actions/checkout@v4
321
+ with:
322
+ fetch-depth: 0
323
+
324
+ - uses: Lethe044/SpecShift@v1
325
+ with:
326
+ spec-path: openapi.yaml
327
+ fail-on: breaking
328
+ # optional: ai-summary: 'true'
329
+ ```
330
+
331
+ Available inputs: `spec-path`, `base-ref` (defaults to the PR base branch),
332
+ `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`.
336
+
337
+ ### Option 2: calling the CLI directly
338
+
339
+ If you'd rather not use the action (or want to combine it with other
340
+ tooling), the CLI works just as well on its own:
248
341
 
249
342
  ```yaml
250
343
  name: API Contract Check
@@ -307,26 +400,39 @@ common scenarios:
307
400
  | Enum value removed | Breaking | Breaking |
308
401
  | Endpoint or method removed | Breaking | Breaking |
309
402
 
403
+ The same table applies to GraphQL: `input` types behave like requests,
404
+ `type`/`interface` fields behave like responses.
405
+
406
+ Protobuf/gRPC follows a different model, based on the wire format rather
407
+ than request/response direction: adding fields or rpc methods is always
408
+ safe, changing a field's number is always breaking, and type changes are
409
+ judged by wire type category (see the `diff-proto` section above).
410
+
310
411
  ## Comparison with other tools
311
412
 
312
413
  | | SpecShift | Raw JSON/YAML diff | oasdiff / openapi-diff style tools |
313
414
  |---|---|---|---|
314
415
  | Context-aware classification | Yes | No | Partially |
416
+ | GraphQL support | Yes | No | Rarely |
417
+ | Protobuf/gRPC support (wire-aware) | Yes | No | Rarely |
315
418
  | Natural-language summary | Yes (optional) | No | No |
419
+ | Standalone HTML report | Yes | No | Rarely |
420
+ | Local interactive dashboard | Yes | No | Rarely |
316
421
  | Free to use | Fully free | Free | Usually free |
317
- | CI integration | Built-in (`check`) | Manual | Varies |
422
+ | CI integration | Built-in (`check` + official Action) | Manual | Varies |
318
423
  | Live URL monitoring | Built-in (`watch`) | No | Rarely |
319
424
 
320
425
  ## Roadmap
321
426
 
322
427
  This project is under active development. Some planned areas:
323
428
 
324
- - Support for gRPC/Protobuf contracts
325
- - GraphQL schema diffing
326
- - An official GitHub Action for posting automatic PR comments
327
- - A web-based result viewer
328
- - More semantic rules (path parameter pattern changes, content-type
329
- changes, etc.)
429
+ - Path parameter pattern (regex) and discriminator-level rule refinements
430
+ - Deeper GraphQL support: directive-aware deprecation reasons, custom
431
+ 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
434
+ - Optional shared/hosted history for the `serve` dashboard (currently
435
+ in-memory and per-session by design)
330
436
 
331
437
  Feel free to open an issue if you have a feature request.
332
438
 
@@ -54,11 +54,21 @@ requirement.
54
54
 
55
55
  - **Comprehensive structural diff**: deep comparison at the path, HTTP
56
56
  method, parameter, request body, response, and schema level.
57
+ - **GraphQL support**: diff GraphQL SDL schemas with `specshift diff-graphql`,
58
+ covering types, fields, arguments, enum values, interfaces, unions, and
59
+ deprecations, no external GraphQL library required.
60
+ - **Protobuf/gRPC support**: diff `.proto` files with `specshift diff-proto`,
61
+ using protobuf's own documented wire-compatibility rules (field numbers,
62
+ wire type categories, zigzag encoding) rather than generic JSON-shape
63
+ heuristics, so severity reflects what will actually break on the wire.
57
64
  - **Context-aware classification**: the same change is weighted
58
- differently depending on whether it occurs in a request or a response.
65
+ differently depending on whether it occurs in a request or a response
66
+ (or, for GraphQL, an input type versus an object type).
59
67
  - **`$ref` resolution and `allOf` merging**: correctly follows the
60
68
  reference and composition patterns common in real-world specifications.
61
- - **Detects enum, format, nullable, and security scheme changes**.
69
+ - **Detects enum, format, nullable, security scheme, content-type,
70
+ `additionalProperties`, `readOnly`/`writeOnly`, validation constraint,
71
+ default value, and `oneOf`/`anyOf` changes**.
62
72
  - **Works entirely for free**: no API key or paid service is required.
63
73
  - **Optional AI summary**: can generate a natural-language summary using
64
74
  Groq, the Gemini free tier, or any OpenAI-compatible endpoint. If no key
@@ -66,12 +76,18 @@ requirement.
66
76
  stops working.
67
77
  - **CI/CD integration**: the `specshift check` command compares the
68
78
  current specification against a branch and fails the build if a
69
- breaking change is found.
79
+ breaking change is found. An official GitHub Action is also available
80
+ for one-line setup with automatic PR comments (see below).
70
81
  - **Live monitoring**: the `specshift watch` command periodically checks
71
82
  a remote API's specification and sends a Slack or Discord notification
72
83
  when it changes.
73
- - **Three output formats**: a colored console table, a Markdown report
74
- (ideal for PR comments), and JSON (for integrating with other tools).
84
+ - **Local web dashboard**: `specshift serve` opens a browser-based tool
85
+ for comparing specs interactively (drag in files or paste text), with
86
+ zero extra dependencies since it runs entirely on Python's standard
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.
75
91
 
76
92
  ## Installation
77
93
 
@@ -171,13 +187,50 @@ Useful options:
171
187
 
172
188
  | Option | Description |
173
189
  |---|---|
174
- | `--format console\|markdown\|json` | Output format (default: console) |
190
+ | `--format console\|markdown\|json\|html` | Output format (default: console) |
175
191
  | `--output <file>` | Writes the output to a file |
176
192
  | `--ai` | Adds a natural-language summary |
177
193
  | `--ai-provider groq\|gemini\|openai_compatible` | Chooses the AI provider |
178
194
  | `--fail-on breaking\|warning\|none` | Determines at which level exit code 1 is returned |
179
195
  | `--quiet` | Only prints the summary line |
180
196
 
197
+ ### `specshift diff-graphql <old> <new>`
198
+
199
+ Compares two GraphQL SDL schemas. `<old>` and `<new>` can be a file path,
200
+ an http(s) URL, or raw SDL text. Supports the same `--format`, `--output`,
201
+ `--ai`, and `--fail-on` options as `diff`.
202
+
203
+ ```bash
204
+ specshift diff-graphql old_schema.graphql new_schema.graphql
205
+ ```
206
+
207
+ ### `specshift diff-proto <old> <new>`
208
+
209
+ Compares two Protobuf/gRPC `.proto` schemas using protobuf's own
210
+ wire-compatibility rules (field numbers, wire type categories) rather
211
+ than generic JSON-shape heuristics. Supports the same `--format`,
212
+ `--output`, `--ai`, and `--fail-on` options as `diff`.
213
+
214
+ ```bash
215
+ specshift diff-proto old_service.proto new_service.proto
216
+ ```
217
+
218
+ ### `specshift serve`
219
+
220
+ Starts a local, dependency-free web dashboard for comparing specs
221
+ interactively. Opens a browser tab where you can drag in or paste two
222
+ files (OpenAPI, GraphQL, or Protobuf, auto-detected), and view the same
223
+ styled HTML report the CLI produces, with a short history of past
224
+ comparisons in the current session.
225
+
226
+ ```bash
227
+ specshift serve
228
+ # or: specshift serve --port 9000 --no-browser
229
+ ```
230
+
231
+ Everything runs in-process on your machine; nothing is uploaded anywhere,
232
+ and the server binds to `127.0.0.1` by default.
233
+
181
234
  ### `specshift check`
182
235
 
183
236
  Designed for CI/CD. Compares the current specification file against a git
@@ -204,8 +257,48 @@ Creates a sample `.specshift.yml` file.
204
257
 
205
258
  ## Using it with GitHub Actions
206
259
 
207
- The workflow below checks your API contract against the `main` branch on
208
- every pull request and fails the build if a breaking change is found:
260
+ ### Option 1: the official SpecShift action (recommended)
261
+
262
+ SpecShift ships its own composite GitHub Action that runs the contract
263
+ check and automatically posts (and keeps updated) a Markdown report as a
264
+ pull request comment:
265
+
266
+ ```yaml
267
+ name: API Contract Check
268
+
269
+ on:
270
+ pull_request:
271
+ paths:
272
+ - "openapi.yaml"
273
+
274
+ permissions:
275
+ pull-requests: write
276
+
277
+ jobs:
278
+ contract-check:
279
+ runs-on: ubuntu-latest
280
+ steps:
281
+ - uses: actions/checkout@v4
282
+ with:
283
+ fetch-depth: 0
284
+
285
+ - uses: Lethe044/SpecShift@v1
286
+ with:
287
+ spec-path: openapi.yaml
288
+ fail-on: breaking
289
+ # optional: ai-summary: 'true'
290
+ ```
291
+
292
+ Available inputs: `spec-path`, `base-ref` (defaults to the PR base branch),
293
+ `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`.
297
+
298
+ ### Option 2: calling the CLI directly
299
+
300
+ If you'd rather not use the action (or want to combine it with other
301
+ tooling), the CLI works just as well on its own:
209
302
 
210
303
  ```yaml
211
304
  name: API Contract Check
@@ -268,26 +361,39 @@ common scenarios:
268
361
  | Enum value removed | Breaking | Breaking |
269
362
  | Endpoint or method removed | Breaking | Breaking |
270
363
 
364
+ The same table applies to GraphQL: `input` types behave like requests,
365
+ `type`/`interface` fields behave like responses.
366
+
367
+ Protobuf/gRPC follows a different model, based on the wire format rather
368
+ than request/response direction: adding fields or rpc methods is always
369
+ safe, changing a field's number is always breaking, and type changes are
370
+ judged by wire type category (see the `diff-proto` section above).
371
+
271
372
  ## Comparison with other tools
272
373
 
273
374
  | | SpecShift | Raw JSON/YAML diff | oasdiff / openapi-diff style tools |
274
375
  |---|---|---|---|
275
376
  | Context-aware classification | Yes | No | Partially |
377
+ | GraphQL support | Yes | No | Rarely |
378
+ | Protobuf/gRPC support (wire-aware) | Yes | No | Rarely |
276
379
  | Natural-language summary | Yes (optional) | No | No |
380
+ | Standalone HTML report | Yes | No | Rarely |
381
+ | Local interactive dashboard | Yes | No | Rarely |
277
382
  | Free to use | Fully free | Free | Usually free |
278
- | CI integration | Built-in (`check`) | Manual | Varies |
383
+ | CI integration | Built-in (`check` + official Action) | Manual | Varies |
279
384
  | Live URL monitoring | Built-in (`watch`) | No | Rarely |
280
385
 
281
386
  ## Roadmap
282
387
 
283
388
  This project is under active development. Some planned areas:
284
389
 
285
- - Support for gRPC/Protobuf contracts
286
- - GraphQL schema diffing
287
- - An official GitHub Action for posting automatic PR comments
288
- - A web-based result viewer
289
- - More semantic rules (path parameter pattern changes, content-type
290
- changes, etc.)
390
+ - Path parameter pattern (regex) and discriminator-level rule refinements
391
+ - Deeper GraphQL support: directive-aware deprecation reasons, custom
392
+ 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
395
+ - Optional shared/hosted history for the `serve` dashboard (currently
396
+ in-memory and per-session by design)
291
397
 
292
398
  Feel free to open an issue if you have a feature request.
293
399
 
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specshift"
7
- version = "1.0.0"
8
- description = "Detects, classifies, and optionally summarizes breaking changes in OpenAPI/Swagger contracts using AI"
7
+ version = "1.2.0"
8
+ description = "Detects, classifies, and optionally summarizes breaking changes 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" }
@@ -13,6 +13,9 @@ authors = [{ name = "Lethe044" }]
13
13
  keywords = [
14
14
  "openapi",
15
15
  "swagger",
16
+ "graphql",
17
+ "grpc",
18
+ "protobuf",
16
19
  "api",
17
20
  "breaking-changes",
18
21
  "contract-testing",
@@ -20,6 +23,7 @@ keywords = [
20
23
  "devtools",
21
24
  "cli",
22
25
  "ci-cd",
26
+ "github-actions",
23
27
  ]
24
28
  classifiers = [
25
29
  "Development Status :: 5 - Production/Stable",
@@ -4,14 +4,20 @@ summarizes changes in OpenAPI and Swagger contracts using AI.
4
4
  """
5
5
 
6
6
  from specshift.differ import diff_specs
7
+ from specshift.graphql_differ import diff_graphql, load_sdl as load_graphql_sdl
7
8
  from specshift.models import Change, Severity, ChangeType, DiffResult
9
+ from specshift.protobuf_differ import diff_protos, load_proto
8
10
  from specshift.spec_loader import load_spec
9
11
 
10
- __version__ = "1.0.0"
12
+ __version__ = "1.2.0"
11
13
 
12
14
  __all__ = [
13
15
  "diff_specs",
16
+ "diff_graphql",
17
+ "diff_protos",
14
18
  "load_spec",
19
+ "load_graphql_sdl",
20
+ "load_proto",
15
21
  "Change",
16
22
  "Severity",
17
23
  "ChangeType",
@@ -15,9 +15,16 @@ from specshift.ai_summary import generate_summary
15
15
  from specshift.config import SpecShiftConfig, write_default_config
16
16
  from specshift.differ import diff_specs
17
17
  from specshift.git_utils import GitError, load_spec_from_git
18
+ from specshift.graphql_differ import GraphQLLoadError, diff_graphql, load_sdl
18
19
  from specshift.models import DiffResult
19
20
  from specshift.notifier import NotifyError, notify_discord, notify_slack
20
- from specshift.reporter import print_console_report, render_json_report, render_markdown_report
21
+ from specshift.protobuf_differ import ProtoLoadError, diff_protos, load_proto
22
+ from specshift.reporter import (
23
+ print_console_report,
24
+ render_html_report,
25
+ render_json_report,
26
+ render_markdown_report,
27
+ )
21
28
  from specshift.spec_loader import SpecLoadError, load_spec
22
29
 
23
30
  CACHE_DIR = Path(".specshift_cache")
@@ -26,17 +33,27 @@ CACHE_DIR = Path(".specshift_cache")
26
33
  def build_parser() -> argparse.ArgumentParser:
27
34
  parser = argparse.ArgumentParser(
28
35
  prog="specshift",
29
- description="Detects and classifies changes in OpenAPI/Swagger specifications.",
36
+ description="Detects and classifies changes in OpenAPI/Swagger, GraphQL, and Protobuf/gRPC contracts.",
30
37
  )
31
38
  parser.add_argument("--version", action="version", version=f"specshift {__version__}")
32
39
 
33
40
  subparsers = parser.add_subparsers(dest="command", required=True)
34
41
 
35
- diff_parser = subparsers.add_parser("diff", help="Compare two specifications")
42
+ diff_parser = subparsers.add_parser("diff", help="Compare two OpenAPI/Swagger specifications")
36
43
  diff_parser.add_argument("old", help="Old specification: file path or URL")
37
44
  diff_parser.add_argument("new", help="New specification: file path or URL")
38
45
  _add_common_diff_args(diff_parser)
39
46
 
47
+ graphql_parser = subparsers.add_parser("diff-graphql", help="Compare two GraphQL SDL schemas")
48
+ graphql_parser.add_argument("old", help="Old GraphQL schema: file path or URL")
49
+ graphql_parser.add_argument("new", help="New GraphQL schema: file path or URL")
50
+ _add_common_diff_args(graphql_parser)
51
+
52
+ proto_parser = subparsers.add_parser("diff-proto", help="Compare two Protobuf/gRPC .proto schemas")
53
+ proto_parser.add_argument("old", help="Old .proto schema: file path or URL")
54
+ proto_parser.add_argument("new", help="New .proto schema: file path or URL")
55
+ _add_common_diff_args(proto_parser)
56
+
40
57
  check_parser = subparsers.add_parser(
41
58
  "check", help="For CI: compares the current specification against a git reference"
42
59
  )
@@ -60,12 +77,19 @@ def build_parser() -> argparse.ArgumentParser:
60
77
  init_parser.add_argument("--path", default=".specshift.yml", help="Path of the file to create")
61
78
  init_parser.add_argument("--force", action="store_true", help="Overwrite an existing file")
62
79
 
80
+ serve_parser = subparsers.add_parser(
81
+ "serve", help="Starts a local web dashboard for comparing specs interactively in the browser"
82
+ )
83
+ serve_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to (default: 127.0.0.1)")
84
+ serve_parser.add_argument("--port", type=int, default=8787, help="Port to listen on (default: 8787)")
85
+ serve_parser.add_argument("--no-browser", action="store_true", help="Don't automatically open a browser tab")
86
+
63
87
  return parser
64
88
 
65
89
 
66
90
  def _add_common_diff_args(parser: argparse.ArgumentParser) -> None:
67
91
  parser.add_argument(
68
- "--format", choices=["console", "markdown", "json"], default="console", help="Output format"
92
+ "--format", choices=["console", "markdown", "json", "html"], default="console", help="Output format"
69
93
  )
70
94
  parser.add_argument("--output", help="Write the output to a file (stdout if omitted)")
71
95
  parser.add_argument("--ai", action="store_true", help="Generate a natural-language AI summary")
@@ -89,13 +113,19 @@ def main(argv: Optional[list[str]] = None) -> int:
89
113
  try:
90
114
  if args.command == "diff":
91
115
  return _run_diff(args)
116
+ if args.command == "diff-graphql":
117
+ return _run_diff_graphql(args)
118
+ if args.command == "diff-proto":
119
+ return _run_diff_proto(args)
92
120
  if args.command == "check":
93
121
  return _run_check(args)
94
122
  if args.command == "watch":
95
123
  return _run_watch(args)
96
124
  if args.command == "init":
97
125
  return _run_init(args)
98
- except (SpecLoadError, GitError) as exc:
126
+ if args.command == "serve":
127
+ return _run_serve(args)
128
+ except (SpecLoadError, GitError, GraphQLLoadError, ProtoLoadError) as exc:
99
129
  print(f"Error: {exc}", file=sys.stderr)
100
130
  return 2
101
131
  except KeyboardInterrupt:
@@ -113,6 +143,20 @@ def _run_diff(args: argparse.Namespace) -> int:
113
143
  return _emit_result(result, args)
114
144
 
115
145
 
146
+ def _run_diff_graphql(args: argparse.Namespace) -> int:
147
+ old_sdl = load_sdl(args.old)
148
+ new_sdl = load_sdl(args.new)
149
+ result = diff_graphql(old_sdl, new_sdl)
150
+ return _emit_result(result, args)
151
+
152
+
153
+ def _run_diff_proto(args: argparse.Namespace) -> int:
154
+ old_proto = load_proto(args.old)
155
+ new_proto = load_proto(args.new)
156
+ result = diff_protos(old_proto, new_proto)
157
+ return _emit_result(result, args)
158
+
159
+
116
160
  def _run_check(args: argparse.Namespace) -> int:
117
161
  config = SpecShiftConfig.load(args.config)
118
162
  spec_path = args.spec or config.spec_path
@@ -151,6 +195,8 @@ def _emit_result(result: DiffResult, args: argparse.Namespace) -> int:
151
195
  output_text = render_json_report(result, ai_summary=ai_summary)
152
196
  elif args.format == "markdown":
153
197
  output_text = render_markdown_report(result, ai_summary=ai_summary)
198
+ elif args.format == "html":
199
+ output_text = render_html_report(result, ai_summary=ai_summary)
154
200
  else:
155
201
  output_text = None # console format is printed directly
156
202
 
@@ -166,9 +212,12 @@ def _emit_result(result: DiffResult, args: argparse.Namespace) -> int:
166
212
  if ai_summary:
167
213
  print(f"\nAI Summary:\n{ai_summary}")
168
214
  else:
169
- if args.output:
170
- Path(args.output).write_text(output_text, encoding="utf-8")
171
- print(f"Report written to: {args.output}")
215
+ output_path = args.output
216
+ if args.format == "html" and not output_path:
217
+ output_path = "specshift-report.html"
218
+ if output_path:
219
+ Path(output_path).write_text(output_text, encoding="utf-8")
220
+ print(f"Report written to: {output_path}")
172
221
  else:
173
222
  print(output_text)
174
223
 
@@ -194,6 +243,13 @@ def _run_init(args: argparse.Namespace) -> int:
194
243
  return 0
195
244
 
196
245
 
246
+ def _run_serve(args: argparse.Namespace) -> int:
247
+ from specshift.webapp import run_server
248
+
249
+ run_server(host=args.host, port=args.port, open_browser=not args.no_browser)
250
+ return 0
251
+
252
+
197
253
  def _run_watch(args: argparse.Namespace) -> int:
198
254
  CACHE_DIR.mkdir(exist_ok=True)
199
255
  cache_key = hashlib.sha256(args.url.encode("utf-8")).hexdigest()[:16]