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
@@ -0,0 +1,628 @@
1
+ import asyncio
2
+ import subprocess
3
+ import typing as t
4
+ from pathlib import Path
5
+ from typing import Protocol
6
+
7
+ from rich.console import Console
8
+ from rich.progress import (
9
+ BarColumn,
10
+ Progress,
11
+ SpinnerColumn,
12
+ TextColumn,
13
+ TimeElapsedColumn,
14
+ )
15
+
16
+ from .server_manager import find_zuban_lsp_processes
17
+ from .zuban_lsp_service import ZubanLSPService
18
+
19
+
20
+ class ProgressCallback(Protocol):
21
+ """Protocol for progress reporting during type checking."""
22
+
23
+ def on_file_start(self, file_path: str) -> None:
24
+ """Called when starting to check a file."""
25
+ ...
26
+
27
+ def on_file_complete(self, file_path: str, error_count: int) -> None:
28
+ """Called when finished checking a file."""
29
+ ...
30
+
31
+ def on_progress(self, current: int, total: int) -> None:
32
+ """Called to report overall progress."""
33
+ ...
34
+
35
+
36
+ class RealTimeTypingFeedback:
37
+ """Provides real-time feedback during type checking operations."""
38
+
39
+ def __init__(self, console: Console | None = None) -> None:
40
+ self.console = console or Console()
41
+ self._total_errors = 0
42
+ self._files_checked = 0
43
+
44
+ def create_progress_display(self) -> Progress:
45
+ """Create a progress display for type checking."""
46
+ return Progress(
47
+ SpinnerColumn(),
48
+ TextColumn("[progress.description]{task.description}"),
49
+ BarColumn(),
50
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
51
+ TextColumn("({task.completed}/{task.total})"),
52
+ TimeElapsedColumn(),
53
+ console=self.console,
54
+ )
55
+
56
+ def on_file_start(self, file_path: str) -> None:
57
+ """Report that we're starting to check a file."""
58
+ rel_path = Path(file_path).name
59
+ self.console.print(f"🔍 Checking {rel_path}...", style="dim")
60
+
61
+ def on_file_complete(self, file_path: str, error_count: int) -> None:
62
+ """Report completion of file checking."""
63
+ rel_path = Path(file_path).name
64
+ self._files_checked += 1
65
+ self._total_errors += error_count
66
+
67
+ if error_count == 0:
68
+ self.console.print(f"✅ {rel_path} - No issues", style="green dim")
69
+ else:
70
+ self.console.print(
71
+ f"❌ {rel_path} - {error_count} error(s)", style="red dim"
72
+ )
73
+
74
+ def on_progress(self, current: int, total: int) -> None:
75
+ """Report overall progress."""
76
+ pass # Progress bar handles this
77
+
78
+ def get_summary(self) -> str:
79
+ """Get a summary of the checking results."""
80
+ if self._total_errors == 0:
81
+ return f"✅ All {self._files_checked} files passed type checking"
82
+ return (
83
+ f"❌ Found {self._total_errors} type errors in {self._files_checked} files"
84
+ )
85
+
86
+
87
+ class JSONRPCClient:
88
+ """JSON-RPC client for LSP communication."""
89
+
90
+ def __init__(self, lsp_service: ZubanLSPService) -> None:
91
+ self.lsp_service = lsp_service
92
+ self._request_id = 0
93
+
94
+ def _next_request_id(self) -> int:
95
+ """Generate next request ID."""
96
+ self._request_id += 1
97
+ return self._request_id
98
+
99
+ async def initialize(self, root_path: str) -> dict[str, t.Any] | None:
100
+ """Initialize the LSP server for a workspace."""
101
+ params = {
102
+ "processId": None,
103
+ "rootPath": root_path,
104
+ "rootUri": f"file://{root_path}",
105
+ "capabilities": {
106
+ "textDocument": {
107
+ "publishDiagnostics": {
108
+ "versionSupport": True,
109
+ "tagSupport": {"valueSet": [1, 2]},
110
+ "relatedInformation": True,
111
+ "codeDescriptionSupport": True,
112
+ "dataSupport": True,
113
+ }
114
+ }
115
+ },
116
+ }
117
+ return await self.lsp_service.send_lsp_request("initialize", params)
118
+
119
+ async def did_open(self, file_path: str) -> dict[str, t.Any] | None:
120
+ """Notify server that a document was opened."""
121
+ content = Path(file_path).read_text(encoding="utf-8")
122
+
123
+ params = {
124
+ "textDocument": {
125
+ "uri": f"file://{file_path}",
126
+ "languageId": "python",
127
+ "version": 1,
128
+ "text": content,
129
+ }
130
+ }
131
+ return await self.lsp_service.send_lsp_request("textDocument/didOpen", params)
132
+
133
+ async def did_close(self, file_path: str) -> dict[str, t.Any] | None:
134
+ """Notify server that a document was closed."""
135
+ params = {
136
+ "textDocument": {
137
+ "uri": f"file://{file_path}",
138
+ }
139
+ }
140
+ return await self.lsp_service.send_lsp_request("textDocument/didClose", params)
141
+
142
+
143
+ class LSPClient:
144
+ """Client for communicating with Zuban LSP server."""
145
+
146
+ def __init__(self, console: Console | None = None) -> None:
147
+ self.console = console or Console()
148
+ self._server_port: int | None = None
149
+ self._server_host: str = "127.0.0.1"
150
+ self._lsp_service: ZubanLSPService | None = None
151
+ self._jsonrpc_client: JSONRPCClient | None = None
152
+
153
+ def is_server_running(self) -> bool:
154
+ """Check if Zuban LSP server is currently running."""
155
+ if self._lsp_service and self._lsp_service.is_running:
156
+ return True
157
+ processes = find_zuban_lsp_processes()
158
+ return len(processes) > 0
159
+
160
+ async def _ensure_lsp_service(self) -> bool:
161
+ """Ensure LSP service is available and initialized."""
162
+ if self._lsp_service and self._lsp_service.is_running:
163
+ return True
164
+
165
+ # Create new LSP service
166
+ self._lsp_service = ZubanLSPService(
167
+ port=self._server_port or 8677,
168
+ mode="stdio", # Currently zuban only supports stdio
169
+ console=self.console,
170
+ )
171
+
172
+ # Start the service
173
+ if await self._lsp_service.start():
174
+ self._jsonrpc_client = JSONRPCClient(self._lsp_service)
175
+ return True
176
+
177
+ return False
178
+
179
+ def get_server_info(self) -> dict[str, t.Any] | None:
180
+ """Get information about the running LSP server."""
181
+ processes = find_zuban_lsp_processes()
182
+ if not processes:
183
+ return None
184
+
185
+ return {
186
+ "pid": processes[0]["pid"],
187
+ "cpu": processes[0]["cpu"],
188
+ "mem": processes[0]["mem"],
189
+ "command": processes[0]["command"],
190
+ }
191
+
192
+ def check_files(
193
+ self,
194
+ file_paths: list[str],
195
+ progress_callback: ProgressCallback | None = None,
196
+ show_progress: bool = True,
197
+ ) -> dict[str, list[dict[str, t.Any]]]:
198
+ """
199
+ Check files for type errors using LSP server with real-time feedback.
200
+
201
+ Args:
202
+ file_paths: List of file paths to check
203
+ progress_callback: Optional callback for progress reporting
204
+ show_progress: Whether to show progress display
205
+
206
+ Returns:
207
+ Dictionary mapping file paths to lists of diagnostic messages.
208
+ Each diagnostic contains: line, column, severity, message, code.
209
+ """
210
+ if not self.is_server_running():
211
+ if progress_callback:
212
+ self.console.print(
213
+ "⚠️ Zuban LSP server not running, falling back to direct zuban calls",
214
+ style="yellow",
215
+ )
216
+ return self._check_files_with_feedback(
217
+ file_paths, progress_callback, show_progress
218
+ )
219
+
220
+ # When LSP server is running, use it for better performance
221
+ return self._check_files_via_lsp(file_paths, progress_callback, show_progress)
222
+
223
+ def _check_files_with_feedback(
224
+ self,
225
+ file_paths: list[str],
226
+ progress_callback: ProgressCallback | None = None,
227
+ show_progress: bool = True,
228
+ ) -> dict[str, list[dict[str, t.Any]]]:
229
+ """Check files with real-time feedback using direct zuban calls."""
230
+ total_files = len(file_paths)
231
+
232
+ if show_progress and total_files > 1:
233
+ return self._check_files_with_progress_display(
234
+ file_paths, progress_callback, total_files
235
+ )
236
+ return self._check_files_simple_feedback(file_paths, progress_callback)
237
+
238
+ def _check_files_with_progress_display(
239
+ self,
240
+ file_paths: list[str],
241
+ progress_callback: ProgressCallback | None,
242
+ total_files: int,
243
+ ) -> dict[str, list[dict[str, t.Any]]]:
244
+ """Check files with progress display."""
245
+ diagnostics = {}
246
+ feedback = RealTimeTypingFeedback(self.console)
247
+
248
+ with feedback.create_progress_display() as progress:
249
+ task = progress.add_task("Type checking files...", total=total_files)
250
+
251
+ for file_path in file_paths:
252
+ diagnostics.update(
253
+ self._process_single_file_with_zuban(file_path, progress_callback)
254
+ )
255
+ progress.update(task, advance=1)
256
+
257
+ return diagnostics
258
+
259
+ def _check_files_simple_feedback(
260
+ self,
261
+ file_paths: list[str],
262
+ progress_callback: ProgressCallback | None,
263
+ ) -> dict[str, list[dict[str, t.Any]]]:
264
+ """Check files without progress display."""
265
+ diagnostics = {}
266
+
267
+ for file_path in file_paths:
268
+ diagnostics.update(
269
+ self._process_single_file_with_zuban(file_path, progress_callback)
270
+ )
271
+
272
+ return diagnostics
273
+
274
+ def _process_single_file_with_zuban(
275
+ self,
276
+ file_path: str,
277
+ progress_callback: ProgressCallback | None,
278
+ ) -> dict[str, list[dict[str, t.Any]]]:
279
+ """Process a single file with zuban and handle callbacks."""
280
+ if progress_callback:
281
+ progress_callback.on_file_start(file_path)
282
+
283
+ file_diagnostics = self._check_file_with_zuban(file_path)
284
+
285
+ if progress_callback:
286
+ progress_callback.on_file_complete(file_path, len(file_diagnostics))
287
+
288
+ return {file_path: file_diagnostics}
289
+
290
+ def _check_files_via_lsp(
291
+ self,
292
+ file_paths: list[str],
293
+ progress_callback: ProgressCallback | None = None,
294
+ show_progress: bool = True,
295
+ ) -> dict[str, list[dict[str, t.Any]]]:
296
+ """Check files using LSP server communication."""
297
+ # Use asyncio to run the async LSP implementation
298
+ try:
299
+ # Try to get or create an event loop
300
+ try:
301
+ asyncio.get_running_loop()
302
+ # We're already in an async context, use a thread pool
303
+ import concurrent.futures
304
+
305
+ with concurrent.futures.ThreadPoolExecutor() as executor:
306
+ future = executor.submit(
307
+ self._run_async_lsp_check,
308
+ file_paths,
309
+ progress_callback,
310
+ show_progress,
311
+ )
312
+ return future.result(timeout=120) # 2 minute timeout
313
+ except RuntimeError:
314
+ # No running loop, create new one
315
+ return asyncio.run(
316
+ self._async_check_files_via_lsp(
317
+ file_paths, progress_callback, show_progress
318
+ )
319
+ )
320
+ except Exception as e:
321
+ self.console.print(
322
+ f"[yellow]⚠️ LSP communication failed: {e}, falling back to direct calls[/yellow]"
323
+ )
324
+ return self._check_files_with_feedback(
325
+ file_paths, progress_callback, show_progress
326
+ )
327
+
328
+ def _run_async_lsp_check(
329
+ self,
330
+ file_paths: list[str],
331
+ progress_callback: ProgressCallback | None = None,
332
+ show_progress: bool = True,
333
+ ) -> dict[str, list[dict[str, t.Any]]]:
334
+ """Run async LSP check in a new event loop."""
335
+ return asyncio.run(
336
+ self._async_check_files_via_lsp(
337
+ file_paths, progress_callback, show_progress
338
+ )
339
+ )
340
+
341
+ async def _async_check_files_via_lsp(
342
+ self,
343
+ file_paths: list[str],
344
+ progress_callback: ProgressCallback | None = None,
345
+ show_progress: bool = True,
346
+ ) -> dict[str, list[dict[str, t.Any]]]:
347
+ """Async implementation of LSP-based file checking."""
348
+ # Validate prerequisites
349
+ if not await self._validate_lsp_prerequisites():
350
+ return self._check_files_with_feedback(
351
+ file_paths, progress_callback, show_progress
352
+ )
353
+
354
+ try:
355
+ # Initialize LSP workspace
356
+ await self._initialize_lsp_workspace(file_paths)
357
+
358
+ # Process files with appropriate progress handling
359
+ return await self._process_files_via_lsp(
360
+ file_paths, progress_callback, show_progress
361
+ )
362
+
363
+ except Exception as e:
364
+ self.console.print(
365
+ f"[yellow]⚠️ LSP protocol error: {e}, falling back to direct calls[/yellow]"
366
+ )
367
+ return self._check_files_with_feedback(
368
+ file_paths, progress_callback, show_progress
369
+ )
370
+
371
+ async def _validate_lsp_prerequisites(self) -> bool:
372
+ """Validate that LSP service and client are ready."""
373
+ if not await self._ensure_lsp_service():
374
+ return False
375
+
376
+ if not self._jsonrpc_client:
377
+ return False
378
+
379
+ return True
380
+
381
+ async def _initialize_lsp_workspace(self, file_paths: list[str]) -> None:
382
+ """Initialize LSP workspace with project root."""
383
+ assert self._jsonrpc_client is not None, "LSP client must be initialized"
384
+ project_root = (
385
+ str(Path(file_paths[0]).parent) if file_paths else str(Path.cwd())
386
+ )
387
+ await self._jsonrpc_client.initialize(project_root)
388
+
389
+ async def _process_files_via_lsp(
390
+ self,
391
+ file_paths: list[str],
392
+ progress_callback: ProgressCallback | None = None,
393
+ show_progress: bool = True,
394
+ ) -> dict[str, list[dict[str, t.Any]]]:
395
+ """Process files via LSP with progress tracking."""
396
+ total_files = len(file_paths)
397
+
398
+ if show_progress and total_files > 1:
399
+ return await self._process_files_with_progress(
400
+ file_paths, progress_callback, total_files
401
+ )
402
+ return await self._process_files_simple(file_paths, progress_callback)
403
+
404
+ async def _process_files_with_progress(
405
+ self,
406
+ file_paths: list[str],
407
+ progress_callback: ProgressCallback | None,
408
+ total_files: int,
409
+ ) -> dict[str, list[dict[str, t.Any]]]:
410
+ """Process files with progress display."""
411
+ diagnostics = {}
412
+ feedback = RealTimeTypingFeedback(self.console)
413
+
414
+ with feedback.create_progress_display() as progress:
415
+ task = progress.add_task("LSP type checking files...", total=total_files)
416
+
417
+ for file_path in file_paths:
418
+ diagnostics.update(
419
+ await self._process_single_file_with_callback(
420
+ file_path, progress_callback
421
+ )
422
+ )
423
+ progress.update(task, advance=1)
424
+
425
+ return diagnostics
426
+
427
+ async def _process_files_simple(
428
+ self,
429
+ file_paths: list[str],
430
+ progress_callback: ProgressCallback | None,
431
+ ) -> dict[str, list[dict[str, t.Any]]]:
432
+ """Process files without progress display."""
433
+ diagnostics = {}
434
+
435
+ for file_path in file_paths:
436
+ diagnostics.update(
437
+ await self._process_single_file_with_callback(
438
+ file_path, progress_callback
439
+ )
440
+ )
441
+
442
+ return diagnostics
443
+
444
+ async def _process_single_file_with_callback(
445
+ self,
446
+ file_path: str,
447
+ progress_callback: ProgressCallback | None,
448
+ ) -> dict[str, list[dict[str, t.Any]]]:
449
+ """Process a single file with progress callback handling."""
450
+ if progress_callback:
451
+ progress_callback.on_file_start(file_path)
452
+
453
+ file_diagnostics = await self._check_file_via_lsp(file_path)
454
+
455
+ if progress_callback:
456
+ progress_callback.on_file_complete(file_path, len(file_diagnostics))
457
+
458
+ return {file_path: file_diagnostics}
459
+
460
+ async def _check_file_via_lsp(self, file_path: str) -> list[dict[str, t.Any]]:
461
+ """Check a single file via LSP protocol."""
462
+ if not self._jsonrpc_client:
463
+ # Fallback to direct zuban call
464
+ return self._check_file_with_zuban(file_path)
465
+
466
+ try:
467
+ # Notify server about file
468
+ await self._jsonrpc_client.did_open(file_path)
469
+
470
+ # Wait briefly for diagnostics to be published
471
+ await asyncio.sleep(0.1)
472
+
473
+ # For now, since we don't have diagnostic collection implemented,
474
+ # we'll fall back to the direct zuban call but log that we used LSP
475
+ # In a full implementation, we'd collect diagnostics from LSP notifications
476
+ diagnostics = self._check_file_with_zuban(file_path)
477
+
478
+ # Clean up
479
+ await self._jsonrpc_client.did_close(file_path)
480
+
481
+ return diagnostics
482
+
483
+ except Exception:
484
+ # Fallback to direct zuban call on any LSP error
485
+ return self._check_file_with_zuban(file_path)
486
+
487
+ def _check_file_with_zuban(self, file_path: str) -> list[dict[str, t.Any]]:
488
+ """
489
+ Check a single file using zuban directly.
490
+
491
+ This is a temporary implementation that calls zuban directly.
492
+ A full LSP integration would use JSON-RPC protocol.
493
+ """
494
+ try:
495
+ result = self._execute_zuban_check(file_path)
496
+
497
+ if result.returncode == 0:
498
+ return [] # No errors
499
+
500
+ return self._parse_zuban_output(result.stderr)
501
+
502
+ except (
503
+ subprocess.TimeoutExpired,
504
+ subprocess.CalledProcessError,
505
+ FileNotFoundError,
506
+ ):
507
+ return []
508
+
509
+ def _execute_zuban_check(self, file_path: str) -> subprocess.CompletedProcess[str]:
510
+ """Execute zuban check command for a file."""
511
+ import subprocess
512
+
513
+ return subprocess.run(
514
+ ["zuban", "check", file_path],
515
+ capture_output=True,
516
+ text=True,
517
+ timeout=30,
518
+ )
519
+
520
+ def _parse_zuban_output(self, stderr_output: str) -> list[dict[str, t.Any]]:
521
+ """Parse zuban stderr output into diagnostic format."""
522
+ diagnostics = []
523
+
524
+ for line in stderr_output.splitlines():
525
+ if self._is_error_line(line):
526
+ diagnostic = self._parse_error_line(line)
527
+ if diagnostic:
528
+ diagnostics.append(diagnostic)
529
+
530
+ return diagnostics
531
+
532
+ def _is_error_line(self, line: str) -> bool:
533
+ """Check if a line contains an error message."""
534
+ return ":" in line and "error:" in line.lower()
535
+
536
+ def _parse_error_line(self, line: str) -> dict[str, t.Any] | None:
537
+ """Parse a single error line into diagnostic format."""
538
+ # Format: file:line:column: error: message
539
+ parts = line.split(":", 4)
540
+ if len(parts) < 4:
541
+ return None
542
+
543
+ try:
544
+ line_num = int(parts[1])
545
+ col_num = int(parts[2]) if parts[2].strip() else 1
546
+ message = parts[4].strip() if len(parts) > 4 else parts[3].strip()
547
+
548
+ return {
549
+ "line": line_num,
550
+ "column": col_num,
551
+ "severity": "error",
552
+ "message": message,
553
+ "code": "type-error",
554
+ }
555
+ except (ValueError, IndexError):
556
+ return None
557
+
558
+ def format_diagnostics(self, diagnostics: dict[str, list[dict[str, t.Any]]]) -> str:
559
+ """Format diagnostics for display."""
560
+ if not diagnostics or all(not diags for diags in diagnostics.values()):
561
+ return "✅ No type errors found"
562
+
563
+ lines = []
564
+ total_errors = sum(len(diags) for diags in diagnostics.values())
565
+ lines.append(f"❌ Found {total_errors} type error(s):")
566
+
567
+ for file_path, file_diagnostics in diagnostics.items():
568
+ if file_diagnostics:
569
+ lines.append(f"\n📄 {file_path}:")
570
+ for diag in file_diagnostics:
571
+ severity_icon = "🔴" if diag["severity"] == "error" else "🟡"
572
+ lines.append(
573
+ f" {severity_icon} Line {diag['line']}:{diag['column']} - {diag['message']}"
574
+ )
575
+
576
+ return "\n".join(lines)
577
+
578
+ def get_project_files(self, project_path: Path) -> list[str]:
579
+ """Get Python files in the project that should be type-checked."""
580
+ python_files = []
581
+
582
+ # Focus on crackerjack source files (matching pre-commit config)
583
+ crackerjack_dir = project_path / "crackerjack"
584
+ if crackerjack_dir.exists():
585
+ for py_file in crackerjack_dir.rglob("*.py"):
586
+ # Exclude patterns from pre-commit config
587
+ rel_path = py_file.relative_to(project_path)
588
+ rel_str = str(rel_path)
589
+
590
+ # Skip excluded directories
591
+ if "/mcp/" in rel_str or "/plugins/" in rel_str:
592
+ continue
593
+ if "code_cleaner.py" in rel_str:
594
+ continue
595
+
596
+ python_files.append(str(py_file))
597
+
598
+ return python_files
599
+
600
+ def check_project_with_feedback(
601
+ self, project_path: Path, show_progress: bool = True
602
+ ) -> tuple[dict[str, list[dict[str, t.Any]]], str]:
603
+ """
604
+ Check an entire project with real-time feedback.
605
+
606
+ Returns:
607
+ Tuple of (diagnostics dict[str, t.Any], summary message)
608
+ """
609
+ python_files = self.get_project_files(project_path)
610
+ if not python_files:
611
+ return {}, "📁 No Python files found to check"
612
+
613
+ feedback = RealTimeTypingFeedback(self.console)
614
+
615
+ self.console.print(
616
+ f"🔍 Starting type check of {len(python_files)} files...", style="bold blue"
617
+ )
618
+
619
+ diagnostics = self.check_files(
620
+ python_files,
621
+ progress_callback=feedback if show_progress else None,
622
+ show_progress=show_progress,
623
+ )
624
+
625
+ summary = feedback.get_summary()
626
+ self.console.print(f"\n{summary}", style="bold")
627
+
628
+ return diagnostics, summary