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/__init__.py +87 -0
- prism/__main__.py +13 -0
- prism/agents.py +191 -0
- prism/agents_policy.py +277 -0
- prism/agents_quality.py +584 -0
- prism/agents_security.py +243 -0
- prism/agents_test.py +145 -0
- prism/ai_architecture.py +65 -0
- prism/ai_common.py +32 -0
- prism/ai_findings.py +181 -0
- prism/ai_graph.py +224 -0
- prism/ai_performance.py +65 -0
- prism/ai_pipeline.py +447 -0
- prism/ai_policy.py +65 -0
- prism/ai_providers.py +514 -0
- prism/ai_quality.py +65 -0
- prism/ai_security.py +65 -0
- prism/ai_test.py +65 -0
- prism/autofix.py +360 -0
- prism/cli.py +248 -0
- prism/cli_commands.py +711 -0
- prism/cli_config.py +202 -0
- prism/cli_review.py +558 -0
- prism/codebase_cache.py +155 -0
- prism/commands_common.py +365 -0
- prism/commands_reports.py +212 -0
- prism/commands_setup.py +703 -0
- prism/feedback.py +254 -0
- prism/ignore.py +344 -0
- prism/langs.py +251 -0
- prism/logging_config.py +52 -0
- prism/lsp.py +194 -0
- prism/modes.py +94 -0
- prism/reports.py +277 -0
- prism/token_usage.py +93 -0
- prism_code_review-2.0.0.dist-info/METADATA +1392 -0
- prism_code_review-2.0.0.dist-info/RECORD +41 -0
- prism_code_review-2.0.0.dist-info/WHEEL +5 -0
- prism_code_review-2.0.0.dist-info/entry_points.txt +2 -0
- prism_code_review-2.0.0.dist-info/licenses/LICENSE +21 -0
- prism_code_review-2.0.0.dist-info/top_level.txt +1 -0
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
|