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,708 @@
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import os
5
+ import time
6
+ import typing as t
7
+ import uuid
8
+ from collections import defaultdict
9
+ from contextlib import asynccontextmanager, suppress
10
+ from pathlib import Path
11
+
12
+ from ..config.global_lock_config import GlobalLockConfig, get_global_lock_config
13
+
14
+
15
+ class HookLockManager:
16
+ _instance: t.Optional["HookLockManager"] = None
17
+ _initialized: bool = False
18
+
19
+ def __new__(cls) -> "HookLockManager":
20
+ if cls._instance is None:
21
+ cls._instance = super().__new__(cls)
22
+ return cls._instance
23
+
24
+ def __init__(self) -> None:
25
+ if self._initialized:
26
+ return
27
+
28
+ self._hooks_requiring_locks = {
29
+ "complexipy",
30
+ }
31
+
32
+ # Create locks for all hooks that require them
33
+ self._hook_locks: dict[str, asyncio.Lock] = {
34
+ hook_name: asyncio.Lock() for hook_name in self._hooks_requiring_locks
35
+ }
36
+
37
+ self._global_config = get_global_lock_config()
38
+ self._global_lock_enabled = self._global_config.enabled
39
+ self._active_global_locks: set[str] = set()
40
+ self._heartbeat_tasks: dict[str, asyncio.Task[None]] = {}
41
+
42
+ self._lock_usage: dict[str, list[float]] = defaultdict(list)
43
+ self._lock_wait_times: dict[str, list[float]] = defaultdict(list)
44
+ self._lock_execution_times: dict[str, list[float]] = defaultdict(list)
45
+ self._max_history = 50
46
+
47
+ self._global_lock_attempts: dict[str, int] = defaultdict(int)
48
+ self._global_lock_successes: dict[str, int] = defaultdict(int)
49
+ self._global_lock_failures: dict[str, int] = defaultdict(int)
50
+ self._stale_locks_cleaned: dict[str, int] = defaultdict(int)
51
+ self._heartbeat_failures: dict[str, int] = defaultdict(int)
52
+
53
+ self._default_lock_timeout = 300.0
54
+ self._lock_timeouts: dict[str, float] = {}
55
+
56
+ self._lock_failures: dict[str, int] = defaultdict(int)
57
+ self._timeout_failures: dict[str, int] = defaultdict(int)
58
+
59
+ self.logger = logging.getLogger(__name__)
60
+ self._initialized = True
61
+
62
+ def requires_lock(self, hook_name: str) -> bool:
63
+ return hook_name in self._hooks_requiring_locks
64
+
65
+ @asynccontextmanager
66
+ async def acquire_hook_lock(self, hook_name: str) -> t.AsyncIterator[None]:
67
+ if not self.requires_lock(hook_name):
68
+ yield
69
+ return
70
+
71
+ if not self._global_lock_enabled:
72
+ async with self._acquire_existing_hook_lock(hook_name):
73
+ yield
74
+ return
75
+
76
+ async with self._acquire_global_coordination_lock(hook_name):
77
+ async with self._acquire_existing_hook_lock(hook_name):
78
+ yield
79
+
80
+ @asynccontextmanager
81
+ async def _acquire_existing_hook_lock(
82
+ self, hook_name: str
83
+ ) -> t.AsyncIterator[None]:
84
+ lock = self._hook_locks[hook_name]
85
+ timeout = self._lock_timeouts.get(hook_name, self._default_lock_timeout)
86
+ start_time = time.time()
87
+
88
+ self.logger.debug(
89
+ f"Acquiring hook-specific lock: {hook_name} (timeout: {timeout}s)"
90
+ )
91
+
92
+ try:
93
+ await asyncio.wait_for(lock.acquire(), timeout=timeout)
94
+
95
+ try:
96
+ acquisition_time = time.time() - start_time
97
+ self.logger.info(
98
+ f"Hook-specific lock acquired for {hook_name} after"
99
+ f" {acquisition_time: .2f}s"
100
+ )
101
+
102
+ self._track_lock_usage(hook_name, acquisition_time)
103
+
104
+ execution_start = time.time()
105
+ try:
106
+ yield
107
+ finally:
108
+ execution_time = time.time() - execution_start
109
+ total_time = time.time() - start_time
110
+
111
+ self._track_lock_execution(hook_name, execution_time, total_time)
112
+ self.logger.debug(
113
+ f"Hook-specific lock released for {hook_name} after"
114
+ f" {total_time: .2f}s total"
115
+ )
116
+
117
+ finally:
118
+ lock.release()
119
+
120
+ except TimeoutError:
121
+ self._timeout_failures[hook_name] += 1
122
+ wait_time = time.time() - start_time
123
+ self.logger.error(
124
+ f"Hook-specific lock acquisition timeout for {hook_name} after"
125
+ f" {wait_time: .2f}s "
126
+ f"(timeout: {timeout}s, total failures: "
127
+ f"{self._timeout_failures[hook_name]})"
128
+ )
129
+ raise
130
+
131
+ except Exception as e:
132
+ self._lock_failures[hook_name] += 1
133
+ self.logger.error(
134
+ f"Hook-specific lock acquisition failed for {hook_name}: {e} "
135
+ f"(total failures: {self._lock_failures[hook_name]})"
136
+ )
137
+ raise
138
+
139
+ @asynccontextmanager
140
+ async def _acquire_global_coordination_lock(
141
+ self, hook_name: str
142
+ ) -> t.AsyncIterator[None]:
143
+ lock_path = self._global_config.get_lock_path(hook_name)
144
+ start_time = time.time()
145
+
146
+ self._global_lock_attempts[hook_name] += 1
147
+ self.logger.debug(
148
+ f"Attempting global lock acquisition for {hook_name}: {lock_path}"
149
+ )
150
+
151
+ await self._cleanup_stale_lock_if_needed(hook_name)
152
+
153
+ try:
154
+ await self._acquire_global_lock_file(hook_name, lock_path)
155
+ self._global_lock_successes[hook_name] += 1
156
+ self._active_global_locks.add(hook_name)
157
+
158
+ heartbeat_task = asyncio.create_task(self._maintain_heartbeat(hook_name))
159
+ self._heartbeat_tasks[hook_name] = heartbeat_task
160
+
161
+ acquisition_time = time.time() - start_time
162
+ self.logger.info(
163
+ f"Global lock acquired for {hook_name} after {acquisition_time: .2f}s"
164
+ )
165
+
166
+ try:
167
+ yield
168
+ finally:
169
+ await self._cleanup_global_lock(hook_name, heartbeat_task)
170
+
171
+ except Exception as e:
172
+ self._global_lock_failures[hook_name] += 1
173
+ self.logger.error(f"Global lock acquisition failed for {hook_name}: {e}")
174
+ raise
175
+
176
+ def _track_lock_usage(self, hook_name: str, acquisition_time: float) -> None:
177
+ usage_list = self._lock_usage[hook_name]
178
+ wait_list = self._lock_wait_times[hook_name]
179
+
180
+ usage_list.append(acquisition_time)
181
+ wait_list.append(acquisition_time)
182
+
183
+ if len(usage_list) > self._max_history:
184
+ usage_list.pop(0)
185
+ if len(wait_list) > self._max_history:
186
+ wait_list.pop(0)
187
+
188
+ def _track_lock_execution(
189
+ self, hook_name: str, execution_time: float, total_time: float
190
+ ) -> None:
191
+ exec_list = self._lock_execution_times[hook_name]
192
+ exec_list.append(execution_time)
193
+
194
+ if len(exec_list) > self._max_history:
195
+ exec_list.pop(0)
196
+
197
+ self.logger.debug(
198
+ f"Hook {hook_name} execution: {execution_time: .2f}s "
199
+ f"(total with lock: {total_time: .2f}s)"
200
+ )
201
+
202
+ async def _acquire_global_lock_file(self, hook_name: str, lock_path: Path) -> None:
203
+ for attempt in range(self._global_config.max_retry_attempts):
204
+ try:
205
+ await self._attempt_lock_acquisition(hook_name, lock_path)
206
+ return
207
+ except FileExistsError:
208
+ if attempt < self._global_config.max_retry_attempts - 1:
209
+ delay = self._global_config.retry_delay_seconds * (2**attempt)
210
+ jitter = delay * 0.1
211
+ wait_time = delay + (jitter * (0.5 - os.urandom(1)[0] / 255))
212
+
213
+ self.logger.debug(
214
+ f"Global lock exists for {hook_name}, retrying in "
215
+ f"{wait_time: .2f}s"
216
+ )
217
+ await asyncio.sleep(wait_time)
218
+ else:
219
+ raise TimeoutError(
220
+ f"Failed to acquire global lock for {hook_name} after"
221
+ f" {self._global_config.max_retry_attempts} attempts"
222
+ )
223
+
224
+ async def _attempt_lock_acquisition(self, hook_name: str, lock_path: Path) -> None:
225
+ temp_path = lock_path.with_suffix(f".tmp.{uuid.uuid4().hex}")
226
+
227
+ lock_data = {
228
+ "session_id": self._global_config.session_id,
229
+ "hostname": self._global_config.hostname,
230
+ "pid": os.getpid(),
231
+ "hook_name": hook_name,
232
+ "acquired_at": time.time(),
233
+ "last_heartbeat": time.time(),
234
+ "crackerjack_version": "0.30.3",
235
+ }
236
+
237
+ try:
238
+ with temp_path.open("x", encoding="utf-8") as f:
239
+ json.dump(lock_data, f, indent=2)
240
+
241
+ temp_path.chmod(0o600)
242
+
243
+ try:
244
+ # Use os.link() for atomic exclusive creation - fails if target exists
245
+ # (Path.rename() will replace existing file, which breaks lock semantics)
246
+ os.link(str(temp_path), str(lock_path))
247
+ self.logger.debug(f"Successfully created global lock file: {lock_path}")
248
+ except FileExistsError:
249
+ with suppress(OSError):
250
+ temp_path.unlink()
251
+ raise
252
+
253
+ except FileExistsError:
254
+ raise FileExistsError(f"Global lock already exists for {hook_name}")
255
+ except Exception as e:
256
+ with suppress(OSError):
257
+ temp_path.unlink()
258
+ self.logger.error(f"Failed to create global lock for {hook_name}: {e}")
259
+ raise
260
+
261
+ async def _maintain_heartbeat(self, hook_name: str) -> None:
262
+ lock_path = self._global_config.get_lock_path(hook_name)
263
+ interval = self._global_config.session_heartbeat_interval
264
+
265
+ self.logger.debug(f"Starting heartbeat for {hook_name} every {interval}s")
266
+
267
+ while hook_name in self._active_global_locks:
268
+ try:
269
+ await asyncio.sleep(interval)
270
+
271
+ if hook_name not in self._active_global_locks:
272
+ break
273
+
274
+ await self._update_heartbeat_timestamp(hook_name, lock_path)
275
+
276
+ except asyncio.CancelledError:
277
+ self.logger.debug(f"Heartbeat cancelled for {hook_name}")
278
+ break
279
+ except Exception as e:
280
+ self._heartbeat_failures[hook_name] += 1
281
+ self.logger.warning(f"Heartbeat update failed for {hook_name}: {e}")
282
+
283
+ if self._heartbeat_failures[hook_name] > 3:
284
+ self.logger.error(
285
+ f"Too many heartbeat failures for {hook_name}, "
286
+ f" stopping heartbeat"
287
+ )
288
+ break
289
+
290
+ async def _update_heartbeat_timestamp(
291
+ self, hook_name: str, lock_path: Path
292
+ ) -> None:
293
+ if not lock_path.exists():
294
+ self.logger.warning(
295
+ f"Lock file disappeared for {hook_name}, stopping heartbeat"
296
+ )
297
+ self._active_global_locks.discard(hook_name)
298
+ return
299
+
300
+ temp_path = lock_path.with_suffix(".heartbeat_tmp")
301
+
302
+ try:
303
+ with lock_path.open(encoding="utf-8") as f:
304
+ lock_data = json.load(f)
305
+
306
+ if lock_data.get("session_id") != self._global_config.session_id:
307
+ self.logger.warning(
308
+ f"Lock ownership changed for {hook_name}, stopping heartbeat"
309
+ )
310
+ self._active_global_locks.discard(hook_name)
311
+ return
312
+
313
+ lock_data["last_heartbeat"] = time.time()
314
+
315
+ with temp_path.open("w", encoding="utf-8") as f:
316
+ json.dump(lock_data, f, indent=2)
317
+
318
+ temp_path.chmod(0o600)
319
+ temp_path.rename(lock_path)
320
+
321
+ except Exception as e:
322
+ with suppress(OSError):
323
+ temp_path.unlink()
324
+ raise RuntimeError(f"Failed to update heartbeat for {hook_name}: {e}")
325
+
326
+ async def _cleanup_global_lock(
327
+ self, hook_name: str, heartbeat_task: asyncio.Task[None] | None = None
328
+ ) -> None:
329
+ self.logger.debug(f"Cleaning up global lock for {hook_name}")
330
+
331
+ self._active_global_locks.discard(hook_name)
332
+
333
+ if heartbeat_task is None:
334
+ heartbeat_task = self._heartbeat_tasks.pop(hook_name, None)
335
+ else:
336
+ self._heartbeat_tasks.pop(hook_name, None)
337
+
338
+ if heartbeat_task:
339
+ heartbeat_task.cancel()
340
+ with suppress(asyncio.CancelledError):
341
+ await heartbeat_task
342
+
343
+ lock_path = self._global_config.get_lock_path(hook_name)
344
+ with suppress(OSError):
345
+ if lock_path.exists():
346
+ try:
347
+ with lock_path.open(encoding="utf-8") as f:
348
+ lock_data = json.load(f)
349
+
350
+ if lock_data.get("session_id") == self._global_config.session_id:
351
+ lock_path.unlink()
352
+ self.logger.debug(f"Removed global lock file: {lock_path}")
353
+ else:
354
+ self.logger.warning(
355
+ f"Lock ownership changed, not removing file: {lock_path}"
356
+ )
357
+
358
+ except Exception as e:
359
+ self.logger.warning(
360
+ f"Could not verify lock ownership for cleanup: {e}"
361
+ )
362
+
363
+ async def _cleanup_stale_lock_if_needed(self, hook_name: str) -> None:
364
+ lock_path = self._global_config.get_lock_path(hook_name)
365
+
366
+ if not lock_path.exists():
367
+ return
368
+
369
+ try:
370
+ with lock_path.open(encoding="utf-8") as f:
371
+ lock_data = json.load(f)
372
+
373
+ last_heartbeat = lock_data.get(
374
+ "last_heartbeat", lock_data.get("acquired_at", 0)
375
+ )
376
+ age_hours = (time.time() - last_heartbeat) / 3600
377
+
378
+ if age_hours > self._global_config.stale_lock_hours:
379
+ self.logger.warning(
380
+ f"Removing stale lock for {hook_name} (age: {age_hours: .2f}h)"
381
+ )
382
+ lock_path.unlink()
383
+ self._stale_locks_cleaned[hook_name] += 1
384
+ else:
385
+ owner = lock_data.get("session_id", "unknown")
386
+ self.logger.debug(
387
+ f"Active lock exists for {hook_name} owned by {owner}"
388
+ )
389
+
390
+ except Exception as e:
391
+ self.logger.warning(f"Could not check lock staleness for {hook_name}: {e}")
392
+
393
+ with suppress(OSError):
394
+ lock_path.unlink()
395
+ self._stale_locks_cleaned[hook_name] += 1
396
+
397
+ def get_lock_stats(self) -> dict[str, t.Any]:
398
+ stats = {}
399
+
400
+ for hook_name in self._hooks_requiring_locks:
401
+ wait_times = self._lock_wait_times[hook_name]
402
+ exec_times = self._lock_execution_times[hook_name]
403
+ usage_list = self._lock_usage[hook_name]
404
+
405
+ if not usage_list:
406
+ stats[hook_name] = {
407
+ "total_acquisitions": 0,
408
+ "avg_wait_time": 0.0,
409
+ "max_wait_time": 0.0,
410
+ "min_wait_time": 0.0,
411
+ "avg_execution_time": 0.0,
412
+ "max_execution_time": 0.0,
413
+ "min_execution_time": 0.0,
414
+ "currently_locked": self._hook_locks[hook_name].locked(),
415
+ "lock_failures": self._lock_failures[hook_name],
416
+ "timeout_failures": self._timeout_failures[hook_name],
417
+ "success_rate": 1.0,
418
+ "lock_timeout": self._lock_timeouts.get(
419
+ hook_name, self._default_lock_timeout
420
+ ),
421
+ }
422
+ else:
423
+ total_attempts = len(usage_list) + self._lock_failures[hook_name]
424
+ success_rate = (
425
+ len(usage_list) / total_attempts if total_attempts > 0 else 1.0
426
+ )
427
+
428
+ base_stats = {
429
+ "total_acquisitions": len(usage_list),
430
+ "total_attempts": total_attempts,
431
+ "currently_locked": self._hook_locks[hook_name].locked(),
432
+ "lock_failures": self._lock_failures[hook_name],
433
+ "timeout_failures": self._timeout_failures[hook_name],
434
+ "success_rate": success_rate,
435
+ "lock_timeout": self._lock_timeouts.get(
436
+ hook_name, self._default_lock_timeout
437
+ ),
438
+ }
439
+
440
+ if wait_times:
441
+ base_stats.update(
442
+ {
443
+ "avg_wait_time": sum(wait_times) / len(wait_times),
444
+ "max_wait_time": max(wait_times),
445
+ "min_wait_time": min(wait_times),
446
+ }
447
+ )
448
+ else:
449
+ base_stats.update(
450
+ {
451
+ "avg_wait_time": 0.0,
452
+ "max_wait_time": 0.0,
453
+ "min_wait_time": 0.0,
454
+ }
455
+ )
456
+
457
+ if exec_times:
458
+ base_stats.update(
459
+ {
460
+ "avg_execution_time": sum(exec_times) / len(exec_times),
461
+ "max_execution_time": max(exec_times),
462
+ "min_execution_time": min(exec_times),
463
+ }
464
+ )
465
+ else:
466
+ base_stats.update(
467
+ {
468
+ "avg_execution_time": 0.0,
469
+ "max_execution_time": 0.0,
470
+ "min_execution_time": 0.0,
471
+ }
472
+ )
473
+
474
+ stats[hook_name] = base_stats
475
+
476
+ return stats
477
+
478
+ def add_hook_to_lock_list(self, hook_name: str) -> None:
479
+ self._hooks_requiring_locks.add(hook_name)
480
+ # Create lock for this hook if it doesn't already exist
481
+ if hook_name not in self._hook_locks:
482
+ self._hook_locks[hook_name] = asyncio.Lock()
483
+ self.logger.info(f"Added {hook_name} to hooks requiring locks")
484
+
485
+ def remove_hook_from_lock_list(self, hook_name: str) -> None:
486
+ self._hooks_requiring_locks.discard(hook_name)
487
+ if hook_name in self._hook_locks:
488
+ del self._hook_locks[hook_name]
489
+ if hook_name in self._lock_usage:
490
+ del self._lock_usage[hook_name]
491
+ self.logger.info(f"Removed {hook_name} from hooks requiring locks")
492
+
493
+ def is_hook_currently_locked(self, hook_name: str) -> bool:
494
+ if not self.requires_lock(hook_name):
495
+ return False
496
+ return self._hook_locks[hook_name].locked()
497
+
498
+ def set_hook_timeout(self, hook_name: str, timeout: float) -> None:
499
+ self._lock_timeouts[hook_name] = timeout
500
+ self.logger.info(f"Set custom timeout for {hook_name}: {timeout}s")
501
+
502
+ def get_hook_timeout(self, hook_name: str) -> float:
503
+ return self._lock_timeouts.get(hook_name, self._default_lock_timeout)
504
+
505
+ def enable_global_lock(self, enabled: bool = True) -> None:
506
+ self._global_lock_enabled = enabled
507
+ # Update the settings model if supported
508
+ if hasattr(self._global_config._settings, "enabled"):
509
+ # Create a new settings object with updated enabled value
510
+ new_settings = self._global_config._settings.model_copy(
511
+ update={"enabled": enabled}
512
+ )
513
+ self._global_config._settings = new_settings
514
+ self.logger.info(
515
+ f"Global lock functionality {'enabled' if enabled else 'disabled'}"
516
+ )
517
+
518
+ def is_global_lock_enabled(self) -> bool:
519
+ return self._global_lock_enabled
520
+
521
+ def get_global_lock_path(self, hook_name: str) -> Path:
522
+ return self._global_config.get_lock_path(hook_name)
523
+
524
+ def cleanup_stale_locks(self, max_age_hours: float = 2.0) -> int:
525
+ locks_dir = self._global_config.lock_directory
526
+ if not locks_dir.exists():
527
+ return 0
528
+
529
+ cleaned_count = 0
530
+ current_time = time.time()
531
+
532
+ try:
533
+ for lock_file in locks_dir.glob("*.lock"):
534
+ cleaned_count += self._process_lock_file(
535
+ lock_file, max_age_hours, current_time
536
+ )
537
+
538
+ except OSError as e:
539
+ self.logger.error(f"Could not access locks directory {locks_dir}: {e}")
540
+
541
+ if cleaned_count > 0:
542
+ self.logger.info(f"Cleaned up {cleaned_count} stale lock files")
543
+
544
+ return cleaned_count
545
+
546
+ def _process_lock_file(
547
+ self, lock_file: Path, max_age_hours: float, current_time: float
548
+ ) -> int:
549
+ # Always attempt to check lock file data (file mtime is unreliable in tests)
550
+ # The JSON data's last_heartbeat is the source of truth for staleness
551
+ return self._cleanup_stale_lock_file(lock_file, max_age_hours, current_time)
552
+
553
+ def _cleanup_stale_lock_file(
554
+ self, lock_file: Path, max_age_hours: float, current_time: float
555
+ ) -> int:
556
+ try:
557
+ with lock_file.open(encoding="utf-8") as f:
558
+ lock_data = json.load(f)
559
+
560
+ last_heartbeat = lock_data.get(
561
+ "last_heartbeat", lock_data.get("acquired_at", 0)
562
+ )
563
+ heartbeat_age_hours = (current_time - last_heartbeat) / 3600
564
+
565
+ if heartbeat_age_hours > max_age_hours:
566
+ lock_file.unlink()
567
+ hook_name = lock_file.stem
568
+ self._stale_locks_cleaned[hook_name] += 1
569
+ self.logger.info(
570
+ f"Cleaned stale lock: {lock_file} (age: {heartbeat_age_hours: .2f}h)"
571
+ )
572
+ return 1
573
+
574
+ except (json.JSONDecodeError, KeyError):
575
+ lock_file.unlink()
576
+ self.logger.warning(f"Cleaned corrupted lock file: {lock_file}")
577
+ return 1
578
+
579
+ return 0
580
+
581
+ def get_global_lock_stats(self) -> dict[str, t.Any]:
582
+ stats: dict[str, t.Any] = {
583
+ "global_lock_enabled": self._global_lock_enabled,
584
+ "lock_directory": str(self._global_config.lock_directory),
585
+ "session_id": self._global_config.session_id,
586
+ "hostname": self._global_config.hostname,
587
+ "active_global_locks": list[t.Any](self._active_global_locks),
588
+ "active_heartbeat_tasks": len(self._heartbeat_tasks),
589
+ "configuration": {
590
+ "timeout_seconds": self._global_config.timeout_seconds,
591
+ "stale_lock_hours": self._global_config.stale_lock_hours,
592
+ "heartbeat_interval": self._global_config.session_heartbeat_interval,
593
+ "max_retry_attempts": self._global_config.max_retry_attempts,
594
+ "retry_delay_seconds": self._global_config.retry_delay_seconds,
595
+ "enable_lock_monitoring": self._global_config.enable_lock_monitoring,
596
+ },
597
+ "statistics": {},
598
+ }
599
+
600
+ all_hooks = (
601
+ set[t.Any](self._global_lock_attempts.keys())
602
+ | set[t.Any](self._global_lock_successes.keys())
603
+ | set[t.Any](self._global_lock_failures.keys())
604
+ )
605
+
606
+ for hook_name in all_hooks:
607
+ attempts = self._global_lock_attempts[hook_name]
608
+ successes = self._global_lock_successes[hook_name]
609
+ failures = self._global_lock_failures[hook_name]
610
+ stale_cleaned = self._stale_locks_cleaned[hook_name]
611
+ heartbeat_failures = self._heartbeat_failures[hook_name]
612
+
613
+ success_rate = (successes / attempts) if attempts > 0 else 0.0
614
+
615
+ stats["statistics"][hook_name] = {
616
+ "attempts": attempts,
617
+ "successes": successes,
618
+ "failures": failures,
619
+ "success_rate": success_rate,
620
+ "stale_locks_cleaned": stale_cleaned,
621
+ "heartbeat_failures": heartbeat_failures,
622
+ "currently_locked": hook_name in self._active_global_locks,
623
+ "has_heartbeat_task": hook_name in self._heartbeat_tasks,
624
+ }
625
+
626
+ total_attempts = sum(self._global_lock_attempts.values())
627
+ total_successes = sum(self._global_lock_successes.values())
628
+ total_failures = sum(self._global_lock_failures.values())
629
+ total_stale_cleaned = sum(self._stale_locks_cleaned.values())
630
+ total_heartbeat_failures = sum(self._heartbeat_failures.values())
631
+
632
+ stats["totals"] = {
633
+ "total_attempts": total_attempts,
634
+ "total_successes": total_successes,
635
+ "total_failures": total_failures,
636
+ "overall_success_rate": (total_successes / total_attempts)
637
+ if total_attempts > 0
638
+ else 0.0,
639
+ "total_stale_locks_cleaned": total_stale_cleaned,
640
+ "total_heartbeat_failures": total_heartbeat_failures,
641
+ }
642
+
643
+ return stats
644
+
645
+ def configure_from_options(self, options: t.Any) -> None:
646
+ """Configure lock manager from CLI options.
647
+
648
+ This is a synchronous method because it only performs configuration
649
+ updates without needing to await any async operations.
650
+ """
651
+ self._global_config = GlobalLockConfig.from_options(options)
652
+ self._global_lock_enabled = self._global_config.enabled
653
+
654
+ if hasattr(options, "global_lock_cleanup") and options.global_lock_cleanup:
655
+ self.cleanup_stale_locks()
656
+
657
+ self.logger.info(
658
+ f"Configured lock manager: global_locks={
659
+ 'enabled' if self._global_lock_enabled else 'disabled'
660
+ }, "
661
+ f" timeout={self._global_config.timeout_seconds}s, "
662
+ f"lock_dir={self._global_config.lock_directory}"
663
+ )
664
+
665
+ def reset_hook_stats(self, hook_name: str | None = None) -> None:
666
+ if hook_name:
667
+ self._lock_usage[hook_name].clear()
668
+ self._lock_wait_times[hook_name].clear()
669
+ self._lock_execution_times[hook_name].clear()
670
+ self._lock_failures[hook_name] = 0
671
+ self._timeout_failures[hook_name] = 0
672
+ self.logger.info(f"Reset statistics for hook: {hook_name}")
673
+ else:
674
+ self._lock_usage.clear()
675
+ self._lock_wait_times.clear()
676
+ self._lock_execution_times.clear()
677
+ self._lock_failures.clear()
678
+ self._timeout_failures.clear()
679
+ self.logger.info("Reset statistics for all hooks")
680
+
681
+ def get_comprehensive_status(self) -> dict[str, t.Any]:
682
+ status = {
683
+ "hooks_requiring_locks": list[t.Any](self._hooks_requiring_locks),
684
+ "default_timeout": self._default_lock_timeout,
685
+ "custom_timeouts": self._lock_timeouts.copy(),
686
+ "max_history": self._max_history,
687
+ "lock_statistics": self.get_lock_stats(),
688
+ "currently_locked_hooks": [
689
+ hook
690
+ for hook in self._hooks_requiring_locks
691
+ if self.is_hook_currently_locked(hook)
692
+ ],
693
+ "total_lock_failures": sum(self._lock_failures.values()),
694
+ "total_timeout_failures": sum(self._timeout_failures.values()),
695
+ }
696
+
697
+ if self._global_lock_enabled:
698
+ status["global_lock_stats"] = self.get_global_lock_stats()
699
+ else:
700
+ status["global_lock_stats"] = {
701
+ "global_lock_enabled": False,
702
+ "message": "Global locking is disabled",
703
+ }
704
+
705
+ return status
706
+
707
+
708
+ hook_lock_manager = HookLockManager()