crackerjack 0.32.0__py3-none-any.whl → 0.33.1__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.

Potentially problematic release.


This version of crackerjack might be problematic. Click here for more details.

Files changed (200) hide show
  1. crackerjack/__main__.py +1350 -34
  2. crackerjack/adapters/__init__.py +17 -0
  3. crackerjack/adapters/lsp_client.py +358 -0
  4. crackerjack/adapters/rust_tool_adapter.py +194 -0
  5. crackerjack/adapters/rust_tool_manager.py +193 -0
  6. crackerjack/adapters/skylos_adapter.py +231 -0
  7. crackerjack/adapters/zuban_adapter.py +560 -0
  8. crackerjack/agents/base.py +7 -3
  9. crackerjack/agents/coordinator.py +271 -33
  10. crackerjack/agents/documentation_agent.py +9 -15
  11. crackerjack/agents/dry_agent.py +3 -15
  12. crackerjack/agents/formatting_agent.py +1 -1
  13. crackerjack/agents/import_optimization_agent.py +36 -180
  14. crackerjack/agents/performance_agent.py +17 -98
  15. crackerjack/agents/performance_helpers.py +7 -31
  16. crackerjack/agents/proactive_agent.py +1 -3
  17. crackerjack/agents/refactoring_agent.py +16 -85
  18. crackerjack/agents/refactoring_helpers.py +7 -42
  19. crackerjack/agents/security_agent.py +9 -48
  20. crackerjack/agents/test_creation_agent.py +356 -513
  21. crackerjack/agents/test_specialist_agent.py +0 -4
  22. crackerjack/api.py +6 -25
  23. crackerjack/cli/cache_handlers.py +204 -0
  24. crackerjack/cli/cache_handlers_enhanced.py +683 -0
  25. crackerjack/cli/facade.py +100 -0
  26. crackerjack/cli/handlers.py +224 -9
  27. crackerjack/cli/interactive.py +6 -4
  28. crackerjack/cli/options.py +642 -55
  29. crackerjack/cli/utils.py +2 -1
  30. crackerjack/code_cleaner.py +58 -117
  31. crackerjack/config/global_lock_config.py +8 -48
  32. crackerjack/config/hooks.py +53 -62
  33. crackerjack/core/async_workflow_orchestrator.py +24 -34
  34. crackerjack/core/autofix_coordinator.py +3 -17
  35. crackerjack/core/enhanced_container.py +64 -6
  36. crackerjack/core/file_lifecycle.py +12 -89
  37. crackerjack/core/performance.py +2 -2
  38. crackerjack/core/performance_monitor.py +15 -55
  39. crackerjack/core/phase_coordinator.py +257 -218
  40. crackerjack/core/resource_manager.py +14 -90
  41. crackerjack/core/service_watchdog.py +62 -95
  42. crackerjack/core/session_coordinator.py +149 -0
  43. crackerjack/core/timeout_manager.py +14 -72
  44. crackerjack/core/websocket_lifecycle.py +13 -78
  45. crackerjack/core/workflow_orchestrator.py +558 -240
  46. crackerjack/docs/INDEX.md +11 -0
  47. crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
  48. crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
  49. crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
  50. crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
  51. crackerjack/docs/generated/api/SERVICES.md +1252 -0
  52. crackerjack/documentation/__init__.py +31 -0
  53. crackerjack/documentation/ai_templates.py +756 -0
  54. crackerjack/documentation/dual_output_generator.py +765 -0
  55. crackerjack/documentation/mkdocs_integration.py +518 -0
  56. crackerjack/documentation/reference_generator.py +977 -0
  57. crackerjack/dynamic_config.py +55 -50
  58. crackerjack/executors/async_hook_executor.py +10 -15
  59. crackerjack/executors/cached_hook_executor.py +117 -43
  60. crackerjack/executors/hook_executor.py +8 -34
  61. crackerjack/executors/hook_lock_manager.py +26 -183
  62. crackerjack/executors/individual_hook_executor.py +13 -11
  63. crackerjack/executors/lsp_aware_hook_executor.py +270 -0
  64. crackerjack/executors/tool_proxy.py +417 -0
  65. crackerjack/hooks/lsp_hook.py +79 -0
  66. crackerjack/intelligence/adaptive_learning.py +25 -10
  67. crackerjack/intelligence/agent_orchestrator.py +2 -5
  68. crackerjack/intelligence/agent_registry.py +34 -24
  69. crackerjack/intelligence/agent_selector.py +5 -7
  70. crackerjack/interactive.py +17 -6
  71. crackerjack/managers/async_hook_manager.py +0 -1
  72. crackerjack/managers/hook_manager.py +79 -1
  73. crackerjack/managers/publish_manager.py +66 -13
  74. crackerjack/managers/test_command_builder.py +5 -17
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +109 -7
  77. crackerjack/managers/test_manager_backup.py +10 -9
  78. crackerjack/mcp/cache.py +2 -2
  79. crackerjack/mcp/client_runner.py +1 -1
  80. crackerjack/mcp/context.py +191 -68
  81. crackerjack/mcp/dashboard.py +7 -5
  82. crackerjack/mcp/enhanced_progress_monitor.py +31 -28
  83. crackerjack/mcp/file_monitor.py +30 -23
  84. crackerjack/mcp/progress_components.py +31 -21
  85. crackerjack/mcp/progress_monitor.py +50 -53
  86. crackerjack/mcp/rate_limiter.py +6 -6
  87. crackerjack/mcp/server_core.py +161 -32
  88. crackerjack/mcp/service_watchdog.py +2 -1
  89. crackerjack/mcp/state.py +4 -7
  90. crackerjack/mcp/task_manager.py +11 -9
  91. crackerjack/mcp/tools/core_tools.py +174 -33
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +15 -12
  94. crackerjack/mcp/tools/execution_tools_backup.py +42 -30
  95. crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
  96. crackerjack/mcp/tools/intelligence_tools.py +5 -2
  97. crackerjack/mcp/tools/monitoring_tools.py +33 -70
  98. crackerjack/mcp/tools/proactive_tools.py +24 -11
  99. crackerjack/mcp/tools/progress_tools.py +5 -8
  100. crackerjack/mcp/tools/utility_tools.py +20 -14
  101. crackerjack/mcp/tools/workflow_executor.py +62 -40
  102. crackerjack/mcp/websocket/app.py +8 -0
  103. crackerjack/mcp/websocket/endpoints.py +352 -357
  104. crackerjack/mcp/websocket/jobs.py +40 -57
  105. crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
  106. crackerjack/mcp/websocket/server.py +7 -25
  107. crackerjack/mcp/websocket/websocket_handler.py +6 -17
  108. crackerjack/mixins/__init__.py +3 -0
  109. crackerjack/mixins/error_handling.py +145 -0
  110. crackerjack/models/config.py +21 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +176 -107
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/models/task.py +3 -0
  115. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  116. crackerjack/monitoring/metrics_collector.py +426 -0
  117. crackerjack/monitoring/regression_prevention.py +8 -8
  118. crackerjack/monitoring/websocket_server.py +643 -0
  119. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  120. crackerjack/orchestration/coverage_improvement.py +3 -3
  121. crackerjack/orchestration/execution_strategies.py +26 -6
  122. crackerjack/orchestration/test_progress_streamer.py +8 -5
  123. crackerjack/plugins/base.py +2 -2
  124. crackerjack/plugins/hooks.py +7 -0
  125. crackerjack/plugins/managers.py +11 -8
  126. crackerjack/security/__init__.py +0 -1
  127. crackerjack/security/audit.py +90 -105
  128. crackerjack/services/anomaly_detector.py +392 -0
  129. crackerjack/services/api_extractor.py +615 -0
  130. crackerjack/services/backup_service.py +2 -2
  131. crackerjack/services/bounded_status_operations.py +15 -152
  132. crackerjack/services/cache.py +127 -1
  133. crackerjack/services/changelog_automation.py +395 -0
  134. crackerjack/services/config.py +18 -11
  135. crackerjack/services/config_merge.py +30 -85
  136. crackerjack/services/config_template.py +506 -0
  137. crackerjack/services/contextual_ai_assistant.py +48 -22
  138. crackerjack/services/coverage_badge_service.py +171 -0
  139. crackerjack/services/coverage_ratchet.py +41 -17
  140. crackerjack/services/debug.py +3 -3
  141. crackerjack/services/dependency_analyzer.py +460 -0
  142. crackerjack/services/dependency_monitor.py +14 -11
  143. crackerjack/services/documentation_generator.py +491 -0
  144. crackerjack/services/documentation_service.py +675 -0
  145. crackerjack/services/enhanced_filesystem.py +6 -5
  146. crackerjack/services/enterprise_optimizer.py +865 -0
  147. crackerjack/services/error_pattern_analyzer.py +676 -0
  148. crackerjack/services/file_hasher.py +1 -1
  149. crackerjack/services/git.py +41 -45
  150. crackerjack/services/health_metrics.py +10 -8
  151. crackerjack/services/heatmap_generator.py +735 -0
  152. crackerjack/services/initialization.py +30 -33
  153. crackerjack/services/input_validator.py +5 -97
  154. crackerjack/services/intelligent_commit.py +327 -0
  155. crackerjack/services/log_manager.py +15 -12
  156. crackerjack/services/logging.py +4 -3
  157. crackerjack/services/lsp_client.py +628 -0
  158. crackerjack/services/memory_optimizer.py +409 -0
  159. crackerjack/services/metrics.py +42 -33
  160. crackerjack/services/parallel_executor.py +416 -0
  161. crackerjack/services/pattern_cache.py +1 -1
  162. crackerjack/services/pattern_detector.py +6 -6
  163. crackerjack/services/performance_benchmarks.py +250 -576
  164. crackerjack/services/performance_cache.py +382 -0
  165. crackerjack/services/performance_monitor.py +565 -0
  166. crackerjack/services/predictive_analytics.py +510 -0
  167. crackerjack/services/quality_baseline.py +234 -0
  168. crackerjack/services/quality_baseline_enhanced.py +646 -0
  169. crackerjack/services/quality_intelligence.py +785 -0
  170. crackerjack/services/regex_patterns.py +605 -524
  171. crackerjack/services/regex_utils.py +43 -123
  172. crackerjack/services/secure_path_utils.py +5 -164
  173. crackerjack/services/secure_status_formatter.py +30 -141
  174. crackerjack/services/secure_subprocess.py +11 -92
  175. crackerjack/services/security.py +61 -30
  176. crackerjack/services/security_logger.py +18 -22
  177. crackerjack/services/server_manager.py +124 -16
  178. crackerjack/services/status_authentication.py +16 -159
  179. crackerjack/services/status_security_manager.py +4 -131
  180. crackerjack/services/terminal_utils.py +0 -0
  181. crackerjack/services/thread_safe_status_collector.py +19 -125
  182. crackerjack/services/unified_config.py +21 -13
  183. crackerjack/services/validation_rate_limiter.py +5 -54
  184. crackerjack/services/version_analyzer.py +459 -0
  185. crackerjack/services/version_checker.py +1 -1
  186. crackerjack/services/websocket_resource_limiter.py +10 -144
  187. crackerjack/services/zuban_lsp_service.py +390 -0
  188. crackerjack/slash_commands/__init__.py +2 -7
  189. crackerjack/slash_commands/run.md +2 -2
  190. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  191. crackerjack/tools/validate_regex_patterns.py +19 -48
  192. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
  193. crackerjack-0.33.1.dist-info/RECORD +229 -0
  194. crackerjack/CLAUDE.md +0 -207
  195. crackerjack/RULES.md +0 -380
  196. crackerjack/py313.py +0 -234
  197. crackerjack-0.32.0.dist-info/RECORD +0 -180
  198. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  199. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  200. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
@@ -202,7 +202,7 @@ class AgentRegistry:
202
202
 
203
203
  def _build_agent_data(self, lines: list[str], yaml_end: int) -> dict[str, t.Any]:
204
204
  yaml_lines = lines[1:yaml_end]
205
- agent_data = {}
205
+ agent_data: dict[str, t.Any] = {}
206
206
 
207
207
  for line in yaml_lines:
208
208
  if ": " in line:
@@ -213,36 +213,46 @@ class AgentRegistry:
213
213
  return agent_data
214
214
 
215
215
  def _infer_capabilities_from_agent(self, agent: SubAgent) -> set[AgentCapability]:
216
- capabilities = set()
217
-
216
+ """Infer agent capabilities from class name using keyword mapping."""
218
217
  class_name = agent.__class__.__name__.lower()
218
+ capability_mapping = self._get_agent_capability_mapping()
219
219
 
220
- if "architect" in class_name:
221
- capabilities.update(
222
- {AgentCapability.ARCHITECTURE, AgentCapability.CODE_ANALYSIS}
223
- )
224
- if "refactor" in class_name:
225
- capabilities.add(AgentCapability.REFACTORING)
226
- if "test" in class_name:
227
- capabilities.add(AgentCapability.TESTING)
228
- if "security" in class_name:
229
- capabilities.add(AgentCapability.SECURITY)
230
- if "performance" in class_name:
231
- capabilities.add(AgentCapability.PERFORMANCE)
232
- if "documentation" in class_name or "doc" in class_name:
233
- capabilities.add(AgentCapability.DOCUMENTATION)
234
- if "format" in class_name:
235
- capabilities.add(AgentCapability.FORMATTING)
236
- if "import" in class_name:
237
- capabilities.add(AgentCapability.CODE_ANALYSIS)
238
- if "dry" in class_name:
239
- capabilities.add(AgentCapability.REFACTORING)
220
+ capabilities = set()
221
+ for keywords, caps in capability_mapping:
222
+ if self._class_name_matches_keywords(class_name, keywords):
223
+ capabilities.update(caps)
240
224
 
225
+ # Fallback to default capability if none found
241
226
  if not capabilities:
242
227
  capabilities.add(AgentCapability.CODE_ANALYSIS)
243
228
 
244
229
  return capabilities
245
230
 
231
+ def _get_agent_capability_mapping(
232
+ self,
233
+ ) -> list[tuple[list[str], set[AgentCapability]]]:
234
+ """Get mapping of keywords to agent capabilities."""
235
+ return [
236
+ (
237
+ ["architect"],
238
+ {AgentCapability.ARCHITECTURE, AgentCapability.CODE_ANALYSIS},
239
+ ),
240
+ (["refactor"], {AgentCapability.REFACTORING}),
241
+ (["test"], {AgentCapability.TESTING}),
242
+ (["security"], {AgentCapability.SECURITY}),
243
+ (["performance"], {AgentCapability.PERFORMANCE}),
244
+ (["documentation", "doc"], {AgentCapability.DOCUMENTATION}),
245
+ (["format"], {AgentCapability.FORMATTING}),
246
+ (["import"], {AgentCapability.CODE_ANALYSIS}),
247
+ (["dry"], {AgentCapability.REFACTORING}),
248
+ ]
249
+
250
+ def _class_name_matches_keywords(
251
+ self, class_name: str, keywords: list[str]
252
+ ) -> bool:
253
+ """Check if class name contains any of the specified keywords."""
254
+ return any(keyword in class_name for keyword in keywords)
255
+
246
256
  def _infer_capabilities_from_user_agent(
247
257
  self, agent_data: dict[str, t.Any]
248
258
  ) -> set[AgentCapability]:
@@ -334,7 +344,7 @@ class AgentRegistry:
334
344
  return self._agents.get(name)
335
345
 
336
346
  def list_all_agents(self) -> list[RegisteredAgent]:
337
- agents = list(self._agents.values())
347
+ agents = list[t.Any](self._agents.values())
338
348
  agents.sort(key=lambda a: a.metadata.priority, reverse=True)
339
349
  return agents
340
350
 
@@ -143,9 +143,7 @@ class AgentSelector:
143
143
 
144
144
  capabilities = set()
145
145
  for pattern, caps in self._task_patterns.items():
146
- if re.search(
147
- pattern, text, re.IGNORECASE
148
- ): # REGEX OK: dynamic pattern matching from config
146
+ if re.search(pattern, text, re.IGNORECASE):
149
147
  capabilities.update(caps)
150
148
 
151
149
  return capabilities
@@ -182,7 +180,7 @@ class AgentSelector:
182
180
  TaskContext.GENERAL: [AgentCapability.CODE_ANALYSIS],
183
181
  }
184
182
 
185
- return set(context_map.get(task.context, []))
183
+ return set[t.Any](context_map.get(task.context, []))
186
184
 
187
185
  def _analyze_file_patterns(self, task: TaskDescription) -> set[AgentCapability]:
188
186
  if not task.file_patterns:
@@ -291,8 +289,8 @@ class AgentSelector:
291
289
  if not agent.metadata.description:
292
290
  return 0.0
293
291
 
294
- desc_words = set(agent.metadata.description.lower().split())
295
- task_words = set(task_text.split())
292
+ desc_words = set[t.Any](agent.metadata.description.lower().split())
293
+ task_words = set[t.Any](task_text.split())
296
294
  common_words = desc_words & task_words
297
295
 
298
296
  if common_words:
@@ -355,7 +353,7 @@ class AgentSelector:
355
353
  parts.append(source_desc.get(agent.metadata.source.value, "Unknown source"))
356
354
 
357
355
  if agent.metadata.capabilities:
358
- top_caps = list(agent.metadata.capabilities)[:2]
356
+ top_caps = list[t.Any](agent.metadata.capabilities)[:2]
359
357
  cap_names = [cap.value.replace("_", " ") for cap in top_caps]
360
358
  parts.append(f"Strengths: {', '.join(cap_names)}")
361
359
 
@@ -37,12 +37,23 @@ class WorkflowOptions:
37
37
  @classmethod
38
38
  def from_args(cls, args: t.Any) -> "WorkflowOptions":
39
39
  return cls(
40
- clean=getattr(args, "clean", False),
41
- test=getattr(args, "test", False),
42
- publish=getattr(args, "publish", None),
43
- bump=getattr(args, "bump", None),
44
- commit=getattr(args, "commit", False),
45
- create_pr=getattr(args, "create_pr", False),
40
+ cleaning=CleaningConfig(
41
+ clean=getattr(args, "clean", False),
42
+ ),
43
+ testing=TestConfig(
44
+ test=getattr(args, "test", False),
45
+ ),
46
+ publishing=PublishConfig(
47
+ publish=getattr(args, "publish", None),
48
+ bump=getattr(args, "bump", None),
49
+ ),
50
+ git=GitConfig(
51
+ commit=getattr(args, "commit", False),
52
+ create_pr=getattr(args, "create_pr", False),
53
+ ),
54
+ ai=AIConfig(
55
+ ai_agent=getattr(args, "ai_agent", False),
56
+ ),
46
57
  interactive=getattr(args, "interactive", True),
47
58
  dry_run=getattr(args, "dry_run", False),
48
59
  )
@@ -28,7 +28,6 @@ class AsyncHookManager:
28
28
  self._config_path: Path | None = None
29
29
 
30
30
  def set_config_path(self, config_path: Path) -> None:
31
- """Set the path to the pre-commit configuration file."""
32
31
  self._config_path = config_path
33
32
 
34
33
  async def run_fast_hooks_async(self) -> list[HookResult]:
@@ -6,6 +6,7 @@ from rich.console import Console
6
6
 
7
7
  from crackerjack.config.hooks import HookConfigLoader
8
8
  from crackerjack.executors.hook_executor import HookExecutor
9
+ from crackerjack.executors.lsp_aware_hook_executor import LSPAwareHookExecutor
9
10
  from crackerjack.models.task import HookResult
10
11
 
11
12
 
@@ -16,12 +17,25 @@ class HookManagerImpl:
16
17
  pkg_path: Path,
17
18
  verbose: bool = False,
18
19
  quiet: bool = False,
20
+ enable_lsp_optimization: bool = False,
21
+ enable_tool_proxy: bool = True,
19
22
  ) -> None:
20
23
  self.console = console
21
24
  self.pkg_path = pkg_path
22
- self.executor = HookExecutor(console, pkg_path, verbose, quiet)
25
+ self.executor: HookExecutor
26
+
27
+ # Use LSP-aware executor if optimization is enabled
28
+ if enable_lsp_optimization:
29
+ self.executor = LSPAwareHookExecutor(
30
+ console, pkg_path, verbose, quiet, use_tool_proxy=enable_tool_proxy
31
+ )
32
+ else:
33
+ self.executor = HookExecutor(console, pkg_path, verbose, quiet)
34
+
23
35
  self.config_loader = HookConfigLoader()
24
36
  self._config_path: Path | None = None
37
+ self.lsp_optimization_enabled = enable_lsp_optimization
38
+ self.tool_proxy_enabled = enable_tool_proxy
25
39
 
26
40
  def set_config_path(self, config_path: Path) -> None:
27
41
  self._config_path = config_path
@@ -49,6 +63,70 @@ class HookManagerImpl:
49
63
  comprehensive_results = self.run_comprehensive_hooks()
50
64
  return fast_results + comprehensive_results
51
65
 
66
+ def get_execution_info(self) -> dict[str, t.Any]:
67
+ """Get information about current execution mode and capabilities."""
68
+ info = {
69
+ "lsp_optimization_enabled": self.lsp_optimization_enabled,
70
+ "tool_proxy_enabled": self.tool_proxy_enabled,
71
+ "executor_type": type(self.executor).__name__,
72
+ }
73
+
74
+ # Get LSP-specific info if available
75
+ if hasattr(self.executor, "get_execution_mode_summary"):
76
+ info.update(self.executor.get_execution_mode_summary())
77
+
78
+ return info
79
+
80
+ def configure_lsp_optimization(self, enable: bool) -> None:
81
+ """Enable or disable LSP optimization by switching executors."""
82
+ if enable == self.lsp_optimization_enabled:
83
+ return # Already in the correct state
84
+
85
+ # Switch executor based on the enable flag
86
+ if enable:
87
+ self.executor = LSPAwareHookExecutor(
88
+ self.console,
89
+ self.pkg_path,
90
+ verbose=getattr(self.executor, "verbose", False),
91
+ quiet=getattr(self.executor, "quiet", True),
92
+ use_tool_proxy=self.tool_proxy_enabled,
93
+ )
94
+ else:
95
+ self.executor = HookExecutor(
96
+ self.console,
97
+ self.pkg_path,
98
+ verbose=getattr(self.executor, "verbose", False),
99
+ quiet=getattr(self.executor, "quiet", True),
100
+ )
101
+
102
+ self.lsp_optimization_enabled = enable
103
+
104
+ # Restore config path if it was set[t.Any]
105
+ if self._config_path:
106
+ # Config path is set[t.Any] at the manager level, not executor level
107
+ pass
108
+
109
+ def configure_tool_proxy(self, enable: bool) -> None:
110
+ """Enable or disable tool proxy resilience."""
111
+ if enable == self.tool_proxy_enabled:
112
+ return # Already in the correct state
113
+
114
+ self.tool_proxy_enabled = enable
115
+
116
+ # If using LSP-aware executor, recreate it with new tool proxy setting
117
+ if isinstance(self.executor, LSPAwareHookExecutor):
118
+ self.executor = LSPAwareHookExecutor(
119
+ self.console,
120
+ self.pkg_path,
121
+ verbose=getattr(self.executor, "verbose", False),
122
+ quiet=getattr(self.executor, "quiet", True),
123
+ use_tool_proxy=enable,
124
+ )
125
+
126
+ # Restore config path if it was set[t.Any]
127
+ if self._config_path:
128
+ pass # Config path handled at manager level
129
+
52
130
  def validate_hooks_config(self) -> bool:
53
131
  try:
54
132
  result = subprocess.run(
@@ -5,17 +5,34 @@ from pathlib import Path
5
5
 
6
6
  from rich.console import Console
7
7
 
8
- from crackerjack.services.filesystem import FileSystemService
9
- from crackerjack.services.security import SecurityService
8
+ from crackerjack.models.protocols import FileSystemInterface, SecurityServiceProtocol
10
9
 
11
10
 
12
11
  class PublishManagerImpl:
13
- def __init__(self, console: Console, pkg_path: Path, dry_run: bool = False) -> None:
12
+ def __init__(
13
+ self,
14
+ console: Console,
15
+ pkg_path: Path,
16
+ dry_run: bool = False,
17
+ filesystem: FileSystemInterface | None = None,
18
+ security: SecurityServiceProtocol | None = None,
19
+ ) -> None:
14
20
  self.console = console
15
21
  self.pkg_path = pkg_path
16
22
  self.dry_run = dry_run
17
- self.filesystem = FileSystemService()
18
- self.security = SecurityService()
23
+
24
+ if filesystem is None:
25
+ from crackerjack.services.filesystem import FileSystemService
26
+
27
+ filesystem = FileSystemService()
28
+
29
+ if security is None:
30
+ from crackerjack.services.security import SecurityService
31
+
32
+ security = SecurityService()
33
+
34
+ self.filesystem = filesystem
35
+ self.security = security
19
36
 
20
37
  def _run_command(
21
38
  self,
@@ -50,7 +67,8 @@ class PublishManagerImpl:
50
67
 
51
68
  content = self.filesystem.read_file(pyproject_path)
52
69
  data = loads(content)
53
- return data.get("project", {}).get("version")
70
+ version = data.get("project", {}).get("version")
71
+ return version if isinstance(version, str) else None
54
72
  except Exception as e:
55
73
  self.console.print(f"[yellow]⚠️[/ yellow] Error reading version: {e}")
56
74
  return None
@@ -117,6 +135,8 @@ class PublishManagerImpl:
117
135
  self.console.print(
118
136
  f"[green]🚀[/ green] Bumped {version_type} version: {current_version} → {new_version}",
119
137
  )
138
+ # Update changelog after successful version bump
139
+ self._update_changelog_for_version(current_version, new_version)
120
140
  else:
121
141
  msg = "Failed to update version in file"
122
142
  raise ValueError(msg)
@@ -180,7 +200,7 @@ class PublishManagerImpl:
180
200
  [
181
201
  "keyring",
182
202
  "get",
183
- "https://upload.pypi.org/legacy/",
203
+ "https: //upload.pypi.org/legacy/",
184
204
  "__token__",
185
205
  ],
186
206
  )
@@ -211,7 +231,7 @@ class PublishManagerImpl:
211
231
  " 1. Set environment variable: export UV_PUBLISH_TOKEN=<your-pypi-token>",
212
232
  )
213
233
  self.console.print(
214
- " 2. Use keyring: keyring set https://upload.pypi.org/legacy/ __token__",
234
+ " 2. Use keyring: keyring set[t.Any] https: //upload.pypi.org/legacy/ __token__",
215
235
  )
216
236
  self.console.print(
217
237
  " 3. Ensure token starts with 'pypi-' and is properly formatted",
@@ -269,7 +289,7 @@ class PublishManagerImpl:
269
289
  if not dist_dir.exists():
270
290
  return
271
291
 
272
- artifacts = list(dist_dir.glob("*"))
292
+ artifacts = list[t.Any](dist_dir.glob("*"))
273
293
  self.console.print(f"[cyan]📦[/ cyan] Build artifacts ({len(artifacts)}): ")
274
294
 
275
295
  for artifact in artifacts[-5:]:
@@ -278,8 +298,8 @@ class PublishManagerImpl:
278
298
 
279
299
  def _format_file_size(self, size: int) -> str:
280
300
  if size < 1024 * 1024:
281
- return f"{size / 1024:.1f}KB"
282
- return f"{size / (1024 * 1024):.1f}MB"
301
+ return f"{size / 1024: .1f}KB"
302
+ return f"{size / (1024 * 1024): .1f}MB"
283
303
 
284
304
  def publish_package(self) -> bool:
285
305
  if not self._validate_prerequisites():
@@ -330,7 +350,7 @@ class PublishManagerImpl:
330
350
  package_name = self._get_package_name()
331
351
 
332
352
  if package_name and current_version:
333
- url = f"https://pypi.org/project/{package_name}/{current_version}/"
353
+ url = f"https: //pypi.org/project/{package_name}/{current_version}/"
334
354
  self.console.print(f"[cyan]🔗[/ cyan] Package URL: {url}")
335
355
 
336
356
  def _get_package_name(self) -> str | None:
@@ -341,7 +361,8 @@ class PublishManagerImpl:
341
361
 
342
362
  content = self.filesystem.read_file(pyproject_path)
343
363
  data = loads(content)
344
- return data.get("project", {}).get("name", "")
364
+ name = data.get("project", {}).get("name", "")
365
+ return name if isinstance(name, str) else None
345
366
 
346
367
  return None
347
368
 
@@ -429,3 +450,35 @@ class PublishManagerImpl:
429
450
  except Exception as e:
430
451
  self.console.print(f"[yellow]⚠️[/ yellow] Error reading package info: {e}")
431
452
  return {}
453
+
454
+ def _update_changelog_for_version(self, old_version: str, new_version: str) -> None:
455
+ """Update changelog with entries from git commits since last version."""
456
+ try:
457
+ from crackerjack.services.changelog_automation import ChangelogGenerator
458
+ from crackerjack.services.git import GitService
459
+
460
+ # Initialize services
461
+ git_service = GitService(self.console, self.pkg_path)
462
+ changelog_generator = ChangelogGenerator(self.console, git_service)
463
+
464
+ # Look for changelog file
465
+ changelog_path = self.pkg_path / "CHANGELOG.md"
466
+
467
+ # Generate changelog entries since last version
468
+ success = changelog_generator.generate_changelog_from_commits(
469
+ changelog_path=changelog_path,
470
+ version=new_version,
471
+ since_version=f"v{old_version}", # Assumes git tags are prefixed with 'v'
472
+ )
473
+
474
+ if success:
475
+ self.console.print(
476
+ f"[green]📝[/green] Updated changelog for version {new_version}"
477
+ )
478
+ else:
479
+ self.console.print(
480
+ "[yellow]⚠️[/yellow] Changelog update encountered issues"
481
+ )
482
+
483
+ except Exception as e:
484
+ self.console.print(f"[yellow]⚠️[/yellow] Failed to update changelog: {e}")
@@ -8,7 +8,7 @@ class TestCommandBuilder:
8
8
  self.pkg_path = pkg_path
9
9
 
10
10
  def build_command(self, options: OptionsProtocol) -> list[str]:
11
- cmd = ["python", "-m", "pytest"]
11
+ cmd = ["uv", "run", "python", "-m", "pytest"]
12
12
 
13
13
  self._add_coverage_options(cmd, options)
14
14
  self._add_worker_options(cmd, options)
@@ -23,22 +23,8 @@ class TestCommandBuilder:
23
23
  if hasattr(options, "test_workers") and options.test_workers:
24
24
  return options.test_workers
25
25
 
26
- # Temporarily disable multi-worker execution due to pytest-xdist
27
- # hanging issues with async tests. See GitHub issue for details.
28
- # TODO: Re-enable after fixing async test timeout issues
29
26
  return 1
30
27
 
31
- # Original multi-worker logic (commented out):
32
- # import multiprocessing
33
- # cpu_count = multiprocessing.cpu_count()
34
- # if cpu_count <= 2:
35
- # return 1
36
- # elif cpu_count <= 4:
37
- # return 2
38
- # elif cpu_count <= 8:
39
- # return 3
40
- # return 4
41
-
42
28
  def get_test_timeout(self, options: OptionsProtocol) -> int:
43
29
  if hasattr(options, "test_timeout") and options.test_timeout:
44
30
  return options.test_timeout
@@ -68,7 +54,7 @@ class TestCommandBuilder:
68
54
  [
69
55
  "--benchmark-only",
70
56
  "--benchmark-sort=mean",
71
- "--benchmark-columns=min,max,mean,stddev",
57
+ "--benchmark-columns=min, max, mean, stddev",
72
58
  ]
73
59
  )
74
60
 
@@ -99,7 +85,7 @@ class TestCommandBuilder:
99
85
  cmd.append(str(self.pkg_path))
100
86
 
101
87
  def build_specific_test_command(self, test_pattern: str) -> list[str]:
102
- cmd = ["python", "-m", "pytest", "-v"]
88
+ cmd = ["uv", "run", "python", "-m", "pytest", "-v"]
103
89
 
104
90
  cmd.extend(
105
91
  [
@@ -116,6 +102,8 @@ class TestCommandBuilder:
116
102
 
117
103
  def build_validation_command(self) -> list[str]:
118
104
  return [
105
+ "uv",
106
+ "run",
119
107
  "python",
120
108
  "-m",
121
109
  "pytest",
@@ -189,9 +189,7 @@ class TestExecutor:
189
189
 
190
190
  def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
191
191
  if "collected" in line and ("item" in line or "test" in line):
192
- match = re.search(
193
- r"(\d +) (?: item | test)", line
194
- ) # REGEX OK: parsing pytest output format
192
+ match = re.search(r"(\d +) (?: item | test)", line)
195
193
  if match:
196
194
  progress.update(
197
195
  total_tests=int(match.group(1)),
@@ -5,25 +5,44 @@ from pathlib import Path
5
5
 
6
6
  from rich.console import Console
7
7
 
8
- from crackerjack.models.protocols import OptionsProtocol
9
- from crackerjack.services.coverage_ratchet import CoverageRatchetService
8
+ from crackerjack.models.protocols import CoverageRatchetProtocol, OptionsProtocol
10
9
 
11
10
  from .test_command_builder import TestCommandBuilder
12
11
  from .test_executor import TestExecutor
13
12
 
14
13
 
15
14
  class TestManager:
16
- def __init__(self, console: Console, pkg_path: Path) -> None:
15
+ def __init__(
16
+ self,
17
+ console: Console,
18
+ pkg_path: Path,
19
+ coverage_ratchet: CoverageRatchetProtocol | None = None,
20
+ ) -> None:
17
21
  self.console = console
18
22
  self.pkg_path = pkg_path
19
23
 
20
24
  self.executor = TestExecutor(console, pkg_path)
21
25
  self.command_builder = TestCommandBuilder(pkg_path)
22
- self.coverage_ratchet = CoverageRatchetService(pkg_path, console)
26
+
27
+ if coverage_ratchet is None:
28
+ from crackerjack.services.coverage_ratchet import CoverageRatchetService
29
+
30
+ coverage_ratchet_obj = CoverageRatchetService(pkg_path, console)
31
+ self.coverage_ratchet: CoverageRatchetProtocol | None = t.cast(
32
+ CoverageRatchetProtocol, coverage_ratchet_obj
33
+ )
34
+ else:
35
+ self.coverage_ratchet = coverage_ratchet
23
36
 
24
37
  self._last_test_failures: list[str] = []
25
38
  self._progress_callback: t.Callable[[dict[str, t.Any]], None] | None = None
26
39
  self.coverage_ratchet_enabled = True
40
+ self.use_lsp_diagnostics = True
41
+
42
+ # Initialize coverage badge service
43
+ from crackerjack.services.coverage_badge_service import CoverageBadgeService
44
+
45
+ self._coverage_badge_service = CoverageBadgeService(console, pkg_path)
27
46
 
28
47
  def set_progress_callback(
29
48
  self,
@@ -145,11 +164,11 @@ class TestManager:
145
164
  test_path = self.pkg_path / test_dir
146
165
  if test_path.exists() and test_path.is_dir():
147
166
  for test_file_pattern in test_files:
148
- if list(test_path.glob(f"**/{test_file_pattern}")):
167
+ if list[t.Any](test_path.glob(f"**/{test_file_pattern}")):
149
168
  return True
150
169
 
151
170
  for test_file_pattern in test_files:
152
- if list(self.pkg_path.glob(test_file_pattern)):
171
+ if list[t.Any](self.pkg_path.glob(test_file_pattern)):
153
172
  return True
154
173
 
155
174
  return False
@@ -201,8 +220,44 @@ class TestManager:
201
220
  return True
202
221
 
203
222
  ratchet_result = self.coverage_ratchet.check_and_update_coverage()
223
+
224
+ # Update coverage badge if coverage information is available
225
+ self._update_coverage_badge(ratchet_result)
226
+
204
227
  return self._handle_ratchet_result(ratchet_result)
205
228
 
229
+ def _update_coverage_badge(self, ratchet_result: dict[str, t.Any]) -> None:
230
+ """Update coverage badge in README.md if coverage changed."""
231
+ try:
232
+ # Get current coverage directly from coverage.json to ensure freshest data
233
+ import json
234
+
235
+ current_coverage = None
236
+ coverage_json_path = self.pkg_path / "coverage.json"
237
+
238
+ if coverage_json_path.exists():
239
+ with coverage_json_path.open() as f:
240
+ data = json.load(f)
241
+ current_coverage = data.get("totals", {}).get("percent_covered")
242
+
243
+ # Fallback to ratchet result if coverage.json not available
244
+ if current_coverage is None:
245
+ current_coverage = ratchet_result.get("current_coverage")
246
+
247
+ # Final fallback to coverage service
248
+ if current_coverage is None:
249
+ coverage_info = self.get_coverage()
250
+ current_coverage = coverage_info.get("coverage_percent")
251
+
252
+ if current_coverage is not None:
253
+ if self._coverage_badge_service.should_update_badge(current_coverage):
254
+ self._coverage_badge_service.update_readme_coverage_badge(
255
+ current_coverage
256
+ )
257
+ except Exception as e:
258
+ # Don't fail the test process if badge update fails
259
+ self.console.print(f"[yellow]⚠️[/yellow] Badge update failed: {e}")
260
+
206
261
  def _handle_ratchet_result(self, ratchet_result: dict[str, t.Any]) -> bool:
207
262
  if ratchet_result.get("success", False):
208
263
  if ratchet_result.get("improved", False):
@@ -216,7 +271,7 @@ class TestManager:
216
271
  previous = ratchet_result.get("previous_coverage", 0)
217
272
  self.console.print(
218
273
  f"[red]📉[/ red] Coverage regression: "
219
- f"{current:.2f}% < {previous:.2f}%"
274
+ f"{current: .2f}% < {previous: .2f}%"
220
275
  )
221
276
  return False
222
277
 
@@ -244,5 +299,52 @@ class TestManager:
244
299
  def _get_timeout(self, options: OptionsProtocol) -> int:
245
300
  return self.command_builder.get_test_timeout(options)
246
301
 
302
+ async def run_pre_test_lsp_diagnostics(self) -> bool:
303
+ """Run LSP diagnostics before tests to catch type errors early."""
304
+ if not self.use_lsp_diagnostics:
305
+ return True
306
+
307
+ try:
308
+ from crackerjack.services.lsp_client import LSPClient
309
+
310
+ lsp_client = LSPClient(self.console)
311
+
312
+ # Check if LSP server is available
313
+ if not lsp_client.is_server_running():
314
+ return True # No LSP server, skip diagnostics
315
+
316
+ # Run type diagnostics on the project
317
+ diagnostics, summary = lsp_client.check_project_with_feedback(
318
+ self.pkg_path,
319
+ show_progress=False, # Keep quiet for test integration
320
+ )
321
+
322
+ # Check if there are type errors
323
+ has_errors = any(diags for diags in diagnostics.values())
324
+
325
+ if has_errors:
326
+ self.console.print(
327
+ "[yellow]⚠️ LSP detected type errors before running tests[/yellow]"
328
+ )
329
+ # Format and show a summary
330
+ error_count = sum(len(diags) for diags in diagnostics.values())
331
+ self.console.print(f"[yellow]Found {error_count} type issues[/yellow]")
332
+
333
+ return not has_errors # Return False if there are type errors
334
+
335
+ except Exception as e:
336
+ # If LSP diagnostics fail, don't block tests
337
+ self.console.print(f"[dim]LSP diagnostics failed: {e}[/dim]")
338
+ return True
339
+
340
+ def configure_lsp_diagnostics(self, enable: bool) -> None:
341
+ """Enable or disable LSP diagnostics integration."""
342
+ self.use_lsp_diagnostics = enable
343
+
344
+ if enable:
345
+ self.console.print(
346
+ "[cyan]🔍 LSP diagnostics enabled for faster test feedback[/cyan]"
347
+ )
348
+
247
349
 
248
350
  TestManagementImpl = TestManager