codereview-loop 0.13.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.
- codereview/__init__.py +6 -0
- codereview/__main__.py +4 -0
- codereview/acceptance.py +649 -0
- codereview/agentsmd.py +259 -0
- codereview/bench.py +881 -0
- codereview/claims.py +182 -0
- codereview/cli.py +2927 -0
- codereview/core.py +9070 -0
- codereview/crash.py +247 -0
- codereview/discovery.py +289 -0
- codereview/disputes.py +749 -0
- codereview/diversity.py +183 -0
- codereview/evidence.py +1023 -0
- codereview/feedback.py +416 -0
- codereview/github.py +286 -0
- codereview/hardening.py +45 -0
- codereview/herdrhost.py +373 -0
- codereview/hooks.py +261 -0
- codereview/intake.py +208 -0
- codereview/mcp_tools.py +455 -0
- codereview/mcp_transport.py +272 -0
- codereview/notify.py +237 -0
- codereview/pilot.py +549 -0
- codereview/policy.py +1086 -0
- codereview/probe.py +534 -0
- codereview/providers.py +1681 -0
- codereview/record_schema.py +904 -0
- codereview/registry.py +294 -0
- codereview/risk.py +368 -0
- codereview/sarif.py +309 -0
- codereview/seatstate.py +398 -0
- codereview/setup_wizard.py +355 -0
- codereview/support.py +328 -0
- codereview/techlead.py +1041 -0
- codereview/tiering.py +265 -0
- codereview/userconfig.py +307 -0
- codereview_loop-0.13.0.dist-info/METADATA +502 -0
- codereview_loop-0.13.0.dist-info/RECORD +44 -0
- codereview_loop-0.13.0.dist-info/WHEEL +5 -0
- codereview_loop-0.13.0.dist-info/entry_points.txt +3 -0
- codereview_loop-0.13.0.dist-info/licenses/LICENSE +21 -0
- codereview_loop-0.13.0.dist-info/top_level.txt +2 -0
- reviewloop/__init__.py +6 -0
- reviewloop/__main__.py +6 -0
codereview/sarif.py
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""SARIF 2.1.0 export (Epic D, ticket D1).
|
|
3
|
+
|
|
4
|
+
`record_to_sarif(record)` converts a saved review record — the
|
|
5
|
+
dict `core.write_report` persists as JSON — into a SARIF 2.1.0
|
|
6
|
+
log (https://docs.oasis-open.org/sarif/sarif/v2.1.0/), the
|
|
7
|
+
interchange format GitHub code scanning and most other static
|
|
8
|
+
analysis surfaces ingest.
|
|
9
|
+
|
|
10
|
+
What is exported: every finding that is still OPEN on the
|
|
11
|
+
record, deduped by finding id — the merged `findings` list, a
|
|
12
|
+
verification record's still-open `remaining` list, and the
|
|
13
|
+
human-decision side lists (`disputed_findings`,
|
|
14
|
+
`refutation_weak_findings`, `escalated_findings`), whose
|
|
15
|
+
findings were partitioned out of `findings` by adjudication
|
|
16
|
+
routing but are not resolved: the gate stopped on them for a
|
|
17
|
+
human. Each result's properties carry its adjudication state.
|
|
18
|
+
Resolved or classified-away lists (cleared, withdrawn,
|
|
19
|
+
adjudicated-refuted, precedent-standing, fixture, archived,
|
|
20
|
+
out-of-scope) are NOT exported: code scanning should show
|
|
21
|
+
current open problems, not the record's full history.
|
|
22
|
+
|
|
23
|
+
Mapping (SARIF forces a few choices; each is deliberate):
|
|
24
|
+
- Rules derive from finding categories. A deterministic-scan
|
|
25
|
+
finding keeps its scan rule id (`eval-exec`, ...) and takes
|
|
26
|
+
its description and help text from the scan's own rule table;
|
|
27
|
+
a model finding has no rule of its own in the record, so its
|
|
28
|
+
rule id is `model-<category>` (`model-security`, ...).
|
|
29
|
+
- Severity maps blocker -> error, major -> warning,
|
|
30
|
+
minor -> note, nit -> note. Every result carries its own
|
|
31
|
+
level; a rule's defaultConfiguration level is the level of
|
|
32
|
+
its most severe result.
|
|
33
|
+
- Locations come from the finding's file + line. A finding
|
|
34
|
+
whose record entry carries a `line_basis` (which numbering
|
|
35
|
+
the cited line uses — written by the dispute tooling) has
|
|
36
|
+
that basis passed through in the result properties; SARIF
|
|
37
|
+
regions have no basis field of their own. A finding with no
|
|
38
|
+
file (a failing test command) gets no location: SARIF
|
|
39
|
+
permits it, and inventing one would be a false claim.
|
|
40
|
+
- The region snippet is the finding's evidence quote (first
|
|
41
|
+
line) — the exact text the reviewer cited.
|
|
42
|
+
- partialFingerprints carries the record's own finding id
|
|
43
|
+
(`codereviewFindingId`), so alerts stay stable across runs
|
|
44
|
+
exactly as far as the record's fingerprint is stable.
|
|
45
|
+
- The gate, mode, patch hash and other record facts ride in
|
|
46
|
+
the run's property bag — SARIF's sanctioned extension point
|
|
47
|
+
— never in fields a strict consumer could misread as its own.
|
|
48
|
+
|
|
49
|
+
The writer is stdlib-only, like the rest of the package.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
from __future__ import annotations
|
|
53
|
+
|
|
54
|
+
import posixpath
|
|
55
|
+
import re
|
|
56
|
+
from urllib.parse import quote
|
|
57
|
+
|
|
58
|
+
from . import core
|
|
59
|
+
|
|
60
|
+
SARIF_SCHEMA_URL = (
|
|
61
|
+
"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/"
|
|
62
|
+
"master/Schemata/sarif-schema-2.1.0.json")
|
|
63
|
+
SARIF_VERSION = "2.1.0"
|
|
64
|
+
|
|
65
|
+
# Task-prescribed mapping; nit shares minor's level.
|
|
66
|
+
LEVEL_BY_SEVERITY = {"blocker": "error", "major": "warning",
|
|
67
|
+
"minor": "note", "nit": "note"}
|
|
68
|
+
_LEVEL_RANK = {"error": 0, "warning": 1, "note": 2, "none": 3}
|
|
69
|
+
|
|
70
|
+
# (record key, adjudication state) for the exported lists, in
|
|
71
|
+
# priority order: the first list a finding id appears in names
|
|
72
|
+
# its state. `findings`/`remaining` are the gate's standing set;
|
|
73
|
+
# the side lists are open items awaiting a human decision.
|
|
74
|
+
_EXPORT_LISTS = (("findings", "standing"),
|
|
75
|
+
("remaining", "standing"),
|
|
76
|
+
("disputed_findings", "disputed"),
|
|
77
|
+
("refutation_weak_findings", "refutation-weak"),
|
|
78
|
+
("escalated_findings", "escalated"))
|
|
79
|
+
|
|
80
|
+
# Canonical text for the deterministic rules, from the scan's
|
|
81
|
+
# own table: rule id -> (title, recommendation, category).
|
|
82
|
+
_DET_RULE_INFO = {rule_id: (title, rec, cat)
|
|
83
|
+
for rule_id, _sev, cat, _rx, title, rec
|
|
84
|
+
in core._RULES}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def level_for_severity(severity):
|
|
88
|
+
"""SARIF level for a record severity. Unknown severities
|
|
89
|
+
take the path `_normalize_finding` takes: they behave as
|
|
90
|
+
minor, hence note."""
|
|
91
|
+
sev = str(severity or "minor").lower()
|
|
92
|
+
return LEVEL_BY_SEVERITY.get(sev, LEVEL_BY_SEVERITY["minor"])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def record_findings(record):
|
|
96
|
+
"""The record's open findings, deduped by id, each paired
|
|
97
|
+
with its adjudication state (see _EXPORT_LISTS)."""
|
|
98
|
+
seen = set()
|
|
99
|
+
out = []
|
|
100
|
+
for key, state in _EXPORT_LISTS:
|
|
101
|
+
for f in record.get(key) or []:
|
|
102
|
+
if not isinstance(f, dict):
|
|
103
|
+
continue
|
|
104
|
+
fid = str(f.get("id") or "")
|
|
105
|
+
if not fid or fid in seen:
|
|
106
|
+
continue
|
|
107
|
+
seen.add(fid)
|
|
108
|
+
out.append((f, state))
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _rule_id(finding):
|
|
113
|
+
rule = str(finding.get("rule") or "").strip()
|
|
114
|
+
if rule:
|
|
115
|
+
return rule
|
|
116
|
+
category = str(finding.get("category") or "correctness")
|
|
117
|
+
return f"model-{category.lower()}"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _rule_name(rule_id):
|
|
121
|
+
parts = [p for p in re.split(r"[^a-zA-Z0-9]+", rule_id) if p]
|
|
122
|
+
return "".join(p[:1].upper() + p[1:] for p in parts) or "Rule"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _build_rules(pairs):
|
|
126
|
+
"""One reportingDescriptor per rule id. The default level
|
|
127
|
+
is the most severe level among the rule's results; every
|
|
128
|
+
result still carries its own level explicitly."""
|
|
129
|
+
by_id = {}
|
|
130
|
+
for finding, _state in pairs:
|
|
131
|
+
rid = _rule_id(finding)
|
|
132
|
+
entry = by_id.setdefault(
|
|
133
|
+
rid, {"finding": finding, "level": None})
|
|
134
|
+
level = level_for_severity(finding.get("severity"))
|
|
135
|
+
if entry["level"] is None \
|
|
136
|
+
or _LEVEL_RANK[level] < _LEVEL_RANK[entry["level"]]:
|
|
137
|
+
entry["level"] = level
|
|
138
|
+
rules = []
|
|
139
|
+
for rid in sorted(by_id):
|
|
140
|
+
entry = by_id[rid]
|
|
141
|
+
finding = entry["finding"]
|
|
142
|
+
category = str(finding.get("category")
|
|
143
|
+
or "correctness").lower()
|
|
144
|
+
canonical = _DET_RULE_INFO.get(rid)
|
|
145
|
+
if canonical is not None:
|
|
146
|
+
title, rec, _cat = canonical
|
|
147
|
+
short = title
|
|
148
|
+
full = (f"{title} — raised by CodeReview's "
|
|
149
|
+
f"deterministic scan (pattern match on the "
|
|
150
|
+
f"diff; no model involved).")
|
|
151
|
+
help_text = rec
|
|
152
|
+
kind = "deterministic"
|
|
153
|
+
elif rid.startswith("model-"):
|
|
154
|
+
short = f"Model-reported {category} finding"
|
|
155
|
+
full = (f"A {category} problem reported by an "
|
|
156
|
+
f"independent reviewer model and carried "
|
|
157
|
+
f"through CodeReview's deterministic gate.")
|
|
158
|
+
help_text = None
|
|
159
|
+
kind = "model"
|
|
160
|
+
else:
|
|
161
|
+
# A scan rule the current table no longer carries
|
|
162
|
+
# (an older record): describe it from the finding
|
|
163
|
+
# itself rather than inventing canonical text.
|
|
164
|
+
short = f"CodeReview deterministic check: {rid}"
|
|
165
|
+
full = short + "."
|
|
166
|
+
help_text = None
|
|
167
|
+
kind = "deterministic"
|
|
168
|
+
rule = {
|
|
169
|
+
"id": rid,
|
|
170
|
+
"name": _rule_name(rid),
|
|
171
|
+
"shortDescription": {"text": short},
|
|
172
|
+
"fullDescription": {"text": full},
|
|
173
|
+
"defaultConfiguration": {"level": entry["level"]},
|
|
174
|
+
"properties": {"category": category,
|
|
175
|
+
"tags": [category, kind]},
|
|
176
|
+
}
|
|
177
|
+
if help_text:
|
|
178
|
+
rule["help"] = {"text": help_text}
|
|
179
|
+
rules.append(rule)
|
|
180
|
+
return rules
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
_CONTROL_CHARS = re.compile(r"[\x00-\x1f\x7f]")
|
|
184
|
+
_MAX_LINE = 2 ** 31 - 1
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _clean_message(text):
|
|
188
|
+
"""message.text with control characters stripped (P5-01):
|
|
189
|
+
a finding title is model/diff-derived text and may carry
|
|
190
|
+
raw newlines, NULs and other Cc characters that strict
|
|
191
|
+
SARIF consumers reject."""
|
|
192
|
+
return _CONTROL_CHARS.sub("", str(text))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _artifact_uri(path):
|
|
196
|
+
"""Normalise a record file path to a SARIF artifactLocation
|
|
197
|
+
uri (P5-01): a percent-encoded RELATIVE URI reference. Drive
|
|
198
|
+
letters and leading slashes are stripped — the uri names a
|
|
199
|
+
repo-relative artefact, and a hostile path must not be
|
|
200
|
+
mistaken for a URI scheme.
|
|
201
|
+
|
|
202
|
+
`..` segments are RESOLVED with posixpath.normpath, not
|
|
203
|
+
dropped (P5-01b): dropping them silently repointed the
|
|
204
|
+
finding (`a/../../b.py` became `a/b.py`, a different file).
|
|
205
|
+
A path that still escapes the root after normalisation
|
|
206
|
+
(a leading `../`) returns "" — the caller then emits no
|
|
207
|
+
location rather than a pointer at the wrong file."""
|
|
208
|
+
p = str(path or "").replace("\\", "/").strip()
|
|
209
|
+
p = re.sub(r"^[A-Za-z]:", "", p) # Windows drive letter
|
|
210
|
+
p = p.lstrip("/")
|
|
211
|
+
if not p:
|
|
212
|
+
return ""
|
|
213
|
+
p = posixpath.normpath(p)
|
|
214
|
+
if p in (".", "..") or p.startswith("../"):
|
|
215
|
+
return ""
|
|
216
|
+
return "/".join(quote(s, safe="") for s in p.split("/"))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _result(finding, state):
|
|
220
|
+
rid = _rule_id(finding)
|
|
221
|
+
severity = str(finding.get("severity") or "minor").lower()
|
|
222
|
+
if severity not in core.SEVERITIES:
|
|
223
|
+
severity = "minor"
|
|
224
|
+
category = str(finding.get("category")
|
|
225
|
+
or "correctness").lower()
|
|
226
|
+
result = {
|
|
227
|
+
"ruleId": rid,
|
|
228
|
+
"level": level_for_severity(severity),
|
|
229
|
+
"message": {"text": _clean_message(finding.get("title")
|
|
230
|
+
or "Untitled finding")},
|
|
231
|
+
"partialFingerprints": {
|
|
232
|
+
"codereviewFindingId": str(finding.get("id") or "")},
|
|
233
|
+
}
|
|
234
|
+
file = _artifact_uri(finding.get("file"))
|
|
235
|
+
if file:
|
|
236
|
+
physical = {"artifactLocation": {"uri": file}}
|
|
237
|
+
region = {}
|
|
238
|
+
line = finding.get("line")
|
|
239
|
+
if isinstance(line, int) and not isinstance(line, bool) \
|
|
240
|
+
and line >= 1:
|
|
241
|
+
region["startLine"] = min(line, _MAX_LINE)
|
|
242
|
+
quote = str(finding.get("evidence_quote") or "")
|
|
243
|
+
if quote:
|
|
244
|
+
region["snippet"] = {"text": quote.splitlines()[0][:500]}
|
|
245
|
+
if region:
|
|
246
|
+
physical["region"] = region
|
|
247
|
+
result["locations"] = [{"physicalLocation": physical}]
|
|
248
|
+
props = {
|
|
249
|
+
"codereviewFindingId": str(finding.get("id") or ""),
|
|
250
|
+
"severity": severity,
|
|
251
|
+
"category": category,
|
|
252
|
+
"adjudicationState": state,
|
|
253
|
+
"sources": [str(s) for s in finding.get("sources") or []],
|
|
254
|
+
"consensus": bool(finding.get("consensus")),
|
|
255
|
+
}
|
|
256
|
+
if finding.get("confidence") is not None:
|
|
257
|
+
props["confidence"] = finding["confidence"]
|
|
258
|
+
rec = str(finding.get("recommendation") or "")
|
|
259
|
+
if rec:
|
|
260
|
+
props["recommendation"] = rec
|
|
261
|
+
# Optional record fields, passed through only when present:
|
|
262
|
+
# grounding state, cross-family confirmation, and the line
|
|
263
|
+
# basis the dispute tooling records (`line_basis` — which
|
|
264
|
+
# numbering `line` uses, e.g. the head blob's).
|
|
265
|
+
for src_key, prop_key in (("grounding", "grounding"),
|
|
266
|
+
("cross_family", "crossFamily"),
|
|
267
|
+
("line_basis", "lineBasis")):
|
|
268
|
+
value = finding.get(src_key)
|
|
269
|
+
if isinstance(value, str) and value:
|
|
270
|
+
props[prop_key] = value
|
|
271
|
+
result["properties"] = props
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def record_to_sarif(record):
|
|
276
|
+
"""Convert a saved review record (dict) to a SARIF 2.1.0
|
|
277
|
+
log (dict). Pure: no I/O, no mutation of the record."""
|
|
278
|
+
pairs = record_findings(record)
|
|
279
|
+
cr_props = {}
|
|
280
|
+
for src_key, prop_key in (("schema", "recordSchema"),
|
|
281
|
+
("mode", "mode"),
|
|
282
|
+
("gate", "gate"),
|
|
283
|
+
("created_at", "recordCreatedAt"),
|
|
284
|
+
("patch_sha256", "patchSha256"),
|
|
285
|
+
("range", "range"),
|
|
286
|
+
("head", "head")):
|
|
287
|
+
value = record.get(src_key)
|
|
288
|
+
if value:
|
|
289
|
+
cr_props[prop_key] = value
|
|
290
|
+
reasons = [str(r) for r in record.get("gate_reasons") or []]
|
|
291
|
+
if reasons:
|
|
292
|
+
cr_props["gateReasons"] = reasons
|
|
293
|
+
if record.get("degraded"):
|
|
294
|
+
cr_props["degraded"] = True
|
|
295
|
+
cr_props["exportedResults"] = len(pairs)
|
|
296
|
+
run = {
|
|
297
|
+
"tool": {"driver": {
|
|
298
|
+
"name": "CodeReview",
|
|
299
|
+
"fullName": "CodeReview — multi-model code review "
|
|
300
|
+
"with a deterministic gate",
|
|
301
|
+
"version": str(record.get("version") or core.VERSION),
|
|
302
|
+
"rules": _build_rules(pairs),
|
|
303
|
+
}},
|
|
304
|
+
"results": [_result(f, state) for f, state in pairs],
|
|
305
|
+
"properties": {"codereview": cr_props},
|
|
306
|
+
}
|
|
307
|
+
return {"$schema": SARIF_SCHEMA_URL,
|
|
308
|
+
"version": SARIF_VERSION,
|
|
309
|
+
"runs": [run]}
|