delimit-cli 2.4.0 → 3.0.0
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/.dockerignore +7 -0
- package/.github/workflows/ci.yml +22 -0
- package/CODE_OF_CONDUCT.md +48 -0
- package/CONTRIBUTING.md +67 -0
- package/Dockerfile +9 -0
- package/LICENSE +21 -0
- package/README.md +18 -69
- package/SECURITY.md +42 -0
- package/adapters/gemini-forge.js +11 -0
- package/adapters/gemini-jamsons.js +152 -0
- package/bin/delimit-cli.js +8 -0
- package/bin/delimit-setup.js +258 -0
- package/gateway/ai/backends/__init__.py +0 -0
- package/gateway/ai/backends/async_utils.py +21 -0
- package/gateway/ai/backends/deploy_bridge.py +150 -0
- package/gateway/ai/backends/gateway_core.py +261 -0
- package/gateway/ai/backends/generate_bridge.py +38 -0
- package/gateway/ai/backends/governance_bridge.py +196 -0
- package/gateway/ai/backends/intel_bridge.py +59 -0
- package/gateway/ai/backends/memory_bridge.py +93 -0
- package/gateway/ai/backends/ops_bridge.py +137 -0
- package/gateway/ai/backends/os_bridge.py +82 -0
- package/gateway/ai/backends/repo_bridge.py +117 -0
- package/gateway/ai/backends/ui_bridge.py +118 -0
- package/gateway/ai/backends/vault_bridge.py +129 -0
- package/gateway/ai/server.py +1182 -0
- package/gateway/core/__init__.py +3 -0
- 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/auto_baseline.py +304 -0
- package/gateway/core/ci_formatter.py +283 -0
- package/gateway/core/complexity_analyzer.py +386 -0
- package/gateway/core/contract_ledger.py +345 -0
- package/gateway/core/dependency_graph.py +218 -0
- package/gateway/core/dependency_manifest.py +223 -0
- package/gateway/core/diff_engine_v2.py +477 -0
- package/gateway/core/diff_engine_v2.py.bak +426 -0
- package/gateway/core/event_backbone.py +268 -0
- package/gateway/core/event_schema.py +258 -0
- package/gateway/core/explainer.py +438 -0
- package/gateway/core/gateway.py +128 -0
- package/gateway/core/gateway_v2.py +154 -0
- package/gateway/core/gateway_v3.py +224 -0
- package/gateway/core/impact_analyzer.py +163 -0
- package/gateway/core/policies/default.yml +13 -0
- package/gateway/core/policies/relaxed.yml +48 -0
- package/gateway/core/policies/strict.yml +55 -0
- package/gateway/core/policy_engine.py +464 -0
- package/gateway/core/registry.py +52 -0
- package/gateway/core/registry_v2.py +132 -0
- package/gateway/core/registry_v3.py +134 -0
- package/gateway/core/semver_classifier.py +152 -0
- package/gateway/core/spec_detector.py +130 -0
- package/gateway/core/surface_bridge.py +307 -0
- package/gateway/core/zero_spec/__init__.py +4 -0
- 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/core/zero_spec/detector.py +353 -0
- package/gateway/core/zero_spec/express_extractor.py +483 -0
- package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
- package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
- package/gateway/tasks/__init__.py +1 -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/gateway/tasks/check_policy.py +177 -0
- package/gateway/tasks/check_policy_v2.py +255 -0
- package/gateway/tasks/check_policy_v3.py +255 -0
- package/gateway/tasks/explain_diff.py +305 -0
- package/gateway/tasks/explain_diff_v2.py +267 -0
- package/gateway/tasks/validate_api.py +131 -0
- package/gateway/tasks/validate_api_v2.py +208 -0
- package/gateway/tasks/validate_api_v3.py +163 -0
- package/package.json +2 -2
- package/adapters/codex-skill.js +0 -87
- package/adapters/cursor-extension.js +0 -190
- package/adapters/gemini-action.js +0 -93
- package/adapters/openai-function.js +0 -112
- package/adapters/xai-plugin.js +0 -151
- package/test-decision-engine.js +0 -181
- package/test-hook.js +0 -27
- package/tests/cli.test.js +0 -359
- package/tests/fixtures/openapi-changed.yaml +0 -56
- package/tests/fixtures/openapi.yaml +0 -87
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Delimit Complexity Analyzer
|
|
3
|
+
Deterministic API complexity scoring for upgrade signal generation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import yaml
|
|
8
|
+
from typing import Dict, Any, Optional, List, Union
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ComplexityAnalyzer:
|
|
13
|
+
"""Analyze OpenAPI specification complexity to determine governance needs."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
"""Initialize the analyzer with scoring thresholds."""
|
|
17
|
+
# Scoring thresholds for each metric
|
|
18
|
+
self.scoring_rules = {
|
|
19
|
+
'endpoint_count': [
|
|
20
|
+
(10, 5), # 0-10 endpoints = 5 points
|
|
21
|
+
(25, 10), # 11-25 = 10 points
|
|
22
|
+
(50, 20), # 26-50 = 20 points
|
|
23
|
+
(float('inf'), 30) # 50+ = 30 points
|
|
24
|
+
],
|
|
25
|
+
'schema_count': [
|
|
26
|
+
(5, 5), # 0-5 = 5 points
|
|
27
|
+
(20, 10), # 6-20 = 10 points
|
|
28
|
+
(50, 20), # 21-50 = 20 points
|
|
29
|
+
(float('inf'), 30) # 50+ = 30 points
|
|
30
|
+
],
|
|
31
|
+
'parameter_count': [
|
|
32
|
+
(20, 5), # 0-20 = 5 points
|
|
33
|
+
(50, 10), # 21-50 = 10 points
|
|
34
|
+
(float('inf'), 20) # 50+ = 20 points
|
|
35
|
+
],
|
|
36
|
+
'response_variants': [
|
|
37
|
+
(5, 5), # ≤5 = 5 points
|
|
38
|
+
(10, 10), # 6-10 = 10 points
|
|
39
|
+
(float('inf'), 20) # >10 = 20 points
|
|
40
|
+
],
|
|
41
|
+
'nested_schema_depth': [
|
|
42
|
+
(3, 5), # ≤3 = 5 points
|
|
43
|
+
(6, 10), # 4-6 = 10 points
|
|
44
|
+
(float('inf'), 20) # >6 = 20 points
|
|
45
|
+
],
|
|
46
|
+
'security_schemes': [
|
|
47
|
+
(0, 0), # 0 = 0 points
|
|
48
|
+
(1, 5), # 1 = 5 points
|
|
49
|
+
(3, 10), # 2-3 = 10 points
|
|
50
|
+
(float('inf'), 20) # >3 = 20 points
|
|
51
|
+
],
|
|
52
|
+
'example_count': [
|
|
53
|
+
(0, 0), # 0 = 0 points
|
|
54
|
+
(10, 5), # 1-10 = 5 points
|
|
55
|
+
(30, 10), # 11-30 = 10 points
|
|
56
|
+
(float('inf'), 20) # >30 = 20 points
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
self.classification_thresholds = [
|
|
61
|
+
(25, "Simple API"),
|
|
62
|
+
(50, "Moderate API"),
|
|
63
|
+
(75, "Complex API"),
|
|
64
|
+
(100, "Enterprise-scale API")
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
def analyze_openapi_complexity(self, spec: Union[str, Dict, Path]) -> Dict[str, Any]:
|
|
68
|
+
"""
|
|
69
|
+
Analyze OpenAPI specification complexity.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
spec: OpenAPI specification as string, dict, or file path
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dictionary with score, classification, and metrics
|
|
76
|
+
"""
|
|
77
|
+
# Parse specification if needed
|
|
78
|
+
parsed_spec = self._parse_spec(spec)
|
|
79
|
+
|
|
80
|
+
# Extract metrics
|
|
81
|
+
metrics = self._extract_metrics(parsed_spec)
|
|
82
|
+
|
|
83
|
+
# Calculate score
|
|
84
|
+
score = self._calculate_score(metrics)
|
|
85
|
+
|
|
86
|
+
# Determine classification
|
|
87
|
+
classification = self._classify_complexity(score)
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
"score": score,
|
|
91
|
+
"classification": classification,
|
|
92
|
+
"metrics": metrics
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
def _parse_spec(self, spec: Union[str, Dict, Path]) -> Dict:
|
|
96
|
+
"""Parse OpenAPI specification from various input formats."""
|
|
97
|
+
if isinstance(spec, dict):
|
|
98
|
+
return spec
|
|
99
|
+
|
|
100
|
+
if isinstance(spec, Path):
|
|
101
|
+
spec = spec.read_text()
|
|
102
|
+
|
|
103
|
+
if isinstance(spec, str):
|
|
104
|
+
# Try JSON first
|
|
105
|
+
try:
|
|
106
|
+
return json.loads(spec)
|
|
107
|
+
except json.JSONDecodeError:
|
|
108
|
+
# Try YAML
|
|
109
|
+
try:
|
|
110
|
+
return yaml.safe_load(spec)
|
|
111
|
+
except yaml.YAMLError:
|
|
112
|
+
# Assume it's a file path
|
|
113
|
+
with open(spec, 'r') as f:
|
|
114
|
+
content = f.read()
|
|
115
|
+
try:
|
|
116
|
+
return json.loads(content)
|
|
117
|
+
except json.JSONDecodeError:
|
|
118
|
+
return yaml.safe_load(content)
|
|
119
|
+
|
|
120
|
+
raise ValueError("Unable to parse OpenAPI specification")
|
|
121
|
+
|
|
122
|
+
def _extract_metrics(self, spec: Dict) -> Dict[str, int]:
|
|
123
|
+
"""Extract complexity metrics from OpenAPI specification."""
|
|
124
|
+
metrics = {
|
|
125
|
+
'endpoint_count': self._count_endpoints(spec),
|
|
126
|
+
'schema_count': self._count_schemas(spec),
|
|
127
|
+
'parameter_count': self._count_parameters(spec),
|
|
128
|
+
'response_variants': self._count_response_variants(spec),
|
|
129
|
+
'nested_schema_depth': self._calculate_max_schema_depth(spec),
|
|
130
|
+
'security_schemes': self._count_security_schemes(spec),
|
|
131
|
+
'example_count': self._count_examples(spec)
|
|
132
|
+
}
|
|
133
|
+
return metrics
|
|
134
|
+
|
|
135
|
+
def _count_endpoints(self, spec: Dict) -> int:
|
|
136
|
+
"""Count total HTTP methods across all paths."""
|
|
137
|
+
count = 0
|
|
138
|
+
paths = spec.get('paths', {})
|
|
139
|
+
|
|
140
|
+
for path, path_item in paths.items():
|
|
141
|
+
if isinstance(path_item, dict):
|
|
142
|
+
# Count HTTP methods (excluding parameters and other non-method keys)
|
|
143
|
+
http_methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']
|
|
144
|
+
for method in http_methods:
|
|
145
|
+
if method in path_item:
|
|
146
|
+
count += 1
|
|
147
|
+
|
|
148
|
+
return count
|
|
149
|
+
|
|
150
|
+
def _count_schemas(self, spec: Dict) -> int:
|
|
151
|
+
"""Count number of schemas in components."""
|
|
152
|
+
components = spec.get('components', {})
|
|
153
|
+
schemas = components.get('schemas', {})
|
|
154
|
+
return len(schemas)
|
|
155
|
+
|
|
156
|
+
def _count_parameters(self, spec: Dict) -> int:
|
|
157
|
+
"""Count total parameters across all endpoints."""
|
|
158
|
+
count = 0
|
|
159
|
+
paths = spec.get('paths', {})
|
|
160
|
+
|
|
161
|
+
for path, path_item in paths.items():
|
|
162
|
+
if isinstance(path_item, dict):
|
|
163
|
+
# Path-level parameters
|
|
164
|
+
count += len(path_item.get('parameters', []))
|
|
165
|
+
|
|
166
|
+
# Method-level parameters
|
|
167
|
+
http_methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']
|
|
168
|
+
for method in http_methods:
|
|
169
|
+
if method in path_item:
|
|
170
|
+
operation = path_item[method]
|
|
171
|
+
if isinstance(operation, dict):
|
|
172
|
+
count += len(operation.get('parameters', []))
|
|
173
|
+
|
|
174
|
+
# Component parameters
|
|
175
|
+
components = spec.get('components', {})
|
|
176
|
+
count += len(components.get('parameters', {}))
|
|
177
|
+
|
|
178
|
+
return count
|
|
179
|
+
|
|
180
|
+
def _count_response_variants(self, spec: Dict) -> int:
|
|
181
|
+
"""Count unique response status codes across API."""
|
|
182
|
+
status_codes = set()
|
|
183
|
+
paths = spec.get('paths', {})
|
|
184
|
+
|
|
185
|
+
for path, path_item in paths.items():
|
|
186
|
+
if isinstance(path_item, dict):
|
|
187
|
+
http_methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']
|
|
188
|
+
for method in http_methods:
|
|
189
|
+
if method in path_item:
|
|
190
|
+
operation = path_item[method]
|
|
191
|
+
if isinstance(operation, dict):
|
|
192
|
+
responses = operation.get('responses', {})
|
|
193
|
+
status_codes.update(responses.keys())
|
|
194
|
+
|
|
195
|
+
return len(status_codes)
|
|
196
|
+
|
|
197
|
+
def _calculate_max_schema_depth(self, spec: Dict) -> int:
|
|
198
|
+
"""Calculate maximum nested depth of JSON schema objects."""
|
|
199
|
+
max_depth = 0
|
|
200
|
+
components = spec.get('components', {})
|
|
201
|
+
schemas = components.get('schemas', {})
|
|
202
|
+
|
|
203
|
+
for schema_name, schema in schemas.items():
|
|
204
|
+
if isinstance(schema, dict):
|
|
205
|
+
depth = self._get_schema_depth(schema, schemas)
|
|
206
|
+
max_depth = max(max_depth, depth)
|
|
207
|
+
|
|
208
|
+
return max_depth
|
|
209
|
+
|
|
210
|
+
def _get_schema_depth(self, schema: Dict, all_schemas: Dict, visited: Optional[set] = None) -> int:
|
|
211
|
+
"""Recursively calculate schema depth."""
|
|
212
|
+
if visited is None:
|
|
213
|
+
visited = set()
|
|
214
|
+
|
|
215
|
+
# Prevent infinite recursion
|
|
216
|
+
schema_id = id(schema)
|
|
217
|
+
if schema_id in visited:
|
|
218
|
+
return 0
|
|
219
|
+
visited.add(schema_id)
|
|
220
|
+
|
|
221
|
+
depth = 1
|
|
222
|
+
|
|
223
|
+
# Check properties
|
|
224
|
+
if 'properties' in schema:
|
|
225
|
+
for prop_name, prop_schema in schema['properties'].items():
|
|
226
|
+
if isinstance(prop_schema, dict):
|
|
227
|
+
# Handle $ref
|
|
228
|
+
if '$ref' in prop_schema:
|
|
229
|
+
ref_name = prop_schema['$ref'].split('/')[-1]
|
|
230
|
+
if ref_name in all_schemas:
|
|
231
|
+
prop_depth = 1 + self._get_schema_depth(all_schemas[ref_name], all_schemas, visited)
|
|
232
|
+
depth = max(depth, prop_depth)
|
|
233
|
+
# Handle nested object
|
|
234
|
+
elif prop_schema.get('type') == 'object':
|
|
235
|
+
prop_depth = 1 + self._get_schema_depth(prop_schema, all_schemas, visited)
|
|
236
|
+
depth = max(depth, prop_depth)
|
|
237
|
+
# Handle array of objects
|
|
238
|
+
elif prop_schema.get('type') == 'array':
|
|
239
|
+
items = prop_schema.get('items', {})
|
|
240
|
+
if isinstance(items, dict):
|
|
241
|
+
if '$ref' in items:
|
|
242
|
+
ref_name = items['$ref'].split('/')[-1]
|
|
243
|
+
if ref_name in all_schemas:
|
|
244
|
+
prop_depth = 1 + self._get_schema_depth(all_schemas[ref_name], all_schemas, visited)
|
|
245
|
+
depth = max(depth, prop_depth)
|
|
246
|
+
elif items.get('type') == 'object':
|
|
247
|
+
prop_depth = 1 + self._get_schema_depth(items, all_schemas, visited)
|
|
248
|
+
depth = max(depth, prop_depth)
|
|
249
|
+
|
|
250
|
+
# Check allOf, oneOf, anyOf
|
|
251
|
+
for composition_key in ['allOf', 'oneOf', 'anyOf']:
|
|
252
|
+
if composition_key in schema:
|
|
253
|
+
for sub_schema in schema[composition_key]:
|
|
254
|
+
if isinstance(sub_schema, dict):
|
|
255
|
+
if '$ref' in sub_schema:
|
|
256
|
+
ref_name = sub_schema['$ref'].split('/')[-1]
|
|
257
|
+
if ref_name in all_schemas:
|
|
258
|
+
sub_depth = self._get_schema_depth(all_schemas[ref_name], all_schemas, visited)
|
|
259
|
+
depth = max(depth, sub_depth)
|
|
260
|
+
else:
|
|
261
|
+
sub_depth = self._get_schema_depth(sub_schema, all_schemas, visited)
|
|
262
|
+
depth = max(depth, sub_depth)
|
|
263
|
+
|
|
264
|
+
return depth
|
|
265
|
+
|
|
266
|
+
def _count_security_schemes(self, spec: Dict) -> int:
|
|
267
|
+
"""Count number of defined authentication methods."""
|
|
268
|
+
components = spec.get('components', {})
|
|
269
|
+
security_schemes = components.get('securitySchemes', {})
|
|
270
|
+
return len(security_schemes)
|
|
271
|
+
|
|
272
|
+
def _count_examples(self, spec: Dict) -> int:
|
|
273
|
+
"""Count number of request/response examples."""
|
|
274
|
+
count = 0
|
|
275
|
+
paths = spec.get('paths', {})
|
|
276
|
+
|
|
277
|
+
for path, path_item in paths.items():
|
|
278
|
+
if isinstance(path_item, dict):
|
|
279
|
+
http_methods = ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace']
|
|
280
|
+
for method in http_methods:
|
|
281
|
+
if method in path_item:
|
|
282
|
+
operation = path_item[method]
|
|
283
|
+
if isinstance(operation, dict):
|
|
284
|
+
# Request examples
|
|
285
|
+
request_body = operation.get('requestBody', {})
|
|
286
|
+
if isinstance(request_body, dict):
|
|
287
|
+
content = request_body.get('content', {})
|
|
288
|
+
for media_type, media_obj in content.items():
|
|
289
|
+
if isinstance(media_obj, dict):
|
|
290
|
+
if 'example' in media_obj:
|
|
291
|
+
count += 1
|
|
292
|
+
if 'examples' in media_obj:
|
|
293
|
+
count += len(media_obj['examples'])
|
|
294
|
+
|
|
295
|
+
# Response examples
|
|
296
|
+
responses = operation.get('responses', {})
|
|
297
|
+
for status_code, response in responses.items():
|
|
298
|
+
if isinstance(response, dict):
|
|
299
|
+
content = response.get('content', {})
|
|
300
|
+
for media_type, media_obj in content.items():
|
|
301
|
+
if isinstance(media_obj, dict):
|
|
302
|
+
if 'example' in media_obj:
|
|
303
|
+
count += 1
|
|
304
|
+
if 'examples' in media_obj:
|
|
305
|
+
count += len(media_obj['examples'])
|
|
306
|
+
|
|
307
|
+
# Component examples
|
|
308
|
+
components = spec.get('components', {})
|
|
309
|
+
examples = components.get('examples', {})
|
|
310
|
+
count += len(examples)
|
|
311
|
+
|
|
312
|
+
return count
|
|
313
|
+
|
|
314
|
+
def _calculate_score(self, metrics: Dict[str, int]) -> int:
|
|
315
|
+
"""Calculate complexity score based on metrics."""
|
|
316
|
+
total_score = 0
|
|
317
|
+
|
|
318
|
+
for metric_name, metric_value in metrics.items():
|
|
319
|
+
if metric_name in self.scoring_rules:
|
|
320
|
+
rules = self.scoring_rules[metric_name]
|
|
321
|
+
for threshold, points in rules:
|
|
322
|
+
if metric_value <= threshold:
|
|
323
|
+
total_score += points
|
|
324
|
+
break
|
|
325
|
+
|
|
326
|
+
# Cap at 100
|
|
327
|
+
return min(total_score, 100)
|
|
328
|
+
|
|
329
|
+
def _classify_complexity(self, score: int) -> str:
|
|
330
|
+
"""Classify API complexity based on score."""
|
|
331
|
+
for threshold, classification in self.classification_thresholds:
|
|
332
|
+
if score <= threshold:
|
|
333
|
+
return classification
|
|
334
|
+
return "Enterprise-scale API"
|
|
335
|
+
|
|
336
|
+
def format_ci_output(self, analysis: Dict[str, Any]) -> Optional[str]:
|
|
337
|
+
"""
|
|
338
|
+
Format analysis output for CI logs.
|
|
339
|
+
Only shows output for complex APIs (score >= 50).
|
|
340
|
+
"""
|
|
341
|
+
if analysis['score'] < 50:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
metrics = analysis['metrics']
|
|
345
|
+
|
|
346
|
+
output = []
|
|
347
|
+
output.append("-" * 50)
|
|
348
|
+
output.append("DELIMIT COMPLEXITY ANALYSIS")
|
|
349
|
+
output.append("-" * 50)
|
|
350
|
+
output.append("")
|
|
351
|
+
output.append(f"Complexity Score: {analysis['score']} / 100")
|
|
352
|
+
output.append(f"Classification: {analysis['classification'].upper()}")
|
|
353
|
+
output.append("")
|
|
354
|
+
output.append("Detected:")
|
|
355
|
+
output.append(f"• {metrics['endpoint_count']} endpoints")
|
|
356
|
+
output.append(f"• {metrics['schema_count']} schemas")
|
|
357
|
+
output.append(f"• {metrics['parameter_count']} parameters")
|
|
358
|
+
output.append(f"• Nested schema depth: {metrics['nested_schema_depth']}")
|
|
359
|
+
output.append("")
|
|
360
|
+
output.append("As your API grows, managing change impact becomes harder.")
|
|
361
|
+
output.append("")
|
|
362
|
+
output.append("Consider enabling the Delimit Governance Dashboard:")
|
|
363
|
+
output.append("https://delimit.ai")
|
|
364
|
+
output.append("")
|
|
365
|
+
output.append("This provides:")
|
|
366
|
+
output.append("• Cross-service impact analysis")
|
|
367
|
+
output.append("• Governance policy visualization")
|
|
368
|
+
output.append("• Audit logs for API evolution")
|
|
369
|
+
output.append("")
|
|
370
|
+
output.append("-" * 50)
|
|
371
|
+
|
|
372
|
+
return "\n".join(output)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def analyze_openapi_complexity(spec: Union[str, Dict, Path]) -> Dict[str, Any]:
|
|
376
|
+
"""
|
|
377
|
+
Main entry point for complexity analysis.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
spec: OpenAPI specification as string, dict, or file path
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Dictionary with score, classification, and metrics
|
|
384
|
+
"""
|
|
385
|
+
analyzer = ComplexityAnalyzer()
|
|
386
|
+
return analyzer.analyze_openapi_complexity(spec)
|