crackerjack 0.18.2__py3-none-any.whl → 0.45.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.
Files changed (533) hide show
  1. crackerjack/README.md +19 -0
  2. crackerjack/__init__.py +96 -2
  3. crackerjack/__main__.py +637 -138
  4. crackerjack/adapters/README.md +18 -0
  5. crackerjack/adapters/__init__.py +39 -0
  6. crackerjack/adapters/_output_paths.py +167 -0
  7. crackerjack/adapters/_qa_adapter_base.py +309 -0
  8. crackerjack/adapters/_tool_adapter_base.py +706 -0
  9. crackerjack/adapters/ai/README.md +65 -0
  10. crackerjack/adapters/ai/__init__.py +5 -0
  11. crackerjack/adapters/ai/claude.py +853 -0
  12. crackerjack/adapters/complexity/README.md +53 -0
  13. crackerjack/adapters/complexity/__init__.py +10 -0
  14. crackerjack/adapters/complexity/complexipy.py +641 -0
  15. crackerjack/adapters/dependency/__init__.py +22 -0
  16. crackerjack/adapters/dependency/pip_audit.py +418 -0
  17. crackerjack/adapters/format/README.md +72 -0
  18. crackerjack/adapters/format/__init__.py +11 -0
  19. crackerjack/adapters/format/mdformat.py +313 -0
  20. crackerjack/adapters/format/ruff.py +516 -0
  21. crackerjack/adapters/lint/README.md +47 -0
  22. crackerjack/adapters/lint/__init__.py +11 -0
  23. crackerjack/adapters/lint/codespell.py +273 -0
  24. crackerjack/adapters/lsp/README.md +49 -0
  25. crackerjack/adapters/lsp/__init__.py +27 -0
  26. crackerjack/adapters/lsp/_base.py +194 -0
  27. crackerjack/adapters/lsp/_client.py +358 -0
  28. crackerjack/adapters/lsp/_manager.py +193 -0
  29. crackerjack/adapters/lsp/skylos.py +283 -0
  30. crackerjack/adapters/lsp/zuban.py +557 -0
  31. crackerjack/adapters/refactor/README.md +59 -0
  32. crackerjack/adapters/refactor/__init__.py +12 -0
  33. crackerjack/adapters/refactor/creosote.py +318 -0
  34. crackerjack/adapters/refactor/refurb.py +406 -0
  35. crackerjack/adapters/refactor/skylos.py +494 -0
  36. crackerjack/adapters/sast/README.md +132 -0
  37. crackerjack/adapters/sast/__init__.py +32 -0
  38. crackerjack/adapters/sast/_base.py +201 -0
  39. crackerjack/adapters/sast/bandit.py +423 -0
  40. crackerjack/adapters/sast/pyscn.py +405 -0
  41. crackerjack/adapters/sast/semgrep.py +241 -0
  42. crackerjack/adapters/security/README.md +111 -0
  43. crackerjack/adapters/security/__init__.py +17 -0
  44. crackerjack/adapters/security/gitleaks.py +339 -0
  45. crackerjack/adapters/type/README.md +52 -0
  46. crackerjack/adapters/type/__init__.py +12 -0
  47. crackerjack/adapters/type/pyrefly.py +402 -0
  48. crackerjack/adapters/type/ty.py +402 -0
  49. crackerjack/adapters/type/zuban.py +522 -0
  50. crackerjack/adapters/utility/README.md +51 -0
  51. crackerjack/adapters/utility/__init__.py +10 -0
  52. crackerjack/adapters/utility/checks.py +884 -0
  53. crackerjack/agents/README.md +264 -0
  54. crackerjack/agents/__init__.py +66 -0
  55. crackerjack/agents/architect_agent.py +238 -0
  56. crackerjack/agents/base.py +167 -0
  57. crackerjack/agents/claude_code_bridge.py +641 -0
  58. crackerjack/agents/coordinator.py +600 -0
  59. crackerjack/agents/documentation_agent.py +520 -0
  60. crackerjack/agents/dry_agent.py +585 -0
  61. crackerjack/agents/enhanced_coordinator.py +279 -0
  62. crackerjack/agents/enhanced_proactive_agent.py +185 -0
  63. crackerjack/agents/error_middleware.py +53 -0
  64. crackerjack/agents/formatting_agent.py +230 -0
  65. crackerjack/agents/helpers/__init__.py +9 -0
  66. crackerjack/agents/helpers/performance/__init__.py +22 -0
  67. crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
  68. crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
  69. crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
  70. crackerjack/agents/helpers/refactoring/__init__.py +22 -0
  71. crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
  72. crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
  73. crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
  74. crackerjack/agents/helpers/test_creation/__init__.py +19 -0
  75. crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
  76. crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
  77. crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
  78. crackerjack/agents/import_optimization_agent.py +1181 -0
  79. crackerjack/agents/performance_agent.py +325 -0
  80. crackerjack/agents/performance_helpers.py +205 -0
  81. crackerjack/agents/proactive_agent.py +55 -0
  82. crackerjack/agents/refactoring_agent.py +511 -0
  83. crackerjack/agents/refactoring_helpers.py +247 -0
  84. crackerjack/agents/security_agent.py +793 -0
  85. crackerjack/agents/semantic_agent.py +479 -0
  86. crackerjack/agents/semantic_helpers.py +356 -0
  87. crackerjack/agents/test_creation_agent.py +570 -0
  88. crackerjack/agents/test_specialist_agent.py +526 -0
  89. crackerjack/agents/tracker.py +110 -0
  90. crackerjack/api.py +647 -0
  91. crackerjack/cli/README.md +394 -0
  92. crackerjack/cli/__init__.py +24 -0
  93. crackerjack/cli/cache_handlers.py +209 -0
  94. crackerjack/cli/cache_handlers_enhanced.py +680 -0
  95. crackerjack/cli/facade.py +162 -0
  96. crackerjack/cli/formatting.py +13 -0
  97. crackerjack/cli/handlers/__init__.py +85 -0
  98. crackerjack/cli/handlers/advanced.py +103 -0
  99. crackerjack/cli/handlers/ai_features.py +62 -0
  100. crackerjack/cli/handlers/analytics.py +479 -0
  101. crackerjack/cli/handlers/changelog.py +271 -0
  102. crackerjack/cli/handlers/config_handlers.py +16 -0
  103. crackerjack/cli/handlers/coverage.py +84 -0
  104. crackerjack/cli/handlers/documentation.py +280 -0
  105. crackerjack/cli/handlers/main_handlers.py +497 -0
  106. crackerjack/cli/handlers/monitoring.py +371 -0
  107. crackerjack/cli/handlers.py +700 -0
  108. crackerjack/cli/interactive.py +488 -0
  109. crackerjack/cli/options.py +1216 -0
  110. crackerjack/cli/semantic_handlers.py +292 -0
  111. crackerjack/cli/utils.py +19 -0
  112. crackerjack/cli/version.py +19 -0
  113. crackerjack/code_cleaner.py +1307 -0
  114. crackerjack/config/README.md +472 -0
  115. crackerjack/config/__init__.py +275 -0
  116. crackerjack/config/global_lock_config.py +207 -0
  117. crackerjack/config/hooks.py +390 -0
  118. crackerjack/config/loader.py +239 -0
  119. crackerjack/config/settings.py +141 -0
  120. crackerjack/config/tool_commands.py +331 -0
  121. crackerjack/core/README.md +393 -0
  122. crackerjack/core/__init__.py +0 -0
  123. crackerjack/core/async_workflow_orchestrator.py +738 -0
  124. crackerjack/core/autofix_coordinator.py +282 -0
  125. crackerjack/core/container.py +105 -0
  126. crackerjack/core/enhanced_container.py +583 -0
  127. crackerjack/core/file_lifecycle.py +472 -0
  128. crackerjack/core/performance.py +244 -0
  129. crackerjack/core/performance_monitor.py +357 -0
  130. crackerjack/core/phase_coordinator.py +1227 -0
  131. crackerjack/core/proactive_workflow.py +267 -0
  132. crackerjack/core/resource_manager.py +425 -0
  133. crackerjack/core/retry.py +275 -0
  134. crackerjack/core/service_watchdog.py +601 -0
  135. crackerjack/core/session_coordinator.py +239 -0
  136. crackerjack/core/timeout_manager.py +563 -0
  137. crackerjack/core/websocket_lifecycle.py +410 -0
  138. crackerjack/core/workflow/__init__.py +21 -0
  139. crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
  140. crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
  141. crackerjack/core/workflow/workflow_issue_parser.py +714 -0
  142. crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
  143. crackerjack/core/workflow/workflow_security_gates.py +400 -0
  144. crackerjack/core/workflow_orchestrator.py +2243 -0
  145. crackerjack/data/README.md +11 -0
  146. crackerjack/data/__init__.py +8 -0
  147. crackerjack/data/models.py +79 -0
  148. crackerjack/data/repository.py +210 -0
  149. crackerjack/decorators/README.md +180 -0
  150. crackerjack/decorators/__init__.py +35 -0
  151. crackerjack/decorators/error_handling.py +649 -0
  152. crackerjack/decorators/error_handling_decorators.py +334 -0
  153. crackerjack/decorators/helpers.py +58 -0
  154. crackerjack/decorators/patterns.py +281 -0
  155. crackerjack/decorators/utils.py +58 -0
  156. crackerjack/docs/INDEX.md +11 -0
  157. crackerjack/docs/README.md +11 -0
  158. crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
  159. crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
  160. crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
  161. crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
  162. crackerjack/docs/generated/api/SERVICES.md +1252 -0
  163. crackerjack/documentation/README.md +11 -0
  164. crackerjack/documentation/__init__.py +31 -0
  165. crackerjack/documentation/ai_templates.py +756 -0
  166. crackerjack/documentation/dual_output_generator.py +767 -0
  167. crackerjack/documentation/mkdocs_integration.py +518 -0
  168. crackerjack/documentation/reference_generator.py +1065 -0
  169. crackerjack/dynamic_config.py +678 -0
  170. crackerjack/errors.py +378 -0
  171. crackerjack/events/README.md +11 -0
  172. crackerjack/events/__init__.py +16 -0
  173. crackerjack/events/telemetry.py +175 -0
  174. crackerjack/events/workflow_bus.py +346 -0
  175. crackerjack/exceptions/README.md +301 -0
  176. crackerjack/exceptions/__init__.py +5 -0
  177. crackerjack/exceptions/config.py +4 -0
  178. crackerjack/exceptions/tool_execution_error.py +245 -0
  179. crackerjack/executors/README.md +591 -0
  180. crackerjack/executors/__init__.py +13 -0
  181. crackerjack/executors/async_hook_executor.py +938 -0
  182. crackerjack/executors/cached_hook_executor.py +316 -0
  183. crackerjack/executors/hook_executor.py +1295 -0
  184. crackerjack/executors/hook_lock_manager.py +708 -0
  185. crackerjack/executors/individual_hook_executor.py +739 -0
  186. crackerjack/executors/lsp_aware_hook_executor.py +349 -0
  187. crackerjack/executors/progress_hook_executor.py +282 -0
  188. crackerjack/executors/tool_proxy.py +433 -0
  189. crackerjack/hooks/README.md +485 -0
  190. crackerjack/hooks/lsp_hook.py +93 -0
  191. crackerjack/intelligence/README.md +557 -0
  192. crackerjack/intelligence/__init__.py +37 -0
  193. crackerjack/intelligence/adaptive_learning.py +693 -0
  194. crackerjack/intelligence/agent_orchestrator.py +485 -0
  195. crackerjack/intelligence/agent_registry.py +377 -0
  196. crackerjack/intelligence/agent_selector.py +439 -0
  197. crackerjack/intelligence/integration.py +250 -0
  198. crackerjack/interactive.py +719 -0
  199. crackerjack/managers/README.md +369 -0
  200. crackerjack/managers/__init__.py +11 -0
  201. crackerjack/managers/async_hook_manager.py +135 -0
  202. crackerjack/managers/hook_manager.py +585 -0
  203. crackerjack/managers/publish_manager.py +631 -0
  204. crackerjack/managers/test_command_builder.py +391 -0
  205. crackerjack/managers/test_executor.py +474 -0
  206. crackerjack/managers/test_manager.py +1357 -0
  207. crackerjack/managers/test_progress.py +187 -0
  208. crackerjack/mcp/README.md +374 -0
  209. crackerjack/mcp/__init__.py +0 -0
  210. crackerjack/mcp/cache.py +352 -0
  211. crackerjack/mcp/client_runner.py +121 -0
  212. crackerjack/mcp/context.py +802 -0
  213. crackerjack/mcp/dashboard.py +657 -0
  214. crackerjack/mcp/enhanced_progress_monitor.py +493 -0
  215. crackerjack/mcp/file_monitor.py +394 -0
  216. crackerjack/mcp/progress_components.py +607 -0
  217. crackerjack/mcp/progress_monitor.py +1016 -0
  218. crackerjack/mcp/rate_limiter.py +336 -0
  219. crackerjack/mcp/server.py +24 -0
  220. crackerjack/mcp/server_core.py +526 -0
  221. crackerjack/mcp/service_watchdog.py +505 -0
  222. crackerjack/mcp/state.py +407 -0
  223. crackerjack/mcp/task_manager.py +259 -0
  224. crackerjack/mcp/tools/README.md +27 -0
  225. crackerjack/mcp/tools/__init__.py +19 -0
  226. crackerjack/mcp/tools/core_tools.py +469 -0
  227. crackerjack/mcp/tools/error_analyzer.py +283 -0
  228. crackerjack/mcp/tools/execution_tools.py +384 -0
  229. crackerjack/mcp/tools/intelligence_tool_registry.py +46 -0
  230. crackerjack/mcp/tools/intelligence_tools.py +264 -0
  231. crackerjack/mcp/tools/monitoring_tools.py +628 -0
  232. crackerjack/mcp/tools/proactive_tools.py +367 -0
  233. crackerjack/mcp/tools/progress_tools.py +222 -0
  234. crackerjack/mcp/tools/semantic_tools.py +584 -0
  235. crackerjack/mcp/tools/utility_tools.py +358 -0
  236. crackerjack/mcp/tools/workflow_executor.py +699 -0
  237. crackerjack/mcp/websocket/README.md +31 -0
  238. crackerjack/mcp/websocket/__init__.py +14 -0
  239. crackerjack/mcp/websocket/app.py +54 -0
  240. crackerjack/mcp/websocket/endpoints.py +492 -0
  241. crackerjack/mcp/websocket/event_bridge.py +188 -0
  242. crackerjack/mcp/websocket/jobs.py +406 -0
  243. crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
  244. crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
  245. crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
  246. crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
  247. crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
  248. crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
  249. crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
  250. crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
  251. crackerjack/mcp/websocket/monitoring/factory.py +109 -0
  252. crackerjack/mcp/websocket/monitoring/filters.py +10 -0
  253. crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
  254. crackerjack/mcp/websocket/monitoring/models.py +90 -0
  255. crackerjack/mcp/websocket/monitoring/utils.py +171 -0
  256. crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
  257. crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
  258. crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
  259. crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
  260. crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
  261. crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
  262. crackerjack/mcp/websocket/monitoring_endpoints.py +21 -0
  263. crackerjack/mcp/websocket/server.py +174 -0
  264. crackerjack/mcp/websocket/websocket_handler.py +276 -0
  265. crackerjack/mcp/websocket_server.py +10 -0
  266. crackerjack/models/README.md +308 -0
  267. crackerjack/models/__init__.py +40 -0
  268. crackerjack/models/config.py +730 -0
  269. crackerjack/models/config_adapter.py +265 -0
  270. crackerjack/models/protocols.py +1535 -0
  271. crackerjack/models/pydantic_models.py +320 -0
  272. crackerjack/models/qa_config.py +145 -0
  273. crackerjack/models/qa_results.py +134 -0
  274. crackerjack/models/resource_protocols.py +299 -0
  275. crackerjack/models/results.py +35 -0
  276. crackerjack/models/semantic_models.py +258 -0
  277. crackerjack/models/task.py +173 -0
  278. crackerjack/models/test_models.py +60 -0
  279. crackerjack/monitoring/README.md +11 -0
  280. crackerjack/monitoring/__init__.py +0 -0
  281. crackerjack/monitoring/ai_agent_watchdog.py +405 -0
  282. crackerjack/monitoring/metrics_collector.py +427 -0
  283. crackerjack/monitoring/regression_prevention.py +580 -0
  284. crackerjack/monitoring/websocket_server.py +406 -0
  285. crackerjack/orchestration/README.md +340 -0
  286. crackerjack/orchestration/__init__.py +43 -0
  287. crackerjack/orchestration/advanced_orchestrator.py +894 -0
  288. crackerjack/orchestration/cache/README.md +312 -0
  289. crackerjack/orchestration/cache/__init__.py +37 -0
  290. crackerjack/orchestration/cache/memory_cache.py +338 -0
  291. crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
  292. crackerjack/orchestration/config.py +297 -0
  293. crackerjack/orchestration/coverage_improvement.py +180 -0
  294. crackerjack/orchestration/execution_strategies.py +361 -0
  295. crackerjack/orchestration/hook_orchestrator.py +1398 -0
  296. crackerjack/orchestration/strategies/README.md +401 -0
  297. crackerjack/orchestration/strategies/__init__.py +39 -0
  298. crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
  299. crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
  300. crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
  301. crackerjack/orchestration/test_progress_streamer.py +647 -0
  302. crackerjack/plugins/README.md +11 -0
  303. crackerjack/plugins/__init__.py +15 -0
  304. crackerjack/plugins/base.py +200 -0
  305. crackerjack/plugins/hooks.py +254 -0
  306. crackerjack/plugins/loader.py +335 -0
  307. crackerjack/plugins/managers.py +264 -0
  308. crackerjack/py313.py +191 -0
  309. crackerjack/security/README.md +11 -0
  310. crackerjack/security/__init__.py +0 -0
  311. crackerjack/security/audit.py +197 -0
  312. crackerjack/services/README.md +374 -0
  313. crackerjack/services/__init__.py +9 -0
  314. crackerjack/services/ai/README.md +295 -0
  315. crackerjack/services/ai/__init__.py +7 -0
  316. crackerjack/services/ai/advanced_optimizer.py +878 -0
  317. crackerjack/services/ai/contextual_ai_assistant.py +542 -0
  318. crackerjack/services/ai/embeddings.py +444 -0
  319. crackerjack/services/ai/intelligent_commit.py +328 -0
  320. crackerjack/services/ai/predictive_analytics.py +510 -0
  321. crackerjack/services/anomaly_detector.py +392 -0
  322. crackerjack/services/api_extractor.py +617 -0
  323. crackerjack/services/backup_service.py +467 -0
  324. crackerjack/services/bounded_status_operations.py +530 -0
  325. crackerjack/services/cache.py +369 -0
  326. crackerjack/services/changelog_automation.py +399 -0
  327. crackerjack/services/command_execution_service.py +305 -0
  328. crackerjack/services/config_integrity.py +132 -0
  329. crackerjack/services/config_merge.py +546 -0
  330. crackerjack/services/config_service.py +198 -0
  331. crackerjack/services/config_template.py +493 -0
  332. crackerjack/services/coverage_badge_service.py +173 -0
  333. crackerjack/services/coverage_ratchet.py +381 -0
  334. crackerjack/services/debug.py +733 -0
  335. crackerjack/services/dependency_analyzer.py +460 -0
  336. crackerjack/services/dependency_monitor.py +622 -0
  337. crackerjack/services/documentation_generator.py +493 -0
  338. crackerjack/services/documentation_service.py +704 -0
  339. crackerjack/services/enhanced_filesystem.py +497 -0
  340. crackerjack/services/enterprise_optimizer.py +865 -0
  341. crackerjack/services/error_pattern_analyzer.py +676 -0
  342. crackerjack/services/file_filter.py +221 -0
  343. crackerjack/services/file_hasher.py +149 -0
  344. crackerjack/services/file_io_service.py +361 -0
  345. crackerjack/services/file_modifier.py +615 -0
  346. crackerjack/services/filesystem.py +381 -0
  347. crackerjack/services/git.py +422 -0
  348. crackerjack/services/health_metrics.py +615 -0
  349. crackerjack/services/heatmap_generator.py +744 -0
  350. crackerjack/services/incremental_executor.py +380 -0
  351. crackerjack/services/initialization.py +823 -0
  352. crackerjack/services/input_validator.py +668 -0
  353. crackerjack/services/intelligent_commit.py +327 -0
  354. crackerjack/services/log_manager.py +289 -0
  355. crackerjack/services/logging.py +228 -0
  356. crackerjack/services/lsp_client.py +628 -0
  357. crackerjack/services/memory_optimizer.py +414 -0
  358. crackerjack/services/metrics.py +587 -0
  359. crackerjack/services/monitoring/README.md +30 -0
  360. crackerjack/services/monitoring/__init__.py +9 -0
  361. crackerjack/services/monitoring/dependency_monitor.py +678 -0
  362. crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
  363. crackerjack/services/monitoring/health_metrics.py +716 -0
  364. crackerjack/services/monitoring/metrics.py +587 -0
  365. crackerjack/services/monitoring/performance_benchmarks.py +410 -0
  366. crackerjack/services/monitoring/performance_cache.py +388 -0
  367. crackerjack/services/monitoring/performance_monitor.py +569 -0
  368. crackerjack/services/parallel_executor.py +527 -0
  369. crackerjack/services/pattern_cache.py +333 -0
  370. crackerjack/services/pattern_detector.py +478 -0
  371. crackerjack/services/patterns/__init__.py +142 -0
  372. crackerjack/services/patterns/agents.py +107 -0
  373. crackerjack/services/patterns/code/__init__.py +15 -0
  374. crackerjack/services/patterns/code/detection.py +118 -0
  375. crackerjack/services/patterns/code/imports.py +107 -0
  376. crackerjack/services/patterns/code/paths.py +159 -0
  377. crackerjack/services/patterns/code/performance.py +119 -0
  378. crackerjack/services/patterns/code/replacement.py +36 -0
  379. crackerjack/services/patterns/core.py +212 -0
  380. crackerjack/services/patterns/documentation/__init__.py +14 -0
  381. crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
  382. crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
  383. crackerjack/services/patterns/documentation/docstrings.py +89 -0
  384. crackerjack/services/patterns/formatting.py +226 -0
  385. crackerjack/services/patterns/operations.py +339 -0
  386. crackerjack/services/patterns/security/__init__.py +23 -0
  387. crackerjack/services/patterns/security/code_injection.py +122 -0
  388. crackerjack/services/patterns/security/credentials.py +190 -0
  389. crackerjack/services/patterns/security/path_traversal.py +221 -0
  390. crackerjack/services/patterns/security/unsafe_operations.py +216 -0
  391. crackerjack/services/patterns/templates.py +62 -0
  392. crackerjack/services/patterns/testing/__init__.py +18 -0
  393. crackerjack/services/patterns/testing/error_patterns.py +107 -0
  394. crackerjack/services/patterns/testing/pytest_output.py +126 -0
  395. crackerjack/services/patterns/tool_output/__init__.py +16 -0
  396. crackerjack/services/patterns/tool_output/bandit.py +72 -0
  397. crackerjack/services/patterns/tool_output/other.py +97 -0
  398. crackerjack/services/patterns/tool_output/pyright.py +67 -0
  399. crackerjack/services/patterns/tool_output/ruff.py +44 -0
  400. crackerjack/services/patterns/url_sanitization.py +114 -0
  401. crackerjack/services/patterns/utilities.py +42 -0
  402. crackerjack/services/patterns/utils.py +339 -0
  403. crackerjack/services/patterns/validation.py +46 -0
  404. crackerjack/services/patterns/versioning.py +62 -0
  405. crackerjack/services/predictive_analytics.py +523 -0
  406. crackerjack/services/profiler.py +280 -0
  407. crackerjack/services/quality/README.md +415 -0
  408. crackerjack/services/quality/__init__.py +11 -0
  409. crackerjack/services/quality/anomaly_detector.py +392 -0
  410. crackerjack/services/quality/pattern_cache.py +333 -0
  411. crackerjack/services/quality/pattern_detector.py +479 -0
  412. crackerjack/services/quality/qa_orchestrator.py +491 -0
  413. crackerjack/services/quality/quality_baseline.py +395 -0
  414. crackerjack/services/quality/quality_baseline_enhanced.py +649 -0
  415. crackerjack/services/quality/quality_intelligence.py +949 -0
  416. crackerjack/services/regex_patterns.py +58 -0
  417. crackerjack/services/regex_utils.py +483 -0
  418. crackerjack/services/secure_path_utils.py +524 -0
  419. crackerjack/services/secure_status_formatter.py +450 -0
  420. crackerjack/services/secure_subprocess.py +635 -0
  421. crackerjack/services/security.py +239 -0
  422. crackerjack/services/security_logger.py +495 -0
  423. crackerjack/services/server_manager.py +411 -0
  424. crackerjack/services/smart_scheduling.py +167 -0
  425. crackerjack/services/status_authentication.py +460 -0
  426. crackerjack/services/status_security_manager.py +315 -0
  427. crackerjack/services/terminal_utils.py +0 -0
  428. crackerjack/services/thread_safe_status_collector.py +441 -0
  429. crackerjack/services/tool_filter.py +368 -0
  430. crackerjack/services/tool_version_service.py +43 -0
  431. crackerjack/services/unified_config.py +115 -0
  432. crackerjack/services/validation_rate_limiter.py +220 -0
  433. crackerjack/services/vector_store.py +689 -0
  434. crackerjack/services/version_analyzer.py +461 -0
  435. crackerjack/services/version_checker.py +223 -0
  436. crackerjack/services/websocket_resource_limiter.py +438 -0
  437. crackerjack/services/zuban_lsp_service.py +391 -0
  438. crackerjack/slash_commands/README.md +11 -0
  439. crackerjack/slash_commands/__init__.py +59 -0
  440. crackerjack/slash_commands/init.md +112 -0
  441. crackerjack/slash_commands/run.md +197 -0
  442. crackerjack/slash_commands/status.md +127 -0
  443. crackerjack/tools/README.md +11 -0
  444. crackerjack/tools/__init__.py +30 -0
  445. crackerjack/tools/_git_utils.py +105 -0
  446. crackerjack/tools/check_added_large_files.py +139 -0
  447. crackerjack/tools/check_ast.py +105 -0
  448. crackerjack/tools/check_json.py +103 -0
  449. crackerjack/tools/check_jsonschema.py +297 -0
  450. crackerjack/tools/check_toml.py +103 -0
  451. crackerjack/tools/check_yaml.py +110 -0
  452. crackerjack/tools/codespell_wrapper.py +72 -0
  453. crackerjack/tools/end_of_file_fixer.py +202 -0
  454. crackerjack/tools/format_json.py +128 -0
  455. crackerjack/tools/mdformat_wrapper.py +114 -0
  456. crackerjack/tools/trailing_whitespace.py +198 -0
  457. crackerjack/tools/validate_input_validator_patterns.py +236 -0
  458. crackerjack/tools/validate_regex_patterns.py +188 -0
  459. crackerjack/ui/README.md +11 -0
  460. crackerjack/ui/__init__.py +1 -0
  461. crackerjack/ui/dashboard_renderer.py +28 -0
  462. crackerjack/ui/templates/README.md +11 -0
  463. crackerjack/utils/console_utils.py +13 -0
  464. crackerjack/utils/dependency_guard.py +230 -0
  465. crackerjack/utils/retry_utils.py +275 -0
  466. crackerjack/workflows/README.md +590 -0
  467. crackerjack/workflows/__init__.py +46 -0
  468. crackerjack/workflows/actions.py +811 -0
  469. crackerjack/workflows/auto_fix.py +444 -0
  470. crackerjack/workflows/container_builder.py +499 -0
  471. crackerjack/workflows/definitions.py +443 -0
  472. crackerjack/workflows/engine.py +177 -0
  473. crackerjack/workflows/event_bridge.py +242 -0
  474. crackerjack-0.45.2.dist-info/METADATA +1678 -0
  475. crackerjack-0.45.2.dist-info/RECORD +478 -0
  476. {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
  477. crackerjack-0.45.2.dist-info/entry_points.txt +2 -0
  478. crackerjack/.gitignore +0 -14
  479. crackerjack/.libcst.codemod.yaml +0 -18
  480. crackerjack/.pdm.toml +0 -1
  481. crackerjack/.pre-commit-config.yaml +0 -91
  482. crackerjack/.pytest_cache/.gitignore +0 -2
  483. crackerjack/.pytest_cache/CACHEDIR.TAG +0 -4
  484. crackerjack/.pytest_cache/README.md +0 -8
  485. crackerjack/.pytest_cache/v/cache/nodeids +0 -1
  486. crackerjack/.pytest_cache/v/cache/stepwise +0 -1
  487. crackerjack/.ruff_cache/.gitignore +0 -1
  488. crackerjack/.ruff_cache/0.1.11/3256171999636029978 +0 -0
  489. crackerjack/.ruff_cache/0.1.14/602324811142551221 +0 -0
  490. crackerjack/.ruff_cache/0.1.4/10355199064880463147 +0 -0
  491. crackerjack/.ruff_cache/0.1.6/15140459877605758699 +0 -0
  492. crackerjack/.ruff_cache/0.1.7/1790508110482614856 +0 -0
  493. crackerjack/.ruff_cache/0.1.9/17041001205004563469 +0 -0
  494. crackerjack/.ruff_cache/0.11.2/4070660268492669020 +0 -0
  495. crackerjack/.ruff_cache/0.11.3/9818742842212983150 +0 -0
  496. crackerjack/.ruff_cache/0.11.4/9818742842212983150 +0 -0
  497. crackerjack/.ruff_cache/0.11.6/3557596832929915217 +0 -0
  498. crackerjack/.ruff_cache/0.11.7/10386934055395314831 +0 -0
  499. crackerjack/.ruff_cache/0.11.7/3557596832929915217 +0 -0
  500. crackerjack/.ruff_cache/0.11.8/530407680854991027 +0 -0
  501. crackerjack/.ruff_cache/0.2.0/10047773857155985907 +0 -0
  502. crackerjack/.ruff_cache/0.2.1/8522267973936635051 +0 -0
  503. crackerjack/.ruff_cache/0.2.2/18053836298936336950 +0 -0
  504. crackerjack/.ruff_cache/0.3.0/12548816621480535786 +0 -0
  505. crackerjack/.ruff_cache/0.3.3/11081883392474770722 +0 -0
  506. crackerjack/.ruff_cache/0.3.4/676973378459347183 +0 -0
  507. crackerjack/.ruff_cache/0.3.5/16311176246009842383 +0 -0
  508. crackerjack/.ruff_cache/0.5.7/1493622539551733492 +0 -0
  509. crackerjack/.ruff_cache/0.5.7/6231957614044513175 +0 -0
  510. crackerjack/.ruff_cache/0.5.7/9932762556785938009 +0 -0
  511. crackerjack/.ruff_cache/0.6.0/11982804814124138945 +0 -0
  512. crackerjack/.ruff_cache/0.6.0/12055761203849489982 +0 -0
  513. crackerjack/.ruff_cache/0.6.2/1206147804896221174 +0 -0
  514. crackerjack/.ruff_cache/0.6.4/1206147804896221174 +0 -0
  515. crackerjack/.ruff_cache/0.6.5/1206147804896221174 +0 -0
  516. crackerjack/.ruff_cache/0.6.7/3657366982708166874 +0 -0
  517. crackerjack/.ruff_cache/0.6.9/285614542852677309 +0 -0
  518. crackerjack/.ruff_cache/0.7.1/1024065805990144819 +0 -0
  519. crackerjack/.ruff_cache/0.7.1/285614542852677309 +0 -0
  520. crackerjack/.ruff_cache/0.7.3/16061516852537040135 +0 -0
  521. crackerjack/.ruff_cache/0.8.4/16354268377385700367 +0 -0
  522. crackerjack/.ruff_cache/0.9.10/12813592349865671909 +0 -0
  523. crackerjack/.ruff_cache/0.9.10/923908772239632759 +0 -0
  524. crackerjack/.ruff_cache/0.9.3/13948373885254993391 +0 -0
  525. crackerjack/.ruff_cache/0.9.9/12813592349865671909 +0 -0
  526. crackerjack/.ruff_cache/0.9.9/8843823720003377982 +0 -0
  527. crackerjack/.ruff_cache/CACHEDIR.TAG +0 -1
  528. crackerjack/crackerjack.py +0 -855
  529. crackerjack/pyproject.toml +0 -214
  530. crackerjack-0.18.2.dist-info/METADATA +0 -420
  531. crackerjack-0.18.2.dist-info/RECORD +0 -59
  532. crackerjack-0.18.2.dist-info/entry_points.txt +0 -4
  533. {crackerjack-0.18.2.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,938 @@
1
+ import asyncio
2
+ import time
3
+ import typing as t
4
+ from contextlib import suppress
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ from acb.console import Console
9
+ from acb.depends import Inject, depends
10
+ from acb.logger import Logger
11
+
12
+ from crackerjack.config import get_console_width
13
+ from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
14
+ from crackerjack.models.protocols import HookLockManagerProtocol
15
+ from crackerjack.models.task import HookResult
16
+ from crackerjack.services.logging import LoggingContext
17
+
18
+
19
+ @dataclass
20
+ class AsyncHookExecutionResult:
21
+ strategy_name: str
22
+ results: list[HookResult]
23
+ total_duration: float
24
+ success: bool
25
+ concurrent_execution: bool = True
26
+ cache_hits: int = 0
27
+ cache_misses: int = 0
28
+ performance_gain: float = 0.0
29
+
30
+ @property
31
+ def failed_count(self) -> int:
32
+ return sum(1 for r in self.results if r.status == "failed")
33
+
34
+ @property
35
+ def passed_count(self) -> int:
36
+ return sum(1 for r in self.results if r.status == "passed")
37
+
38
+ @property
39
+ def cache_hit_rate(self) -> float:
40
+ total_requests = self.cache_hits + self.cache_misses
41
+ return (self.cache_hits / total_requests * 100) if total_requests > 0 else 0.0
42
+
43
+ @property
44
+ def performance_summary(self) -> dict[str, t.Any]:
45
+ return {
46
+ "total_hooks": len(self.results),
47
+ "passed": self.passed_count,
48
+ "failed": self.failed_count,
49
+ "duration_seconds": round(self.total_duration, 2),
50
+ "concurrent": self.concurrent_execution,
51
+ "cache_hits": self.cache_hits,
52
+ "cache_misses": self.cache_misses,
53
+ "cache_hit_rate_percent": round(self.cache_hit_rate, 1),
54
+ "performance_gain_percent": round(self.performance_gain, 1),
55
+ }
56
+
57
+
58
+ class AsyncHookExecutor:
59
+ @depends.inject
60
+ def __init__(
61
+ self,
62
+ logger: Inject[Logger],
63
+ console: Console,
64
+ pkg_path: Path,
65
+ max_concurrent: int = 4,
66
+ timeout: int = 300,
67
+ quiet: bool = False,
68
+ hook_lock_manager: HookLockManagerProtocol | None = None,
69
+ ) -> None:
70
+ self.console = console
71
+ self.pkg_path = pkg_path
72
+ self.max_concurrent = max_concurrent
73
+ self.timeout = timeout
74
+ self.quiet = quiet
75
+ self.logger = logger
76
+
77
+ self._semaphore = asyncio.Semaphore(max_concurrent)
78
+ self._running_processes: set = set() # Track running subprocesses
79
+ self._last_stdout: bytes | None = None
80
+ self._last_stderr: bytes | None = None
81
+
82
+ if hook_lock_manager is None:
83
+ from crackerjack.executors.hook_lock_manager import (
84
+ hook_lock_manager as default_manager,
85
+ )
86
+
87
+ self.hook_lock_manager: HookLockManagerProtocol = t.cast(
88
+ HookLockManagerProtocol, default_manager
89
+ )
90
+ else:
91
+ self.hook_lock_manager = hook_lock_manager
92
+
93
+ async def execute_strategy(
94
+ self,
95
+ strategy: HookStrategy,
96
+ ) -> AsyncHookExecutionResult:
97
+ with LoggingContext(
98
+ "async_hook_strategy",
99
+ strategy_name=strategy.name,
100
+ hook_count=len(strategy.hooks),
101
+ ):
102
+ start_time = time.time()
103
+ self.logger.info(
104
+ "Starting async hook strategy execution",
105
+ strategy=strategy.name,
106
+ hooks=len(strategy.hooks),
107
+ parallel=strategy.parallel,
108
+ max_workers=getattr(strategy, "max_workers", self.max_concurrent),
109
+ )
110
+
111
+ # Header is displayed by PhaseCoordinator; suppress here to avoid duplicates
112
+
113
+ estimated_sequential = sum(
114
+ getattr(hook, "timeout", 30) for hook in strategy.hooks
115
+ )
116
+
117
+ if strategy.parallel and len(strategy.hooks) > 1:
118
+ results = await self._execute_parallel(strategy)
119
+ else:
120
+ results = await self._execute_sequential(strategy)
121
+
122
+ if strategy.retry_policy != RetryPolicy.NONE:
123
+ results = await self._handle_retries(strategy, results)
124
+
125
+ total_duration = time.time() - start_time
126
+ success = all(r.status == "passed" for r in results)
127
+ performance_gain = max(
128
+ 0,
129
+ ((estimated_sequential - total_duration) / estimated_sequential) * 100,
130
+ )
131
+
132
+ self.logger.info(
133
+ "Async hook strategy completed",
134
+ strategy=strategy.name,
135
+ success=success,
136
+ duration_seconds=round(total_duration, 2),
137
+ performance_gain_percent=round(performance_gain, 1),
138
+ passed=sum(1 for r in results if r.status == "passed"),
139
+ failed=sum(1 for r in results if r.status == "failed"),
140
+ errors=sum(1 for r in results if r.status in ("timeout", "error")),
141
+ )
142
+
143
+ if not self.quiet:
144
+ self._print_summary(strategy, results, success, performance_gain)
145
+
146
+ return AsyncHookExecutionResult(
147
+ strategy_name=strategy.name,
148
+ results=results,
149
+ total_duration=total_duration,
150
+ success=success,
151
+ performance_gain=performance_gain,
152
+ )
153
+
154
+ def get_lock_statistics(self) -> dict[str, t.Any]:
155
+ return self.hook_lock_manager.get_lock_stats()
156
+
157
+ def get_comprehensive_status(self) -> dict[str, t.Any]:
158
+ return {
159
+ "executor_config": {
160
+ "max_concurrent": self.max_concurrent,
161
+ "timeout": self.timeout,
162
+ "quiet": self.quiet,
163
+ },
164
+ "lock_manager_status": self.hook_lock_manager.get_lock_stats(),
165
+ }
166
+
167
+ def _print_strategy_header(self, strategy: HookStrategy) -> None:
168
+ # Intentionally no-op: PhaseCoordinator controls stage headers
169
+ return None
170
+
171
+ async def _execute_sequential(self, strategy: HookStrategy) -> list[HookResult]:
172
+ results: list[HookResult] = []
173
+ for hook in strategy.hooks:
174
+ result = await self._execute_single_hook(hook)
175
+ results.append(result)
176
+ self._display_hook_result(result)
177
+ return results
178
+
179
+ async def _execute_parallel(self, strategy: HookStrategy) -> list[HookResult]:
180
+ results: list[HookResult] = []
181
+
182
+ formatting_hooks = [
183
+ h for h in strategy.hooks if getattr(h, "is_formatting", False)
184
+ ]
185
+ other_hooks = [
186
+ h for h in strategy.hooks if not getattr(h, "is_formatting", False)
187
+ ]
188
+
189
+ for hook in formatting_hooks:
190
+ result = await self._execute_single_hook(hook)
191
+ results.append(result)
192
+ self._display_hook_result(result)
193
+
194
+ if other_hooks:
195
+ tasks = [self._execute_single_hook(hook) for hook in other_hooks]
196
+ parallel_results = await asyncio.gather(*tasks, return_exceptions=True)
197
+
198
+ for i, task_result in enumerate(parallel_results):
199
+ if isinstance(task_result, Exception):
200
+ hook = other_hooks[i]
201
+ error_result = HookResult(
202
+ id=getattr(hook, "name", f"hook_{i}"),
203
+ name=getattr(hook, "name", f"hook_{i}"),
204
+ status="error",
205
+ duration=0.0,
206
+ issues_found=[str(task_result)],
207
+ stage=hook.stage.value,
208
+ )
209
+ results.append(error_result)
210
+ self._display_hook_result(error_result)
211
+ else:
212
+ hook_result = t.cast(HookResult, task_result)
213
+ results.append(hook_result)
214
+ self._display_hook_result(hook_result)
215
+
216
+ return results
217
+
218
+ async def cleanup(self) -> None:
219
+ """Clean up any remaining resources before event loop closes."""
220
+ await self._cleanup_running_processes()
221
+ self._running_processes.clear()
222
+ await self._cleanup_pending_tasks()
223
+
224
+ async def _cleanup_running_processes(self) -> None:
225
+ """Terminate all running subprocesses."""
226
+ for proc in list(self._running_processes):
227
+ await self._terminate_single_process(proc)
228
+
229
+ async def _terminate_single_process(self, proc: asyncio.subprocess.Process) -> None:
230
+ """Terminate a single subprocess safely."""
231
+ try:
232
+ if proc.returncode is None:
233
+ proc.kill()
234
+ await self._wait_for_process_termination(proc)
235
+ except ProcessLookupError:
236
+ pass
237
+ except Exception:
238
+ pass
239
+
240
+ async def _wait_for_process_termination(
241
+ self, proc: asyncio.subprocess.Process
242
+ ) -> None:
243
+ """Wait briefly for process to terminate."""
244
+ with suppress(TimeoutError, RuntimeError):
245
+ await asyncio.wait_for(proc.wait(), timeout=0.1)
246
+
247
+ async def _cleanup_pending_tasks(self) -> None:
248
+ """Cancel any pending hook-related tasks."""
249
+ with suppress(RuntimeError):
250
+ loop = asyncio.get_running_loop()
251
+ pending_tasks = self._get_pending_hook_tasks(loop)
252
+ await self._cancel_tasks(pending_tasks)
253
+
254
+ def _get_pending_hook_tasks(self, loop: asyncio.AbstractEventLoop) -> list:
255
+ """Get list of pending hook-related tasks."""
256
+ return [
257
+ task
258
+ for task in asyncio.all_tasks(loop)
259
+ if not task.done() and "hook" in str(task).lower()
260
+ ]
261
+
262
+ async def _cancel_tasks(self, tasks: list) -> None:
263
+ """Cancel a list of tasks safely."""
264
+ for task in tasks:
265
+ if not task.done():
266
+ await self._cancel_single_task(task)
267
+
268
+ async def _cancel_single_task(self, task: asyncio.Task) -> None:
269
+ """Cancel a single task safely."""
270
+ try:
271
+ task.cancel()
272
+ await asyncio.wait_for(task, timeout=0.1)
273
+ except (TimeoutError, asyncio.CancelledError):
274
+ pass
275
+ except RuntimeError as e:
276
+ if "Event loop is closed" in str(e):
277
+ return
278
+ else:
279
+ raise
280
+
281
+ async def _execute_single_hook(self, hook: HookDefinition) -> HookResult:
282
+ async with self._semaphore:
283
+ if self.hook_lock_manager.requires_lock(hook.name):
284
+ self.logger.debug(
285
+ f"Hook {hook.name} requires sequential execution lock"
286
+ )
287
+ if not self.quiet:
288
+ self.console.print(
289
+ f"[dim]🔒 {hook.name} (sequential execution)[/dim]"
290
+ )
291
+
292
+ if self.hook_lock_manager.requires_lock(hook.name):
293
+ self.logger.debug(
294
+ f"Hook {hook.name} requires sequential execution lock"
295
+ )
296
+ if not self.quiet:
297
+ self.console.print(
298
+ f"[dim]🔒 {hook.name} (sequential execution)[/dim]"
299
+ )
300
+
301
+ async with self.hook_lock_manager.acquire_hook_lock(hook.name):
302
+ return await self._run_hook_subprocess(hook)
303
+ else:
304
+ return await self._run_hook_subprocess(hook)
305
+
306
+ async def _run_hook_subprocess(self, hook: HookDefinition) -> HookResult:
307
+ start_time = time.time()
308
+
309
+ try:
310
+ cmd = hook.get_command() if hasattr(hook, "get_command") else [str(hook)]
311
+ timeout_val = getattr(hook, "timeout", self.timeout)
312
+
313
+ self.logger.debug(
314
+ "Starting hook execution",
315
+ hook=hook.name,
316
+ command=" ".join(cmd),
317
+ timeout=timeout_val,
318
+ )
319
+
320
+ repo_root = self._get_repo_root()
321
+ process = await asyncio.create_subprocess_exec(
322
+ *cmd,
323
+ cwd=repo_root,
324
+ stdout=asyncio.subprocess.PIPE,
325
+ stderr=asyncio.subprocess.PIPE,
326
+ )
327
+
328
+ # Track this process for cleanup
329
+ self._running_processes.add(process)
330
+
331
+ result = await self._execute_process_with_timeout(
332
+ process, hook, timeout_val, start_time
333
+ )
334
+ if result is not None:
335
+ return result
336
+
337
+ # Process completed successfully
338
+ duration = time.time() - start_time
339
+ return await self._build_success_result(process, hook, duration)
340
+
341
+ except RuntimeError as e:
342
+ return self._handle_runtime_error(e, hook, start_time)
343
+ except Exception as e:
344
+ return self._handle_general_error(e, hook, start_time)
345
+
346
+ def _get_repo_root(self) -> Path:
347
+ """Determine the repository root directory.
348
+
349
+ Returns pkg_path directly to ensure hooks run in the correct project directory
350
+ regardless of the project name.
351
+ """
352
+ return self.pkg_path
353
+
354
+ async def _execute_process_with_timeout(
355
+ self,
356
+ process: asyncio.subprocess.Process,
357
+ hook: HookDefinition,
358
+ timeout_val: int,
359
+ start_time: float,
360
+ ) -> HookResult | None:
361
+ """Execute process with timeout handling. Returns HookResult on timeout, None on success."""
362
+ try:
363
+ stdout, stderr = await asyncio.wait_for(
364
+ process.communicate(),
365
+ timeout=timeout_val,
366
+ )
367
+ # Process completed normally - remove from tracking
368
+ self._running_processes.discard(process)
369
+ # Store output for later use
370
+ self._last_stdout = stdout
371
+ self._last_stderr = stderr
372
+ return None
373
+ except TimeoutError:
374
+ return await self._handle_process_timeout(
375
+ process, hook, timeout_val, start_time
376
+ )
377
+
378
+ async def _handle_process_timeout(
379
+ self,
380
+ process: asyncio.subprocess.Process,
381
+ hook: HookDefinition,
382
+ timeout_val: int,
383
+ start_time: float,
384
+ ) -> HookResult:
385
+ """Handle process timeout by killing process and returning timeout result."""
386
+ await self._terminate_process_safely(process, hook)
387
+ duration = time.time() - start_time
388
+
389
+ self.logger.warning(
390
+ "Hook execution timed out",
391
+ hook=hook.name,
392
+ timeout=timeout_val,
393
+ duration_seconds=round(duration, 2),
394
+ )
395
+
396
+ return HookResult(
397
+ id=hook.name,
398
+ name=hook.name,
399
+ status="timeout",
400
+ duration=duration,
401
+ issues_found=[f"Hook timed out after {duration: .1f}s"],
402
+ issues_count=1, # Timeout counts as 1 issue
403
+ stage=hook.stage.value,
404
+ exit_code=124, # Standard timeout exit code
405
+ error_message=f"Hook execution exceeded timeout of {timeout_val}s",
406
+ is_timeout=True,
407
+ )
408
+
409
+ async def _terminate_process_safely(
410
+ self,
411
+ process: asyncio.subprocess.Process,
412
+ hook: HookDefinition,
413
+ ) -> None:
414
+ """Safely terminate a process and handle termination errors."""
415
+ try:
416
+ process.kill()
417
+ await asyncio.wait_for(process.wait(), timeout=0.1)
418
+ self._running_processes.discard(process)
419
+ except (TimeoutError, RuntimeError) as e_wait:
420
+ self._log_termination_error(e_wait, hook)
421
+ self._running_processes.discard(process)
422
+
423
+ def _log_termination_error(
424
+ self,
425
+ error: Exception,
426
+ hook: HookDefinition,
427
+ ) -> None:
428
+ """Log process termination errors appropriately."""
429
+ error_str = str(error)
430
+ if "Event loop is closed" in error_str:
431
+ self.logger.debug(
432
+ "Event loop closed while waiting for process termination",
433
+ hook=hook.name,
434
+ )
435
+ elif "handle" in error_str.lower() or "pid" in error_str.lower():
436
+ self.logger.debug(
437
+ "Subprocess handle issue during termination",
438
+ hook=hook.name,
439
+ )
440
+
441
+ async def _build_success_result(
442
+ self,
443
+ process: asyncio.subprocess.Process,
444
+ hook: HookDefinition,
445
+ duration: float,
446
+ ) -> HookResult:
447
+ """Build HookResult from successful process execution."""
448
+ output_text = self._decode_process_output(self._last_stdout, self._last_stderr)
449
+ return_code = process.returncode if process.returncode is not None else -1
450
+ parsed_output = self._parse_hook_output(return_code, output_text, hook.name)
451
+
452
+ status = "passed" if return_code == 0 else "failed"
453
+
454
+ self.logger.info(
455
+ "Hook execution completed",
456
+ hook=hook.name,
457
+ status=status,
458
+ duration_seconds=round(duration, 2),
459
+ return_code=process.returncode,
460
+ files_processed=parsed_output.get("files_processed", 0),
461
+ issues_count=len(parsed_output.get("issues", [])),
462
+ )
463
+
464
+ issues = parsed_output.get("issues", [])
465
+ # If hook failed but has no parsed issues, use raw output as error details
466
+ if status == "failed" and not issues and output_text:
467
+ # Split output into lines and take first 10 non-empty lines as issues
468
+ error_lines = [
469
+ line.strip() for line in output_text.split("\n") if line.strip()
470
+ ][:10]
471
+ issues = error_lines or ["Hook failed with non-zero exit code"]
472
+
473
+ # Ensure failed hooks always have at least 1 issue count
474
+ issues_count = max(len(issues), 1 if status == "failed" else 0)
475
+
476
+ return HookResult(
477
+ id=parsed_output.get("hook_id", hook.name),
478
+ name=hook.name,
479
+ status=status,
480
+ duration=duration,
481
+ files_processed=parsed_output.get("files_processed", 0),
482
+ issues_found=issues,
483
+ issues_count=issues_count,
484
+ stage=hook.stage.value,
485
+ exit_code=return_code, # Include exit code for debugging
486
+ error_message=output_text[:500]
487
+ if status == "failed" and output_text
488
+ else None, # First 500 chars of error
489
+ is_timeout=False,
490
+ )
491
+
492
+ def _decode_process_output(self, stdout: bytes | None, stderr: bytes | None) -> str:
493
+ """Decode process stdout and stderr into a single string."""
494
+ stdout_text = stdout.decode() if stdout else ""
495
+ stderr_text = stderr.decode() if stderr else ""
496
+ return stdout_text + stderr_text
497
+
498
+ def _handle_runtime_error(
499
+ self,
500
+ error: RuntimeError,
501
+ hook: HookDefinition,
502
+ start_time: float,
503
+ ) -> HookResult:
504
+ """Handle RuntimeError during hook execution."""
505
+ if "Event loop is closed" in str(error):
506
+ duration = time.time() - start_time
507
+ self.logger.warning(
508
+ "Event loop closed during hook execution, returning error",
509
+ hook=hook.name,
510
+ duration_seconds=round(duration, 2),
511
+ )
512
+ return HookResult(
513
+ id=hook.name,
514
+ name=hook.name,
515
+ status="error",
516
+ duration=duration,
517
+ issues_found=["Event loop closed during execution"],
518
+ issues_count=1, # Error counts as 1 issue
519
+ stage=hook.stage.value,
520
+ exit_code=1,
521
+ error_message="Event loop closed during hook execution",
522
+ is_timeout=False,
523
+ )
524
+ else:
525
+ raise
526
+
527
+ def _handle_general_error(
528
+ self,
529
+ error: Exception,
530
+ hook: HookDefinition,
531
+ start_time: float,
532
+ ) -> HookResult:
533
+ """Handle general exceptions during hook execution."""
534
+ duration = time.time() - start_time
535
+ self.logger.exception(
536
+ "Hook execution failed with exception",
537
+ hook=hook.name,
538
+ error=str(error),
539
+ error_type=type(error).__name__,
540
+ duration_seconds=round(duration, 2),
541
+ )
542
+ return HookResult(
543
+ id=hook.name,
544
+ name=hook.name,
545
+ status="error",
546
+ duration=duration,
547
+ issues_found=[str(error)],
548
+ issues_count=1, # Error counts as 1 issue
549
+ stage=hook.stage.value,
550
+ exit_code=1,
551
+ error_message=f"{type(error).__name__}: {error}",
552
+ is_timeout=False,
553
+ )
554
+
555
+ def _parse_semgrep_output_async(self, output: str) -> int:
556
+ """Parse Semgrep output to count files with issues, not total files scanned."""
557
+
558
+ # Try JSON parsing first
559
+ json_result = self._try_parse_semgrep_json(output)
560
+ if json_result is not None:
561
+ return json_result
562
+
563
+ # Fall back to text pattern matching
564
+ return self._parse_semgrep_text_patterns(output)
565
+
566
+ def _try_parse_semgrep_json(self, output: str) -> int | None:
567
+ """Try to parse Semgrep JSON output."""
568
+
569
+ try:
570
+ stripped_output = output.strip()
571
+
572
+ # Try parsing entire output as JSON
573
+ if stripped_output.startswith("{"):
574
+ count = self._extract_file_count_from_json(stripped_output)
575
+ if count is not None:
576
+ return count
577
+
578
+ # Try line-by-line JSON parsing
579
+ return self._parse_semgrep_json_lines(output)
580
+ except Exception:
581
+ return None
582
+
583
+ def _extract_file_count_from_json(self, json_str: str) -> int | None:
584
+ """Extract file count from JSON string."""
585
+ import json
586
+
587
+ try:
588
+ json_data = json.loads(json_str)
589
+ if "results" in json_data:
590
+ file_paths = {
591
+ result.get("path") for result in json_data.get("results", [])
592
+ }
593
+ return len([p for p in file_paths if p])
594
+ except json.JSONDecodeError:
595
+ pass
596
+ return None
597
+
598
+ def _parse_semgrep_json_lines(self, output: str) -> int | None:
599
+ """Parse JSON from individual lines in output."""
600
+
601
+ lines = output.splitlines()
602
+ for line in lines:
603
+ line = line.strip()
604
+ if line.startswith("{") and line.endswith("}"):
605
+ count = self._extract_file_count_from_json(line)
606
+ if count is not None:
607
+ return count
608
+ return None
609
+
610
+ def _parse_semgrep_text_patterns(self, output: str) -> int:
611
+ """Parse Semgrep text output using regex patterns."""
612
+ import re
613
+
614
+ semgrep_patterns = [
615
+ r"found\s+(\d+)\s+issues?\s+in\s+(\d+)\s+files?",
616
+ r"found\s+no\s+issues",
617
+ r"scanning\s+(\d+)\s+files?",
618
+ ]
619
+
620
+ for pattern in semgrep_patterns:
621
+ matches = re.findall(pattern, output, re.IGNORECASE)
622
+ if matches:
623
+ result = self._process_semgrep_matches(matches, output)
624
+ if result is not None:
625
+ return result
626
+
627
+ return 0
628
+
629
+ def _process_semgrep_matches(self, matches: list, output: str) -> int | None:
630
+ """Process regex matches from Semgrep output."""
631
+ for match in matches:
632
+ if isinstance(match, tuple):
633
+ if len(match) == 2:
634
+ issue_count, file_count = int(match[0]), int(match[1])
635
+ return file_count if issue_count > 0 else 0
636
+ elif len(match) == 1 and "no issues" not in output.lower():
637
+ continue
638
+ elif "no issues" in output.lower():
639
+ return 0
640
+ return None
641
+
642
+ def _parse_semgrep_issues_async(self, output: str) -> list[str]:
643
+ """Parse semgrep JSON output to extract both findings and errors.
644
+
645
+ Semgrep returns JSON with two arrays:
646
+ - "results": Security/code quality findings
647
+ - "errors": Configuration, download, or execution errors
648
+
649
+ This method extracts issues from both arrays to provide comprehensive error reporting.
650
+ """
651
+ import json
652
+
653
+ issues = []
654
+
655
+ try:
656
+ # Try to parse as JSON
657
+ json_data = json.loads(output.strip())
658
+
659
+ # Extract findings from results array
660
+ if "results" in json_data:
661
+ for result in json_data.get("results", []):
662
+ # Format: "file.py:line - rule_id: message"
663
+ path = result.get("path", "unknown")
664
+ line_num = result.get("start", {}).get("line", "?")
665
+ rule_id = result.get("check_id", "unknown-rule")
666
+ message = result.get("extra", {}).get(
667
+ "message", "Security issue detected"
668
+ )
669
+ issues.append(f"{path}:{line_num} - {rule_id}: {message}")
670
+
671
+ # Extract errors from errors array (config errors, download failures, etc.)
672
+ if "errors" in json_data:
673
+ for error in json_data.get("errors", []):
674
+ error_type = error.get("type", "SemgrepError")
675
+ error_msg = error.get("message", str(error))
676
+ issues.append(f"{error_type}: {error_msg}")
677
+
678
+ except json.JSONDecodeError:
679
+ # If JSON parsing fails, return raw output (shouldn't happen with --json flag)
680
+ if output.strip():
681
+ issues = [line.strip() for line in output.split("\n") if line.strip()][
682
+ :10
683
+ ]
684
+
685
+ return issues
686
+
687
+ def _parse_hook_output(
688
+ self, returncode: int, output: str, hook_name: str = ""
689
+ ) -> dict[str, t.Any]:
690
+ """Parse hook output to extract file counts and other metrics.
691
+
692
+ Args:
693
+ returncode: Exit code from the subprocess
694
+ output: Raw output from the hook execution
695
+ hook_name: Name of the hook being executed to allow special handling
696
+
697
+ Returns:
698
+ Dictionary with parsed results including files_processed
699
+ """
700
+ result = self._initialize_parse_result(returncode, output)
701
+
702
+ # Special handling for semgrep
703
+ if hook_name == "semgrep":
704
+ result["files_processed"] = self._parse_semgrep_output_async(output)
705
+ result["issues"] = self._parse_semgrep_issues_async(output)
706
+ return result
707
+
708
+ # Special handling for check-added-large-files
709
+ if hook_name == "check-added-large-files":
710
+ result["files_processed"] = self._parse_large_files_output(
711
+ output, returncode
712
+ )
713
+ return result
714
+
715
+ # General hook parsing
716
+ result["files_processed"] = self._extract_file_count_from_output(output)
717
+ return result
718
+
719
+ def _initialize_parse_result(
720
+ self, returncode: int, output: str
721
+ ) -> dict[str, t.Any]:
722
+ """Initialize result dictionary with default values."""
723
+ return {
724
+ "hook_id": None,
725
+ "exit_code": returncode,
726
+ "files_processed": 0,
727
+ "issues": [],
728
+ "raw_output": output,
729
+ }
730
+
731
+ def _parse_large_files_output(self, output: str, returncode: int) -> int:
732
+ """Parse check-added-large-files output to count files exceeding size limit."""
733
+
734
+ clean_output = output.replace("\\n", "\n").replace("\\t", "\t")
735
+
736
+ # Try to find explicit failure patterns
737
+ failure_count = self._find_large_file_failures(clean_output)
738
+ if failure_count is not None:
739
+ return failure_count
740
+
741
+ # Check for "all files under limit" success case
742
+ if self._is_all_files_under_limit(clean_output, returncode):
743
+ return 0
744
+
745
+ # If hook failed but no pattern matched, assume at least 1 file failed
746
+ if returncode != 0:
747
+ return 1
748
+
749
+ # Default: no large files found
750
+ return 0
751
+
752
+ def _find_large_file_failures(self, clean_output: str) -> int | None:
753
+ """Find count of files that exceeded size limit."""
754
+ import re
755
+
756
+ failure_patterns = [
757
+ r"large file(?:s)? found:?\s*(\d+)",
758
+ r"found\s+(\d+)\s+large file",
759
+ r"(\d+)\s+file(?:s)?\s+exceed(?:ed)?\s+size\s+limit",
760
+ r"(\d+)\s+large file(?:s)?\s+found",
761
+ r"(\d+)\s+file(?:s)?\s+(?:failed|violated|exceeded)",
762
+ ]
763
+
764
+ for pattern in failure_patterns:
765
+ matches = re.findall(pattern, clean_output, re.IGNORECASE)
766
+ if matches:
767
+ return int(max([int(m) for m in matches if m.isdigit()]))
768
+
769
+ return None
770
+
771
+ def _is_all_files_under_limit(self, clean_output: str, returncode: int) -> bool:
772
+ """Check if output indicates all files are under size limit."""
773
+ import re
774
+
775
+ pattern = r"All files are under size limit"
776
+ return bool(re.search(pattern, clean_output, re.IGNORECASE) and returncode == 0)
777
+
778
+ def _extract_file_count_from_output(self, output: str) -> int:
779
+ """Extract file count from general hook output."""
780
+ import re
781
+
782
+ clean_output = output.replace("\\n", "\n").replace("\\t", "\t")
783
+ patterns = self._get_file_count_patterns()
784
+
785
+ all_matches = []
786
+ for pattern in patterns:
787
+ matches = re.findall(pattern, clean_output, re.IGNORECASE)
788
+ if matches:
789
+ all_matches.extend([int(m) for m in matches if m.isdigit()])
790
+
791
+ return max(all_matches) if all_matches else 0
792
+
793
+ def _get_file_count_patterns(self) -> list[str]:
794
+ """Get regex patterns for extracting file counts from hook output."""
795
+ return [
796
+ r"(\d+)\s+files?\s+(?:processed|checked|examined|scanned|formatted|found|affected)",
797
+ r"found\s+(\d+)\s+files?",
798
+ r"(\d+)\s+files?\s+with\s+issues?",
799
+ r"(\d+)\s+files?\s+(?:would\s+be|were)\s+(?:formatted|modified|fixed)",
800
+ r"(\d+)\s+files?\s+would\s+be\s+?(?:formatted|fixed|updated)",
801
+ r"(\d+)\s+files?\s+?(?:formatted|fixed|updated)",
802
+ r"(\d+)\s+files?\s+formatted",
803
+ r"analyzed\s+(\d+)\s+deps",
804
+ r"(\d+)\s+findings?",
805
+ r"(\d+)\s+issues?\s+found",
806
+ r"(\d+)\s+tests ran",
807
+ r"(\d+)\s+files\s+scanned",
808
+ r"Checked\s+(\d+)\s+files?",
809
+ r"for\s+(\d+)\s+files?",
810
+ r"(\d+)\s+files?",
811
+ ]
812
+
813
+ def _display_hook_result(self, result: HookResult) -> None:
814
+ if self.quiet:
815
+ return
816
+ width = get_console_width()
817
+ dots = "." * max(0, (width - len(result.name)))
818
+ status_text = "Passed" if result.status == "passed" else "Failed"
819
+ status_color = "green" if result.status == "passed" else "red"
820
+
821
+ self.console.print(
822
+ f"{result.name}{dots}[{status_color}]{status_text}[/{status_color}]"
823
+ )
824
+
825
+ if result.status != "passed" and result.issues_found:
826
+ for issue in result.issues_found:
827
+ if issue and "raw_output" not in issue:
828
+ self.console.print(issue)
829
+
830
+ async def _handle_retries(
831
+ self,
832
+ strategy: HookStrategy,
833
+ results: list[HookResult],
834
+ ) -> list[HookResult]:
835
+ if strategy.retry_policy == RetryPolicy.FORMATTING_ONLY:
836
+ return await self._retry_formatting_hooks(strategy, results)
837
+ if strategy.retry_policy == RetryPolicy.ALL_HOOKS:
838
+ return await self._retry_all_hooks(strategy, results)
839
+ return results
840
+
841
+ async def _retry_formatting_hooks(
842
+ self,
843
+ strategy: HookStrategy,
844
+ results: list[HookResult],
845
+ ) -> list[HookResult]:
846
+ formatting_hooks_failed: set[str] = set()
847
+
848
+ for i, result in enumerate(results):
849
+ hook = strategy.hooks[i]
850
+ if getattr(hook, "is_formatting", False) and result.status == "failed":
851
+ formatting_hooks_failed.add(hook.name)
852
+
853
+ if not formatting_hooks_failed:
854
+ return results
855
+
856
+ retry_tasks = [self._execute_single_hook(hook) for hook in strategy.hooks]
857
+ retry_results = await asyncio.gather(*retry_tasks, return_exceptions=True)
858
+
859
+ updated_results: list[HookResult] = []
860
+ for i, (prev_result, new_result) in enumerate(
861
+ zip(results, retry_results, strict=False)
862
+ ):
863
+ if isinstance(new_result, Exception):
864
+ hook = strategy.hooks[i]
865
+ error_result = HookResult(
866
+ id=hook.name,
867
+ name=hook.name,
868
+ status="error",
869
+ duration=prev_result.duration,
870
+ issues_found=[str(new_result)],
871
+ stage=hook.stage.value,
872
+ )
873
+ updated_results.append(error_result)
874
+ else:
875
+ hook_result = t.cast("HookResult", new_result)
876
+ hook_result.duration += prev_result.duration
877
+ updated_results.append(hook_result)
878
+
879
+ self._display_hook_result(updated_results[-1])
880
+
881
+ return updated_results
882
+
883
+ async def _retry_all_hooks(
884
+ self,
885
+ strategy: HookStrategy,
886
+ results: list[HookResult],
887
+ ) -> list[HookResult]:
888
+ failed_indices = [i for i, r in enumerate(results) if r.status == "failed"]
889
+
890
+ if not failed_indices:
891
+ return results
892
+
893
+ updated_results = results.copy()
894
+ retry_tasks: list[t.Awaitable[HookResult]] = []
895
+ retry_indices: list[int] = []
896
+
897
+ for i in failed_indices:
898
+ hook = strategy.hooks[i]
899
+ retry_tasks.append(self._execute_single_hook(hook))
900
+ retry_indices.append(i)
901
+
902
+ retry_results = await asyncio.gather(*retry_tasks, return_exceptions=True)
903
+
904
+ for result_idx, new_result in zip(retry_indices, retry_results, strict=False):
905
+ prev_result = results[result_idx]
906
+
907
+ if isinstance(new_result, Exception):
908
+ hook = strategy.hooks[result_idx]
909
+ error_result = HookResult(
910
+ id=hook.name,
911
+ name=hook.name,
912
+ status="error",
913
+ duration=prev_result.duration,
914
+ issues_found=[str(new_result)],
915
+ stage=hook.stage.value,
916
+ )
917
+ updated_results[result_idx] = error_result
918
+ else:
919
+ hook_result = t.cast("HookResult", new_result)
920
+ hook_result.duration += prev_result.duration
921
+ updated_results[result_idx] = hook_result
922
+
923
+ self._display_hook_result(updated_results[result_idx])
924
+
925
+ return updated_results
926
+
927
+ def _print_summary(
928
+ self,
929
+ strategy: HookStrategy,
930
+ results: list[HookResult],
931
+ success: bool,
932
+ performance_gain: float,
933
+ ) -> None:
934
+ if success:
935
+ self.console.print(
936
+ f"[green]✅[/ green] {strategy.name.title()} hooks passed: {len(results)} / {len(results)} "
937
+ f"(async, {performance_gain: .1f} % faster)",
938
+ )