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.
Files changed (75) hide show
  1. k_cli/__init__.py +77 -0
  2. k_cli/agents/__init__.py +0 -0
  3. k_cli/agents/adversarial_swarm.py +338 -0
  4. k_cli/agents/agent_core.py +255 -0
  5. k_cli/agents/background_daemon.py +141 -0
  6. k_cli/agents/orchestrator.py +376 -0
  7. k_cli/agents/persona.py +649 -0
  8. k_cli/agents/scaffold_engine.py +121 -0
  9. k_cli/agents/strands_agent.py +832 -0
  10. k_cli/agents/subagents.py +1496 -0
  11. k_cli/cli.py +3297 -0
  12. k_cli/core/__init__.py +0 -0
  13. k_cli/core/airgap.py +95 -0
  14. k_cli/core/credentials.py +548 -0
  15. k_cli/core/intent_sensor.py +177 -0
  16. k_cli/core/llm_driver.py +1028 -0
  17. k_cli/core/model_manager.py +1109 -0
  18. k_cli/core/models_hub.py +913 -0
  19. k_cli/core/prompting.py +41 -0
  20. k_cli/core/sdk.py +322 -0
  21. k_cli/core/session.py +826 -0
  22. k_cli/core/smart_router.py +230 -0
  23. k_cli/core/storage_manager.py +176 -0
  24. k_cli/core/viewport_engine.py +117 -0
  25. k_cli/demo/demo_runner.py +579 -0
  26. k_cli/git/__init__.py +0 -0
  27. k_cli/git/ai_bisect.py +208 -0
  28. k_cli/git/conflict_resolver.py +1039 -0
  29. k_cli/git/git_guard.py +417 -0
  30. k_cli/git/patcher.py +1175 -0
  31. k_cli/git/repo_map.py +1780 -0
  32. k_cli/git/smart_git.py +928 -0
  33. k_cli/git/verifier.py +969 -0
  34. k_cli/github/__init__.py +0 -0
  35. k_cli/github/dedup_engine.py +787 -0
  36. k_cli/github/github_client.py +1702 -0
  37. k_cli/github/github_engine.py +641 -0
  38. k_cli/github/local_hub.py +209 -0
  39. k_cli/github/pr_watcher.py +129 -0
  40. k_cli/github/trending.py +205 -0
  41. k_cli/tools/__init__.py +0 -0
  42. k_cli/tools/audit.py +79 -0
  43. k_cli/tools/chaos_immunity.py +377 -0
  44. k_cli/tools/codebase_qa.py +106 -0
  45. k_cli/tools/command_runner.py +256 -0
  46. k_cli/tools/diagram_generator.py +547 -0
  47. k_cli/tools/doc_retriever.py +1332 -0
  48. k_cli/tools/feature.py +105 -0
  49. k_cli/tools/ghost_daemon.py +122 -0
  50. k_cli/tools/incident_triage.py +1365 -0
  51. k_cli/tools/mcp_client.py +1846 -0
  52. k_cli/tools/repo_gardener.py +142 -0
  53. k_cli/tools/rules.py +109 -0
  54. k_cli/tools/security.py +52 -0
  55. k_cli/tools/security_healer.py +999 -0
  56. k_cli/tools/synapse_graph.py +155 -0
  57. k_cli/tui/__init__.py +0 -0
  58. k_cli/tui/diff_viewer.py +223 -0
  59. k_cli/tui/tui.py +1145 -0
  60. k_cli/tui/tui_animations.py +648 -0
  61. k_cli/tui/tui_app.py +2788 -0
  62. k_cli/ui/__init__.py +10 -0
  63. k_cli/ui/simple_repl.py +315 -0
  64. k_cli/web/__init__.py +7 -0
  65. k_cli/web/server.py +624 -0
  66. k_cli/web/static/app.js +830 -0
  67. k_cli/web/static/index.html +495 -0
  68. k_cli/web/static/monitor.html +189 -0
  69. k_cli/web/static/style.css +838 -0
  70. k_cli_for_devs-1.0.0.dist-info/METADATA +461 -0
  71. k_cli_for_devs-1.0.0.dist-info/RECORD +75 -0
  72. k_cli_for_devs-1.0.0.dist-info/WHEEL +5 -0
  73. k_cli_for_devs-1.0.0.dist-info/entry_points.txt +2 -0
  74. k_cli_for_devs-1.0.0.dist-info/licenses/LICENSE +21 -0
  75. 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
+ ]