crackerjack 0.33.0__py3-none-any.whl → 0.33.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of crackerjack might be problematic. Click here for more details.
- crackerjack/__main__.py +1350 -34
- crackerjack/adapters/__init__.py +17 -0
- crackerjack/adapters/lsp_client.py +358 -0
- crackerjack/adapters/rust_tool_adapter.py +194 -0
- crackerjack/adapters/rust_tool_manager.py +193 -0
- crackerjack/adapters/skylos_adapter.py +231 -0
- crackerjack/adapters/zuban_adapter.py +560 -0
- crackerjack/agents/base.py +7 -3
- crackerjack/agents/coordinator.py +271 -33
- crackerjack/agents/documentation_agent.py +9 -15
- crackerjack/agents/dry_agent.py +3 -15
- crackerjack/agents/formatting_agent.py +1 -1
- crackerjack/agents/import_optimization_agent.py +36 -180
- crackerjack/agents/performance_agent.py +17 -98
- crackerjack/agents/performance_helpers.py +7 -31
- crackerjack/agents/proactive_agent.py +1 -3
- crackerjack/agents/refactoring_agent.py +16 -85
- crackerjack/agents/refactoring_helpers.py +7 -42
- crackerjack/agents/security_agent.py +9 -48
- crackerjack/agents/test_creation_agent.py +356 -513
- crackerjack/agents/test_specialist_agent.py +0 -4
- crackerjack/api.py +6 -25
- crackerjack/cli/cache_handlers.py +204 -0
- crackerjack/cli/cache_handlers_enhanced.py +683 -0
- crackerjack/cli/facade.py +100 -0
- crackerjack/cli/handlers.py +224 -9
- crackerjack/cli/interactive.py +6 -4
- crackerjack/cli/options.py +642 -55
- crackerjack/cli/utils.py +2 -1
- crackerjack/code_cleaner.py +58 -117
- crackerjack/config/global_lock_config.py +8 -48
- crackerjack/config/hooks.py +53 -62
- crackerjack/core/async_workflow_orchestrator.py +24 -34
- crackerjack/core/autofix_coordinator.py +3 -17
- crackerjack/core/enhanced_container.py +4 -13
- crackerjack/core/file_lifecycle.py +12 -89
- crackerjack/core/performance.py +2 -2
- crackerjack/core/performance_monitor.py +15 -55
- crackerjack/core/phase_coordinator.py +104 -204
- crackerjack/core/resource_manager.py +14 -90
- crackerjack/core/service_watchdog.py +62 -95
- crackerjack/core/session_coordinator.py +149 -0
- crackerjack/core/timeout_manager.py +14 -72
- crackerjack/core/websocket_lifecycle.py +13 -78
- crackerjack/core/workflow_orchestrator.py +171 -174
- crackerjack/docs/INDEX.md +11 -0
- crackerjack/docs/generated/api/API_REFERENCE.md +10895 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +109 -0
- crackerjack/docs/generated/api/CROSS_REFERENCES.md +1755 -0
- crackerjack/docs/generated/api/PROTOCOLS.md +3 -0
- crackerjack/docs/generated/api/SERVICES.md +1252 -0
- crackerjack/documentation/__init__.py +31 -0
- crackerjack/documentation/ai_templates.py +756 -0
- crackerjack/documentation/dual_output_generator.py +765 -0
- crackerjack/documentation/mkdocs_integration.py +518 -0
- crackerjack/documentation/reference_generator.py +977 -0
- crackerjack/dynamic_config.py +55 -50
- crackerjack/executors/async_hook_executor.py +10 -15
- crackerjack/executors/cached_hook_executor.py +117 -43
- crackerjack/executors/hook_executor.py +8 -34
- crackerjack/executors/hook_lock_manager.py +26 -183
- crackerjack/executors/individual_hook_executor.py +13 -11
- crackerjack/executors/lsp_aware_hook_executor.py +270 -0
- crackerjack/executors/tool_proxy.py +417 -0
- crackerjack/hooks/lsp_hook.py +79 -0
- crackerjack/intelligence/adaptive_learning.py +25 -10
- crackerjack/intelligence/agent_orchestrator.py +2 -5
- crackerjack/intelligence/agent_registry.py +34 -24
- crackerjack/intelligence/agent_selector.py +5 -7
- crackerjack/interactive.py +17 -6
- crackerjack/managers/async_hook_manager.py +0 -1
- crackerjack/managers/hook_manager.py +79 -1
- crackerjack/managers/publish_manager.py +44 -8
- crackerjack/managers/test_command_builder.py +1 -15
- crackerjack/managers/test_executor.py +1 -3
- crackerjack/managers/test_manager.py +98 -7
- crackerjack/managers/test_manager_backup.py +10 -9
- crackerjack/mcp/cache.py +2 -2
- crackerjack/mcp/client_runner.py +1 -1
- crackerjack/mcp/context.py +191 -68
- crackerjack/mcp/dashboard.py +7 -5
- crackerjack/mcp/enhanced_progress_monitor.py +31 -28
- crackerjack/mcp/file_monitor.py +30 -23
- crackerjack/mcp/progress_components.py +31 -21
- crackerjack/mcp/progress_monitor.py +50 -53
- crackerjack/mcp/rate_limiter.py +6 -6
- crackerjack/mcp/server_core.py +17 -16
- crackerjack/mcp/service_watchdog.py +2 -1
- crackerjack/mcp/state.py +4 -7
- crackerjack/mcp/task_manager.py +11 -9
- crackerjack/mcp/tools/core_tools.py +173 -32
- crackerjack/mcp/tools/error_analyzer.py +3 -2
- crackerjack/mcp/tools/execution_tools.py +8 -10
- crackerjack/mcp/tools/execution_tools_backup.py +42 -30
- crackerjack/mcp/tools/intelligence_tool_registry.py +7 -5
- crackerjack/mcp/tools/intelligence_tools.py +5 -2
- crackerjack/mcp/tools/monitoring_tools.py +33 -70
- crackerjack/mcp/tools/proactive_tools.py +24 -11
- crackerjack/mcp/tools/progress_tools.py +5 -8
- crackerjack/mcp/tools/utility_tools.py +20 -14
- crackerjack/mcp/tools/workflow_executor.py +62 -40
- crackerjack/mcp/websocket/app.py +8 -0
- crackerjack/mcp/websocket/endpoints.py +352 -357
- crackerjack/mcp/websocket/jobs.py +40 -57
- crackerjack/mcp/websocket/monitoring_endpoints.py +2935 -0
- crackerjack/mcp/websocket/server.py +7 -25
- crackerjack/mcp/websocket/websocket_handler.py +6 -17
- crackerjack/mixins/__init__.py +0 -2
- crackerjack/mixins/error_handling.py +1 -70
- crackerjack/models/config.py +12 -1
- crackerjack/models/config_adapter.py +49 -1
- crackerjack/models/protocols.py +122 -122
- crackerjack/models/resource_protocols.py +55 -210
- crackerjack/monitoring/ai_agent_watchdog.py +13 -13
- crackerjack/monitoring/metrics_collector.py +426 -0
- crackerjack/monitoring/regression_prevention.py +8 -8
- crackerjack/monitoring/websocket_server.py +643 -0
- crackerjack/orchestration/advanced_orchestrator.py +11 -6
- crackerjack/orchestration/coverage_improvement.py +3 -3
- crackerjack/orchestration/execution_strategies.py +26 -6
- crackerjack/orchestration/test_progress_streamer.py +8 -5
- crackerjack/plugins/base.py +2 -2
- crackerjack/plugins/hooks.py +7 -0
- crackerjack/plugins/managers.py +11 -8
- crackerjack/security/__init__.py +0 -1
- crackerjack/security/audit.py +6 -35
- crackerjack/services/anomaly_detector.py +392 -0
- crackerjack/services/api_extractor.py +615 -0
- crackerjack/services/backup_service.py +2 -2
- crackerjack/services/bounded_status_operations.py +15 -152
- crackerjack/services/cache.py +127 -1
- crackerjack/services/changelog_automation.py +395 -0
- crackerjack/services/config.py +15 -9
- crackerjack/services/config_merge.py +19 -80
- crackerjack/services/config_template.py +506 -0
- crackerjack/services/contextual_ai_assistant.py +48 -22
- crackerjack/services/coverage_badge_service.py +171 -0
- crackerjack/services/coverage_ratchet.py +27 -25
- crackerjack/services/debug.py +3 -3
- crackerjack/services/dependency_analyzer.py +460 -0
- crackerjack/services/dependency_monitor.py +14 -11
- crackerjack/services/documentation_generator.py +491 -0
- crackerjack/services/documentation_service.py +675 -0
- crackerjack/services/enhanced_filesystem.py +6 -5
- crackerjack/services/enterprise_optimizer.py +865 -0
- crackerjack/services/error_pattern_analyzer.py +676 -0
- crackerjack/services/file_hasher.py +1 -1
- crackerjack/services/git.py +8 -25
- crackerjack/services/health_metrics.py +10 -8
- crackerjack/services/heatmap_generator.py +735 -0
- crackerjack/services/initialization.py +11 -30
- crackerjack/services/input_validator.py +5 -97
- crackerjack/services/intelligent_commit.py +327 -0
- crackerjack/services/log_manager.py +15 -12
- crackerjack/services/logging.py +4 -3
- crackerjack/services/lsp_client.py +628 -0
- crackerjack/services/memory_optimizer.py +19 -87
- crackerjack/services/metrics.py +42 -33
- crackerjack/services/parallel_executor.py +9 -67
- crackerjack/services/pattern_cache.py +1 -1
- crackerjack/services/pattern_detector.py +6 -6
- crackerjack/services/performance_benchmarks.py +18 -59
- crackerjack/services/performance_cache.py +20 -81
- crackerjack/services/performance_monitor.py +27 -95
- crackerjack/services/predictive_analytics.py +510 -0
- crackerjack/services/quality_baseline.py +234 -0
- crackerjack/services/quality_baseline_enhanced.py +646 -0
- crackerjack/services/quality_intelligence.py +785 -0
- crackerjack/services/regex_patterns.py +618 -524
- crackerjack/services/regex_utils.py +43 -123
- crackerjack/services/secure_path_utils.py +5 -164
- crackerjack/services/secure_status_formatter.py +30 -141
- crackerjack/services/secure_subprocess.py +11 -92
- crackerjack/services/security.py +9 -41
- crackerjack/services/security_logger.py +12 -24
- crackerjack/services/server_manager.py +124 -16
- crackerjack/services/status_authentication.py +16 -159
- crackerjack/services/status_security_manager.py +4 -131
- crackerjack/services/thread_safe_status_collector.py +19 -125
- crackerjack/services/unified_config.py +21 -13
- crackerjack/services/validation_rate_limiter.py +5 -54
- crackerjack/services/version_analyzer.py +459 -0
- crackerjack/services/version_checker.py +1 -1
- crackerjack/services/websocket_resource_limiter.py +10 -144
- crackerjack/services/zuban_lsp_service.py +390 -0
- crackerjack/slash_commands/__init__.py +2 -7
- crackerjack/slash_commands/run.md +2 -2
- crackerjack/tools/validate_input_validator_patterns.py +14 -40
- crackerjack/tools/validate_regex_patterns.py +19 -48
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/METADATA +196 -25
- crackerjack-0.33.2.dist-info/RECORD +229 -0
- crackerjack/CLAUDE.md +0 -207
- crackerjack/RULES.md +0 -380
- crackerjack/py313.py +0 -234
- crackerjack-0.33.0.dist-info/RECORD +0 -187
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.2.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
|
+
)
|