crackerjack 0.32.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 (200) 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 +64 -6
  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 +257 -218
  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 +558 -240
  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 +66 -13
  74. crackerjack/managers/test_command_builder.py +5 -17
  75. crackerjack/managers/test_executor.py +1 -3
  76. crackerjack/managers/test_manager.py +109 -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 +161 -32
  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 +174 -33
  92. crackerjack/mcp/tools/error_analyzer.py +3 -2
  93. crackerjack/mcp/tools/execution_tools.py +15 -12
  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 +3 -0
  109. crackerjack/mixins/error_handling.py +145 -0
  110. crackerjack/models/config.py +21 -1
  111. crackerjack/models/config_adapter.py +49 -1
  112. crackerjack/models/protocols.py +176 -107
  113. crackerjack/models/resource_protocols.py +55 -210
  114. crackerjack/models/task.py +3 -0
  115. crackerjack/monitoring/ai_agent_watchdog.py +13 -13
  116. crackerjack/monitoring/metrics_collector.py +426 -0
  117. crackerjack/monitoring/regression_prevention.py +8 -8
  118. crackerjack/monitoring/websocket_server.py +643 -0
  119. crackerjack/orchestration/advanced_orchestrator.py +11 -6
  120. crackerjack/orchestration/coverage_improvement.py +3 -3
  121. crackerjack/orchestration/execution_strategies.py +26 -6
  122. crackerjack/orchestration/test_progress_streamer.py +8 -5
  123. crackerjack/plugins/base.py +2 -2
  124. crackerjack/plugins/hooks.py +7 -0
  125. crackerjack/plugins/managers.py +11 -8
  126. crackerjack/security/__init__.py +0 -1
  127. crackerjack/security/audit.py +90 -105
  128. crackerjack/services/anomaly_detector.py +392 -0
  129. crackerjack/services/api_extractor.py +615 -0
  130. crackerjack/services/backup_service.py +2 -2
  131. crackerjack/services/bounded_status_operations.py +15 -152
  132. crackerjack/services/cache.py +127 -1
  133. crackerjack/services/changelog_automation.py +395 -0
  134. crackerjack/services/config.py +18 -11
  135. crackerjack/services/config_merge.py +30 -85
  136. crackerjack/services/config_template.py +506 -0
  137. crackerjack/services/contextual_ai_assistant.py +48 -22
  138. crackerjack/services/coverage_badge_service.py +171 -0
  139. crackerjack/services/coverage_ratchet.py +41 -17
  140. crackerjack/services/debug.py +3 -3
  141. crackerjack/services/dependency_analyzer.py +460 -0
  142. crackerjack/services/dependency_monitor.py +14 -11
  143. crackerjack/services/documentation_generator.py +491 -0
  144. crackerjack/services/documentation_service.py +675 -0
  145. crackerjack/services/enhanced_filesystem.py +6 -5
  146. crackerjack/services/enterprise_optimizer.py +865 -0
  147. crackerjack/services/error_pattern_analyzer.py +676 -0
  148. crackerjack/services/file_hasher.py +1 -1
  149. crackerjack/services/git.py +41 -45
  150. crackerjack/services/health_metrics.py +10 -8
  151. crackerjack/services/heatmap_generator.py +735 -0
  152. crackerjack/services/initialization.py +30 -33
  153. crackerjack/services/input_validator.py +5 -97
  154. crackerjack/services/intelligent_commit.py +327 -0
  155. crackerjack/services/log_manager.py +15 -12
  156. crackerjack/services/logging.py +4 -3
  157. crackerjack/services/lsp_client.py +628 -0
  158. crackerjack/services/memory_optimizer.py +409 -0
  159. crackerjack/services/metrics.py +42 -33
  160. crackerjack/services/parallel_executor.py +416 -0
  161. crackerjack/services/pattern_cache.py +1 -1
  162. crackerjack/services/pattern_detector.py +6 -6
  163. crackerjack/services/performance_benchmarks.py +250 -576
  164. crackerjack/services/performance_cache.py +382 -0
  165. crackerjack/services/performance_monitor.py +565 -0
  166. crackerjack/services/predictive_analytics.py +510 -0
  167. crackerjack/services/quality_baseline.py +234 -0
  168. crackerjack/services/quality_baseline_enhanced.py +646 -0
  169. crackerjack/services/quality_intelligence.py +785 -0
  170. crackerjack/services/regex_patterns.py +605 -524
  171. crackerjack/services/regex_utils.py +43 -123
  172. crackerjack/services/secure_path_utils.py +5 -164
  173. crackerjack/services/secure_status_formatter.py +30 -141
  174. crackerjack/services/secure_subprocess.py +11 -92
  175. crackerjack/services/security.py +61 -30
  176. crackerjack/services/security_logger.py +18 -22
  177. crackerjack/services/server_manager.py +124 -16
  178. crackerjack/services/status_authentication.py +16 -159
  179. crackerjack/services/status_security_manager.py +4 -131
  180. crackerjack/services/terminal_utils.py +0 -0
  181. crackerjack/services/thread_safe_status_collector.py +19 -125
  182. crackerjack/services/unified_config.py +21 -13
  183. crackerjack/services/validation_rate_limiter.py +5 -54
  184. crackerjack/services/version_analyzer.py +459 -0
  185. crackerjack/services/version_checker.py +1 -1
  186. crackerjack/services/websocket_resource_limiter.py +10 -144
  187. crackerjack/services/zuban_lsp_service.py +390 -0
  188. crackerjack/slash_commands/__init__.py +2 -7
  189. crackerjack/slash_commands/run.md +2 -2
  190. crackerjack/tools/validate_input_validator_patterns.py +14 -40
  191. crackerjack/tools/validate_regex_patterns.py +19 -48
  192. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/METADATA +197 -26
  193. crackerjack-0.33.1.dist-info/RECORD +229 -0
  194. crackerjack/CLAUDE.md +0 -207
  195. crackerjack/RULES.md +0 -380
  196. crackerjack/py313.py +0 -234
  197. crackerjack-0.32.0.dist-info/RECORD +0 -180
  198. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/WHEEL +0 -0
  199. {crackerjack-0.32.0.dist-info → crackerjack-0.33.1.dist-info}/entry_points.txt +0 -0
  200. {crackerjack-0.32.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
@@ -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(set(self.remaining_issues + other.remaining_issues)),
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(set(self.files_modified + other.files_modified)),
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