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.
Files changed (35) hide show
  1. {specshift-1.3.0/specshift.egg-info → specshift-1.4.0}/PKG-INFO +46 -3
  2. {specshift-1.3.0 → specshift-1.4.0}/README.md +43 -0
  3. {specshift-1.3.0 → specshift-1.4.0}/pyproject.toml +3 -2
  4. {specshift-1.3.0 → specshift-1.4.0}/specshift/__init__.py +3 -1
  5. {specshift-1.3.0 → specshift-1.4.0}/specshift/ai_summary.py +80 -13
  6. {specshift-1.3.0 → specshift-1.4.0}/specshift/cli.py +23 -7
  7. specshift-1.4.0/specshift/linter.py +326 -0
  8. {specshift-1.3.0 → specshift-1.4.0}/specshift/reporter.py +61 -46
  9. {specshift-1.3.0 → specshift-1.4.0/specshift.egg-info}/PKG-INFO +46 -3
  10. {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/SOURCES.txt +2 -0
  11. {specshift-1.3.0 → specshift-1.4.0}/tests/test_cli.py +26 -0
  12. specshift-1.4.0/tests/test_linter.py +166 -0
  13. {specshift-1.3.0 → specshift-1.4.0}/LICENSE +0 -0
  14. {specshift-1.3.0 → specshift-1.4.0}/setup.cfg +0 -0
  15. {specshift-1.3.0 → specshift-1.4.0}/specshift/changelog.py +0 -0
  16. {specshift-1.3.0 → specshift-1.4.0}/specshift/config.py +0 -0
  17. {specshift-1.3.0 → specshift-1.4.0}/specshift/differ.py +0 -0
  18. {specshift-1.3.0 → specshift-1.4.0}/specshift/git_utils.py +0 -0
  19. {specshift-1.3.0 → specshift-1.4.0}/specshift/graphql_differ.py +0 -0
  20. {specshift-1.3.0 → specshift-1.4.0}/specshift/models.py +0 -0
  21. {specshift-1.3.0 → specshift-1.4.0}/specshift/notifier.py +0 -0
  22. {specshift-1.3.0 → specshift-1.4.0}/specshift/protobuf_differ.py +0 -0
  23. {specshift-1.3.0 → specshift-1.4.0}/specshift/spec_loader.py +0 -0
  24. {specshift-1.3.0 → specshift-1.4.0}/specshift/webapp.py +0 -0
  25. {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/dependency_links.txt +0 -0
  26. {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/entry_points.txt +0 -0
  27. {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/requires.txt +0 -0
  28. {specshift-1.3.0 → specshift-1.4.0}/specshift.egg-info/top_level.txt +0 -0
  29. {specshift-1.3.0 → specshift-1.4.0}/tests/test_changelog.py +0 -0
  30. {specshift-1.3.0 → specshift-1.4.0}/tests/test_differ.py +0 -0
  31. {specshift-1.3.0 → specshift-1.4.0}/tests/test_graphql_differ.py +0 -0
  32. {specshift-1.3.0 → specshift-1.4.0}/tests/test_protobuf_differ.py +0 -0
  33. {specshift-1.3.0 → specshift-1.4.0}/tests/test_reporter.py +0 -0
  34. {specshift-1.3.0 → specshift-1.4.0}/tests/test_spec_loader.py +0 -0
  35. {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.3.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,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.3.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,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.3.0"
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
- "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)
@@ -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: