Coverage for src/ai_jury/hints.py: 92%

50 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-17 21:31 +0000

1"""Static analysis hints pre-pass (issue #523). 

2 

3Collects fast deterministic static linter findings (Ruff, ESLint, Flake8, Gitleaks) 

4and injects compact hints into Round 1 prompt context so LLM reviewers focus their 

5attention on deep logic bugs and security flaws rather than trivial formatting. 

6""" 

7 

8from __future__ import annotations 

9 

10import shutil 

11import subprocess 

12from pathlib import Path 

13 

14 

15def collect_static_hints(files: list[str] | None = None, root_dir: Path | None = None) -> str: 

16 """Run fast local linters on modified files and return a prompt hints string. 

17 

18 Never fails or throws: returns empty string if linters are unavailable. 

19 """ 

20 root = root_dir or Path.cwd() 

21 hints: list[str] = [] 

22 

23 # 1. Check for ruff 

24 if shutil.which("ruff"): 

25 try: 

26 cmd = ["ruff", "check", "--select", "E,F", "--output-format", "concise"] 

27 if files: 

28 py_files = [f for f in files if f.endswith(".py") and not f.startswith("-")] 

29 if py_files: 29 ↛ 33line 29 didn't jump to line 33 because the condition on line 29 was always true

30 cmd.append("--") 

31 cmd.extend(py_files) 

32 else: 

33 cmd = [] 

34 else: 

35 cmd.extend(["--", "."]) 

36 if cmd: 36 ↛ 47line 36 didn't jump to line 47 because the condition on line 36 was always true

37 res = subprocess.run(cmd, cwd=str(root), capture_output=True, text=True, timeout=5) 

38 if res.returncode != 0 and res.stdout.strip(): 

39 lines = [line.strip() for line in res.stdout.splitlines()[:5] if line.strip()] 

40 if lines: 40 ↛ 47line 40 didn't jump to line 47 because the condition on line 40 was always true

41 hints.append("Python linter (Ruff) warnings:\n" + "\n".join(f"- {item}" for item in lines)) 

42 except (subprocess.SubprocessError, OSError, Exception): 

43 # Best-effort local Ruff linter invocation; gracefully ignore errors. 

44 pass 

45 

46 # 2. Check for eslint 

47 if shutil.which("npx") and (root / "package.json").exists(): 

48 try: 

49 cmd = ["npx", "eslint", "--format", "compact"] 

50 if files is not None: 

51 js_files = [f for f in files if f.endswith((".js", ".ts", ".jsx", ".tsx")) and not f.startswith("-")] 

52 if not js_files: 

53 cmd = [] 

54 else: 

55 cmd.append("--") 

56 cmd.extend(js_files) 

57 else: 

58 cmd.extend(["--", "."]) 

59 if cmd: 

60 res = subprocess.run(cmd, cwd=str(root), capture_output=True, text=True, timeout=5) 

61 if res.returncode != 0 and res.stdout.strip(): 61 ↛ 69line 61 didn't jump to line 69 because the condition on line 61 was always true

62 lines = [line.strip() for line in res.stdout.splitlines()[:5] if line.strip()] 

63 if lines: 63 ↛ 69line 63 didn't jump to line 69 because the condition on line 63 was always true

64 hints.append("JS/TS linter (ESLint) warnings:\n" + "\n".join(f"- {item}" for item in lines)) 

65 except (subprocess.SubprocessError, OSError, Exception): 

66 # Best-effort local ESLint invocation; gracefully ignore errors. 

67 pass 

68 

69 if not hints: 

70 return "" 

71 

72 out = ["## Static Analysis Hints (Pre-pass)\n"] 

73 out.append("Static linters flagged basic syntax/formatting on modified files:") 

74 out.extend(hints) 

75 out.append( 

76 "\n👉 Note to reviewers: Focus your review on deep logic bugs, security vulnerabilities, " 

77 "race conditions, edge cases, and architectural design." 

78 ) 

79 return "\n\n".join(out)