crackerjack 0.29.0__py3-none-any.whl → 0.31.4__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.

Files changed (158) hide show
  1. crackerjack/CLAUDE.md +1005 -0
  2. crackerjack/RULES.md +380 -0
  3. crackerjack/__init__.py +42 -13
  4. crackerjack/__main__.py +225 -253
  5. crackerjack/agents/__init__.py +41 -0
  6. crackerjack/agents/architect_agent.py +281 -0
  7. crackerjack/agents/base.py +169 -0
  8. crackerjack/agents/coordinator.py +512 -0
  9. crackerjack/agents/documentation_agent.py +498 -0
  10. crackerjack/agents/dry_agent.py +388 -0
  11. crackerjack/agents/formatting_agent.py +245 -0
  12. crackerjack/agents/import_optimization_agent.py +281 -0
  13. crackerjack/agents/performance_agent.py +669 -0
  14. crackerjack/agents/proactive_agent.py +104 -0
  15. crackerjack/agents/refactoring_agent.py +788 -0
  16. crackerjack/agents/security_agent.py +529 -0
  17. crackerjack/agents/test_creation_agent.py +652 -0
  18. crackerjack/agents/test_specialist_agent.py +486 -0
  19. crackerjack/agents/tracker.py +212 -0
  20. crackerjack/api.py +560 -0
  21. crackerjack/cli/__init__.py +24 -0
  22. crackerjack/cli/facade.py +104 -0
  23. crackerjack/cli/handlers.py +267 -0
  24. crackerjack/cli/interactive.py +471 -0
  25. crackerjack/cli/options.py +401 -0
  26. crackerjack/cli/utils.py +18 -0
  27. crackerjack/code_cleaner.py +670 -0
  28. crackerjack/config/__init__.py +19 -0
  29. crackerjack/config/hooks.py +218 -0
  30. crackerjack/core/__init__.py +0 -0
  31. crackerjack/core/async_workflow_orchestrator.py +406 -0
  32. crackerjack/core/autofix_coordinator.py +200 -0
  33. crackerjack/core/container.py +104 -0
  34. crackerjack/core/enhanced_container.py +542 -0
  35. crackerjack/core/performance.py +243 -0
  36. crackerjack/core/phase_coordinator.py +561 -0
  37. crackerjack/core/proactive_workflow.py +316 -0
  38. crackerjack/core/session_coordinator.py +289 -0
  39. crackerjack/core/workflow_orchestrator.py +640 -0
  40. crackerjack/dynamic_config.py +577 -0
  41. crackerjack/errors.py +263 -41
  42. crackerjack/executors/__init__.py +11 -0
  43. crackerjack/executors/async_hook_executor.py +431 -0
  44. crackerjack/executors/cached_hook_executor.py +242 -0
  45. crackerjack/executors/hook_executor.py +345 -0
  46. crackerjack/executors/individual_hook_executor.py +669 -0
  47. crackerjack/intelligence/__init__.py +44 -0
  48. crackerjack/intelligence/adaptive_learning.py +751 -0
  49. crackerjack/intelligence/agent_orchestrator.py +551 -0
  50. crackerjack/intelligence/agent_registry.py +414 -0
  51. crackerjack/intelligence/agent_selector.py +502 -0
  52. crackerjack/intelligence/integration.py +290 -0
  53. crackerjack/interactive.py +576 -315
  54. crackerjack/managers/__init__.py +11 -0
  55. crackerjack/managers/async_hook_manager.py +135 -0
  56. crackerjack/managers/hook_manager.py +137 -0
  57. crackerjack/managers/publish_manager.py +411 -0
  58. crackerjack/managers/test_command_builder.py +151 -0
  59. crackerjack/managers/test_executor.py +435 -0
  60. crackerjack/managers/test_manager.py +258 -0
  61. crackerjack/managers/test_manager_backup.py +1124 -0
  62. crackerjack/managers/test_progress.py +144 -0
  63. crackerjack/mcp/__init__.py +0 -0
  64. crackerjack/mcp/cache.py +336 -0
  65. crackerjack/mcp/client_runner.py +104 -0
  66. crackerjack/mcp/context.py +615 -0
  67. crackerjack/mcp/dashboard.py +636 -0
  68. crackerjack/mcp/enhanced_progress_monitor.py +479 -0
  69. crackerjack/mcp/file_monitor.py +336 -0
  70. crackerjack/mcp/progress_components.py +569 -0
  71. crackerjack/mcp/progress_monitor.py +949 -0
  72. crackerjack/mcp/rate_limiter.py +332 -0
  73. crackerjack/mcp/server.py +22 -0
  74. crackerjack/mcp/server_core.py +244 -0
  75. crackerjack/mcp/service_watchdog.py +501 -0
  76. crackerjack/mcp/state.py +395 -0
  77. crackerjack/mcp/task_manager.py +257 -0
  78. crackerjack/mcp/tools/__init__.py +17 -0
  79. crackerjack/mcp/tools/core_tools.py +249 -0
  80. crackerjack/mcp/tools/error_analyzer.py +308 -0
  81. crackerjack/mcp/tools/execution_tools.py +370 -0
  82. crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
  83. crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
  84. crackerjack/mcp/tools/intelligence_tools.py +314 -0
  85. crackerjack/mcp/tools/monitoring_tools.py +502 -0
  86. crackerjack/mcp/tools/proactive_tools.py +384 -0
  87. crackerjack/mcp/tools/progress_tools.py +141 -0
  88. crackerjack/mcp/tools/utility_tools.py +341 -0
  89. crackerjack/mcp/tools/workflow_executor.py +360 -0
  90. crackerjack/mcp/websocket/__init__.py +14 -0
  91. crackerjack/mcp/websocket/app.py +39 -0
  92. crackerjack/mcp/websocket/endpoints.py +559 -0
  93. crackerjack/mcp/websocket/jobs.py +253 -0
  94. crackerjack/mcp/websocket/server.py +116 -0
  95. crackerjack/mcp/websocket/websocket_handler.py +78 -0
  96. crackerjack/mcp/websocket_server.py +10 -0
  97. crackerjack/models/__init__.py +31 -0
  98. crackerjack/models/config.py +93 -0
  99. crackerjack/models/config_adapter.py +230 -0
  100. crackerjack/models/protocols.py +118 -0
  101. crackerjack/models/task.py +154 -0
  102. crackerjack/monitoring/ai_agent_watchdog.py +450 -0
  103. crackerjack/monitoring/regression_prevention.py +638 -0
  104. crackerjack/orchestration/__init__.py +0 -0
  105. crackerjack/orchestration/advanced_orchestrator.py +970 -0
  106. crackerjack/orchestration/execution_strategies.py +341 -0
  107. crackerjack/orchestration/test_progress_streamer.py +636 -0
  108. crackerjack/plugins/__init__.py +15 -0
  109. crackerjack/plugins/base.py +200 -0
  110. crackerjack/plugins/hooks.py +246 -0
  111. crackerjack/plugins/loader.py +335 -0
  112. crackerjack/plugins/managers.py +259 -0
  113. crackerjack/py313.py +8 -3
  114. crackerjack/services/__init__.py +22 -0
  115. crackerjack/services/cache.py +314 -0
  116. crackerjack/services/config.py +347 -0
  117. crackerjack/services/config_integrity.py +99 -0
  118. crackerjack/services/contextual_ai_assistant.py +516 -0
  119. crackerjack/services/coverage_ratchet.py +347 -0
  120. crackerjack/services/debug.py +736 -0
  121. crackerjack/services/dependency_monitor.py +617 -0
  122. crackerjack/services/enhanced_filesystem.py +439 -0
  123. crackerjack/services/file_hasher.py +151 -0
  124. crackerjack/services/filesystem.py +395 -0
  125. crackerjack/services/git.py +165 -0
  126. crackerjack/services/health_metrics.py +611 -0
  127. crackerjack/services/initialization.py +847 -0
  128. crackerjack/services/log_manager.py +286 -0
  129. crackerjack/services/logging.py +174 -0
  130. crackerjack/services/metrics.py +578 -0
  131. crackerjack/services/pattern_cache.py +362 -0
  132. crackerjack/services/pattern_detector.py +515 -0
  133. crackerjack/services/performance_benchmarks.py +653 -0
  134. crackerjack/services/security.py +163 -0
  135. crackerjack/services/server_manager.py +234 -0
  136. crackerjack/services/smart_scheduling.py +144 -0
  137. crackerjack/services/tool_version_service.py +61 -0
  138. crackerjack/services/unified_config.py +437 -0
  139. crackerjack/services/version_checker.py +248 -0
  140. crackerjack/slash_commands/__init__.py +14 -0
  141. crackerjack/slash_commands/init.md +122 -0
  142. crackerjack/slash_commands/run.md +163 -0
  143. crackerjack/slash_commands/status.md +127 -0
  144. crackerjack-0.31.4.dist-info/METADATA +742 -0
  145. crackerjack-0.31.4.dist-info/RECORD +148 -0
  146. crackerjack-0.31.4.dist-info/entry_points.txt +2 -0
  147. crackerjack/.gitignore +0 -34
  148. crackerjack/.libcst.codemod.yaml +0 -18
  149. crackerjack/.pdm.toml +0 -1
  150. crackerjack/.pre-commit-config-ai.yaml +0 -149
  151. crackerjack/.pre-commit-config-fast.yaml +0 -69
  152. crackerjack/.pre-commit-config.yaml +0 -114
  153. crackerjack/crackerjack.py +0 -4140
  154. crackerjack/pyproject.toml +0 -285
  155. crackerjack-0.29.0.dist-info/METADATA +0 -1289
  156. crackerjack-0.29.0.dist-info/RECORD +0 -17
  157. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/WHEEL +0 -0
  158. {crackerjack-0.29.0.dist-info → crackerjack-0.31.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,653 @@
1
+ import json
2
+ import statistics
3
+ import subprocess
4
+ import time
5
+ import typing as t
6
+ from contextlib import suppress
7
+ from dataclasses import dataclass, field
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ from crackerjack.models.protocols import FileSystemInterface
15
+
16
+
17
+ @dataclass
18
+ class BenchmarkResult:
19
+ name: str
20
+ duration_seconds: float
21
+ memory_usage_mb: float = 0.0
22
+ cpu_percent: float = 0.0
23
+ iterations: int = 1
24
+ metadata: dict[str, Any] = field(default_factory=dict)
25
+
26
+
27
+ @dataclass
28
+ class PerformanceReport:
29
+ total_duration: float
30
+ workflow_benchmarks: list[BenchmarkResult] = field(default_factory=list)
31
+ test_benchmarks: dict[str, Any] = field(default_factory=dict)
32
+ hook_performance: dict[str, float] = field(default_factory=dict)
33
+ file_operation_stats: dict[str, float] = field(default_factory=dict)
34
+ recommendations: list[str] = field(default_factory=list)
35
+ baseline_comparison: dict[str, float] = field(default_factory=dict)
36
+
37
+
38
+ class PerformanceBenchmarkService:
39
+ def __init__(
40
+ self,
41
+ filesystem: FileSystemInterface,
42
+ console: Console | None = None,
43
+ ) -> None:
44
+ self.filesystem = filesystem
45
+ self.console = console or Console()
46
+ self.project_root = Path.cwd()
47
+ self.benchmarks_dir = self.project_root / ".benchmarks"
48
+ self.history_file = self.benchmarks_dir / "performance_history.json"
49
+
50
+ self.benchmarks_dir.mkdir(exist_ok=True)
51
+
52
+ def run_comprehensive_benchmark(
53
+ self,
54
+ run_tests: bool = True,
55
+ run_hooks: bool = True,
56
+ iterations: int = 1,
57
+ ) -> PerformanceReport:
58
+ """Run comprehensive performance benchmark across all components."""
59
+ self.console.print(
60
+ "[cyan]🚀 Starting comprehensive performance benchmark...[/cyan]",
61
+ )
62
+
63
+ start_time = time.time()
64
+ report = self._initialize_performance_report()
65
+
66
+ self._run_requested_benchmarks(report, run_tests, run_hooks, iterations)
67
+ self._finalize_performance_report(report, start_time)
68
+
69
+ return report
70
+
71
+ def _initialize_performance_report(self) -> PerformanceReport:
72
+ """Initialize a new performance report."""
73
+ return PerformanceReport(total_duration=0.0)
74
+
75
+ def _run_requested_benchmarks(
76
+ self,
77
+ report: PerformanceReport,
78
+ run_tests: bool,
79
+ run_hooks: bool,
80
+ iterations: int,
81
+ ) -> None:
82
+ """Run the requested benchmark types."""
83
+ if run_tests:
84
+ report.test_benchmarks = self._benchmark_test_suite(iterations)
85
+
86
+ if run_hooks:
87
+ report.hook_performance = self._benchmark_hooks(iterations)
88
+
89
+ report.workflow_benchmarks = self._benchmark_workflow_components(iterations)
90
+ report.file_operation_stats = self._benchmark_file_operations()
91
+
92
+ def _finalize_performance_report(
93
+ self,
94
+ report: PerformanceReport,
95
+ start_time: float,
96
+ ) -> None:
97
+ """Finalize performance report with analysis and history."""
98
+ report.total_duration = time.time() - start_time
99
+ report.recommendations = self._generate_performance_recommendations(report)
100
+ report.baseline_comparison = self._compare_with_baseline(report)
101
+ self._save_performance_history(report)
102
+
103
+ def _benchmark_test_suite(self, iterations: int = 1) -> dict[str, Any]:
104
+ self.console.print("[dim]📊 Benchmarking test suite...[/dim]")
105
+
106
+ benchmark_results = {}
107
+
108
+ try:
109
+ for i in range(iterations):
110
+ start_time = time.time()
111
+
112
+ result = subprocess.run(
113
+ [
114
+ "uv",
115
+ "run",
116
+ "pytest",
117
+ "--benchmark-only",
118
+ "--benchmark-json=.benchmarks/test_benchmark.json",
119
+ "--tb=no",
120
+ "-q",
121
+ ],
122
+ check=False,
123
+ capture_output=True,
124
+ text=True,
125
+ timeout=300,
126
+ )
127
+
128
+ duration = time.time() - start_time
129
+
130
+ benchmark_file = self.benchmarks_dir / "test_benchmark.json"
131
+ if benchmark_file.exists():
132
+ with benchmark_file.open() as f:
133
+ benchmark_data = json.load(f)
134
+
135
+ benchmark_results[f"iteration_{i + 1}"] = {
136
+ "total_duration": duration,
137
+ "benchmark_data": benchmark_data,
138
+ "success": result.returncode == 0,
139
+ }
140
+ else:
141
+ benchmark_results[f"iteration_{i + 1}"] = {
142
+ "total_duration": duration,
143
+ "success": result.returncode == 0,
144
+ "note": "No benchmark tests found",
145
+ }
146
+
147
+ except subprocess.TimeoutExpired:
148
+ benchmark_results["error"] = "Test benchmarking timed out"
149
+ except Exception as e:
150
+ benchmark_results["error"] = f"Test benchmarking failed: {e}"
151
+
152
+ return benchmark_results
153
+
154
+ def _benchmark_hooks(self, iterations: int = 1) -> dict[str, float]:
155
+ self.console.print("[dim]🔧 Benchmarking hooks performance...[/dim]")
156
+
157
+ hook_performance = {}
158
+
159
+ hooks_to_test = [
160
+ "trailing-whitespace",
161
+ "end-of-file-fixer",
162
+ "ruff-format",
163
+ "ruff-check",
164
+ "gitleaks",
165
+ "pyright",
166
+ "bandit",
167
+ "vulture",
168
+ ]
169
+
170
+ for hook_name in hooks_to_test:
171
+ durations: list[float] = []
172
+
173
+ for _i in range(iterations):
174
+ try:
175
+ start_time = time.time()
176
+ subprocess.run(
177
+ [
178
+ "uv",
179
+ "run",
180
+ "pre-commit",
181
+ "run",
182
+ hook_name,
183
+ "--all-files",
184
+ ],
185
+ check=False,
186
+ capture_output=True,
187
+ text=True,
188
+ timeout=120,
189
+ )
190
+ duration = time.time() - start_time
191
+ durations.append(duration)
192
+ except subprocess.TimeoutExpired:
193
+ durations.append(120.0)
194
+ except Exception:
195
+ durations.append(float("inf"))
196
+
197
+ if durations and all(d != float("inf") for d in durations):
198
+ hook_performance[hook_name] = {
199
+ "mean_duration": statistics.mean(durations),
200
+ "min_duration": min(durations),
201
+ "max_duration": max(durations),
202
+ }
203
+
204
+ return hook_performance
205
+
206
+ def _benchmark_workflow_components(
207
+ self,
208
+ iterations: int = 1,
209
+ ) -> list[BenchmarkResult]:
210
+ self.console.print("[dim]⚙️ Benchmarking workflow components...[/dim]")
211
+
212
+ results = []
213
+
214
+ start_time = time.time()
215
+ python_files = list(self.project_root.rglob("*.py"))
216
+ file_discovery_duration = time.time() - start_time
217
+
218
+ results.append(
219
+ BenchmarkResult(
220
+ name="file_discovery",
221
+ duration_seconds=file_discovery_duration,
222
+ metadata={"files_found": len(python_files)},
223
+ ),
224
+ )
225
+
226
+ start_time = time.time()
227
+ pyproject_path = self.project_root / "pyproject.toml"
228
+ if pyproject_path.exists():
229
+ with suppress(Exception):
230
+ import tomllib
231
+
232
+ with pyproject_path.open("rb") as f:
233
+ tomllib.load(f)
234
+ config_load_duration = time.time() - start_time
235
+
236
+ results.append(
237
+ BenchmarkResult(
238
+ name="config_loading",
239
+ duration_seconds=config_load_duration,
240
+ ),
241
+ )
242
+
243
+ return results
244
+
245
+ def _benchmark_file_operations(self) -> dict[str, float]:
246
+ stats = {}
247
+
248
+ test_files = list(self.project_root.glob("*.py"))[:10]
249
+ if test_files:
250
+ start_time = time.time()
251
+ for file_path in test_files:
252
+ with suppress(Exception):
253
+ file_path.read_text(encoding="utf-8")
254
+ read_duration = time.time() - start_time
255
+ stats["file_read_ops"] = read_duration / len(test_files)
256
+
257
+ return stats
258
+
259
+ def _generate_performance_recommendations(
260
+ self,
261
+ report: PerformanceReport,
262
+ ) -> list[str]:
263
+ """Generate performance recommendations based on benchmark results."""
264
+ recommendations = []
265
+
266
+ self._add_test_suite_recommendations(report, recommendations)
267
+ self._add_hook_performance_recommendations(report, recommendations)
268
+ self._add_component_performance_recommendations(report, recommendations)
269
+ self._add_overall_performance_recommendations(report, recommendations)
270
+
271
+ return recommendations
272
+
273
+ def _add_test_suite_recommendations(
274
+ self,
275
+ report: PerformanceReport,
276
+ recommendations: list[str],
277
+ ) -> None:
278
+ """Add recommendations for test suite performance."""
279
+ if not report.test_benchmarks:
280
+ return
281
+
282
+ for iteration_data in report.test_benchmarks.values():
283
+ if self._is_slow_test_iteration(iteration_data):
284
+ recommendations.append(
285
+ "Consider optimizing test suite - execution time exceeds 1 minute",
286
+ )
287
+ break
288
+
289
+ def _is_slow_test_iteration(self, iteration_data: Any) -> bool:
290
+ """Check if test iteration is slow."""
291
+ return (
292
+ isinstance(iteration_data, dict)
293
+ and iteration_data.get("total_duration", 0) > 60
294
+ )
295
+
296
+ def _add_hook_performance_recommendations(
297
+ self,
298
+ report: PerformanceReport,
299
+ recommendations: list[str],
300
+ ) -> None:
301
+ """Add recommendations for hook performance."""
302
+ slow_hooks = self._identify_slow_hooks(report.hook_performance)
303
+ if slow_hooks:
304
+ recommendations.append(self._format_slow_hooks_message(slow_hooks))
305
+
306
+ def _identify_slow_hooks(
307
+ self,
308
+ hook_performance: dict[str, float],
309
+ ) -> list[tuple[str, float]]:
310
+ """Identify hooks with slow performance."""
311
+ slow_hooks = []
312
+ for hook_name, perf_data in hook_performance.items():
313
+ if isinstance(perf_data, dict):
314
+ mean_duration = perf_data.get("mean_duration", 0)
315
+ if mean_duration > 30:
316
+ slow_hooks.append((hook_name, mean_duration))
317
+ return slow_hooks
318
+
319
+ def _format_slow_hooks_message(self, slow_hooks: list[tuple[str, float]]) -> str:
320
+ """Format message for slow hooks recommendation."""
321
+ hooks_info = ", ".join(f"{h}({d:.1f}s)" for h, d in slow_hooks[:3])
322
+ return (
323
+ f"Slow hooks detected: {hooks_info}. "
324
+ "Consider hook optimization or selective execution."
325
+ )
326
+
327
+ def _add_component_performance_recommendations(
328
+ self,
329
+ report: PerformanceReport,
330
+ recommendations: list[str],
331
+ ) -> None:
332
+ """Add recommendations for component performance."""
333
+ slow_components = self._identify_slow_components(report.workflow_benchmarks)
334
+ if slow_components:
335
+ components_names = ", ".join(c.name for c in slow_components)
336
+ recommendations.append(
337
+ f"Slow workflow components: {components_names}. "
338
+ "Consider caching or optimization.",
339
+ )
340
+
341
+ def _identify_slow_components(
342
+ self,
343
+ workflow_benchmarks: list[BenchmarkResult],
344
+ ) -> list[BenchmarkResult]:
345
+ """Identify slow workflow components."""
346
+ return [b for b in workflow_benchmarks if b.duration_seconds > 5]
347
+
348
+ def _add_overall_performance_recommendations(
349
+ self,
350
+ report: PerformanceReport,
351
+ recommendations: list[str],
352
+ ) -> None:
353
+ """Add recommendations for overall performance."""
354
+ if report.total_duration > 300:
355
+ recommendations.append(
356
+ "Overall workflow execution is slow. Consider enabling --skip-hooks "
357
+ "during development iterations.",
358
+ )
359
+
360
+ def _compare_with_baseline(
361
+ self,
362
+ current_report: PerformanceReport,
363
+ ) -> dict[str, float]:
364
+ """Compare current performance with historical baseline."""
365
+ baseline_comparison = {}
366
+
367
+ try:
368
+ history = self._load_performance_history()
369
+ if not history:
370
+ return baseline_comparison
371
+
372
+ self._add_overall_performance_comparison(
373
+ current_report,
374
+ history,
375
+ baseline_comparison,
376
+ )
377
+ self._add_component_performance_comparison(
378
+ current_report,
379
+ history,
380
+ baseline_comparison,
381
+ )
382
+
383
+ except Exception as e:
384
+ baseline_comparison["error"] = f"Could not load baseline: {e}"
385
+
386
+ return baseline_comparison
387
+
388
+ def _load_performance_history(self) -> list[dict[str, Any]] | None:
389
+ """Load performance history from file."""
390
+ if not self.history_file.exists():
391
+ return None
392
+
393
+ with self.history_file.open() as f:
394
+ history = json.load(f)
395
+
396
+ return history if history and len(history) > 1 else None
397
+
398
+ def _add_overall_performance_comparison(
399
+ self,
400
+ current_report: PerformanceReport,
401
+ history: list[dict[str, Any]],
402
+ comparison: dict[str, Any],
403
+ ) -> None:
404
+ """Add overall performance comparison to baseline."""
405
+ recent_runs = history[-5:]
406
+ baseline_duration = statistics.median(
407
+ [r["total_duration"] for r in recent_runs],
408
+ )
409
+
410
+ performance_change = (
411
+ (current_report.total_duration - baseline_duration) / baseline_duration
412
+ ) * 100
413
+ comparison["overall_performance_change_percent"] = performance_change
414
+
415
+ def _add_component_performance_comparison(
416
+ self,
417
+ current_report: PerformanceReport,
418
+ history: list[dict[str, Any]],
419
+ comparison: dict[str, Any],
420
+ ) -> None:
421
+ """Add component-level performance comparison."""
422
+ recent_runs = history[-5:]
423
+ if not recent_runs:
424
+ return
425
+
426
+ component_durations = recent_runs[-1].get("component_durations", {})
427
+
428
+ for component in current_report.workflow_benchmarks:
429
+ if component.name in component_durations:
430
+ old_duration = component_durations[component.name]
431
+ change = self._calculate_performance_change(
432
+ component.duration_seconds,
433
+ old_duration,
434
+ )
435
+ comparison[f"{component.name}_change_percent"] = change
436
+
437
+ def _calculate_performance_change(
438
+ self,
439
+ current_duration: float,
440
+ old_duration: float,
441
+ ) -> float:
442
+ """Calculate performance change percentage."""
443
+ return ((current_duration - old_duration) / old_duration) * 100
444
+
445
+ def _save_performance_history(self, report: PerformanceReport) -> None:
446
+ try:
447
+ history = []
448
+ if self.history_file.exists():
449
+ with self.history_file.open() as f:
450
+ history = json.load(f)
451
+
452
+ record = {
453
+ "timestamp": time.time(),
454
+ "total_duration": report.total_duration,
455
+ "component_durations": {
456
+ c.name: c.duration_seconds for c in report.workflow_benchmarks
457
+ },
458
+ "hook_durations": {
459
+ hook: (perf["mean_duration"] if isinstance(perf, dict) else perf)
460
+ for hook, perf in report.hook_performance.items()
461
+ },
462
+ "recommendations_count": len(report.recommendations),
463
+ }
464
+
465
+ history.append(record)
466
+
467
+ history = history[-50:]
468
+
469
+ with self.history_file.open("w") as f:
470
+ json.dump(history, f, indent=2)
471
+
472
+ except Exception as e:
473
+ self.console.print(
474
+ f"[yellow]⚠️[/yellow] Could not save performance history: {e}",
475
+ )
476
+
477
+ def display_performance_report(self, report: PerformanceReport) -> None:
478
+ self.console.print("\n[bold cyan]🚀 Performance Benchmark Report[/bold cyan]\n")
479
+
480
+ self._display_overall_stats(report)
481
+ self._display_workflow_components(report)
482
+ self._display_hook_performance(report)
483
+ self._display_baseline_comparison(report)
484
+ self._display_recommendations(report)
485
+
486
+ self.console.print(
487
+ f"\n[dim]📁 Benchmark data saved to: {self.benchmarks_dir}[/dim]",
488
+ )
489
+
490
+ def _display_overall_stats(self, report: PerformanceReport) -> None:
491
+ self.console.print(
492
+ f"[green]⏱️ Total Duration: {report.total_duration:.2f}s[/green]",
493
+ )
494
+
495
+ def _display_workflow_components(self, report: PerformanceReport) -> None:
496
+ if not report.workflow_benchmarks:
497
+ return
498
+
499
+ table = Table(title="Workflow Component Performance")
500
+ table.add_column("Component", style="cyan")
501
+ table.add_column("Duration (s)", style="yellow", justify="right")
502
+ table.add_column("Metadata", style="dim")
503
+
504
+ for benchmark in report.workflow_benchmarks:
505
+ metadata_str = ", ".join(f"{k}={v}" for k, v in benchmark.metadata.items())
506
+ table.add_row(
507
+ benchmark.name,
508
+ f"{benchmark.duration_seconds:.3f}",
509
+ metadata_str,
510
+ )
511
+
512
+ self.console.print(table)
513
+ self.console.print()
514
+
515
+ def _display_hook_performance(self, report: PerformanceReport) -> None:
516
+ if not report.hook_performance:
517
+ return
518
+
519
+ table = Table(title="Hook Performance Analysis")
520
+ table.add_column("Hook", style="cyan")
521
+ table.add_column("Mean (s)", style="yellow", justify="right")
522
+ table.add_column("Min (s)", style="green", justify="right")
523
+ table.add_column("Max (s)", style="red", justify="right")
524
+
525
+ for hook_name, perf_data in report.hook_performance.items():
526
+ if isinstance(perf_data, dict):
527
+ table.add_row(
528
+ hook_name,
529
+ f"{perf_data.get('mean_duration', 0):.2f}",
530
+ f"{perf_data.get('min_duration', 0):.2f}",
531
+ f"{perf_data.get('max_duration', 0):.2f}",
532
+ )
533
+
534
+ self.console.print(table)
535
+ self.console.print()
536
+
537
+ def _display_baseline_comparison(self, report: PerformanceReport) -> None:
538
+ if not report.baseline_comparison:
539
+ return
540
+
541
+ self._print_comparison_header()
542
+ self._print_comparison_metrics(report.baseline_comparison)
543
+ self.console.print()
544
+
545
+ def _print_comparison_header(self) -> None:
546
+ """Print performance comparison header."""
547
+ self.console.print("[bold]📊 Performance Comparison[/bold]")
548
+
549
+ def _print_comparison_metrics(self, baseline_comparison: dict[str, t.Any]) -> None:
550
+ """Print individual comparison metrics with appropriate colors."""
551
+ for metric, value in baseline_comparison.items():
552
+ if isinstance(value, float | int) and "percent" in metric:
553
+ color = "green" if value < 0 else "red" if value > 10 else "yellow"
554
+ direction = "faster" if value < 0 else "slower"
555
+ self.console.print(
556
+ f" {metric}: [{color}]{abs(value):.1f}% {direction}[/{color}]",
557
+ )
558
+
559
+ def _display_recommendations(self, report: PerformanceReport) -> None:
560
+ if report.recommendations:
561
+ self.console.print(
562
+ "[bold yellow]💡 Performance Recommendations[/bold yellow]",
563
+ )
564
+ for i, rec in enumerate(report.recommendations, 1):
565
+ self.console.print(f" {i}. {rec}")
566
+ else:
567
+ self.console.print("[green]✨ No performance issues detected![/green]")
568
+
569
+ def get_performance_trends(self, days: int = 7) -> dict[str, Any]:
570
+ """Get performance trends over specified time period."""
571
+ try:
572
+ recent_history = self._get_recent_history(days)
573
+ if not recent_history:
574
+ return self._handle_insufficient_trend_data()
575
+
576
+ trends = {}
577
+ self._add_duration_trends(recent_history, trends)
578
+ self._add_component_trends(recent_history, trends)
579
+ trends["data_points"] = len(recent_history)
580
+
581
+ return trends
582
+
583
+ except Exception as e:
584
+ return {"error": f"Could not analyze trends: {e}"}
585
+
586
+ def _get_recent_history(self, days: int) -> list[dict[str, Any]] | None:
587
+ """Get recent performance history within specified days."""
588
+ if not self.history_file.exists():
589
+ return None
590
+
591
+ with self.history_file.open() as f:
592
+ history = json.load(f)
593
+
594
+ cutoff_time = time.time() - (days * 86400)
595
+ recent_history = [r for r in history if r.get("timestamp", 0) > cutoff_time]
596
+
597
+ return recent_history if len(recent_history) >= 2 else None
598
+
599
+ def _handle_insufficient_trend_data(self) -> dict[str, str]:
600
+ """Handle cases where insufficient data is available for trend analysis."""
601
+ if not self.history_file.exists():
602
+ return {"error": "No performance history available"}
603
+ return {"error": "Insufficient data for trend analysis"}
604
+
605
+ def _add_duration_trends(
606
+ self, recent_history: list[dict[str, Any]], trends: dict[str, Any]
607
+ ) -> None:
608
+ """Add overall duration trends to results."""
609
+ durations = [r["total_duration"] for r in recent_history]
610
+ trends["duration_trend"] = {
611
+ "current": durations[-1],
612
+ "average": statistics.mean(durations),
613
+ "trend": self._determine_trend_direction(durations),
614
+ }
615
+
616
+ def _add_component_trends(
617
+ self, recent_history: list[dict[str, Any]], trends: dict[str, Any]
618
+ ) -> None:
619
+ """Add component-level trends to results."""
620
+ component_trends = {}
621
+ latest_components = recent_history[-1].get("component_durations", {})
622
+
623
+ for component in latest_components:
624
+ component_durations = self._extract_component_durations(
625
+ recent_history,
626
+ component,
627
+ )
628
+ if len(component_durations) >= 2:
629
+ component_trends[component] = {
630
+ "current": component_durations[-1],
631
+ "average": statistics.mean(component_durations),
632
+ "trend": self._determine_trend_direction(component_durations),
633
+ }
634
+
635
+ trends["component_trends"] = component_trends
636
+
637
+ def _extract_component_durations(
638
+ self,
639
+ recent_history: list[dict[str, Any]],
640
+ component: str,
641
+ ) -> list[float]:
642
+ """Extract duration data for a specific component."""
643
+ return [
644
+ r.get("component_durations", {}).get(component)
645
+ for r in recent_history
646
+ if component in r.get("component_durations", {})
647
+ ]
648
+
649
+ def _determine_trend_direction(self, durations: list[float]) -> str:
650
+ """Determine if trend is improving or degrading."""
651
+ current = durations[-1]
652
+ historical_average = statistics.mean(durations[:-1])
653
+ return "improving" if current < historical_average else "degrading"