crackerjack 0.33.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 (198) 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 +4 -13
  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 +104 -204
  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 +171 -174
  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 +44 -8
  74. crackerjack/managers/test_command_builder.py +1 -15
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +98 -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 +17 -16
  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 +173 -32
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +8 -10
  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 +0 -2
  109. crackerjack/mixins/error_handling.py +1 -70
  110. crackerjack/models/config.py +12 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +122 -122
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  115. crackerjack/monitoring/metrics_collector.py +426 -0
  116. crackerjack/monitoring/regression_prevention.py +8 -8
  117. crackerjack/monitoring/websocket_server.py +643 -0
  118. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  119. crackerjack/orchestration/coverage_improvement.py +3 -3
  120. crackerjack/orchestration/execution_strategies.py +26 -6
  121. crackerjack/orchestration/test_progress_streamer.py +8 -5
  122. crackerjack/plugins/base.py +2 -2
  123. crackerjack/plugins/hooks.py +7 -0
  124. crackerjack/plugins/managers.py +11 -8
  125. crackerjack/security/__init__.py +0 -1
  126. crackerjack/security/audit.py +6 -35
  127. crackerjack/services/anomaly_detector.py +392 -0
  128. crackerjack/services/api_extractor.py +615 -0
  129. crackerjack/services/backup_service.py +2 -2
  130. crackerjack/services/bounded_status_operations.py +15 -152
  131. crackerjack/services/cache.py +127 -1
  132. crackerjack/services/changelog_automation.py +395 -0
  133. crackerjack/services/config.py +15 -9
  134. crackerjack/services/config_merge.py +19 -80
  135. crackerjack/services/config_template.py +506 -0
  136. crackerjack/services/contextual_ai_assistant.py +48 -22
  137. crackerjack/services/coverage_badge_service.py +171 -0
  138. crackerjack/services/coverage_ratchet.py +27 -25
  139. crackerjack/services/debug.py +3 -3
  140. crackerjack/services/dependency_analyzer.py +460 -0
  141. crackerjack/services/dependency_monitor.py +14 -11
  142. crackerjack/services/documentation_generator.py +491 -0
  143. crackerjack/services/documentation_service.py +675 -0
  144. crackerjack/services/enhanced_filesystem.py +6 -5
  145. crackerjack/services/enterprise_optimizer.py +865 -0
  146. crackerjack/services/error_pattern_analyzer.py +676 -0
  147. crackerjack/services/file_hasher.py +1 -1
  148. crackerjack/services/git.py +8 -25
  149. crackerjack/services/health_metrics.py +10 -8
  150. crackerjack/services/heatmap_generator.py +735 -0
  151. crackerjack/services/initialization.py +11 -30
  152. crackerjack/services/input_validator.py +5 -97
  153. crackerjack/services/intelligent_commit.py +327 -0
  154. crackerjack/services/log_manager.py +15 -12
  155. crackerjack/services/logging.py +4 -3
  156. crackerjack/services/lsp_client.py +628 -0
  157. crackerjack/services/memory_optimizer.py +19 -87
  158. crackerjack/services/metrics.py +42 -33
  159. crackerjack/services/parallel_executor.py +9 -67
  160. crackerjack/services/pattern_cache.py +1 -1
  161. crackerjack/services/pattern_detector.py +6 -6
  162. crackerjack/services/performance_benchmarks.py +18 -59
  163. crackerjack/services/performance_cache.py +20 -81
  164. crackerjack/services/performance_monitor.py +27 -95
  165. crackerjack/services/predictive_analytics.py +510 -0
  166. crackerjack/services/quality_baseline.py +234 -0
  167. crackerjack/services/quality_baseline_enhanced.py +646 -0
  168. crackerjack/services/quality_intelligence.py +785 -0
  169. crackerjack/services/regex_patterns.py +605 -524
  170. crackerjack/services/regex_utils.py +43 -123
  171. crackerjack/services/secure_path_utils.py +5 -164
  172. crackerjack/services/secure_status_formatter.py +30 -141
  173. crackerjack/services/secure_subprocess.py +11 -92
  174. crackerjack/services/security.py +9 -41
  175. crackerjack/services/security_logger.py +12 -24
  176. crackerjack/services/server_manager.py +124 -16
  177. crackerjack/services/status_authentication.py +16 -159
  178. crackerjack/services/status_security_manager.py +4 -131
  179. crackerjack/services/thread_safe_status_collector.py +19 -125
  180. crackerjack/services/unified_config.py +21 -13
  181. crackerjack/services/validation_rate_limiter.py +5 -54
  182. crackerjack/services/version_analyzer.py +459 -0
  183. crackerjack/services/version_checker.py +1 -1
  184. crackerjack/services/websocket_resource_limiter.py +10 -144
  185. crackerjack/services/zuban_lsp_service.py +390 -0
  186. crackerjack/slash_commands/__init__.py +2 -7
  187. crackerjack/slash_commands/run.md +2 -2
  188. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  189. crackerjack/tools/validate_regex_patterns.py +19 -48
  190. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +196 -25
  191. crackerjack-0.33.1.dist-info/RECORD +229 -0
  192. crackerjack/CLAUDE.md +0 -207
  193. crackerjack/RULES.md +0 -380
  194. crackerjack/py313.py +0 -234
  195. crackerjack-0.33.0.dist-info/RECORD +0 -187
  196. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  197. {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  198. {crackerjack-0.33.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(
@@ -67,7 +67,8 @@ class PublishManagerImpl:
67
67
 
68
68
  content = self.filesystem.read_file(pyproject_path)
69
69
  data = loads(content)
70
- return data.get("project", {}).get("version")
70
+ version = data.get("project", {}).get("version")
71
+ return version if isinstance(version, str) else None
71
72
  except Exception as e:
72
73
  self.console.print(f"[yellow]⚠️[/ yellow] Error reading version: {e}")
73
74
  return None
@@ -134,6 +135,8 @@ class PublishManagerImpl:
134
135
  self.console.print(
135
136
  f"[green]🚀[/ green] Bumped {version_type} version: {current_version} → {new_version}",
136
137
  )
138
+ # Update changelog after successful version bump
139
+ self._update_changelog_for_version(current_version, new_version)
137
140
  else:
138
141
  msg = "Failed to update version in file"
139
142
  raise ValueError(msg)
@@ -197,7 +200,7 @@ class PublishManagerImpl:
197
200
  [
198
201
  "keyring",
199
202
  "get",
200
- "https://upload.pypi.org/legacy/",
203
+ "https: //upload.pypi.org/legacy/",
201
204
  "__token__",
202
205
  ],
203
206
  )
@@ -228,7 +231,7 @@ class PublishManagerImpl:
228
231
  " 1. Set environment variable: export UV_PUBLISH_TOKEN=<your-pypi-token>",
229
232
  )
230
233
  self.console.print(
231
- " 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__",
232
235
  )
233
236
  self.console.print(
234
237
  " 3. Ensure token starts with 'pypi-' and is properly formatted",
@@ -286,7 +289,7 @@ class PublishManagerImpl:
286
289
  if not dist_dir.exists():
287
290
  return
288
291
 
289
- artifacts = list(dist_dir.glob("*"))
292
+ artifacts = list[t.Any](dist_dir.glob("*"))
290
293
  self.console.print(f"[cyan]📦[/ cyan] Build artifacts ({len(artifacts)}): ")
291
294
 
292
295
  for artifact in artifacts[-5:]:
@@ -295,8 +298,8 @@ class PublishManagerImpl:
295
298
 
296
299
  def _format_file_size(self, size: int) -> str:
297
300
  if size < 1024 * 1024:
298
- return f"{size / 1024:.1f}KB"
299
- return f"{size / (1024 * 1024):.1f}MB"
301
+ return f"{size / 1024: .1f}KB"
302
+ return f"{size / (1024 * 1024): .1f}MB"
300
303
 
301
304
  def publish_package(self) -> bool:
302
305
  if not self._validate_prerequisites():
@@ -347,7 +350,7 @@ class PublishManagerImpl:
347
350
  package_name = self._get_package_name()
348
351
 
349
352
  if package_name and current_version:
350
- url = f"https://pypi.org/project/{package_name}/{current_version}/"
353
+ url = f"https: //pypi.org/project/{package_name}/{current_version}/"
351
354
  self.console.print(f"[cyan]🔗[/ cyan] Package URL: {url}")
352
355
 
353
356
  def _get_package_name(self) -> str | None:
@@ -358,7 +361,8 @@ class PublishManagerImpl:
358
361
 
359
362
  content = self.filesystem.read_file(pyproject_path)
360
363
  data = loads(content)
361
- return data.get("project", {}).get("name", "")
364
+ name = data.get("project", {}).get("name", "")
365
+ return name if isinstance(name, str) else None
362
366
 
363
367
  return None
364
368
 
@@ -446,3 +450,35 @@ class PublishManagerImpl:
446
450
  except Exception as e:
447
451
  self.console.print(f"[yellow]⚠️[/ yellow] Error reading package info: {e}")
448
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}")
@@ -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
 
@@ -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)),
@@ -25,16 +25,24 @@ class TestManager:
25
25
  self.command_builder = TestCommandBuilder(pkg_path)
26
26
 
27
27
  if coverage_ratchet is None:
28
- # Import here to avoid circular imports
29
28
  from crackerjack.services.coverage_ratchet import CoverageRatchetService
30
29
 
31
- coverage_ratchet = CoverageRatchetService(pkg_path, console)
32
-
33
- self.coverage_ratchet = coverage_ratchet
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
34
36
 
35
37
  self._last_test_failures: list[str] = []
36
38
  self._progress_callback: t.Callable[[dict[str, t.Any]], None] | None = None
37
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)
38
46
 
39
47
  def set_progress_callback(
40
48
  self,
@@ -156,11 +164,11 @@ class TestManager:
156
164
  test_path = self.pkg_path / test_dir
157
165
  if test_path.exists() and test_path.is_dir():
158
166
  for test_file_pattern in test_files:
159
- if list(test_path.glob(f"**/{test_file_pattern}")):
167
+ if list[t.Any](test_path.glob(f"**/{test_file_pattern}")):
160
168
  return True
161
169
 
162
170
  for test_file_pattern in test_files:
163
- if list(self.pkg_path.glob(test_file_pattern)):
171
+ if list[t.Any](self.pkg_path.glob(test_file_pattern)):
164
172
  return True
165
173
 
166
174
  return False
@@ -212,8 +220,44 @@ class TestManager:
212
220
  return True
213
221
 
214
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
+
215
227
  return self._handle_ratchet_result(ratchet_result)
216
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
+
217
261
  def _handle_ratchet_result(self, ratchet_result: dict[str, t.Any]) -> bool:
218
262
  if ratchet_result.get("success", False):
219
263
  if ratchet_result.get("improved", False):
@@ -227,7 +271,7 @@ class TestManager:
227
271
  previous = ratchet_result.get("previous_coverage", 0)
228
272
  self.console.print(
229
273
  f"[red]📉[/ red] Coverage regression: "
230
- f"{current:.2f}% < {previous:.2f}%"
274
+ f"{current: .2f}% < {previous: .2f}%"
231
275
  )
232
276
  return False
233
277
 
@@ -255,5 +299,52 @@ class TestManager:
255
299
  def _get_timeout(self, options: OptionsProtocol) -> int:
256
300
  return self.command_builder.get_test_timeout(options)
257
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
+
258
349
 
259
350
  TestManagementImpl = TestManager
@@ -172,7 +172,7 @@ class TestManagementImpl:
172
172
  self.console.print("[yellow]⚠️[/ yellow] Coverage ratchet disabled")
173
173
 
174
174
  def get_coverage_ratchet_status(self) -> dict[str, t.Any]:
175
- return self.coverage_ratchet.get_status_report()
175
+ return self.coverage_ratchet.get_ratchet_data()
176
176
 
177
177
  def _run_test_command(
178
178
  self,
@@ -482,9 +482,8 @@ class TestManagementImpl:
482
482
  self._handle_running_test(line, progress)
483
483
 
484
484
  def _handle_collection_completion(self, line: str, progress: TestProgress) -> bool:
485
- if match := re.search(
486
- r"collected (\d +) items?", line
487
- ): # REGEX OK: parsing pytest collection output
485
+ match = re.search(r"collected (\d+) items?", line)
486
+ if match:
488
487
  progress.update(
489
488
  total_tests=int(match.group(1)),
490
489
  is_collecting=False,
@@ -728,7 +727,7 @@ class TestManagementImpl:
728
727
  import os
729
728
 
730
729
  cpu_count = os.cpu_count() or 1
731
- test_files = list(self.pkg_path.glob("tests/test_*.py"))
730
+ test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
732
731
  if len(test_files) < 5:
733
732
  return min(2, cpu_count)
734
733
 
@@ -737,7 +736,7 @@ class TestManagementImpl:
737
736
  def _get_test_timeout(self, options: OptionsProtocol) -> int:
738
737
  if options.test_timeout > 0:
739
738
  return options.test_timeout
740
- test_files = list(self.pkg_path.glob("tests/test_*.py"))
739
+ test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
741
740
  base_timeout = 300
742
741
 
743
742
  import math
@@ -1020,7 +1019,9 @@ class TestManagementImpl:
1020
1019
  test_dir = self.pkg_path / "tests"
1021
1020
  if not test_dir.exists():
1022
1021
  issues.append("tests directory not found")
1023
- test_files = list(test_dir.glob("test_ *.py")) if test_dir.exists() else []
1022
+ test_files = (
1023
+ list[t.Any](test_dir.glob("test_ *.py")) if test_dir.exists() else []
1024
+ )
1024
1025
  if not test_files:
1025
1026
  issues.append("no test files found")
1026
1027
  if issues:
@@ -1035,7 +1036,7 @@ class TestManagementImpl:
1035
1036
  test_dir = self.pkg_path / "tests"
1036
1037
  if not test_dir.exists():
1037
1038
  return {"test_files": 0, "total_tests": 0, "test_lines": 0}
1038
- test_files = list(test_dir.glob("test_ *.py"))
1039
+ test_files = list[t.Any](test_dir.glob("test_ *.py"))
1039
1040
  total_lines = 0
1040
1041
  total_tests = 0
1041
1042
  for test_file in test_files:
@@ -1070,5 +1071,5 @@ class TestManagementImpl:
1070
1071
  return None
1071
1072
 
1072
1073
  def has_tests(self) -> bool:
1073
- test_files = list(self.pkg_path.glob("tests/test_*.py"))
1074
+ test_files = list[t.Any](self.pkg_path.glob("tests/test_*.py"))
1074
1075
  return len(test_files) > 0
crackerjack/mcp/cache.py CHANGED
@@ -97,7 +97,7 @@ class ErrorCache:
97
97
  ]
98
98
 
99
99
  def get_common_patterns(self, limit: int = 20) -> list[ErrorPattern]:
100
- patterns = list(self.patterns.values())
100
+ patterns = list[t.Any](self.patterns.values())
101
101
  patterns.sort(key=lambda p: p.frequency, reverse=True)
102
102
  return patterns[:limit]
103
103
 
@@ -252,7 +252,7 @@ class ErrorCache:
252
252
  successful_fixes = sum(1 for result in self.fix_results if result.success)
253
253
  frequencies = [pattern.frequency for pattern in self.patterns.values()]
254
254
  avg_frequency = sum(frequencies) / len(frequencies) if frequencies else 0
255
- type_counts = {}
255
+ type_counts: dict[str, int] = {}
256
256
  for pattern in self.patterns.values():
257
257
  type_counts[pattern.error_type] = type_counts.get(pattern.error_type, 0) + 1
258
258
 
@@ -22,7 +22,7 @@ def is_mcp_server_running(host: str = "localhost", port: int = 5173) -> bool:
22
22
  sock.close()
23
23
 
24
24
 
25
- async def ensure_mcp_server_running() -> subprocess.Popen | None:
25
+ async def ensure_mcp_server_running() -> subprocess.Popen[bytes] | None:
26
26
  console = Console()
27
27
 
28
28
  if is_mcp_server_running():