superqode 0.1.5__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.
Files changed (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,538 @@
1
+ """
2
+ Linter Runner - Executes fast linters across supported languages.
3
+
4
+ Detects languages, respects existing configs, and runs linters in-place without
5
+ modifying the repository. Reports findings as structured data.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import os
14
+ import shutil
15
+ import tempfile
16
+ from dataclasses import dataclass, field
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional, Tuple
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ IGNORE_DIRS = {".git", ".superqode", ".venv", "venv", "node_modules", "__pycache__"}
24
+
25
+
26
+ @dataclass
27
+ class LinterRunResult:
28
+ """Result of a single linter execution."""
29
+
30
+ tool: str
31
+ language: str
32
+ success: bool
33
+ findings: List[Dict[str, Any]] = field(default_factory=list)
34
+ errors: List[str] = field(default_factory=list)
35
+
36
+
37
+ class LinterRunner:
38
+ """Run linters for detected languages in the repository."""
39
+
40
+ def __init__(self, project_root: Path, timeout_seconds: int = 300):
41
+ self.project_root = project_root.resolve()
42
+ self.timeout_seconds = timeout_seconds
43
+
44
+ async def run(self) -> LinterRunResult:
45
+ """Run all applicable linters and return merged results."""
46
+ results: List[LinterRunResult] = []
47
+
48
+ if self._has_extension({".py"}):
49
+ results.append(await self._run_python())
50
+ if self._has_extension({".js", ".jsx", ".ts", ".tsx"}):
51
+ results.append(await self._run_js_ts())
52
+ if self._has_extension({".go"}):
53
+ results.append(await self._run_go())
54
+ if (self.project_root / "Cargo.toml").exists() or self._has_extension({".rs"}):
55
+ results.append(await self._run_rust())
56
+ if self._has_extension({".rb"}):
57
+ results.append(await self._run_ruby())
58
+ if self._has_extension({".swift"}):
59
+ results.append(await self._run_swift())
60
+
61
+ merged = LinterRunResult(
62
+ tool="multi",
63
+ language="multi",
64
+ success=True,
65
+ )
66
+ for result in results:
67
+ merged.success = merged.success and result.success
68
+ merged.findings.extend(result.findings)
69
+ merged.errors.extend(result.errors)
70
+ return merged
71
+
72
+ def _has_extension(self, extensions: set[str]) -> bool:
73
+ """Check if repository contains files with any of the extensions."""
74
+ for root, dirs, files in os.walk(self.project_root):
75
+ dirs[:] = [d for d in dirs if d not in IGNORE_DIRS]
76
+ for filename in files:
77
+ if Path(filename).suffix in extensions:
78
+ return True
79
+ return False
80
+
81
+ async def _run_python(self) -> LinterRunResult:
82
+ tool = "ruff"
83
+ if not shutil.which(tool):
84
+ return self._missing_tool(tool, "python")
85
+
86
+ config_path = self._find_ruff_config()
87
+ with tempfile.TemporaryDirectory() as temp_dir:
88
+ config_arg = None
89
+ if config_path is None:
90
+ temp_config = Path(temp_dir) / "ruff.toml"
91
+ temp_config.write_text("line-length = 100\n")
92
+ config_arg = temp_config
93
+
94
+ args = [tool, "check", ".", "--output-format", "json"]
95
+ if config_path:
96
+ args.extend(["--config", str(config_path)])
97
+ if config_arg:
98
+ args.extend(["--config", str(config_arg)])
99
+
100
+ stdout, stderr, code = await self._run_command(args)
101
+ findings = self._parse_ruff_json(stdout, "python")
102
+ return LinterRunResult(
103
+ tool=tool,
104
+ language="python",
105
+ success=code == 0,
106
+ findings=findings,
107
+ errors=self._to_errors(stderr, code),
108
+ )
109
+
110
+ async def _run_js_ts(self) -> LinterRunResult:
111
+ eslint_config = self._find_eslint_config()
112
+ if eslint_config and shutil.which("eslint"):
113
+ args = ["eslint", ".", "--format", "json"]
114
+ stdout, stderr, code = await self._run_command(args)
115
+ findings = self._parse_eslint_json(stdout)
116
+ return LinterRunResult(
117
+ tool="eslint",
118
+ language="javascript",
119
+ success=code == 0,
120
+ findings=findings,
121
+ errors=self._to_errors(stderr, code),
122
+ )
123
+
124
+ if shutil.which("biome"):
125
+ with tempfile.TemporaryDirectory() as temp_dir:
126
+ config_path = self._find_biome_config()
127
+ if config_path is None:
128
+ temp_config = Path(temp_dir) / "biome.json"
129
+ temp_config.write_text(
130
+ json.dumps(
131
+ {
132
+ "linter": {"enabled": True},
133
+ "formatter": {"enabled": False},
134
+ }
135
+ )
136
+ )
137
+ args = [
138
+ "biome",
139
+ "lint",
140
+ "--reporter",
141
+ "json",
142
+ ".",
143
+ "--config-path",
144
+ str(temp_config),
145
+ ]
146
+ else:
147
+ args = ["biome", "lint", "--reporter", "json", "."]
148
+
149
+ stdout, stderr, code = await self._run_command(args)
150
+ findings = self._parse_biome_json(stdout)
151
+ return LinterRunResult(
152
+ tool="biome",
153
+ language="javascript",
154
+ success=code == 0,
155
+ findings=findings,
156
+ errors=self._to_errors(stderr, code),
157
+ )
158
+
159
+ return self._missing_tool("eslint/biome", "javascript")
160
+
161
+ async def _run_go(self) -> LinterRunResult:
162
+ tool = "golangci-lint"
163
+ if shutil.which(tool):
164
+ args = [tool, "run", "--out-format", "json", "./..."]
165
+ stdout, stderr, code = await self._run_command(args)
166
+ findings = self._parse_golangci_json(stdout)
167
+ return LinterRunResult(
168
+ tool=tool,
169
+ language="go",
170
+ success=code == 0,
171
+ findings=findings,
172
+ errors=self._to_errors(stderr, code),
173
+ )
174
+
175
+ if shutil.which("go"):
176
+ args = ["go", "vet", "./..."]
177
+ stdout, stderr, code = await self._run_command(args)
178
+ findings = self._parse_go_vet(stderr or stdout)
179
+ return LinterRunResult(
180
+ tool="go vet",
181
+ language="go",
182
+ success=code == 0,
183
+ findings=findings,
184
+ errors=self._to_errors(stderr, code),
185
+ )
186
+
187
+ return self._missing_tool("golangci-lint", "go")
188
+
189
+ async def _run_rust(self) -> LinterRunResult:
190
+ if not shutil.which("cargo"):
191
+ return self._missing_tool("cargo clippy", "rust")
192
+
193
+ args = ["cargo", "clippy", "--message-format=json"]
194
+ stdout, stderr, code = await self._run_command(args)
195
+ findings = self._parse_cargo_clippy(stdout)
196
+ return LinterRunResult(
197
+ tool="cargo clippy",
198
+ language="rust",
199
+ success=code == 0,
200
+ findings=findings,
201
+ errors=self._to_errors(stderr, code),
202
+ )
203
+
204
+ async def _run_ruby(self) -> LinterRunResult:
205
+ tool = "rubocop"
206
+ if not shutil.which(tool):
207
+ return self._missing_tool(tool, "ruby")
208
+
209
+ args = [tool, "--format", "json"]
210
+ stdout, stderr, code = await self._run_command(args)
211
+ findings = self._parse_rubocop_json(stdout)
212
+ return LinterRunResult(
213
+ tool=tool,
214
+ language="ruby",
215
+ success=code == 0,
216
+ findings=findings,
217
+ errors=self._to_errors(stderr, code),
218
+ )
219
+
220
+ async def _run_swift(self) -> LinterRunResult:
221
+ tool = "swiftlint"
222
+ if not shutil.which(tool):
223
+ return self._missing_tool(tool, "swift")
224
+
225
+ args = [tool, "lint", "--reporter", "json"]
226
+ stdout, stderr, code = await self._run_command(args)
227
+ findings = self._parse_swiftlint_json(stdout)
228
+ return LinterRunResult(
229
+ tool=tool,
230
+ language="swift",
231
+ success=code == 0,
232
+ findings=findings,
233
+ errors=self._to_errors(stderr, code),
234
+ )
235
+
236
+ def _missing_tool(self, tool: str, language: str) -> LinterRunResult:
237
+ return LinterRunResult(
238
+ tool=tool,
239
+ language=language,
240
+ success=True,
241
+ findings=[
242
+ {
243
+ "id": f"{language}-linter-missing",
244
+ "severity": "info",
245
+ "title": f"{language.title()} linter unavailable",
246
+ "description": f"{tool} is not installed. Install it to enable lint checks.",
247
+ "file_path": None,
248
+ "line_number": None,
249
+ "evidence": f"Missing tool: {tool}",
250
+ "confidence": 0.5,
251
+ "category": "tooling",
252
+ }
253
+ ],
254
+ )
255
+
256
+ def _find_ruff_config(self) -> Optional[Path]:
257
+ for name in ("ruff.toml", ".ruff.toml", "pyproject.toml"):
258
+ candidate = self.project_root / name
259
+ if candidate.exists():
260
+ if name == "pyproject.toml":
261
+ if "[tool.ruff]" in candidate.read_text():
262
+ return candidate
263
+ else:
264
+ return candidate
265
+ return None
266
+
267
+ def _find_eslint_config(self) -> Optional[Path]:
268
+ eslint_files = [
269
+ ".eslintrc",
270
+ ".eslintrc.js",
271
+ ".eslintrc.cjs",
272
+ ".eslintrc.json",
273
+ ".eslintrc.yml",
274
+ ".eslintrc.yaml",
275
+ "eslint.config.js",
276
+ "eslint.config.mjs",
277
+ "eslint.config.cjs",
278
+ ]
279
+ for name in eslint_files:
280
+ candidate = self.project_root / name
281
+ if candidate.exists():
282
+ return candidate
283
+ package_json = self.project_root / "package.json"
284
+ if package_json.exists():
285
+ try:
286
+ data = json.loads(package_json.read_text())
287
+ if "eslintConfig" in data:
288
+ return package_json
289
+ except json.JSONDecodeError:
290
+ return None
291
+ return None
292
+
293
+ def _find_biome_config(self) -> Optional[Path]:
294
+ for name in ("biome.json", "biome.jsonc", ".biomerc.json"):
295
+ candidate = self.project_root / name
296
+ if candidate.exists():
297
+ return candidate
298
+ return None
299
+
300
+ async def _run_command(self, args: List[str]) -> Tuple[str, str, int]:
301
+ """Run a command and return stdout, stderr, exit code."""
302
+ try:
303
+ proc = await asyncio.create_subprocess_exec(
304
+ *args,
305
+ cwd=str(self.project_root),
306
+ stdout=asyncio.subprocess.PIPE,
307
+ stderr=asyncio.subprocess.PIPE,
308
+ )
309
+ except FileNotFoundError:
310
+ return "", f"Command not found: {args[0]}", 127
311
+
312
+ try:
313
+ stdout_bytes, stderr_bytes = await asyncio.wait_for(
314
+ proc.communicate(), timeout=self.timeout_seconds
315
+ )
316
+ except asyncio.TimeoutError:
317
+ proc.kill()
318
+ return "", f"Timed out running {' '.join(args)}", 124
319
+
320
+ stdout = stdout_bytes.decode(errors="replace")
321
+ stderr = stderr_bytes.decode(errors="replace")
322
+ return stdout, stderr, proc.returncode
323
+
324
+ def _to_errors(self, stderr: str, code: int) -> List[str]:
325
+ if code == 0 or not stderr:
326
+ return []
327
+ return [stderr.strip()]
328
+
329
+ def _parse_ruff_json(self, stdout: str, language: str) -> List[Dict[str, Any]]:
330
+ try:
331
+ data = json.loads(stdout) if stdout.strip() else []
332
+ except json.JSONDecodeError:
333
+ return []
334
+
335
+ findings = []
336
+ for item in data:
337
+ location = item.get("location", {})
338
+ findings.append(
339
+ {
340
+ "id": item.get("code", "ruff"),
341
+ "severity": "warning",
342
+ "title": f"Ruff {item.get('code', '')}".strip(),
343
+ "description": item.get("message", ""),
344
+ "file_path": item.get("filename"),
345
+ "line_number": location.get("row"),
346
+ "evidence": item.get("message", ""),
347
+ "confidence": 1.0,
348
+ "category": f"lint:{language}",
349
+ "rule_id": item.get("code"),
350
+ "tool": "ruff",
351
+ }
352
+ )
353
+ return findings
354
+
355
+ def _parse_eslint_json(self, stdout: str) -> List[Dict[str, Any]]:
356
+ try:
357
+ data = json.loads(stdout) if stdout.strip() else []
358
+ except json.JSONDecodeError:
359
+ return []
360
+
361
+ findings = []
362
+ for file_entry in data:
363
+ file_path = file_entry.get("filePath")
364
+ for message in file_entry.get("messages", []):
365
+ findings.append(
366
+ {
367
+ "id": message.get("ruleId") or "eslint",
368
+ "severity": "warning" if message.get("severity", 1) == 1 else "critical",
369
+ "title": f"ESLint {message.get('ruleId', '')}".strip(),
370
+ "description": message.get("message", ""),
371
+ "file_path": file_path,
372
+ "line_number": message.get("line"),
373
+ "evidence": message.get("message", ""),
374
+ "confidence": 1.0,
375
+ "category": "lint:javascript",
376
+ "rule_id": message.get("ruleId"),
377
+ "tool": "eslint",
378
+ }
379
+ )
380
+ return findings
381
+
382
+ def _parse_biome_json(self, stdout: str) -> List[Dict[str, Any]]:
383
+ try:
384
+ data = json.loads(stdout) if stdout.strip() else {}
385
+ except json.JSONDecodeError:
386
+ return []
387
+
388
+ findings = []
389
+ for diagnostic in data.get("diagnostics", []):
390
+ location = diagnostic.get("location", {})
391
+ findings.append(
392
+ {
393
+ "id": diagnostic.get("category", "biome"),
394
+ "severity": "warning"
395
+ if diagnostic.get("severity") == "warning"
396
+ else "critical",
397
+ "title": f"Biome {diagnostic.get('category', '')}".strip(),
398
+ "description": diagnostic.get("message", ""),
399
+ "file_path": location.get("path"),
400
+ "line_number": location.get("span", {}).get("start", {}).get("line"),
401
+ "evidence": diagnostic.get("message", ""),
402
+ "confidence": 1.0,
403
+ "category": "lint:javascript",
404
+ "rule_id": diagnostic.get("category"),
405
+ "tool": "biome",
406
+ }
407
+ )
408
+ return findings
409
+
410
+ def _parse_golangci_json(self, stdout: str) -> List[Dict[str, Any]]:
411
+ try:
412
+ data = json.loads(stdout) if stdout.strip() else {}
413
+ except json.JSONDecodeError:
414
+ return []
415
+
416
+ findings = []
417
+ for issue in data.get("Issues", []):
418
+ pos = issue.get("Pos", {})
419
+ findings.append(
420
+ {
421
+ "id": issue.get("FromLinter", "golangci-lint"),
422
+ "severity": "warning",
423
+ "title": f"GolangCI {issue.get('FromLinter', '')}".strip(),
424
+ "description": issue.get("Text", ""),
425
+ "file_path": pos.get("Filename"),
426
+ "line_number": pos.get("Line"),
427
+ "evidence": issue.get("Text", ""),
428
+ "confidence": 1.0,
429
+ "category": "lint:go",
430
+ "rule_id": issue.get("FromLinter"),
431
+ "tool": "golangci-lint",
432
+ }
433
+ )
434
+ return findings
435
+
436
+ def _parse_go_vet(self, output: str) -> List[Dict[str, Any]]:
437
+ findings = []
438
+ for line in output.splitlines():
439
+ if ":" not in line:
440
+ continue
441
+ findings.append(
442
+ {
443
+ "id": "go-vet",
444
+ "severity": "warning",
445
+ "title": "go vet issue",
446
+ "description": line.strip(),
447
+ "file_path": line.split(":", 1)[0],
448
+ "line_number": None,
449
+ "evidence": line.strip(),
450
+ "confidence": 0.8,
451
+ "category": "lint:go",
452
+ "rule_id": "go-vet",
453
+ "tool": "go vet",
454
+ }
455
+ )
456
+ return findings
457
+
458
+ def _parse_cargo_clippy(self, stdout: str) -> List[Dict[str, Any]]:
459
+ findings = []
460
+ for line in stdout.splitlines():
461
+ try:
462
+ message = json.loads(line)
463
+ except json.JSONDecodeError:
464
+ continue
465
+ if message.get("reason") != "compiler-message":
466
+ continue
467
+ diag = message.get("message", {})
468
+ level = diag.get("level")
469
+ spans = diag.get("spans", [])
470
+ primary = spans[0] if spans else {}
471
+ findings.append(
472
+ {
473
+ "id": diag.get("code", {}).get("code", "clippy"),
474
+ "severity": "critical" if level in {"error", "failure"} else "warning",
475
+ "title": f"Clippy {diag.get('code', {}).get('code', '')}".strip(),
476
+ "description": diag.get("message", ""),
477
+ "file_path": primary.get("file_name"),
478
+ "line_number": primary.get("line_start"),
479
+ "evidence": diag.get("message", ""),
480
+ "confidence": 1.0,
481
+ "category": "lint:rust",
482
+ "rule_id": diag.get("code", {}).get("code"),
483
+ "tool": "cargo clippy",
484
+ }
485
+ )
486
+ return findings
487
+
488
+ def _parse_rubocop_json(self, stdout: str) -> List[Dict[str, Any]]:
489
+ try:
490
+ data = json.loads(stdout) if stdout.strip() else {}
491
+ except json.JSONDecodeError:
492
+ return []
493
+
494
+ findings = []
495
+ for file_entry in data.get("files", []):
496
+ for offense in file_entry.get("offenses", []):
497
+ location = offense.get("location", {})
498
+ findings.append(
499
+ {
500
+ "id": offense.get("cop_name", "rubocop"),
501
+ "severity": "warning",
502
+ "title": f"RuboCop {offense.get('cop_name', '')}".strip(),
503
+ "description": offense.get("message", ""),
504
+ "file_path": file_entry.get("path"),
505
+ "line_number": location.get("start_line"),
506
+ "evidence": offense.get("message", ""),
507
+ "confidence": 1.0,
508
+ "category": "lint:ruby",
509
+ "rule_id": offense.get("cop_name"),
510
+ "tool": "rubocop",
511
+ }
512
+ )
513
+ return findings
514
+
515
+ def _parse_swiftlint_json(self, stdout: str) -> List[Dict[str, Any]]:
516
+ try:
517
+ data = json.loads(stdout) if stdout.strip() else []
518
+ except json.JSONDecodeError:
519
+ return []
520
+
521
+ findings = []
522
+ for item in data:
523
+ findings.append(
524
+ {
525
+ "id": item.get("rule_id", "swiftlint"),
526
+ "severity": "warning" if item.get("severity") == "Warning" else "critical",
527
+ "title": f"SwiftLint {item.get('rule_id', '')}".strip(),
528
+ "description": item.get("reason", ""),
529
+ "file_path": item.get("file"),
530
+ "line_number": item.get("line"),
531
+ "evidence": item.get("reason", ""),
532
+ "confidence": 1.0,
533
+ "category": "lint:swift",
534
+ "rule_id": item.get("rule_id"),
535
+ "tool": "swiftlint",
536
+ }
537
+ )
538
+ return findings