prism-code-review 2.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.
prism/cli_config.py ADDED
@@ -0,0 +1,202 @@
1
+ import json
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ DEFAULT_AGENTS = ["security", "quality", "policy", "test",
6
+ "ai_security", "ai_quality", "ai_policy", "ai_test",
7
+ "ai_architecture", "ai_performance"]
8
+ CONFIDENCE_THRESHOLD = 0.7
9
+
10
+
11
+ def enabled_agents(config):
12
+ """Return the list of agents to run from ``config``.
13
+
14
+ ``prism.json`` stores agents either as a list of enabled names or as a
15
+ ``{name: bool}`` map (the shape `prism agents --disable` writes). Both must
16
+ yield the same enabled set; a plain ``list(config["agents"])`` on a map
17
+ returns the keys and would run disabled agents.
18
+ """
19
+ agents = (config or {}).get("agents", DEFAULT_AGENTS)
20
+ if isinstance(agents, dict):
21
+ return [name for name, on in agents.items() if on]
22
+ return list(agents)
23
+
24
+
25
+ CONFIG_FILES = ["prism.json", ".prism.json"]
26
+
27
+
28
+ def get_config_path():
29
+ """Return the config file that ``load_config`` would read, or the default.
30
+
31
+ ``prism config --set`` must write back to the same file it read from;
32
+ otherwise a repo using ``.prism.json`` silently gets a second
33
+ ``prism.json`` that shadows it.
34
+ """
35
+ for name in CONFIG_FILES:
36
+ if Path(name).exists():
37
+ return name
38
+ return CONFIG_FILES[0]
39
+
40
+
41
+ def load_config():
42
+ """Load prism.json or .prism.json, falling back to defaults."""
43
+ for name in CONFIG_FILES:
44
+ p = Path(name)
45
+ if p.exists():
46
+ with open(p) as f:
47
+ return json.load(f)
48
+ return {
49
+ "agents": DEFAULT_AGENTS,
50
+ "block_on": ["critical", "high"],
51
+ "quality_gates": {
52
+ "max_complexity": 15,
53
+ "max_file_length": 800,
54
+ "min_coverage": 0,
55
+ },
56
+ "confidence_threshold": CONFIDENCE_THRESHOLD,
57
+ }
58
+
59
+
60
+ def load_deviation_policy():
61
+ """Load .prism-policy.json or prism-policy.json, falling back to empty."""
62
+ for name in [".prism-policy.json", "prism-policy.json"]:
63
+ p = Path(name)
64
+ if p.exists():
65
+ with open(p) as f:
66
+ return json.load(f)
67
+ return {"deviations": []}
68
+
69
+
70
+ def get_staged_files(apply_ignore=True):
71
+ """Return the list of files staged for commit (git diff --cached).
72
+
73
+ Paths matched by .prismignore (or the config ``ignore`` list) are left out
74
+ so documentation, assets and lock files do not fail the pre-commit review.
75
+ """
76
+ result = subprocess.run(
77
+ ["git", "diff", "--cached", "--name-only", "--diff-filter=ACM"],
78
+ capture_output=True, text=True,
79
+ )
80
+ files = [f.strip() for f in result.stdout.strip().split("\n") if f.strip()]
81
+ if not apply_ignore or not files:
82
+ return files
83
+
84
+ from prism.ignore import filter_files
85
+ kept, _skipped = filter_files(files)
86
+ return kept
87
+
88
+
89
+ def run_changed_tests(changed_files, timeout=120):
90
+ """Run pytest on changed Python files.
91
+
92
+ Returns a dict with test execution results:
93
+ - ok: bool - did tests pass
94
+ - passed: int - tests passed
95
+ - failed: int - tests failed
96
+ - files: list of changed .py files tested
97
+ - output: str - pytest output (last 2000 chars)
98
+ - error: str - if pytest itself crashed
99
+
100
+ This closes the "test execution" gap - CodeRabbit runs tests and reports
101
+ real failures. We run only on changed files (fast-path) so it's cheap
102
+ enough for a pre-commit hook.
103
+ """
104
+ import re
105
+ py_files = [f for f in changed_files if f.endswith(".py")]
106
+ if not py_files:
107
+ return {"ok": True, "passed": 0, "failed": 0, "files": [],
108
+ "output": "No Python files changed", "error": None}
109
+
110
+ try:
111
+ result = subprocess.run(
112
+ ["python", "-m", "pytest"] + py_files + ["-q", "--no-header"],
113
+ capture_output=True, text=True, timeout=timeout,
114
+ )
115
+ output = (result.stdout + result.stderr)[-2000:]
116
+ passed, failed = 0, 0
117
+ m_pass = re.search(r"(\d+)\s+passed", output)
118
+ m_fail = re.search(r"(\d+)\s+failed", output)
119
+ if m_pass:
120
+ passed = int(m_pass.group(1))
121
+ if m_fail:
122
+ failed = int(m_fail.group(1))
123
+
124
+ return {
125
+ "ok": result.returncode == 0,
126
+ "passed": passed,
127
+ "failed": failed,
128
+ "files": py_files,
129
+ "output": output,
130
+ "error": None,
131
+ }
132
+ except subprocess.TimeoutExpired:
133
+ return {
134
+ "ok": False, "passed": 0, "failed": 0, "files": py_files,
135
+ "output": "pytest timed out after {}s".format(timeout),
136
+ "error": "timeout",
137
+ }
138
+ except Exception as exc:
139
+ return {
140
+ "ok": False, "passed": 0, "failed": 0, "files": py_files,
141
+ "output": str(exc),
142
+ "error": str(exc),
143
+ }
144
+
145
+
146
+ def get_file_diff(file_path, context_lines=3):
147
+ """Get the git diff for a file (staged + unstaged changes)."""
148
+ try:
149
+ result = subprocess.run(
150
+ ["git", "diff", "--cached", "--unified={}".format(context_lines),
151
+ "--", file_path],
152
+ capture_output=True, text=True, timeout=10,
153
+ )
154
+ if result.returncode == 0 and result.stdout.strip():
155
+ return result.stdout
156
+ except Exception:
157
+ pass
158
+ return ""
159
+
160
+
161
+ def get_diff_files(base="origin/main"):
162
+ """Return files changed between `base` and HEAD (PR-style review).
163
+
164
+ Falls back to the working-tree diff when the three-dot ref is unavailable
165
+ (e.g. a shallow clone without the base branch).
166
+ """
167
+ for ref in ("{}...HEAD".format(base), base):
168
+ try:
169
+ result = subprocess.run(
170
+ ["git", "diff", "--name-only", ref],
171
+ capture_output=True, text=True, timeout=10,
172
+ )
173
+ except Exception:
174
+ continue
175
+ if result.returncode == 0 and result.stdout.strip():
176
+ return [f.strip() for f in result.stdout.split("\n") if f.strip()]
177
+ return []
178
+
179
+
180
+ def get_diff_for_file(base, file_path, context_lines=3):
181
+ """Return the diff of `file_path` between `base` and HEAD."""
182
+ for ref in ("{}...HEAD".format(base), base):
183
+ try:
184
+ result = subprocess.run(
185
+ ["git", "diff", "--unified={}".format(context_lines), ref,
186
+ "--", file_path],
187
+ capture_output=True, text=True, timeout=10,
188
+ )
189
+ except Exception:
190
+ continue
191
+ if result.returncode == 0 and result.stdout.strip():
192
+ return result.stdout
193
+ return ""
194
+
195
+
196
+ def is_deviation(finding, policy):
197
+ """Return True if the finding matches a configured deviation."""
198
+ for dev in policy.get("deviations", []):
199
+ if (dev.get("rule_id") == finding.rule_id
200
+ and dev.get("file", finding.file) == finding.file):
201
+ return True
202
+ return False