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
@@ -0,0 +1,716 @@
1
+ import asyncio
2
+ import json
3
+ import subprocess
4
+ import time
5
+ import tomllib
6
+ import typing as t
7
+ from contextlib import suppress
8
+ from dataclasses import dataclass, field
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from urllib.parse import urlparse
13
+
14
+ import requests
15
+ from acb.console import Console
16
+ from acb.depends import Inject, depends
17
+
18
+ from crackerjack.data.repository import HealthMetricsRepository
19
+ from crackerjack.models.protocols import (
20
+ FileSystemInterface,
21
+ HealthMetricsServiceProtocol,
22
+ ServiceProtocol,
23
+ )
24
+
25
+
26
+ @dataclass
27
+ class ProjectHealth:
28
+ lint_error_trend: list[int] = field(default_factory=list)
29
+ test_coverage_trend: list[float] = field(default_factory=list)
30
+ dependency_age: dict[str, int] = field(default_factory=dict[str, t.Any])
31
+ config_completeness: float = 0.0
32
+ last_updated: float = field(default_factory=time.time)
33
+
34
+ def needs_init(self) -> bool:
35
+ if self._is_trending_up(self.lint_error_trend):
36
+ return True
37
+
38
+ if self._is_trending_down(self.test_coverage_trend):
39
+ return True
40
+
41
+ if any(age > 180 for age in self.dependency_age.values()):
42
+ return True
43
+
44
+ return self.config_completeness < 0.8
45
+
46
+ def _is_trending_up(
47
+ self, values: list[int] | list[float], min_points: int = 3
48
+ ) -> bool:
49
+ if len(values) < min_points:
50
+ return False
51
+
52
+ recent = values[-min_points:]
53
+
54
+ return all(a <= b for a, b in zip(recent, recent[1:]))
55
+
56
+ def _is_trending_down(
57
+ self, values: list[int] | list[float], min_points: int = 3
58
+ ) -> bool:
59
+ if len(values) < min_points:
60
+ return False
61
+
62
+ recent = values[-min_points:]
63
+
64
+ return all(a >= b for a, b in zip(recent, recent[1:]))
65
+
66
+ def get_health_score(self) -> float:
67
+ scores: list[float] = []
68
+
69
+ if self.lint_error_trend:
70
+ recent_errors = sum(self.lint_error_trend[-5:]) / min(
71
+ len(self.lint_error_trend),
72
+ 5,
73
+ )
74
+ lint_score = max(0, 1.0 - (recent_errors / 100))
75
+ scores.append(lint_score)
76
+
77
+ if self.test_coverage_trend:
78
+ recent_coverage = sum(self.test_coverage_trend[-5:]) / min(
79
+ len(self.test_coverage_trend),
80
+ 5,
81
+ )
82
+ coverage_score = recent_coverage / 100.0
83
+ scores.append(coverage_score)
84
+
85
+ if self.dependency_age:
86
+ avg_age = sum(self.dependency_age.values()) / len(self.dependency_age)
87
+
88
+ dependency_score = max(0, 1.0 - (avg_age / 365))
89
+ scores.append(dependency_score)
90
+
91
+ scores.append(self.config_completeness)
92
+
93
+ return sum(scores) / len(scores) if scores else 0.0
94
+
95
+ def get_recommendations(self) -> list[str]:
96
+ recommendations: list[str] = []
97
+
98
+ if self._is_trending_up(self.lint_error_trend):
99
+ recommendations.append(
100
+ "🔧 Lint errors are increasing-consider running formatting tools",
101
+ )
102
+
103
+ if self._is_trending_down(self.test_coverage_trend):
104
+ recommendations.append("🧪 Test coverage is declining-add more tests")
105
+
106
+ if any(age > 365 for age in self.dependency_age.values()):
107
+ old_deps: list[str] = [
108
+ pkg for pkg, age in self.dependency_age.items() if age > 365
109
+ ]
110
+ recommendations.append(
111
+ f"📦 Very old dependencies detected: {', '.join(old_deps[:3])}",
112
+ )
113
+
114
+ if self.config_completeness < 0.5:
115
+ recommendations.append(
116
+ "⚙️ Project configuration is incomplete-run crackerjack init",
117
+ )
118
+ elif self.config_completeness < 0.8:
119
+ recommendations.append("⚙️ Project configuration could be improved")
120
+
121
+ if len(self.lint_error_trend) > 10:
122
+ recent_avg = sum(self.lint_error_trend[-5:]) / 5
123
+ older_avg = sum(self.lint_error_trend[-10:-5]) / 5
124
+ if recent_avg > older_avg * 1.5:
125
+ recommendations.append(
126
+ "📈 Quality is degrading rapidly - immediate attention needed",
127
+ )
128
+
129
+ return recommendations
130
+
131
+
132
+ class HealthMetricsService(HealthMetricsServiceProtocol, ServiceProtocol):
133
+ @depends.inject
134
+ def __init__(
135
+ self,
136
+ filesystem: FileSystemInterface,
137
+ console: Inject[Console],
138
+ ) -> None:
139
+ self.filesystem = filesystem
140
+ self.console = console
141
+ self.project_root = Path.cwd()
142
+ self.pyproject_path = self.project_root / "pyproject.toml"
143
+ self.max_trend_points = 20
144
+ try:
145
+ self._repository: HealthMetricsRepository | None = depends.get_sync(
146
+ HealthMetricsRepository
147
+ )
148
+ except Exception:
149
+ self._repository = None
150
+
151
+ def initialize(self) -> None:
152
+ pass
153
+
154
+ def cleanup(self) -> None:
155
+ pass
156
+
157
+ def health_check(self) -> bool:
158
+ return True
159
+
160
+ def shutdown(self) -> None:
161
+ pass
162
+
163
+ def metrics(self) -> dict[str, t.Any]:
164
+ return {}
165
+
166
+ def is_healthy(self) -> bool:
167
+ return True
168
+
169
+ def register_resource(self, resource: t.Any) -> None:
170
+ pass
171
+
172
+ def cleanup_resource(self, resource: t.Any) -> None:
173
+ pass
174
+
175
+ def record_error(self, error: Exception) -> None:
176
+ pass
177
+
178
+ def increment_requests(self) -> None:
179
+ pass
180
+
181
+ def get_custom_metric(self, name: str) -> t.Any:
182
+ return None
183
+
184
+ def set_custom_metric(self, name: str, value: t.Any) -> None:
185
+ pass
186
+
187
+ def _run_async(self, coro: t.Awaitable[t.Any]) -> t.Any:
188
+ try:
189
+ asyncio.get_running_loop()
190
+ except RuntimeError:
191
+ return asyncio.run(coro)
192
+ msg = (
193
+ "HealthMetricsService synchronous method called while an event loop is running."
194
+ " Use async variants instead."
195
+ )
196
+ raise RuntimeError(msg)
197
+
198
+ def collect_current_metrics(self) -> ProjectHealth:
199
+ health = self._load_health_history()
200
+
201
+ lint_errors = self._count_lint_errors()
202
+ if lint_errors is not None:
203
+ health.lint_error_trend.append(lint_errors)
204
+ health.lint_error_trend = health.lint_error_trend[-self.max_trend_points :]
205
+
206
+ coverage = self._get_test_coverage()
207
+ if coverage is not None:
208
+ health.test_coverage_trend.append(coverage)
209
+ health.test_coverage_trend = health.test_coverage_trend[
210
+ -self.max_trend_points :
211
+ ]
212
+
213
+ health.dependency_age = self._calculate_dependency_ages()
214
+
215
+ health.config_completeness = self._assess_config_completeness()
216
+
217
+ health.last_updated = time.time()
218
+
219
+ self._save_health_metrics(health)
220
+
221
+ return health
222
+
223
+ def _load_health_history(self) -> ProjectHealth:
224
+ if not self._repository:
225
+ return self._load_from_legacy_cache()
226
+
227
+ async def _load() -> ProjectHealth:
228
+ record = await self._repository.get(str(self.project_root))
229
+ if not record:
230
+ legacy = self._load_from_legacy_cache()
231
+ if legacy and self._repository:
232
+ await self._repository.upsert(
233
+ project_root=str(self.project_root),
234
+ data={
235
+ "lint_error_trend": legacy.lint_error_trend.copy(),
236
+ "test_coverage_trend": legacy.test_coverage_trend.copy(),
237
+ "dependency_age": legacy.dependency_age.copy(),
238
+ "config_completeness": legacy.config_completeness,
239
+ "last_updated": datetime.fromtimestamp(
240
+ legacy.last_updated, tz=UTC
241
+ ),
242
+ },
243
+ )
244
+ return legacy or ProjectHealth()
245
+ return ProjectHealth(
246
+ lint_error_trend=(record.lint_error_trend or []).copy(),
247
+ test_coverage_trend=(record.test_coverage_trend or []).copy(),
248
+ dependency_age=(record.dependency_age or {}).copy(),
249
+ config_completeness=record.config_completeness,
250
+ last_updated=record.last_updated.timestamp(),
251
+ )
252
+
253
+ return self._run_async(_load())
254
+
255
+ def _load_from_legacy_cache(self) -> ProjectHealth | None:
256
+ legacy_cache = self.project_root / ".crackerjack" / "health_metrics.json"
257
+ with suppress(Exception):
258
+ if legacy_cache.exists():
259
+ with legacy_cache.open() as f:
260
+ data = json.load(f)
261
+ return ProjectHealth(**data)
262
+ return None
263
+
264
+ def _save_health_metrics(self, health: ProjectHealth) -> None:
265
+ if not self._repository:
266
+ return
267
+
268
+ async def _save() -> None:
269
+ await self._repository.upsert(
270
+ project_root=str(self.project_root),
271
+ data={
272
+ "lint_error_trend": health.lint_error_trend.copy(),
273
+ "test_coverage_trend": health.test_coverage_trend.copy(),
274
+ "dependency_age": health.dependency_age.copy(),
275
+ "config_completeness": health.config_completeness,
276
+ "last_updated": datetime.fromtimestamp(health.last_updated, tz=UTC),
277
+ },
278
+ )
279
+
280
+ try:
281
+ self._run_async(_save())
282
+ except Exception as e:
283
+ self.console.print(
284
+ f"[yellow]Warning: Failed to persist health metrics: {e}[/yellow]",
285
+ )
286
+
287
+ def _count_lint_errors(self) -> int | None:
288
+ with suppress(Exception):
289
+ result = subprocess.run(
290
+ ["uv", "run", "ruff", "check", ".", "- - output-format=json"],
291
+ check=False,
292
+ capture_output=True,
293
+ text=True,
294
+ timeout=30,
295
+ cwd=self.project_root,
296
+ )
297
+
298
+ if result.returncode == 0:
299
+ return 0
300
+
301
+ if result.stdout:
302
+ try:
303
+ lint_data = json.loads(result.stdout)
304
+ return len(lint_data) if isinstance(lint_data, list) else 0
305
+ except json.JSONDecodeError:
306
+ return len(result.stdout.splitlines())
307
+
308
+ return None
309
+
310
+ def _get_test_coverage(self) -> float | None:
311
+ with suppress(Exception):
312
+ existing_coverage = self._check_existing_coverage_files()
313
+ if existing_coverage is not None:
314
+ return existing_coverage
315
+
316
+ generated_coverage = self._generate_coverage_report()
317
+ if generated_coverage is not None:
318
+ return generated_coverage
319
+
320
+ return self._get_coverage_from_command()
321
+
322
+ return None
323
+
324
+ def _check_existing_coverage_files(self) -> float | None:
325
+ coverage_files = [
326
+ self.project_root / ".coverage",
327
+ self.project_root / "htmlcov" / "index.html",
328
+ self.project_root / "coverage.xml",
329
+ ]
330
+
331
+ for coverage_file in coverage_files:
332
+ if coverage_file.exists():
333
+ return self._get_coverage_from_command()
334
+
335
+ return None
336
+
337
+ def _generate_coverage_report(self) -> float | None:
338
+ subprocess.run(
339
+ [
340
+ "uv",
341
+ "run",
342
+ "python",
343
+ "- m",
344
+ "pytest",
345
+ "--cov =.",
346
+ "- - cov-report=json",
347
+ "--tb=no",
348
+ "- q",
349
+ "--maxfail=1",
350
+ ],
351
+ check=False,
352
+ capture_output=True,
353
+ text=True,
354
+ timeout=60,
355
+ cwd=self.project_root,
356
+ )
357
+
358
+ coverage_json = self.project_root / "coverage.json"
359
+ if coverage_json.exists():
360
+ with coverage_json.open() as f:
361
+ data = json.load(f)
362
+ return float(data.get("totals", {}).get("percent_covered", 0))
363
+
364
+ return None
365
+
366
+ def _get_coverage_from_command(self) -> float | None:
367
+ result = subprocess.run(
368
+ ["uv", "run", "coverage", "report", "--format=json"],
369
+ check=False,
370
+ capture_output=True,
371
+ text=True,
372
+ timeout=15,
373
+ cwd=self.project_root,
374
+ )
375
+
376
+ if result.returncode == 0 and result.stdout:
377
+ data = json.loads(result.stdout)
378
+ return float(data.get("totals", {}).get("percent_covered", 0))
379
+
380
+ return None
381
+
382
+ def _calculate_dependency_ages(self) -> dict[str, int]:
383
+ dependency_ages: dict[str, int] = {}
384
+
385
+ with suppress(Exception):
386
+ if not self.pyproject_path.exists():
387
+ return dependency_ages
388
+
389
+ project_data = self._load_project_data()
390
+ dependencies = self._extract_all_dependencies(project_data)
391
+ dependency_ages = self._get_ages_for_dependencies(dependencies)
392
+
393
+ return dependency_ages
394
+
395
+ def _load_project_data(self) -> dict[str, t.Any]:
396
+ with self.pyproject_path.open("rb") as f:
397
+ return tomllib.load(f)
398
+
399
+ def _extract_all_dependencies(self, project_data: dict[str, t.Any]) -> list[str]:
400
+ dependencies: list[str] = []
401
+
402
+ if "dependencies" in project_data.get("project", {}):
403
+ dependencies.extend(project_data["project"]["dependencies"])
404
+
405
+ if "optional-dependencies" in project_data.get("project", {}):
406
+ for group_deps in project_data["project"]["optional-dependencies"].values():
407
+ dependencies.extend(group_deps)
408
+
409
+ return dependencies
410
+
411
+ def _get_ages_for_dependencies(self, dependencies: list[str]) -> dict[str, int]:
412
+ dependency_ages: dict[str, int] = {}
413
+
414
+ for dep_spec in dependencies:
415
+ package_name = self._extract_package_name(dep_spec)
416
+ if package_name:
417
+ age = self._get_package_age(package_name)
418
+ if age is not None:
419
+ dependency_ages[package_name] = age
420
+
421
+ return dependency_ages
422
+
423
+ def _extract_package_name(self, dep_spec: str) -> str | None:
424
+ if not dep_spec or dep_spec.startswith("-"):
425
+ return None
426
+
427
+ for operator in ("> =", "< =", "= =", "~=", "! =", ">", "<"):
428
+ if operator in dep_spec:
429
+ return dep_spec.split(operator)[0].strip()
430
+
431
+ return dep_spec.strip()
432
+
433
+ def _get_package_age(self, package_name: str) -> int | None:
434
+ try:
435
+ package_data = self._fetch_package_data(package_name)
436
+ if not package_data:
437
+ return None
438
+
439
+ upload_time = self._extract_upload_time(package_data)
440
+ if not upload_time:
441
+ return None
442
+
443
+ return self._calculate_days_since_upload(upload_time)
444
+ except Exception:
445
+ return None
446
+
447
+ def _fetch_package_data(self, package_name: str) -> dict[str, t.Any] | None:
448
+ try:
449
+ url = f"https://pypi.org/pypi/{package_name}/json"
450
+
451
+ parsed = urlparse(url)
452
+ if parsed.scheme != "https" or parsed.netloc != "pypi.org":
453
+ msg = f"Invalid URL: only https://pypi.org URLs are allowed, got {url}"
454
+ raise ValueError(msg)
455
+
456
+ if not parsed.path.startswith("/pypi/") or not parsed.path.endswith(
457
+ "/json"
458
+ ):
459
+ msg = f"Invalid PyPI API path: {parsed.path}"
460
+ raise ValueError(msg)
461
+
462
+ response = requests.get(url, timeout=10, verify=True)
463
+ response.raise_for_status()
464
+ json_result = response.json()
465
+ return t.cast(dict[str, t.Any] | None, json_result)
466
+ except Exception:
467
+ return None
468
+
469
+ def _extract_upload_time(self, package_data: dict[str, t.Any]) -> str | None:
470
+ info = package_data.get("info", {})
471
+ releases = package_data.get("releases", {})
472
+
473
+ latest_version = info.get("version", "")
474
+ if not latest_version or latest_version not in releases:
475
+ return None
476
+
477
+ release_info = releases[latest_version]
478
+ if not release_info:
479
+ return None
480
+
481
+ upload_time_raw = release_info[0].get("upload_time", "")
482
+ return t.cast(str | None, upload_time_raw)
483
+
484
+ def _calculate_days_since_upload(self, upload_time: str) -> int | None:
485
+ try:
486
+ upload_date = datetime.fromisoformat(upload_time)
487
+ return (datetime.now(upload_date.tzinfo) - upload_date).days
488
+ except Exception:
489
+ return None
490
+
491
+ def _assess_config_completeness(self) -> float:
492
+ score = 0.0
493
+ total_checks = 0
494
+
495
+ pyproject_score, pyproject_checks = self._assess_pyproject_config()
496
+ score += pyproject_score
497
+ total_checks += pyproject_checks
498
+
499
+ precommit_score, precommit_checks = self._assess_precommit_config()
500
+ score += precommit_score
501
+ total_checks += precommit_checks
502
+
503
+ ci_score, ci_checks = self._assess_ci_config()
504
+ score += ci_score
505
+ total_checks += ci_checks
506
+
507
+ doc_score, doc_checks = self._assess_documentation_config()
508
+ score += doc_score
509
+ total_checks += doc_checks
510
+
511
+ return min(1.0, score) if total_checks > 0 else 0.0
512
+
513
+ def _assess_pyproject_config(self) -> tuple[float, int]:
514
+ score = 0.0
515
+ total_checks = 1
516
+
517
+ if not self.pyproject_path.exists():
518
+ return score, total_checks
519
+
520
+ score += 0.2
521
+
522
+ with suppress(Exception):
523
+ with self.pyproject_path.open("rb") as f:
524
+ data = tomllib.load(f)
525
+
526
+ project_score, project_checks = self._assess_project_metadata(data)
527
+ score += project_score
528
+ total_checks += project_checks
529
+
530
+ tool_score, tool_checks = self._assess_tool_configs(data)
531
+ score += tool_score
532
+ total_checks += tool_checks
533
+
534
+ return score, total_checks
535
+
536
+ def _assess_project_metadata(self, data: dict[str, t.Any]) -> tuple[float, int]:
537
+ score = 0.0
538
+ total_checks = 0
539
+
540
+ if "project" not in data:
541
+ return score, total_checks
542
+
543
+ project_data = data["project"]
544
+ essential_fields = ["name", "version", "description", "dependencies"]
545
+
546
+ for field_name in essential_fields:
547
+ total_checks += 1
548
+ if field_name in project_data:
549
+ score += 0.1
550
+
551
+ return score, total_checks
552
+
553
+ def _assess_tool_configs(self, data: dict[str, t.Any]) -> tuple[float, int]:
554
+ score = 0.0
555
+ tool_configs = ["tool.ruff", "tool.pytest", "tool.coverage"]
556
+
557
+ for tool in tool_configs:
558
+ keys = tool.split(".")
559
+ current = data
560
+ with suppress(KeyError):
561
+ for key in keys:
562
+ current = current[key]
563
+ score += 0.05
564
+
565
+ return score, len(tool_configs)
566
+
567
+ def _assess_precommit_config(self) -> tuple[float, int]:
568
+ precommit_files = [
569
+ self.project_root / ".pre-commit-config.yaml",
570
+ self.project_root / ".pre - commit-config.yml",
571
+ ]
572
+ score = 0.1 if any(f.exists() for f in precommit_files) else 0.0
573
+ return score, 1
574
+
575
+ def _assess_ci_config(self) -> tuple[float, int]:
576
+ ci_files = [
577
+ self.project_root / ".github" / "workflows",
578
+ self.project_root / ".gitlab-ci.yml",
579
+ self.project_root / "azure-pipelines.yml",
580
+ ]
581
+ score = 0.1 if any(f.exists() for f in ci_files) else 0.0
582
+ return score, 1
583
+
584
+ def _assess_documentation_config(self) -> tuple[float, int]:
585
+ doc_files = [
586
+ self.project_root / "README.md",
587
+ self.project_root / "README.rst",
588
+ self.project_root / "docs",
589
+ ]
590
+ score = 0.1 if any(f.exists() for f in doc_files) else 0.0
591
+ return score, 1
592
+
593
+ def analyze_project_health(self, save_metrics: bool = True) -> ProjectHealth:
594
+ health = self.collect_current_metrics()
595
+
596
+ if save_metrics:
597
+ self._save_health_metrics(health)
598
+
599
+ return health
600
+
601
+ def report_health_status(self, health: ProjectHealth) -> None:
602
+ health_score = health.get_health_score()
603
+
604
+ self._print_health_summary(health_score)
605
+ self._print_health_metrics(health)
606
+ self._print_health_recommendations(health)
607
+
608
+ def _print_health_summary(self, health_score: float) -> None:
609
+ status_icon, status_text, status_color = self._get_health_status_display(
610
+ health_score,
611
+ )
612
+
613
+ self.console.print("\n[bold]📊 Project Health Report[/ bold]")
614
+ self.console.print(
615
+ f"{status_icon} Overall Health: [{status_color}]{status_text} ({health_score: .1 %})[/{status_color}]",
616
+ )
617
+
618
+ def _get_health_status_display(self, health_score: float) -> tuple[str, str, str]:
619
+ if health_score >= 0.8:
620
+ return "🟢", "Excellent", "green"
621
+ if health_score >= 0.6:
622
+ return "🟡", "Good", "yellow"
623
+ if health_score >= 0.4:
624
+ return "🟠", "Fair", "orange"
625
+ return "🔴", "Poor", "red"
626
+
627
+ def _print_health_metrics(self, health: ProjectHealth) -> None:
628
+ if health.lint_error_trend:
629
+ recent_errors = health.lint_error_trend[-1]
630
+ self.console.print(f"🔧 Lint Errors: {recent_errors}")
631
+
632
+ if health.test_coverage_trend:
633
+ recent_coverage = health.test_coverage_trend[-1]
634
+ self.console.print(f"🧪 Test Coverage: {recent_coverage: .1f}%")
635
+
636
+ if health.dependency_age:
637
+ avg_age = sum(health.dependency_age.values()) / len(health.dependency_age)
638
+ self.console.print(f"📦 Avg Dependency Age: {avg_age: .0f} days")
639
+
640
+ self.console.print(f"⚙️ Config Completeness: {health.config_completeness: .1 %}")
641
+
642
+ def _print_health_recommendations(self, health: ProjectHealth) -> None:
643
+ recommendations = health.get_recommendations()
644
+ if recommendations:
645
+ self.console.print("\n[bold]💡 Recommendations: [/ bold]")
646
+ for rec in recommendations:
647
+ self.console.print(f" {rec}")
648
+
649
+ if health.needs_init():
650
+ self.console.print(
651
+ "\n[bold yellow]⚠️ Consider running `crackerjack --init` to improve project health[/ bold yellow]",
652
+ )
653
+
654
+ def get_health_trend_summary(self, days: int = 30) -> dict[str, Any]:
655
+ health = self._load_health_history()
656
+
657
+ return {
658
+ "health_score": health.get_health_score(),
659
+ "needs_attention": health.needs_init(),
660
+ "recommendations": health.get_recommendations(),
661
+ "metrics": {
662
+ "lint_errors": self._get_lint_errors_metrics(health),
663
+ "test_coverage": self._get_test_coverage_metrics(health),
664
+ "dependency_age": self._get_dependency_age_metrics(health),
665
+ "config_completeness": health.config_completeness,
666
+ },
667
+ }
668
+
669
+ def _get_lint_errors_metrics(
670
+ self, health: ProjectHealth
671
+ ) -> dict[str, str | int | None]:
672
+ return {
673
+ "current": health.lint_error_trend[-1] if health.lint_error_trend else None,
674
+ "trend": self._get_trend_direction(health, health.lint_error_trend),
675
+ }
676
+
677
+ def _get_test_coverage_metrics(
678
+ self, health: ProjectHealth
679
+ ) -> dict[str, str | float | None]:
680
+ return {
681
+ "current": health.test_coverage_trend[-1]
682
+ if health.test_coverage_trend
683
+ else None,
684
+ "trend": self._get_coverage_trend_direction(
685
+ health, health.test_coverage_trend
686
+ ),
687
+ }
688
+
689
+ def _get_dependency_age_metrics(
690
+ self, health: ProjectHealth
691
+ ) -> dict[str, float | int | None]:
692
+ if not health.dependency_age:
693
+ return {"average": None, "outdated_count": 0}
694
+
695
+ return {
696
+ "average": sum(health.dependency_age.values()) / len(health.dependency_age),
697
+ "outdated_count": sum(
698
+ 1 for age in health.dependency_age.values() if age > 180
699
+ ),
700
+ }
701
+
702
+ def _get_trend_direction(self, health: ProjectHealth, trend_data: list[int]) -> str:
703
+ if health._is_trending_up([float(x) for x in trend_data]):
704
+ return "up"
705
+ elif health._is_trending_down([float(x) for x in trend_data]):
706
+ return "down"
707
+ return "stable"
708
+
709
+ def _get_coverage_trend_direction(
710
+ self, health: ProjectHealth, coverage_trend: list[float]
711
+ ) -> str:
712
+ if health._is_trending_up([int(x) for x in coverage_trend]):
713
+ return "up"
714
+ elif health._is_trending_down(coverage_trend):
715
+ return "down"
716
+ return "stable"