delimit-cli 3.6.4 → 3.6.6
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.
- package/README.md +1 -1
- package/bin/delimit-setup.js +14 -3
- package/package.json +1 -1
- package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
- package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
- package/gateway/core/diff_engine_v2.py.bak +0 -426
- package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
- package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
- package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ That's it. Delimit auto-fetches the base branch spec, diffs it, and posts a PR c
|
|
|
39
39
|
- Step-by-step migration guide
|
|
40
40
|
- Policy violations
|
|
41
41
|
|
|
42
|
-
[View on GitHub Marketplace →](https://github.com/marketplace/actions/delimit-api-governance)
|
|
42
|
+
[View on GitHub Marketplace →](https://github.com/marketplace/actions/delimit-api-governance) · [See a live PR comment →](https://github.com/delimit-ai/delimit-quickstart/pull/1)
|
|
43
43
|
|
|
44
44
|
### Example PR comment
|
|
45
45
|
|
package/bin/delimit-setup.js
CHANGED
|
@@ -184,10 +184,14 @@ async function main() {
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
// Step 3d: Configure Gemini CLI (if installed)
|
|
187
|
-
const
|
|
188
|
-
|
|
187
|
+
const GEMINI_DIR = path.join(os.homedir(), '.gemini');
|
|
188
|
+
const GEMINI_CONFIG = path.join(GEMINI_DIR, 'settings.json');
|
|
189
|
+
if (fs.existsSync(GEMINI_DIR)) {
|
|
189
190
|
try {
|
|
190
|
-
let geminiConfig =
|
|
191
|
+
let geminiConfig = {};
|
|
192
|
+
if (fs.existsSync(GEMINI_CONFIG)) {
|
|
193
|
+
geminiConfig = JSON.parse(fs.readFileSync(GEMINI_CONFIG, 'utf-8'));
|
|
194
|
+
}
|
|
191
195
|
if (!geminiConfig.mcpServers) geminiConfig.mcpServers = {};
|
|
192
196
|
if (geminiConfig.mcpServers.delimit) {
|
|
193
197
|
log(` ${green('✓')} Delimit already in Gemini CLI config`);
|
|
@@ -308,6 +312,13 @@ Run full governance compliance checks. Verify security, policy compliance, evide
|
|
|
308
312
|
log('');
|
|
309
313
|
log(` ${green('Delimit is installed.')} Your AI now has persistent memory and governance.`);
|
|
310
314
|
log('');
|
|
315
|
+
log(' Configured for:');
|
|
316
|
+
const tools = ['Claude Code'];
|
|
317
|
+
if (fs.existsSync(CODEX_CONFIG)) tools.push('Codex');
|
|
318
|
+
if (fs.existsSync(path.join(os.homedir(), '.cursor'))) tools.push('Cursor');
|
|
319
|
+
if (fs.existsSync(GEMINI_DIR)) tools.push('Gemini CLI');
|
|
320
|
+
log(` ${green('✓')} ${tools.join(', ')}`);
|
|
321
|
+
log('');
|
|
311
322
|
log(' Try it now:');
|
|
312
323
|
log(` ${bold('$ claude')}`);
|
|
313
324
|
log('');
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Enhanced OpenAPI diff engine with deep schema comparison.
|
|
3
|
-
Handles nested objects, response schemas, enums, and edge cases.
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
from typing import Dict, List, Any, Optional, Set, Tuple
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from enum import Enum
|
|
9
|
-
|
|
10
|
-
class ChangeType(Enum):
|
|
11
|
-
# Breaking changes
|
|
12
|
-
ENDPOINT_REMOVED = "endpoint_removed"
|
|
13
|
-
METHOD_REMOVED = "method_removed"
|
|
14
|
-
REQUIRED_PARAM_ADDED = "required_param_added"
|
|
15
|
-
PARAM_REMOVED = "param_removed"
|
|
16
|
-
RESPONSE_REMOVED = "response_removed"
|
|
17
|
-
REQUIRED_FIELD_ADDED = "required_field_added"
|
|
18
|
-
FIELD_REMOVED = "field_removed"
|
|
19
|
-
TYPE_CHANGED = "type_changed"
|
|
20
|
-
FORMAT_CHANGED = "format_changed"
|
|
21
|
-
ENUM_VALUE_REMOVED = "enum_value_removed"
|
|
22
|
-
|
|
23
|
-
# Non-breaking changes
|
|
24
|
-
ENDPOINT_ADDED = "endpoint_added"
|
|
25
|
-
METHOD_ADDED = "method_added"
|
|
26
|
-
OPTIONAL_PARAM_ADDED = "optional_param_added"
|
|
27
|
-
RESPONSE_ADDED = "response_added"
|
|
28
|
-
OPTIONAL_FIELD_ADDED = "optional_field_added"
|
|
29
|
-
ENUM_VALUE_ADDED = "enum_value_added"
|
|
30
|
-
DESCRIPTION_CHANGED = "description_changed"
|
|
31
|
-
|
|
32
|
-
@dataclass
|
|
33
|
-
class Change:
|
|
34
|
-
type: ChangeType
|
|
35
|
-
path: str
|
|
36
|
-
details: Dict[str, Any]
|
|
37
|
-
severity: str # high, medium, low
|
|
38
|
-
message: str
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def is_breaking(self) -> bool:
|
|
42
|
-
return self.type in [
|
|
43
|
-
ChangeType.ENDPOINT_REMOVED,
|
|
44
|
-
ChangeType.METHOD_REMOVED,
|
|
45
|
-
ChangeType.REQUIRED_PARAM_ADDED,
|
|
46
|
-
ChangeType.PARAM_REMOVED,
|
|
47
|
-
ChangeType.RESPONSE_REMOVED,
|
|
48
|
-
ChangeType.REQUIRED_FIELD_ADDED,
|
|
49
|
-
ChangeType.FIELD_REMOVED,
|
|
50
|
-
ChangeType.TYPE_CHANGED,
|
|
51
|
-
ChangeType.FORMAT_CHANGED,
|
|
52
|
-
ChangeType.ENUM_VALUE_REMOVED,
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
class OpenAPIDiffEngine:
|
|
56
|
-
"""Advanced diff engine for OpenAPI specifications."""
|
|
57
|
-
|
|
58
|
-
def __init__(self):
|
|
59
|
-
self.changes: List[Change] = []
|
|
60
|
-
|
|
61
|
-
def compare(self, old_spec: Dict, new_spec: Dict) -> List[Change]:
|
|
62
|
-
"""Compare two OpenAPI specifications and return all changes."""
|
|
63
|
-
self.changes = []
|
|
64
|
-
|
|
65
|
-
# Compare paths
|
|
66
|
-
self._compare_paths(old_spec.get("paths", {}), new_spec.get("paths", {}))
|
|
67
|
-
|
|
68
|
-
# Compare components/schemas
|
|
69
|
-
self._compare_schemas(
|
|
70
|
-
old_spec.get("components", {}).get("schemas", {}),
|
|
71
|
-
new_spec.get("components", {}).get("schemas", {})
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
# Compare security schemes
|
|
75
|
-
self._compare_security(
|
|
76
|
-
old_spec.get("components", {}).get("securitySchemes", {}),
|
|
77
|
-
new_spec.get("components", {}).get("securitySchemes", {})
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
return self.changes
|
|
81
|
-
|
|
82
|
-
def _compare_paths(self, old_paths: Dict, new_paths: Dict):
|
|
83
|
-
"""Compare API paths/endpoints."""
|
|
84
|
-
old_set = set(old_paths.keys())
|
|
85
|
-
new_set = set(new_paths.keys())
|
|
86
|
-
|
|
87
|
-
# Check removed endpoints
|
|
88
|
-
for path in old_set - new_set:
|
|
89
|
-
self.changes.append(Change(
|
|
90
|
-
type=ChangeType.ENDPOINT_REMOVED,
|
|
91
|
-
path=path,
|
|
92
|
-
details={"endpoint": path},
|
|
93
|
-
severity="high",
|
|
94
|
-
message=f"Endpoint removed: {path}"
|
|
95
|
-
))
|
|
96
|
-
|
|
97
|
-
# Check added endpoints
|
|
98
|
-
for path in new_set - old_set:
|
|
99
|
-
self.changes.append(Change(
|
|
100
|
-
type=ChangeType.ENDPOINT_ADDED,
|
|
101
|
-
path=path,
|
|
102
|
-
details={"endpoint": path},
|
|
103
|
-
severity="low",
|
|
104
|
-
message=f"New endpoint added: {path}"
|
|
105
|
-
))
|
|
106
|
-
|
|
107
|
-
# Check modified endpoints
|
|
108
|
-
for path in old_set & new_set:
|
|
109
|
-
self._compare_methods(path, old_paths[path], new_paths[path])
|
|
110
|
-
|
|
111
|
-
def _compare_methods(self, path: str, old_methods: Dict, new_methods: Dict):
|
|
112
|
-
"""Compare HTTP methods for an endpoint."""
|
|
113
|
-
old_set = set(m for m in old_methods.keys() if m in ["get", "post", "put", "delete", "patch", "head", "options"])
|
|
114
|
-
new_set = set(m for m in new_methods.keys() if m in ["get", "post", "put", "delete", "patch", "head", "options"])
|
|
115
|
-
|
|
116
|
-
# Check removed methods
|
|
117
|
-
for method in old_set - new_set:
|
|
118
|
-
self.changes.append(Change(
|
|
119
|
-
type=ChangeType.METHOD_REMOVED,
|
|
120
|
-
path=f"{path}:{method.upper()}",
|
|
121
|
-
details={"endpoint": path, "method": method.upper()},
|
|
122
|
-
severity="high",
|
|
123
|
-
message=f"Method removed: {method.upper()} {path}"
|
|
124
|
-
))
|
|
125
|
-
|
|
126
|
-
# Check modified methods
|
|
127
|
-
for method in old_set & new_set:
|
|
128
|
-
self._compare_operation(
|
|
129
|
-
f"{path}:{method.upper()}",
|
|
130
|
-
old_methods[method],
|
|
131
|
-
new_methods[method]
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
def _compare_operation(self, operation_id: str, old_op: Dict, new_op: Dict):
|
|
135
|
-
"""Compare operation details (parameters, responses, etc.)."""
|
|
136
|
-
|
|
137
|
-
# Compare parameters
|
|
138
|
-
old_params = {self._param_key(p): p for p in old_op.get("parameters", [])}
|
|
139
|
-
new_params = {self._param_key(p): p for p in new_op.get("parameters", [])}
|
|
140
|
-
|
|
141
|
-
# Check removed parameters
|
|
142
|
-
for param_key in set(old_params.keys()) - set(new_params.keys()):
|
|
143
|
-
param = old_params[param_key]
|
|
144
|
-
self.changes.append(Change(
|
|
145
|
-
type=ChangeType.PARAM_REMOVED,
|
|
146
|
-
path=operation_id,
|
|
147
|
-
details={"parameter": param["name"], "in": param["in"]},
|
|
148
|
-
severity="high",
|
|
149
|
-
message=f"Parameter removed: {param['name']} from {operation_id}"
|
|
150
|
-
))
|
|
151
|
-
|
|
152
|
-
# Check added required parameters
|
|
153
|
-
for param_key in set(new_params.keys()) - set(old_params.keys()):
|
|
154
|
-
param = new_params[param_key]
|
|
155
|
-
if param.get("required", False):
|
|
156
|
-
self.changes.append(Change(
|
|
157
|
-
type=ChangeType.REQUIRED_PARAM_ADDED,
|
|
158
|
-
path=operation_id,
|
|
159
|
-
details={"parameter": param["name"], "in": param["in"]},
|
|
160
|
-
severity="high",
|
|
161
|
-
message=f"Required parameter added: {param['name']} to {operation_id}"
|
|
162
|
-
))
|
|
163
|
-
|
|
164
|
-
# Check parameter schema changes
|
|
165
|
-
for param_key in set(old_params.keys()) & set(new_params.keys()):
|
|
166
|
-
self._compare_parameter_schemas(
|
|
167
|
-
operation_id,
|
|
168
|
-
old_params[param_key],
|
|
169
|
-
new_params[param_key]
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
# Compare request body
|
|
173
|
-
if "requestBody" in old_op or "requestBody" in new_op:
|
|
174
|
-
self._compare_request_body(
|
|
175
|
-
operation_id,
|
|
176
|
-
old_op.get("requestBody"),
|
|
177
|
-
new_op.get("requestBody")
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
# Compare responses
|
|
181
|
-
self._compare_responses(
|
|
182
|
-
operation_id,
|
|
183
|
-
old_op.get("responses", {}),
|
|
184
|
-
new_op.get("responses", {})
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
def _compare_parameter_schemas(self, operation_id: str, old_param: Dict, new_param: Dict):
|
|
188
|
-
"""Compare parameter schemas for type changes."""
|
|
189
|
-
old_schema = old_param.get("schema", {})
|
|
190
|
-
new_schema = new_param.get("schema", {})
|
|
191
|
-
|
|
192
|
-
# Check type changes
|
|
193
|
-
if old_schema.get("type") != new_schema.get("type"):
|
|
194
|
-
self.changes.append(Change(
|
|
195
|
-
type=ChangeType.TYPE_CHANGED,
|
|
196
|
-
path=operation_id,
|
|
197
|
-
details={
|
|
198
|
-
"parameter": old_param["name"],
|
|
199
|
-
"old_type": old_schema.get("type"),
|
|
200
|
-
"new_type": new_schema.get("type")
|
|
201
|
-
},
|
|
202
|
-
severity="high",
|
|
203
|
-
message=f"Parameter type changed: {old_param['name']} from {old_schema.get('type')} to {new_schema.get('type')}"
|
|
204
|
-
))
|
|
205
|
-
|
|
206
|
-
# Check enum changes
|
|
207
|
-
if "enum" in old_schema or "enum" in new_schema:
|
|
208
|
-
self._compare_enums(
|
|
209
|
-
f"{operation_id}:{old_param['name']}",
|
|
210
|
-
old_schema.get("enum", []),
|
|
211
|
-
new_schema.get("enum", [])
|
|
212
|
-
)
|
|
213
|
-
|
|
214
|
-
def _compare_request_body(self, operation_id: str, old_body: Optional[Dict], new_body: Optional[Dict]):
|
|
215
|
-
"""Compare request body schemas."""
|
|
216
|
-
if old_body and not new_body:
|
|
217
|
-
self.changes.append(Change(
|
|
218
|
-
type=ChangeType.FIELD_REMOVED,
|
|
219
|
-
path=operation_id,
|
|
220
|
-
details={"field": "request_body"},
|
|
221
|
-
severity="high",
|
|
222
|
-
message=f"Request body removed from {operation_id}"
|
|
223
|
-
))
|
|
224
|
-
elif not old_body and new_body and new_body.get("required", False):
|
|
225
|
-
self.changes.append(Change(
|
|
226
|
-
type=ChangeType.REQUIRED_FIELD_ADDED,
|
|
227
|
-
path=operation_id,
|
|
228
|
-
details={"field": "request_body"},
|
|
229
|
-
severity="high",
|
|
230
|
-
message=f"Required request body added to {operation_id}"
|
|
231
|
-
))
|
|
232
|
-
elif old_body and new_body:
|
|
233
|
-
# Compare content types
|
|
234
|
-
old_content = old_body.get("content", {})
|
|
235
|
-
new_content = new_body.get("content", {})
|
|
236
|
-
|
|
237
|
-
for content_type in old_content.keys() & new_content.keys():
|
|
238
|
-
self._compare_schema_deep(
|
|
239
|
-
f"{operation_id}:request",
|
|
240
|
-
old_content[content_type].get("schema", {}),
|
|
241
|
-
new_content[content_type].get("schema", {})
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
def _compare_responses(self, operation_id: str, old_responses: Dict, new_responses: Dict):
|
|
245
|
-
"""Compare response definitions."""
|
|
246
|
-
old_codes = set(old_responses.keys())
|
|
247
|
-
new_codes = set(new_responses.keys())
|
|
248
|
-
|
|
249
|
-
# Check removed responses
|
|
250
|
-
for code in old_codes - new_codes:
|
|
251
|
-
# Only flag 2xx responses as breaking
|
|
252
|
-
if code.startswith("2"):
|
|
253
|
-
self.changes.append(Change(
|
|
254
|
-
type=ChangeType.RESPONSE_REMOVED,
|
|
255
|
-
path=operation_id,
|
|
256
|
-
details={"response_code": code},
|
|
257
|
-
severity="high",
|
|
258
|
-
message=f"Success response {code} removed from {operation_id}"
|
|
259
|
-
))
|
|
260
|
-
|
|
261
|
-
# Compare response schemas
|
|
262
|
-
for code in old_codes & new_codes:
|
|
263
|
-
old_resp = old_responses[code]
|
|
264
|
-
new_resp = new_responses[code]
|
|
265
|
-
|
|
266
|
-
if "content" in old_resp or "content" in new_resp:
|
|
267
|
-
old_content = old_resp.get("content", {})
|
|
268
|
-
new_content = new_resp.get("content", {})
|
|
269
|
-
|
|
270
|
-
for content_type in old_content.keys() & new_content.keys():
|
|
271
|
-
self._compare_schema_deep(
|
|
272
|
-
f"{operation_id}:{code}",
|
|
273
|
-
old_content[content_type].get("schema", {}),
|
|
274
|
-
new_content[content_type].get("schema", {})
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
def _compare_schema_deep(self, path: str, old_schema: Dict, new_schema: Dict, required_fields: Optional[Set[str]] = None):
|
|
278
|
-
"""Deep comparison of schemas including nested objects."""
|
|
279
|
-
|
|
280
|
-
# Handle references
|
|
281
|
-
if "$ref" in old_schema or "$ref" in new_schema:
|
|
282
|
-
# TODO: Resolve references properly
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
# Compare types
|
|
286
|
-
old_type = old_schema.get("type")
|
|
287
|
-
new_type = new_schema.get("type")
|
|
288
|
-
|
|
289
|
-
if old_type != new_type and old_type is not None:
|
|
290
|
-
self.changes.append(Change(
|
|
291
|
-
type=ChangeType.TYPE_CHANGED,
|
|
292
|
-
path=path,
|
|
293
|
-
details={"old_type": old_type, "new_type": new_type},
|
|
294
|
-
severity="high",
|
|
295
|
-
message=f"Type changed from {old_type} to {new_type} at {path}"
|
|
296
|
-
))
|
|
297
|
-
return
|
|
298
|
-
|
|
299
|
-
# Compare object properties
|
|
300
|
-
if old_type == "object":
|
|
301
|
-
old_props = old_schema.get("properties", {})
|
|
302
|
-
new_props = new_schema.get("properties", {})
|
|
303
|
-
old_required = set(old_schema.get("required", []))
|
|
304
|
-
new_required = set(new_schema.get("required", []))
|
|
305
|
-
|
|
306
|
-
# Check removed fields
|
|
307
|
-
for prop in set(old_props.keys()) - set(new_props.keys()):
|
|
308
|
-
if prop in old_required:
|
|
309
|
-
self.changes.append(Change(
|
|
310
|
-
type=ChangeType.FIELD_REMOVED,
|
|
311
|
-
path=f"{path}.{prop}",
|
|
312
|
-
details={"field": prop},
|
|
313
|
-
severity="high",
|
|
314
|
-
message=f"Required field '{prop}' removed at {path}"
|
|
315
|
-
))
|
|
316
|
-
|
|
317
|
-
# Check new required fields
|
|
318
|
-
for prop in new_required - old_required:
|
|
319
|
-
if prop not in old_props:
|
|
320
|
-
self.changes.append(Change(
|
|
321
|
-
type=ChangeType.REQUIRED_FIELD_ADDED,
|
|
322
|
-
path=f"{path}.{prop}",
|
|
323
|
-
details={"field": prop},
|
|
324
|
-
severity="high",
|
|
325
|
-
message=f"New required field '{prop}' added at {path}"
|
|
326
|
-
))
|
|
327
|
-
|
|
328
|
-
# Recursively compare nested properties
|
|
329
|
-
for prop in set(old_props.keys()) & set(new_props.keys()):
|
|
330
|
-
self._compare_schema_deep(
|
|
331
|
-
f"{path}.{prop}",
|
|
332
|
-
old_props[prop],
|
|
333
|
-
new_props[prop],
|
|
334
|
-
old_required if prop in old_required else None
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
# Compare arrays
|
|
338
|
-
elif old_type == "array":
|
|
339
|
-
if "items" in old_schema and "items" in new_schema:
|
|
340
|
-
self._compare_schema_deep(
|
|
341
|
-
f"{path}[]",
|
|
342
|
-
old_schema["items"],
|
|
343
|
-
new_schema["items"]
|
|
344
|
-
)
|
|
345
|
-
|
|
346
|
-
# Compare enums
|
|
347
|
-
if "enum" in old_schema or "enum" in new_schema:
|
|
348
|
-
self._compare_enums(path, old_schema.get("enum", []), new_schema.get("enum", []))
|
|
349
|
-
|
|
350
|
-
def _compare_enums(self, path: str, old_enum: List, new_enum: List):
|
|
351
|
-
"""Compare enum values."""
|
|
352
|
-
old_set = set(old_enum)
|
|
353
|
-
new_set = set(new_enum)
|
|
354
|
-
|
|
355
|
-
# Removed enum values are breaking
|
|
356
|
-
for value in old_set - new_set:
|
|
357
|
-
self.changes.append(Change(
|
|
358
|
-
type=ChangeType.ENUM_VALUE_REMOVED,
|
|
359
|
-
path=path,
|
|
360
|
-
details={"value": value},
|
|
361
|
-
severity="high",
|
|
362
|
-
message=f"Enum value '{value}' removed at {path}"
|
|
363
|
-
))
|
|
364
|
-
|
|
365
|
-
# Added enum values are non-breaking
|
|
366
|
-
for value in new_set - old_set:
|
|
367
|
-
self.changes.append(Change(
|
|
368
|
-
type=ChangeType.ENUM_VALUE_ADDED,
|
|
369
|
-
path=path,
|
|
370
|
-
details={"value": value},
|
|
371
|
-
severity="low",
|
|
372
|
-
message=f"Enum value '{value}' added at {path}"
|
|
373
|
-
))
|
|
374
|
-
|
|
375
|
-
def _compare_schemas(self, old_schemas: Dict, new_schemas: Dict):
|
|
376
|
-
"""Compare component schemas."""
|
|
377
|
-
# Schema removal is breaking if referenced
|
|
378
|
-
for schema_name in set(old_schemas.keys()) - set(new_schemas.keys()):
|
|
379
|
-
self.changes.append(Change(
|
|
380
|
-
type=ChangeType.FIELD_REMOVED,
|
|
381
|
-
path=f"#/components/schemas/{schema_name}",
|
|
382
|
-
details={"schema": schema_name},
|
|
383
|
-
severity="medium",
|
|
384
|
-
message=f"Schema '{schema_name}' removed"
|
|
385
|
-
))
|
|
386
|
-
|
|
387
|
-
# Compare existing schemas
|
|
388
|
-
for schema_name in set(old_schemas.keys()) & set(new_schemas.keys()):
|
|
389
|
-
self._compare_schema_deep(
|
|
390
|
-
f"#/components/schemas/{schema_name}",
|
|
391
|
-
old_schemas[schema_name],
|
|
392
|
-
new_schemas[schema_name]
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
def _compare_security(self, old_security: Dict, new_security: Dict):
|
|
396
|
-
"""Compare security schemes."""
|
|
397
|
-
# Security scheme changes are usually breaking
|
|
398
|
-
for scheme in set(old_security.keys()) - set(new_security.keys()):
|
|
399
|
-
self.changes.append(Change(
|
|
400
|
-
type=ChangeType.FIELD_REMOVED,
|
|
401
|
-
path=f"#/components/securitySchemes/{scheme}",
|
|
402
|
-
details={"scheme": scheme},
|
|
403
|
-
severity="high",
|
|
404
|
-
message=f"Security scheme '{scheme}' removed"
|
|
405
|
-
))
|
|
406
|
-
|
|
407
|
-
def _param_key(self, param: Dict) -> str:
|
|
408
|
-
"""Generate unique key for parameter."""
|
|
409
|
-
return f"{param.get('in', 'query')}:{param.get('name', '')}"
|
|
410
|
-
|
|
411
|
-
def get_breaking_changes(self) -> List[Change]:
|
|
412
|
-
"""Get only breaking changes."""
|
|
413
|
-
return [c for c in self.changes if c.is_breaking]
|
|
414
|
-
|
|
415
|
-
def get_summary(self) -> Dict[str, Any]:
|
|
416
|
-
"""Get summary of all changes."""
|
|
417
|
-
breaking = self.get_breaking_changes()
|
|
418
|
-
return {
|
|
419
|
-
"total_changes": len(self.changes),
|
|
420
|
-
"breaking_changes": len(breaking),
|
|
421
|
-
"endpoints_removed": len([c for c in breaking if c.type == ChangeType.ENDPOINT_REMOVED]),
|
|
422
|
-
"methods_removed": len([c for c in breaking if c.type == ChangeType.METHOD_REMOVED]),
|
|
423
|
-
"parameters_changed": len([c for c in breaking if c.type in [ChangeType.PARAM_REMOVED, ChangeType.REQUIRED_PARAM_ADDED]]),
|
|
424
|
-
"schemas_changed": len([c for c in breaking if c.type in [ChangeType.FIELD_REMOVED, ChangeType.REQUIRED_FIELD_ADDED, ChangeType.TYPE_CHANGED]]),
|
|
425
|
-
"is_breaking": len(breaking) > 0
|
|
426
|
-
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|