crackerjack 0.33.0__py3-none-any.whl → 0.33.2__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.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +4 -13
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +104 -204
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +171 -174
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +17 -16
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +6 -35
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +15 -9
- crackerjack/services/config_merge.py +19 -80
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +27 -25
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +618 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/METADATA +196 -25
- crackerjack-0.33.2.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""ML-based anomaly detection service for quality metrics analysis."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import statistics
|
|
5
|
+
import typing as t
|
|
6
|
+
from collections import defaultdict, deque
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class MetricPoint:
|
|
16
|
+
"""Individual metric data point."""
|
|
17
|
+
|
|
18
|
+
timestamp: datetime
|
|
19
|
+
value: float
|
|
20
|
+
metric_type: str
|
|
21
|
+
metadata: dict[str, t.Any] = field(default_factory=dict[str, t.Any])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class AnomalyDetection:
|
|
26
|
+
"""Anomaly detection result."""
|
|
27
|
+
|
|
28
|
+
timestamp: datetime
|
|
29
|
+
metric_type: str
|
|
30
|
+
value: float
|
|
31
|
+
expected_range: tuple[float, float]
|
|
32
|
+
severity: str # low, medium, high, critical
|
|
33
|
+
confidence: float
|
|
34
|
+
description: str
|
|
35
|
+
metadata: dict[str, t.Any] = field(default_factory=dict[str, t.Any])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class BaselineModel:
|
|
40
|
+
"""Statistical baseline model for a metric."""
|
|
41
|
+
|
|
42
|
+
metric_type: str
|
|
43
|
+
mean: float
|
|
44
|
+
std_dev: float
|
|
45
|
+
min_value: float
|
|
46
|
+
max_value: float
|
|
47
|
+
sample_count: int
|
|
48
|
+
last_updated: datetime
|
|
49
|
+
seasonal_patterns: dict[str, float] = field(default_factory=dict[str, t.Any])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class AnomalyDetector:
|
|
53
|
+
"""ML-based anomaly detection system for quality metrics."""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
baseline_window: int = 100,
|
|
58
|
+
sensitivity: float = 2.0,
|
|
59
|
+
min_samples: int = 10,
|
|
60
|
+
):
|
|
61
|
+
"""Initialize anomaly detector.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
baseline_window: Number of recent samples for baseline calculation
|
|
65
|
+
sensitivity: Standard deviation multiplier for anomaly threshold
|
|
66
|
+
min_samples: Minimum samples required before anomaly detection
|
|
67
|
+
"""
|
|
68
|
+
self.baseline_window = baseline_window
|
|
69
|
+
self.sensitivity = sensitivity
|
|
70
|
+
self.min_samples = min_samples
|
|
71
|
+
|
|
72
|
+
# Data storage
|
|
73
|
+
self.metric_history: dict[str, deque[MetricPoint]] = defaultdict(
|
|
74
|
+
lambda: deque[MetricPoint](maxlen=baseline_window)
|
|
75
|
+
)
|
|
76
|
+
self.baselines: dict[str, BaselineModel] = {}
|
|
77
|
+
self.anomalies: list[AnomalyDetection] = []
|
|
78
|
+
|
|
79
|
+
# Configuration
|
|
80
|
+
self.metric_configs = {
|
|
81
|
+
"test_pass_rate": {"critical_threshold": 0.8, "direction": "both"},
|
|
82
|
+
"coverage_percentage": {"critical_threshold": 0.7, "direction": "down"},
|
|
83
|
+
"complexity_score": {"critical_threshold": 15.0, "direction": "up"},
|
|
84
|
+
"execution_time": {"critical_threshold": 300.0, "direction": "up"},
|
|
85
|
+
"memory_usage": {"critical_threshold": 1024.0, "direction": "up"},
|
|
86
|
+
"error_count": {"critical_threshold": 5.0, "direction": "up"},
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
def add_metric(
|
|
90
|
+
self,
|
|
91
|
+
metric_type: str,
|
|
92
|
+
value: float,
|
|
93
|
+
timestamp: datetime | None = None,
|
|
94
|
+
metadata: dict[str, t.Any] | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Add new metric point and update baseline."""
|
|
97
|
+
if timestamp is None:
|
|
98
|
+
timestamp = datetime.now()
|
|
99
|
+
|
|
100
|
+
point = MetricPoint(
|
|
101
|
+
timestamp=timestamp,
|
|
102
|
+
value=value,
|
|
103
|
+
metric_type=metric_type,
|
|
104
|
+
metadata=metadata or {},
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
self.metric_history[metric_type].append(point)
|
|
108
|
+
|
|
109
|
+
# Update baseline if we have enough samples
|
|
110
|
+
if len(self.metric_history[metric_type]) >= self.min_samples:
|
|
111
|
+
self._update_baseline(metric_type)
|
|
112
|
+
|
|
113
|
+
# Check for anomalies
|
|
114
|
+
anomaly = self._detect_anomaly(point)
|
|
115
|
+
if anomaly:
|
|
116
|
+
self.anomalies.append(anomaly)
|
|
117
|
+
logger.info(f"Anomaly detected: {anomaly.description}")
|
|
118
|
+
|
|
119
|
+
def _update_baseline(self, metric_type: str) -> None:
|
|
120
|
+
"""Update statistical baseline for a metric type."""
|
|
121
|
+
history = list[t.Any](self.metric_history[metric_type])
|
|
122
|
+
values = [point.value for point in history]
|
|
123
|
+
|
|
124
|
+
# Calculate basic statistics
|
|
125
|
+
mean = statistics.mean(values)
|
|
126
|
+
std_dev = statistics.stdev(values) if len(values) > 1 else 0
|
|
127
|
+
min_val = min(values)
|
|
128
|
+
max_val = max(values)
|
|
129
|
+
|
|
130
|
+
# Detect seasonal patterns (hourly, daily)
|
|
131
|
+
seasonal_patterns = self._detect_seasonal_patterns(history)
|
|
132
|
+
|
|
133
|
+
self.baselines[metric_type] = BaselineModel(
|
|
134
|
+
metric_type=metric_type,
|
|
135
|
+
mean=mean,
|
|
136
|
+
std_dev=std_dev,
|
|
137
|
+
min_value=min_val,
|
|
138
|
+
max_value=max_val,
|
|
139
|
+
sample_count=len(values),
|
|
140
|
+
last_updated=datetime.now(),
|
|
141
|
+
seasonal_patterns=seasonal_patterns,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _detect_seasonal_patterns(self, history: list[MetricPoint]) -> dict[str, float]:
|
|
145
|
+
"""Detect seasonal patterns in metric history."""
|
|
146
|
+
patterns: dict[str, float] = {}
|
|
147
|
+
|
|
148
|
+
if len(history) < 24: # Need at least 24 points for pattern detection
|
|
149
|
+
return patterns
|
|
150
|
+
|
|
151
|
+
# Group by hour of day
|
|
152
|
+
hourly_values = defaultdict(list)
|
|
153
|
+
for point in history:
|
|
154
|
+
hour = point.timestamp.hour
|
|
155
|
+
hourly_values[hour].append(point.value)
|
|
156
|
+
|
|
157
|
+
# Calculate hourly averages
|
|
158
|
+
for hour, values in hourly_values.items():
|
|
159
|
+
if len(values) >= 3: # Need at least 3 samples
|
|
160
|
+
patterns[f"hour_{hour}"] = statistics.mean(values)
|
|
161
|
+
|
|
162
|
+
return patterns
|
|
163
|
+
|
|
164
|
+
def _detect_anomaly(self, point: MetricPoint) -> AnomalyDetection | None:
|
|
165
|
+
"""Detect if a metric point is anomalous."""
|
|
166
|
+
metric_type = point.metric_type
|
|
167
|
+
baseline = self.baselines.get(metric_type)
|
|
168
|
+
|
|
169
|
+
if not baseline:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
# Calculate expected range
|
|
173
|
+
lower_bound = baseline.mean - (self.sensitivity * baseline.std_dev)
|
|
174
|
+
upper_bound = baseline.mean + (self.sensitivity * baseline.std_dev)
|
|
175
|
+
|
|
176
|
+
# Apply seasonal adjustment if available
|
|
177
|
+
seasonal_adjustment = self._get_seasonal_adjustment(point, baseline)
|
|
178
|
+
if seasonal_adjustment:
|
|
179
|
+
lower_bound += seasonal_adjustment
|
|
180
|
+
upper_bound += seasonal_adjustment
|
|
181
|
+
|
|
182
|
+
# Check for anomaly
|
|
183
|
+
is_anomaly = point.value < lower_bound or point.value > upper_bound
|
|
184
|
+
|
|
185
|
+
if not is_anomaly:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
# Determine severity
|
|
189
|
+
severity = self._calculate_severity(point, baseline, lower_bound, upper_bound)
|
|
190
|
+
|
|
191
|
+
# Calculate confidence
|
|
192
|
+
confidence = self._calculate_confidence(point, baseline)
|
|
193
|
+
|
|
194
|
+
# Generate description
|
|
195
|
+
description = self._generate_anomaly_description(
|
|
196
|
+
point, baseline, lower_bound, upper_bound, severity
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return AnomalyDetection(
|
|
200
|
+
timestamp=point.timestamp,
|
|
201
|
+
metric_type=metric_type,
|
|
202
|
+
value=point.value,
|
|
203
|
+
expected_range=(lower_bound, upper_bound),
|
|
204
|
+
severity=severity,
|
|
205
|
+
confidence=confidence,
|
|
206
|
+
description=description,
|
|
207
|
+
metadata=point.metadata,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _get_seasonal_adjustment(
|
|
211
|
+
self, point: MetricPoint, baseline: BaselineModel
|
|
212
|
+
) -> float:
|
|
213
|
+
"""Get seasonal adjustment for the current time."""
|
|
214
|
+
hour = point.timestamp.hour
|
|
215
|
+
hour_pattern = baseline.seasonal_patterns.get(f"hour_{hour}")
|
|
216
|
+
|
|
217
|
+
if hour_pattern is not None:
|
|
218
|
+
return hour_pattern - baseline.mean
|
|
219
|
+
|
|
220
|
+
return 0.0
|
|
221
|
+
|
|
222
|
+
def _calculate_severity(
|
|
223
|
+
self,
|
|
224
|
+
point: MetricPoint,
|
|
225
|
+
baseline: BaselineModel,
|
|
226
|
+
lower_bound: float,
|
|
227
|
+
upper_bound: float,
|
|
228
|
+
) -> str:
|
|
229
|
+
"""Calculate anomaly severity based on deviation magnitude."""
|
|
230
|
+
if baseline.std_dev == 0:
|
|
231
|
+
return "medium"
|
|
232
|
+
|
|
233
|
+
# Check for critical threshold breaches first
|
|
234
|
+
if self._is_critical_threshold_breached(point):
|
|
235
|
+
return "critical"
|
|
236
|
+
|
|
237
|
+
# Calculate z-score and map to severity
|
|
238
|
+
z_score = self._calculate_z_score(point, baseline, lower_bound, upper_bound)
|
|
239
|
+
return self._severity_from_z_score(z_score)
|
|
240
|
+
|
|
241
|
+
def _is_critical_threshold_breached(self, point: MetricPoint) -> bool:
|
|
242
|
+
"""Check if point breaches critical thresholds."""
|
|
243
|
+
config = self.metric_configs.get(point.metric_type, {})
|
|
244
|
+
critical_threshold = config.get("critical_threshold")
|
|
245
|
+
|
|
246
|
+
if not critical_threshold:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
direction = config.get("direction", "both")
|
|
250
|
+
threshold_float: float = (
|
|
251
|
+
float(str(critical_threshold)) if critical_threshold is not None else 0.0
|
|
252
|
+
)
|
|
253
|
+
return self._threshold_breached_in_direction(
|
|
254
|
+
point.value, threshold_float, str(direction)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
def _threshold_breached_in_direction(
|
|
258
|
+
self, value: float, threshold: float, direction: str
|
|
259
|
+
) -> bool:
|
|
260
|
+
"""Check if value breaches threshold in specified direction."""
|
|
261
|
+
if direction == "up":
|
|
262
|
+
return value > threshold
|
|
263
|
+
elif direction == "down":
|
|
264
|
+
return value < threshold
|
|
265
|
+
elif direction == "both":
|
|
266
|
+
return value > threshold or value < -threshold
|
|
267
|
+
return False
|
|
268
|
+
|
|
269
|
+
def _calculate_z_score(
|
|
270
|
+
self,
|
|
271
|
+
point: MetricPoint,
|
|
272
|
+
baseline: BaselineModel,
|
|
273
|
+
lower_bound: float,
|
|
274
|
+
upper_bound: float,
|
|
275
|
+
) -> float:
|
|
276
|
+
"""Calculate z-score for the point."""
|
|
277
|
+
deviation = min(abs(point.value - lower_bound), abs(point.value - upper_bound))
|
|
278
|
+
return deviation / baseline.std_dev
|
|
279
|
+
|
|
280
|
+
def _severity_from_z_score(self, z_score: float) -> str:
|
|
281
|
+
"""Map z-score to severity level."""
|
|
282
|
+
if z_score > 4:
|
|
283
|
+
return "critical"
|
|
284
|
+
elif z_score > 3:
|
|
285
|
+
return "high"
|
|
286
|
+
elif z_score > 2:
|
|
287
|
+
return "medium"
|
|
288
|
+
return "low"
|
|
289
|
+
|
|
290
|
+
def _calculate_confidence(
|
|
291
|
+
self, point: MetricPoint, baseline: BaselineModel
|
|
292
|
+
) -> float:
|
|
293
|
+
"""Calculate confidence in anomaly detection."""
|
|
294
|
+
# Base confidence on sample size and consistency
|
|
295
|
+
sample_factor = min(baseline.sample_count / 50, 1.0) # Max at 50 samples
|
|
296
|
+
|
|
297
|
+
# Factor in standard deviation consistency
|
|
298
|
+
if baseline.std_dev == 0:
|
|
299
|
+
std_factor = 0.5 # Low confidence for constant values
|
|
300
|
+
else:
|
|
301
|
+
# Higher confidence for more consistent baselines
|
|
302
|
+
cv = baseline.std_dev / abs(baseline.mean) if baseline.mean != 0 else 1
|
|
303
|
+
std_factor = max(0.1, min(1.0, 1.0 - cv))
|
|
304
|
+
|
|
305
|
+
return sample_factor * std_factor
|
|
306
|
+
|
|
307
|
+
def _generate_anomaly_description(
|
|
308
|
+
self,
|
|
309
|
+
point: MetricPoint,
|
|
310
|
+
baseline: BaselineModel,
|
|
311
|
+
lower_bound: float,
|
|
312
|
+
upper_bound: float,
|
|
313
|
+
severity: str,
|
|
314
|
+
) -> str:
|
|
315
|
+
"""Generate human-readable anomaly description."""
|
|
316
|
+
direction = "above" if point.value > upper_bound else "below"
|
|
317
|
+
expected_range = f"{lower_bound:.2f}-{upper_bound:.2f}"
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
f"{severity.title()} anomaly in {point.metric_type}: "
|
|
321
|
+
f"value {point.value:.2f} is {direction} expected range "
|
|
322
|
+
f"{expected_range} (baseline: {baseline.mean:.2f})"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
def get_anomalies(
|
|
326
|
+
self,
|
|
327
|
+
metric_type: str | None = None,
|
|
328
|
+
severity: str | None = None,
|
|
329
|
+
since: datetime | None = None,
|
|
330
|
+
limit: int = 100,
|
|
331
|
+
) -> list[AnomalyDetection]:
|
|
332
|
+
"""Get filtered anomalies."""
|
|
333
|
+
anomalies = self.anomalies
|
|
334
|
+
|
|
335
|
+
# Apply filters
|
|
336
|
+
if metric_type:
|
|
337
|
+
anomalies = [a for a in anomalies if a.metric_type == metric_type]
|
|
338
|
+
|
|
339
|
+
if severity:
|
|
340
|
+
anomalies = [a for a in anomalies if a.severity == severity]
|
|
341
|
+
|
|
342
|
+
if since:
|
|
343
|
+
anomalies = [a for a in anomalies if a.timestamp >= since]
|
|
344
|
+
|
|
345
|
+
# Sort by timestamp (newest first) and limit
|
|
346
|
+
anomalies.sort(key=lambda x: x.timestamp, reverse=True)
|
|
347
|
+
return anomalies[:limit]
|
|
348
|
+
|
|
349
|
+
def get_baseline_summary(self) -> dict[str, dict[str, t.Any]]:
|
|
350
|
+
"""Get summary of all baseline models."""
|
|
351
|
+
summary = {}
|
|
352
|
+
|
|
353
|
+
for metric_type, baseline in self.baselines.items():
|
|
354
|
+
summary[metric_type] = {
|
|
355
|
+
"mean": baseline.mean,
|
|
356
|
+
"std_dev": baseline.std_dev,
|
|
357
|
+
"range": (baseline.min_value, baseline.max_value),
|
|
358
|
+
"sample_count": baseline.sample_count,
|
|
359
|
+
"last_updated": baseline.last_updated.isoformat(),
|
|
360
|
+
"seasonal_patterns": len(baseline.seasonal_patterns),
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return summary
|
|
364
|
+
|
|
365
|
+
def export_model(self, output_path: str | Path) -> None:
|
|
366
|
+
"""Export anomaly detection model for persistence."""
|
|
367
|
+
import json
|
|
368
|
+
|
|
369
|
+
model_data = {
|
|
370
|
+
"baselines": {
|
|
371
|
+
metric_type: {
|
|
372
|
+
"metric_type": baseline.metric_type,
|
|
373
|
+
"mean": baseline.mean,
|
|
374
|
+
"std_dev": baseline.std_dev,
|
|
375
|
+
"min_value": baseline.min_value,
|
|
376
|
+
"max_value": baseline.max_value,
|
|
377
|
+
"sample_count": baseline.sample_count,
|
|
378
|
+
"last_updated": baseline.last_updated.isoformat(),
|
|
379
|
+
"seasonal_patterns": baseline.seasonal_patterns,
|
|
380
|
+
}
|
|
381
|
+
for metric_type, baseline in self.baselines.items()
|
|
382
|
+
},
|
|
383
|
+
"config": {
|
|
384
|
+
"baseline_window": self.baseline_window,
|
|
385
|
+
"sensitivity": self.sensitivity,
|
|
386
|
+
"min_samples": self.min_samples,
|
|
387
|
+
},
|
|
388
|
+
"exported_at": datetime.now().isoformat(),
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
392
|
+
json.dump(model_data, f, indent=2)
|