crackerjack 0.30.3__py3-none-any.whl → 0.31.7__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/CLAUDE.md +1005 -0
- crackerjack/RULES.md +380 -0
- crackerjack/__init__.py +42 -13
- crackerjack/__main__.py +227 -299
- crackerjack/agents/__init__.py +41 -0
- crackerjack/agents/architect_agent.py +281 -0
- crackerjack/agents/base.py +170 -0
- crackerjack/agents/coordinator.py +512 -0
- crackerjack/agents/documentation_agent.py +498 -0
- crackerjack/agents/dry_agent.py +388 -0
- crackerjack/agents/formatting_agent.py +245 -0
- crackerjack/agents/import_optimization_agent.py +281 -0
- crackerjack/agents/performance_agent.py +669 -0
- crackerjack/agents/proactive_agent.py +104 -0
- crackerjack/agents/refactoring_agent.py +788 -0
- crackerjack/agents/security_agent.py +529 -0
- crackerjack/agents/test_creation_agent.py +657 -0
- crackerjack/agents/test_specialist_agent.py +486 -0
- crackerjack/agents/tracker.py +212 -0
- crackerjack/api.py +560 -0
- crackerjack/cli/__init__.py +24 -0
- crackerjack/cli/facade.py +104 -0
- crackerjack/cli/handlers.py +267 -0
- crackerjack/cli/interactive.py +471 -0
- crackerjack/cli/options.py +409 -0
- crackerjack/cli/utils.py +18 -0
- crackerjack/code_cleaner.py +618 -928
- crackerjack/config/__init__.py +19 -0
- crackerjack/config/hooks.py +218 -0
- crackerjack/core/__init__.py +0 -0
- crackerjack/core/async_workflow_orchestrator.py +406 -0
- crackerjack/core/autofix_coordinator.py +200 -0
- crackerjack/core/container.py +104 -0
- crackerjack/core/enhanced_container.py +542 -0
- crackerjack/core/performance.py +243 -0
- crackerjack/core/phase_coordinator.py +585 -0
- crackerjack/core/proactive_workflow.py +316 -0
- crackerjack/core/session_coordinator.py +289 -0
- crackerjack/core/workflow_orchestrator.py +826 -0
- crackerjack/dynamic_config.py +94 -103
- crackerjack/errors.py +263 -41
- crackerjack/executors/__init__.py +11 -0
- crackerjack/executors/async_hook_executor.py +431 -0
- crackerjack/executors/cached_hook_executor.py +242 -0
- crackerjack/executors/hook_executor.py +345 -0
- crackerjack/executors/individual_hook_executor.py +669 -0
- crackerjack/intelligence/__init__.py +44 -0
- crackerjack/intelligence/adaptive_learning.py +751 -0
- crackerjack/intelligence/agent_orchestrator.py +551 -0
- crackerjack/intelligence/agent_registry.py +414 -0
- crackerjack/intelligence/agent_selector.py +502 -0
- crackerjack/intelligence/integration.py +290 -0
- crackerjack/interactive.py +576 -315
- crackerjack/managers/__init__.py +11 -0
- crackerjack/managers/async_hook_manager.py +135 -0
- crackerjack/managers/hook_manager.py +137 -0
- crackerjack/managers/publish_manager.py +433 -0
- crackerjack/managers/test_command_builder.py +151 -0
- crackerjack/managers/test_executor.py +443 -0
- crackerjack/managers/test_manager.py +258 -0
- crackerjack/managers/test_manager_backup.py +1124 -0
- crackerjack/managers/test_progress.py +114 -0
- crackerjack/mcp/__init__.py +0 -0
- crackerjack/mcp/cache.py +336 -0
- crackerjack/mcp/client_runner.py +104 -0
- crackerjack/mcp/context.py +621 -0
- crackerjack/mcp/dashboard.py +636 -0
- crackerjack/mcp/enhanced_progress_monitor.py +479 -0
- crackerjack/mcp/file_monitor.py +336 -0
- crackerjack/mcp/progress_components.py +569 -0
- crackerjack/mcp/progress_monitor.py +949 -0
- crackerjack/mcp/rate_limiter.py +332 -0
- crackerjack/mcp/server.py +22 -0
- crackerjack/mcp/server_core.py +244 -0
- crackerjack/mcp/service_watchdog.py +501 -0
- crackerjack/mcp/state.py +395 -0
- crackerjack/mcp/task_manager.py +257 -0
- crackerjack/mcp/tools/__init__.py +17 -0
- crackerjack/mcp/tools/core_tools.py +249 -0
- crackerjack/mcp/tools/error_analyzer.py +308 -0
- crackerjack/mcp/tools/execution_tools.py +372 -0
- crackerjack/mcp/tools/execution_tools_backup.py +1097 -0
- crackerjack/mcp/tools/intelligence_tool_registry.py +80 -0
- crackerjack/mcp/tools/intelligence_tools.py +314 -0
- crackerjack/mcp/tools/monitoring_tools.py +502 -0
- crackerjack/mcp/tools/proactive_tools.py +384 -0
- crackerjack/mcp/tools/progress_tools.py +217 -0
- crackerjack/mcp/tools/utility_tools.py +341 -0
- crackerjack/mcp/tools/workflow_executor.py +565 -0
- crackerjack/mcp/websocket/__init__.py +14 -0
- crackerjack/mcp/websocket/app.py +39 -0
- crackerjack/mcp/websocket/endpoints.py +559 -0
- crackerjack/mcp/websocket/jobs.py +253 -0
- crackerjack/mcp/websocket/server.py +116 -0
- crackerjack/mcp/websocket/websocket_handler.py +78 -0
- crackerjack/mcp/websocket_server.py +10 -0
- crackerjack/models/__init__.py +31 -0
- crackerjack/models/config.py +93 -0
- crackerjack/models/config_adapter.py +230 -0
- crackerjack/models/protocols.py +118 -0
- crackerjack/models/task.py +154 -0
- crackerjack/monitoring/ai_agent_watchdog.py +450 -0
- crackerjack/monitoring/regression_prevention.py +638 -0
- crackerjack/orchestration/__init__.py +0 -0
- crackerjack/orchestration/advanced_orchestrator.py +970 -0
- crackerjack/orchestration/coverage_improvement.py +223 -0
- crackerjack/orchestration/execution_strategies.py +341 -0
- crackerjack/orchestration/test_progress_streamer.py +636 -0
- crackerjack/plugins/__init__.py +15 -0
- crackerjack/plugins/base.py +200 -0
- crackerjack/plugins/hooks.py +246 -0
- crackerjack/plugins/loader.py +335 -0
- crackerjack/plugins/managers.py +259 -0
- crackerjack/py313.py +8 -3
- crackerjack/services/__init__.py +22 -0
- crackerjack/services/cache.py +314 -0
- crackerjack/services/config.py +358 -0
- crackerjack/services/config_integrity.py +99 -0
- crackerjack/services/contextual_ai_assistant.py +516 -0
- crackerjack/services/coverage_ratchet.py +356 -0
- crackerjack/services/debug.py +736 -0
- crackerjack/services/dependency_monitor.py +617 -0
- crackerjack/services/enhanced_filesystem.py +439 -0
- crackerjack/services/file_hasher.py +151 -0
- crackerjack/services/filesystem.py +421 -0
- crackerjack/services/git.py +176 -0
- crackerjack/services/health_metrics.py +611 -0
- crackerjack/services/initialization.py +873 -0
- crackerjack/services/log_manager.py +286 -0
- crackerjack/services/logging.py +174 -0
- crackerjack/services/metrics.py +578 -0
- crackerjack/services/pattern_cache.py +362 -0
- crackerjack/services/pattern_detector.py +515 -0
- crackerjack/services/performance_benchmarks.py +653 -0
- crackerjack/services/security.py +163 -0
- crackerjack/services/server_manager.py +234 -0
- crackerjack/services/smart_scheduling.py +144 -0
- crackerjack/services/tool_version_service.py +61 -0
- crackerjack/services/unified_config.py +437 -0
- crackerjack/services/version_checker.py +248 -0
- crackerjack/slash_commands/__init__.py +14 -0
- crackerjack/slash_commands/init.md +122 -0
- crackerjack/slash_commands/run.md +163 -0
- crackerjack/slash_commands/status.md +127 -0
- crackerjack-0.31.7.dist-info/METADATA +742 -0
- crackerjack-0.31.7.dist-info/RECORD +149 -0
- crackerjack-0.31.7.dist-info/entry_points.txt +2 -0
- crackerjack/.gitignore +0 -34
- crackerjack/.libcst.codemod.yaml +0 -18
- crackerjack/.pdm.toml +0 -1
- crackerjack/crackerjack.py +0 -3805
- crackerjack/pyproject.toml +0 -286
- crackerjack-0.30.3.dist-info/METADATA +0 -1290
- crackerjack-0.30.3.dist-info/RECORD +0 -16
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/WHEEL +0 -0
- {crackerjack-0.30.3.dist-info → crackerjack-0.31.7.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
import typing as t
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
|
|
10
|
+
from crackerjack.models.task import HookResult
|
|
11
|
+
from crackerjack.services.logging import LoggingContext, get_logger
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AsyncHookExecutionResult:
|
|
16
|
+
strategy_name: str
|
|
17
|
+
results: list[HookResult]
|
|
18
|
+
total_duration: float
|
|
19
|
+
success: bool
|
|
20
|
+
concurrent_execution: bool = True
|
|
21
|
+
cache_hits: int = 0
|
|
22
|
+
cache_misses: int = 0
|
|
23
|
+
performance_gain: float = 0.0
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def failed_count(self) -> int:
|
|
27
|
+
return sum(1 for r in self.results if r.status == "failed")
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def passed_count(self) -> int:
|
|
31
|
+
return sum(1 for r in self.results if r.status == "passed")
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def cache_hit_rate(self) -> float:
|
|
35
|
+
total_requests = self.cache_hits + self.cache_misses
|
|
36
|
+
return (self.cache_hits / total_requests * 100) if total_requests > 0 else 0.0
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def performance_summary(self) -> dict[str, t.Any]:
|
|
40
|
+
return {
|
|
41
|
+
"total_hooks": len(self.results),
|
|
42
|
+
"passed": self.passed_count,
|
|
43
|
+
"failed": self.failed_count,
|
|
44
|
+
"duration_seconds": round(self.total_duration, 2),
|
|
45
|
+
"concurrent": self.concurrent_execution,
|
|
46
|
+
"cache_hits": self.cache_hits,
|
|
47
|
+
"cache_misses": self.cache_misses,
|
|
48
|
+
"cache_hit_rate_percent": round(self.cache_hit_rate, 1),
|
|
49
|
+
"performance_gain_percent": round(self.performance_gain, 1),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class AsyncHookExecutor:
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
console: Console,
|
|
57
|
+
pkg_path: Path,
|
|
58
|
+
max_concurrent: int = 4,
|
|
59
|
+
timeout: int = 300,
|
|
60
|
+
quiet: bool = False,
|
|
61
|
+
) -> None:
|
|
62
|
+
self.console = console
|
|
63
|
+
self.pkg_path = pkg_path
|
|
64
|
+
self.max_concurrent = max_concurrent
|
|
65
|
+
self.timeout = timeout
|
|
66
|
+
self.quiet = quiet
|
|
67
|
+
self.logger = get_logger("crackerjack.async_hook_executor")
|
|
68
|
+
|
|
69
|
+
self._semaphore = asyncio.Semaphore(max_concurrent)
|
|
70
|
+
|
|
71
|
+
async def execute_strategy(
|
|
72
|
+
self,
|
|
73
|
+
strategy: HookStrategy,
|
|
74
|
+
) -> AsyncHookExecutionResult:
|
|
75
|
+
with LoggingContext(
|
|
76
|
+
"async_hook_strategy",
|
|
77
|
+
strategy_name=strategy.name,
|
|
78
|
+
hook_count=len(strategy.hooks),
|
|
79
|
+
):
|
|
80
|
+
start_time = time.time()
|
|
81
|
+
self.logger.info(
|
|
82
|
+
"Starting async hook strategy execution",
|
|
83
|
+
strategy=strategy.name,
|
|
84
|
+
hooks=len(strategy.hooks),
|
|
85
|
+
parallel=strategy.parallel,
|
|
86
|
+
max_workers=getattr(strategy, "max_workers", self.max_concurrent),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
self._print_strategy_header(strategy)
|
|
90
|
+
|
|
91
|
+
estimated_sequential = sum(
|
|
92
|
+
getattr(hook, "timeout", 30) for hook in strategy.hooks
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if strategy.parallel and len(strategy.hooks) > 1:
|
|
96
|
+
results = await self._execute_parallel(strategy)
|
|
97
|
+
else:
|
|
98
|
+
results = await self._execute_sequential(strategy)
|
|
99
|
+
|
|
100
|
+
if strategy.retry_policy != RetryPolicy.NONE:
|
|
101
|
+
results = await self._handle_retries(strategy, results)
|
|
102
|
+
|
|
103
|
+
total_duration = time.time() - start_time
|
|
104
|
+
success = all(r.status == "passed" for r in results)
|
|
105
|
+
performance_gain = max(
|
|
106
|
+
0,
|
|
107
|
+
((estimated_sequential - total_duration) / estimated_sequential) * 100,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self.logger.info(
|
|
111
|
+
"Async hook strategy completed",
|
|
112
|
+
strategy=strategy.name,
|
|
113
|
+
success=success,
|
|
114
|
+
duration_seconds=round(total_duration, 2),
|
|
115
|
+
performance_gain_percent=round(performance_gain, 1),
|
|
116
|
+
passed=sum(1 for r in results if r.status == "passed"),
|
|
117
|
+
failed=sum(1 for r in results if r.status == "failed"),
|
|
118
|
+
errors=sum(1 for r in results if r.status in ("timeout", "error")),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
if not self.quiet:
|
|
122
|
+
self._print_summary(strategy, results, success, performance_gain)
|
|
123
|
+
|
|
124
|
+
return AsyncHookExecutionResult(
|
|
125
|
+
strategy_name=strategy.name,
|
|
126
|
+
results=results,
|
|
127
|
+
total_duration=total_duration,
|
|
128
|
+
success=success,
|
|
129
|
+
performance_gain=performance_gain,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _print_strategy_header(self, strategy: HookStrategy) -> None:
|
|
133
|
+
self.console.print("\n" + "-" * 80)
|
|
134
|
+
if strategy.name == "fast":
|
|
135
|
+
self.console.print(
|
|
136
|
+
"[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running code quality checks (async)[/bold bright_white]",
|
|
137
|
+
)
|
|
138
|
+
elif strategy.name == "comprehensive":
|
|
139
|
+
self.console.print(
|
|
140
|
+
"[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running comprehensive quality checks (async)[/bold bright_white]",
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
self.console.print(
|
|
144
|
+
f"[bold bright_cyan]🔍 HOOKS[/bold bright_cyan] [bold bright_white]Running {strategy.name} hooks (async)[/bold bright_white]",
|
|
145
|
+
)
|
|
146
|
+
self.console.print("-" * 80 + "\n")
|
|
147
|
+
|
|
148
|
+
async def _execute_sequential(self, strategy: HookStrategy) -> list[HookResult]:
|
|
149
|
+
results: list[HookResult] = []
|
|
150
|
+
for hook in strategy.hooks:
|
|
151
|
+
result = await self._execute_single_hook(hook)
|
|
152
|
+
results.append(result)
|
|
153
|
+
self._display_hook_result(result)
|
|
154
|
+
return results
|
|
155
|
+
|
|
156
|
+
async def _execute_parallel(self, strategy: HookStrategy) -> list[HookResult]:
|
|
157
|
+
results: list[HookResult] = []
|
|
158
|
+
|
|
159
|
+
formatting_hooks = [
|
|
160
|
+
h for h in strategy.hooks if getattr(h, "is_formatting", False)
|
|
161
|
+
]
|
|
162
|
+
other_hooks = [
|
|
163
|
+
h for h in strategy.hooks if not getattr(h, "is_formatting", False)
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
for hook in formatting_hooks:
|
|
167
|
+
result = await self._execute_single_hook(hook)
|
|
168
|
+
results.append(result)
|
|
169
|
+
self._display_hook_result(result)
|
|
170
|
+
|
|
171
|
+
if other_hooks:
|
|
172
|
+
tasks = [self._execute_single_hook(hook) for hook in other_hooks]
|
|
173
|
+
parallel_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
174
|
+
|
|
175
|
+
for i, result in enumerate(parallel_results):
|
|
176
|
+
if isinstance(result, Exception):
|
|
177
|
+
hook = other_hooks[i]
|
|
178
|
+
error_result = HookResult(
|
|
179
|
+
id=getattr(hook, "name", f"hook_{i}"),
|
|
180
|
+
name=getattr(hook, "name", f"hook_{i}"),
|
|
181
|
+
status="error",
|
|
182
|
+
duration=0.0,
|
|
183
|
+
issues_found=[str(result)],
|
|
184
|
+
stage=hook.stage.value,
|
|
185
|
+
)
|
|
186
|
+
results.append(error_result)
|
|
187
|
+
self._display_hook_result(error_result)
|
|
188
|
+
else:
|
|
189
|
+
hook_result = t.cast("HookResult", result)
|
|
190
|
+
results.append(hook_result)
|
|
191
|
+
self._display_hook_result(hook_result)
|
|
192
|
+
|
|
193
|
+
return results
|
|
194
|
+
|
|
195
|
+
async def _execute_single_hook(self, hook: HookDefinition) -> HookResult:
|
|
196
|
+
async with self._semaphore:
|
|
197
|
+
return await self._run_hook_subprocess(hook)
|
|
198
|
+
|
|
199
|
+
async def _run_hook_subprocess(self, hook: HookDefinition) -> HookResult:
|
|
200
|
+
start_time = time.time()
|
|
201
|
+
|
|
202
|
+
try:
|
|
203
|
+
cmd = hook.get_command() if hasattr(hook, "get_command") else [str(hook)]
|
|
204
|
+
timeout_val = getattr(hook, "timeout", self.timeout)
|
|
205
|
+
|
|
206
|
+
self.logger.debug(
|
|
207
|
+
"Starting hook execution",
|
|
208
|
+
hook=hook.name,
|
|
209
|
+
command=" ".join(cmd),
|
|
210
|
+
timeout=timeout_val,
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
process = await asyncio.create_subprocess_exec(
|
|
214
|
+
*cmd,
|
|
215
|
+
cwd=self.pkg_path,
|
|
216
|
+
stdout=asyncio.subprocess.PIPE,
|
|
217
|
+
stderr=asyncio.subprocess.PIPE,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
stdout, stderr = await asyncio.wait_for(
|
|
222
|
+
process.communicate(),
|
|
223
|
+
timeout=timeout_val,
|
|
224
|
+
)
|
|
225
|
+
except TimeoutError:
|
|
226
|
+
process.kill()
|
|
227
|
+
await process.wait()
|
|
228
|
+
duration = time.time() - start_time
|
|
229
|
+
|
|
230
|
+
self.logger.warning(
|
|
231
|
+
"Hook execution timed out",
|
|
232
|
+
hook=hook.name,
|
|
233
|
+
timeout=timeout_val,
|
|
234
|
+
duration_seconds=round(duration, 2),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return HookResult(
|
|
238
|
+
id=hook.name,
|
|
239
|
+
name=hook.name,
|
|
240
|
+
status="timeout",
|
|
241
|
+
duration=duration,
|
|
242
|
+
issues_found=[f"Hook timed out after {duration: .1f}s"],
|
|
243
|
+
stage=hook.stage.value,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
duration = time.time() - start_time
|
|
247
|
+
output_text = (
|
|
248
|
+
(stdout.decode() + stderr.decode()) if stdout and stderr else ""
|
|
249
|
+
)
|
|
250
|
+
return_code = process.returncode if process.returncode is not None else -1
|
|
251
|
+
parsed_output = self._parse_hook_output(return_code, output_text)
|
|
252
|
+
|
|
253
|
+
status = "passed" if return_code == 0 else "failed"
|
|
254
|
+
|
|
255
|
+
self.logger.info(
|
|
256
|
+
"Hook execution completed",
|
|
257
|
+
hook=hook.name,
|
|
258
|
+
status=status,
|
|
259
|
+
duration_seconds=round(duration, 2),
|
|
260
|
+
return_code=process.returncode,
|
|
261
|
+
files_processed=parsed_output.get("files_processed", 0),
|
|
262
|
+
issues_count=len(parsed_output.get("issues", [])),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return HookResult(
|
|
266
|
+
id=parsed_output.get("hook_id", hook.name),
|
|
267
|
+
name=hook.name,
|
|
268
|
+
status=status,
|
|
269
|
+
duration=duration,
|
|
270
|
+
files_processed=parsed_output.get("files_processed", 0),
|
|
271
|
+
issues_found=parsed_output.get("issues", []),
|
|
272
|
+
stage=hook.stage.value,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
duration = time.time() - start_time
|
|
277
|
+
self.logger.exception(
|
|
278
|
+
"Hook execution failed with exception",
|
|
279
|
+
hook=hook.name,
|
|
280
|
+
error=str(e),
|
|
281
|
+
error_type=type(e).__name__,
|
|
282
|
+
duration_seconds=round(duration, 2),
|
|
283
|
+
)
|
|
284
|
+
return HookResult(
|
|
285
|
+
id=hook.name,
|
|
286
|
+
name=hook.name,
|
|
287
|
+
status="error",
|
|
288
|
+
duration=duration,
|
|
289
|
+
issues_found=[str(e)],
|
|
290
|
+
stage=hook.stage.value,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _parse_hook_output(self, returncode: int, output: str) -> dict[str, t.Any]:
|
|
294
|
+
return {
|
|
295
|
+
"hook_id": None,
|
|
296
|
+
"exit_code": returncode,
|
|
297
|
+
"files_processed": 0,
|
|
298
|
+
"issues": [],
|
|
299
|
+
"raw_output": output,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
def _display_hook_result(self, result: HookResult) -> None:
|
|
303
|
+
dots = "." * (60 - len(result.name))
|
|
304
|
+
status_text = "Passed" if result.status == "passed" else "Failed"
|
|
305
|
+
status_color = "green" if result.status == "passed" else "red"
|
|
306
|
+
|
|
307
|
+
self.console.print(
|
|
308
|
+
f"{result.name}{dots}[{status_color}]{status_text}[/{status_color}]",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if result.status != "passed" and result.issues_found:
|
|
312
|
+
for issue in result.issues_found:
|
|
313
|
+
if issue and "raw_output" not in issue:
|
|
314
|
+
self.console.print(issue)
|
|
315
|
+
|
|
316
|
+
async def _handle_retries(
|
|
317
|
+
self,
|
|
318
|
+
strategy: HookStrategy,
|
|
319
|
+
results: list[HookResult],
|
|
320
|
+
) -> list[HookResult]:
|
|
321
|
+
if strategy.retry_policy == RetryPolicy.FORMATTING_ONLY:
|
|
322
|
+
return await self._retry_formatting_hooks(strategy, results)
|
|
323
|
+
if strategy.retry_policy == RetryPolicy.ALL_HOOKS:
|
|
324
|
+
return await self._retry_all_hooks(strategy, results)
|
|
325
|
+
return results
|
|
326
|
+
|
|
327
|
+
async def _retry_formatting_hooks(
|
|
328
|
+
self,
|
|
329
|
+
strategy: HookStrategy,
|
|
330
|
+
results: list[HookResult],
|
|
331
|
+
) -> list[HookResult]:
|
|
332
|
+
formatting_hooks_failed: set[str] = set()
|
|
333
|
+
|
|
334
|
+
for i, result in enumerate(results):
|
|
335
|
+
hook = strategy.hooks[i]
|
|
336
|
+
if getattr(hook, "is_formatting", False) and result.status == "failed":
|
|
337
|
+
formatting_hooks_failed.add(hook.name)
|
|
338
|
+
|
|
339
|
+
if not formatting_hooks_failed:
|
|
340
|
+
return results
|
|
341
|
+
|
|
342
|
+
retry_tasks = [self._execute_single_hook(hook) for hook in strategy.hooks]
|
|
343
|
+
retry_results = await asyncio.gather(*retry_tasks, return_exceptions=True)
|
|
344
|
+
|
|
345
|
+
updated_results: list[HookResult] = []
|
|
346
|
+
for i, (prev_result, new_result) in enumerate(
|
|
347
|
+
zip(results, retry_results, strict=False)
|
|
348
|
+
):
|
|
349
|
+
if isinstance(new_result, Exception):
|
|
350
|
+
hook = strategy.hooks[i]
|
|
351
|
+
error_result = HookResult(
|
|
352
|
+
id=hook.name,
|
|
353
|
+
name=hook.name,
|
|
354
|
+
status="error",
|
|
355
|
+
duration=prev_result.duration,
|
|
356
|
+
issues_found=[str(new_result)],
|
|
357
|
+
stage=hook.stage.value,
|
|
358
|
+
)
|
|
359
|
+
updated_results.append(error_result)
|
|
360
|
+
else:
|
|
361
|
+
hook_result = t.cast("HookResult", new_result)
|
|
362
|
+
hook_result.duration += prev_result.duration
|
|
363
|
+
updated_results.append(hook_result)
|
|
364
|
+
|
|
365
|
+
self._display_hook_result(updated_results[-1])
|
|
366
|
+
|
|
367
|
+
return updated_results
|
|
368
|
+
|
|
369
|
+
async def _retry_all_hooks(
|
|
370
|
+
self,
|
|
371
|
+
strategy: HookStrategy,
|
|
372
|
+
results: list[HookResult],
|
|
373
|
+
) -> list[HookResult]:
|
|
374
|
+
failed_indices = [i for i, r in enumerate(results) if r.status == "failed"]
|
|
375
|
+
|
|
376
|
+
if not failed_indices:
|
|
377
|
+
return results
|
|
378
|
+
|
|
379
|
+
updated_results = results.copy()
|
|
380
|
+
retry_tasks: list[t.Awaitable[HookResult]] = []
|
|
381
|
+
retry_indices: list[int] = []
|
|
382
|
+
|
|
383
|
+
for i in failed_indices:
|
|
384
|
+
hook = strategy.hooks[i]
|
|
385
|
+
retry_tasks.append(self._execute_single_hook(hook))
|
|
386
|
+
retry_indices.append(i)
|
|
387
|
+
|
|
388
|
+
retry_results = await asyncio.gather(*retry_tasks, return_exceptions=True)
|
|
389
|
+
|
|
390
|
+
for result_idx, new_result in zip(retry_indices, retry_results, strict=False):
|
|
391
|
+
prev_result = results[result_idx]
|
|
392
|
+
|
|
393
|
+
if isinstance(new_result, Exception):
|
|
394
|
+
hook = strategy.hooks[result_idx]
|
|
395
|
+
error_result = HookResult(
|
|
396
|
+
id=hook.name,
|
|
397
|
+
name=hook.name,
|
|
398
|
+
status="error",
|
|
399
|
+
duration=prev_result.duration,
|
|
400
|
+
issues_found=[str(new_result)],
|
|
401
|
+
stage=hook.stage.value,
|
|
402
|
+
)
|
|
403
|
+
updated_results[result_idx] = error_result
|
|
404
|
+
else:
|
|
405
|
+
hook_result = t.cast("HookResult", new_result)
|
|
406
|
+
hook_result.duration += prev_result.duration
|
|
407
|
+
updated_results[result_idx] = hook_result
|
|
408
|
+
|
|
409
|
+
self._display_hook_result(updated_results[result_idx])
|
|
410
|
+
|
|
411
|
+
return updated_results
|
|
412
|
+
|
|
413
|
+
def _print_summary(
|
|
414
|
+
self,
|
|
415
|
+
strategy: HookStrategy,
|
|
416
|
+
results: list[HookResult],
|
|
417
|
+
success: bool,
|
|
418
|
+
performance_gain: float,
|
|
419
|
+
) -> None:
|
|
420
|
+
if success:
|
|
421
|
+
self.console.print(
|
|
422
|
+
f"[green]✅[/green] {strategy.name.title()} hooks passed: {len(results)} / {len(results)} "
|
|
423
|
+
f"(async, {performance_gain: .1f} % faster)",
|
|
424
|
+
)
|
|
425
|
+
else:
|
|
426
|
+
failed_count = sum(1 for r in results if r.status == "failed")
|
|
427
|
+
error_count = sum(1 for r in results if r.status in ("timeout", "error"))
|
|
428
|
+
self.console.print(
|
|
429
|
+
f"[red]❌[/red] {strategy.name.title()} hooks failed: {failed_count} failed, {error_count} errors "
|
|
430
|
+
f"(async, {performance_gain: .1f} % faster)",
|
|
431
|
+
)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import typing as t
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from crackerjack.config.hooks import HookDefinition, HookStrategy
|
|
9
|
+
from crackerjack.models.task import HookResult
|
|
10
|
+
from crackerjack.services.cache import CrackerjackCache
|
|
11
|
+
from crackerjack.services.file_hasher import FileHasher
|
|
12
|
+
|
|
13
|
+
from .hook_executor import HookExecutionResult, HookExecutor
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CachedHookExecutor:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
console: Console,
|
|
20
|
+
pkg_path: Path,
|
|
21
|
+
cache: CrackerjackCache | None = None,
|
|
22
|
+
cache_ttl_seconds: int = 1800,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.console = console
|
|
25
|
+
self.pkg_path = pkg_path
|
|
26
|
+
self.cache = cache or CrackerjackCache()
|
|
27
|
+
self.cache_ttl_seconds = cache_ttl_seconds
|
|
28
|
+
self.file_hasher = FileHasher(self.cache)
|
|
29
|
+
self.base_executor = HookExecutor(console, pkg_path, quiet=True)
|
|
30
|
+
self.logger = logging.getLogger("crackerjack.cached_executor")
|
|
31
|
+
|
|
32
|
+
self.file_patterns = {
|
|
33
|
+
"python": [" * .py"],
|
|
34
|
+
"config": [" * .toml", " * .cfg", " * .ini", " * .yaml", " * .yml"],
|
|
35
|
+
"all": [
|
|
36
|
+
" * .py",
|
|
37
|
+
" * .toml",
|
|
38
|
+
" * .cfg",
|
|
39
|
+
" * .ini",
|
|
40
|
+
" * .yaml",
|
|
41
|
+
" * .yml",
|
|
42
|
+
" * .md",
|
|
43
|
+
" * .txt",
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def execute_strategy(self, strategy: HookStrategy) -> HookExecutionResult:
|
|
48
|
+
self.logger.info(
|
|
49
|
+
f"Executing cached strategy '{strategy.name}' with {len(strategy.hooks)} hooks",
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
start_time = time.time()
|
|
53
|
+
results: list[HookResult] = []
|
|
54
|
+
cache_hits = 0
|
|
55
|
+
cache_misses = 0
|
|
56
|
+
|
|
57
|
+
relevant_files = self._get_relevant_files_for_strategy(strategy)
|
|
58
|
+
current_file_hashes = self.file_hasher.get_files_hash_list(relevant_files)
|
|
59
|
+
|
|
60
|
+
for hook_def in strategy.hooks:
|
|
61
|
+
cached_result = None
|
|
62
|
+
try:
|
|
63
|
+
cached_result = self.cache.get_hook_result(
|
|
64
|
+
hook_def.name,
|
|
65
|
+
current_file_hashes,
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
self.logger.warning(f"Cache error for hook {hook_def.name}: {e}")
|
|
69
|
+
cached_result = None
|
|
70
|
+
|
|
71
|
+
if cached_result and self._is_cache_valid(cached_result, hook_def):
|
|
72
|
+
self.logger.debug(f"Using cached result for hook: {hook_def.name}")
|
|
73
|
+
results.append(cached_result)
|
|
74
|
+
cache_hits += 1
|
|
75
|
+
else:
|
|
76
|
+
self.logger.debug(f"Executing hook (cache miss): {hook_def.name}")
|
|
77
|
+
|
|
78
|
+
hook_result = self.base_executor.execute_single_hook(hook_def)
|
|
79
|
+
results.append(hook_result)
|
|
80
|
+
cache_misses += 1
|
|
81
|
+
|
|
82
|
+
if hook_result.status == "passed":
|
|
83
|
+
try:
|
|
84
|
+
self.cache.set_hook_result(
|
|
85
|
+
hook_def.name,
|
|
86
|
+
current_file_hashes,
|
|
87
|
+
hook_result,
|
|
88
|
+
)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
self.logger.warning(
|
|
91
|
+
f"Failed to cache result for {hook_def.name}: {e}",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
total_time = time.time() - start_time
|
|
95
|
+
success = all(result.status == "passed" for result in results)
|
|
96
|
+
|
|
97
|
+
self.logger.info(
|
|
98
|
+
f"Cached strategy '{strategy.name}' completed in {total_time: .2f}s - "
|
|
99
|
+
f"Success: {success}, Cache hits: {cache_hits}, Cache misses: {cache_misses}",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return HookExecutionResult(
|
|
103
|
+
strategy_name=strategy.name,
|
|
104
|
+
results=results,
|
|
105
|
+
success=success,
|
|
106
|
+
total_duration=total_time,
|
|
107
|
+
cache_hits=cache_hits,
|
|
108
|
+
cache_misses=cache_misses,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _get_relevant_files_for_strategy(self, strategy: HookStrategy) -> list[Path]:
|
|
112
|
+
if self._strategy_affects_python_only(strategy):
|
|
113
|
+
patterns = self.file_patterns["python"]
|
|
114
|
+
elif self._strategy_affects_config_only(strategy):
|
|
115
|
+
patterns = self.file_patterns["config"]
|
|
116
|
+
else:
|
|
117
|
+
patterns = self.file_patterns["all"]
|
|
118
|
+
|
|
119
|
+
files: list[Path] = []
|
|
120
|
+
for pattern in patterns:
|
|
121
|
+
files.extend(list(self.pkg_path.rglob(pattern)))
|
|
122
|
+
|
|
123
|
+
return [f for f in files if f.is_file() and not self._should_ignore_file(f)]
|
|
124
|
+
|
|
125
|
+
def _strategy_affects_python_only(self, strategy: HookStrategy) -> bool:
|
|
126
|
+
python_only_hooks = {
|
|
127
|
+
"ruff - format",
|
|
128
|
+
"ruff - check",
|
|
129
|
+
"pyright",
|
|
130
|
+
"bandit",
|
|
131
|
+
"vulture",
|
|
132
|
+
"refurb",
|
|
133
|
+
"complexipy",
|
|
134
|
+
}
|
|
135
|
+
return all(hook.name in python_only_hooks for hook in strategy.hooks)
|
|
136
|
+
|
|
137
|
+
def _strategy_affects_config_only(self, strategy: HookStrategy) -> bool:
|
|
138
|
+
config_only_hooks = {"creosote"}
|
|
139
|
+
return all(hook.name in config_only_hooks for hook in strategy.hooks)
|
|
140
|
+
|
|
141
|
+
def _should_ignore_file(self, file_path: Path) -> bool:
|
|
142
|
+
ignore_patterns = [
|
|
143
|
+
".git/",
|
|
144
|
+
".venv/",
|
|
145
|
+
"__pycache__/",
|
|
146
|
+
".pytest_cache/",
|
|
147
|
+
".coverage",
|
|
148
|
+
".crackerjack_cache/",
|
|
149
|
+
"node_modules/",
|
|
150
|
+
".tox/",
|
|
151
|
+
"dist/",
|
|
152
|
+
"build/",
|
|
153
|
+
".egg - info/",
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
path_str = str(file_path)
|
|
157
|
+
return any(pattern in path_str for pattern in ignore_patterns)
|
|
158
|
+
|
|
159
|
+
def _is_cache_valid(
|
|
160
|
+
self,
|
|
161
|
+
cached_result: HookResult,
|
|
162
|
+
hook_def: HookDefinition,
|
|
163
|
+
) -> bool:
|
|
164
|
+
if cached_result.status != "passed":
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
cache_age = time.time() - getattr(cached_result, "timestamp", time.time())
|
|
168
|
+
return not cache_age > self.cache_ttl_seconds
|
|
169
|
+
|
|
170
|
+
def invalidate_hook_cache(self, hook_name: str | None = None) -> None:
|
|
171
|
+
self.cache.invalidate_hook_cache(hook_name)
|
|
172
|
+
self.logger.info(f"Invalidated cache for hook: {hook_name or 'all hooks'}")
|
|
173
|
+
|
|
174
|
+
def get_cache_stats(self) -> dict[str, t.Any]:
|
|
175
|
+
return self.cache.get_cache_stats()
|
|
176
|
+
|
|
177
|
+
def cleanup_cache(self) -> dict[str, int]:
|
|
178
|
+
return self.cache.cleanup_all()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class SmartCacheManager:
|
|
182
|
+
def __init__(self, cached_executor: CachedHookExecutor) -> None:
|
|
183
|
+
self.cached_executor = cached_executor
|
|
184
|
+
self.logger = logging.getLogger("crackerjack.cache_manager")
|
|
185
|
+
|
|
186
|
+
def should_use_cache_for_hook(
|
|
187
|
+
self,
|
|
188
|
+
hook_name: str,
|
|
189
|
+
project_state: dict[str, t.Any],
|
|
190
|
+
) -> bool:
|
|
191
|
+
external_hooks = {"detect - secrets"}
|
|
192
|
+
if hook_name in external_hooks:
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
expensive_hooks = {"pyright", "bandit", "vulture", "complexipy"}
|
|
196
|
+
if hook_name in expensive_hooks:
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
formatting_hooks = {
|
|
200
|
+
"ruff-format",
|
|
201
|
+
"trailing-whitespace",
|
|
202
|
+
"end-of-file-fixer",
|
|
203
|
+
}
|
|
204
|
+
if hook_name in formatting_hooks:
|
|
205
|
+
recent_changes = project_state.get("recent_changes", 0)
|
|
206
|
+
return recent_changes < 5
|
|
207
|
+
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
def get_optimal_cache_strategy(
|
|
211
|
+
self,
|
|
212
|
+
hook_strategy: HookStrategy,
|
|
213
|
+
) -> dict[str, bool]:
|
|
214
|
+
project_state = self._analyze_project_state()
|
|
215
|
+
|
|
216
|
+
cache_decisions = {}
|
|
217
|
+
for hook_def in hook_strategy.hooks:
|
|
218
|
+
cache_decisions[hook_def.name] = self.should_use_cache_for_hook(
|
|
219
|
+
hook_def.name,
|
|
220
|
+
project_state,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
return cache_decisions
|
|
224
|
+
|
|
225
|
+
def _analyze_project_state(self) -> dict[str, t.Any]:
|
|
226
|
+
pkg_path = self.cached_executor.pkg_path
|
|
227
|
+
|
|
228
|
+
recent_changes = 0
|
|
229
|
+
for py_file in pkg_path.rglob("*.py"):
|
|
230
|
+
if py_file.is_file():
|
|
231
|
+
from contextlib import suppress
|
|
232
|
+
|
|
233
|
+
with suppress(OSError):
|
|
234
|
+
mtime = py_file.stat().st_mtime
|
|
235
|
+
if time.time() - mtime < 3600:
|
|
236
|
+
recent_changes += 1
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
"recent_changes": recent_changes,
|
|
240
|
+
"total_python_files": len(list(pkg_path.rglob("*.py"))),
|
|
241
|
+
"project_size": "large" if recent_changes > 50 else "small",
|
|
242
|
+
}
|