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.
- 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 +605 -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.1.dist-info}/METADATA +196 -25
- crackerjack-0.33.1.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.1.dist-info}/WHEEL +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.33.0.dist-info → crackerjack-0.33.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""Zuban adapter for type checking with LSP integration."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import typing as t
|
|
5
|
+
from contextlib import suppress
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .rust_tool_adapter import BaseRustToolAdapter, Issue, ToolResult
|
|
10
|
+
|
|
11
|
+
if t.TYPE_CHECKING:
|
|
12
|
+
from crackerjack.orchestration.execution_strategies import ExecutionContext
|
|
13
|
+
from crackerjack.services.lsp_client import LSPClient
|
|
14
|
+
|
|
15
|
+
# Import the LSP client wrapper
|
|
16
|
+
from .lsp_client import ZubanLSPClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TypeIssue(Issue):
|
|
21
|
+
"""Zuban type checking issue."""
|
|
22
|
+
|
|
23
|
+
severity: str = "error" # Override default, type errors are typically errors
|
|
24
|
+
column: int = 1
|
|
25
|
+
error_code: str | None = None
|
|
26
|
+
|
|
27
|
+
def to_dict(self) -> dict[str, t.Any]:
|
|
28
|
+
"""Convert issue to dictionary with Zuban-specific fields."""
|
|
29
|
+
base_dict = super().to_dict()
|
|
30
|
+
base_dict.update(
|
|
31
|
+
{
|
|
32
|
+
"column": self.column,
|
|
33
|
+
"error_code": self.error_code,
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
return base_dict
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ZubanAdapter(BaseRustToolAdapter):
|
|
40
|
+
"""Zuban type checking adapter with LSP integration."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
context: "ExecutionContext",
|
|
45
|
+
strict_mode: bool = True,
|
|
46
|
+
mypy_compatibility: bool = True,
|
|
47
|
+
use_lsp: bool = True,
|
|
48
|
+
) -> None:
|
|
49
|
+
"""Initialize Zuban adapter.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
context: Execution context
|
|
53
|
+
strict_mode: Enable strict type checking
|
|
54
|
+
mypy_compatibility: Use MyPy-compatible mode
|
|
55
|
+
use_lsp: Enable LSP integration for faster checking
|
|
56
|
+
"""
|
|
57
|
+
super().__init__(context)
|
|
58
|
+
self.strict_mode = strict_mode
|
|
59
|
+
self.mypy_compatibility = mypy_compatibility
|
|
60
|
+
self.use_lsp = use_lsp
|
|
61
|
+
self._lsp_client: LSPClient | None = None
|
|
62
|
+
self._lsp_wrapper: ZubanLSPClient | None = None
|
|
63
|
+
self._lsp_available = False
|
|
64
|
+
|
|
65
|
+
def get_tool_name(self) -> str:
|
|
66
|
+
"""Get the name of the tool."""
|
|
67
|
+
return "zuban"
|
|
68
|
+
|
|
69
|
+
def check_tool_health(self) -> bool:
|
|
70
|
+
"""Check if Zuban is functional before use.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if Zuban can be used safely, False otherwise
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
import subprocess
|
|
77
|
+
|
|
78
|
+
# Test basic version command - this should not crash
|
|
79
|
+
result = subprocess.run(
|
|
80
|
+
["uv", "run", "zuban", "--version"],
|
|
81
|
+
capture_output=True,
|
|
82
|
+
text=True,
|
|
83
|
+
timeout=10,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if result.returncode != 0:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
# Test if we can parse TOML without crashing
|
|
90
|
+
# Create a minimal test to check for TOML parsing bug
|
|
91
|
+
result = subprocess.run(
|
|
92
|
+
["uv", "run", "zuban", "--help"],
|
|
93
|
+
capture_output=True,
|
|
94
|
+
text=True,
|
|
95
|
+
timeout=10,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return result.returncode == 0
|
|
99
|
+
|
|
100
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, Exception):
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def supports_json_output(self) -> bool:
|
|
104
|
+
"""Zuban does not support JSON output mode."""
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
def _ensure_lsp_client(self) -> None:
|
|
108
|
+
"""Initialize LSP client if not already available."""
|
|
109
|
+
if not self.use_lsp or self._lsp_client is not None:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
# Import here to avoid circular imports
|
|
114
|
+
from rich.console import Console
|
|
115
|
+
|
|
116
|
+
from crackerjack.services.lsp_client import LSPClient
|
|
117
|
+
|
|
118
|
+
console = getattr(self.context, "console", Console())
|
|
119
|
+
self._lsp_client = LSPClient(console=console)
|
|
120
|
+
self._lsp_available = self._lsp_client.is_server_running()
|
|
121
|
+
|
|
122
|
+
except ImportError:
|
|
123
|
+
self._lsp_available = False
|
|
124
|
+
|
|
125
|
+
async def get_lsp_diagnostics(self, target_files: list[Path]) -> list[TypeIssue]:
|
|
126
|
+
"""Get real-time diagnostics from LSP server.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
target_files: List of files to check
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
List of type issues found by LSP server
|
|
133
|
+
"""
|
|
134
|
+
self._ensure_lsp_client()
|
|
135
|
+
|
|
136
|
+
if not self._lsp_available or not self._lsp_client:
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# Convert paths to strings for LSP client
|
|
141
|
+
[str(f.resolve()) for f in target_files]
|
|
142
|
+
|
|
143
|
+
# Get diagnostics from LSP client
|
|
144
|
+
diagnostics, _ = self._lsp_client.check_project_with_feedback(
|
|
145
|
+
project_path=target_files[0].parent if target_files else Path.cwd(),
|
|
146
|
+
show_progress=False,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Convert LSP diagnostics to TypeIssue objects
|
|
150
|
+
issues: list[TypeIssue] = []
|
|
151
|
+
for file_path, file_diagnostics in diagnostics.items():
|
|
152
|
+
for diag in file_diagnostics:
|
|
153
|
+
issues.append(
|
|
154
|
+
TypeIssue(
|
|
155
|
+
file_path=Path(file_path),
|
|
156
|
+
line_number=diag.get("line", 1),
|
|
157
|
+
column=diag.get("column", 1),
|
|
158
|
+
message=diag.get("message", "Type error"),
|
|
159
|
+
severity=diag.get("severity", "error"),
|
|
160
|
+
error_code=diag.get("code"),
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return issues
|
|
165
|
+
|
|
166
|
+
except Exception:
|
|
167
|
+
# LSP failed, return empty list[t.Any] to trigger fallback
|
|
168
|
+
self._lsp_available = False
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
async def get_lsp_diagnostics_optimized(
|
|
172
|
+
self, target_files: list[Path]
|
|
173
|
+
) -> list[TypeIssue]:
|
|
174
|
+
"""Get diagnostics using optimized LSP wrapper.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
target_files: List of files to check
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
List of type issues found by LSP server
|
|
181
|
+
"""
|
|
182
|
+
if not self.use_lsp:
|
|
183
|
+
return []
|
|
184
|
+
|
|
185
|
+
if not self._lsp_wrapper:
|
|
186
|
+
self._lsp_wrapper = ZubanLSPClient()
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
async with self._lsp_wrapper as lsp:
|
|
190
|
+
if not await self._initialize_lsp_workspace(lsp, target_files):
|
|
191
|
+
return []
|
|
192
|
+
|
|
193
|
+
issues = await self._process_files_with_lsp(lsp, target_files)
|
|
194
|
+
return issues
|
|
195
|
+
|
|
196
|
+
except Exception:
|
|
197
|
+
# Fallback to original LSP client method
|
|
198
|
+
return await self.get_lsp_diagnostics(target_files)
|
|
199
|
+
|
|
200
|
+
async def _initialize_lsp_workspace(
|
|
201
|
+
self, lsp: t.Any, target_files: list[Path]
|
|
202
|
+
) -> bool:
|
|
203
|
+
"""Initialize LSP workspace and return success status."""
|
|
204
|
+
root_path = target_files[0].parent if target_files else Path.cwd()
|
|
205
|
+
init_result = await lsp.initialize(root_path)
|
|
206
|
+
return init_result and not init_result.get("error")
|
|
207
|
+
|
|
208
|
+
async def _process_files_with_lsp(
|
|
209
|
+
self, lsp: t.Any, target_files: list[Path]
|
|
210
|
+
) -> list[TypeIssue]:
|
|
211
|
+
"""Process files with LSP and collect diagnostics."""
|
|
212
|
+
issues: list[TypeIssue] = []
|
|
213
|
+
for file_path in target_files:
|
|
214
|
+
if file_path.exists():
|
|
215
|
+
file_issues = await self._get_file_diagnostics_from_lsp(lsp, file_path)
|
|
216
|
+
issues.extend(file_issues)
|
|
217
|
+
return issues
|
|
218
|
+
|
|
219
|
+
async def _get_file_diagnostics_from_lsp(
|
|
220
|
+
self, lsp: t.Any, file_path: Path
|
|
221
|
+
) -> list[TypeIssue]:
|
|
222
|
+
"""Get diagnostics for a single file from LSP."""
|
|
223
|
+
await lsp.text_document_did_open(file_path)
|
|
224
|
+
await asyncio.sleep(0.1) # Wait briefly for diagnostics
|
|
225
|
+
|
|
226
|
+
diagnostics = await lsp.get_diagnostics()
|
|
227
|
+
issues = []
|
|
228
|
+
|
|
229
|
+
for diag in diagnostics:
|
|
230
|
+
issue = self._create_type_issue_from_diagnostic(diag, file_path)
|
|
231
|
+
issues.append(issue)
|
|
232
|
+
|
|
233
|
+
await lsp.text_document_did_close(file_path)
|
|
234
|
+
return issues
|
|
235
|
+
|
|
236
|
+
def _create_type_issue_from_diagnostic(
|
|
237
|
+
self, diag: dict[str, t.Any], file_path: Path
|
|
238
|
+
) -> TypeIssue:
|
|
239
|
+
"""Create a TypeIssue from an LSP diagnostic."""
|
|
240
|
+
return TypeIssue(
|
|
241
|
+
file_path=Path(diag.get("uri", str(file_path)).replace("file://", "")),
|
|
242
|
+
line_number=diag.get("range", {}).get("start", {}).get("line", 0) + 1,
|
|
243
|
+
column=diag.get("range", {}).get("start", {}).get("character", 0) + 1,
|
|
244
|
+
message=diag.get("message", "Type error"),
|
|
245
|
+
severity=self._map_lsp_severity(diag.get("severity", 1)),
|
|
246
|
+
error_code=diag.get("code"),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _map_lsp_severity(self, lsp_severity: int) -> str:
|
|
250
|
+
"""Map LSP severity codes to string values.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
lsp_severity: LSP severity (1=Error, 2=Warning, 3=Information, 4=Hint)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
String severity level
|
|
257
|
+
"""
|
|
258
|
+
return {1: "error", 2: "warning", 3: "info", 4: "info"}.get(
|
|
259
|
+
lsp_severity, "error"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def get_command_args(self, target_files: list[Path]) -> list[str]:
|
|
263
|
+
"""Get command arguments for Zuban execution (fallback mode)."""
|
|
264
|
+
args = ["uv", "run", "zuban"]
|
|
265
|
+
|
|
266
|
+
# Mode selection
|
|
267
|
+
if self.mypy_compatibility:
|
|
268
|
+
args.append("mypy") # MyPy-compatible mode
|
|
269
|
+
else:
|
|
270
|
+
args.append("check") # Native Zuban mode
|
|
271
|
+
|
|
272
|
+
# Strictness
|
|
273
|
+
if self.strict_mode:
|
|
274
|
+
args.append("--strict")
|
|
275
|
+
|
|
276
|
+
# Add error codes for better parsing
|
|
277
|
+
args.append("--show-error-codes")
|
|
278
|
+
|
|
279
|
+
# Target files/directories
|
|
280
|
+
if target_files:
|
|
281
|
+
args.extend(str(f) for f in target_files)
|
|
282
|
+
else:
|
|
283
|
+
args.append(".") # Check entire project
|
|
284
|
+
|
|
285
|
+
return args
|
|
286
|
+
|
|
287
|
+
async def check_with_lsp_or_fallback(self, target_files: list[Path]) -> ToolResult:
|
|
288
|
+
"""Check files using LSP if available, otherwise fallback to CLI mode.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
target_files: Files to type-check
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
ToolResult with issues found
|
|
295
|
+
"""
|
|
296
|
+
# First check if Zuban is functional at all
|
|
297
|
+
if not self.check_tool_health():
|
|
298
|
+
return self._create_error_result(
|
|
299
|
+
"Zuban is not functional due to TOML parsing bug. "
|
|
300
|
+
"Consider using pyright as alternative. "
|
|
301
|
+
"See ZUBAN_TOML_PARSING_BUG_ANALYSIS.md for details."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Try optimized LSP mode first
|
|
305
|
+
if self.use_lsp:
|
|
306
|
+
# Try optimized LSP wrapper first, then fallback to basic LSP client
|
|
307
|
+
lsp_issues = await self.get_lsp_diagnostics_optimized(target_files)
|
|
308
|
+
if not lsp_issues: # If optimized fails, try basic LSP client
|
|
309
|
+
lsp_issues = await self.get_lsp_diagnostics(target_files)
|
|
310
|
+
|
|
311
|
+
if lsp_issues is not None: # LSP succeeded (empty list is valid)
|
|
312
|
+
# Create successful result from LSP
|
|
313
|
+
error_issues = [i for i in lsp_issues if i.severity == "error"]
|
|
314
|
+
success = len(error_issues) == 0
|
|
315
|
+
|
|
316
|
+
result = ToolResult(
|
|
317
|
+
success=success,
|
|
318
|
+
issues=list[Issue](
|
|
319
|
+
lsp_issues
|
|
320
|
+
), # Convert TypeIssue list to Issue list
|
|
321
|
+
raw_output=f"LSP diagnostics: {len(lsp_issues)} issue(s) found",
|
|
322
|
+
tool_version=self.get_tool_version(),
|
|
323
|
+
)
|
|
324
|
+
# Add execution mode as custom attribute for tracking
|
|
325
|
+
result._execution_mode = "lsp"
|
|
326
|
+
return result
|
|
327
|
+
|
|
328
|
+
# LSP unavailable or failed, fallback to CLI mode
|
|
329
|
+
return await self._run_cli_fallback(target_files)
|
|
330
|
+
|
|
331
|
+
async def _run_cli_fallback(self, target_files: list[Path]) -> ToolResult:
|
|
332
|
+
"""Run Zuban in CLI mode as fallback.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
target_files: Files to check
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
ToolResult from CLI execution
|
|
339
|
+
"""
|
|
340
|
+
# Use the existing CLI execution logic
|
|
341
|
+
import subprocess
|
|
342
|
+
|
|
343
|
+
try:
|
|
344
|
+
cmd_args = self.get_command_args(target_files)
|
|
345
|
+
result = subprocess.run(
|
|
346
|
+
cmd_args,
|
|
347
|
+
capture_output=True,
|
|
348
|
+
text=True,
|
|
349
|
+
timeout=60,
|
|
350
|
+
cwd=self.context.root_path
|
|
351
|
+
if hasattr(self.context, "root_path")
|
|
352
|
+
else None,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# Parse the CLI output using existing methods
|
|
356
|
+
tool_result = self.parse_output(result.stdout + result.stderr)
|
|
357
|
+
|
|
358
|
+
# Mark as CLI mode for tracking
|
|
359
|
+
tool_result._execution_mode = "cli"
|
|
360
|
+
|
|
361
|
+
return tool_result
|
|
362
|
+
|
|
363
|
+
except subprocess.TimeoutExpired:
|
|
364
|
+
return self._create_error_result("Zuban execution timed out")
|
|
365
|
+
except Exception as e:
|
|
366
|
+
return self._create_error_result(f"Zuban execution failed: {e}")
|
|
367
|
+
|
|
368
|
+
def parse_output(self, output: str) -> ToolResult:
|
|
369
|
+
"""Parse Zuban output into standardized result."""
|
|
370
|
+
if self._should_use_json_output():
|
|
371
|
+
return self._parse_json_output(output)
|
|
372
|
+
return self._parse_text_output(output)
|
|
373
|
+
|
|
374
|
+
def _parse_json_output(self, output: str) -> ToolResult:
|
|
375
|
+
"""Parse JSON output for AI agents."""
|
|
376
|
+
data = self._parse_json_output_safe(output)
|
|
377
|
+
if data is None:
|
|
378
|
+
return self._create_error_result(
|
|
379
|
+
"Invalid JSON output from Zuban", raw_output=output
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
issues: list[Issue] = []
|
|
384
|
+
for item in data.get("diagnostics", []):
|
|
385
|
+
# Determine severity
|
|
386
|
+
severity = item.get("severity", "error").lower()
|
|
387
|
+
if severity not in ("error", "warning", "info"):
|
|
388
|
+
severity = "error"
|
|
389
|
+
|
|
390
|
+
issues.append(
|
|
391
|
+
TypeIssue(
|
|
392
|
+
file_path=Path(item["file"]),
|
|
393
|
+
line_number=item.get("line", 1),
|
|
394
|
+
column=item.get("column", 1),
|
|
395
|
+
message=item["message"],
|
|
396
|
+
severity=severity,
|
|
397
|
+
error_code=item.get("code"),
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Success if no error-level issues
|
|
402
|
+
error_issues = [i for i in issues if i.severity == "error"]
|
|
403
|
+
success = len(error_issues) == 0
|
|
404
|
+
|
|
405
|
+
return ToolResult(
|
|
406
|
+
success=success,
|
|
407
|
+
issues=issues,
|
|
408
|
+
raw_output=output,
|
|
409
|
+
tool_version=self.get_tool_version(),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
except (KeyError, TypeError, ValueError) as e:
|
|
413
|
+
return self._create_error_result(
|
|
414
|
+
f"Failed to parse Zuban JSON output: {e}", raw_output=output
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def _parse_text_output(self, output: str) -> ToolResult:
|
|
418
|
+
"""Parse text output for human-readable display."""
|
|
419
|
+
issues: list[Issue] = []
|
|
420
|
+
|
|
421
|
+
if not output.strip():
|
|
422
|
+
# No output typically means no type errors found
|
|
423
|
+
return ToolResult(
|
|
424
|
+
success=True,
|
|
425
|
+
issues=[],
|
|
426
|
+
raw_output=output,
|
|
427
|
+
tool_version=self.get_tool_version(),
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
# Parse Zuban/MyPy-style text output
|
|
431
|
+
for line in output.strip().split("\\n"):
|
|
432
|
+
line = line.strip()
|
|
433
|
+
if not line:
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
issue = self._parse_text_line(line)
|
|
437
|
+
if issue:
|
|
438
|
+
issues.append(issue)
|
|
439
|
+
|
|
440
|
+
# Success if no error-level issues
|
|
441
|
+
error_issues = [i for i in issues if i.severity == "error"]
|
|
442
|
+
success = len(error_issues) == 0
|
|
443
|
+
|
|
444
|
+
return ToolResult(
|
|
445
|
+
success=success,
|
|
446
|
+
issues=issues,
|
|
447
|
+
raw_output=output,
|
|
448
|
+
tool_version=self.get_tool_version(),
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def _parse_text_line(self, line: str) -> TypeIssue | None:
|
|
452
|
+
"""Parse a single line of Zuban text output."""
|
|
453
|
+
try:
|
|
454
|
+
basic_info = self._extract_line_components(line)
|
|
455
|
+
if not basic_info:
|
|
456
|
+
return None
|
|
457
|
+
|
|
458
|
+
file_path, line_number, message_part = basic_info
|
|
459
|
+
column = self._extract_column_number(message_part)
|
|
460
|
+
|
|
461
|
+
message_data = self._parse_message_content(message_part)
|
|
462
|
+
severity = self._normalize_severity(message_data["severity"] or "error")
|
|
463
|
+
|
|
464
|
+
return TypeIssue(
|
|
465
|
+
file_path=file_path,
|
|
466
|
+
line_number=line_number,
|
|
467
|
+
column=column,
|
|
468
|
+
message=message_data["message"] or "Unknown error",
|
|
469
|
+
severity=severity,
|
|
470
|
+
error_code=message_data["error_code"],
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
except (IndexError, ValueError):
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
def _extract_line_components(self, line: str) -> tuple[Path, int, str] | None:
|
|
477
|
+
"""Extract file path, line number, and remaining message from line."""
|
|
478
|
+
if ":" not in line:
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
parts = line.split(":", 3)
|
|
482
|
+
if len(parts) < 3:
|
|
483
|
+
return None
|
|
484
|
+
|
|
485
|
+
file_path = Path(parts[0].strip())
|
|
486
|
+
|
|
487
|
+
try:
|
|
488
|
+
line_number = int(parts[1].strip())
|
|
489
|
+
except ValueError:
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
# Handle both 3-part and 4-part formats
|
|
493
|
+
if len(parts) == 4:
|
|
494
|
+
message_part = f"{parts[2]}:{parts[3]}".strip()
|
|
495
|
+
else:
|
|
496
|
+
message_part = parts[2].strip()
|
|
497
|
+
|
|
498
|
+
return file_path, line_number, message_part
|
|
499
|
+
|
|
500
|
+
def _extract_column_number(self, message_part: str) -> int:
|
|
501
|
+
"""Extract column number if present in message part."""
|
|
502
|
+
# Try to extract column from the beginning of message_part
|
|
503
|
+
parts = message_part.split(":", 2)
|
|
504
|
+
if len(parts) >= 2:
|
|
505
|
+
with suppress(ValueError):
|
|
506
|
+
return int(parts[0].strip())
|
|
507
|
+
return 1
|
|
508
|
+
|
|
509
|
+
def _parse_message_content(self, message_part: str) -> dict[str, str | None]:
|
|
510
|
+
"""Parse message content to extract severity, message, and error code."""
|
|
511
|
+
# Skip column number if present
|
|
512
|
+
parts = message_part.split(":", 2)
|
|
513
|
+
if len(parts) >= 2:
|
|
514
|
+
try:
|
|
515
|
+
int(parts[0].strip()) # Check if first part is column number
|
|
516
|
+
working_message = ":".join(parts[1:]).strip()
|
|
517
|
+
except ValueError:
|
|
518
|
+
working_message = message_part
|
|
519
|
+
else:
|
|
520
|
+
working_message = message_part
|
|
521
|
+
|
|
522
|
+
severity, message = self._extract_severity_and_message(working_message)
|
|
523
|
+
error_code = self._extract_error_code(message)
|
|
524
|
+
|
|
525
|
+
# Remove error code from message if found
|
|
526
|
+
if error_code and "[" in message:
|
|
527
|
+
code_start = message.rfind("[")
|
|
528
|
+
message = message[:code_start].strip()
|
|
529
|
+
|
|
530
|
+
return {"severity": severity, "message": message, "error_code": error_code}
|
|
531
|
+
|
|
532
|
+
def _extract_severity_and_message(self, working_message: str) -> tuple[str, str]:
|
|
533
|
+
"""Extract severity indicator and remaining message."""
|
|
534
|
+
severity_indicators = ["error:", "warning:", "note:", "info:"]
|
|
535
|
+
|
|
536
|
+
for indicator in severity_indicators:
|
|
537
|
+
if working_message.lower().startswith(indicator):
|
|
538
|
+
severity = indicator[:-1] # Remove colon
|
|
539
|
+
message = working_message[len(indicator) :].strip()
|
|
540
|
+
return severity, message
|
|
541
|
+
|
|
542
|
+
# Default to error severity
|
|
543
|
+
return "error", working_message
|
|
544
|
+
|
|
545
|
+
def _extract_error_code(self, message: str) -> str | None:
|
|
546
|
+
"""Extract error code from message if present."""
|
|
547
|
+
if "[" in message and "]" in message:
|
|
548
|
+
code_start = message.rfind("[")
|
|
549
|
+
code_end = message.rfind("]")
|
|
550
|
+
if code_start < code_end:
|
|
551
|
+
return message[code_start + 1 : code_end]
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
def _normalize_severity(self, severity: str) -> str:
|
|
555
|
+
"""Normalize severity to standard values."""
|
|
556
|
+
if severity in ("note", "info"):
|
|
557
|
+
return "info"
|
|
558
|
+
elif severity not in ("error", "warning"):
|
|
559
|
+
return "error"
|
|
560
|
+
return severity
|
crackerjack/agents/base.py
CHANGED
|
@@ -56,9 +56,13 @@ class FixResult:
|
|
|
56
56
|
success=self.success and other.success,
|
|
57
57
|
confidence=max(self.confidence, other.confidence),
|
|
58
58
|
fixes_applied=self.fixes_applied + other.fixes_applied,
|
|
59
|
-
remaining_issues=list(
|
|
59
|
+
remaining_issues=list[t.Any](
|
|
60
|
+
set[t.Any](self.remaining_issues + other.remaining_issues)
|
|
61
|
+
),
|
|
60
62
|
recommendations=self.recommendations + other.recommendations,
|
|
61
|
-
files_modified=list(
|
|
63
|
+
files_modified=list[t.Any](
|
|
64
|
+
set[t.Any](self.files_modified + other.files_modified)
|
|
65
|
+
),
|
|
62
66
|
)
|
|
63
67
|
|
|
64
68
|
|
|
@@ -66,7 +70,7 @@ class FixResult:
|
|
|
66
70
|
class AgentContext:
|
|
67
71
|
project_path: Path
|
|
68
72
|
temp_dir: Path | None = None
|
|
69
|
-
config: dict[str, t.Any] = field(default_factory=dict)
|
|
73
|
+
config: dict[str, t.Any] = field(default_factory=dict[str, t.Any])
|
|
70
74
|
session_id: str | None = None
|
|
71
75
|
|
|
72
76
|
subprocess_timeout: int = 300
|