crackerjack 0.37.9__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 (425) hide show
  1. crackerjack/README.md +19 -0
  2. crackerjack/__init__.py +30 -1
  3. crackerjack/__main__.py +342 -1263
  4. crackerjack/adapters/README.md +18 -0
  5. crackerjack/adapters/__init__.py +27 -5
  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/{rust_tool_manager.py → lsp/_manager.py} +3 -3
  27. crackerjack/adapters/{skylos_adapter.py → lsp/skylos.py} +59 -7
  28. crackerjack/adapters/{zuban_adapter.py → lsp/zuban.py} +3 -6
  29. crackerjack/adapters/refactor/README.md +59 -0
  30. crackerjack/adapters/refactor/__init__.py +12 -0
  31. crackerjack/adapters/refactor/creosote.py +318 -0
  32. crackerjack/adapters/refactor/refurb.py +406 -0
  33. crackerjack/adapters/refactor/skylos.py +494 -0
  34. crackerjack/adapters/sast/README.md +132 -0
  35. crackerjack/adapters/sast/__init__.py +32 -0
  36. crackerjack/adapters/sast/_base.py +201 -0
  37. crackerjack/adapters/sast/bandit.py +423 -0
  38. crackerjack/adapters/sast/pyscn.py +405 -0
  39. crackerjack/adapters/sast/semgrep.py +241 -0
  40. crackerjack/adapters/security/README.md +111 -0
  41. crackerjack/adapters/security/__init__.py +17 -0
  42. crackerjack/adapters/security/gitleaks.py +339 -0
  43. crackerjack/adapters/type/README.md +52 -0
  44. crackerjack/adapters/type/__init__.py +12 -0
  45. crackerjack/adapters/type/pyrefly.py +402 -0
  46. crackerjack/adapters/type/ty.py +402 -0
  47. crackerjack/adapters/type/zuban.py +522 -0
  48. crackerjack/adapters/utility/README.md +51 -0
  49. crackerjack/adapters/utility/__init__.py +10 -0
  50. crackerjack/adapters/utility/checks.py +884 -0
  51. crackerjack/agents/README.md +264 -0
  52. crackerjack/agents/__init__.py +40 -12
  53. crackerjack/agents/base.py +1 -0
  54. crackerjack/agents/claude_code_bridge.py +641 -0
  55. crackerjack/agents/coordinator.py +49 -53
  56. crackerjack/agents/dry_agent.py +187 -3
  57. crackerjack/agents/enhanced_coordinator.py +279 -0
  58. crackerjack/agents/enhanced_proactive_agent.py +185 -0
  59. crackerjack/agents/error_middleware.py +53 -0
  60. crackerjack/agents/formatting_agent.py +6 -8
  61. crackerjack/agents/helpers/__init__.py +9 -0
  62. crackerjack/agents/helpers/performance/__init__.py +22 -0
  63. crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
  64. crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
  65. crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
  66. crackerjack/agents/helpers/refactoring/__init__.py +22 -0
  67. crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
  68. crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
  69. crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
  70. crackerjack/agents/helpers/test_creation/__init__.py +19 -0
  71. crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
  72. crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
  73. crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
  74. crackerjack/agents/performance_agent.py +121 -1152
  75. crackerjack/agents/refactoring_agent.py +156 -655
  76. crackerjack/agents/semantic_agent.py +479 -0
  77. crackerjack/agents/semantic_helpers.py +356 -0
  78. crackerjack/agents/test_creation_agent.py +19 -1605
  79. crackerjack/api.py +5 -7
  80. crackerjack/cli/README.md +394 -0
  81. crackerjack/cli/__init__.py +1 -1
  82. crackerjack/cli/cache_handlers.py +23 -18
  83. crackerjack/cli/cache_handlers_enhanced.py +1 -4
  84. crackerjack/cli/facade.py +70 -8
  85. crackerjack/cli/formatting.py +13 -0
  86. crackerjack/cli/handlers/__init__.py +85 -0
  87. crackerjack/cli/handlers/advanced.py +103 -0
  88. crackerjack/cli/handlers/ai_features.py +62 -0
  89. crackerjack/cli/handlers/analytics.py +479 -0
  90. crackerjack/cli/handlers/changelog.py +271 -0
  91. crackerjack/cli/handlers/config_handlers.py +16 -0
  92. crackerjack/cli/handlers/coverage.py +84 -0
  93. crackerjack/cli/handlers/documentation.py +280 -0
  94. crackerjack/cli/handlers/main_handlers.py +497 -0
  95. crackerjack/cli/handlers/monitoring.py +371 -0
  96. crackerjack/cli/handlers.py +249 -49
  97. crackerjack/cli/interactive.py +8 -5
  98. crackerjack/cli/options.py +203 -110
  99. crackerjack/cli/semantic_handlers.py +292 -0
  100. crackerjack/cli/version.py +19 -0
  101. crackerjack/code_cleaner.py +60 -24
  102. crackerjack/config/README.md +472 -0
  103. crackerjack/config/__init__.py +256 -0
  104. crackerjack/config/global_lock_config.py +191 -54
  105. crackerjack/config/hooks.py +188 -16
  106. crackerjack/config/loader.py +239 -0
  107. crackerjack/config/settings.py +141 -0
  108. crackerjack/config/tool_commands.py +331 -0
  109. crackerjack/core/README.md +393 -0
  110. crackerjack/core/async_workflow_orchestrator.py +79 -53
  111. crackerjack/core/autofix_coordinator.py +22 -9
  112. crackerjack/core/container.py +10 -9
  113. crackerjack/core/enhanced_container.py +9 -9
  114. crackerjack/core/performance.py +1 -1
  115. crackerjack/core/performance_monitor.py +5 -3
  116. crackerjack/core/phase_coordinator.py +1018 -634
  117. crackerjack/core/proactive_workflow.py +3 -3
  118. crackerjack/core/retry.py +275 -0
  119. crackerjack/core/service_watchdog.py +167 -23
  120. crackerjack/core/session_coordinator.py +187 -382
  121. crackerjack/core/timeout_manager.py +161 -44
  122. crackerjack/core/workflow/__init__.py +21 -0
  123. crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
  124. crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
  125. crackerjack/core/workflow/workflow_issue_parser.py +714 -0
  126. crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
  127. crackerjack/core/workflow/workflow_security_gates.py +400 -0
  128. crackerjack/core/workflow_orchestrator.py +1247 -953
  129. crackerjack/data/README.md +11 -0
  130. crackerjack/data/__init__.py +8 -0
  131. crackerjack/data/models.py +79 -0
  132. crackerjack/data/repository.py +210 -0
  133. crackerjack/decorators/README.md +180 -0
  134. crackerjack/decorators/__init__.py +35 -0
  135. crackerjack/decorators/error_handling.py +649 -0
  136. crackerjack/decorators/error_handling_decorators.py +334 -0
  137. crackerjack/decorators/helpers.py +58 -0
  138. crackerjack/decorators/patterns.py +281 -0
  139. crackerjack/decorators/utils.py +58 -0
  140. crackerjack/docs/README.md +11 -0
  141. crackerjack/docs/generated/api/CLI_REFERENCE.md +1 -1
  142. crackerjack/documentation/README.md +11 -0
  143. crackerjack/documentation/ai_templates.py +1 -1
  144. crackerjack/documentation/dual_output_generator.py +11 -9
  145. crackerjack/documentation/reference_generator.py +104 -59
  146. crackerjack/dynamic_config.py +52 -61
  147. crackerjack/errors.py +1 -1
  148. crackerjack/events/README.md +11 -0
  149. crackerjack/events/__init__.py +16 -0
  150. crackerjack/events/telemetry.py +175 -0
  151. crackerjack/events/workflow_bus.py +346 -0
  152. crackerjack/exceptions/README.md +301 -0
  153. crackerjack/exceptions/__init__.py +5 -0
  154. crackerjack/exceptions/config.py +4 -0
  155. crackerjack/exceptions/tool_execution_error.py +245 -0
  156. crackerjack/executors/README.md +591 -0
  157. crackerjack/executors/__init__.py +2 -0
  158. crackerjack/executors/async_hook_executor.py +539 -77
  159. crackerjack/executors/cached_hook_executor.py +3 -3
  160. crackerjack/executors/hook_executor.py +967 -102
  161. crackerjack/executors/hook_lock_manager.py +31 -22
  162. crackerjack/executors/individual_hook_executor.py +66 -32
  163. crackerjack/executors/lsp_aware_hook_executor.py +136 -57
  164. crackerjack/executors/progress_hook_executor.py +282 -0
  165. crackerjack/executors/tool_proxy.py +23 -7
  166. crackerjack/hooks/README.md +485 -0
  167. crackerjack/hooks/lsp_hook.py +8 -9
  168. crackerjack/intelligence/README.md +557 -0
  169. crackerjack/interactive.py +37 -10
  170. crackerjack/managers/README.md +369 -0
  171. crackerjack/managers/async_hook_manager.py +41 -57
  172. crackerjack/managers/hook_manager.py +449 -79
  173. crackerjack/managers/publish_manager.py +81 -36
  174. crackerjack/managers/test_command_builder.py +290 -12
  175. crackerjack/managers/test_executor.py +93 -8
  176. crackerjack/managers/test_manager.py +1082 -75
  177. crackerjack/managers/test_progress.py +118 -26
  178. crackerjack/mcp/README.md +374 -0
  179. crackerjack/mcp/cache.py +25 -2
  180. crackerjack/mcp/client_runner.py +35 -18
  181. crackerjack/mcp/context.py +9 -9
  182. crackerjack/mcp/dashboard.py +24 -8
  183. crackerjack/mcp/enhanced_progress_monitor.py +34 -23
  184. crackerjack/mcp/file_monitor.py +27 -6
  185. crackerjack/mcp/progress_components.py +45 -34
  186. crackerjack/mcp/progress_monitor.py +6 -9
  187. crackerjack/mcp/rate_limiter.py +11 -7
  188. crackerjack/mcp/server.py +2 -0
  189. crackerjack/mcp/server_core.py +187 -55
  190. crackerjack/mcp/service_watchdog.py +12 -9
  191. crackerjack/mcp/task_manager.py +2 -2
  192. crackerjack/mcp/tools/README.md +27 -0
  193. crackerjack/mcp/tools/__init__.py +2 -0
  194. crackerjack/mcp/tools/core_tools.py +75 -52
  195. crackerjack/mcp/tools/execution_tools.py +87 -31
  196. crackerjack/mcp/tools/intelligence_tools.py +2 -2
  197. crackerjack/mcp/tools/proactive_tools.py +1 -1
  198. crackerjack/mcp/tools/semantic_tools.py +584 -0
  199. crackerjack/mcp/tools/utility_tools.py +180 -132
  200. crackerjack/mcp/tools/workflow_executor.py +87 -46
  201. crackerjack/mcp/websocket/README.md +31 -0
  202. crackerjack/mcp/websocket/app.py +11 -1
  203. crackerjack/mcp/websocket/event_bridge.py +188 -0
  204. crackerjack/mcp/websocket/jobs.py +27 -4
  205. crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
  206. crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
  207. crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
  208. crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
  209. crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
  210. crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
  211. crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
  212. crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
  213. crackerjack/mcp/websocket/monitoring/factory.py +109 -0
  214. crackerjack/mcp/websocket/monitoring/filters.py +10 -0
  215. crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
  216. crackerjack/mcp/websocket/monitoring/models.py +90 -0
  217. crackerjack/mcp/websocket/monitoring/utils.py +171 -0
  218. crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
  219. crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
  220. crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
  221. crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
  222. crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
  223. crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
  224. crackerjack/mcp/websocket/monitoring_endpoints.py +16 -2930
  225. crackerjack/mcp/websocket/server.py +1 -3
  226. crackerjack/mcp/websocket/websocket_handler.py +107 -6
  227. crackerjack/models/README.md +308 -0
  228. crackerjack/models/__init__.py +10 -1
  229. crackerjack/models/config.py +639 -22
  230. crackerjack/models/config_adapter.py +6 -6
  231. crackerjack/models/protocols.py +1167 -23
  232. crackerjack/models/pydantic_models.py +320 -0
  233. crackerjack/models/qa_config.py +145 -0
  234. crackerjack/models/qa_results.py +134 -0
  235. crackerjack/models/results.py +35 -0
  236. crackerjack/models/semantic_models.py +258 -0
  237. crackerjack/models/task.py +19 -3
  238. crackerjack/models/test_models.py +60 -0
  239. crackerjack/monitoring/README.md +11 -0
  240. crackerjack/monitoring/ai_agent_watchdog.py +5 -4
  241. crackerjack/monitoring/metrics_collector.py +4 -3
  242. crackerjack/monitoring/regression_prevention.py +4 -3
  243. crackerjack/monitoring/websocket_server.py +4 -241
  244. crackerjack/orchestration/README.md +340 -0
  245. crackerjack/orchestration/__init__.py +43 -0
  246. crackerjack/orchestration/advanced_orchestrator.py +20 -67
  247. crackerjack/orchestration/cache/README.md +312 -0
  248. crackerjack/orchestration/cache/__init__.py +37 -0
  249. crackerjack/orchestration/cache/memory_cache.py +338 -0
  250. crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
  251. crackerjack/orchestration/config.py +297 -0
  252. crackerjack/orchestration/coverage_improvement.py +13 -6
  253. crackerjack/orchestration/execution_strategies.py +6 -6
  254. crackerjack/orchestration/hook_orchestrator.py +1398 -0
  255. crackerjack/orchestration/strategies/README.md +401 -0
  256. crackerjack/orchestration/strategies/__init__.py +39 -0
  257. crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
  258. crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
  259. crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
  260. crackerjack/orchestration/test_progress_streamer.py +1 -1
  261. crackerjack/plugins/README.md +11 -0
  262. crackerjack/plugins/hooks.py +3 -2
  263. crackerjack/plugins/loader.py +3 -3
  264. crackerjack/plugins/managers.py +1 -1
  265. crackerjack/py313.py +191 -0
  266. crackerjack/security/README.md +11 -0
  267. crackerjack/services/README.md +374 -0
  268. crackerjack/services/__init__.py +8 -21
  269. crackerjack/services/ai/README.md +295 -0
  270. crackerjack/services/ai/__init__.py +7 -0
  271. crackerjack/services/ai/advanced_optimizer.py +878 -0
  272. crackerjack/services/{contextual_ai_assistant.py → ai/contextual_ai_assistant.py} +5 -3
  273. crackerjack/services/ai/embeddings.py +444 -0
  274. crackerjack/services/ai/intelligent_commit.py +328 -0
  275. crackerjack/services/ai/predictive_analytics.py +510 -0
  276. crackerjack/services/api_extractor.py +5 -3
  277. crackerjack/services/bounded_status_operations.py +45 -5
  278. crackerjack/services/cache.py +249 -318
  279. crackerjack/services/changelog_automation.py +7 -3
  280. crackerjack/services/command_execution_service.py +305 -0
  281. crackerjack/services/config_integrity.py +83 -39
  282. crackerjack/services/config_merge.py +9 -6
  283. crackerjack/services/config_service.py +198 -0
  284. crackerjack/services/config_template.py +13 -26
  285. crackerjack/services/coverage_badge_service.py +6 -4
  286. crackerjack/services/coverage_ratchet.py +53 -27
  287. crackerjack/services/debug.py +18 -7
  288. crackerjack/services/dependency_analyzer.py +4 -4
  289. crackerjack/services/dependency_monitor.py +13 -13
  290. crackerjack/services/documentation_generator.py +4 -2
  291. crackerjack/services/documentation_service.py +62 -33
  292. crackerjack/services/enhanced_filesystem.py +81 -27
  293. crackerjack/services/enterprise_optimizer.py +1 -1
  294. crackerjack/services/error_pattern_analyzer.py +10 -10
  295. crackerjack/services/file_filter.py +221 -0
  296. crackerjack/services/file_hasher.py +5 -7
  297. crackerjack/services/file_io_service.py +361 -0
  298. crackerjack/services/file_modifier.py +615 -0
  299. crackerjack/services/filesystem.py +80 -109
  300. crackerjack/services/git.py +99 -5
  301. crackerjack/services/health_metrics.py +4 -6
  302. crackerjack/services/heatmap_generator.py +12 -3
  303. crackerjack/services/incremental_executor.py +380 -0
  304. crackerjack/services/initialization.py +101 -49
  305. crackerjack/services/log_manager.py +2 -2
  306. crackerjack/services/logging.py +120 -68
  307. crackerjack/services/lsp_client.py +12 -12
  308. crackerjack/services/memory_optimizer.py +27 -22
  309. crackerjack/services/monitoring/README.md +30 -0
  310. crackerjack/services/monitoring/__init__.py +9 -0
  311. crackerjack/services/monitoring/dependency_monitor.py +678 -0
  312. crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
  313. crackerjack/services/monitoring/health_metrics.py +716 -0
  314. crackerjack/services/monitoring/metrics.py +587 -0
  315. crackerjack/services/{performance_benchmarks.py → monitoring/performance_benchmarks.py} +100 -14
  316. crackerjack/services/{performance_cache.py → monitoring/performance_cache.py} +21 -15
  317. crackerjack/services/{performance_monitor.py → monitoring/performance_monitor.py} +10 -6
  318. crackerjack/services/parallel_executor.py +166 -55
  319. crackerjack/services/patterns/__init__.py +142 -0
  320. crackerjack/services/patterns/agents.py +107 -0
  321. crackerjack/services/patterns/code/__init__.py +15 -0
  322. crackerjack/services/patterns/code/detection.py +118 -0
  323. crackerjack/services/patterns/code/imports.py +107 -0
  324. crackerjack/services/patterns/code/paths.py +159 -0
  325. crackerjack/services/patterns/code/performance.py +119 -0
  326. crackerjack/services/patterns/code/replacement.py +36 -0
  327. crackerjack/services/patterns/core.py +212 -0
  328. crackerjack/services/patterns/documentation/__init__.py +14 -0
  329. crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
  330. crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
  331. crackerjack/services/patterns/documentation/docstrings.py +89 -0
  332. crackerjack/services/patterns/formatting.py +226 -0
  333. crackerjack/services/patterns/operations.py +339 -0
  334. crackerjack/services/patterns/security/__init__.py +23 -0
  335. crackerjack/services/patterns/security/code_injection.py +122 -0
  336. crackerjack/services/patterns/security/credentials.py +190 -0
  337. crackerjack/services/patterns/security/path_traversal.py +221 -0
  338. crackerjack/services/patterns/security/unsafe_operations.py +216 -0
  339. crackerjack/services/patterns/templates.py +62 -0
  340. crackerjack/services/patterns/testing/__init__.py +18 -0
  341. crackerjack/services/patterns/testing/error_patterns.py +107 -0
  342. crackerjack/services/patterns/testing/pytest_output.py +126 -0
  343. crackerjack/services/patterns/tool_output/__init__.py +16 -0
  344. crackerjack/services/patterns/tool_output/bandit.py +72 -0
  345. crackerjack/services/patterns/tool_output/other.py +97 -0
  346. crackerjack/services/patterns/tool_output/pyright.py +67 -0
  347. crackerjack/services/patterns/tool_output/ruff.py +44 -0
  348. crackerjack/services/patterns/url_sanitization.py +114 -0
  349. crackerjack/services/patterns/utilities.py +42 -0
  350. crackerjack/services/patterns/utils.py +339 -0
  351. crackerjack/services/patterns/validation.py +46 -0
  352. crackerjack/services/patterns/versioning.py +62 -0
  353. crackerjack/services/predictive_analytics.py +21 -8
  354. crackerjack/services/profiler.py +280 -0
  355. crackerjack/services/quality/README.md +415 -0
  356. crackerjack/services/quality/__init__.py +11 -0
  357. crackerjack/services/quality/anomaly_detector.py +392 -0
  358. crackerjack/services/quality/pattern_cache.py +333 -0
  359. crackerjack/services/quality/pattern_detector.py +479 -0
  360. crackerjack/services/quality/qa_orchestrator.py +491 -0
  361. crackerjack/services/{quality_baseline.py → quality/quality_baseline.py} +163 -2
  362. crackerjack/services/{quality_baseline_enhanced.py → quality/quality_baseline_enhanced.py} +4 -1
  363. crackerjack/services/{quality_intelligence.py → quality/quality_intelligence.py} +180 -16
  364. crackerjack/services/regex_patterns.py +58 -2987
  365. crackerjack/services/regex_utils.py +55 -29
  366. crackerjack/services/secure_status_formatter.py +42 -15
  367. crackerjack/services/secure_subprocess.py +35 -2
  368. crackerjack/services/security.py +16 -8
  369. crackerjack/services/server_manager.py +40 -51
  370. crackerjack/services/smart_scheduling.py +46 -6
  371. crackerjack/services/status_authentication.py +3 -3
  372. crackerjack/services/thread_safe_status_collector.py +1 -0
  373. crackerjack/services/tool_filter.py +368 -0
  374. crackerjack/services/tool_version_service.py +9 -5
  375. crackerjack/services/unified_config.py +43 -351
  376. crackerjack/services/vector_store.py +689 -0
  377. crackerjack/services/version_analyzer.py +6 -4
  378. crackerjack/services/version_checker.py +14 -8
  379. crackerjack/services/zuban_lsp_service.py +5 -4
  380. crackerjack/slash_commands/README.md +11 -0
  381. crackerjack/slash_commands/init.md +2 -12
  382. crackerjack/slash_commands/run.md +84 -50
  383. crackerjack/tools/README.md +11 -0
  384. crackerjack/tools/__init__.py +30 -0
  385. crackerjack/tools/_git_utils.py +105 -0
  386. crackerjack/tools/check_added_large_files.py +139 -0
  387. crackerjack/tools/check_ast.py +105 -0
  388. crackerjack/tools/check_json.py +103 -0
  389. crackerjack/tools/check_jsonschema.py +297 -0
  390. crackerjack/tools/check_toml.py +103 -0
  391. crackerjack/tools/check_yaml.py +110 -0
  392. crackerjack/tools/codespell_wrapper.py +72 -0
  393. crackerjack/tools/end_of_file_fixer.py +202 -0
  394. crackerjack/tools/format_json.py +128 -0
  395. crackerjack/tools/mdformat_wrapper.py +114 -0
  396. crackerjack/tools/trailing_whitespace.py +198 -0
  397. crackerjack/tools/validate_regex_patterns.py +7 -3
  398. crackerjack/ui/README.md +11 -0
  399. crackerjack/ui/dashboard_renderer.py +28 -0
  400. crackerjack/ui/templates/README.md +11 -0
  401. crackerjack/utils/console_utils.py +13 -0
  402. crackerjack/utils/dependency_guard.py +230 -0
  403. crackerjack/utils/retry_utils.py +275 -0
  404. crackerjack/workflows/README.md +590 -0
  405. crackerjack/workflows/__init__.py +46 -0
  406. crackerjack/workflows/actions.py +811 -0
  407. crackerjack/workflows/auto_fix.py +444 -0
  408. crackerjack/workflows/container_builder.py +499 -0
  409. crackerjack/workflows/definitions.py +443 -0
  410. crackerjack/workflows/engine.py +177 -0
  411. crackerjack/workflows/event_bridge.py +242 -0
  412. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/METADATA +678 -98
  413. crackerjack-0.45.2.dist-info/RECORD +478 -0
  414. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
  415. crackerjack/managers/test_manager_backup.py +0 -1075
  416. crackerjack/mcp/tools/execution_tools_backup.py +0 -1011
  417. crackerjack/mixins/__init__.py +0 -3
  418. crackerjack/mixins/error_handling.py +0 -145
  419. crackerjack/services/config.py +0 -358
  420. crackerjack/ui/server_panels.py +0 -125
  421. crackerjack-0.37.9.dist-info/RECORD +0 -231
  422. /crackerjack/adapters/{rust_tool_adapter.py → lsp/_base.py} +0 -0
  423. /crackerjack/adapters/{lsp_client.py → lsp/_client.py} +0 -0
  424. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/entry_points.txt +0 -0
  425. {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
@@ -1,15 +1,19 @@
1
1
  import asyncio
2
2
  import time
3
3
  import typing as t
4
+ from contextlib import suppress
4
5
  from dataclasses import dataclass
5
6
  from pathlib import Path
6
7
 
7
- from rich.console import Console
8
+ from acb.console import Console
9
+ from acb.depends import Inject, depends
10
+ from acb.logger import Logger
8
11
 
12
+ from crackerjack.config import get_console_width
9
13
  from crackerjack.config.hooks import HookDefinition, HookStrategy, RetryPolicy
10
14
  from crackerjack.models.protocols import HookLockManagerProtocol
11
15
  from crackerjack.models.task import HookResult
12
- from crackerjack.services.logging import LoggingContext, get_logger
16
+ from crackerjack.services.logging import LoggingContext
13
17
 
14
18
 
15
19
  @dataclass
@@ -52,8 +56,10 @@ class AsyncHookExecutionResult:
52
56
 
53
57
 
54
58
  class AsyncHookExecutor:
59
+ @depends.inject
55
60
  def __init__(
56
61
  self,
62
+ logger: Inject[Logger],
57
63
  console: Console,
58
64
  pkg_path: Path,
59
65
  max_concurrent: int = 4,
@@ -66,9 +72,12 @@ class AsyncHookExecutor:
66
72
  self.max_concurrent = max_concurrent
67
73
  self.timeout = timeout
68
74
  self.quiet = quiet
69
- self.logger = get_logger("crackerjack.async_hook_executor")
75
+ self.logger = logger
70
76
 
71
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
72
81
 
73
82
  if hook_lock_manager is None:
74
83
  from crackerjack.executors.hook_lock_manager import (
@@ -99,7 +108,7 @@ class AsyncHookExecutor:
99
108
  max_workers=getattr(strategy, "max_workers", self.max_concurrent),
100
109
  )
101
110
 
102
- self._print_strategy_header(strategy)
111
+ # Header is displayed by PhaseCoordinator; suppress here to avoid duplicates
103
112
 
104
113
  estimated_sequential = sum(
105
114
  getattr(hook, "timeout", 30) for hook in strategy.hooks
@@ -156,20 +165,8 @@ class AsyncHookExecutor:
156
165
  }
157
166
 
158
167
  def _print_strategy_header(self, strategy: HookStrategy) -> None:
159
- self.console.print("\n" + "-" * 74)
160
- if strategy.name == "fast":
161
- self.console.print(
162
- "[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running code quality checks (async)[/ bold bright_white]",
163
- )
164
- elif strategy.name == "comprehensive":
165
- self.console.print(
166
- "[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running comprehensive quality checks (async)[/ bold bright_white]",
167
- )
168
- else:
169
- self.console.print(
170
- f"[bold bright_cyan]🔍 HOOKS[/ bold bright_cyan] [bold bright_white]Running {strategy.name} hooks (async)[/ bold bright_white]",
171
- )
172
- self.console.print("-" * 74 + "\n")
168
+ # Intentionally no-op: PhaseCoordinator controls stage headers
169
+ return None
173
170
 
174
171
  async def _execute_sequential(self, strategy: HookStrategy) -> list[HookResult]:
175
172
  results: list[HookResult] = []
@@ -218,6 +215,69 @@ class AsyncHookExecutor:
218
215
 
219
216
  return results
220
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
+
221
281
  async def _execute_single_hook(self, hook: HookDefinition) -> HookResult:
222
282
  async with self._semaphore:
223
283
  if self.hook_lock_manager.requires_lock(hook.name):
@@ -257,11 +317,7 @@ class AsyncHookExecutor:
257
317
  timeout=timeout_val,
258
318
  )
259
319
 
260
- repo_root = (
261
- self.pkg_path.parent
262
- if self.pkg_path.name == "crackerjack"
263
- else self.pkg_path
264
- )
320
+ repo_root = self._get_repo_root()
265
321
  process = await asyncio.create_subprocess_exec(
266
322
  *cmd,
267
323
  cwd=repo_root,
@@ -269,68 +325,188 @@ class AsyncHookExecutor:
269
325
  stderr=asyncio.subprocess.PIPE,
270
326
  )
271
327
 
272
- try:
273
- stdout, stderr = await asyncio.wait_for(
274
- process.communicate(),
275
- timeout=timeout_val,
276
- )
277
- except TimeoutError:
278
- process.kill()
279
- await process.wait()
280
- duration = time.time() - start_time
281
-
282
- self.logger.warning(
283
- "Hook execution timed out",
284
- hook=hook.name,
285
- timeout=timeout_val,
286
- duration_seconds=round(duration, 2),
287
- )
328
+ # Track this process for cleanup
329
+ self._running_processes.add(process)
288
330
 
289
- return HookResult(
290
- id=hook.name,
291
- name=hook.name,
292
- status="timeout",
293
- duration=duration,
294
- issues_found=[f"Hook timed out after {duration: .1f}s"],
295
- stage=hook.stage.value,
296
- )
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
297
336
 
337
+ # Process completed successfully
298
338
  duration = time.time() - start_time
299
- output_text = (
300
- (stdout.decode() + stderr.decode()) if stdout and stderr else ""
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,
301
366
  )
302
- return_code = process.returncode if process.returncode is not None else -1
303
- parsed_output = self._parse_hook_output(return_code, output_text)
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
+ )
304
395
 
305
- status = "passed" if return_code == 0 else "failed"
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
+ )
306
408
 
307
- self.logger.info(
308
- "Hook execution completed",
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",
309
433
  hook=hook.name,
310
- status=status,
311
- duration_seconds=round(duration, 2),
312
- return_code=process.returncode,
313
- files_processed=parsed_output.get("files_processed", 0),
314
- issues_count=len(parsed_output.get("issues", [])),
315
434
  )
316
-
317
- return HookResult(
318
- id=parsed_output.get("hook_id", hook.name),
319
- name=hook.name,
320
- status=status,
321
- duration=duration,
322
- files_processed=parsed_output.get("files_processed", 0),
323
- issues_found=parsed_output.get("issues", []),
324
- stage=hook.stage.value,
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,
325
439
  )
326
440
 
327
- except Exception as e:
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):
328
506
  duration = time.time() - start_time
329
- self.logger.exception(
330
- "Hook execution failed with exception",
507
+ self.logger.warning(
508
+ "Event loop closed during hook execution, returning error",
331
509
  hook=hook.name,
332
- error=str(e),
333
- error_type=type(e).__name__,
334
510
  duration_seconds=round(duration, 2),
335
511
  )
336
512
  return HookResult(
@@ -338,11 +514,212 @@ class AsyncHookExecutor:
338
514
  name=hook.name,
339
515
  status="error",
340
516
  duration=duration,
341
- issues_found=[str(e)],
517
+ issues_found=["Event loop closed during execution"],
518
+ issues_count=1, # Error counts as 1 issue
342
519
  stage=hook.stage.value,
520
+ exit_code=1,
521
+ error_message="Event loop closed during hook execution",
522
+ is_timeout=False,
343
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()
344
571
 
345
- def _parse_hook_output(self, returncode: int, output: str) -> dict[str, t.Any]:
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."""
346
723
  return {
347
724
  "hook_id": None,
348
725
  "exit_code": returncode,
@@ -351,13 +728,98 @@ class AsyncHookExecutor:
351
728
  "raw_output": output,
352
729
  }
353
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
+
354
813
  def _display_hook_result(self, result: HookResult) -> None:
355
- dots = "." * (60 - len(result.name))
814
+ if self.quiet:
815
+ return
816
+ width = get_console_width()
817
+ dots = "." * max(0, (width - len(result.name)))
356
818
  status_text = "Passed" if result.status == "passed" else "Failed"
357
819
  status_color = "green" if result.status == "passed" else "red"
358
820
 
359
821
  self.console.print(
360
- f"{result.name}{dots}[{status_color}]{status_text}[/{status_color}]",
822
+ f"{result.name}{dots}[{status_color}]{status_text}[/{status_color}]"
361
823
  )
362
824
 
363
825
  if result.status != "passed" and result.issues_found: