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,1227 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import typing as t
5
+ from pathlib import Path
6
+
7
+ from acb.console import Console
8
+ from acb.depends import Inject, depends
9
+ from acb.logger import Logger
10
+ from rich import box
11
+ from rich.panel import Panel
12
+ from rich.progress import (
13
+ BarColumn,
14
+ MofNCompleteColumn,
15
+ Progress,
16
+ SpinnerColumn,
17
+ TextColumn,
18
+ TimeElapsedColumn,
19
+ )
20
+ from rich.table import Table
21
+
22
+ from crackerjack.cli.formatting import separator as make_separator
23
+ from crackerjack.code_cleaner import CodeCleaner
24
+ from crackerjack.config import get_console_width
25
+ from crackerjack.core.autofix_coordinator import AutofixCoordinator
26
+ from crackerjack.core.session_coordinator import SessionCoordinator
27
+ from crackerjack.decorators import handle_errors
28
+ from crackerjack.models.protocols import (
29
+ ConfigMergeServiceProtocol,
30
+ FileSystemInterface,
31
+ GitInterface,
32
+ HookManager,
33
+ MemoryOptimizerProtocol,
34
+ OptionsProtocol,
35
+ PublishManager,
36
+ TestManagerProtocol,
37
+ )
38
+ from crackerjack.models.task import HookResult
39
+ from crackerjack.services.memory_optimizer import create_lazy_service
40
+ from crackerjack.services.monitoring.performance_cache import (
41
+ FileSystemCache,
42
+ GitOperationCache,
43
+ )
44
+ from crackerjack.services.parallel_executor import (
45
+ AsyncCommandExecutor,
46
+ ParallelHookExecutor,
47
+ )
48
+
49
+ if t.TYPE_CHECKING:
50
+ pass # All imports moved to top-level for runtime availability
51
+
52
+
53
+ class PhaseCoordinator:
54
+ @depends.inject
55
+ def __init__(
56
+ self,
57
+ console: Inject[Console],
58
+ logger: Inject[Logger],
59
+ memory_optimizer: Inject[MemoryOptimizerProtocol],
60
+ parallel_executor: Inject[ParallelHookExecutor],
61
+ async_executor: Inject[AsyncCommandExecutor],
62
+ git_cache: Inject[GitOperationCache],
63
+ filesystem_cache: Inject[FileSystemCache],
64
+ pkg_path: Inject[Path],
65
+ session: Inject[SessionCoordinator],
66
+ filesystem: Inject[FileSystemInterface],
67
+ git_service: Inject[GitInterface],
68
+ hook_manager: Inject[HookManager],
69
+ test_manager: Inject[TestManagerProtocol],
70
+ publish_manager: Inject[PublishManager],
71
+ config_merge_service: Inject[ConfigMergeServiceProtocol],
72
+ ) -> None:
73
+ self.console = console
74
+ self.pkg_path = pkg_path
75
+ self.session = session
76
+
77
+ # Dependencies injected via ACB's depends.get() from WorkflowOrchestrator
78
+ self.filesystem = filesystem
79
+ self.git_service = git_service
80
+ self.hook_manager = hook_manager
81
+ self.test_manager = test_manager
82
+ self.publish_manager = publish_manager
83
+ self.config_merge_service = config_merge_service
84
+
85
+ self.code_cleaner = CodeCleaner(
86
+ console=console,
87
+ base_directory=pkg_path,
88
+ file_processor=None,
89
+ error_handler=None,
90
+ pipeline=None,
91
+ logger=None,
92
+ security_logger=None,
93
+ backup_service=None,
94
+ )
95
+
96
+ # Ensure logger is a proper instance, not an empty tuple, string, or other invalid value
97
+ if isinstance(logger, tuple) and len(logger) == 0:
98
+ # Log this issue for debugging
99
+ print(
100
+ "WARNING: PhaseCoordinator received empty tuple for logger dependency, creating fallback"
101
+ )
102
+ # Import and create a fallback logger if we got an empty tuple
103
+ from acb.logger import Logger as ACBLogger
104
+
105
+ self._logger = ACBLogger()
106
+ elif isinstance(logger, str):
107
+ # Log this issue for debugging
108
+ print(
109
+ f"WARNING: PhaseCoordinator received string for logger dependency: {logger!r}, creating fallback"
110
+ )
111
+ # Import and create a fallback logger if we got a string
112
+ from acb.logger import Logger as ACBLogger
113
+
114
+ self._logger = ACBLogger()
115
+ else:
116
+ self._logger = logger
117
+
118
+ # Services injected via ACB DI
119
+ self._memory_optimizer = memory_optimizer
120
+ self._parallel_executor = parallel_executor
121
+ self._async_executor = async_executor
122
+ self._git_cache = git_cache
123
+ self._filesystem_cache = filesystem_cache
124
+
125
+ self._last_hook_summary: dict[str, t.Any] | None = None
126
+ self._last_hook_results: list[HookResult] = []
127
+
128
+ self._lazy_autofix = create_lazy_service(
129
+ lambda: AutofixCoordinator(pkg_path=pkg_path),
130
+ "autofix_coordinator",
131
+ )
132
+ self.console.print()
133
+
134
+ # Track if fast hooks have already started in this session to prevent duplicates
135
+ self._fast_hooks_started: bool = False
136
+
137
+ @property
138
+ def logger(self) -> Logger:
139
+ """Safely access the logger instance, ensuring it's not an empty tuple or string."""
140
+ if hasattr(self, "_logger") and (
141
+ (isinstance(self._logger, tuple) and len(self._logger) == 0)
142
+ or isinstance(self._logger, str)
143
+ ):
144
+ from acb.logger import Logger as ACBLogger
145
+
146
+ print(
147
+ f"WARNING: PhaseCoordinator logger was invalid type ({type(self._logger).__name__}: {self._logger!r}), creating fresh logger instance"
148
+ )
149
+ self._logger = ACBLogger()
150
+ return self._logger
151
+
152
+ @logger.setter
153
+ def logger(self, value: Logger) -> None:
154
+ """Set the logger instance."""
155
+ self._logger = value
156
+
157
+ # --- Output/formatting helpers -------------------------------------------------
158
+ @staticmethod
159
+ def _strip_ansi(text: str) -> str:
160
+ """Remove ANSI escape sequences (SGR and cursor controls).
161
+
162
+ This is more comprehensive than stripping only color codes ending with 'm'.
163
+ """
164
+ ansi_re = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
165
+ return ansi_re.sub("", text)
166
+
167
+ def _is_plain_output(self) -> bool:
168
+ """Detect if we should avoid rich formatting entirely.
169
+
170
+ Leverages ACB Console's plain-mode flag when available and falls back
171
+ to Rich Console properties when not.
172
+ """
173
+ try:
174
+ if bool(getattr(self.console, "_plain_mode", False)):
175
+ return True
176
+ # Fallback on Rich Console capabilities
177
+ is_tty = bool(getattr(self.console, "is_terminal", True))
178
+ color_system = getattr(self.console, "color_system", None)
179
+ return (not is_tty) or (color_system in (None, "null"))
180
+ except Exception:
181
+ # Prefer plain in ambiguous environments
182
+ return True
183
+
184
+ @handle_errors
185
+ def run_cleaning_phase(self, options: OptionsProtocol) -> bool:
186
+ if not options.clean:
187
+ return True
188
+
189
+ self.session.track_task("cleaning", "Code cleaning")
190
+ self._display_cleaning_header()
191
+ return self._execute_cleaning_process()
192
+
193
+ @handle_errors
194
+ def run_configuration_phase(self, options: OptionsProtocol) -> bool:
195
+ if options.no_config_updates:
196
+ return True
197
+ self.session.track_task("configuration", "Configuration updates")
198
+ self.console.print(
199
+ "[dim]⚙️ Configuration phase skipped (no automated updates defined).[/dim]"
200
+ )
201
+ self.session.complete_task(
202
+ "configuration", "No configuration updates were required."
203
+ )
204
+ return True
205
+
206
+ @handle_errors
207
+ def run_hooks_phase(self, options: OptionsProtocol) -> bool:
208
+ if options.skip_hooks:
209
+ return True
210
+
211
+ if not self.run_fast_hooks_only(options):
212
+ return False
213
+
214
+ return self.run_comprehensive_hooks_only(options)
215
+
216
+ def run_fast_hooks_only(self, options: OptionsProtocol) -> bool:
217
+ if options.skip_hooks:
218
+ self.console.print("[yellow]⚠️[/yellow] Skipping fast hooks (--skip-hooks)")
219
+ return True
220
+
221
+ # Prevent multiple fast-hook runs in a single workflow session unless
222
+ # explicitly reset by post-cleaning sanity check.
223
+ if getattr(self, "_fast_hooks_started", False):
224
+ self.logger.debug("Duplicate fast hooks invocation detected; skipping")
225
+ return True
226
+
227
+ # Mark fast hooks as started immediately to prevent duplicate calls in case of failures
228
+ self._fast_hooks_started = True
229
+ self.session.track_task("hooks_fast", "Fast quality checks")
230
+
231
+ # Fast hooks get 2 attempts (auto-fix on failure), comprehensive hooks run once
232
+ max_attempts = 2
233
+ attempt = 0
234
+
235
+ while attempt < max_attempts:
236
+ attempt += 1
237
+
238
+ # Display stage header for each attempt
239
+ if attempt > 1:
240
+ self.console.print(
241
+ f"\n[yellow]♻️[/yellow] Verification Retry {attempt}/{max_attempts}\n"
242
+ )
243
+
244
+ self._display_hook_phase_header(
245
+ "FAST HOOKS",
246
+ "Formatters, import sorting, and quick static analysis",
247
+ )
248
+
249
+ # Run hooks (now configured to run in fix mode by default)
250
+ success = self._execute_hooks_once(
251
+ "fast", self.hook_manager.run_fast_hooks, options, attempt
252
+ )
253
+
254
+ if success:
255
+ break
256
+
257
+ # Fast iteration mode intentionally avoids retries
258
+ if getattr(options, "fast_iteration", False):
259
+ break
260
+
261
+ # If we have more attempts, continue to retry to verify fixes worked
262
+ if attempt < max_attempts:
263
+ self._display_hook_failures("fast", self._last_hook_results, options)
264
+
265
+ summary = self._last_hook_summary or {}
266
+ details = self._format_hook_summary(summary)
267
+
268
+ if success:
269
+ self.session.complete_task("hooks_fast", details=details)
270
+ else:
271
+ self.session.fail_task("hooks_fast", "Fast hook failures detected")
272
+
273
+ # Ensure fast hooks output is fully rendered before comprehensive hooks start
274
+ self.console.print()
275
+
276
+ return success
277
+
278
+ def run_comprehensive_hooks_only(self, options: OptionsProtocol) -> bool:
279
+ if options.skip_hooks:
280
+ self.console.print(
281
+ "[yellow]⚠️[/yellow] Skipping comprehensive hooks (--skip-hooks)"
282
+ )
283
+ return True
284
+
285
+ self.session.track_task("hooks_comprehensive", "Comprehensive quality checks")
286
+ self._display_hook_phase_header(
287
+ "COMPREHENSIVE HOOKS",
288
+ "Type, security, and complexity checking",
289
+ )
290
+
291
+ # Comprehensive hooks run once (no retry)
292
+ success = self._execute_hooks_once(
293
+ "comprehensive",
294
+ self.hook_manager.run_comprehensive_hooks,
295
+ options,
296
+ attempt=1,
297
+ )
298
+
299
+ if not success:
300
+ self._display_hook_failures(
301
+ "comprehensive", self._last_hook_results, options
302
+ )
303
+
304
+ summary = self._last_hook_summary or {}
305
+ details = self._format_hook_summary(summary)
306
+
307
+ if success:
308
+ self.session.complete_task("hooks_comprehensive", details=details)
309
+ else:
310
+ self.session.fail_task(
311
+ "hooks_comprehensive", "Comprehensive hook failures detected"
312
+ )
313
+
314
+ return success
315
+
316
+ @handle_errors
317
+ def run_testing_phase(self, options: OptionsProtocol) -> bool:
318
+ if not options.test:
319
+ return True
320
+ self.session.track_task("testing", "Test execution")
321
+ self.console.print("\n" + make_separator("-"))
322
+ self.console.print(
323
+ "[bold bright_blue]🧪 TESTS[/bold bright_blue] [bold bright_white]Running test suite[/bold bright_white]",
324
+ )
325
+ self.console.print(make_separator("-") + "\n")
326
+ if not self.test_manager.validate_test_environment():
327
+ self.session.fail_task("testing", "Test environment validation failed")
328
+ return False
329
+ test_success = self.test_manager.run_tests(options)
330
+ if test_success:
331
+ coverage_info = self.test_manager.get_coverage()
332
+ self.session.complete_task(
333
+ "testing",
334
+ f"Tests passed, coverage: {coverage_info.get('total_coverage', 0):.1f}%",
335
+ )
336
+ else:
337
+ self.session.fail_task("testing", "Tests failed")
338
+
339
+ return test_success
340
+
341
+ @handle_errors
342
+ def run_publishing_phase(self, options: OptionsProtocol) -> bool:
343
+ version_type = self._determine_version_type(options)
344
+ if not version_type:
345
+ return True
346
+
347
+ self.session.track_task("publishing", f"Publishing ({version_type})")
348
+ return self._execute_publishing_workflow(options, version_type)
349
+
350
+ @handle_errors
351
+ def run_commit_phase(self, options: OptionsProtocol) -> bool:
352
+ if not options.commit:
353
+ return True
354
+
355
+ # Skip if publishing phase already handled commits
356
+ # (Publishing phase handles both pre-publish and version-bump commits when -c is used)
357
+ version_type = self._determine_version_type(options)
358
+ if version_type:
359
+ # Publishing workflow already committed everything
360
+ self.console.print(
361
+ "[dim]ℹ️ Commit phase skipped (handled by publish workflow)[/dim]"
362
+ )
363
+ return True
364
+
365
+ # Display commit & push header
366
+ self._display_commit_push_header()
367
+ self.session.track_task("commit", "Git commit and push")
368
+ changed_files = self.git_service.get_changed_files()
369
+ if not changed_files:
370
+ return self._handle_no_changes_to_commit()
371
+ commit_message = self._get_commit_message(changed_files, options)
372
+ return self._execute_commit_and_push(changed_files, commit_message)
373
+
374
+ def _execute_hooks_once(
375
+ self,
376
+ suite_name: str,
377
+ hook_runner: t.Callable[[], list[HookResult]],
378
+ options: OptionsProtocol,
379
+ attempt: int,
380
+ ) -> bool:
381
+ """Execute a hook suite once with progress bar (no retry logic - retry is handled at stage level)."""
382
+ self._last_hook_summary = None
383
+ self._last_hook_results = []
384
+
385
+ hook_count = self.hook_manager.get_hook_count(suite_name)
386
+ progress = self._create_progress_bar()
387
+
388
+ callbacks = self._setup_progress_callbacks(progress)
389
+ elapsed_time = self._run_hooks_with_progress(
390
+ suite_name, hook_runner, progress, hook_count, attempt, callbacks
391
+ )
392
+
393
+ if elapsed_time is None:
394
+ return False
395
+
396
+ return self._process_hook_results(suite_name, elapsed_time, attempt)
397
+
398
+ def _create_progress_bar(self) -> Progress:
399
+ """Create compact progress bar for hook execution."""
400
+ return Progress(
401
+ SpinnerColumn(spinner_name="dots"),
402
+ TextColumn("[cyan]{task.description}[/cyan]"),
403
+ BarColumn(bar_width=20),
404
+ MofNCompleteColumn(),
405
+ TimeElapsedColumn(),
406
+ console=self.console,
407
+ transient=True,
408
+ )
409
+
410
+ def _setup_progress_callbacks(
411
+ self, progress: Progress
412
+ ) -> dict[str, t.Callable[[int, int], None] | None | dict[str, t.Any]]:
413
+ """Setup progress callbacks and store originals for restoration."""
414
+ task_id_holder = {"task_id": None}
415
+
416
+ def update_progress(completed: int, total: int) -> None:
417
+ if task_id_holder["task_id"] is not None:
418
+ progress.update(task_id_holder["task_id"], completed=completed)
419
+
420
+ def update_progress_started(started: int, total: int) -> None:
421
+ if task_id_holder["task_id"] is not None:
422
+ progress.update(task_id_holder["task_id"], completed=started)
423
+
424
+ original_callback = getattr(self.hook_manager, "_progress_callback", None)
425
+ original_start_callback = getattr(
426
+ self.hook_manager, "_progress_start_callback", None
427
+ )
428
+
429
+ self.hook_manager._progress_callback = update_progress
430
+ self.hook_manager._progress_start_callback = update_progress_started
431
+
432
+ return {
433
+ "update": update_progress,
434
+ "update_started": update_progress_started,
435
+ "original": original_callback,
436
+ "original_started": original_start_callback,
437
+ "task_id_holder": task_id_holder,
438
+ }
439
+
440
+ def _run_hooks_with_progress(
441
+ self,
442
+ suite_name: str,
443
+ hook_runner: t.Callable[[], list[HookResult]],
444
+ progress: Progress,
445
+ hook_count: int,
446
+ attempt: int,
447
+ callbacks: dict[str, t.Any],
448
+ ) -> float | None:
449
+ """Run hooks with progress tracking, return elapsed time or None on error."""
450
+ try:
451
+ with progress:
452
+ task_id = progress.add_task(
453
+ f"Running {suite_name} hooks:",
454
+ total=hook_count,
455
+ )
456
+ callbacks["task_id_holder"]["task_id"] = task_id
457
+
458
+ import time
459
+
460
+ start_time = time.time()
461
+ hook_results = hook_runner()
462
+ self._last_hook_results = hook_results
463
+ elapsed_time = time.time() - start_time
464
+
465
+ return elapsed_time
466
+
467
+ except Exception as exc:
468
+ self._handle_hook_execution_error(suite_name, exc, attempt)
469
+ return None
470
+ finally:
471
+ self._restore_progress_callbacks(callbacks)
472
+
473
+ def _handle_hook_execution_error(
474
+ self, suite_name: str, exc: Exception, attempt: int
475
+ ) -> None:
476
+ """Handle errors during hook execution."""
477
+ self.console.print(
478
+ f"[red]❌[/red] {suite_name.title()} hooks encountered an unexpected error: {exc}"
479
+ )
480
+ self.logger.exception(
481
+ "Hook execution raised exception",
482
+ extra={"suite": suite_name, "attempt": attempt},
483
+ )
484
+
485
+ def _restore_progress_callbacks(self, callbacks: dict[str, t.Any]) -> None:
486
+ """Restore original progress callbacks."""
487
+ self.hook_manager._progress_callback = callbacks["original"]
488
+ self.hook_manager._progress_start_callback = callbacks["original_started"]
489
+
490
+ def _process_hook_results(
491
+ self, suite_name: str, elapsed_time: float, attempt: int
492
+ ) -> bool:
493
+ """Process hook results and determine success."""
494
+ summary = self.hook_manager.get_hook_summary(
495
+ self._last_hook_results, elapsed_time=elapsed_time
496
+ )
497
+ self._last_hook_summary = summary
498
+ self._report_hook_results(suite_name, self._last_hook_results, summary, attempt)
499
+
500
+ if summary.get("failed", 0) == 0 == summary.get("errors", 0):
501
+ return True
502
+
503
+ self.logger.warning(
504
+ "Hook suite reported failures",
505
+ extra={
506
+ "suite": suite_name,
507
+ "attempt": attempt,
508
+ "failed": summary.get("failed", 0),
509
+ "errors": summary.get("errors", 0),
510
+ },
511
+ )
512
+ return False
513
+
514
+ def _display_hook_phase_header(self, title: str, description: str) -> None:
515
+ sep = make_separator("-")
516
+ self.console.print("\n" + sep)
517
+ # Combine title and description into a single line with leading icon
518
+ pretty_title = title.title()
519
+ message = (
520
+ f"[bold bright_cyan]🔍 {pretty_title}[/bold bright_cyan][bold bright_white]"
521
+ f" - {description}[/bold bright_white]"
522
+ )
523
+ self.console.print(message)
524
+ self.console.print(sep + "\n")
525
+
526
+ def _report_hook_results(
527
+ self,
528
+ suite_name: str,
529
+ results: list[HookResult],
530
+ summary: dict[str, t.Any],
531
+ attempt: int,
532
+ ) -> None:
533
+ total = summary.get("total", 0)
534
+ passed = summary.get("passed", 0)
535
+ failed = summary.get("failed", 0)
536
+ errors = summary.get("errors", 0)
537
+ duration = summary.get("total_duration", 0.0)
538
+
539
+ if total == 0:
540
+ self.console.print(
541
+ f"[yellow]⚠️[/yellow] No {suite_name} hooks are configured for this project."
542
+ )
543
+ return
544
+
545
+ base_message = (
546
+ f"{suite_name.title()} hooks attempt {attempt}: "
547
+ f"{passed}/{total} passed in {duration:.2f}s"
548
+ )
549
+
550
+ if failed or errors:
551
+ self.console.print(f"\n[red]❌[/red] {base_message}\n")
552
+ # Always show a results table to aid debugging when there are failures
553
+ self._render_hook_results_table(suite_name, results)
554
+ else:
555
+ self.console.print(f"\n[green]✅[/green] {base_message}\n")
556
+ self._render_hook_results_table(suite_name, results)
557
+
558
+ def _render_hook_results_table(
559
+ self,
560
+ suite_name: str,
561
+ results: list[HookResult],
562
+ ) -> None:
563
+ if not results:
564
+ return
565
+
566
+ if self._is_plain_output():
567
+ self._render_plain_hook_results(suite_name, results)
568
+ else:
569
+ self._render_rich_hook_results(suite_name, results)
570
+
571
+ def _render_plain_hook_results(
572
+ self, suite_name: str, results: list[HookResult]
573
+ ) -> None:
574
+ """Render hook results in plain text format."""
575
+ self.console.print(f"{suite_name.title()} Hook Results:", highlight=False)
576
+
577
+ stats = self._calculate_hook_statistics(results)
578
+ hooks_to_show = stats["failed_hooks"] or results
579
+
580
+ for result in hooks_to_show:
581
+ self._print_plain_hook_result(result)
582
+
583
+ if not stats["failed_hooks"] and results:
584
+ self._print_plain_summary(stats)
585
+
586
+ self.console.print()
587
+
588
+ def _calculate_hook_statistics(self, results: list[HookResult]) -> dict[str, t.Any]:
589
+ """Calculate statistics from hook results."""
590
+ passed_hooks = [r for r in results if r.status.lower() in {"passed", "success"}]
591
+ failed_hooks = [
592
+ r for r in results if r.status.lower() in {"failed", "error", "timeout"}
593
+ ]
594
+ other_hooks = [
595
+ r
596
+ for r in results
597
+ if r.status.lower()
598
+ not in {"passed", "success", "failed", "error", "timeout"}
599
+ ]
600
+
601
+ # Calculate total issues using issues_count (which may be larger than len(issues_found))
602
+ # Passed hooks always contribute 0 issues
603
+ # Config errors (is_config_error=True) are counted separately
604
+ total_issues = 0
605
+ config_errors = 0
606
+ for r in results:
607
+ if r.status == "passed":
608
+ continue
609
+ # Count config errors separately - they're not code quality issues
610
+ if hasattr(r, "is_config_error") and r.is_config_error:
611
+ config_errors += 1
612
+ continue
613
+ # Use issues_count directly (don't fall back to len(issues_found))
614
+ # because issues_found may contain error detail lines, not actual issues
615
+ if hasattr(r, "issues_count"):
616
+ total_issues += r.issues_count
617
+ elif r.issues_found:
618
+ # Legacy fallback for old HookResults without issues_count
619
+ total_issues += len(r.issues_found)
620
+
621
+ return {
622
+ "total_hooks": len(results),
623
+ "passed_hooks": passed_hooks,
624
+ "failed_hooks": failed_hooks,
625
+ "other_hooks": other_hooks,
626
+ "total_passed": len(passed_hooks),
627
+ "total_failed": len(failed_hooks),
628
+ "total_other": len(other_hooks),
629
+ "total_issues_found": total_issues,
630
+ "config_errors": config_errors,
631
+ }
632
+
633
+ def _print_plain_hook_result(self, result: HookResult) -> None:
634
+ """Print a single hook result in plain format."""
635
+ name = self._strip_ansi(result.name)
636
+ status = result.status.upper()
637
+ duration = f"{result.duration:.2f}s"
638
+
639
+ # Determine issues display (matches Rich table logic)
640
+ if result.status == "passed":
641
+ issues = "0"
642
+ elif hasattr(result, "is_config_error") and result.is_config_error:
643
+ # Config/tool error - show simple symbol instead of misleading count
644
+ issues = "[yellow]![/yellow]"
645
+ else:
646
+ # For failed hooks with code violations, use issues_count
647
+ # Don't fall back to len(issues_found) - it may contain error detail lines
648
+ issues = str(result.issues_count if hasattr(result, "issues_count") else 0)
649
+
650
+ self.console.print(
651
+ f" - {name} :: {status} | {duration} | issues={issues}",
652
+ )
653
+
654
+ def _print_plain_summary(self, stats: dict[str, t.Any]) -> None:
655
+ """Print summary statistics in plain format."""
656
+ issues_text = f"{stats['total_issues_found']} issues found"
657
+ if stats.get("config_errors", 0) > 0:
658
+ issues_text += f" ({stats['config_errors']} config)"
659
+
660
+ self.console.print(
661
+ f" Summary: {stats['total_passed']}/{stats['total_hooks']} hooks passed, {issues_text}",
662
+ highlight=False,
663
+ )
664
+
665
+ def _render_rich_hook_results(
666
+ self, suite_name: str, results: list[HookResult]
667
+ ) -> None:
668
+ """Render hook results in Rich format."""
669
+ stats = self._calculate_hook_statistics(results)
670
+ summary_text = self._build_summary_text(stats)
671
+ table = self._build_results_table(results)
672
+ panel = self._build_results_panel(suite_name, table, summary_text)
673
+
674
+ self.console.print(panel)
675
+
676
+ # Add legend if any config errors are present
677
+ has_config_errors = any(
678
+ hasattr(r, "is_config_error") and r.is_config_error for r in results
679
+ )
680
+ if has_config_errors:
681
+ self.console.print(
682
+ " [dim][yellow]![/yellow] = Configuration or tool error (not code "
683
+ "issues)[/dim]"
684
+ )
685
+
686
+ self.console.print()
687
+
688
+ @staticmethod
689
+ def _build_summary_text(stats: dict[str, t.Any]) -> str:
690
+ """Build summary text for Rich display."""
691
+ summary_text = (
692
+ f"Total: [white]{stats['total_hooks']}[/white] | Passed:"
693
+ f" [green]{stats['total_passed']}[/green] | Failed: [red]{stats['total_failed']}[/red]"
694
+ )
695
+ if stats["total_other"] > 0:
696
+ summary_text += f" | Other: [yellow]{stats['total_other']}[/yellow]"
697
+
698
+ # Show issues found with config count in parentheses if present
699
+ issues_text = f"[white]{stats['total_issues_found']}[/white]"
700
+ if stats.get("config_errors", 0) > 0:
701
+ issues_text += f" [dim]({stats['config_errors']} config)[/dim]"
702
+ summary_text += f" | Issues found: {issues_text}"
703
+ return summary_text
704
+
705
+ def _build_results_table(self, results: list[HookResult]) -> Table:
706
+ """Build Rich table from hook results."""
707
+ table = Table(
708
+ box=box.SIMPLE,
709
+ header_style="bold bright_white",
710
+ expand=True,
711
+ )
712
+ table.add_column("Hook", style="cyan", overflow="fold", min_width=20)
713
+ table.add_column("Status", style="bright_white", min_width=8)
714
+ table.add_column("Duration", justify="right", style="magenta", min_width=10)
715
+ table.add_column("Issues", justify="right", style="bright_white", min_width=8)
716
+
717
+ for result in results:
718
+ status_style = self._status_style(result.status)
719
+ # Passed hooks always show 0 issues (files processed != issues found)
720
+ if result.status == "passed":
721
+ issues_display = "0"
722
+ elif hasattr(result, "is_config_error") and result.is_config_error:
723
+ # Config/tool error - show simple symbol instead of misleading count
724
+ # Using "!" instead of emoji to avoid width issues in terminal
725
+ issues_display = "[yellow]![/yellow]"
726
+ else:
727
+ # For failed hooks with code violations, use issues_count
728
+ # IMPORTANT: Use issues_count directly, don't fall back to len(issues_found)
729
+ # because issues_found may contain display messages that aren't actual issues
730
+ issues_display = str(
731
+ result.issues_count if hasattr(result, "issues_count") else 0
732
+ )
733
+ table.add_row(
734
+ self._strip_ansi(result.name),
735
+ f"[{status_style}]{result.status.upper()}[/{status_style}]",
736
+ f"{result.duration:.2f}s",
737
+ issues_display,
738
+ )
739
+
740
+ return table
741
+
742
+ def _format_issues(self, issues: list[str]) -> list[dict[str, str | int | None]]:
743
+ """Format hook issues into structured dictionaries."""
744
+
745
+ def _format_single_issue(issue):
746
+ if hasattr(issue, "file_path") and hasattr(issue, "line_number"):
747
+ return {
748
+ "file": str(getattr(issue, "file_path", "unknown")),
749
+ "line": getattr(issue, "line_number", 0),
750
+ "message": getattr(issue, "message", str(issue)),
751
+ "code": getattr(issue, "code", None),
752
+ "severity": getattr(issue, "severity", "warning"),
753
+ "suggestion": getattr(issue, "suggestion", None),
754
+ }
755
+
756
+ return {
757
+ "file": "unknown",
758
+ "line": 0,
759
+ "message": str(issue),
760
+ "code": None,
761
+ "severity": "warning",
762
+ "suggestion": None,
763
+ }
764
+
765
+ return [_format_single_issue(issue) for issue in issues]
766
+
767
+ def to_json(self, results: list[HookResult], suite_name: str = "") -> dict:
768
+ """Export hook results as structured JSON for automation.
769
+
770
+ Args:
771
+ results: List of HookResult objects to export
772
+ suite_name: Optional suite name (fast/comprehensive)
773
+
774
+ Returns:
775
+ Dictionary with structured results data
776
+
777
+ Example:
778
+ >>> json_data = coordinator.to_json(results, "comprehensive")
779
+ >>> print(json.dumps(json_data, indent=2))
780
+ """
781
+ return {
782
+ "suite": suite_name,
783
+ "summary": self._calculate_hook_statistics(results),
784
+ "hooks": [
785
+ {
786
+ "name": result.name,
787
+ "status": result.status,
788
+ "duration": round(result.duration, 2),
789
+ "issues_count": len(result.issues_found)
790
+ if result.issues_found
791
+ else 0,
792
+ "issues": self._format_issues(result.issues_found)
793
+ if result.issues_found
794
+ else [],
795
+ }
796
+ for result in results
797
+ ],
798
+ }
799
+
800
+ def _build_results_panel(
801
+ self, suite_name: str, table: Table, summary_text: str
802
+ ) -> Panel:
803
+ """Build Rich panel containing results table."""
804
+ return Panel(
805
+ table,
806
+ title=f"[bold]{suite_name.title()} Hook Results[/bold]",
807
+ subtitle=summary_text,
808
+ border_style="cyan" if suite_name == "fast" else "magenta",
809
+ padding=(0, 1),
810
+ width=get_console_width(),
811
+ expand=True,
812
+ )
813
+
814
+ def _format_failing_hooks(
815
+ self, suite_name: str, results: list[HookResult]
816
+ ) -> list[HookResult]:
817
+ """Get list of failing hooks and print header.
818
+
819
+ Returns:
820
+ List of failing hook results
821
+ """
822
+ failing = [
823
+ result
824
+ for result in results
825
+ if result.status.lower() in {"failed", "error", "timeout"}
826
+ ]
827
+
828
+ if failing:
829
+ self.console.print(
830
+ f"[red]Details for failing {suite_name} hooks:[/red]", highlight=False
831
+ )
832
+
833
+ return failing
834
+
835
+ def _display_issue_details(self, result: HookResult) -> None:
836
+ """Display specific issue details if found."""
837
+ if not result.issues_found:
838
+ return
839
+
840
+ for issue in result.issues_found:
841
+ self.console.print(f" - {self._strip_ansi(issue)}", highlight=False)
842
+
843
+ def _display_timeout_info(self, result: HookResult) -> None:
844
+ """Display timeout information."""
845
+ if result.is_timeout:
846
+ self.console.print(
847
+ " - Hook timed out during execution", highlight=False
848
+ )
849
+
850
+ def _display_exit_code_info(self, result: HookResult) -> None:
851
+ """Display exit code with helpful context."""
852
+ if result.exit_code is not None and result.exit_code != 0:
853
+ exit_msg = f"Exit code: {result.exit_code}"
854
+ # Add helpful context for common exit codes
855
+ if result.exit_code == 137:
856
+ exit_msg += " (killed - possibly timeout or out of memory)"
857
+ elif result.exit_code == 139:
858
+ exit_msg += " (segmentation fault)"
859
+ elif result.exit_code in {126, 127}:
860
+ exit_msg += " (command not found or not executable)"
861
+ self.console.print(f" - {exit_msg}", highlight=False)
862
+
863
+ def _display_error_message(self, result: HookResult) -> None:
864
+ """Display error message preview."""
865
+ if result.error_message:
866
+ # Show first line or first 200 chars of error
867
+ error_preview = result.error_message.split("\n")[0][:200]
868
+ self.console.print(f" - Error: {error_preview}", highlight=False)
869
+
870
+ def _display_generic_failure(self, result: HookResult) -> None:
871
+ """Display generic failure message if no specific details available."""
872
+ if not result.is_timeout and not result.exit_code and not result.error_message:
873
+ self.console.print(
874
+ " - Hook failed with no detailed error information",
875
+ highlight=False,
876
+ )
877
+
878
+ def _display_hook_failures(
879
+ self,
880
+ suite_name: str,
881
+ results: list[HookResult],
882
+ options: OptionsProtocol,
883
+ ) -> None:
884
+ # Show detailed failures if --verbose or --ai-debug flag is set
885
+ if not (options.verbose or getattr(options, "ai_debug", False)):
886
+ return
887
+
888
+ failing = self._format_failing_hooks(suite_name, results)
889
+ if not failing:
890
+ return
891
+
892
+ # Process each failing hook
893
+ for result in failing:
894
+ self._print_single_hook_failure(result)
895
+
896
+ self.console.print()
897
+
898
+ def _print_single_hook_failure(self, result: HookResult) -> None:
899
+ """Print details of a single hook failure."""
900
+ self.console.print(
901
+ f" - [red]{self._strip_ansi(result.name)}[/red] ({result.status})",
902
+ highlight=False,
903
+ )
904
+
905
+ if result.issues_found:
906
+ self._print_hook_issues(result)
907
+ else:
908
+ self._display_failure_reasons(result)
909
+
910
+ def _print_hook_issues(self, result: HookResult) -> None:
911
+ """Print issues found for a hook."""
912
+ # Type assertion: issues_found is never None after __post_init__
913
+ assert result.issues_found is not None
914
+ for issue in result.issues_found:
915
+ # Show the issue with consistent formatting
916
+ self.console.print(f" - {self._strip_ansi(issue)}", highlight=False)
917
+
918
+ def _display_failure_reasons(self, result: HookResult) -> None:
919
+ """Display reasons why a hook failed."""
920
+ self._display_timeout_info(result)
921
+ self._display_exit_code_info(result)
922
+ self._display_error_message(result)
923
+ self._display_generic_failure(result)
924
+
925
+ def _display_cleaning_header(self) -> None:
926
+ sep = make_separator("-")
927
+ self.console.print("\n" + sep)
928
+ self.console.print("[bold bright_green]🧹 CLEANING[/bold bright_green]")
929
+ self.console.print(sep + "\n")
930
+
931
+ def _execute_cleaning_process(self) -> bool:
932
+ py_files = list(self.pkg_path.rglob("*.py"))
933
+ if not py_files:
934
+ return self._handle_no_files_to_clean()
935
+
936
+ cleaned_files = self._clean_python_files(py_files)
937
+ self._report_cleaning_results(cleaned_files)
938
+ return True
939
+
940
+ def _handle_no_files_to_clean(self) -> bool:
941
+ self.console.print("No Python files found to clean")
942
+ self.session.complete_task("cleaning", "No files to clean")
943
+ return True
944
+
945
+ def _clean_python_files(self, files: list[Path]) -> list[str]:
946
+ cleaned_files = []
947
+ for file in files:
948
+ if self.code_cleaner.should_process_file(file):
949
+ result = self.code_cleaner.clean_file(file)
950
+ if result.success:
951
+ cleaned_files.append(str(file))
952
+ return cleaned_files
953
+
954
+ def _report_cleaning_results(self, cleaned_files: list[str]) -> None:
955
+ if cleaned_files:
956
+ self.console.print(f"Cleaned {len(cleaned_files)} files")
957
+ self.session.complete_task(
958
+ "cleaning", f"Cleaned {len(cleaned_files)} files"
959
+ )
960
+ else:
961
+ self.console.print("No cleaning needed for any files")
962
+ self.session.complete_task("cleaning", "No cleaning needed")
963
+
964
+ @staticmethod
965
+ def _determine_version_type(options: OptionsProtocol) -> str | None:
966
+ return options.publish or options.all or options.bump
967
+
968
+ def _execute_publishing_workflow(
969
+ self, options: OptionsProtocol, version_type: str
970
+ ) -> bool:
971
+ # Store reference to current HEAD to allow rollback if needed
972
+ original_head = (
973
+ self.git_service.get_current_commit_hash()
974
+ if hasattr(self.git_service, "get_current_commit_hash")
975
+ else None
976
+ )
977
+
978
+ # STAGE 0: Pre-publish commit if needed
979
+ if not self._handle_pre_publish_commit(options):
980
+ return False
981
+
982
+ # STAGE 1: Version bump
983
+ new_version = self._perform_version_bump(version_type)
984
+ if not new_version:
985
+ return False
986
+
987
+ # STAGE 2: Commit, tag, and push changes
988
+ current_commit_hash = self._commit_version_changes(new_version)
989
+ if not current_commit_hash:
990
+ return False
991
+
992
+ # STAGE 3: Publish to PyPI
993
+ if not self._publish_to_pypi(
994
+ options, new_version, original_head, current_commit_hash
995
+ ):
996
+ return False
997
+
998
+ # Finalize publishing
999
+ self._finalize_publishing(options, new_version)
1000
+
1001
+ self.session.complete_task("publishing", f"Published version {new_version}")
1002
+ return True
1003
+
1004
+ def _handle_pre_publish_commit(self, options: OptionsProtocol) -> bool:
1005
+ """Handle committing existing changes before version bump if needed."""
1006
+ if not options.commit:
1007
+ return True
1008
+
1009
+ existing_changes = self.git_service.get_changed_files()
1010
+ if not existing_changes:
1011
+ return True
1012
+
1013
+ self._display_commit_push_header()
1014
+ self.console.print(
1015
+ "[cyan]ℹ️[/cyan] Committing existing changes before version bump..."
1016
+ )
1017
+ commit_message = self._get_commit_message(existing_changes, options)
1018
+ if not self._execute_commit_and_push(existing_changes, commit_message):
1019
+ self.session.fail_task("publishing", "Failed to commit pre-publish changes")
1020
+ return False
1021
+ self.console.print(
1022
+ "[green]✅[/green] Pre-publish changes committed and pushed\n"
1023
+ )
1024
+ return True
1025
+
1026
+ def _perform_version_bump(self, version_type: str) -> str | None:
1027
+ """Perform the version bump operation."""
1028
+ self._display_version_bump_header()
1029
+
1030
+ new_version = self.publish_manager.bump_version(version_type)
1031
+ if not new_version:
1032
+ self.session.fail_task("publishing", "Version bumping failed")
1033
+ return None
1034
+
1035
+ self.console.print(f"[green]✅[/green] Version bumped to {new_version}")
1036
+ self.console.print(
1037
+ f"[green]✅[/green] Changelog updated for version {new_version}"
1038
+ )
1039
+ return new_version
1040
+
1041
+ def _commit_version_changes(self, new_version: str) -> str | None:
1042
+ """Commit the version changes to git."""
1043
+ self._display_commit_push_header()
1044
+
1045
+ # Stage changes
1046
+ changed_files = self.git_service.get_changed_files()
1047
+ if not changed_files:
1048
+ self.console.print("[yellow]⚠️[/yellow] No changes to stage")
1049
+ self.session.fail_task("publishing", "No changes to commit")
1050
+ return None
1051
+
1052
+ if not self.git_service.add_files(changed_files):
1053
+ self.session.fail_task("publishing", "Failed to stage files")
1054
+ return None
1055
+ self.console.print(f"[green]✅[/green] Staged {len(changed_files)} files")
1056
+
1057
+ # Commit
1058
+ commit_message = f"chore: bump version to {new_version}"
1059
+ if not self.git_service.commit(commit_message):
1060
+ self.session.fail_task("publishing", "Failed to commit changes")
1061
+ return None
1062
+ current_commit_hash = (
1063
+ self.git_service.get_current_commit_hash()
1064
+ if hasattr(self.git_service, "get_current_commit_hash")
1065
+ else None
1066
+ )
1067
+ return current_commit_hash
1068
+
1069
+ def _publish_to_pypi(
1070
+ self,
1071
+ options: OptionsProtocol,
1072
+ new_version: str,
1073
+ original_head: str | None,
1074
+ current_commit_hash: str | None,
1075
+ ) -> bool:
1076
+ """Publish the package to PyPI."""
1077
+ self._display_publish_header()
1078
+
1079
+ # Build and publish package
1080
+ if not self.publish_manager.publish_package():
1081
+ self.session.fail_task("publishing", "Package publishing failed")
1082
+ # Attempt to rollback the version bump commit if publishing fails
1083
+ if current_commit_hash and original_head:
1084
+ self._attempt_rollback_version_bump(original_head, current_commit_hash)
1085
+ return False
1086
+ return True
1087
+
1088
+ def _finalize_publishing(self, options: OptionsProtocol, new_version: str) -> None:
1089
+ """Finalize the publishing process after successful PyPI publishing."""
1090
+ # Create git tag and push only after successful PyPI publishing
1091
+ if not options.no_git_tags:
1092
+ if not self.publish_manager.create_git_tag_local(new_version):
1093
+ self.console.print(
1094
+ f"[yellow]⚠️[/yellow] Failed to create git tag v{new_version}"
1095
+ )
1096
+
1097
+ # Push commit and tag together in single operation only after successful PyPI publishing
1098
+ if not self.git_service.push_with_tags():
1099
+ self.console.print("[yellow]⚠️[/yellow] Push failed. Please push manually")
1100
+ # Not failing the whole workflow for a push failure
1101
+
1102
+ def _attempt_rollback_version_bump(
1103
+ self, original_head: str, current_commit_hash: str
1104
+ ) -> bool:
1105
+ """Attempt to undo the version bump commit if publishing fails."""
1106
+ try:
1107
+ self.console.print(
1108
+ "[yellow]🔄 Attempting to rollback version bump commit...[/yellow]"
1109
+ )
1110
+
1111
+ # Reset to the original HEAD (before version bump commit)
1112
+ result = self.git_service.reset_hard(original_head)
1113
+
1114
+ if result:
1115
+ self.console.print(
1116
+ f"[green]✅ Version bump commit ({current_commit_hash[:8]}...) reverted[/green]"
1117
+ )
1118
+ return True
1119
+ else:
1120
+ self.console.print("[red]❌ Failed to revert version bump commit[/red]")
1121
+ return False
1122
+ except Exception as e:
1123
+ self.console.print(f"[red]❌ Error during version rollback: {e}[/red]")
1124
+ return False
1125
+
1126
+ def _display_version_bump_header(self) -> None:
1127
+ sep = make_separator("-")
1128
+ self.console.print("\n" + sep)
1129
+ self.console.print("[bold bright_cyan]📝 VERSION BUMP[/bold bright_cyan]")
1130
+ self.console.print(sep + "\n")
1131
+
1132
+ def _display_commit_push_header(self) -> None:
1133
+ sep = make_separator("-")
1134
+ self.console.print("\n" + sep)
1135
+ self.console.print("[bold bright_blue]📦 COMMIT & PUSH[/bold bright_blue]")
1136
+ self.console.print(sep + "\n")
1137
+
1138
+ def _display_publish_header(self) -> None:
1139
+ sep = make_separator("-")
1140
+ self.console.print("\n" + sep)
1141
+ self.console.print("[bold bright_green]🚀 PUBLISH TO PYPI[/bold bright_green]")
1142
+ self.console.print(sep + "\n")
1143
+
1144
+ def _handle_no_changes_to_commit(self) -> bool:
1145
+ self.console.print("No changes to commit")
1146
+ self.session.complete_task("commit", "No changes to commit")
1147
+ return True
1148
+
1149
+ def _get_commit_message(
1150
+ self, changed_files: list[str], options: OptionsProtocol
1151
+ ) -> str:
1152
+ suggestions = self.git_service.get_commit_message_suggestions(changed_files)
1153
+ if not suggestions:
1154
+ return "Update project files"
1155
+
1156
+ if not options.interactive:
1157
+ return suggestions[0]
1158
+
1159
+ return self._interactive_commit_message_selection(suggestions)
1160
+
1161
+ def _interactive_commit_message_selection(self, suggestions: list[str]) -> str:
1162
+ self._display_commit_suggestions(suggestions)
1163
+ choice = self.console.input(
1164
+ "\nEnter number, custom message, or press Enter for default: "
1165
+ ).strip()
1166
+ return self._process_commit_choice(choice, suggestions)
1167
+
1168
+ def _display_commit_suggestions(self, suggestions: list[str]) -> None:
1169
+ self.console.print("\n[bold]Commit message suggestions:[/bold]")
1170
+ for i, suggestion in enumerate(suggestions, 1):
1171
+ self.console.print(f" [cyan]{i}[/cyan]: {suggestion}")
1172
+
1173
+ @staticmethod
1174
+ def _process_commit_choice(choice: str, suggestions: list[str]) -> str:
1175
+ if not choice:
1176
+ return suggestions[0]
1177
+ if choice.isdigit() and 1 <= int(choice) <= len(suggestions):
1178
+ return suggestions[int(choice) - 1]
1179
+ return choice
1180
+
1181
+ def _execute_commit_and_push(
1182
+ self, changed_files: list[str], commit_message: str
1183
+ ) -> bool:
1184
+ if not self.git_service.add_files(changed_files):
1185
+ self.session.fail_task("commit", "Failed to add files to git")
1186
+ return False
1187
+ if not self.git_service.commit(commit_message):
1188
+ self.session.fail_task("commit", "Failed to commit files")
1189
+ return False
1190
+ if not self.git_service.push():
1191
+ self.console.print("[yellow]⚠️[/yellow] Push failed. Please push manually")
1192
+ # Not failing the whole workflow for a push failure
1193
+ self.session.complete_task("commit", "Committed and pushed changes")
1194
+ return True
1195
+
1196
+ @staticmethod
1197
+ def _format_hook_summary(summary: dict[str, t.Any]) -> str:
1198
+ if not summary:
1199
+ return "No hooks executed"
1200
+
1201
+ total = summary.get("total", 0)
1202
+ passed = summary.get("passed", 0)
1203
+ failed = summary.get("failed", 0)
1204
+ errors = summary.get("errors", 0)
1205
+ duration = summary.get("total_duration", 0.0)
1206
+
1207
+ parts = [f"{passed}/{total} passed"]
1208
+ if failed:
1209
+ parts.append(f"{failed} failed")
1210
+ if errors:
1211
+ parts.append(f"{errors} errors")
1212
+
1213
+ summary_str = ", ".join(parts)
1214
+ return f"{summary_str} in {duration:.2f}s"
1215
+
1216
+ @staticmethod
1217
+ def _status_style(status: str) -> str:
1218
+ normalized = status.lower()
1219
+ if normalized == "passed":
1220
+ return "green"
1221
+ if normalized in {"failed", "error"}:
1222
+ return "red"
1223
+ if normalized == "timeout":
1224
+ return "yellow"
1225
+ return "bright_white"
1226
+
1227
+ # (All printing is handled by acb.console.Console which supports robust I/O.)