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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-17 21:31 +0000
1"""Static analysis hints pre-pass (issue #523).
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"""
8from __future__ import annotations
10import shutil
11import subprocess
12from pathlib import Path
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.
18 Never fails or throws: returns empty string if linters are unavailable.
19 """
20 root = root_dir or Path.cwd()
21 hints: list[str] = []
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
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
69 if not hints:
70 return ""
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)