delimit-cli 3.14.28 → 3.14.29
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/gateway/ai/backends/deploy_bridge.py +56 -2
- package/gateway/ai/backends/gateway_core.py +212 -1
- package/gateway/ai/backends/generate_bridge.py +84 -13
- package/gateway/ai/backends/governance_bridge.py +63 -16
- package/gateway/ai/backends/memory_bridge.py +77 -76
- package/gateway/ai/backends/ops_bridge.py +76 -6
- package/gateway/ai/backends/os_bridge.py +23 -3
- package/gateway/ai/backends/repo_bridge.py +156 -17
- package/gateway/ai/backends/tools_design.py +116 -9
- package/gateway/ai/backends/tools_infra.py +200 -72
- package/gateway/ai/backends/tools_real.py +8 -0
- package/gateway/ai/backends/ui_bridge.py +115 -5
- package/gateway/ai/backends/vault_bridge.py +69 -114
- package/gateway/ai/content_engine.py +1276 -0
- package/gateway/ai/context_fs.py +193 -0
- package/gateway/ai/daemon.py +500 -0
- package/gateway/ai/data_plane.py +291 -0
- package/gateway/ai/deliberation.py +1033 -6
- package/gateway/ai/events.py +39 -0
- package/gateway/ai/founding_users.py +162 -0
- package/gateway/ai/governance.py +698 -4
- package/gateway/ai/inbox_daemon.py +78 -17
- package/gateway/ai/integrations/__init__.py +1 -0
- package/gateway/ai/integrations/opensage_wrapper.py +288 -0
- package/gateway/ai/key_resolver.py +95 -0
- package/gateway/ai/ledger_manager.py +289 -1
- package/gateway/ai/license.py +62 -4
- package/gateway/ai/license_core.py +208 -7
- package/gateway/ai/local_server.py +215 -0
- package/gateway/ai/loop_engine.py +408 -0
- package/gateway/ai/mcp_bridge.py +178 -0
- package/gateway/ai/release_sync.py +2 -2
- package/gateway/ai/screen_record.py +374 -0
- package/gateway/ai/secrets_broker.py +235 -0
- package/gateway/ai/social.py +189 -27
- package/gateway/ai/social_target.py +1635 -0
- package/gateway/ai/supabase_sync.py +190 -0
- package/gateway/ai/tracing.py +195 -0
- package/gateway/core/contract_ledger.py +1 -1
- package/gateway/core/dependency_graph.py +1 -1
- package/gateway/core/dependency_manifest.py +1 -1
- package/gateway/core/diff_engine_v2.py +272 -78
- package/gateway/core/event_backbone.py +2 -2
- package/gateway/core/event_schema.py +1 -1
- package/gateway/core/impact_analyzer.py +1 -1
- package/gateway/core/policy_engine.py +4 -0
- package/package.json +1 -1
|
@@ -19,7 +19,14 @@ class ChangeType(Enum):
|
|
|
19
19
|
TYPE_CHANGED = "type_changed"
|
|
20
20
|
FORMAT_CHANGED = "format_changed"
|
|
21
21
|
ENUM_VALUE_REMOVED = "enum_value_removed"
|
|
22
|
-
|
|
22
|
+
PARAM_TYPE_CHANGED = "param_type_changed"
|
|
23
|
+
PARAM_REQUIRED_CHANGED = "param_required_changed"
|
|
24
|
+
RESPONSE_TYPE_CHANGED = "response_type_changed"
|
|
25
|
+
SECURITY_REMOVED = "security_removed"
|
|
26
|
+
SECURITY_SCOPE_REMOVED = "security_scope_removed"
|
|
27
|
+
MAX_LENGTH_DECREASED = "max_length_decreased"
|
|
28
|
+
MIN_LENGTH_INCREASED = "min_length_increased"
|
|
29
|
+
|
|
23
30
|
# Non-breaking changes
|
|
24
31
|
ENDPOINT_ADDED = "endpoint_added"
|
|
25
32
|
METHOD_ADDED = "method_added"
|
|
@@ -28,6 +35,9 @@ class ChangeType(Enum):
|
|
|
28
35
|
OPTIONAL_FIELD_ADDED = "optional_field_added"
|
|
29
36
|
ENUM_VALUE_ADDED = "enum_value_added"
|
|
30
37
|
DESCRIPTION_CHANGED = "description_changed"
|
|
38
|
+
SECURITY_ADDED = "security_added"
|
|
39
|
+
DEPRECATED_ADDED = "deprecated_added"
|
|
40
|
+
DEFAULT_CHANGED = "default_changed"
|
|
31
41
|
|
|
32
42
|
@dataclass
|
|
33
43
|
class Change:
|
|
@@ -50,6 +60,13 @@ class Change:
|
|
|
50
60
|
ChangeType.TYPE_CHANGED,
|
|
51
61
|
ChangeType.FORMAT_CHANGED,
|
|
52
62
|
ChangeType.ENUM_VALUE_REMOVED,
|
|
63
|
+
ChangeType.PARAM_TYPE_CHANGED,
|
|
64
|
+
ChangeType.PARAM_REQUIRED_CHANGED,
|
|
65
|
+
ChangeType.RESPONSE_TYPE_CHANGED,
|
|
66
|
+
ChangeType.SECURITY_REMOVED,
|
|
67
|
+
ChangeType.SECURITY_SCOPE_REMOVED,
|
|
68
|
+
ChangeType.MAX_LENGTH_DECREASED,
|
|
69
|
+
ChangeType.MIN_LENGTH_INCREASED,
|
|
53
70
|
]
|
|
54
71
|
|
|
55
72
|
class OpenAPIDiffEngine:
|
|
@@ -57,30 +74,26 @@ class OpenAPIDiffEngine:
|
|
|
57
74
|
|
|
58
75
|
def __init__(self):
|
|
59
76
|
self.changes: List[Change] = []
|
|
60
|
-
|
|
61
|
-
self._new_spec: Dict = {}
|
|
62
|
-
self._ref_trail: Set[Tuple[str, str]] = set()
|
|
63
|
-
|
|
77
|
+
|
|
64
78
|
def compare(self, old_spec: Dict, new_spec: Dict) -> List[Change]:
|
|
65
79
|
"""Compare two OpenAPI specifications and return all changes."""
|
|
66
80
|
self.changes = []
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
self._ref_trail = set()
|
|
81
|
+
old_spec = old_spec or {}
|
|
82
|
+
new_spec = new_spec or {}
|
|
70
83
|
|
|
71
84
|
# Compare paths
|
|
72
|
-
self._compare_paths(
|
|
85
|
+
self._compare_paths(old_spec.get("paths", {}), new_spec.get("paths", {}))
|
|
73
86
|
|
|
74
87
|
# Compare components/schemas
|
|
75
88
|
self._compare_schemas(
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
old_spec.get("components", {}).get("schemas", {}),
|
|
90
|
+
new_spec.get("components", {}).get("schemas", {})
|
|
78
91
|
)
|
|
79
92
|
|
|
80
93
|
# Compare security schemes
|
|
81
94
|
self._compare_security(
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
old_spec.get("components", {}).get("securitySchemes", {}),
|
|
96
|
+
new_spec.get("components", {}).get("securitySchemes", {})
|
|
84
97
|
)
|
|
85
98
|
|
|
86
99
|
return self.changes
|
|
@@ -167,6 +180,18 @@ class OpenAPIDiffEngine:
|
|
|
167
180
|
message=f"Required parameter added: {param['name']} to {operation_id}"
|
|
168
181
|
))
|
|
169
182
|
|
|
183
|
+
# Check added optional parameters (non-breaking)
|
|
184
|
+
for param_key in set(new_params.keys()) - set(old_params.keys()):
|
|
185
|
+
param = new_params[param_key]
|
|
186
|
+
if not param.get("required", False):
|
|
187
|
+
self.changes.append(Change(
|
|
188
|
+
type=ChangeType.OPTIONAL_PARAM_ADDED,
|
|
189
|
+
path=operation_id,
|
|
190
|
+
details={"parameter": param["name"], "in": param["in"]},
|
|
191
|
+
severity="low",
|
|
192
|
+
message=f"Optional parameter added: {param['name']} to {operation_id}"
|
|
193
|
+
))
|
|
194
|
+
|
|
170
195
|
# Check parameter schema changes
|
|
171
196
|
for param_key in set(old_params.keys()) & set(new_params.keys()):
|
|
172
197
|
self._compare_parameter_schemas(
|
|
@@ -174,7 +199,27 @@ class OpenAPIDiffEngine:
|
|
|
174
199
|
old_params[param_key],
|
|
175
200
|
new_params[param_key]
|
|
176
201
|
)
|
|
177
|
-
|
|
202
|
+
|
|
203
|
+
# Compare operation-level security
|
|
204
|
+
if "security" in old_op or "security" in new_op:
|
|
205
|
+
self._compare_operation_security(
|
|
206
|
+
operation_id,
|
|
207
|
+
old_op.get("security"),
|
|
208
|
+
new_op.get("security")
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Check deprecated flag
|
|
212
|
+
old_deprecated = old_op.get("deprecated", False)
|
|
213
|
+
new_deprecated = new_op.get("deprecated", False)
|
|
214
|
+
if not old_deprecated and new_deprecated:
|
|
215
|
+
self.changes.append(Change(
|
|
216
|
+
type=ChangeType.DEPRECATED_ADDED,
|
|
217
|
+
path=operation_id,
|
|
218
|
+
details={"target": "operation"},
|
|
219
|
+
severity="low",
|
|
220
|
+
message=f"Operation marked as deprecated: {operation_id}"
|
|
221
|
+
))
|
|
222
|
+
|
|
178
223
|
# Compare request body
|
|
179
224
|
if "requestBody" in old_op or "requestBody" in new_op:
|
|
180
225
|
self._compare_request_body(
|
|
@@ -191,30 +236,64 @@ class OpenAPIDiffEngine:
|
|
|
191
236
|
)
|
|
192
237
|
|
|
193
238
|
def _compare_parameter_schemas(self, operation_id: str, old_param: Dict, new_param: Dict):
|
|
194
|
-
"""Compare parameter schemas for type changes."""
|
|
239
|
+
"""Compare parameter schemas for type changes, required changes, and constraints."""
|
|
195
240
|
old_schema = old_param.get("schema", {})
|
|
196
241
|
new_schema = new_param.get("schema", {})
|
|
242
|
+
param_name = old_param["name"]
|
|
197
243
|
|
|
198
|
-
#
|
|
199
|
-
if "$ref" in old_schema:
|
|
200
|
-
old_schema = self._resolve_schema(old_schema, self._old_spec)
|
|
201
|
-
if "$ref" in new_schema:
|
|
202
|
-
new_schema = self._resolve_schema(new_schema, self._new_spec)
|
|
203
|
-
|
|
204
|
-
# Check type changes
|
|
244
|
+
# Check type changes — emit both PARAM_TYPE_CHANGED (specific) and TYPE_CHANGED (legacy)
|
|
205
245
|
if old_schema.get("type") != new_schema.get("type"):
|
|
246
|
+
self.changes.append(Change(
|
|
247
|
+
type=ChangeType.PARAM_TYPE_CHANGED,
|
|
248
|
+
path=operation_id,
|
|
249
|
+
details={
|
|
250
|
+
"parameter": param_name,
|
|
251
|
+
"old_type": old_schema.get("type"),
|
|
252
|
+
"new_type": new_schema.get("type")
|
|
253
|
+
},
|
|
254
|
+
severity="high",
|
|
255
|
+
message=f"Parameter type changed: {param_name} from {old_schema.get('type')} to {new_schema.get('type')} in {operation_id}"
|
|
256
|
+
))
|
|
206
257
|
self.changes.append(Change(
|
|
207
258
|
type=ChangeType.TYPE_CHANGED,
|
|
208
259
|
path=operation_id,
|
|
209
260
|
details={
|
|
210
|
-
"parameter":
|
|
261
|
+
"parameter": param_name,
|
|
211
262
|
"old_type": old_schema.get("type"),
|
|
212
263
|
"new_type": new_schema.get("type")
|
|
213
264
|
},
|
|
214
265
|
severity="high",
|
|
215
|
-
message=f"Parameter type changed: {
|
|
266
|
+
message=f"Parameter type changed: {param_name} from {old_schema.get('type')} to {new_schema.get('type')}"
|
|
216
267
|
))
|
|
217
|
-
|
|
268
|
+
|
|
269
|
+
# Check required changed (optional -> required)
|
|
270
|
+
old_required = old_param.get("required", False)
|
|
271
|
+
new_required = new_param.get("required", False)
|
|
272
|
+
if not old_required and new_required:
|
|
273
|
+
self.changes.append(Change(
|
|
274
|
+
type=ChangeType.PARAM_REQUIRED_CHANGED,
|
|
275
|
+
path=operation_id,
|
|
276
|
+
details={"parameter": param_name, "old_required": False, "new_required": True},
|
|
277
|
+
severity="high",
|
|
278
|
+
message=f"Parameter changed from optional to required: {param_name} in {operation_id}"
|
|
279
|
+
))
|
|
280
|
+
|
|
281
|
+
# Check constraint changes
|
|
282
|
+
self._compare_constraints(f"{operation_id}:{param_name}", old_schema, new_schema)
|
|
283
|
+
|
|
284
|
+
# Check default value changes
|
|
285
|
+
if "default" in old_schema or "default" in new_schema:
|
|
286
|
+
old_default = old_schema.get("default")
|
|
287
|
+
new_default = new_schema.get("default")
|
|
288
|
+
if old_default != new_default:
|
|
289
|
+
self.changes.append(Change(
|
|
290
|
+
type=ChangeType.DEFAULT_CHANGED,
|
|
291
|
+
path=f"{operation_id}:{param_name}",
|
|
292
|
+
details={"old_default": old_default, "new_default": new_default},
|
|
293
|
+
severity="low",
|
|
294
|
+
message=f"Default value changed for {param_name} from {old_default} to {new_default}"
|
|
295
|
+
))
|
|
296
|
+
|
|
218
297
|
# Check enum changes
|
|
219
298
|
if "enum" in old_schema or "enum" in new_schema:
|
|
220
299
|
self._compare_enums(
|
|
@@ -286,58 +365,34 @@ class OpenAPIDiffEngine:
|
|
|
286
365
|
new_content[content_type].get("schema", {})
|
|
287
366
|
)
|
|
288
367
|
|
|
289
|
-
def _resolve_ref(self, ref_string: str, spec: Dict) -> Optional[Dict]:
|
|
290
|
-
"""Resolve a JSON $ref pointer like #/components/schemas/User."""
|
|
291
|
-
if not ref_string.startswith('#/'):
|
|
292
|
-
return None
|
|
293
|
-
parts = ref_string[2:].split('/')
|
|
294
|
-
current = spec
|
|
295
|
-
for part in parts:
|
|
296
|
-
part = part.replace('~1', '/').replace('~0', '~')
|
|
297
|
-
if isinstance(current, dict) and part in current:
|
|
298
|
-
current = current[part]
|
|
299
|
-
else:
|
|
300
|
-
return None
|
|
301
|
-
return current if isinstance(current, dict) else None
|
|
302
|
-
|
|
303
|
-
def _resolve_schema(self, schema: Dict, spec: Dict, visited: Optional[Set[str]] = None) -> Dict:
|
|
304
|
-
"""Follow $ref chains, detecting circular references."""
|
|
305
|
-
if visited is None:
|
|
306
|
-
visited = set()
|
|
307
|
-
if '$ref' not in schema:
|
|
308
|
-
return schema
|
|
309
|
-
ref = schema['$ref']
|
|
310
|
-
if ref in visited:
|
|
311
|
-
return schema # circular — return as-is
|
|
312
|
-
visited.add(ref)
|
|
313
|
-
resolved = self._resolve_ref(ref, spec)
|
|
314
|
-
if resolved is None:
|
|
315
|
-
return schema # unresolvable — return as-is
|
|
316
|
-
if '$ref' in resolved:
|
|
317
|
-
return self._resolve_schema(resolved, spec, visited)
|
|
318
|
-
return resolved
|
|
319
|
-
|
|
320
368
|
def _compare_schema_deep(self, path: str, old_schema: Dict, new_schema: Dict, required_fields: Optional[Set[str]] = None):
|
|
321
369
|
"""Deep comparison of schemas including nested objects."""
|
|
322
370
|
|
|
323
|
-
# Handle references
|
|
371
|
+
# Handle references
|
|
324
372
|
if "$ref" in old_schema or "$ref" in new_schema:
|
|
325
|
-
|
|
326
|
-
new_resolved = self._resolve_schema(new_schema, self._new_spec) if "$ref" in new_schema else new_schema
|
|
327
|
-
# Track ref pairs to avoid infinite loops on circular schemas
|
|
328
|
-
ref_key = (old_schema.get("$ref", ""), new_schema.get("$ref", ""))
|
|
329
|
-
if ref_key in self._ref_trail:
|
|
330
|
-
return
|
|
331
|
-
self._ref_trail.add(ref_key)
|
|
332
|
-
# Compare the resolved schemas
|
|
333
|
-
self._compare_schema_deep(path, old_resolved, new_resolved, required_fields)
|
|
373
|
+
# TODO: Resolve references properly
|
|
334
374
|
return
|
|
335
|
-
|
|
375
|
+
|
|
336
376
|
# Compare types
|
|
337
377
|
old_type = old_schema.get("type")
|
|
338
378
|
new_type = new_schema.get("type")
|
|
339
|
-
|
|
379
|
+
|
|
340
380
|
if old_type != new_type and old_type is not None:
|
|
381
|
+
# Determine if this is a response context for RESPONSE_TYPE_CHANGED
|
|
382
|
+
is_response = bool(
|
|
383
|
+
":" in path and any(
|
|
384
|
+
code in path for code in
|
|
385
|
+
["200", "201", "202", "204", "301", "400", "401", "403", "404", "500"]
|
|
386
|
+
)
|
|
387
|
+
)
|
|
388
|
+
if is_response:
|
|
389
|
+
self.changes.append(Change(
|
|
390
|
+
type=ChangeType.RESPONSE_TYPE_CHANGED,
|
|
391
|
+
path=path,
|
|
392
|
+
details={"old_type": old_type, "new_type": new_type},
|
|
393
|
+
severity="high",
|
|
394
|
+
message=f"Response type changed from {old_type} to {new_type} at {path}"
|
|
395
|
+
))
|
|
341
396
|
self.changes.append(Change(
|
|
342
397
|
type=ChangeType.TYPE_CHANGED,
|
|
343
398
|
path=path,
|
|
@@ -346,14 +401,14 @@ class OpenAPIDiffEngine:
|
|
|
346
401
|
message=f"Type changed from {old_type} to {new_type} at {path}"
|
|
347
402
|
))
|
|
348
403
|
return
|
|
349
|
-
|
|
404
|
+
|
|
350
405
|
# Compare object properties
|
|
351
406
|
if old_type == "object":
|
|
352
407
|
old_props = old_schema.get("properties", {})
|
|
353
408
|
new_props = new_schema.get("properties", {})
|
|
354
409
|
old_required = set(old_schema.get("required", []))
|
|
355
410
|
new_required = set(new_schema.get("required", []))
|
|
356
|
-
|
|
411
|
+
|
|
357
412
|
# Check removed fields
|
|
358
413
|
for prop in set(old_props.keys()) - set(new_props.keys()):
|
|
359
414
|
if prop in old_required:
|
|
@@ -364,7 +419,7 @@ class OpenAPIDiffEngine:
|
|
|
364
419
|
severity="high",
|
|
365
420
|
message=f"Required field '{prop}' removed at {path}"
|
|
366
421
|
))
|
|
367
|
-
|
|
422
|
+
|
|
368
423
|
# Check new required fields
|
|
369
424
|
for prop in new_required - old_required:
|
|
370
425
|
if prop not in old_props:
|
|
@@ -375,16 +430,45 @@ class OpenAPIDiffEngine:
|
|
|
375
430
|
severity="high",
|
|
376
431
|
message=f"New required field '{prop}' added at {path}"
|
|
377
432
|
))
|
|
378
|
-
|
|
433
|
+
|
|
379
434
|
# Recursively compare nested properties
|
|
380
435
|
for prop in set(old_props.keys()) & set(new_props.keys()):
|
|
436
|
+
old_prop_schema = old_props[prop]
|
|
437
|
+
new_prop_schema = new_props[prop]
|
|
438
|
+
|
|
439
|
+
# Check deprecated on fields
|
|
440
|
+
if not old_prop_schema.get("deprecated", False) and new_prop_schema.get("deprecated", False):
|
|
441
|
+
self.changes.append(Change(
|
|
442
|
+
type=ChangeType.DEPRECATED_ADDED,
|
|
443
|
+
path=f"{path}.{prop}",
|
|
444
|
+
details={"target": "field", "field": prop},
|
|
445
|
+
severity="low",
|
|
446
|
+
message=f"Field '{prop}' marked as deprecated at {path}"
|
|
447
|
+
))
|
|
448
|
+
|
|
449
|
+
# Check default value changes on fields
|
|
450
|
+
if "default" in old_prop_schema or "default" in new_prop_schema:
|
|
451
|
+
old_default = old_prop_schema.get("default")
|
|
452
|
+
new_default = new_prop_schema.get("default")
|
|
453
|
+
if old_default != new_default:
|
|
454
|
+
self.changes.append(Change(
|
|
455
|
+
type=ChangeType.DEFAULT_CHANGED,
|
|
456
|
+
path=f"{path}.{prop}",
|
|
457
|
+
details={"old_default": old_default, "new_default": new_default},
|
|
458
|
+
severity="low",
|
|
459
|
+
message=f"Default value changed for '{prop}' from {old_default} to {new_default} at {path}"
|
|
460
|
+
))
|
|
461
|
+
|
|
462
|
+
# Check constraint changes on fields
|
|
463
|
+
self._compare_constraints(f"{path}.{prop}", old_prop_schema, new_prop_schema)
|
|
464
|
+
|
|
381
465
|
self._compare_schema_deep(
|
|
382
466
|
f"{path}.{prop}",
|
|
383
|
-
|
|
384
|
-
|
|
467
|
+
old_prop_schema,
|
|
468
|
+
new_prop_schema,
|
|
385
469
|
old_required if prop in old_required else None
|
|
386
470
|
)
|
|
387
|
-
|
|
471
|
+
|
|
388
472
|
# Compare arrays
|
|
389
473
|
elif old_type == "array":
|
|
390
474
|
if "items" in old_schema and "items" in new_schema:
|
|
@@ -393,10 +477,14 @@ class OpenAPIDiffEngine:
|
|
|
393
477
|
old_schema["items"],
|
|
394
478
|
new_schema["items"]
|
|
395
479
|
)
|
|
396
|
-
|
|
480
|
+
|
|
397
481
|
# Compare enums
|
|
398
482
|
if "enum" in old_schema or "enum" in new_schema:
|
|
399
483
|
self._compare_enums(path, old_schema.get("enum", []), new_schema.get("enum", []))
|
|
484
|
+
|
|
485
|
+
# Compare constraints at top level of schema (non-object)
|
|
486
|
+
if old_type != "object":
|
|
487
|
+
self._compare_constraints(path, old_schema, new_schema)
|
|
400
488
|
|
|
401
489
|
def _compare_enums(self, path: str, old_enum: List, new_enum: List):
|
|
402
490
|
"""Compare enum values."""
|
|
@@ -443,17 +531,123 @@ class OpenAPIDiffEngine:
|
|
|
443
531
|
new_schemas[schema_name]
|
|
444
532
|
)
|
|
445
533
|
|
|
534
|
+
def _compare_constraints(self, path: str, old_schema: Dict, new_schema: Dict):
|
|
535
|
+
"""Compare schema constraints (maxLength, minLength, maxItems, minItems)."""
|
|
536
|
+
# maxLength / maxItems decreased = breaking (stricter)
|
|
537
|
+
for prop in ("maxLength", "maxItems"):
|
|
538
|
+
old_val = old_schema.get(prop)
|
|
539
|
+
new_val = new_schema.get(prop)
|
|
540
|
+
if old_val is not None and new_val is not None and new_val < old_val:
|
|
541
|
+
self.changes.append(Change(
|
|
542
|
+
type=ChangeType.MAX_LENGTH_DECREASED,
|
|
543
|
+
path=path,
|
|
544
|
+
details={"constraint": prop, "old_value": old_val, "new_value": new_val},
|
|
545
|
+
severity="high",
|
|
546
|
+
message=f"{prop} decreased from {old_val} to {new_val} at {path}"
|
|
547
|
+
))
|
|
548
|
+
elif old_val is None and new_val is not None:
|
|
549
|
+
# Adding a max constraint where there was none is also stricter
|
|
550
|
+
self.changes.append(Change(
|
|
551
|
+
type=ChangeType.MAX_LENGTH_DECREASED,
|
|
552
|
+
path=path,
|
|
553
|
+
details={"constraint": prop, "old_value": None, "new_value": new_val},
|
|
554
|
+
severity="high",
|
|
555
|
+
message=f"{prop} added ({new_val}) at {path} where none existed"
|
|
556
|
+
))
|
|
557
|
+
|
|
558
|
+
# minLength / minItems increased = breaking (stricter)
|
|
559
|
+
for prop in ("minLength", "minItems"):
|
|
560
|
+
old_val = old_schema.get(prop)
|
|
561
|
+
new_val = new_schema.get(prop)
|
|
562
|
+
if old_val is not None and new_val is not None and new_val > old_val:
|
|
563
|
+
self.changes.append(Change(
|
|
564
|
+
type=ChangeType.MIN_LENGTH_INCREASED,
|
|
565
|
+
path=path,
|
|
566
|
+
details={"constraint": prop, "old_value": old_val, "new_value": new_val},
|
|
567
|
+
severity="high",
|
|
568
|
+
message=f"{prop} increased from {old_val} to {new_val} at {path}"
|
|
569
|
+
))
|
|
570
|
+
elif old_val is None and new_val is not None and new_val > 0:
|
|
571
|
+
# Adding a min constraint where there was none is stricter
|
|
572
|
+
self.changes.append(Change(
|
|
573
|
+
type=ChangeType.MIN_LENGTH_INCREASED,
|
|
574
|
+
path=path,
|
|
575
|
+
details={"constraint": prop, "old_value": None, "new_value": new_val},
|
|
576
|
+
severity="high",
|
|
577
|
+
message=f"{prop} added ({new_val}) at {path} where none existed"
|
|
578
|
+
))
|
|
579
|
+
|
|
580
|
+
def _compare_operation_security(self, operation_id: str, old_security: Optional[list], new_security: Optional[list]):
|
|
581
|
+
"""Compare operation-level security requirements."""
|
|
582
|
+
if old_security is None:
|
|
583
|
+
old_security = []
|
|
584
|
+
if new_security is None:
|
|
585
|
+
new_security = []
|
|
586
|
+
|
|
587
|
+
# Build maps: scheme_name -> set of scopes
|
|
588
|
+
def _security_map(sec_list):
|
|
589
|
+
result = {}
|
|
590
|
+
for item in sec_list:
|
|
591
|
+
for scheme, scopes in item.items():
|
|
592
|
+
result[scheme] = set(scopes) if scopes else set()
|
|
593
|
+
return result
|
|
594
|
+
|
|
595
|
+
old_map = _security_map(old_security)
|
|
596
|
+
new_map = _security_map(new_security)
|
|
597
|
+
|
|
598
|
+
# Removed security schemes from operation
|
|
599
|
+
for scheme in set(old_map.keys()) - set(new_map.keys()):
|
|
600
|
+
self.changes.append(Change(
|
|
601
|
+
type=ChangeType.SECURITY_REMOVED,
|
|
602
|
+
path=operation_id,
|
|
603
|
+
details={"scheme": scheme},
|
|
604
|
+
severity="high",
|
|
605
|
+
message=f"Security scheme '{scheme}' removed from {operation_id}"
|
|
606
|
+
))
|
|
607
|
+
|
|
608
|
+
# Added security schemes to operation
|
|
609
|
+
for scheme in set(new_map.keys()) - set(old_map.keys()):
|
|
610
|
+
self.changes.append(Change(
|
|
611
|
+
type=ChangeType.SECURITY_ADDED,
|
|
612
|
+
path=operation_id,
|
|
613
|
+
details={"scheme": scheme},
|
|
614
|
+
severity="low",
|
|
615
|
+
message=f"Security scheme '{scheme}' added to {operation_id}"
|
|
616
|
+
))
|
|
617
|
+
|
|
618
|
+
# Check scope changes for shared schemes
|
|
619
|
+
for scheme in set(old_map.keys()) & set(new_map.keys()):
|
|
620
|
+
removed_scopes = old_map[scheme] - new_map[scheme]
|
|
621
|
+
for scope in removed_scopes:
|
|
622
|
+
self.changes.append(Change(
|
|
623
|
+
type=ChangeType.SECURITY_SCOPE_REMOVED,
|
|
624
|
+
path=operation_id,
|
|
625
|
+
details={"scheme": scheme, "scope": scope},
|
|
626
|
+
severity="high",
|
|
627
|
+
message=f"OAuth scope '{scope}' removed from scheme '{scheme}' at {operation_id}"
|
|
628
|
+
))
|
|
629
|
+
|
|
446
630
|
def _compare_security(self, old_security: Dict, new_security: Dict):
|
|
447
631
|
"""Compare security schemes."""
|
|
448
|
-
# Security scheme
|
|
632
|
+
# Security scheme removal is breaking
|
|
449
633
|
for scheme in set(old_security.keys()) - set(new_security.keys()):
|
|
450
634
|
self.changes.append(Change(
|
|
451
|
-
type=ChangeType.
|
|
635
|
+
type=ChangeType.SECURITY_REMOVED,
|
|
452
636
|
path=f"#/components/securitySchemes/{scheme}",
|
|
453
637
|
details={"scheme": scheme},
|
|
454
638
|
severity="high",
|
|
455
639
|
message=f"Security scheme '{scheme}' removed"
|
|
456
640
|
))
|
|
641
|
+
|
|
642
|
+
# Security scheme addition is non-breaking
|
|
643
|
+
for scheme in set(new_security.keys()) - set(old_security.keys()):
|
|
644
|
+
self.changes.append(Change(
|
|
645
|
+
type=ChangeType.SECURITY_ADDED,
|
|
646
|
+
path=f"#/components/securitySchemes/{scheme}",
|
|
647
|
+
details={"scheme": scheme},
|
|
648
|
+
severity="low",
|
|
649
|
+
message=f"Security scheme '{scheme}' added"
|
|
650
|
+
))
|
|
457
651
|
|
|
458
652
|
def _param_key(self, param: Dict) -> str:
|
|
459
653
|
"""Generate unique key for parameter."""
|
|
@@ -3,7 +3,7 @@ Delimit Event Backbone
|
|
|
3
3
|
Constructs ledger events, generates SHA-256 hashes, links hash chains,
|
|
4
4
|
and appends to the append-only JSONL ledger.
|
|
5
5
|
|
|
6
|
-
Per
|
|
6
|
+
Per Jamsons Doctrine:
|
|
7
7
|
- Deterministic outputs
|
|
8
8
|
- Append-only artifacts
|
|
9
9
|
- Fail-closed CI behavior (ledger failures never affect CI)
|
|
@@ -199,7 +199,7 @@ class EventBackbone:
|
|
|
199
199
|
This is the primary API for event generation. It is best-effort:
|
|
200
200
|
if the ledger write fails, the event is still returned but not persisted.
|
|
201
201
|
|
|
202
|
-
CRITICAL: This method NEVER raises exceptions. Per
|
|
202
|
+
CRITICAL: This method NEVER raises exceptions. Per Jamsons Doctrine,
|
|
203
203
|
ledger failures must not affect CI pass/fail outcome.
|
|
204
204
|
|
|
205
205
|
Returns:
|
|
@@ -3,7 +3,7 @@ Delimit Impact Analyzer
|
|
|
3
3
|
Determines downstream consumers affected by an API change
|
|
4
4
|
and produces informational impact summaries for CI output.
|
|
5
5
|
|
|
6
|
-
Per
|
|
6
|
+
Per Jamsons Doctrine:
|
|
7
7
|
- Impact analysis is INFORMATIONAL ONLY
|
|
8
8
|
- NEVER affects CI pass/fail outcome
|
|
9
9
|
- Deterministic outputs
|
|
@@ -11,12 +11,16 @@ from pathlib import Path
|
|
|
11
11
|
|
|
12
12
|
from core.diff_engine_v2 import OpenAPIDiffEngine, Change, ChangeType
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
class RuleSeverity(Enum):
|
|
16
|
+
"""Severity levels for governance policy rules."""
|
|
15
17
|
ERROR = "error" # Fails CI
|
|
16
18
|
WARNING = "warning" # Shows warning but passes
|
|
17
19
|
INFO = "info" # Informational only
|
|
18
20
|
|
|
21
|
+
|
|
19
22
|
class RuleAction(Enum):
|
|
23
|
+
"""Actions to take when a policy rule is triggered."""
|
|
20
24
|
FORBID = "forbid" # Forbids the change
|
|
21
25
|
ALLOW = "allow" # Explicitly allows
|
|
22
26
|
WARN = "warn" # Warns but allows
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "delimit-cli",
|
|
3
3
|
"mcpName": "io.github.delimit-ai/delimit-mcp-server",
|
|
4
|
-
"version": "3.14.
|
|
4
|
+
"version": "3.14.29",
|
|
5
5
|
"description": "Unify Claude Code, Codex, Cursor, and Gemini CLI with persistent context, governance, and multi-model debate.",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"files": [
|