k-cli-for-devs 1.0.0__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.
- k_cli/__init__.py +77 -0
- k_cli/agents/__init__.py +0 -0
- k_cli/agents/adversarial_swarm.py +338 -0
- k_cli/agents/agent_core.py +255 -0
- k_cli/agents/background_daemon.py +141 -0
- k_cli/agents/orchestrator.py +376 -0
- k_cli/agents/persona.py +649 -0
- k_cli/agents/scaffold_engine.py +121 -0
- k_cli/agents/strands_agent.py +832 -0
- k_cli/agents/subagents.py +1496 -0
- k_cli/cli.py +3297 -0
- k_cli/core/__init__.py +0 -0
- k_cli/core/airgap.py +95 -0
- k_cli/core/credentials.py +548 -0
- k_cli/core/intent_sensor.py +177 -0
- k_cli/core/llm_driver.py +1028 -0
- k_cli/core/model_manager.py +1109 -0
- k_cli/core/models_hub.py +913 -0
- k_cli/core/prompting.py +41 -0
- k_cli/core/sdk.py +322 -0
- k_cli/core/session.py +826 -0
- k_cli/core/smart_router.py +230 -0
- k_cli/core/storage_manager.py +176 -0
- k_cli/core/viewport_engine.py +117 -0
- k_cli/demo/demo_runner.py +579 -0
- k_cli/git/__init__.py +0 -0
- k_cli/git/ai_bisect.py +208 -0
- k_cli/git/conflict_resolver.py +1039 -0
- k_cli/git/git_guard.py +417 -0
- k_cli/git/patcher.py +1175 -0
- k_cli/git/repo_map.py +1780 -0
- k_cli/git/smart_git.py +928 -0
- k_cli/git/verifier.py +969 -0
- k_cli/github/__init__.py +0 -0
- k_cli/github/dedup_engine.py +787 -0
- k_cli/github/github_client.py +1702 -0
- k_cli/github/github_engine.py +641 -0
- k_cli/github/local_hub.py +209 -0
- k_cli/github/pr_watcher.py +129 -0
- k_cli/github/trending.py +205 -0
- k_cli/tools/__init__.py +0 -0
- k_cli/tools/audit.py +79 -0
- k_cli/tools/chaos_immunity.py +377 -0
- k_cli/tools/codebase_qa.py +106 -0
- k_cli/tools/command_runner.py +256 -0
- k_cli/tools/diagram_generator.py +547 -0
- k_cli/tools/doc_retriever.py +1332 -0
- k_cli/tools/feature.py +105 -0
- k_cli/tools/ghost_daemon.py +122 -0
- k_cli/tools/incident_triage.py +1365 -0
- k_cli/tools/mcp_client.py +1846 -0
- k_cli/tools/repo_gardener.py +142 -0
- k_cli/tools/rules.py +109 -0
- k_cli/tools/security.py +52 -0
- k_cli/tools/security_healer.py +999 -0
- k_cli/tools/synapse_graph.py +155 -0
- k_cli/tui/__init__.py +0 -0
- k_cli/tui/diff_viewer.py +223 -0
- k_cli/tui/tui.py +1145 -0
- k_cli/tui/tui_animations.py +648 -0
- k_cli/tui/tui_app.py +2788 -0
- k_cli/ui/__init__.py +10 -0
- k_cli/ui/simple_repl.py +315 -0
- k_cli/web/__init__.py +7 -0
- k_cli/web/server.py +624 -0
- k_cli/web/static/app.js +830 -0
- k_cli/web/static/index.html +495 -0
- k_cli/web/static/monitor.html +189 -0
- k_cli/web/static/style.css +838 -0
- k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
- k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
- k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
- k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
- k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
- k_cli_for_devs-1.0.0.dist-info/top_level.txt +1 -0
k_cli/git/verifier.py
ADDED
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
"""
|
|
2
|
+
verifier.py - Ground-Truth Execution Guard for K-CLI (Project Bankai Engine v1.0.0)
|
|
3
|
+
|
|
4
|
+
Intercepts generated code blocks (Python, C++, Bash), performs immediate AST and
|
|
5
|
+
syntax checks, auto-detects project test frameworks (pytest, cargo test, npm test,
|
|
6
|
+
go test, make test), executes isolated compilation/test suites via subprocess,
|
|
7
|
+
triggers instant automatic rollbacks on test/AST failures, and extracts
|
|
8
|
+
precise line numbers and stack traces for auto-debug loops.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import ast
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import signal
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from enum import Enum
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import resource
|
|
26
|
+
except ImportError: # pragma: no cover - non-POSIX hosts
|
|
27
|
+
resource = None # type: ignore
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _sanitize_path(path_input: Union[str, Path], base_dir: Optional[Union[str, Path]] = None) -> Path:
|
|
31
|
+
"""Sanitizes user-provided path inputs to prevent directory traversal vulnerabilities."""
|
|
32
|
+
resolved_path = Path(path_input).resolve()
|
|
33
|
+
if base_dir is not None:
|
|
34
|
+
base = Path(base_dir).resolve()
|
|
35
|
+
if not (resolved_path == base or resolved_path.is_relative_to(base)):
|
|
36
|
+
raise ValueError(f"Path '{path_input}' escapes base directory '{base}'")
|
|
37
|
+
return resolved_path
|
|
38
|
+
|
|
39
|
+
cwd = Path.cwd().resolve()
|
|
40
|
+
tmp_dir = Path(tempfile.gettempdir()).resolve()
|
|
41
|
+
if not (resolved_path == cwd or resolved_path.is_relative_to(cwd) or resolved_path == tmp_dir or resolved_path.is_relative_to(tmp_dir)):
|
|
42
|
+
resolved_path = (cwd / resolved_path.name).resolve()
|
|
43
|
+
|
|
44
|
+
return resolved_path
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestFramework(str, Enum):
|
|
48
|
+
"""Supported auto-detected project test frameworks."""
|
|
49
|
+
PYTEST = "pytest"
|
|
50
|
+
CARGO = "cargo"
|
|
51
|
+
NPM = "npm"
|
|
52
|
+
GO = "go"
|
|
53
|
+
MAKE = "make"
|
|
54
|
+
CUSTOM = "custom"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class VerificationResult:
|
|
59
|
+
"""Structured result returned by the Verifier guard."""
|
|
60
|
+
success: bool
|
|
61
|
+
error_trace: str
|
|
62
|
+
code: str
|
|
63
|
+
line_number: Optional[int] = None
|
|
64
|
+
language: str = "python"
|
|
65
|
+
stdout: str = ""
|
|
66
|
+
stderr: str = ""
|
|
67
|
+
verification_type: str = "syntax" # "syntax", "compilation", "pytest", "cargo", "npm", "go", "make", "project_test", "execution"
|
|
68
|
+
rolled_back: bool = False
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict:
|
|
71
|
+
return {
|
|
72
|
+
"success": self.success,
|
|
73
|
+
"error_trace": self.error_trace,
|
|
74
|
+
"code": self.code,
|
|
75
|
+
"line_number": self.line_number,
|
|
76
|
+
"language": self.language,
|
|
77
|
+
"stdout": self.stdout,
|
|
78
|
+
"stderr": self.stderr,
|
|
79
|
+
"verification_type": self.verification_type,
|
|
80
|
+
"rolled_back": self.rolled_back,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CodeExtractor:
|
|
85
|
+
"""Extracts isolated code blocks and language metadata from markdown outputs."""
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def extract_code_blocks(text: str, default_lang: str = "python") -> List[Tuple[str, str]]:
|
|
89
|
+
"""
|
|
90
|
+
Finds all markdown code blocks in text.
|
|
91
|
+
Returns a list of tuples: (language, code_content)
|
|
92
|
+
If no code blocks are found and text is non-empty, returns raw text tagged with default_lang.
|
|
93
|
+
"""
|
|
94
|
+
pattern = r"```([a-zA-Z0-9_+\-#]*)\n(.*?)```"
|
|
95
|
+
matches = re.findall(pattern, text, re.DOTALL)
|
|
96
|
+
|
|
97
|
+
extracted = []
|
|
98
|
+
for lang, code in matches:
|
|
99
|
+
lang_clean = lang.strip().lower() or default_lang
|
|
100
|
+
if lang_clean in ("py", "python3"):
|
|
101
|
+
lang_clean = "python"
|
|
102
|
+
elif lang_clean in ("cpp", "c++", "cc", "cxx"):
|
|
103
|
+
lang_clean = "cpp"
|
|
104
|
+
elif lang_clean in ("bash", "sh", "zsh", "shell"):
|
|
105
|
+
lang_clean = "bash"
|
|
106
|
+
elif lang_clean in ("rs", "rust"):
|
|
107
|
+
lang_clean = "rust"
|
|
108
|
+
elif lang_clean in ("js", "javascript", "ts", "typescript"):
|
|
109
|
+
lang_clean = "javascript"
|
|
110
|
+
elif lang_clean in ("golang", "go"):
|
|
111
|
+
lang_clean = "go"
|
|
112
|
+
extracted.append((lang_clean, code.strip()))
|
|
113
|
+
|
|
114
|
+
if not extracted and text.strip():
|
|
115
|
+
return [(default_lang, text.strip())]
|
|
116
|
+
|
|
117
|
+
return extracted
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def extract_primary_code(text: str, default_lang: str = "python") -> Tuple[str, str]:
|
|
121
|
+
"""Extracts the primary code block matching default_lang, or the first code block."""
|
|
122
|
+
blocks = CodeExtractor.extract_code_blocks(text, default_lang=default_lang)
|
|
123
|
+
if not blocks:
|
|
124
|
+
return default_lang, text.strip()
|
|
125
|
+
for lang, code in blocks:
|
|
126
|
+
if lang == default_lang:
|
|
127
|
+
return lang, code
|
|
128
|
+
return blocks[0]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Verifier:
|
|
132
|
+
"""Ground-Truth Execution Guard providing static AST parsing, isolated execution, and framework tests."""
|
|
133
|
+
|
|
134
|
+
def __init__(self, python_executable: Optional[str] = None):
|
|
135
|
+
self.python_executable = python_executable or sys.executable
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _safe_environment() -> Dict[str, str]:
|
|
139
|
+
"""Keep tool discovery while preventing common provider secrets from leaking."""
|
|
140
|
+
env = dict(os.environ)
|
|
141
|
+
for key in list(env):
|
|
142
|
+
upper = key.upper()
|
|
143
|
+
if any(token in upper for token in ("API_KEY", "TOKEN", "SECRET", "PASSWORD", "PRIVATE_KEY")):
|
|
144
|
+
env.pop(key, None)
|
|
145
|
+
return env
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _limit_child_resources() -> None:
|
|
149
|
+
"""Apply conservative POSIX limits to verifier children when supported."""
|
|
150
|
+
if resource is None:
|
|
151
|
+
return
|
|
152
|
+
resource.setrlimit(resource.RLIMIT_CPU, (20, 20))
|
|
153
|
+
resource.setrlimit(resource.RLIMIT_FSIZE, (64 * 1024 * 1024, 64 * 1024 * 1024))
|
|
154
|
+
resource.setrlimit(resource.RLIMIT_NOFILE, (256, 256))
|
|
155
|
+
|
|
156
|
+
@classmethod
|
|
157
|
+
def _run_subprocess(
|
|
158
|
+
cls,
|
|
159
|
+
cmd: List[str],
|
|
160
|
+
*,
|
|
161
|
+
cwd: Union[str, Path],
|
|
162
|
+
timeout: float,
|
|
163
|
+
) -> subprocess.CompletedProcess:
|
|
164
|
+
"""Run a verifier child with bounded resources and reliable descendant cleanup."""
|
|
165
|
+
safe_cwd = Path(cwd).resolve()
|
|
166
|
+
if not safe_cwd.exists():
|
|
167
|
+
raise FileNotFoundError(f"Subprocess working directory does not exist: {cwd}")
|
|
168
|
+
kwargs: Dict[str, Any] = {
|
|
169
|
+
"cwd": str(safe_cwd),
|
|
170
|
+
"stdout": subprocess.PIPE,
|
|
171
|
+
"stderr": subprocess.PIPE,
|
|
172
|
+
"text": True,
|
|
173
|
+
"env": cls._safe_environment(),
|
|
174
|
+
}
|
|
175
|
+
if os.name == "posix":
|
|
176
|
+
kwargs["start_new_session"] = True
|
|
177
|
+
kwargs["preexec_fn"] = cls._limit_child_resources
|
|
178
|
+
proc = subprocess.Popen(cmd, **kwargs)
|
|
179
|
+
try:
|
|
180
|
+
stdout, stderr = proc.communicate(timeout=timeout)
|
|
181
|
+
except subprocess.TimeoutExpired:
|
|
182
|
+
if os.name == "posix":
|
|
183
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
184
|
+
else:
|
|
185
|
+
proc.kill()
|
|
186
|
+
stdout, stderr = proc.communicate()
|
|
187
|
+
raise subprocess.TimeoutExpired(cmd, timeout, output=stdout, stderr=stderr)
|
|
188
|
+
return subprocess.CompletedProcess(cmd, proc.returncode, stdout, stderr)
|
|
189
|
+
|
|
190
|
+
def verify_python_ast(self, code: str) -> VerificationResult:
|
|
191
|
+
"""Runs immediate Python ast.parse to catch syntax errors instantly."""
|
|
192
|
+
try:
|
|
193
|
+
ast.parse(code)
|
|
194
|
+
return VerificationResult(
|
|
195
|
+
success=True,
|
|
196
|
+
error_trace="",
|
|
197
|
+
code=code,
|
|
198
|
+
language="python",
|
|
199
|
+
verification_type="syntax",
|
|
200
|
+
)
|
|
201
|
+
except SyntaxError as e:
|
|
202
|
+
error_msg = f"SyntaxError: {e.msg} at line {e.lineno}, column {e.offset}"
|
|
203
|
+
if e.text:
|
|
204
|
+
error_msg += f"\nLine content: {e.text.strip()}"
|
|
205
|
+
return VerificationResult(
|
|
206
|
+
success=False,
|
|
207
|
+
error_trace=error_msg,
|
|
208
|
+
code=code,
|
|
209
|
+
line_number=e.lineno,
|
|
210
|
+
language="python",
|
|
211
|
+
stderr=error_msg,
|
|
212
|
+
verification_type="syntax",
|
|
213
|
+
)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
return VerificationResult(
|
|
216
|
+
success=False,
|
|
217
|
+
error_trace=f"AST Parse Error: {str(e)}",
|
|
218
|
+
code=code,
|
|
219
|
+
language="python",
|
|
220
|
+
verification_type="syntax",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def verify_python_execution(
|
|
224
|
+
self,
|
|
225
|
+
code: str,
|
|
226
|
+
test_code: Optional[str] = None,
|
|
227
|
+
timeout: float = 15.0,
|
|
228
|
+
) -> VerificationResult:
|
|
229
|
+
"""
|
|
230
|
+
Runs Python code or pytest suite in an isolated temporary directory.
|
|
231
|
+
If test_code is supplied, executes pytest against the code.
|
|
232
|
+
Otherwise executes py_compile.
|
|
233
|
+
"""
|
|
234
|
+
# Check AST first
|
|
235
|
+
ast_result = self.verify_python_ast(code)
|
|
236
|
+
if not ast_result.success:
|
|
237
|
+
return ast_result
|
|
238
|
+
|
|
239
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
240
|
+
tmppath = Path(tmpdir)
|
|
241
|
+
source_file = tmppath / "solution.py"
|
|
242
|
+
source_file.write_text(code, encoding="utf-8")
|
|
243
|
+
|
|
244
|
+
if test_code:
|
|
245
|
+
test_file = tmppath / "test_solution.py"
|
|
246
|
+
auto_import = ""
|
|
247
|
+
if "from solution import" not in test_code and "import solution" not in test_code:
|
|
248
|
+
auto_import = "import sys\nfrom pathlib import Path\nsys.path.insert(0, str(Path(__file__).parent))\nfrom solution import *\n\n"
|
|
249
|
+
test_file.write_text(auto_import + test_code, encoding="utf-8")
|
|
250
|
+
if "def test_" in test_code:
|
|
251
|
+
cmd = [self.python_executable, "-m", "pytest", "-v", str(test_file)]
|
|
252
|
+
vtype = "pytest"
|
|
253
|
+
else:
|
|
254
|
+
cmd = [self.python_executable, str(test_file)]
|
|
255
|
+
vtype = "execution"
|
|
256
|
+
else:
|
|
257
|
+
cmd = [self.python_executable, "-m", "py_compile", str(source_file)]
|
|
258
|
+
vtype = "compilation"
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
proc = self._run_subprocess(
|
|
262
|
+
cmd,
|
|
263
|
+
cwd=tmpdir,
|
|
264
|
+
timeout=timeout,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
if proc.returncode == 0:
|
|
268
|
+
return VerificationResult(
|
|
269
|
+
success=True,
|
|
270
|
+
error_trace="",
|
|
271
|
+
code=code,
|
|
272
|
+
language="python",
|
|
273
|
+
stdout=proc.stdout,
|
|
274
|
+
stderr=proc.stderr,
|
|
275
|
+
verification_type=vtype,
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
combined_err = (proc.stdout + "\n" + proc.stderr).strip()
|
|
279
|
+
line_no = self._extract_python_line_number(combined_err)
|
|
280
|
+
return VerificationResult(
|
|
281
|
+
success=False,
|
|
282
|
+
error_trace=combined_err,
|
|
283
|
+
code=code,
|
|
284
|
+
line_number=line_no,
|
|
285
|
+
language="python",
|
|
286
|
+
stdout=proc.stdout,
|
|
287
|
+
stderr=proc.stderr,
|
|
288
|
+
verification_type=vtype,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
except subprocess.TimeoutExpired:
|
|
292
|
+
err_msg = f"Execution timed out after {timeout} seconds."
|
|
293
|
+
return VerificationResult(
|
|
294
|
+
success=False,
|
|
295
|
+
error_trace=err_msg,
|
|
296
|
+
code=code,
|
|
297
|
+
language="python",
|
|
298
|
+
stderr=err_msg,
|
|
299
|
+
verification_type=vtype,
|
|
300
|
+
)
|
|
301
|
+
except Exception as e:
|
|
302
|
+
err_msg = f"Subprocess execution error: {str(e)}"
|
|
303
|
+
return VerificationResult(
|
|
304
|
+
success=False,
|
|
305
|
+
error_trace=err_msg,
|
|
306
|
+
code=code,
|
|
307
|
+
language="python",
|
|
308
|
+
stderr=err_msg,
|
|
309
|
+
verification_type=vtype,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def verify_bash_syntax(self, code: str, timeout: float = 10.0) -> VerificationResult:
|
|
313
|
+
"""Verifies bash script syntax using `bash -n` in subprocess."""
|
|
314
|
+
bash_bin = shutil.which("bash")
|
|
315
|
+
if not bash_bin:
|
|
316
|
+
return VerificationResult(
|
|
317
|
+
success=True,
|
|
318
|
+
error_trace="Warning: 'bash' executable not available on host system.",
|
|
319
|
+
code=code,
|
|
320
|
+
language="bash",
|
|
321
|
+
verification_type="syntax",
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
325
|
+
script_file = Path(tmpdir) / "script.sh"
|
|
326
|
+
script_file.write_text(code, encoding="utf-8")
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
proc = self._run_subprocess(
|
|
330
|
+
[bash_bin, "-n", str(script_file)],
|
|
331
|
+
cwd=tmpdir,
|
|
332
|
+
timeout=timeout,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
if proc.returncode == 0:
|
|
336
|
+
return VerificationResult(
|
|
337
|
+
success=True,
|
|
338
|
+
error_trace="",
|
|
339
|
+
code=code,
|
|
340
|
+
language="bash",
|
|
341
|
+
stdout=proc.stdout,
|
|
342
|
+
stderr=proc.stderr,
|
|
343
|
+
verification_type="syntax",
|
|
344
|
+
)
|
|
345
|
+
else:
|
|
346
|
+
err_msg = proc.stderr.strip()
|
|
347
|
+
line_no = self._extract_bash_line_number(err_msg)
|
|
348
|
+
return VerificationResult(
|
|
349
|
+
success=False,
|
|
350
|
+
error_trace=err_msg,
|
|
351
|
+
code=code,
|
|
352
|
+
line_number=line_no,
|
|
353
|
+
language="bash",
|
|
354
|
+
stdout=proc.stdout,
|
|
355
|
+
stderr=proc.stderr,
|
|
356
|
+
verification_type="syntax",
|
|
357
|
+
)
|
|
358
|
+
except subprocess.TimeoutExpired:
|
|
359
|
+
err_msg = f"Bash verification timed out after {timeout} seconds."
|
|
360
|
+
return VerificationResult(
|
|
361
|
+
success=False,
|
|
362
|
+
error_trace=err_msg,
|
|
363
|
+
code=code,
|
|
364
|
+
language="bash",
|
|
365
|
+
stderr=err_msg,
|
|
366
|
+
verification_type="syntax",
|
|
367
|
+
)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
return VerificationResult(
|
|
370
|
+
success=False,
|
|
371
|
+
error_trace=f"Bash verification exception: {str(e)}",
|
|
372
|
+
code=code,
|
|
373
|
+
language="bash",
|
|
374
|
+
verification_type="syntax",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def verify_cpp_syntax(self, code: str, timeout: float = 30.0) -> VerificationResult:
|
|
378
|
+
"""Verifies C++ code compilation syntax using g++ or clang++."""
|
|
379
|
+
compiler = shutil.which("g++") or shutil.which("clang++")
|
|
380
|
+
if not compiler:
|
|
381
|
+
# Fallback checking for unmatched brackets
|
|
382
|
+
unmatched = self._check_unmatched_brackets(code)
|
|
383
|
+
if unmatched:
|
|
384
|
+
return VerificationResult(
|
|
385
|
+
success=False,
|
|
386
|
+
error_trace=f"Fallback C++ check: {unmatched}",
|
|
387
|
+
code=code,
|
|
388
|
+
language="cpp",
|
|
389
|
+
verification_type="syntax",
|
|
390
|
+
)
|
|
391
|
+
return VerificationResult(
|
|
392
|
+
success=True,
|
|
393
|
+
error_trace="Note: g++/clang++ compiler not present; passed basic structural validation.",
|
|
394
|
+
code=code,
|
|
395
|
+
language="cpp",
|
|
396
|
+
verification_type="syntax",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
400
|
+
cpp_file = Path(tmpdir) / "main.cpp"
|
|
401
|
+
cpp_file.write_text(code, encoding="utf-8")
|
|
402
|
+
|
|
403
|
+
cmd = [compiler, "-std=c++17", "-fsyntax-only", str(cpp_file)]
|
|
404
|
+
try:
|
|
405
|
+
proc = self._run_subprocess(
|
|
406
|
+
cmd,
|
|
407
|
+
cwd=tmpdir,
|
|
408
|
+
timeout=timeout,
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if proc.returncode == 0:
|
|
412
|
+
return VerificationResult(
|
|
413
|
+
success=True,
|
|
414
|
+
error_trace="",
|
|
415
|
+
code=code,
|
|
416
|
+
language="cpp",
|
|
417
|
+
stdout=proc.stdout,
|
|
418
|
+
stderr=proc.stderr,
|
|
419
|
+
verification_type="compilation",
|
|
420
|
+
)
|
|
421
|
+
else:
|
|
422
|
+
err_msg = proc.stderr.strip()
|
|
423
|
+
line_no = self._extract_cpp_line_number(err_msg)
|
|
424
|
+
return VerificationResult(
|
|
425
|
+
success=False,
|
|
426
|
+
error_trace=err_msg,
|
|
427
|
+
code=code,
|
|
428
|
+
line_number=line_no,
|
|
429
|
+
language="cpp",
|
|
430
|
+
stdout=proc.stdout,
|
|
431
|
+
stderr=proc.stderr,
|
|
432
|
+
verification_type="compilation",
|
|
433
|
+
)
|
|
434
|
+
except subprocess.TimeoutExpired:
|
|
435
|
+
err_msg = f"C++ compiler check timed out after {timeout} seconds."
|
|
436
|
+
return VerificationResult(
|
|
437
|
+
success=False,
|
|
438
|
+
error_trace=err_msg,
|
|
439
|
+
code=code,
|
|
440
|
+
language="cpp",
|
|
441
|
+
stderr=err_msg,
|
|
442
|
+
verification_type="compilation",
|
|
443
|
+
)
|
|
444
|
+
except Exception as e:
|
|
445
|
+
return VerificationResult(
|
|
446
|
+
success=False,
|
|
447
|
+
error_trace=f"C++ compiler check error: {str(e)}",
|
|
448
|
+
code=code,
|
|
449
|
+
language="cpp",
|
|
450
|
+
verification_type="compilation",
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
def verify(
|
|
454
|
+
self,
|
|
455
|
+
code: str,
|
|
456
|
+
language: str = "python",
|
|
457
|
+
test_code: Optional[str] = None,
|
|
458
|
+
timeout: float = 30.0,
|
|
459
|
+
) -> VerificationResult:
|
|
460
|
+
"""
|
|
461
|
+
Unified verification entrypoint. Accepts raw code or markdown block,
|
|
462
|
+
extracts clean code, and invokes target language verifier.
|
|
463
|
+
"""
|
|
464
|
+
extracted_lang, clean_code = CodeExtractor.extract_primary_code(code, default_lang=language)
|
|
465
|
+
target_lang = extracted_lang or language
|
|
466
|
+
|
|
467
|
+
if target_lang == "python":
|
|
468
|
+
return self.verify_python_execution(clean_code, test_code=test_code, timeout=timeout)
|
|
469
|
+
elif target_lang == "bash":
|
|
470
|
+
return self.verify_bash_syntax(clean_code, timeout=timeout)
|
|
471
|
+
elif target_lang == "cpp":
|
|
472
|
+
return self.verify_cpp_syntax(clean_code, timeout=timeout)
|
|
473
|
+
else:
|
|
474
|
+
return VerificationResult(
|
|
475
|
+
success=True,
|
|
476
|
+
error_trace="",
|
|
477
|
+
code=clean_code,
|
|
478
|
+
language=target_lang,
|
|
479
|
+
verification_type="syntax",
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
@staticmethod
|
|
483
|
+
def detect_test_framework(project_dir: Union[str, Path] = ".") -> Optional[str]:
|
|
484
|
+
"""
|
|
485
|
+
Auto-detects project test frameworks based on workspace configuration files.
|
|
486
|
+
|
|
487
|
+
Detection priority:
|
|
488
|
+
1. Cargo (Rust): Cargo.toml
|
|
489
|
+
2. NPM (Node.js/TS): package.json
|
|
490
|
+
3. Go: go.mod or *_test.go files
|
|
491
|
+
4. Make: Makefile / makefile / GNUmakefile with test target or generic
|
|
492
|
+
5. Pytest (Python): pytest.ini, pyproject.toml, setup.cfg, conftest.py, tests/ dir, test_*.py, *_test.py
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
Detected framework name ("pytest", "cargo", "npm", "go", "make") or None.
|
|
496
|
+
"""
|
|
497
|
+
p_dir = _sanitize_path(project_dir)
|
|
498
|
+
if not p_dir.exists() or not p_dir.is_dir():
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
# 1. Rust (Cargo)
|
|
502
|
+
if (p_dir / "Cargo.toml").exists():
|
|
503
|
+
return TestFramework.CARGO.value
|
|
504
|
+
|
|
505
|
+
# 2. Node (NPM)
|
|
506
|
+
if (p_dir / "package.json").exists():
|
|
507
|
+
return TestFramework.NPM.value
|
|
508
|
+
|
|
509
|
+
# 3. Go
|
|
510
|
+
if (p_dir / "go.mod").exists() or list(p_dir.glob("*_test.go")):
|
|
511
|
+
return TestFramework.GO.value
|
|
512
|
+
|
|
513
|
+
# 4. Make
|
|
514
|
+
for makefile_name in ("Makefile", "makefile", "GNUmakefile"):
|
|
515
|
+
mf = p_dir / makefile_name
|
|
516
|
+
if mf.exists():
|
|
517
|
+
return TestFramework.MAKE.value
|
|
518
|
+
|
|
519
|
+
# 5. Python / Pytest
|
|
520
|
+
pytest_indicators = [
|
|
521
|
+
p_dir / "pytest.ini",
|
|
522
|
+
p_dir / "conftest.py",
|
|
523
|
+
p_dir / "setup.cfg",
|
|
524
|
+
p_dir / "tox.ini",
|
|
525
|
+
p_dir / ".pytest_cache",
|
|
526
|
+
]
|
|
527
|
+
if any(ind.exists() for ind in pytest_indicators):
|
|
528
|
+
return TestFramework.PYTEST.value
|
|
529
|
+
|
|
530
|
+
pyproject = p_dir / "pyproject.toml"
|
|
531
|
+
if pyproject.exists():
|
|
532
|
+
try:
|
|
533
|
+
content = pyproject.read_text(encoding="utf-8")
|
|
534
|
+
if "pytest" in content or "tool.pytest" in content or "project" in content:
|
|
535
|
+
return TestFramework.PYTEST.value
|
|
536
|
+
except Exception:
|
|
537
|
+
return TestFramework.PYTEST.value
|
|
538
|
+
|
|
539
|
+
tests_dir = p_dir / "tests"
|
|
540
|
+
if tests_dir.exists() and tests_dir.is_dir() and any(tests_dir.glob("*.py")):
|
|
541
|
+
return TestFramework.PYTEST.value
|
|
542
|
+
|
|
543
|
+
if list(p_dir.glob("test_*.py")) or list(p_dir.glob("*_test.py")):
|
|
544
|
+
return TestFramework.PYTEST.value
|
|
545
|
+
|
|
546
|
+
return None
|
|
547
|
+
|
|
548
|
+
def get_test_command(
|
|
549
|
+
self,
|
|
550
|
+
framework: str,
|
|
551
|
+
project_dir: Union[str, Path] = ".",
|
|
552
|
+
extra_args: Optional[List[str]] = None,
|
|
553
|
+
) -> List[str]:
|
|
554
|
+
"""
|
|
555
|
+
Constructs CLI command array for a given test framework.
|
|
556
|
+
"""
|
|
557
|
+
fw = framework.lower().strip()
|
|
558
|
+
cmd: List[str] = []
|
|
559
|
+
|
|
560
|
+
if fw in ("pytest", "python", "py"):
|
|
561
|
+
cmd = [self.python_executable, "-m", "pytest", "-v"]
|
|
562
|
+
elif fw in ("cargo", "cargo test", "rust"):
|
|
563
|
+
cmd = ["cargo", "test"]
|
|
564
|
+
elif fw in ("npm", "npm test", "node", "javascript", "typescript"):
|
|
565
|
+
cmd = ["npm", "test"]
|
|
566
|
+
elif fw in ("go", "go test", "golang"):
|
|
567
|
+
cmd = ["go", "test", "./..."]
|
|
568
|
+
elif fw in ("make", "make test", "makefile"):
|
|
569
|
+
cmd = ["make", "test"]
|
|
570
|
+
else:
|
|
571
|
+
cmd = framework.split()
|
|
572
|
+
|
|
573
|
+
if extra_args:
|
|
574
|
+
cmd.extend(extra_args)
|
|
575
|
+
return cmd
|
|
576
|
+
|
|
577
|
+
def run_project_tests(
|
|
578
|
+
self,
|
|
579
|
+
project_dir: Union[str, Path] = ".",
|
|
580
|
+
framework: Optional[str] = None,
|
|
581
|
+
timeout: float = 60.0,
|
|
582
|
+
extra_args: Optional[List[str]] = None,
|
|
583
|
+
) -> VerificationResult:
|
|
584
|
+
"""
|
|
585
|
+
Auto-detects project test framework (pytest, cargo test, npm test, go test, make test)
|
|
586
|
+
and runs post-patch verification suite in the target project directory.
|
|
587
|
+
"""
|
|
588
|
+
p_dir = _sanitize_path(project_dir)
|
|
589
|
+
detected_fw = framework or self.detect_test_framework(p_dir)
|
|
590
|
+
|
|
591
|
+
if not detected_fw:
|
|
592
|
+
return VerificationResult(
|
|
593
|
+
success=True,
|
|
594
|
+
error_trace="",
|
|
595
|
+
code="",
|
|
596
|
+
language="generic",
|
|
597
|
+
stdout="No test framework detected; verification passed by default.",
|
|
598
|
+
stderr="",
|
|
599
|
+
verification_type="project_test",
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
cmd = self.get_test_command(detected_fw, project_dir=p_dir, extra_args=extra_args)
|
|
603
|
+
exec_name = cmd[0] if cmd else ""
|
|
604
|
+
|
|
605
|
+
# Check if executable exists
|
|
606
|
+
if not shutil.which(exec_name) and exec_name != self.python_executable:
|
|
607
|
+
err_msg = f"Test executable '{exec_name}' not found on system path."
|
|
608
|
+
return VerificationResult(
|
|
609
|
+
success=False,
|
|
610
|
+
error_trace=err_msg,
|
|
611
|
+
code="",
|
|
612
|
+
language=detected_fw,
|
|
613
|
+
stderr=err_msg,
|
|
614
|
+
verification_type=detected_fw,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
try:
|
|
618
|
+
proc = self._run_subprocess(
|
|
619
|
+
cmd,
|
|
620
|
+
cwd=str(p_dir),
|
|
621
|
+
timeout=timeout,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
stdout = proc.stdout or ""
|
|
625
|
+
stderr = proc.stderr or ""
|
|
626
|
+
combined_output = (stdout + "\n" + stderr).strip()
|
|
627
|
+
|
|
628
|
+
if proc.returncode == 0:
|
|
629
|
+
return VerificationResult(
|
|
630
|
+
success=True,
|
|
631
|
+
error_trace="",
|
|
632
|
+
code="",
|
|
633
|
+
language=detected_fw,
|
|
634
|
+
stdout=stdout,
|
|
635
|
+
stderr=stderr,
|
|
636
|
+
verification_type=detected_fw,
|
|
637
|
+
)
|
|
638
|
+
else:
|
|
639
|
+
line_no = self._extract_framework_line_number(combined_output, detected_fw)
|
|
640
|
+
return VerificationResult(
|
|
641
|
+
success=False,
|
|
642
|
+
error_trace=combined_output,
|
|
643
|
+
code="",
|
|
644
|
+
line_number=line_no,
|
|
645
|
+
language=detected_fw,
|
|
646
|
+
stdout=stdout,
|
|
647
|
+
stderr=stderr,
|
|
648
|
+
verification_type=detected_fw,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
except subprocess.TimeoutExpired:
|
|
652
|
+
err_msg = f"Project tests timed out after {timeout} seconds ({' '.join(cmd)})."
|
|
653
|
+
return VerificationResult(
|
|
654
|
+
success=False,
|
|
655
|
+
error_trace=err_msg,
|
|
656
|
+
code="",
|
|
657
|
+
language=detected_fw,
|
|
658
|
+
stderr=err_msg,
|
|
659
|
+
verification_type=detected_fw,
|
|
660
|
+
)
|
|
661
|
+
except Exception as e:
|
|
662
|
+
err_msg = f"Failed to execute project tests ({' '.join(cmd)}): {str(e)}"
|
|
663
|
+
return VerificationResult(
|
|
664
|
+
success=False,
|
|
665
|
+
error_trace=err_msg,
|
|
666
|
+
code="",
|
|
667
|
+
language=detected_fw,
|
|
668
|
+
stderr=err_msg,
|
|
669
|
+
verification_type=detected_fw,
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
def verify_post_patch(
|
|
673
|
+
self,
|
|
674
|
+
project_dir: Union[str, Path] = ".",
|
|
675
|
+
git_guard: Optional[Any] = None,
|
|
676
|
+
checkpoint_id: Optional[str] = None,
|
|
677
|
+
auto_rollback: bool = True,
|
|
678
|
+
framework: Optional[str] = None,
|
|
679
|
+
timeout: float = 60.0,
|
|
680
|
+
) -> VerificationResult:
|
|
681
|
+
"""
|
|
682
|
+
Runs project test verification post-patch and triggers instant automatic
|
|
683
|
+
rollback (git reset to checkpoint) if tests fail or AST syntax breaks.
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
project_dir: Path to the workspace / project directory.
|
|
687
|
+
git_guard: Optional GitGuard instance for managing rollback.
|
|
688
|
+
checkpoint_id: Optional checkpoint ID to restore on failure.
|
|
689
|
+
auto_rollback: Whether to trigger instant rollback if verification fails.
|
|
690
|
+
framework: Optional explicit test framework override.
|
|
691
|
+
timeout: Test execution timeout in seconds.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
VerificationResult with `rolled_back=True` if rollback was performed.
|
|
695
|
+
"""
|
|
696
|
+
p_dir = _sanitize_path(project_dir)
|
|
697
|
+
|
|
698
|
+
# Step 1: Pre-scan Python files in workspace for AST syntax errors
|
|
699
|
+
py_files = list(p_dir.glob("*.py"))
|
|
700
|
+
for py_f in py_files:
|
|
701
|
+
try:
|
|
702
|
+
code_text = py_f.read_text(encoding="utf-8")
|
|
703
|
+
ast_res = self.verify_python_ast(code_text)
|
|
704
|
+
if not ast_res.success:
|
|
705
|
+
if auto_rollback and git_guard:
|
|
706
|
+
git_guard.restore_checkpoint(checkpoint_id)
|
|
707
|
+
ast_res.rolled_back = True
|
|
708
|
+
return ast_res
|
|
709
|
+
except Exception:
|
|
710
|
+
pass
|
|
711
|
+
|
|
712
|
+
# Step 2: Run post-patch test suite
|
|
713
|
+
res = self.run_project_tests(project_dir=p_dir, framework=framework, timeout=timeout)
|
|
714
|
+
|
|
715
|
+
# Step 3: Instant rollback if tests failed
|
|
716
|
+
if not res.success and auto_rollback and git_guard:
|
|
717
|
+
git_guard.restore_checkpoint(checkpoint_id)
|
|
718
|
+
res.rolled_back = True
|
|
719
|
+
|
|
720
|
+
return res
|
|
721
|
+
|
|
722
|
+
@classmethod
|
|
723
|
+
def _extract_framework_line_number(cls, trace: str, framework: str) -> Optional[int]:
|
|
724
|
+
"""Extracts error line numbers based on framework-specific error formats."""
|
|
725
|
+
fw = framework.lower().strip()
|
|
726
|
+
if fw in ("pytest", "python"):
|
|
727
|
+
return cls._extract_python_line_number(trace)
|
|
728
|
+
elif fw in ("cargo", "rust"):
|
|
729
|
+
return cls._extract_rust_line_number(trace)
|
|
730
|
+
elif fw in ("go", "golang"):
|
|
731
|
+
return cls._extract_go_line_number(trace)
|
|
732
|
+
elif fw in ("npm", "node", "javascript", "typescript"):
|
|
733
|
+
return cls._extract_js_line_number(trace)
|
|
734
|
+
elif fw in ("make", "cpp", "c"):
|
|
735
|
+
return cls._extract_cpp_line_number(trace)
|
|
736
|
+
return cls._extract_python_line_number(trace)
|
|
737
|
+
|
|
738
|
+
@staticmethod
|
|
739
|
+
def _extract_python_line_number(trace: str) -> Optional[int]:
|
|
740
|
+
"""Parses line numbers from Python tracebacks, Pytest outputs, or error messages."""
|
|
741
|
+
if not trace:
|
|
742
|
+
return None
|
|
743
|
+
|
|
744
|
+
lines = trace.splitlines()
|
|
745
|
+
|
|
746
|
+
pytest_pat = re.compile(r'([a-zA-Z0-9_\-\./\\]+\.py):(\d+):')
|
|
747
|
+
tb_pat = re.compile(r'File\s+"([^"]+)",\s+line\s+(\d+)')
|
|
748
|
+
syntax_pat = re.compile(r'line\s+(\d+)', re.IGNORECASE)
|
|
749
|
+
coord_pat = re.compile(r'line\s+\d+\s*,?\s*column\s+\d+', re.IGNORECASE)
|
|
750
|
+
|
|
751
|
+
stdlib_indicators = ('/usr/lib/python', '/lib/python', 'site-packages', '<frozen ', '/_pytest/')
|
|
752
|
+
|
|
753
|
+
non_stdlib_matches = []
|
|
754
|
+
all_matches = []
|
|
755
|
+
|
|
756
|
+
# Pass 1: Scan structured traceback patterns
|
|
757
|
+
for line in lines:
|
|
758
|
+
m = pytest_pat.search(line)
|
|
759
|
+
if m:
|
|
760
|
+
file_path = m.group(1)
|
|
761
|
+
line_no = int(m.group(2))
|
|
762
|
+
all_matches.append(line_no)
|
|
763
|
+
is_stdlib = any(ind in line for ind in stdlib_indicators) or any(ind in file_path for ind in stdlib_indicators)
|
|
764
|
+
if not is_stdlib:
|
|
765
|
+
non_stdlib_matches.append(line_no)
|
|
766
|
+
continue
|
|
767
|
+
|
|
768
|
+
m = tb_pat.search(line)
|
|
769
|
+
if m:
|
|
770
|
+
file_path = m.group(1)
|
|
771
|
+
line_no = int(m.group(2))
|
|
772
|
+
all_matches.append(line_no)
|
|
773
|
+
is_stdlib = any(ind in line for ind in stdlib_indicators) or any(ind in file_path for ind in stdlib_indicators)
|
|
774
|
+
if not is_stdlib:
|
|
775
|
+
non_stdlib_matches.append(line_no)
|
|
776
|
+
continue
|
|
777
|
+
|
|
778
|
+
if non_stdlib_matches:
|
|
779
|
+
return non_stdlib_matches[-1]
|
|
780
|
+
if all_matches:
|
|
781
|
+
return all_matches[-1]
|
|
782
|
+
|
|
783
|
+
# Pass 2: Generic syntax line matching ONLY if Pass 1 found zero structured traceback frames
|
|
784
|
+
for line in lines:
|
|
785
|
+
if coord_pat.search(line):
|
|
786
|
+
continue
|
|
787
|
+
m = syntax_pat.search(line)
|
|
788
|
+
if m:
|
|
789
|
+
line_no = int(m.group(1))
|
|
790
|
+
is_stdlib = any(ind in line for ind in stdlib_indicators)
|
|
791
|
+
all_matches.append(line_no)
|
|
792
|
+
if not is_stdlib:
|
|
793
|
+
non_stdlib_matches.append(line_no)
|
|
794
|
+
|
|
795
|
+
if non_stdlib_matches:
|
|
796
|
+
return non_stdlib_matches[-1]
|
|
797
|
+
if all_matches:
|
|
798
|
+
return all_matches[-1]
|
|
799
|
+
return None
|
|
800
|
+
|
|
801
|
+
@staticmethod
|
|
802
|
+
def _extract_bash_line_number(trace: str) -> Optional[int]:
|
|
803
|
+
"""Parses line numbers from bash syntax error messages."""
|
|
804
|
+
match = re.search(r'line\s+(\d+)', trace, re.IGNORECASE)
|
|
805
|
+
if match:
|
|
806
|
+
return int(match.group(1))
|
|
807
|
+
return None
|
|
808
|
+
|
|
809
|
+
@staticmethod
|
|
810
|
+
def _extract_cpp_line_number(trace: str) -> Optional[int]:
|
|
811
|
+
"""Parses line numbers from GCC/Clang compiler messages."""
|
|
812
|
+
match = re.search(r':(\d+):\d+:\s+(?:fatal\s+)?error:', trace, re.IGNORECASE)
|
|
813
|
+
if match:
|
|
814
|
+
return int(match.group(1))
|
|
815
|
+
return None
|
|
816
|
+
|
|
817
|
+
@staticmethod
|
|
818
|
+
def _extract_rust_line_number(trace: str) -> Optional[int]:
|
|
819
|
+
"""Parses error line numbers from cargo / rustc output: '--> src/main.rs:14:5'."""
|
|
820
|
+
m = re.search(r'-->\s+[^:]+:(\d+):\d+', trace)
|
|
821
|
+
if m:
|
|
822
|
+
return int(m.group(1))
|
|
823
|
+
m2 = re.search(r':(\d+):\d+:\s+error', trace, re.IGNORECASE)
|
|
824
|
+
if m2:
|
|
825
|
+
return int(m2.group(1))
|
|
826
|
+
return None
|
|
827
|
+
|
|
828
|
+
@staticmethod
|
|
829
|
+
def _extract_go_line_number(trace: str) -> Optional[int]:
|
|
830
|
+
"""Parses error line numbers from go test output: 'main_test.go:25: assertion failed'."""
|
|
831
|
+
m = re.search(r'([a-zA-Z0-9_\-\.]+\.go):(\d+):', trace)
|
|
832
|
+
if m:
|
|
833
|
+
return int(m.group(2))
|
|
834
|
+
return None
|
|
835
|
+
|
|
836
|
+
@staticmethod
|
|
837
|
+
def _extract_js_line_number(trace: str) -> Optional[int]:
|
|
838
|
+
"""Parses error line numbers from Jest / Mocha / Node output: 'at Object.<anonymous> (test.js:12:7)'."""
|
|
839
|
+
m = re.search(r'\((?:[^\)]+[/\\])?([a-zA-Z0-9_\-\.]+\.[jt]sx?):(\d+):\d+\)', trace)
|
|
840
|
+
if m:
|
|
841
|
+
return int(m.group(2))
|
|
842
|
+
m2 = re.search(r'([a-zA-Z0-9_\-\.]+\.[jt]sx?):(\d+):\d+', trace)
|
|
843
|
+
if m2:
|
|
844
|
+
return int(m2.group(2))
|
|
845
|
+
return None
|
|
846
|
+
|
|
847
|
+
@staticmethod
|
|
848
|
+
def _check_unmatched_brackets(code: str) -> Optional[str]:
|
|
849
|
+
"""Fallback check for matching pairs of braces, brackets, parentheses."""
|
|
850
|
+
stack = []
|
|
851
|
+
pairs = {')': '(', '}': '{', ']': '['}
|
|
852
|
+
for i, char in enumerate(code, 1):
|
|
853
|
+
if char in pairs.values():
|
|
854
|
+
stack.append((char, i))
|
|
855
|
+
elif char in pairs.keys():
|
|
856
|
+
if not stack or stack[-1][0] != pairs[char]:
|
|
857
|
+
return f"Unmatched closing '{char}' at index {i}"
|
|
858
|
+
stack.pop()
|
|
859
|
+
if stack:
|
|
860
|
+
unopened_char, pos = stack[-1]
|
|
861
|
+
return f"Unclosed '{unopened_char}' opened at index {pos}"
|
|
862
|
+
return None
|
|
863
|
+
|
|
864
|
+
|
|
865
|
+
# Helper Top-Level Module Functions
|
|
866
|
+
|
|
867
|
+
def strip_fluff(raw_text: str) -> str:
|
|
868
|
+
"""Strips markdown code blocks and conversational fluff from text."""
|
|
869
|
+
if not raw_text:
|
|
870
|
+
return ""
|
|
871
|
+
if "```" in raw_text:
|
|
872
|
+
_, code = CodeExtractor.extract_primary_code(raw_text)
|
|
873
|
+
return code
|
|
874
|
+
text = raw_text.strip()
|
|
875
|
+
text = re.sub(r'^(?:Here is|Sure|Here\'s|Below is|Certainly|This is)[^\n]*\n+', '', text, flags=re.IGNORECASE)
|
|
876
|
+
text = re.sub(r'\n+(?:Hope this helps|Let me know|Enjoy|Note:)[^\n]*$', '', text, flags=re.IGNORECASE)
|
|
877
|
+
return text.strip()
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def parse_ast(code: str) -> Tuple[bool, Optional[str], Optional[int]]:
|
|
881
|
+
"""Standalone helper to parse Python code AST and return (success, error_msg, line_number)."""
|
|
882
|
+
res = Verifier().verify_python_ast(code)
|
|
883
|
+
if res.success:
|
|
884
|
+
return True, None, None
|
|
885
|
+
return False, res.error_trace, res.line_number
|
|
886
|
+
|
|
887
|
+
|
|
888
|
+
def extract_error_line(stderr: str, language: str = "python") -> Optional[int]:
|
|
889
|
+
"""Standalone helper to extract error line numbers from stderr/traceback for a given language."""
|
|
890
|
+
if not stderr:
|
|
891
|
+
return None
|
|
892
|
+
lang = language.lower()
|
|
893
|
+
if lang in ("bash", "sh", "shell"):
|
|
894
|
+
return Verifier._extract_bash_line_number(stderr)
|
|
895
|
+
elif lang in ("cpp", "c++", "cxx"):
|
|
896
|
+
return Verifier._extract_cpp_line_number(stderr)
|
|
897
|
+
elif lang in ("rust", "rs", "cargo"):
|
|
898
|
+
return Verifier._extract_rust_line_number(stderr)
|
|
899
|
+
elif lang in ("go", "golang"):
|
|
900
|
+
return Verifier._extract_go_line_number(stderr)
|
|
901
|
+
elif lang in ("javascript", "typescript", "js", "ts", "node", "npm"):
|
|
902
|
+
return Verifier._extract_js_line_number(stderr)
|
|
903
|
+
else:
|
|
904
|
+
return Verifier._extract_python_line_number(stderr)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def extract_stack_trace(stderr: str) -> Optional[str]:
|
|
908
|
+
"""Standalone helper to extract and clean stack trace / error details from stderr."""
|
|
909
|
+
if not stderr or not stderr.strip():
|
|
910
|
+
return None
|
|
911
|
+
clean_trace = re.sub(r'\x1b\[[0-9;]*[a-zA-Z]', '', stderr).strip()
|
|
912
|
+
return clean_trace if clean_trace else None
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def detect_test_framework(project_dir: Union[str, Path] = ".") -> Optional[str]:
|
|
916
|
+
"""Standalone helper to auto-detect project test framework."""
|
|
917
|
+
return Verifier.detect_test_framework(project_dir)
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def run_project_tests(
|
|
921
|
+
project_dir: Union[str, Path] = ".",
|
|
922
|
+
framework: Optional[str] = None,
|
|
923
|
+
timeout: float = 60.0,
|
|
924
|
+
) -> VerificationResult:
|
|
925
|
+
"""Standalone helper to run project test suite."""
|
|
926
|
+
return Verifier().run_project_tests(project_dir=project_dir, framework=framework, timeout=timeout)
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def verify_post_patch(
|
|
930
|
+
project_dir: Union[str, Path] = ".",
|
|
931
|
+
git_guard: Optional[Any] = None,
|
|
932
|
+
checkpoint_id: Optional[str] = None,
|
|
933
|
+
auto_rollback: bool = True,
|
|
934
|
+
timeout: float = 60.0,
|
|
935
|
+
) -> VerificationResult:
|
|
936
|
+
"""Standalone helper for post-patch verification with auto-rollback."""
|
|
937
|
+
return Verifier().verify_post_patch(
|
|
938
|
+
project_dir=project_dir,
|
|
939
|
+
git_guard=git_guard,
|
|
940
|
+
checkpoint_id=checkpoint_id,
|
|
941
|
+
auto_rollback=auto_rollback,
|
|
942
|
+
timeout=timeout,
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
def verify(
|
|
947
|
+
code: str,
|
|
948
|
+
language: str = "python",
|
|
949
|
+
test_code: Optional[str] = None,
|
|
950
|
+
timeout: float = 30.0,
|
|
951
|
+
) -> VerificationResult:
|
|
952
|
+
"""Top-level shortcut function for code verification."""
|
|
953
|
+
return Verifier().verify(code=code, language=language, test_code=test_code, timeout=timeout)
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
__all__ = [
|
|
957
|
+
"TestFramework",
|
|
958
|
+
"VerificationResult",
|
|
959
|
+
"CodeExtractor",
|
|
960
|
+
"Verifier",
|
|
961
|
+
"strip_fluff",
|
|
962
|
+
"parse_ast",
|
|
963
|
+
"extract_error_line",
|
|
964
|
+
"extract_stack_trace",
|
|
965
|
+
"detect_test_framework",
|
|
966
|
+
"run_project_tests",
|
|
967
|
+
"verify_post_patch",
|
|
968
|
+
"verify",
|
|
969
|
+
]
|