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,17 @@
1
+ """Rust tool adapters for unified integration."""
2
+
3
+ from .rust_tool_adapter import Issue, RustToolAdapter, ToolResult
4
+ from .rust_tool_manager import RustToolHookManager
5
+ from .skylos_adapter import DeadCodeIssue, SkylosAdapter
6
+ from .zuban_adapter import TypeIssue, ZubanAdapter
7
+
8
+ __all__ = [
9
+ "RustToolAdapter",
10
+ "ToolResult",
11
+ "Issue",
12
+ "SkylosAdapter",
13
+ "DeadCodeIssue",
14
+ "ZubanAdapter",
15
+ "TypeIssue",
16
+ "RustToolHookManager",
17
+ ]
@@ -0,0 +1,358 @@
1
+ """LSP client wrapper for Zuban communication."""
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ import socket
7
+ import typing as t
8
+ from pathlib import Path
9
+
10
+ logger = logging.getLogger("crackerjack.lsp_client")
11
+
12
+
13
+ class ZubanLSPClient:
14
+ """Minimal LSP client for zuban communication."""
15
+
16
+ def __init__(self, host: str = "127.0.0.1", port: int = 8677) -> None:
17
+ """Initialize LSP client.
18
+
19
+ Args:
20
+ host: LSP server host
21
+ port: LSP server port
22
+ """
23
+ self.host = host
24
+ self.port = port
25
+ self._socket: socket.socket | None = None
26
+ self._reader: asyncio.StreamReader | None = None
27
+ self._writer: asyncio.StreamWriter | None = None
28
+ self._request_id = 0
29
+ self._initialized = False
30
+
31
+ def _next_request_id(self) -> int:
32
+ """Generate next request ID."""
33
+ self._request_id += 1
34
+ return self._request_id
35
+
36
+ async def connect(self, timeout: float = 5.0) -> bool:
37
+ """Connect to zuban LSP server.
38
+
39
+ Args:
40
+ timeout: Connection timeout in seconds
41
+
42
+ Returns:
43
+ True if connected successfully
44
+ """
45
+ try:
46
+ # Attempt TCP connection
47
+ self._reader, self._writer = await asyncio.wait_for(
48
+ asyncio.open_connection(self.host, self.port), timeout=timeout
49
+ )
50
+
51
+ logger.info(f"Connected to Zuban LSP server at {self.host}:{self.port}")
52
+ return True
53
+
54
+ except (TimeoutError, OSError) as e:
55
+ logger.warning(f"Failed to connect to LSP server: {e}")
56
+ return False
57
+
58
+ async def disconnect(self) -> None:
59
+ """Disconnect from LSP server."""
60
+ if self._writer:
61
+ try:
62
+ # Send shutdown request
63
+ if self._initialized:
64
+ await self._send_request("shutdown")
65
+ await self._send_notification("exit")
66
+
67
+ self._writer.close()
68
+ await self._writer.wait_closed()
69
+
70
+ except Exception as e:
71
+ logger.warning(f"Error during disconnect: {e}")
72
+ finally:
73
+ self._writer = None
74
+ self._reader = None
75
+ self._initialized = False
76
+
77
+ async def initialize(self, root_path: Path) -> dict[str, t.Any] | None:
78
+ """Send initialize request.
79
+
80
+ Args:
81
+ root_path: Workspace root path
82
+
83
+ Returns:
84
+ Initialize response from server
85
+ """
86
+ if self._initialized:
87
+ return {"status": "already_initialized"}
88
+
89
+ params = {
90
+ "processId": None,
91
+ "rootPath": str(root_path),
92
+ "rootUri": f"file://{root_path}",
93
+ "capabilities": {
94
+ "textDocument": {
95
+ "publishDiagnostics": {
96
+ "versionSupport": True,
97
+ "tagSupport": {"valueSet": [1, 2]},
98
+ "relatedInformation": True,
99
+ }
100
+ },
101
+ "workspace": {
102
+ "workspaceFolders": True,
103
+ "configuration": True,
104
+ },
105
+ },
106
+ "workspaceFolders": [
107
+ {"uri": f"file://{root_path}", "name": root_path.name}
108
+ ],
109
+ }
110
+
111
+ response = await self._send_request("initialize", params)
112
+ if response and not response.get("error"):
113
+ # Send initialized notification
114
+ await self._send_notification("initialized")
115
+ self._initialized = True
116
+ logger.info("LSP client initialized successfully")
117
+
118
+ return response
119
+
120
+ async def text_document_did_open(self, file_path: Path) -> None:
121
+ """Notify server of opened document.
122
+
123
+ Args:
124
+ file_path: Path to opened file
125
+ """
126
+ if not file_path.exists():
127
+ logger.warning(f"File does not exist: {file_path}")
128
+ return
129
+
130
+ try:
131
+ content = file_path.read_text(encoding="utf-8")
132
+ except UnicodeDecodeError:
133
+ logger.warning(f"Could not read file as UTF-8: {file_path}")
134
+ return
135
+
136
+ params = {
137
+ "textDocument": {
138
+ "uri": f"file://{file_path}",
139
+ "languageId": "python",
140
+ "version": 1,
141
+ "text": content,
142
+ }
143
+ }
144
+
145
+ await self._send_notification("textDocument/didOpen", params)
146
+
147
+ async def text_document_did_change(
148
+ self, file_path: Path, content: str, version: int = 2
149
+ ) -> None:
150
+ """Notify server of document changes.
151
+
152
+ Args:
153
+ file_path: Path to changed file
154
+ content: New file content
155
+ version: Document version number
156
+ """
157
+ params = {
158
+ "textDocument": {
159
+ "uri": f"file://{file_path}",
160
+ "version": version,
161
+ },
162
+ "contentChanges": [{"text": content}],
163
+ }
164
+
165
+ await self._send_notification("textDocument/didChange", params)
166
+
167
+ async def text_document_did_close(self, file_path: Path) -> None:
168
+ """Notify server of closed document.
169
+
170
+ Args:
171
+ file_path: Path to closed file
172
+ """
173
+ params = {
174
+ "textDocument": {
175
+ "uri": f"file://{file_path}",
176
+ }
177
+ }
178
+
179
+ await self._send_notification("textDocument/didClose", params)
180
+
181
+ async def get_diagnostics(self, timeout: float = 2.0) -> list[dict[str, t.Any]]:
182
+ """Retrieve current diagnostics from server.
183
+
184
+ Args:
185
+ timeout: Timeout for waiting for diagnostics
186
+
187
+ Returns:
188
+ List of diagnostic messages
189
+ """
190
+ # For now, return empty list[t.Any] as diagnostics are typically pushed via notifications
191
+ # In a full implementation, we'd maintain a diagnostics store updated by notifications
192
+ return []
193
+
194
+ async def _send_request(
195
+ self, method: str, params: dict[str, t.Any] | None = None, timeout: float = 10.0
196
+ ) -> dict[str, t.Any] | None:
197
+ """Send LSP request and wait for response.
198
+
199
+ Args:
200
+ method: LSP method name
201
+ params: Request parameters
202
+ timeout: Response timeout
203
+
204
+ Returns:
205
+ Response from server
206
+ """
207
+ if not self._writer or not self._reader:
208
+ return None
209
+
210
+ request_id = self._next_request_id()
211
+ request = {
212
+ "jsonrpc": "2.0",
213
+ "id": request_id,
214
+ "method": method,
215
+ }
216
+
217
+ if params is not None:
218
+ request["params"] = params
219
+
220
+ try:
221
+ # Send request
222
+ await self._send_message(request)
223
+
224
+ # Wait for response
225
+ response = await asyncio.wait_for(
226
+ self._read_response(request_id), timeout=timeout
227
+ )
228
+
229
+ return response
230
+
231
+ except TimeoutError:
232
+ logger.warning(f"LSP request {method} timed out")
233
+ return {"error": "timeout", "id": request_id}
234
+ except Exception as e:
235
+ logger.error(f"LSP request {method} failed: {e}")
236
+ return {"error": str(e), "id": request_id}
237
+
238
+ async def _send_notification(
239
+ self, method: str, params: dict[str, t.Any] | None = None
240
+ ) -> None:
241
+ """Send LSP notification (no response expected).
242
+
243
+ Args:
244
+ method: LSP method name
245
+ params: Notification parameters
246
+ """
247
+ if not self._writer:
248
+ return
249
+
250
+ notification: dict[str, t.Any] = {
251
+ "jsonrpc": "2.0",
252
+ "method": method,
253
+ }
254
+
255
+ if params is not None:
256
+ notification["params"] = params
257
+
258
+ try:
259
+ await self._send_message(notification)
260
+ except Exception as e:
261
+ logger.error(f"LSP notification {method} failed: {e}")
262
+
263
+ async def _send_message(self, message: dict[str, t.Any]) -> None:
264
+ """Send LSP message with proper formatting.
265
+
266
+ Args:
267
+ message: Message to send
268
+ """
269
+ if not self._writer:
270
+ return
271
+
272
+ content_bytes = json.dumps(message).encode("utf-8")
273
+ content_length = len(content_bytes)
274
+
275
+ # LSP protocol: Content-Length header + \r\n\r\n + JSON
276
+ header = f"Content-Length: {content_length}\r\n\r\n"
277
+ full_message = header.encode("ascii") + content_bytes
278
+
279
+ self._writer.write(full_message)
280
+ await self._writer.drain()
281
+
282
+ async def _read_response(self, expected_id: int) -> dict[str, t.Any] | None:
283
+ """Read LSP response for specific request ID.
284
+
285
+ Args:
286
+ expected_id: Expected request ID
287
+
288
+ Returns:
289
+ Response message
290
+ """
291
+ while True:
292
+ message = await self._read_message()
293
+ if not message:
294
+ return None
295
+
296
+ # Check if this is the response we're waiting for
297
+ if message.get("id") == expected_id:
298
+ return message
299
+
300
+ # If it's a different response or notification, log and continue
301
+ if "id" in message:
302
+ logger.debug(
303
+ f"Received response for ID {message['id']}, expected {expected_id}"
304
+ )
305
+ else:
306
+ logger.debug(
307
+ f"Received notification: {message.get('method', 'unknown')}"
308
+ )
309
+
310
+ async def _read_message(self) -> dict[str, t.Any] | None:
311
+ """Read complete LSP message.
312
+
313
+ Returns:
314
+ Parsed message dictionary
315
+ """
316
+ if not self._reader:
317
+ return None
318
+
319
+ try:
320
+ # Read Content-Length header
321
+ header_line = await self._reader.readline()
322
+ header_str = header_line.decode("ascii").strip()
323
+
324
+ if not header_str.startswith("Content-Length:"):
325
+ logger.warning(f"Invalid LSP header: {header_str}")
326
+ return None
327
+
328
+ content_length = int(header_str.split(":", 1)[1].strip())
329
+
330
+ # Read separator line
331
+ separator = await self._reader.readline()
332
+ if separator.strip():
333
+ logger.warning("Expected empty separator line")
334
+
335
+ # Read JSON content
336
+ content_bytes = await self._reader.readexactly(content_length)
337
+ content = content_bytes.decode()
338
+
339
+ json_result = json.loads(content)
340
+ return t.cast(dict[str, t.Any] | None, json_result)
341
+
342
+ except Exception as e:
343
+ logger.error(f"Failed to read LSP message: {e}")
344
+ return None
345
+
346
+ async def __aenter__(self) -> "ZubanLSPClient":
347
+ """Async context manager entry."""
348
+ await self.connect()
349
+ return self
350
+
351
+ async def __aexit__(
352
+ self,
353
+ exc_type: type[BaseException] | None,
354
+ exc_val: BaseException | None,
355
+ exc_tb: t.Any,
356
+ ) -> None:
357
+ """Async context manager exit."""
358
+ await self.disconnect()
@@ -0,0 +1,194 @@
1
+ """Base protocol and classes for Rust tool integration."""
2
+
3
+ import json
4
+ import typing as t
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Protocol
9
+
10
+ if t.TYPE_CHECKING:
11
+ from crackerjack.orchestration.execution_strategies import ExecutionContext
12
+
13
+
14
+ @dataclass
15
+ class Issue:
16
+ """Base class for tool issues."""
17
+
18
+ file_path: Path
19
+ line_number: int
20
+ message: str
21
+ severity: str = "error"
22
+
23
+ def to_dict(self) -> dict[str, t.Any]:
24
+ """Convert issue to dictionary."""
25
+ return {
26
+ "file_path": str(self.file_path),
27
+ "line_number": self.line_number,
28
+ "message": self.message,
29
+ "severity": self.severity,
30
+ }
31
+
32
+
33
+ @dataclass
34
+ class ToolResult:
35
+ """Unified result format for all Rust tools."""
36
+
37
+ success: bool
38
+ issues: list[Issue] = field(default_factory=list)
39
+ error: str | None = None
40
+ raw_output: str = ""
41
+ execution_time: float = 0.0
42
+ tool_version: str | None = None
43
+ _execution_mode: str | None = None
44
+
45
+ @property
46
+ def has_errors(self) -> bool:
47
+ """Check if result contains error-level issues."""
48
+ return any(issue.severity == "error" for issue in self.issues)
49
+
50
+ @property
51
+ def error_count(self) -> int:
52
+ """Count of error-level issues."""
53
+ return len([i for i in self.issues if i.severity == "error"])
54
+
55
+ @property
56
+ def warning_count(self) -> int:
57
+ """Count of warning-level issues."""
58
+ return len([i for i in self.issues if i.severity == "warning"])
59
+
60
+ def to_dict(self) -> dict[str, t.Any]:
61
+ """Convert result to dictionary."""
62
+ return {
63
+ "success": self.success,
64
+ "issues": [issue.to_dict() for issue in self.issues],
65
+ "error": self.error,
66
+ "raw_output": self.raw_output,
67
+ "execution_time": self.execution_time,
68
+ "tool_version": self.tool_version,
69
+ "error_count": self.error_count,
70
+ "warning_count": self.warning_count,
71
+ }
72
+
73
+
74
+ class RustToolAdapter(Protocol):
75
+ """Protocol for Rust-based analysis tools."""
76
+
77
+ def __init__(self, context: "ExecutionContext") -> None:
78
+ """Initialize adapter with execution context."""
79
+ ...
80
+
81
+ def get_command_args(self, target_files: list[Path]) -> list[str]:
82
+ """Get command arguments for tool execution."""
83
+ ...
84
+
85
+ def parse_output(self, output: str) -> ToolResult:
86
+ """Parse tool output into standardized result."""
87
+ ...
88
+
89
+ def supports_json_output(self) -> bool:
90
+ """Check if tool supports JSON output mode."""
91
+ ...
92
+
93
+ def get_tool_version(self) -> str | None:
94
+ """Get tool version if available."""
95
+ ...
96
+
97
+ def validate_tool_available(self) -> bool:
98
+ """Validate that the tool is available and executable."""
99
+ ...
100
+
101
+
102
+ class BaseRustToolAdapter(ABC):
103
+ """Abstract base implementation of RustToolAdapter."""
104
+
105
+ def __init__(self, context: "ExecutionContext") -> None:
106
+ """Initialize adapter with execution context."""
107
+ self.context = context
108
+ self._tool_version: str | None = None
109
+
110
+ @abstractmethod
111
+ def get_command_args(self, target_files: list[Path]) -> list[str]:
112
+ """Get command arguments for tool execution."""
113
+ pass
114
+
115
+ @abstractmethod
116
+ def parse_output(self, output: str) -> ToolResult:
117
+ """Parse tool output into standardized result."""
118
+ pass
119
+
120
+ @abstractmethod
121
+ def supports_json_output(self) -> bool:
122
+ """Check if tool supports JSON output mode."""
123
+ pass
124
+
125
+ @abstractmethod
126
+ def get_tool_name(self) -> str:
127
+ """Get the name of the tool."""
128
+ pass
129
+
130
+ def get_tool_version(self) -> str | None:
131
+ """Get tool version if available."""
132
+ if self._tool_version is None:
133
+ self._tool_version = self._fetch_tool_version()
134
+ return self._tool_version
135
+
136
+ def validate_tool_available(self) -> bool:
137
+ """Validate that the tool is available and executable."""
138
+ import subprocess
139
+
140
+ tool_name = self.get_tool_name()
141
+ try:
142
+ result = subprocess.run(
143
+ ["which", tool_name], capture_output=True, text=True, check=False
144
+ )
145
+ return result.returncode == 0
146
+ except (subprocess.SubprocessError, FileNotFoundError):
147
+ return False
148
+
149
+ def _fetch_tool_version(self) -> str | None:
150
+ """Fetch tool version from command line."""
151
+ import subprocess
152
+
153
+ tool_name = self.get_tool_name()
154
+ try:
155
+ result = subprocess.run(
156
+ [tool_name, "--version"],
157
+ capture_output=True,
158
+ text=True,
159
+ check=True,
160
+ timeout=10,
161
+ )
162
+ return result.stdout.strip().split("\\n")[0]
163
+ except (
164
+ subprocess.SubprocessError,
165
+ FileNotFoundError,
166
+ subprocess.TimeoutExpired,
167
+ ):
168
+ return None
169
+
170
+ def _should_use_json_output(self) -> bool:
171
+ """Determine if JSON output should be used based on context."""
172
+ return self.supports_json_output() and (
173
+ self.context.ai_agent_mode or self.context.ai_debug_mode
174
+ )
175
+
176
+ def _parse_json_output_safe(self, output: str) -> dict[str, t.Any] | None:
177
+ """Safely parse JSON output with error handling."""
178
+ try:
179
+ json_result = json.loads(output)
180
+ return t.cast(dict[str, t.Any] | None, json_result)
181
+ except json.JSONDecodeError:
182
+ # Log the error but don't fail completely
183
+ return None
184
+
185
+ def _create_error_result(
186
+ self, error_message: str, raw_output: str = ""
187
+ ) -> ToolResult:
188
+ """Create a ToolResult for error conditions."""
189
+ return ToolResult(
190
+ success=False,
191
+ error=error_message,
192
+ raw_output=raw_output,
193
+ tool_version=self.get_tool_version(),
194
+ )