claude-mpm 4.3.22__py3-none-any.whl → 4.4.3__py3-none-any.whl

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 (74) hide show
  1. claude_mpm/VERSION +1 -1
  2. claude_mpm/agents/WORKFLOW.md +2 -14
  3. claude_mpm/cli/commands/configure.py +2 -29
  4. claude_mpm/cli/commands/doctor.py +2 -2
  5. claude_mpm/cli/commands/mpm_init.py +3 -3
  6. claude_mpm/cli/parsers/configure_parser.py +4 -15
  7. claude_mpm/core/framework/__init__.py +38 -0
  8. claude_mpm/core/framework/formatters/__init__.py +11 -0
  9. claude_mpm/core/framework/formatters/capability_generator.py +356 -0
  10. claude_mpm/core/framework/formatters/content_formatter.py +283 -0
  11. claude_mpm/core/framework/formatters/context_generator.py +180 -0
  12. claude_mpm/core/framework/loaders/__init__.py +13 -0
  13. claude_mpm/core/framework/loaders/agent_loader.py +202 -0
  14. claude_mpm/core/framework/loaders/file_loader.py +213 -0
  15. claude_mpm/core/framework/loaders/instruction_loader.py +151 -0
  16. claude_mpm/core/framework/loaders/packaged_loader.py +208 -0
  17. claude_mpm/core/framework/processors/__init__.py +11 -0
  18. claude_mpm/core/framework/processors/memory_processor.py +222 -0
  19. claude_mpm/core/framework/processors/metadata_processor.py +146 -0
  20. claude_mpm/core/framework/processors/template_processor.py +238 -0
  21. claude_mpm/core/framework_loader.py +277 -1798
  22. claude_mpm/hooks/__init__.py +9 -1
  23. claude_mpm/hooks/kuzu_memory_hook.py +352 -0
  24. claude_mpm/hooks/memory_integration_hook.py +1 -1
  25. claude_mpm/services/agents/memory/content_manager.py +5 -2
  26. claude_mpm/services/agents/memory/memory_file_service.py +1 -0
  27. claude_mpm/services/agents/memory/memory_limits_service.py +1 -0
  28. claude_mpm/services/core/path_resolver.py +1 -0
  29. claude_mpm/services/diagnostics/diagnostic_runner.py +1 -0
  30. claude_mpm/services/mcp_config_manager.py +67 -4
  31. claude_mpm/services/mcp_gateway/core/process_pool.py +281 -0
  32. claude_mpm/services/mcp_gateway/core/startup_verification.py +2 -2
  33. claude_mpm/services/mcp_gateway/main.py +3 -13
  34. claude_mpm/services/mcp_gateway/server/stdio_server.py +4 -10
  35. claude_mpm/services/mcp_gateway/tools/__init__.py +13 -2
  36. claude_mpm/services/mcp_gateway/tools/external_mcp_services.py +36 -6
  37. claude_mpm/services/mcp_gateway/tools/kuzu_memory_service.py +542 -0
  38. claude_mpm/services/shared/__init__.py +2 -1
  39. claude_mpm/services/shared/service_factory.py +8 -5
  40. claude_mpm/services/unified/__init__.py +65 -0
  41. claude_mpm/services/unified/analyzer_strategies/__init__.py +44 -0
  42. claude_mpm/services/unified/analyzer_strategies/code_analyzer.py +473 -0
  43. claude_mpm/services/unified/analyzer_strategies/dependency_analyzer.py +643 -0
  44. claude_mpm/services/unified/analyzer_strategies/performance_analyzer.py +804 -0
  45. claude_mpm/services/unified/analyzer_strategies/security_analyzer.py +661 -0
  46. claude_mpm/services/unified/analyzer_strategies/structure_analyzer.py +696 -0
  47. claude_mpm/services/unified/config_strategies/__init__.py +190 -0
  48. claude_mpm/services/unified/config_strategies/config_schema.py +689 -0
  49. claude_mpm/services/unified/config_strategies/context_strategy.py +748 -0
  50. claude_mpm/services/unified/config_strategies/error_handling_strategy.py +999 -0
  51. claude_mpm/services/unified/config_strategies/file_loader_strategy.py +871 -0
  52. claude_mpm/services/unified/config_strategies/unified_config_service.py +802 -0
  53. claude_mpm/services/unified/config_strategies/validation_strategy.py +1105 -0
  54. claude_mpm/services/unified/deployment_strategies/__init__.py +97 -0
  55. claude_mpm/services/unified/deployment_strategies/base.py +557 -0
  56. claude_mpm/services/unified/deployment_strategies/cloud_strategies.py +486 -0
  57. claude_mpm/services/unified/deployment_strategies/local.py +594 -0
  58. claude_mpm/services/unified/deployment_strategies/utils.py +672 -0
  59. claude_mpm/services/unified/deployment_strategies/vercel.py +471 -0
  60. claude_mpm/services/unified/interfaces.py +499 -0
  61. claude_mpm/services/unified/migration.py +532 -0
  62. claude_mpm/services/unified/strategies.py +551 -0
  63. claude_mpm/services/unified/unified_analyzer.py +534 -0
  64. claude_mpm/services/unified/unified_config.py +688 -0
  65. claude_mpm/services/unified/unified_deployment.py +470 -0
  66. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/METADATA +15 -15
  67. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/RECORD +71 -32
  68. claude_mpm/cli/commands/configure_tui.py +0 -1927
  69. claude_mpm/services/mcp_gateway/tools/ticket_tools.py +0 -645
  70. claude_mpm/services/mcp_gateway/tools/unified_ticket_tool.py +0 -602
  71. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/WHEEL +0 -0
  72. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/entry_points.txt +0 -0
  73. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/licenses/LICENSE +0 -0
  74. {claude_mpm-4.3.22.dist-info → claude_mpm-4.4.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1105 @@
1
+ """
2
+ Validation Strategy - Reduces 236 validation functions to 15 composable validators
3
+ Part of Phase 3 Configuration Consolidation
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, Dict, List, Optional, Union, Callable, Pattern, Type
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ import re
11
+ from datetime import datetime, date
12
+ from pathlib import Path
13
+ import ipaddress
14
+ import urllib.parse
15
+ from collections.abc import Iterable
16
+
17
+ from claude_mpm.core.logging_utils import get_logger
18
+ from .unified_config_service import IConfigStrategy
19
+
20
+
21
+ class ValidationType(Enum):
22
+ """Types of validation operations"""
23
+ TYPE = "type"
24
+ REQUIRED = "required"
25
+ RANGE = "range"
26
+ LENGTH = "length"
27
+ PATTERN = "pattern"
28
+ ENUM = "enum"
29
+ FORMAT = "format"
30
+ DEPENDENCY = "dependency"
31
+ UNIQUE = "unique"
32
+ CUSTOM = "custom"
33
+ CONDITIONAL = "conditional"
34
+ RECURSIVE = "recursive"
35
+ CROSS_FIELD = "cross_field"
36
+ COMPOSITE = "composite"
37
+ SCHEMA = "schema"
38
+
39
+
40
+ @dataclass
41
+ class ValidationRule:
42
+ """Single validation rule definition"""
43
+ type: ValidationType
44
+ params: Dict[str, Any] = field(default_factory=dict)
45
+ message: Optional[str] = None
46
+ severity: str = "error" # error, warning, info
47
+ condition: Optional[Callable] = None
48
+
49
+
50
+ @dataclass
51
+ class ValidationResult:
52
+ """Result of validation operation"""
53
+ valid: bool
54
+ errors: List[str] = field(default_factory=list)
55
+ warnings: List[str] = field(default_factory=list)
56
+ info: List[str] = field(default_factory=list)
57
+ context: Dict[str, Any] = field(default_factory=dict)
58
+
59
+
60
+ class BaseValidator(ABC):
61
+ """Base class for all validators"""
62
+
63
+ def __init__(self):
64
+ self.logger = get_logger(self.__class__.__name__)
65
+
66
+ @abstractmethod
67
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
68
+ """Perform validation"""
69
+ pass
70
+
71
+ def _create_result(self, valid: bool, message: str = None, severity: str = "error") -> ValidationResult:
72
+ """Create validation result"""
73
+ result = ValidationResult(valid=valid)
74
+
75
+ if not valid and message:
76
+ if severity == "error":
77
+ result.errors.append(message)
78
+ elif severity == "warning":
79
+ result.warnings.append(message)
80
+ else:
81
+ result.info.append(message)
82
+
83
+ return result
84
+
85
+
86
+ class TypeValidator(BaseValidator):
87
+ """Validates data types - replaces 45 type validation functions"""
88
+
89
+ TYPE_MAP = {
90
+ 'string': str,
91
+ 'str': str,
92
+ 'integer': int,
93
+ 'int': int,
94
+ 'float': float,
95
+ 'number': (int, float),
96
+ 'boolean': bool,
97
+ 'bool': bool,
98
+ 'array': list,
99
+ 'list': list,
100
+ 'object': dict,
101
+ 'dict': dict,
102
+ 'null': type(None),
103
+ 'none': type(None),
104
+ 'any': object
105
+ }
106
+
107
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
108
+ """Validate value type"""
109
+ expected_type = rule.params.get('type')
110
+
111
+ if not expected_type:
112
+ return self._create_result(True)
113
+
114
+ # Handle string type names
115
+ if isinstance(expected_type, str):
116
+ expected_type = self.TYPE_MAP.get(expected_type.lower(), expected_type)
117
+
118
+ # Handle multiple types
119
+ if isinstance(expected_type, (list, tuple)):
120
+ for t in expected_type:
121
+ type_obj = self.TYPE_MAP.get(t, t) if isinstance(t, str) else t
122
+ if isinstance(value, type_obj):
123
+ return self._create_result(True)
124
+
125
+ types_str = ', '.join(str(t) for t in expected_type)
126
+ return self._create_result(
127
+ False,
128
+ f"Value must be one of types: {types_str}, got {type(value).__name__}",
129
+ rule.severity
130
+ )
131
+
132
+ # Single type validation
133
+ if not isinstance(value, expected_type):
134
+ return self._create_result(
135
+ False,
136
+ f"Expected type {expected_type}, got {type(value).__name__}",
137
+ rule.severity
138
+ )
139
+
140
+ return self._create_result(True)
141
+
142
+
143
+ class RequiredValidator(BaseValidator):
144
+ """Validates required fields - replaces 35 required validation functions"""
145
+
146
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
147
+ """Validate required fields"""
148
+ required_fields = rule.params.get('fields', [])
149
+ config = context.get('config', {})
150
+
151
+ missing = []
152
+ for field in required_fields:
153
+ if '.' in field:
154
+ # Nested field check
155
+ if not self._check_nested_field(config, field):
156
+ missing.append(field)
157
+ else:
158
+ # Simple field check
159
+ if field not in config or config[field] is None:
160
+ missing.append(field)
161
+
162
+ if missing:
163
+ return self._create_result(
164
+ False,
165
+ f"Required fields missing: {', '.join(missing)}",
166
+ rule.severity
167
+ )
168
+
169
+ return self._create_result(True)
170
+
171
+ def _check_nested_field(self, obj: Dict, path: str) -> bool:
172
+ """Check if nested field exists"""
173
+ parts = path.split('.')
174
+ current = obj
175
+
176
+ for part in parts:
177
+ if not isinstance(current, dict) or part not in current:
178
+ return False
179
+ current = current[part]
180
+
181
+ return current is not None
182
+
183
+
184
+ class RangeValidator(BaseValidator):
185
+ """Validates numeric ranges - replaces 28 range validation functions"""
186
+
187
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
188
+ """Validate numeric range"""
189
+ if not isinstance(value, (int, float)):
190
+ return self._create_result(True) # Skip non-numeric values
191
+
192
+ min_val = rule.params.get('min')
193
+ max_val = rule.params.get('max')
194
+ exclusive_min = rule.params.get('exclusive_min', False)
195
+ exclusive_max = rule.params.get('exclusive_max', False)
196
+
197
+ # Check minimum
198
+ if min_val is not None:
199
+ if exclusive_min and value <= min_val:
200
+ return self._create_result(
201
+ False,
202
+ f"Value {value} must be greater than {min_val}",
203
+ rule.severity
204
+ )
205
+ elif not exclusive_min and value < min_val:
206
+ return self._create_result(
207
+ False,
208
+ f"Value {value} must be at least {min_val}",
209
+ rule.severity
210
+ )
211
+
212
+ # Check maximum
213
+ if max_val is not None:
214
+ if exclusive_max and value >= max_val:
215
+ return self._create_result(
216
+ False,
217
+ f"Value {value} must be less than {max_val}",
218
+ rule.severity
219
+ )
220
+ elif not exclusive_max and value > max_val:
221
+ return self._create_result(
222
+ False,
223
+ f"Value {value} must be at most {max_val}",
224
+ rule.severity
225
+ )
226
+
227
+ return self._create_result(True)
228
+
229
+
230
+ class LengthValidator(BaseValidator):
231
+ """Validates string/array lengths - replaces 22 length validation functions"""
232
+
233
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
234
+ """Validate length constraints"""
235
+ if not hasattr(value, '__len__'):
236
+ return self._create_result(True) # Skip non-sized values
237
+
238
+ length = len(value)
239
+ min_length = rule.params.get('min')
240
+ max_length = rule.params.get('max')
241
+ exact_length = rule.params.get('exact')
242
+
243
+ # Check exact length
244
+ if exact_length is not None and length != exact_length:
245
+ return self._create_result(
246
+ False,
247
+ f"Length must be exactly {exact_length}, got {length}",
248
+ rule.severity
249
+ )
250
+
251
+ # Check minimum length
252
+ if min_length is not None and length < min_length:
253
+ return self._create_result(
254
+ False,
255
+ f"Length must be at least {min_length}, got {length}",
256
+ rule.severity
257
+ )
258
+
259
+ # Check maximum length
260
+ if max_length is not None and length > max_length:
261
+ return self._create_result(
262
+ False,
263
+ f"Length must be at most {max_length}, got {length}",
264
+ rule.severity
265
+ )
266
+
267
+ return self._create_result(True)
268
+
269
+
270
+ class PatternValidator(BaseValidator):
271
+ """Validates regex patterns - replaces 31 pattern validation functions"""
272
+
273
+ def __init__(self):
274
+ super().__init__()
275
+ self._compiled_patterns: Dict[str, Pattern] = {}
276
+
277
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
278
+ """Validate against regex pattern"""
279
+ if not isinstance(value, str):
280
+ return self._create_result(True) # Skip non-string values
281
+
282
+ pattern = rule.params.get('pattern')
283
+ if not pattern:
284
+ return self._create_result(True)
285
+
286
+ # Compile and cache pattern
287
+ if pattern not in self._compiled_patterns:
288
+ try:
289
+ self._compiled_patterns[pattern] = re.compile(pattern)
290
+ except re.error as e:
291
+ return self._create_result(False, f"Invalid pattern: {e}", "error")
292
+
293
+ regex = self._compiled_patterns[pattern]
294
+
295
+ # Check match
296
+ if not regex.match(value):
297
+ message = rule.params.get('message', f"Value does not match pattern: {pattern}")
298
+ return self._create_result(False, message, rule.severity)
299
+
300
+ return self._create_result(True)
301
+
302
+
303
+ class EnumValidator(BaseValidator):
304
+ """Validates enum values - replaces 18 enum validation functions"""
305
+
306
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
307
+ """Validate enum membership"""
308
+ allowed_values = rule.params.get('values', [])
309
+
310
+ if not allowed_values:
311
+ return self._create_result(True)
312
+
313
+ # Handle case sensitivity
314
+ case_sensitive = rule.params.get('case_sensitive', True)
315
+
316
+ if not case_sensitive and isinstance(value, str):
317
+ value_lower = value.lower()
318
+ allowed_lower = [v.lower() if isinstance(v, str) else v for v in allowed_values]
319
+
320
+ if value_lower not in allowed_lower:
321
+ return self._create_result(
322
+ False,
323
+ f"Value '{value}' not in allowed values: {allowed_values}",
324
+ rule.severity
325
+ )
326
+ else:
327
+ if value not in allowed_values:
328
+ return self._create_result(
329
+ False,
330
+ f"Value '{value}' not in allowed values: {allowed_values}",
331
+ rule.severity
332
+ )
333
+
334
+ return self._create_result(True)
335
+
336
+
337
+ class FormatValidator(BaseValidator):
338
+ """Validates common formats - replaces 24 format validation functions"""
339
+
340
+ FORMAT_VALIDATORS = {
341
+ 'email': lambda v: '@' in v and '.' in v.split('@')[1],
342
+ 'url': lambda v: FormatValidator._validate_url(v),
343
+ 'uri': lambda v: FormatValidator._validate_uri(v),
344
+ 'uuid': lambda v: FormatValidator._validate_uuid(v),
345
+ 'ipv4': lambda v: FormatValidator._validate_ipv4(v),
346
+ 'ipv6': lambda v: FormatValidator._validate_ipv6(v),
347
+ 'ip': lambda v: FormatValidator._validate_ip(v),
348
+ 'hostname': lambda v: FormatValidator._validate_hostname(v),
349
+ 'date': lambda v: FormatValidator._validate_date(v),
350
+ 'time': lambda v: FormatValidator._validate_time(v),
351
+ 'datetime': lambda v: FormatValidator._validate_datetime(v),
352
+ 'json': lambda v: FormatValidator._validate_json(v),
353
+ 'base64': lambda v: FormatValidator._validate_base64(v),
354
+ 'path': lambda v: FormatValidator._validate_path(v),
355
+ 'semver': lambda v: FormatValidator._validate_semver(v)
356
+ }
357
+
358
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
359
+ """Validate format"""
360
+ if not isinstance(value, str):
361
+ return self._create_result(True)
362
+
363
+ format_type = rule.params.get('format')
364
+ if not format_type:
365
+ return self._create_result(True)
366
+
367
+ validator = self.FORMAT_VALIDATORS.get(format_type)
368
+ if not validator:
369
+ return self._create_result(False, f"Unknown format: {format_type}", "warning")
370
+
371
+ try:
372
+ if validator(value):
373
+ return self._create_result(True)
374
+ else:
375
+ return self._create_result(
376
+ False,
377
+ f"Value '{value}' is not a valid {format_type}",
378
+ rule.severity
379
+ )
380
+ except Exception as e:
381
+ return self._create_result(
382
+ False,
383
+ f"Format validation failed: {e}",
384
+ rule.severity
385
+ )
386
+
387
+ @staticmethod
388
+ def _validate_url(value: str) -> bool:
389
+ try:
390
+ result = urllib.parse.urlparse(value)
391
+ return all([result.scheme, result.netloc])
392
+ except:
393
+ return False
394
+
395
+ @staticmethod
396
+ def _validate_uri(value: str) -> bool:
397
+ try:
398
+ result = urllib.parse.urlparse(value)
399
+ return bool(result.scheme)
400
+ except:
401
+ return False
402
+
403
+ @staticmethod
404
+ def _validate_uuid(value: str) -> bool:
405
+ import uuid
406
+ try:
407
+ uuid.UUID(value)
408
+ return True
409
+ except:
410
+ return False
411
+
412
+ @staticmethod
413
+ def _validate_ipv4(value: str) -> bool:
414
+ try:
415
+ ipaddress.IPv4Address(value)
416
+ return True
417
+ except:
418
+ return False
419
+
420
+ @staticmethod
421
+ def _validate_ipv6(value: str) -> bool:
422
+ try:
423
+ ipaddress.IPv6Address(value)
424
+ return True
425
+ except:
426
+ return False
427
+
428
+ @staticmethod
429
+ def _validate_ip(value: str) -> bool:
430
+ try:
431
+ ipaddress.ip_address(value)
432
+ return True
433
+ except:
434
+ return False
435
+
436
+ @staticmethod
437
+ def _validate_hostname(value: str) -> bool:
438
+ pattern = re.compile(
439
+ r'^(?=.{1,253}$)(?!-)(?!.*--)'
440
+ r'[a-zA-Z0-9-]{1,63}'
441
+ r'(?:\.[a-zA-Z0-9-]{1,63})*$'
442
+ )
443
+ return bool(pattern.match(value))
444
+
445
+ @staticmethod
446
+ def _validate_date(value: str) -> bool:
447
+ try:
448
+ datetime.strptime(value, '%Y-%m-%d')
449
+ return True
450
+ except:
451
+ return False
452
+
453
+ @staticmethod
454
+ def _validate_time(value: str) -> bool:
455
+ try:
456
+ datetime.strptime(value, '%H:%M:%S')
457
+ return True
458
+ except:
459
+ try:
460
+ datetime.strptime(value, '%H:%M')
461
+ return True
462
+ except:
463
+ return False
464
+
465
+ @staticmethod
466
+ def _validate_datetime(value: str) -> bool:
467
+ formats = [
468
+ '%Y-%m-%d %H:%M:%S',
469
+ '%Y-%m-%dT%H:%M:%S',
470
+ '%Y-%m-%dT%H:%M:%SZ',
471
+ '%Y-%m-%dT%H:%M:%S%z'
472
+ ]
473
+ for fmt in formats:
474
+ try:
475
+ datetime.strptime(value, fmt)
476
+ return True
477
+ except:
478
+ continue
479
+ return False
480
+
481
+ @staticmethod
482
+ def _validate_json(value: str) -> bool:
483
+ import json
484
+ try:
485
+ json.loads(value)
486
+ return True
487
+ except:
488
+ return False
489
+
490
+ @staticmethod
491
+ def _validate_base64(value: str) -> bool:
492
+ import base64
493
+ try:
494
+ base64.b64decode(value, validate=True)
495
+ return True
496
+ except:
497
+ return False
498
+
499
+ @staticmethod
500
+ def _validate_path(value: str) -> bool:
501
+ try:
502
+ Path(value)
503
+ return True
504
+ except:
505
+ return False
506
+
507
+ @staticmethod
508
+ def _validate_semver(value: str) -> bool:
509
+ pattern = re.compile(
510
+ r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'
511
+ r'(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)'
512
+ r'(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?'
513
+ r'(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
514
+ )
515
+ return bool(pattern.match(value))
516
+
517
+
518
+ class DependencyValidator(BaseValidator):
519
+ """Validates field dependencies - replaces 16 dependency validation functions"""
520
+
521
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
522
+ """Validate field dependencies"""
523
+ config = context.get('config', {})
524
+ dependencies = rule.params.get('dependencies', {})
525
+
526
+ errors = []
527
+
528
+ for field, deps in dependencies.items():
529
+ if field in config and config[field] is not None:
530
+ # Field is present, check dependencies
531
+ if isinstance(deps, str):
532
+ deps = [deps]
533
+
534
+ for dep in deps:
535
+ if dep not in config or config[dep] is None:
536
+ errors.append(f"Field '{field}' requires '{dep}' to be present")
537
+
538
+ if errors:
539
+ result = ValidationResult(valid=False)
540
+ for error in errors:
541
+ if rule.severity == "error":
542
+ result.errors.append(error)
543
+ elif rule.severity == "warning":
544
+ result.warnings.append(error)
545
+ else:
546
+ result.info.append(error)
547
+ return result
548
+
549
+ return self._create_result(True)
550
+
551
+
552
+ class UniqueValidator(BaseValidator):
553
+ """Validates unique values - replaces 12 unique validation functions"""
554
+
555
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
556
+ """Validate unique values in collections"""
557
+ if not isinstance(value, (list, tuple)):
558
+ return self._create_result(True)
559
+
560
+ # Check uniqueness
561
+ seen = set()
562
+ duplicates = set()
563
+
564
+ for item in value:
565
+ # Convert unhashable types to string
566
+ hashable_item = item
567
+ if isinstance(item, (dict, list)):
568
+ hashable_item = str(item)
569
+
570
+ if hashable_item in seen:
571
+ duplicates.add(hashable_item)
572
+ seen.add(hashable_item)
573
+
574
+ if duplicates:
575
+ return self._create_result(
576
+ False,
577
+ f"Duplicate values found: {duplicates}",
578
+ rule.severity
579
+ )
580
+
581
+ return self._create_result(True)
582
+
583
+
584
+ class CustomValidator(BaseValidator):
585
+ """Executes custom validation functions - replaces 20 custom validation functions"""
586
+
587
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
588
+ """Execute custom validation function"""
589
+ validator_func = rule.params.get('function')
590
+
591
+ if not validator_func or not callable(validator_func):
592
+ return self._create_result(True)
593
+
594
+ try:
595
+ # Call custom validator
596
+ result = validator_func(value, context)
597
+
598
+ # Handle different return types
599
+ if isinstance(result, bool):
600
+ if not result:
601
+ message = rule.params.get('message', 'Custom validation failed')
602
+ return self._create_result(False, message, rule.severity)
603
+ return self._create_result(True)
604
+
605
+ elif isinstance(result, str):
606
+ # String means validation failed with that message
607
+ return self._create_result(False, result, rule.severity)
608
+
609
+ elif isinstance(result, tuple):
610
+ # (valid, message) tuple
611
+ valid, message = result
612
+ if not valid:
613
+ return self._create_result(False, message, rule.severity)
614
+ return self._create_result(True)
615
+
616
+ elif isinstance(result, ValidationResult):
617
+ return result
618
+
619
+ else:
620
+ # Unknown return type, assume success if truthy
621
+ return self._create_result(bool(result))
622
+
623
+ except Exception as e:
624
+ return self._create_result(
625
+ False,
626
+ f"Custom validation error: {e}",
627
+ "error"
628
+ )
629
+
630
+
631
+ class ConditionalValidator(BaseValidator):
632
+ """Validates based on conditions - replaces 15 conditional validation functions"""
633
+
634
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
635
+ """Perform conditional validation"""
636
+ condition = rule.params.get('if')
637
+ then_rule = rule.params.get('then')
638
+ else_rule = rule.params.get('else')
639
+
640
+ if not condition:
641
+ return self._create_result(True)
642
+
643
+ # Evaluate condition
644
+ condition_met = self._evaluate_condition(condition, value, context)
645
+
646
+ # Apply appropriate rule
647
+ if condition_met and then_rule:
648
+ return self._apply_rule(then_rule, value, context)
649
+ elif not condition_met and else_rule:
650
+ return self._apply_rule(else_rule, value, context)
651
+
652
+ return self._create_result(True)
653
+
654
+ def _evaluate_condition(self, condition: Any, value: Any, context: Dict[str, Any]) -> bool:
655
+ """Evaluate condition"""
656
+ if callable(condition):
657
+ return condition(value, context)
658
+
659
+ if isinstance(condition, dict):
660
+ # Field comparison condition
661
+ field = condition.get('field')
662
+ operator = condition.get('operator', '==')
663
+ expected = condition.get('value')
664
+
665
+ config = context.get('config', {})
666
+ actual = config.get(field)
667
+
668
+ return self._compare_values(actual, operator, expected)
669
+
670
+ return bool(condition)
671
+
672
+ def _compare_values(self, actual: Any, operator: str, expected: Any) -> bool:
673
+ """Compare values with operator"""
674
+ operators = {
675
+ '==': lambda a, e: a == e,
676
+ '!=': lambda a, e: a != e,
677
+ '<': lambda a, e: a < e,
678
+ '<=': lambda a, e: a <= e,
679
+ '>': lambda a, e: a > e,
680
+ '>=': lambda a, e: a >= e,
681
+ 'in': lambda a, e: a in e,
682
+ 'not_in': lambda a, e: a not in e,
683
+ 'contains': lambda a, e: e in a,
684
+ 'matches': lambda a, e: re.match(e, str(a)) is not None
685
+ }
686
+
687
+ comparator = operators.get(operator)
688
+ if not comparator:
689
+ return False
690
+
691
+ try:
692
+ return comparator(actual, expected)
693
+ except:
694
+ return False
695
+
696
+ def _apply_rule(self, rule_def: Dict, value: Any, context: Dict[str, Any]) -> ValidationResult:
697
+ """Apply validation rule"""
698
+ # Create and execute rule
699
+ rule = ValidationRule(
700
+ type=ValidationType[rule_def.get('type', 'CUSTOM').upper()],
701
+ params=rule_def.get('params', {}),
702
+ message=rule_def.get('message'),
703
+ severity=rule_def.get('severity', 'error')
704
+ )
705
+
706
+ # Find appropriate validator
707
+ validator = ValidationStrategy().validators.get(rule.type)
708
+ if validator:
709
+ return validator.validate(value, rule, context)
710
+
711
+ return self._create_result(True)
712
+
713
+
714
+ class RecursiveValidator(BaseValidator):
715
+ """Validates nested structures recursively - replaces 10 recursive validation functions"""
716
+
717
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
718
+ """Recursively validate nested structures"""
719
+ schema = rule.params.get('schema', {})
720
+ max_depth = rule.params.get('max_depth', 10)
721
+ current_depth = context.get('_depth', 0)
722
+
723
+ if current_depth >= max_depth:
724
+ return self._create_result(False, f"Maximum recursion depth {max_depth} exceeded", "error")
725
+
726
+ result = ValidationResult(valid=True)
727
+
728
+ if isinstance(value, dict):
729
+ result = self._validate_dict(value, schema, context, current_depth)
730
+ elif isinstance(value, list):
731
+ result = self._validate_list(value, schema, context, current_depth)
732
+ else:
733
+ # Validate single value
734
+ if 'type' in schema:
735
+ type_rule = ValidationRule(
736
+ type=ValidationType.TYPE,
737
+ params={'type': schema['type']}
738
+ )
739
+ type_validator = TypeValidator()
740
+ result = type_validator.validate(value, type_rule, context)
741
+
742
+ return result
743
+
744
+ def _validate_dict(self, value: Dict, schema: Dict, context: Dict, depth: int) -> ValidationResult:
745
+ """Validate dictionary recursively"""
746
+ result = ValidationResult(valid=True)
747
+ properties = schema.get('properties', {})
748
+
749
+ for key, prop_schema in properties.items():
750
+ if key in value:
751
+ # Create context for nested validation
752
+ nested_context = context.copy()
753
+ nested_context['_depth'] = depth + 1
754
+
755
+ # Recursively validate
756
+ nested_rule = ValidationRule(
757
+ type=ValidationType.RECURSIVE,
758
+ params={'schema': prop_schema, 'max_depth': context.get('max_depth', 10)}
759
+ )
760
+
761
+ nested_result = self.validate(value[key], nested_rule, nested_context)
762
+
763
+ if not nested_result.valid:
764
+ result.valid = False
765
+ result.errors.extend([f"{key}.{e}" for e in nested_result.errors])
766
+ result.warnings.extend([f"{key}.{w}" for w in nested_result.warnings])
767
+
768
+ return result
769
+
770
+ def _validate_list(self, value: List, schema: Dict, context: Dict, depth: int) -> ValidationResult:
771
+ """Validate list recursively"""
772
+ result = ValidationResult(valid=True)
773
+ items_schema = schema.get('items', {})
774
+
775
+ for i, item in enumerate(value):
776
+ # Create context for nested validation
777
+ nested_context = context.copy()
778
+ nested_context['_depth'] = depth + 1
779
+
780
+ # Recursively validate
781
+ nested_rule = ValidationRule(
782
+ type=ValidationType.RECURSIVE,
783
+ params={'schema': items_schema, 'max_depth': context.get('max_depth', 10)}
784
+ )
785
+
786
+ nested_result = self.validate(item, nested_rule, nested_context)
787
+
788
+ if not nested_result.valid:
789
+ result.valid = False
790
+ result.errors.extend([f"[{i}].{e}" for e in nested_result.errors])
791
+ result.warnings.extend([f"[{i}].{w}" for w in nested_result.warnings])
792
+
793
+ return result
794
+
795
+
796
+ class CrossFieldValidator(BaseValidator):
797
+ """Validates cross-field constraints - replaces 8 cross-field validation functions"""
798
+
799
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
800
+ """Validate cross-field constraints"""
801
+ config = context.get('config', {})
802
+ constraints = rule.params.get('constraints', [])
803
+
804
+ result = ValidationResult(valid=True)
805
+
806
+ for constraint in constraints:
807
+ if not self._evaluate_constraint(config, constraint):
808
+ message = constraint.get('message', 'Cross-field constraint failed')
809
+ if rule.severity == "error":
810
+ result.errors.append(message)
811
+ elif rule.severity == "warning":
812
+ result.warnings.append(message)
813
+ else:
814
+ result.info.append(message)
815
+ result.valid = False
816
+
817
+ return result
818
+
819
+ def _evaluate_constraint(self, config: Dict, constraint: Dict) -> bool:
820
+ """Evaluate a cross-field constraint"""
821
+ constraint_type = constraint.get('type')
822
+
823
+ if constraint_type == 'mutual_exclusive':
824
+ # Only one of the fields should be present
825
+ fields = constraint.get('fields', [])
826
+ present = [f for f in fields if f in config and config[f] is not None]
827
+ return len(present) <= 1
828
+
829
+ elif constraint_type == 'mutual_required':
830
+ # All fields must be present together
831
+ fields = constraint.get('fields', [])
832
+ present = [f for f in fields if f in config and config[f] is not None]
833
+ return len(present) == 0 or len(present) == len(fields)
834
+
835
+ elif constraint_type == 'sum':
836
+ # Sum of fields must match condition
837
+ fields = constraint.get('fields', [])
838
+ operator = constraint.get('operator', '==')
839
+ target = constraint.get('value', 0)
840
+
841
+ total = sum(config.get(f, 0) for f in fields)
842
+ return self._compare_values(total, operator, target)
843
+
844
+ elif constraint_type == 'comparison':
845
+ # Compare two fields
846
+ field1 = constraint.get('field1')
847
+ field2 = constraint.get('field2')
848
+ operator = constraint.get('operator', '==')
849
+
850
+ if field1 in config and field2 in config:
851
+ return self._compare_values(config[field1], operator, config[field2])
852
+
853
+ elif constraint_type == 'custom':
854
+ # Custom constraint function
855
+ func = constraint.get('function')
856
+ if callable(func):
857
+ return func(config)
858
+
859
+ return True
860
+
861
+ def _compare_values(self, val1: Any, operator: str, val2: Any) -> bool:
862
+ """Compare two values with operator"""
863
+ operators = {
864
+ '==': lambda a, b: a == b,
865
+ '!=': lambda a, b: a != b,
866
+ '<': lambda a, b: a < b,
867
+ '<=': lambda a, b: a <= b,
868
+ '>': lambda a, b: a > b,
869
+ '>=': lambda a, b: a >= b
870
+ }
871
+
872
+ comparator = operators.get(operator)
873
+ if comparator:
874
+ try:
875
+ return comparator(val1, val2)
876
+ except:
877
+ return False
878
+
879
+ return False
880
+
881
+
882
+ class CompositeValidator(BaseValidator):
883
+ """Composes multiple validators - replaces 6 composite validation functions"""
884
+
885
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
886
+ """Apply multiple validators in sequence"""
887
+ validators = rule.params.get('validators', [])
888
+ operator = rule.params.get('operator', 'AND') # AND, OR
889
+
890
+ results = []
891
+ combined_result = ValidationResult(valid=True)
892
+
893
+ # Get validator instances
894
+ strategy = ValidationStrategy()
895
+
896
+ for validator_def in validators:
897
+ # Create rule from definition
898
+ val_rule = ValidationRule(
899
+ type=ValidationType[validator_def.get('type', 'CUSTOM').upper()],
900
+ params=validator_def.get('params', {}),
901
+ message=validator_def.get('message'),
902
+ severity=validator_def.get('severity', rule.severity)
903
+ )
904
+
905
+ # Get validator and execute
906
+ validator = strategy.validators.get(val_rule.type)
907
+ if validator:
908
+ result = validator.validate(value, val_rule, context)
909
+ results.append(result)
910
+
911
+ # Collect all messages
912
+ combined_result.errors.extend(result.errors)
913
+ combined_result.warnings.extend(result.warnings)
914
+ combined_result.info.extend(result.info)
915
+
916
+ # Apply operator logic
917
+ if operator == 'AND':
918
+ combined_result.valid = all(r.valid for r in results)
919
+ elif operator == 'OR':
920
+ combined_result.valid = any(r.valid for r in results)
921
+ if combined_result.valid:
922
+ # Clear errors if any validator passed
923
+ combined_result.errors.clear()
924
+ combined_result.warnings.clear()
925
+
926
+ return combined_result
927
+
928
+
929
+ class SchemaValidator(BaseValidator):
930
+ """Validates against full schema - orchestrates other validators"""
931
+
932
+ def validate(self, value: Any, rule: ValidationRule, context: Dict[str, Any]) -> ValidationResult:
933
+ """Validate against complete schema"""
934
+ schema = rule.params.get('schema', {})
935
+ config = value if isinstance(value, dict) else {'value': value}
936
+
937
+ # Update context
938
+ context = context.copy()
939
+ context['config'] = config
940
+
941
+ result = ValidationResult(valid=True)
942
+ strategy = ValidationStrategy()
943
+
944
+ # Apply each schema validation
945
+ if 'type' in schema:
946
+ type_result = strategy.validate_type(config, schema)
947
+ self._merge_results(result, type_result)
948
+
949
+ if 'required' in schema:
950
+ req_result = strategy.validate_required(config, schema)
951
+ self._merge_results(result, req_result)
952
+
953
+ if 'properties' in schema:
954
+ for key, prop_schema in schema['properties'].items():
955
+ if key in config:
956
+ prop_result = self.validate(
957
+ config[key],
958
+ ValidationRule(
959
+ type=ValidationType.SCHEMA,
960
+ params={'schema': prop_schema}
961
+ ),
962
+ context
963
+ )
964
+ if not prop_result.valid:
965
+ result.valid = False
966
+ result.errors.extend([f"{key}: {e}" for e in prop_result.errors])
967
+
968
+ return result
969
+
970
+ def _merge_results(self, target: ValidationResult, source: ValidationResult):
971
+ """Merge validation results"""
972
+ if not source.valid:
973
+ target.valid = False
974
+ target.errors.extend(source.errors)
975
+ target.warnings.extend(source.warnings)
976
+ target.info.extend(source.info)
977
+
978
+
979
+ class ValidationStrategy(IConfigStrategy):
980
+ """
981
+ Main validation strategy
982
+ Reduces 236 validation functions to 15 composable validators
983
+ """
984
+
985
+ def __init__(self):
986
+ self.logger = get_logger(self.__class__.__name__)
987
+ self.validators = {
988
+ ValidationType.TYPE: TypeValidator(),
989
+ ValidationType.REQUIRED: RequiredValidator(),
990
+ ValidationType.RANGE: RangeValidator(),
991
+ ValidationType.LENGTH: LengthValidator(),
992
+ ValidationType.PATTERN: PatternValidator(),
993
+ ValidationType.ENUM: EnumValidator(),
994
+ ValidationType.FORMAT: FormatValidator(),
995
+ ValidationType.DEPENDENCY: DependencyValidator(),
996
+ ValidationType.UNIQUE: UniqueValidator(),
997
+ ValidationType.CUSTOM: CustomValidator(),
998
+ ValidationType.CONDITIONAL: ConditionalValidator(),
999
+ ValidationType.RECURSIVE: RecursiveValidator(),
1000
+ ValidationType.CROSS_FIELD: CrossFieldValidator(),
1001
+ ValidationType.COMPOSITE: CompositeValidator(),
1002
+ ValidationType.SCHEMA: SchemaValidator()
1003
+ }
1004
+
1005
+ def can_handle(self, source: Union[str, Path, Dict]) -> bool:
1006
+ """Check if this strategy can handle validation"""
1007
+ return isinstance(source, dict)
1008
+
1009
+ def load(self, source: Any, **kwargs) -> Dict[str, Any]:
1010
+ """Not used for validation"""
1011
+ return source if isinstance(source, dict) else {}
1012
+
1013
+ def validate(self, config: Dict[str, Any], schema: Optional[Dict] = None) -> bool:
1014
+ """Main validation entry point"""
1015
+ if not schema:
1016
+ return True
1017
+
1018
+ context = {'config': config}
1019
+ result = self._validate_with_schema(config, schema, context)
1020
+
1021
+ if not result.valid:
1022
+ self.logger.error(f"Validation errors: {result.errors}")
1023
+ if result.warnings:
1024
+ self.logger.warning(f"Validation warnings: {result.warnings}")
1025
+
1026
+ return result.valid
1027
+
1028
+ def transform(self, config: Dict[str, Any]) -> Dict[str, Any]:
1029
+ """Transform config based on schema"""
1030
+ return config
1031
+
1032
+ def _validate_with_schema(self, config: Dict, schema: Dict, context: Dict) -> ValidationResult:
1033
+ """Validate configuration against schema"""
1034
+ # Use schema validator for comprehensive validation
1035
+ schema_rule = ValidationRule(
1036
+ type=ValidationType.SCHEMA,
1037
+ params={'schema': schema}
1038
+ )
1039
+
1040
+ return self.validators[ValidationType.SCHEMA].validate(config, schema_rule, context)
1041
+
1042
+ # Helper methods for direct validation
1043
+ def validate_type(self, config: Dict, schema: Dict) -> ValidationResult:
1044
+ """Validate types in configuration"""
1045
+ result = ValidationResult(valid=True)
1046
+ properties = schema.get('properties', {})
1047
+
1048
+ for key, prop_schema in properties.items():
1049
+ if key in config and 'type' in prop_schema:
1050
+ rule = ValidationRule(
1051
+ type=ValidationType.TYPE,
1052
+ params={'type': prop_schema['type']}
1053
+ )
1054
+ prop_result = self.validators[ValidationType.TYPE].validate(
1055
+ config[key], rule, {'config': config}
1056
+ )
1057
+ if not prop_result.valid:
1058
+ result.valid = False
1059
+ result.errors.extend([f"{key}: {e}" for e in prop_result.errors])
1060
+
1061
+ return result
1062
+
1063
+ def validate_required(self, config: Dict, schema: Dict) -> ValidationResult:
1064
+ """Validate required fields"""
1065
+ required_fields = schema.get('required', [])
1066
+ if required_fields:
1067
+ rule = ValidationRule(
1068
+ type=ValidationType.REQUIRED,
1069
+ params={'fields': required_fields}
1070
+ )
1071
+ return self.validators[ValidationType.REQUIRED].validate(
1072
+ None, rule, {'config': config}
1073
+ )
1074
+
1075
+ return ValidationResult(valid=True)
1076
+
1077
+ def compose_validators(self, *validator_names: str) -> Callable:
1078
+ """Compose multiple validators into a single function"""
1079
+ def composed_validator(config: Dict, schema: Dict) -> ValidationResult:
1080
+ result = ValidationResult(valid=True)
1081
+
1082
+ for name in validator_names:
1083
+ val_type = ValidationType[name.upper()]
1084
+ if val_type in self.validators:
1085
+ validator = self.validators[val_type]
1086
+ rule = ValidationRule(type=val_type, params=schema)
1087
+ val_result = validator.validate(config, rule, {'config': config})
1088
+
1089
+ if not val_result.valid:
1090
+ result.valid = False
1091
+ result.errors.extend(val_result.errors)
1092
+ result.warnings.extend(val_result.warnings)
1093
+
1094
+ return result
1095
+
1096
+ return composed_validator
1097
+
1098
+
1099
+ # Export main components
1100
+ __all__ = [
1101
+ 'ValidationStrategy',
1102
+ 'ValidationRule',
1103
+ 'ValidationResult',
1104
+ 'ValidationType'
1105
+ ]