delimit-cli 2.3.2 → 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.
Files changed (113) hide show
  1. package/.dockerignore +7 -0
  2. package/.github/workflows/ci.yml +22 -0
  3. package/CHANGELOG.md +33 -0
  4. package/CODE_OF_CONDUCT.md +48 -0
  5. package/CONTRIBUTING.md +67 -0
  6. package/Dockerfile +9 -0
  7. package/LICENSE +21 -0
  8. package/README.md +51 -130
  9. package/SECURITY.md +42 -0
  10. package/adapters/codex-forge.js +107 -0
  11. package/adapters/codex-jamsons.js +142 -0
  12. package/adapters/codex-security.js +94 -0
  13. package/adapters/gemini-forge.js +120 -0
  14. package/adapters/gemini-jamsons.js +152 -0
  15. package/bin/delimit-cli.js +52 -2
  16. package/bin/delimit-setup.js +258 -0
  17. package/gateway/ai/backends/__init__.py +0 -0
  18. package/gateway/ai/backends/async_utils.py +21 -0
  19. package/gateway/ai/backends/deploy_bridge.py +150 -0
  20. package/gateway/ai/backends/gateway_core.py +261 -0
  21. package/gateway/ai/backends/generate_bridge.py +38 -0
  22. package/gateway/ai/backends/governance_bridge.py +196 -0
  23. package/gateway/ai/backends/intel_bridge.py +59 -0
  24. package/gateway/ai/backends/memory_bridge.py +93 -0
  25. package/gateway/ai/backends/ops_bridge.py +137 -0
  26. package/gateway/ai/backends/os_bridge.py +82 -0
  27. package/gateway/ai/backends/repo_bridge.py +117 -0
  28. package/gateway/ai/backends/ui_bridge.py +118 -0
  29. package/gateway/ai/backends/vault_bridge.py +129 -0
  30. package/gateway/ai/server.py +1182 -0
  31. package/gateway/core/__init__.py +3 -0
  32. package/gateway/core/__pycache__/__init__.cpython-310.pyc +0 -0
  33. package/gateway/core/__pycache__/auto_baseline.cpython-310.pyc +0 -0
  34. package/gateway/core/__pycache__/ci_formatter.cpython-310.pyc +0 -0
  35. package/gateway/core/__pycache__/contract_ledger.cpython-310.pyc +0 -0
  36. package/gateway/core/__pycache__/dependency_graph.cpython-310.pyc +0 -0
  37. package/gateway/core/__pycache__/dependency_manifest.cpython-310.pyc +0 -0
  38. package/gateway/core/__pycache__/diff_engine_v2.cpython-310.pyc +0 -0
  39. package/gateway/core/__pycache__/event_backbone.cpython-310.pyc +0 -0
  40. package/gateway/core/__pycache__/event_schema.cpython-310.pyc +0 -0
  41. package/gateway/core/__pycache__/explainer.cpython-310.pyc +0 -0
  42. package/gateway/core/__pycache__/gateway.cpython-310.pyc +0 -0
  43. package/gateway/core/__pycache__/gateway_v2.cpython-310.pyc +0 -0
  44. package/gateway/core/__pycache__/gateway_v3.cpython-310.pyc +0 -0
  45. package/gateway/core/__pycache__/impact_analyzer.cpython-310.pyc +0 -0
  46. package/gateway/core/__pycache__/policy_engine.cpython-310.pyc +0 -0
  47. package/gateway/core/__pycache__/registry.cpython-310.pyc +0 -0
  48. package/gateway/core/__pycache__/registry_v2.cpython-310.pyc +0 -0
  49. package/gateway/core/__pycache__/registry_v3.cpython-310.pyc +0 -0
  50. package/gateway/core/__pycache__/semver_classifier.cpython-310.pyc +0 -0
  51. package/gateway/core/__pycache__/spec_detector.cpython-310.pyc +0 -0
  52. package/gateway/core/__pycache__/surface_bridge.cpython-310.pyc +0 -0
  53. package/gateway/core/auto_baseline.py +304 -0
  54. package/gateway/core/ci_formatter.py +283 -0
  55. package/gateway/core/complexity_analyzer.py +386 -0
  56. package/gateway/core/contract_ledger.py +345 -0
  57. package/gateway/core/dependency_graph.py +218 -0
  58. package/gateway/core/dependency_manifest.py +223 -0
  59. package/gateway/core/diff_engine_v2.py +477 -0
  60. package/gateway/core/diff_engine_v2.py.bak +426 -0
  61. package/gateway/core/event_backbone.py +268 -0
  62. package/gateway/core/event_schema.py +258 -0
  63. package/gateway/core/explainer.py +438 -0
  64. package/gateway/core/gateway.py +128 -0
  65. package/gateway/core/gateway_v2.py +154 -0
  66. package/gateway/core/gateway_v3.py +224 -0
  67. package/gateway/core/impact_analyzer.py +163 -0
  68. package/gateway/core/policies/default.yml +13 -0
  69. package/gateway/core/policies/relaxed.yml +48 -0
  70. package/gateway/core/policies/strict.yml +55 -0
  71. package/gateway/core/policy_engine.py +464 -0
  72. package/gateway/core/registry.py +52 -0
  73. package/gateway/core/registry_v2.py +132 -0
  74. package/gateway/core/registry_v3.py +134 -0
  75. package/gateway/core/semver_classifier.py +152 -0
  76. package/gateway/core/spec_detector.py +130 -0
  77. package/gateway/core/surface_bridge.py +307 -0
  78. package/gateway/core/zero_spec/__init__.py +4 -0
  79. package/gateway/core/zero_spec/__pycache__/__init__.cpython-310.pyc +0 -0
  80. package/gateway/core/zero_spec/__pycache__/detector.cpython-310.pyc +0 -0
  81. package/gateway/core/zero_spec/__pycache__/express_extractor.cpython-310.pyc +0 -0
  82. package/gateway/core/zero_spec/__pycache__/fastapi_extractor.cpython-310.pyc +0 -0
  83. package/gateway/core/zero_spec/__pycache__/nestjs_extractor.cpython-310.pyc +0 -0
  84. package/gateway/core/zero_spec/detector.py +353 -0
  85. package/gateway/core/zero_spec/express_extractor.py +483 -0
  86. package/gateway/core/zero_spec/fastapi_extractor.py +254 -0
  87. package/gateway/core/zero_spec/nestjs_extractor.py +369 -0
  88. package/gateway/tasks/__init__.py +1 -0
  89. package/gateway/tasks/__pycache__/__init__.cpython-310.pyc +0 -0
  90. package/gateway/tasks/__pycache__/check_policy.cpython-310.pyc +0 -0
  91. package/gateway/tasks/__pycache__/check_policy_v2.cpython-310.pyc +0 -0
  92. package/gateway/tasks/__pycache__/check_policy_v3.cpython-310.pyc +0 -0
  93. package/gateway/tasks/__pycache__/explain_diff.cpython-310.pyc +0 -0
  94. package/gateway/tasks/__pycache__/explain_diff_v2.cpython-310.pyc +0 -0
  95. package/gateway/tasks/__pycache__/validate_api.cpython-310.pyc +0 -0
  96. package/gateway/tasks/__pycache__/validate_api_v2.cpython-310.pyc +0 -0
  97. package/gateway/tasks/__pycache__/validate_api_v3.cpython-310.pyc +0 -0
  98. package/gateway/tasks/check_policy.py +177 -0
  99. package/gateway/tasks/check_policy_v2.py +255 -0
  100. package/gateway/tasks/check_policy_v3.py +255 -0
  101. package/gateway/tasks/explain_diff.py +305 -0
  102. package/gateway/tasks/explain_diff_v2.py +267 -0
  103. package/gateway/tasks/validate_api.py +131 -0
  104. package/gateway/tasks/validate_api_v2.py +208 -0
  105. package/gateway/tasks/validate_api_v3.py +163 -0
  106. package/package.json +3 -3
  107. package/adapters/codex-skill.js +0 -87
  108. package/adapters/cursor-extension.js +0 -190
  109. package/adapters/gemini-action.js +0 -93
  110. package/adapters/openai-function.js +0 -112
  111. package/adapters/xai-plugin.js +0 -151
  112. package/test-decision-engine.js +0 -181
  113. package/test-hook.js +0 -27
@@ -0,0 +1,48 @@
1
+ # Delimit Policy Preset: relaxed
2
+ # For internal APIs, early-stage startups, and rapid iteration.
3
+ # Only blocks the most destructive changes. Everything else is a warning.
4
+
5
+ override_defaults: true
6
+
7
+ rules:
8
+ - id: no_endpoint_removal
9
+ name: Warn on Endpoint Removal
10
+ change_types: [endpoint_removed]
11
+ severity: warning
12
+ action: warn
13
+ message: "Endpoint {path} was removed. Make sure no consumers depend on it."
14
+
15
+ - id: no_method_removal
16
+ name: Warn on Method Removal
17
+ change_types: [method_removed]
18
+ severity: warning
19
+ action: warn
20
+ message: "HTTP method removed from {path}. Check downstream consumers."
21
+
22
+ - id: warn_required_param
23
+ name: Warn on Required Parameter Addition
24
+ change_types: [required_param_added]
25
+ severity: warning
26
+ action: warn
27
+ message: "New required parameter at {path}. Existing clients will need to update."
28
+
29
+ - id: warn_type_change
30
+ name: Warn on Type Changes
31
+ change_types: [type_changed]
32
+ severity: warning
33
+ action: warn
34
+ message: "Type changed at {path}. Verify client compatibility."
35
+
36
+ - id: allow_field_removal
37
+ name: Allow Field Removal (warn)
38
+ change_types: [field_removed]
39
+ severity: info
40
+ action: allow
41
+ message: "Field removed from {path}."
42
+
43
+ - id: allow_enum_changes
44
+ name: Allow Enum Changes
45
+ change_types: [enum_value_removed]
46
+ severity: info
47
+ action: allow
48
+ message: "Enum value removed at {path}."
@@ -0,0 +1,55 @@
1
+ # Delimit Policy Preset: strict
2
+ # For public APIs, payment systems, and regulated environments.
3
+ # Zero tolerance for breaking changes. All breaking changes are errors.
4
+
5
+ override_defaults: true
6
+
7
+ rules:
8
+ - id: no_endpoint_removal
9
+ name: Forbid Endpoint Removal
10
+ change_types: [endpoint_removed]
11
+ severity: error
12
+ action: forbid
13
+ message: "Endpoint {path} cannot be removed. Deprecate with Sunset header first."
14
+
15
+ - id: no_method_removal
16
+ name: Forbid Method Removal
17
+ change_types: [method_removed]
18
+ severity: error
19
+ action: forbid
20
+ message: "HTTP method removed from {path}. This breaks all clients using this method."
21
+
22
+ - id: no_required_param_addition
23
+ name: Forbid Required Parameter Addition
24
+ change_types: [required_param_added]
25
+ severity: error
26
+ action: forbid
27
+ message: "Cannot add required parameter to {path}. Make it optional with a default."
28
+
29
+ - id: no_field_removal
30
+ name: Forbid Response Field Removal
31
+ change_types: [field_removed]
32
+ severity: error
33
+ action: forbid
34
+ message: "Cannot remove field from {path}. Deprecate it first."
35
+
36
+ - id: no_type_change
37
+ name: Forbid Type Changes
38
+ change_types: [type_changed]
39
+ severity: error
40
+ action: forbid
41
+ message: "Type change at {path} breaks client deserialization."
42
+
43
+ - id: no_enum_removal
44
+ name: Forbid Enum Value Removal
45
+ change_types: [enum_value_removed]
46
+ severity: error
47
+ action: forbid
48
+ message: "Enum value removed at {path}. Clients may be sending this value."
49
+
50
+ - id: no_param_removal
51
+ name: Forbid Parameter Removal
52
+ change_types: [param_removed]
53
+ severity: error
54
+ action: forbid
55
+ message: "Parameter removed from {path}. Clients may depend on this parameter."
@@ -0,0 +1,464 @@
1
+ """
2
+ Delimit Policy Engine - Define and enforce custom governance rules.
3
+ Organizations can define policies to control API evolution.
4
+ """
5
+
6
+ import yaml
7
+ from typing import Dict, List, Any, Optional
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from pathlib import Path
11
+
12
+ from core.diff_engine_v2 import OpenAPIDiffEngine, Change, ChangeType
13
+
14
+ class RuleSeverity(Enum):
15
+ ERROR = "error" # Fails CI
16
+ WARNING = "warning" # Shows warning but passes
17
+ INFO = "info" # Informational only
18
+
19
+ class RuleAction(Enum):
20
+ FORBID = "forbid" # Forbids the change
21
+ ALLOW = "allow" # Explicitly allows
22
+ WARN = "warn" # Warns but allows
23
+
24
+ @dataclass
25
+ class PolicyRule:
26
+ """A single governance rule."""
27
+ id: str
28
+ name: str
29
+ description: str
30
+ change_types: List[ChangeType]
31
+ severity: RuleSeverity
32
+ action: RuleAction
33
+ conditions: Optional[Dict] = None
34
+ message_template: Optional[str] = None
35
+
36
+ def evaluate(self, change: Change) -> Optional['Violation']:
37
+ """Evaluate if this rule applies to a change."""
38
+ if change.type not in self.change_types:
39
+ return None
40
+
41
+ # Check additional conditions if specified
42
+ if self.conditions:
43
+ if not self._check_conditions(change, self.conditions):
44
+ return None
45
+
46
+ # Create violation if rule matches
47
+ if self.action == RuleAction.FORBID:
48
+ return Violation(
49
+ rule_id=self.id,
50
+ rule_name=self.name,
51
+ severity=self.severity.value,
52
+ message=self._format_message(change),
53
+ change=change
54
+ )
55
+ elif self.action == RuleAction.WARN:
56
+ return Violation(
57
+ rule_id=self.id,
58
+ rule_name=self.name,
59
+ severity=RuleSeverity.WARNING.value,
60
+ message=self._format_message(change),
61
+ change=change
62
+ )
63
+
64
+ return None
65
+
66
+ def _check_conditions(self, change: Change, conditions: Dict) -> bool:
67
+ """Check if change meets additional conditions."""
68
+ # Example conditions: path patterns, specific fields, etc.
69
+ if "path_pattern" in conditions:
70
+ import re
71
+ pattern = conditions["path_pattern"]
72
+ if not re.match(pattern, change.path):
73
+ return False
74
+
75
+ if "exclude_paths" in conditions:
76
+ for excluded in conditions["exclude_paths"]:
77
+ if excluded in change.path:
78
+ return False
79
+
80
+ return True
81
+
82
+ def _format_message(self, change: Change) -> str:
83
+ """Format violation message."""
84
+ if self.message_template:
85
+ return self.message_template.format(
86
+ path=change.path,
87
+ details=change.details,
88
+ message=change.message
89
+ )
90
+ return f"{self.name}: {change.message}"
91
+
92
+ @dataclass
93
+ class Violation:
94
+ """A policy violation."""
95
+ rule_id: str
96
+ rule_name: str
97
+ severity: str
98
+ message: str
99
+ change: Change
100
+
101
+ # Available policy presets
102
+ POLICY_PRESETS = ("strict", "default", "relaxed")
103
+ _PRESETS_DIR = Path(__file__).resolve().parent / "policies"
104
+
105
+
106
+ class PolicyEngine:
107
+ """Main policy engine for evaluating governance rules."""
108
+
109
+ # Default rules that are always active unless overridden
110
+ DEFAULT_RULES = [
111
+ PolicyRule(
112
+ id="no_endpoint_removal",
113
+ name="Forbid Endpoint Removal",
114
+ description="Endpoints cannot be removed without deprecation",
115
+ change_types=[ChangeType.ENDPOINT_REMOVED],
116
+ severity=RuleSeverity.ERROR,
117
+ action=RuleAction.FORBID,
118
+ message_template="Endpoint {path} cannot be removed. Deprecate it first."
119
+ ),
120
+ PolicyRule(
121
+ id="no_method_removal",
122
+ name="Forbid Method Removal",
123
+ description="HTTP methods cannot be removed from endpoints",
124
+ change_types=[ChangeType.METHOD_REMOVED],
125
+ severity=RuleSeverity.ERROR,
126
+ action=RuleAction.FORBID,
127
+ message_template="Method {details[method]} cannot be removed from {details[endpoint]}"
128
+ ),
129
+ PolicyRule(
130
+ id="no_required_param_addition",
131
+ name="Forbid Required Parameter Addition",
132
+ description="New required parameters break existing clients",
133
+ change_types=[ChangeType.REQUIRED_PARAM_ADDED],
134
+ severity=RuleSeverity.ERROR,
135
+ action=RuleAction.FORBID,
136
+ message_template="Cannot add required parameter {details[parameter]} to {path}"
137
+ ),
138
+ PolicyRule(
139
+ id="no_response_field_removal",
140
+ name="Forbid Response Field Removal",
141
+ description="Removing response fields breaks clients",
142
+ change_types=[ChangeType.FIELD_REMOVED],
143
+ severity=RuleSeverity.ERROR,
144
+ action=RuleAction.FORBID,
145
+ conditions={"path_pattern": ".*:2\\d\\d.*"}, # Only 2xx responses
146
+ message_template="Cannot remove field {details[field]} from response"
147
+ ),
148
+ PolicyRule(
149
+ id="warn_type_change",
150
+ name="Warn on Type Changes",
151
+ description="Type changes may break clients",
152
+ change_types=[ChangeType.TYPE_CHANGED],
153
+ severity=RuleSeverity.WARNING,
154
+ action=RuleAction.WARN,
155
+ message_template="Type changed from {details[old_type]} to {details[new_type]} at {path}"
156
+ ),
157
+ PolicyRule(
158
+ id="allow_enum_expansion",
159
+ name="Allow Enum Expansion",
160
+ description="Adding enum values is safe",
161
+ change_types=[ChangeType.ENUM_VALUE_ADDED],
162
+ severity=RuleSeverity.INFO,
163
+ action=RuleAction.ALLOW,
164
+ message_template="Enum value {details[value]} added (non-breaking)"
165
+ )
166
+ ]
167
+
168
+ def __init__(self, policy_file: Optional[str] = None):
169
+ """Initialize policy engine with optional custom policy file or preset name.
170
+
171
+ policy_file can be:
172
+ - A file path to a YAML policy file
173
+ - A preset name: "strict", "default", or "relaxed"
174
+ """
175
+ self.rules: List[PolicyRule] = []
176
+ self.custom_rules: List[PolicyRule] = []
177
+
178
+ # Load default rules
179
+ self.rules.extend(self.DEFAULT_RULES)
180
+
181
+ # Load custom policy if provided
182
+ if policy_file:
183
+ # Check if it's a preset name
184
+ if policy_file in POLICY_PRESETS:
185
+ preset_path = _PRESETS_DIR / f"{policy_file}.yml"
186
+ if preset_path.exists():
187
+ self.load_policy(str(preset_path))
188
+ # "default" preset = built-in defaults, no-op
189
+ else:
190
+ self.load_policy(policy_file)
191
+
192
+ def load_policy(self, policy_file: str):
193
+ """Load custom policy from YAML file."""
194
+ path = Path(policy_file)
195
+
196
+ # Check common locations if file not found
197
+ if not path.exists():
198
+ for location in [".delimit/policies.yml", ".delimit/policy.yaml", "delimit.yml"]:
199
+ test_path = Path(location)
200
+ if test_path.exists():
201
+ path = test_path
202
+ break
203
+
204
+ if not path.exists():
205
+ return # No custom policy, use defaults
206
+
207
+ with open(path, 'r') as f:
208
+ config = yaml.safe_load(f)
209
+
210
+ if not config:
211
+ return
212
+
213
+ # Parse rules
214
+ for rule_config in config.get("rules", []):
215
+ rule = self._parse_rule(rule_config)
216
+ if rule:
217
+ self.custom_rules.append(rule)
218
+
219
+ # Override defaults if specified
220
+ if config.get("override_defaults", False):
221
+ self.rules = self.custom_rules
222
+ else:
223
+ # Merge custom rules with defaults
224
+ # Custom rules take precedence for same IDs
225
+ custom_ids = {r.id for r in self.custom_rules}
226
+ self.rules = self.custom_rules + [r for r in self.DEFAULT_RULES if r.id not in custom_ids]
227
+
228
+ def _parse_rule(self, config: Dict) -> Optional[PolicyRule]:
229
+ """Parse a rule from configuration."""
230
+ try:
231
+ # Map string change types to enum
232
+ change_type_map = {
233
+ "endpoint_removed": ChangeType.ENDPOINT_REMOVED,
234
+ "method_removed": ChangeType.METHOD_REMOVED,
235
+ "required_param_added": ChangeType.REQUIRED_PARAM_ADDED,
236
+ "param_removed": ChangeType.PARAM_REMOVED,
237
+ "field_removed": ChangeType.FIELD_REMOVED,
238
+ "type_changed": ChangeType.TYPE_CHANGED,
239
+ "enum_value_removed": ChangeType.ENUM_VALUE_REMOVED,
240
+ }
241
+
242
+ change_types = []
243
+ for ct in config.get("change_types", []):
244
+ if ct in change_type_map:
245
+ change_types.append(change_type_map[ct])
246
+
247
+ if not change_types:
248
+ return None
249
+
250
+ return PolicyRule(
251
+ id=config["id"],
252
+ name=config.get("name", config["id"]),
253
+ description=config.get("description", ""),
254
+ change_types=change_types,
255
+ severity=RuleSeverity(config.get("severity", "error")),
256
+ action=RuleAction(config.get("action", "forbid")),
257
+ conditions=config.get("conditions"),
258
+ message_template=config.get("message")
259
+ )
260
+ except Exception as e:
261
+ print(f"Warning: Failed to parse rule {config.get('id', 'unknown')}: {e}")
262
+ return None
263
+
264
+ def evaluate(self, changes: List[Change]) -> List[Violation]:
265
+ """Evaluate all changes against policy rules."""
266
+ violations = []
267
+
268
+ for change in changes:
269
+ for rule in self.rules:
270
+ violation = rule.evaluate(change)
271
+ if violation:
272
+ violations.append(violation)
273
+
274
+ return violations
275
+
276
+ def create_policy_template(self) -> str:
277
+ """Generate a template policy file."""
278
+ template = """# Delimit Policy Configuration
279
+ # Define custom governance rules for your API
280
+
281
+ # Override default rules completely (default: false)
282
+ override_defaults: false
283
+
284
+ # Custom governance rules
285
+ rules:
286
+ # Forbid removing any endpoint
287
+ - id: no_endpoint_removal
288
+ name: Forbid Endpoint Removal
289
+ description: Endpoints cannot be removed without deprecation
290
+ change_types:
291
+ - endpoint_removed
292
+ severity: error # error | warning | info
293
+ action: forbid # forbid | allow | warn
294
+ message: "Endpoint {path} cannot be removed. Use deprecation headers instead."
295
+
296
+ # Forbid type changes in responses
297
+ - id: no_response_type_change
298
+ name: Forbid Response Type Changes
299
+ change_types:
300
+ - type_changed
301
+ severity: error
302
+ action: forbid
303
+ conditions:
304
+ path_pattern: ".*:2\\d\\d.*" # Only 2xx responses
305
+ message: "Type change not allowed in {path}"
306
+
307
+ # Allow adding optional fields
308
+ - id: allow_optional_fields
309
+ name: Allow Optional Field Addition
310
+ change_types:
311
+ - optional_field_added
312
+ severity: info
313
+ action: allow
314
+ message: "Optional field added (safe change)"
315
+
316
+ # Warn about enum changes
317
+ - id: warn_enum_removal
318
+ name: Warn on Enum Value Removal
319
+ change_types:
320
+ - enum_value_removed
321
+ severity: warning
322
+ action: warn
323
+ message: "Enum value removed - may break clients using this value"
324
+
325
+ # Custom rule for specific paths
326
+ - id: protect_v1_api
327
+ name: Protect V1 API
328
+ description: V1 endpoints are frozen
329
+ change_types:
330
+ - endpoint_removed
331
+ - method_removed
332
+ - field_removed
333
+ severity: error
334
+ action: forbid
335
+ conditions:
336
+ path_pattern: "^/v1/.*"
337
+ message: "V1 API is frozen. Changes must be made in V2."
338
+
339
+ # Organization-specific rules
340
+ organization:
341
+ # Require approval for high-risk changes
342
+ require_approval:
343
+ - endpoint_removed
344
+ - method_removed
345
+ - required_param_added
346
+
347
+ # Deprecation policy
348
+ deprecation:
349
+ min_notice_days: 30
350
+ require_sunset_header: true
351
+ """
352
+ return template
353
+
354
+
355
+ def evaluate_with_policy(
356
+ old_spec: Dict,
357
+ new_spec: Dict,
358
+ policy_file: Optional[str] = None,
359
+ include_semver: bool = False,
360
+ current_version: Optional[str] = None,
361
+ api_name: Optional[str] = None,
362
+ ) -> Dict[str, Any]:
363
+ """
364
+ Main entry point for policy evaluation.
365
+
366
+ Args:
367
+ old_spec: Old OpenAPI spec dict.
368
+ new_spec: New OpenAPI spec dict.
369
+ policy_file: Optional custom policy file.
370
+ include_semver: Attach semver classification + migration guide.
371
+ current_version: Current version for next-version computation.
372
+ api_name: API name for explainer context.
373
+
374
+ Returns:
375
+ Dictionary with violations, summary, decision, and optionally
376
+ semver/migration data for PR comment rendering.
377
+ """
378
+ # Run diff engine
379
+ diff_engine = OpenAPIDiffEngine()
380
+ changes = diff_engine.compare(old_spec, new_spec)
381
+
382
+ # Run policy engine
383
+ policy_engine = PolicyEngine(policy_file)
384
+ violations = policy_engine.evaluate(changes)
385
+
386
+ # Determine decision
387
+ has_errors = any(v.severity == "error" for v in violations)
388
+ has_warnings = any(v.severity == "warning" for v in violations)
389
+
390
+ if has_errors:
391
+ decision = "fail"
392
+ exit_code = 1
393
+ elif has_warnings:
394
+ decision = "warn"
395
+ exit_code = 0
396
+ else:
397
+ decision = "pass"
398
+ exit_code = 0
399
+
400
+ result: Dict[str, Any] = {
401
+ "decision": decision,
402
+ "exit_code": exit_code,
403
+ "violations": [
404
+ {
405
+ "rule": v.rule_id,
406
+ "name": v.rule_name,
407
+ "severity": v.severity,
408
+ "message": v.message,
409
+ "path": v.change.path,
410
+ "details": v.change.details
411
+ }
412
+ for v in violations
413
+ ],
414
+ "summary": {
415
+ "total_changes": len(changes),
416
+ "breaking_changes": len([c for c in changes if c.is_breaking]),
417
+ "violations": len(violations),
418
+ "errors": len([v for v in violations if v.severity == "error"]),
419
+ "warnings": len([v for v in violations if v.severity == "warning"])
420
+ },
421
+ "all_changes": [
422
+ {
423
+ "type": c.type.value,
424
+ "path": c.path,
425
+ "message": c.message,
426
+ "is_breaking": c.is_breaking
427
+ }
428
+ for c in changes
429
+ ],
430
+ }
431
+
432
+ # Attach semver + migration for PR comment rendering
433
+ if include_semver and changes:
434
+ try:
435
+ from core.semver_classifier import classify_detailed, bump_version, classify
436
+ from core.explainer import explain
437
+
438
+ detail = classify_detailed(changes)
439
+ semver_data = {
440
+ "bump": detail["bump"],
441
+ "counts": detail["counts"],
442
+ }
443
+ if current_version:
444
+ semver_data["current_version"] = current_version
445
+ semver_data["next_version"] = bump_version(
446
+ current_version, classify(changes)
447
+ )
448
+ result["semver"] = semver_data
449
+
450
+ # Generate migration guide for breaking changes
451
+ if detail["counts"]["breaking"] > 0:
452
+ old_ver = current_version or old_spec.get("info", {}).get("version")
453
+ new_ver = new_spec.get("info", {}).get("version")
454
+ result["migration"] = explain(
455
+ changes,
456
+ template="migration",
457
+ old_version=old_ver,
458
+ new_version=new_ver,
459
+ api_name=api_name,
460
+ )
461
+ except Exception:
462
+ pass # semver/explainer not available — degrade gracefully
463
+
464
+ return result
@@ -0,0 +1,52 @@
1
+ from typing import Callable, Dict, Optional
2
+ from functools import wraps
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class TaskRegistry:
8
+ """Registry for task handlers following Gemini's recommendation"""
9
+
10
+ def __init__(self):
11
+ self._tasks: Dict[str, Callable] = {}
12
+ self._task_metadata: Dict[str, Dict] = {}
13
+
14
+ def register(self, task_name: str, version: str = "v1", **metadata):
15
+ """Decorator to register a task handler"""
16
+ def decorator(func: Callable):
17
+ full_name = f"{task_name}:{version}" if version != "v1" else task_name
18
+
19
+ @wraps(func)
20
+ def wrapper(*args, **kwargs):
21
+ return func(*args, **kwargs)
22
+
23
+ self._tasks[full_name] = wrapper
24
+ self._tasks[task_name] = wrapper # Default version alias
25
+ self._task_metadata[full_name] = {
26
+ "name": task_name,
27
+ "version": version,
28
+ "handler": func.__name__,
29
+ **metadata
30
+ }
31
+
32
+ logger.info(f"Registered task: {full_name}")
33
+ return wrapper
34
+ return decorator
35
+
36
+ def get_handler(self, task_name: str, version: Optional[str] = None) -> Optional[Callable]:
37
+ """Get a task handler by name and optional version"""
38
+ if version:
39
+ full_name = f"{task_name}:{version}"
40
+ return self._tasks.get(full_name)
41
+ return self._tasks.get(task_name)
42
+
43
+ def list_tasks(self) -> list:
44
+ """List all registered tasks"""
45
+ return list(self._tasks.keys())
46
+
47
+ def has_task(self, task_name: str) -> bool:
48
+ """Check if a task is registered"""
49
+ return task_name in self._tasks
50
+
51
+ # Global registry instance
52
+ task_registry = TaskRegistry()