delimit-cli 2.4.0 → 3.0.1

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 (112) hide show
  1. package/.dockerignore +7 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/CODE_OF_CONDUCT.md +48 -0
  4. package/CONTRIBUTING.md +67 -0
  5. package/Dockerfile +9 -0
  6. package/LICENSE +21 -0
  7. package/README.md +18 -69
  8. package/SECURITY.md +42 -0
  9. package/adapters/gemini-forge.js +11 -0
  10. package/adapters/gemini-jamsons.js +152 -0
  11. package/bin/delimit-cli.js +8 -0
  12. package/bin/delimit-setup.js +258 -0
  13. package/gateway/ai/backends/__init__.py +0 -0
  14. package/gateway/ai/backends/async_utils.py +21 -0
  15. package/gateway/ai/backends/deploy_bridge.py +150 -0
  16. package/gateway/ai/backends/gateway_core.py +261 -0
  17. package/gateway/ai/backends/generate_bridge.py +38 -0
  18. package/gateway/ai/backends/governance_bridge.py +196 -0
  19. package/gateway/ai/backends/intel_bridge.py +59 -0
  20. package/gateway/ai/backends/memory_bridge.py +93 -0
  21. package/gateway/ai/backends/ops_bridge.py +137 -0
  22. package/gateway/ai/backends/os_bridge.py +82 -0
  23. package/gateway/ai/backends/repo_bridge.py +117 -0
  24. package/gateway/ai/backends/ui_bridge.py +118 -0
  25. package/gateway/ai/backends/vault_bridge.py +129 -0
  26. package/gateway/ai/server.py +1182 -0
  27. package/gateway/core/__init__.py +3 -0
  28. package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
  29. package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
  30. package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
  31. package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
  32. package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
  33. package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
  34. package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
  35. package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
  36. package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
  37. package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
  38. package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
  39. package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
  40. package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
  41. package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
  42. package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
  43. package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
  44. package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
  45. package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
  46. package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
  47. package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
  48. package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
  49. package/gateway/core/auto_baseline.py +304 -0
  50. package/gateway/core/ci_formatter.py +283 -0
  51. package/gateway/core/complexity_analyzer.py +386 -0
  52. package/gateway/core/contract_ledger.py +345 -0
  53. package/gateway/core/dependency_graph.py +218 -0
  54. package/gateway/core/dependency_manifest.py +223 -0
  55. package/gateway/core/diff_engine_v2.py +477 -0
  56. package/gateway/core/diff_engine_v2.py.bak +426 -0
  57. package/gateway/core/event_backbone.py +268 -0
  58. package/gateway/core/event_schema.py +258 -0
  59. package/gateway/core/explainer.py +438 -0
  60. package/gateway/core/gateway.py +128 -0
  61. package/gateway/core/gateway_v2.py +154 -0
  62. package/gateway/core/gateway_v3.py +224 -0
  63. package/gateway/core/impact_analyzer.py +163 -0
  64. package/gateway/core/policies/default.yml +13 -0
  65. package/gateway/core/policies/relaxed.yml +48 -0
  66. package/gateway/core/policies/strict.yml +55 -0
  67. package/gateway/core/policy_engine.py +464 -0
  68. package/gateway/core/registry.py +52 -0
  69. package/gateway/core/registry_v2.py +132 -0
  70. package/gateway/core/registry_v3.py +134 -0
  71. package/gateway/core/semver_classifier.py +152 -0
  72. package/gateway/core/spec_detector.py +130 -0
  73. package/gateway/core/surface_bridge.py +307 -0
  74. package/gateway/core/zero_spec/__init__.py +4 -0
  75. package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
  76. package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
  77. package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
  78. package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
  79. package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
  80. package/gateway/core/zero_spec/detector.py +353 -0
  81. package/gateway/core/zero_spec/express_extractor.py +483 -0
  82. package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
  83. package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
  84. package/gateway/tasks/__init__.py +1 -0
  85. package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
  86. package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
  87. package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
  88. package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
  89. package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
  90. package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
  91. package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
  92. package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
  93. package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
  94. package/gateway/tasks/check_policy.py +177 -0
  95. package/gateway/tasks/check_policy_v2.py +255 -0
  96. package/gateway/tasks/check_policy_v3.py +255 -0
  97. package/gateway/tasks/explain_diff.py +305 -0
  98. package/gateway/tasks/explain_diff_v2.py +267 -0
  99. package/gateway/tasks/validate_api.py +131 -0
  100. package/gateway/tasks/validate_api_v2.py +208 -0
  101. package/gateway/tasks/validate_api_v3.py +163 -0
  102. package/package.json +4 -3
  103. package/adapters/codex-skill.js +0 -87
  104. package/adapters/cursor-extension.js +0 -190
  105. package/adapters/gemini-action.js +0 -93
  106. package/adapters/openai-function.js +0 -112
  107. package/adapters/xai-plugin.js +0 -151
  108. package/test-decision-engine.js +0 -181
  109. package/test-hook.js +0 -27
  110. package/tests/cli.test.js +0 -359
  111. package/tests/fixtures/openapi-changed.yaml +0 -56
  112. 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)