Refresh structured code review comments

This commit is contained in:
Bryan Helmkamp 2026-08-28 13:53:59 -04:00
parent afa7298071
commit 1c35efb495
No known key found for this signature in database
10 changed files with 619 additions and 88 deletions

View file

@ -33,7 +33,7 @@ digraph CodeReview {
timeout="300s",
output_schema="routing",
stdin_source="context.internal.run_id",
script="python3 -c \"import hashlib,sys; pairs=list(zip(sys.argv[1::2],sys.argv[2::2])); sys.exit(0 if pairs and all(hashlib.sha256(open(path,'rb').read()).hexdigest()==expected for path,expected in pairs) else 91)\" .fabro/workflows/code-review/scripts/code_review.py e05d017c71a16c8418c1cf941ae134695520ccd6db62186f251706d7e635eafa .fabro/workflows/code-review/scripts/git_readonly.py bcd4364ba3aca2ee1e12d5909204f645c16bdf22e3753a39d74c79d8d37cf73e .fabro/workflows/code-review/scripts/publish_pr.py 2ba754164e91524f0b4b0dd6bb761d3ba64876ed21ed4f59a1b64ebd0ec52080 .fabro/workflows/code-review/scripts/render_report.py fd8f5eb237a75e3d1e946279b27dabf7d08757e2a6aac8ff1732b504ee3d2655 .fabro/workflows/code-review/scripts/rule_loader.py f885409c64c631075d7e31fcb6e7a100592430c5eba9a6e989222006061a9f52 .fabro/workflows/code-review/specs/report-spec.md 006898b9e84951237b3fe862bd07f9be77070fa8d9cc8d28ce528c5ccf270fda .fabro/workflows/code-review/templates/report.html 04e8bffbbec4c4848646fc5ff3261aa06dd3fa95b1dfa21474f7dec1b91abd22 .fabro/workflows/code-review/schemas/findings.schema.json 6fdf7c63fb6183c8bef64a874cd56f805d92c2aa3c011a513ee07f07b0797b6d .fabro/workflows/code-review/schemas/verdict.schema.json 4cb752e624f1b88809017ac5db472d59850ffe09ec5a93d3b63ba636364b8b19 .fabro/workflows/code-review/schemas/file-groups.schema.json b53c4e1c0bbd07bbf70e83f4f3b35fd96cb880c621c7c424e95b9aea34e13d7c .fabro/workflows/code-review/prompts/finder.md.j2 86c2e6a032f7c54c1bbab1c12496a8f0d6bf48703abe6017eb175330608cf223 .fabro/workflows/code-review/prompts/verify.md.j2 6528cffd1bf199f27a32c19547aebf6f0c78a546698aaef81c6a89441b80f7da .fabro/workflows/code-review/prompts/sweep.md.j2 e6f89b47b11c57030a6ef7d5896ccb37dbd2a2982e9fa7eb7f2e3df73acab82c .fabro/workflows/code-review/prompts/group-files.md.j2 5b291313a1266d1d658f80ea7989cdefcd8609b6893b7197b17914d553dab041 .fabro/workflows/code-review/prompts/partials/finding-fields.md.j2 b91ddeb86dc7f552323f7ac04823984da9365d013bfe7965c3eb6f51fa8abe0f .fabro/workflows/code-review/prompts/partials/guidance.md.j2 53bc0c40bb917288708bed1f9ba478fbd89b9790c92497762224cc752f40bef5 .fabro/workflows/code-review/prompts/partials/output-schema.md.j2 811994bb357739f2562d84f66dc05075ebe3c7f8d58034f8c25ee1c36bee996b .fabro/workflows/code-review/prompts/partials/read-only-explorer.md.j2 44a0244e7aa62fdb0dbbfdbadcffbfb640af249bae3e96895dedd5c7a33bad10 .fabro/workflows/code-review/prompts/partials/review-target.md.j2 abffeeff0e16b89a0754cd53f1833b3744494cd54ff761798b782a80467446ea .fabro/workflows/code-review/prompts/partials/safe-git-history.md.j2 4ddd8d36d5c51d7e166a6b7f1dff51b72cce0e64108cc7e892002ca909af8b3a .fabro/workflows/code-review/rules/builtin-manifest.json 47e3123fb25c560e36216dc9b8323c86e8301f1868a4fa3219bfd59bda3a533a && python3 .fabro/workflows/code-review/scripts/code_review.py prepare --review-id-stdin --mode {{ inputs.mode }} --effort {{ inputs.effort }} --scope {{ inputs.scope }} --base {{ inputs.base }} --commit {{ inputs.commit }} --range {{ inputs.range }} --model {{ inputs.model }} --guidance {{ inputs.guidance }}"
script="python3 -c \"import hashlib,sys; pairs=list(zip(sys.argv[1::2],sys.argv[2::2])); sys.exit(0 if pairs and all(hashlib.sha256(open(path,'rb').read()).hexdigest()==expected for path,expected in pairs) else 91)\" .fabro/workflows/code-review/scripts/code_review.py 961fe81afa6a5c80432d8e8a05791d6c12ccc724e9f945e7190bbac77a6121d2 .fabro/workflows/code-review/scripts/git_readonly.py bcd4364ba3aca2ee1e12d5909204f645c16bdf22e3753a39d74c79d8d37cf73e .fabro/workflows/code-review/scripts/publish_pr.py fe65b5c710e6f486bf0b4a0da4933b53b954c32481313efd8d60d34b3dcc3a44 .fabro/workflows/code-review/scripts/render_report.py 91ea86428ed4759fbb368b4fa90ca8f54f1ab05f09f18bc8a4e9ffa188ed2295 .fabro/workflows/code-review/scripts/rule_loader.py f885409c64c631075d7e31fcb6e7a100592430c5eba9a6e989222006061a9f52 .fabro/workflows/code-review/specs/report-spec.md 7a54f72ee46f09218d18854d184a1f36875f9011877e94779c6b1f0d5dd118a9 .fabro/workflows/code-review/templates/report.html 5def570da34ca186da31781378367d70fb9c58e82f7aeec4aaf420fd348a8e61 .fabro/workflows/code-review/schemas/findings.schema.json 2f4d0a9052d5af0dad92db12a1e9d49cc91a282c4dddda495791352bf1559ed8 .fabro/workflows/code-review/schemas/verdict.schema.json de13ce02c5fd0c088640542831cc732e35dee3ddb38f89d4412f6a46fea75567 .fabro/workflows/code-review/schemas/file-groups.schema.json b53c4e1c0bbd07bbf70e83f4f3b35fd96cb880c621c7c424e95b9aea34e13d7c .fabro/workflows/code-review/prompts/finder.md.j2 86c2e6a032f7c54c1bbab1c12496a8f0d6bf48703abe6017eb175330608cf223 .fabro/workflows/code-review/prompts/verify.md.j2 cb3866240077d1bc8993b2f012a8d66a6ea61d4a9f2e88a1fefc6ef375f630e2 .fabro/workflows/code-review/prompts/sweep.md.j2 e6f89b47b11c57030a6ef7d5896ccb37dbd2a2982e9fa7eb7f2e3df73acab82c .fabro/workflows/code-review/prompts/group-files.md.j2 5b291313a1266d1d658f80ea7989cdefcd8609b6893b7197b17914d553dab041 .fabro/workflows/code-review/prompts/partials/finding-fields.md.j2 a81ee5b0ac134eb121dbf503025387c64126d3276e4673ebc836cfb62a3689fb .fabro/workflows/code-review/prompts/partials/guidance.md.j2 53bc0c40bb917288708bed1f9ba478fbd89b9790c92497762224cc752f40bef5 .fabro/workflows/code-review/prompts/partials/output-schema.md.j2 811994bb357739f2562d84f66dc05075ebe3c7f8d58034f8c25ee1c36bee996b .fabro/workflows/code-review/prompts/partials/read-only-explorer.md.j2 44a0244e7aa62fdb0dbbfdbadcffbfb640af249bae3e96895dedd5c7a33bad10 .fabro/workflows/code-review/prompts/partials/review-target.md.j2 abffeeff0e16b89a0754cd53f1833b3744494cd54ff761798b782a80467446ea .fabro/workflows/code-review/prompts/partials/safe-git-history.md.j2 4ddd8d36d5c51d7e166a6b7f1dff51b72cce0e64108cc7e892002ca909af8b3a .fabro/workflows/code-review/rules/builtin-manifest.json 47e3123fb25c560e36216dc9b8323c86e8301f1868a4fa3219bfd59bda3a533a && python3 .fabro/workflows/code-review/scripts/code_review.py prepare --review-id-stdin --mode {{ inputs.mode }} --effort {{ inputs.effort }} --scope {{ inputs.scope }} --base {{ inputs.base }} --commit {{ inputs.commit }} --range {{ inputs.range }} --model {{ inputs.model }} --guidance {{ inputs.guidance }}"
]
grouping [

View file

@ -1,7 +1,9 @@
Report each candidate finding with:
- `file`: the repository-relative path;
- `line`: the line in the reviewed revision the finding anchors to;
- `start_line` and `end_line`: the smallest contiguous line range in the
reviewed revision that demonstrates the defect. Use the same value for both
fields for a single-line finding;
- `summary`: one sentence stating the defect;
- `short_summary`: the same claim compressed to at most 60 characters, with no
rationale or consequence clause;
@ -12,8 +14,16 @@ Report each candidate finding with:
or which AGENTS.md or CLAUDE.md rule is broken;
- `category`: `correctness` for bugs, otherwise the cleanup category that
names the problem (`conventions` only with a `rule_id`);
- `issue_type`: the problem type: `bug`, `security`, `performance`,
`maintainability`, `test`, `style`, or `documentation`. This is independent
of `category`: for example, a security defect normally has category
`correctness` and issue type `security`;
- `severity`: `HIGH`, `MEDIUM`, or `LOW`, for how much the defect matters;
- `confidence`: `HIGH`, `MEDIUM`, or `LOW`, for how certain you are;
- `suggestion_code`: optional replacement text for exactly the
`start_line` through `end_line` range. Include it only when that replacement
completely fixes the finding without edits outside the range. Preserve the
file's indentation and omit diff markers and Markdown fences;
- `rule_id`: the violated check's compiled `id`, verbatim. It is required for
rule-audit findings. In other jobs, include it only when the assignment
supplies the violated check; omit it otherwise.

View file

@ -1,10 +1,12 @@
Judge one candidate code-review finding.
The workflow appends one untrusted JSON item. It contains the candidate
`claim` -- the file and line, the category, `severityAsReported`, the
`claim` -- the file and exact location range, the category and issue type,
`severityAsReported`, the
`summary`, the `failure_scenario`, and `reports`, the number of finder jobs
that reported it independently -- plus the verification `bias`, the exact
review `target`, and a stable `job_id`.
that reported it independently. It can also contain a proposed `suggestion`
for the engine-derived `location.existing_code`. The item also contains the
verification `bias`, the exact review `target`, and a stable `job_id`.
Everything in the claim is an assertion by an earlier pass, including the line
number. Verify it against the repository: the reporter may have misread, the
@ -58,6 +60,13 @@ Cite the decisive repository-relative `file:line` locations in `reasoning`.
Judge the finding as written; a different nearby bug does not make it true. Do
not invent a guard, and do not assume one exists without reading it.
If the claim contains a `suggestion`, also return `suggestion_valid`: `true`
only when replacing the complete location range with `replacement_code`
fully fixes the finding, preserves intended behavior, and needs no edit
outside that range. Return `false` when it is incomplete, unsafe, unrelated,
or cannot be validated from the repository. Omit `suggestion_valid` when the
claim has no suggestion.
Read and search with whatever read-only commands suit the question, history
included. Never build, test, execute, install, fetch, use the network, or
modify files. Nothing blocks those here; not attempting them is the rule you

View file

@ -9,21 +9,29 @@
"type": "object",
"required": [
"file",
"line",
"start_line",
"end_line",
"summary",
"short_summary",
"failure_scenario",
"category",
"issue_type",
"severity",
"confidence"
],
"properties": {
"file": { "type": "string" },
"line": { "type": "integer" },
"line": {
"type": "integer",
"description": "Deprecated single-line anchor; use start_line and end_line."
},
"start_line": { "type": "integer", "minimum": 1 },
"end_line": { "type": "integer", "minimum": 1 },
"rule_id": { "type": "string" },
"summary": { "type": "string" },
"short_summary": { "type": "string", "maxLength": 60 },
"failure_scenario": { "type": "string" },
"suggestion_code": { "type": "string", "maxLength": 8000 },
"category": {
"type": "string",
"enum": [
@ -36,6 +44,18 @@
"test-coverage"
]
},
"issue_type": {
"type": "string",
"enum": [
"bug",
"security",
"performance",
"maintainability",
"test",
"style",
"documentation"
]
},
"severity": {
"type": "string",
"enum": ["HIGH", "MEDIUM", "LOW"]

View file

@ -7,6 +7,7 @@
"enum": ["CONFIRMED", "PLAUSIBLE", "REFUTED"]
},
"reasoning": { "type": "string" },
"duplicate_of": { "type": "string" }
"duplicate_of": { "type": "string" },
"suggestion_valid": { "type": "boolean" }
}
}

View file

@ -91,6 +91,15 @@ CATEGORIES = (
"conventions",
"test-coverage",
)
ISSUE_TYPES = (
"bug",
"security",
"performance",
"maintainability",
"test",
"style",
"documentation",
)
# Correctness bugs always outrank cleanup findings when a cap forces a cut.
CLEANUP_CATEGORIES = frozenset(CATEGORIES) - {"correctness"}
# Policy filters drop well-formed findings the review does not want; unlike a
@ -116,6 +125,8 @@ SIBLING_CAP = 6
CODE_FRAME_CONTEXT = 4
CODE_FRAME_MAX_LINE_LENGTH = 400
CODE_FRAME_MAX_BYTES = 2 * 1024 * 1024
LOCATION_MAX_LINES = 50
SUGGESTION_CODE_MAX_LENGTH = 8000
CODE_FRAME_LANGUAGES = {
"c": "C",
"cc": "C++",
@ -150,7 +161,7 @@ CODE_FRAME_LANGUAGES = {
"yml": "YAML",
}
CANONICAL_SCHEMA_VERSION = 3
CANONICAL_SCHEMA_VERSION = 4
CANONICAL_FILES = (
"review-manifest.json",
"candidate-ledger.jsonl",
@ -1013,6 +1024,19 @@ def verify_schema_sources() -> None:
f"{FINDINGS_SCHEMA_PATH} category enum does not match this "
"engine's category list"
)
try:
schema_issue_types = findings_schema["properties"]["findings"]["items"][
"properties"
]["issue_type"]["enum"]
except (KeyError, TypeError) as error:
raise WorkflowDataError(
f"{FINDINGS_SCHEMA_PATH} has no issue_type enum"
) from error
if list(schema_issue_types) != list(ISSUE_TYPES):
raise WorkflowDataError(
f"{FINDINGS_SCHEMA_PATH} issue_type enum does not match this "
"engine's issue type list"
)
verdict_schema = read_json(root() / VERDICT_SCHEMA_PATH)
try:
schema_verdicts = verdict_schema["properties"]["verdict"]["enum"]
@ -1870,27 +1894,69 @@ def finding_or_rejection(
if not isinstance(value, dict):
return None, "the finding is not a JSON object"
path = normalize_repo_path(value.get("file"))
line = value.get("line")
legacy_line = value.get("line")
start_line = value.get("start_line", legacy_line)
end_line = value.get("end_line", legacy_line)
summary = one_line(value.get("summary"), 600).strip()
short_summary = one_line(value.get("short_summary"), 200).strip()[:60]
failure_scenario = clean_text(value.get("failure_scenario"), 4000).strip()
category = one_line(value.get("category"), 40).strip().lower()
issue_type = one_line(value.get("issue_type"), 40).strip().lower()
severity = one_line(value.get("severity"), 20).upper()
confidence = one_line(value.get("confidence"), 20).upper()
raw_suggestion = value.get("suggestion_code", "")
for failed, reason in (
(path is None or path == ".", "file does not name a repository file"),
(
isinstance(line, bool) or not isinstance(line, int) or line < 1,
"line is not a positive integer",
isinstance(start_line, bool)
or not isinstance(start_line, int)
or start_line < 1,
"start_line is not a positive integer",
),
(
isinstance(end_line, bool)
or not isinstance(end_line, int)
or end_line < 1,
"end_line is not a positive integer",
),
(
isinstance(start_line, int)
and isinstance(end_line, int)
and start_line > end_line,
"start_line is after end_line",
),
(
isinstance(start_line, int)
and isinstance(end_line, int)
and end_line - start_line + 1 > LOCATION_MAX_LINES,
f"location spans more than {LOCATION_MAX_LINES} lines",
),
(not summary, "summary is empty"),
(not failure_scenario, "failure_scenario is empty"),
(category not in CATEGORIES, "category is not in the closed list"),
(issue_type not in ISSUE_TYPES, "issue_type is not in the closed list"),
(severity not in SEVERITY_RANK, "severity is not HIGH, MEDIUM, or LOW"),
(
confidence not in CONFIDENCE_RANK,
"confidence is not HIGH, MEDIUM, or LOW",
),
(
not isinstance(raw_suggestion, str),
"suggestion_code is not a string",
),
(
isinstance(raw_suggestion, str)
and len(raw_suggestion) > SUGGESTION_CODE_MAX_LENGTH,
f"suggestion_code exceeds {SUGGESTION_CODE_MAX_LENGTH} characters",
),
(
isinstance(raw_suggestion, str)
and any(
character not in "\n\t" and ord(character) < 0x20
for character in raw_suggestion
),
"suggestion_code contains control characters",
),
):
if failed:
return None, reason
@ -1922,13 +1988,20 @@ def finding_or_rejection(
return {
"file": path,
"line": line,
# ``line`` remains the stable end-line alias used by ranking,
# deduplication, and older consumers. The canonical finding also
# carries the complete range.
"line": end_line,
"start_line": start_line,
"end_line": end_line,
"summary": summary,
"short_summary": short_summary,
"failure_scenario": failure_scenario,
"category": category,
"issue_type": issue_type,
"severity": severity,
"confidence": confidence,
"suggestion_code": raw_suggestion if raw_suggestion.strip() else "",
"rule_ids": rule_ids,
}, None
@ -1975,7 +2048,7 @@ def state_rule_context(
}
def normalize_verdict(value: Any) -> Optional[Dict[str, str]]:
def normalize_verdict(value: Any) -> Optional[Dict[str, Any]]:
if not isinstance(value, dict):
return None
verdict = value.get("verdict")
@ -1992,6 +2065,9 @@ def normalize_verdict(value: Any) -> Optional[Dict[str, str]]:
duplicate_of.strip()
):
result["duplicate_of"] = duplicate_of.strip()
suggestion_valid = value.get("suggestion_valid")
if isinstance(suggestion_valid, bool):
result["suggestion_valid"] = suggestion_valid
return result
@ -2338,16 +2414,26 @@ def verification_claim(
about applicability, and the verifier judges only violation. ``pool``
supplies the same-file siblings the verifier may name as duplicates.
"""
start_line = int(candidate.get("start_line") or candidate.get("line") or 0)
end_line = int(candidate.get("end_line") or candidate.get("line") or 0)
location = resolved_location(
str(candidate.get("file") or ""), start_line, end_line
)
claim: Dict[str, Any] = {
"file": candidate.get("file"),
"line": candidate.get("line"),
"location": location,
"category": candidate.get("category"),
"issue_type": candidate.get("issue_type"),
"severityAsReported": candidate.get("severity"),
"summary": candidate.get("summary"),
"failure_scenario": candidate.get("failure_scenario"),
"reports": int(candidate.get("reports") or 1),
"siblings": sibling_claims(candidate, pool or []),
}
suggestion_code = str(candidate.get("suggestion_code") or "")
if suggestion_code and location["existing_code"]:
claim["suggestion"] = {"replacement_code": suggestion_code}
rules_state = (state or {}).get("rules")
if isinstance(rules_state, dict) and rules_state.get("enabled"):
catalog = rules_state.get("catalog") or {}
@ -2487,6 +2573,24 @@ def plan_verify() -> None:
set(existing.get("rule_ids") or [])
| set(report.get("rule_ids") or [])
)
# A fix is publishable only when reporting passes agree on its exact
# range and replacement. A pass that offers no fix does not veto an
# otherwise consistent proposal.
existing_suggestion = str(existing.get("suggestion_code") or "")
report_suggestion = str(report.get("suggestion_code") or "")
if not existing_suggestion and report_suggestion:
existing["start_line"] = report["start_line"]
existing["end_line"] = report["end_line"]
existing["line"] = report["end_line"]
existing["suggestion_code"] = report_suggestion
elif existing_suggestion and report_suggestion and (
existing_suggestion != report_suggestion
or existing.get("start_line") != report.get("start_line")
or existing.get("end_line") != report.get("end_line")
):
existing["suggestion_code"] = ""
if report.get("issue_type") == "security":
existing["issue_type"] = "security"
if (
SEVERITY_RANK[report["severity"]]
> SEVERITY_RANK[existing["severity"]]
@ -2712,46 +2816,84 @@ def safe_code_text(value: str) -> str:
return text
def code_frame(file_path: str, line: int) -> Dict[str, Any]:
"""Read the lines around a finding's anchor from the reviewed tree.
def reviewed_source_lines(file_path: str) -> Optional[List[str]]:
"""Read one UTF-8 source file from the unchanged reviewed tree."""
target = root() / file_path
try:
if target.is_symlink() or not target.is_file():
return None
if target.stat().st_size > CODE_FRAME_MAX_BYTES:
return None
raw = target.read_bytes()
except OSError:
return None
if b"\0" in raw:
return None
try:
return raw.decode("utf-8").splitlines()
except UnicodeError:
return None
def resolved_location(
file_path: str, start_line: int, end_line: int
) -> Dict[str, Any]:
"""Build an engine-derived exact anchor for a finding."""
existing_code = ""
source_lines = reviewed_source_lines(file_path)
if (
source_lines is not None
and 1 <= start_line <= end_line <= len(source_lines)
):
candidate = "\n".join(source_lines[start_line - 1:end_line])
if (
len(candidate) <= SUGGESTION_CODE_MAX_LENGTH
and not any(
character not in "\n\t" and ord(character) < 0x20
for character in candidate
)
):
existing_code = candidate
return {
"start_line": start_line,
"end_line": end_line,
"existing_code": existing_code,
}
def code_frame(
file_path: str, start_line: int, end_line: Optional[int] = None
) -> Dict[str, Any]:
"""Read the lines around a finding's anchor range from the reviewed tree.
The excerpt shown in the report is read here, so its line numbers are the
tree's own and no agent transcribes them. An unreadable, binary,
oversized, or out-of-range target yields an empty excerpt.
"""
end_line = start_line if end_line is None else end_line
language = code_frame_language(file_path)
empty: Dict[str, Any] = {
"language": language,
"label": f"{file_path}:{line}",
"label": f"{file_path}:{start_line}-{end_line}",
"lines": [],
}
target = root() / file_path
try:
if target.is_symlink() or not target.is_file():
return empty
if target.stat().st_size > CODE_FRAME_MAX_BYTES:
return empty
raw = target.read_bytes()
except OSError:
source_lines = reviewed_source_lines(file_path)
if (
source_lines is None
or start_line < 1
or end_line < start_line
or end_line > len(source_lines)
):
return empty
if b"\0" in raw:
return empty
try:
text = raw.decode("utf-8")
except UnicodeError:
return empty
source_lines = text.splitlines()
if line > len(source_lines):
return empty
start = max(1, line - CODE_FRAME_CONTEXT)
end = min(len(source_lines), line + CODE_FRAME_CONTEXT)
start = max(1, start_line - CODE_FRAME_CONTEXT)
end = min(len(source_lines), end_line + CODE_FRAME_CONTEXT)
lines: List[Dict[str, Any]] = []
for number in range(start, end + 1):
entry: Dict[str, Any] = {
"number": number,
"text": safe_code_text(source_lines[number - 1]),
}
if number == line:
if start_line <= number <= end_line:
entry["highlight"] = True
lines.append(entry)
return {
@ -2767,14 +2909,19 @@ def reportable_finding(
) -> Dict[str, Any]:
candidate = record["candidate"]
verdict = record.get("verdict")
return {
start_line = int(candidate.get("start_line") or candidate["line"])
end_line = int(candidate.get("end_line") or candidate["line"])
location = resolved_location(candidate["file"], start_line, end_line)
finding = {
"id": display_id,
"file": candidate["file"],
"line": candidate["line"],
"line": end_line,
"location": location,
"summary": candidate["summary"],
"short_summary": candidate["short_summary"],
"failure_scenario": candidate["failure_scenario"],
"category": candidate["category"],
"issue_type": candidate["issue_type"],
"severity": candidate["severity"],
"confidence": candidate["confidence"],
"reports": int(candidate.get("reports") or 1),
@ -2785,8 +2932,18 @@ def reportable_finding(
"source": candidate.get("source", "finder"),
"verdict": verdict["verdict"] if verdict else "UNVERIFIED",
"verdict_reasoning": verdict["reasoning"] if verdict else "",
"code": code_frame(candidate["file"], int(candidate["line"])),
"code": code_frame(candidate["file"], start_line, end_line),
}
suggestion_code = str(candidate.get("suggestion_code") or "")
if (
suggestion_code
and location["existing_code"]
and verdict is not None
and verdict.get("suggestion_valid") is True
and suggestion_code != location["existing_code"]
):
finding["suggestion"] = {"replacement_code": suggestion_code}
return finding
def finding_reports(state: Mapping[str, Any], key: str) -> List[str]:
@ -2825,6 +2982,8 @@ def vote_records(
entry["reasoning"] = verdict["reasoning"]
if verdict.get("duplicate_of"):
entry["duplicate_of"] = verdict["duplicate_of"]
if "suggestion_valid" in verdict:
entry["suggestion_valid"] = verdict["suggestion_valid"]
records.append(entry)
return records
@ -3103,7 +3262,10 @@ def final_tally() -> None:
"id": candidate.get("id"),
"file": candidate.get("file"),
"line": candidate.get("line"),
"start_line": candidate.get("start_line"),
"end_line": candidate.get("end_line"),
"category": candidate.get("category"),
"issue_type": candidate.get("issue_type"),
"severity": candidate.get("severity"),
"confidence": candidate.get("confidence"),
"reports": int(candidate.get("reports") or 1),
@ -3114,6 +3276,8 @@ def final_tally() -> None:
"failure_scenario": candidate.get("failure_scenario"),
"disposition": disposition,
}
if candidate.get("suggestion_code"):
entry["suggestion_code"] = candidate["suggestion_code"]
if verdict is not None:
entry["verdict"] = verdict["verdict"]
if disposition == "duplicate":

View file

@ -49,6 +49,7 @@ DEFAULT_BATCH_SIZE = 50
SUMMARY_BUDGET = 65000
GITHUB_BODY_CAP = 65536
SEVERITY_RANK = {"LOW": 0, "MEDIUM": 1, "HIGH": 2}
SEVERITY_EMOJI = {"LOW": "🟡", "MEDIUM": "🟠", "HIGH": "🔴"}
CANONICAL_FILE_NAMES = (
"review-manifest.json",
"candidate-ledger.jsonl",
@ -68,6 +69,14 @@ UNVERIFIED_NOTE = (
"_This finding comes from a low-effort single-pass review and was not "
"independently verified._"
)
PLAUSIBLE_WARNING = (
"> **Needs confirmation:** The verifier could not fully confirm this "
"finding from the available evidence."
)
UNVERIFIED_WARNING = (
"> **Not verified:** This finding comes from a low-effort single-pass "
"review and was not independently verified."
)
class PublishError(RuntimeError):
@ -201,6 +210,19 @@ def has_diff_position(
return any(start <= line <= end for start, end in hunks.get(path, ()))
def has_diff_range(
hunks: Mapping[str, Sequence[Tuple[int, int]]],
path: str,
start_line: int,
end_line: int,
) -> bool:
"""True when one RIGHT-side hunk contains the complete range."""
return any(
hunk_start <= start_line <= end_line <= hunk_end
for hunk_start, hunk_end in hunks.get(path, ())
)
# --- Routing configuration (R3-R5, fail-closed) ------------------------------
@ -296,6 +318,11 @@ def safe_code_block(code: Mapping[str, Any]) -> List[str]:
return body
def raw_code_block(text: str, language: str = "text") -> List[str]:
fence = backtick_fence([text])
return [fence + language, text, fence]
def finding_detail_lines(finding: Mapping[str, Any]) -> List[str]:
lines: List[str] = []
if finding["summary"].strip() != finding["short_summary"].strip():
@ -318,29 +345,99 @@ def finding_detail_lines(finding: Mapping[str, Any]) -> List[str]:
return lines
def inline_more_lines(finding: Mapping[str, Any]) -> List[str]:
verdict = str(finding["verdict"]).lower().capitalize()
confidence = str(finding["confidence"]).lower().capitalize()
lines = [
"",
"<details>",
f"<summary>More · {verdict} · {confidence} confidence</summary>",
"",
"**Impact:** "
+ renderer.escape_markdown(finding["failure_scenario"]),
"",
]
reasoning = str(finding.get("verdict_reasoning") or "").strip()
if reasoning:
evidence = renderer.escape_markdown(reasoning)
elif finding["verdict"] == "UNVERIFIED":
evidence = "No independent verification ran at this effort level."
else:
evidence = "No verifier reasoning was recorded."
lines.append("**Evidence:** " + evidence)
metadata: List[str] = []
reports = finding.get("reports")
reporters = finding.get("reporters") or []
if isinstance(reports, int) and reports > 1:
report_text = f"- Reported by {reports} review passes"
if reporters:
report_text += ": " + renderer.escape_markdown(
", ".join(str(reporter) for reporter in reporters)
)
metadata.append(report_text)
rule_ids = finding.get("rule_ids") or []
if rule_ids:
label = "Rule" if len(rule_ids) == 1 else "Rules"
metadata.append(
f"- {label}: "
+ ", ".join(renderer.code_span(rule_id) for rule_id in rule_ids)
)
for anchor in finding.get("anchors") or []:
location = f"{anchor['file']}:{anchor['line']}"
metadata.append(
f"- Related location: {renderer.code_span(location)} "
f"({renderer.escape_markdown(anchor['category'])}, "
f"{renderer.escape_markdown(anchor['id'])})"
)
if metadata:
lines.extend(["", *metadata])
lines.extend(["", "</details>"])
return lines
def inline_comment_body(finding: Mapping[str, Any], review_id: str) -> str:
tag = comment_tag(review_id, finding["id"])
issue_type = str(finding["issue_type"]).lower().capitalize()
lines = [
f"<!-- {tag} -->",
"",
f"**{finding['severity']} · {finding['category']}** — "
f"**{SEVERITY_EMOJI[finding['severity']]} {issue_type}** — "
+ renderer.escape_markdown(finding["short_summary"]),
*finding_detail_lines(finding),
]
meta = f"Verdict {finding['verdict']} · confidence {finding['confidence']}"
rule_ids = finding.get("rule_ids") or []
if rule_ids:
meta += " · rule " + ", ".join(
renderer.code_span(rule_id) for rule_id in rule_ids
if finding["summary"].strip() != finding["short_summary"].strip():
lines.extend(["", renderer.escape_markdown(finding["summary"])])
if finding["verdict"] == "PLAUSIBLE":
lines.extend(["", PLAUSIBLE_WARNING])
elif finding["verdict"] == "UNVERIFIED":
lines.extend(["", UNVERIFIED_WARNING])
suggestion = finding.get("suggestion")
if isinstance(suggestion, dict):
lines.extend(
[
"",
*raw_code_block(suggestion["replacement_code"], "suggestion"),
]
)
lines.extend(["", f"_{meta}_"])
lines.extend(inline_more_lines(finding))
return "\n".join(lines)
def summary_section(
finding: Mapping[str, Any], reason_text: Optional[str]
) -> str:
location = renderer.code_span(f"{finding['file']}:{finding['line']}")
location_data = finding["location"]
start_line = location_data["start_line"]
end_line = location_data["end_line"]
location_text = (
f"{finding['file']}:{start_line}"
if start_line == end_line
else f"{finding['file']}:{start_line}-{end_line}"
)
location = renderer.code_span(location_text)
facts = [location]
if reason_text:
facts.append(reason_text)
@ -353,7 +450,8 @@ def summary_section(
+ ", ".join(renderer.code_span(rule_id) for rule_id in rule_ids)
)
lines = [
f"### {finding['id']} · {finding['severity']} {finding['category']}"
f"### {finding['id']} · {finding['severity']} "
f"{finding['issue_type']} / {finding['category']}"
+ renderer.escape_markdown(finding["short_summary"]),
"",
" · ".join(facts),
@ -362,6 +460,22 @@ def summary_section(
excerpt = safe_code_block(finding["code"])
if excerpt:
lines.extend(["", *excerpt])
suggestion = finding.get("suggestion")
if isinstance(suggestion, dict):
lines.extend(
[
"",
"<details><summary>Suggested change</summary>",
"",
"**Before:**",
*raw_code_block(location_data["existing_code"]),
"",
"**After:**",
*raw_code_block(suggestion["replacement_code"]),
"",
"</details>",
]
)
return "\n".join(lines)
@ -597,12 +711,19 @@ def command_plan(args: argparse.Namespace) -> int:
# no-position reason and routing can never hide a placement.
placements: List[Dict[str, Any]] = []
for finding in findings:
location = finding["location"]
start_line = location["start_line"]
end_line = location["end_line"]
base_entry = {
"finding_id": finding["id"],
"path": finding["file"],
"line": finding["line"],
"line": end_line,
"start_line": start_line,
"end_line": end_line,
}
if not has_diff_position(hunks, finding["file"], finding["line"]):
if not has_diff_range(
hunks, finding["file"], start_line, end_line
):
placements.append(
{
**base_entry,
@ -811,6 +932,24 @@ def validate_plan_document(
line = entry.get("line")
if isinstance(line, bool) or not isinstance(line, int) or line < 1:
fail(f"plan {field}.line must be a positive integer")
start_line = entry.get("start_line")
end_line = entry.get("end_line")
if (
isinstance(start_line, bool)
or not isinstance(start_line, int)
or start_line < 1
):
fail(f"plan {field}.start_line must be a positive integer")
if (
isinstance(end_line, bool)
or not isinstance(end_line, int)
or end_line < start_line
or end_line != line
):
fail(
f"plan {field}.end_line must end at its line and not precede "
"start_line"
)
body = require_text(entry.get("body"), f"{field}.body", GITHUB_BODY_CAP)
placement = entry.get("placement")
if placement == "inline":
@ -1014,15 +1153,19 @@ class BatchPoster:
self.batches_succeeded = 0
def post_review(self, finding_ids: Sequence[str]) -> Optional[int]:
comments = [
{
"path": self.by_id[finding_id]["path"],
"line": self.by_id[finding_id]["line"],
comments: List[Dict[str, Any]] = []
for finding_id in finding_ids:
entry = self.by_id[finding_id]
comment: Dict[str, Any] = {
"path": entry["path"],
"line": entry["end_line"],
"side": "RIGHT",
"body": self.by_id[finding_id]["body"],
"body": entry["body"],
}
for finding_id in finding_ids
]
if entry["start_line"] != entry["end_line"]:
comment["start_line"] = entry["start_line"]
comment["start_side"] = "RIGHT"
comments.append(comment)
# No review body: the run tag lives in the sticky summary and in
# every inline comment's identity tag, so body text here would
# only add a noise bubble to the PR timeline (R12).

View file

@ -14,6 +14,7 @@ Python 3.9-compatible. Standard library only.
from __future__ import annotations
import json
import hashlib
import os
import re
import sys
@ -22,7 +23,7 @@ from typing import Any, Dict, List, Mapping, NoReturn, Sequence, Tuple
from urllib.parse import quote
CANONICAL_SCHEMA_VERSION = 3
CANONICAL_SCHEMA_VERSION = 4
TEMPLATE_RELATIVE_PATH = ("..", "templates", "report.html")
PAYLOAD_PLACEHOLDER = "__CODE_REVIEW_PAYLOAD__"
@ -35,6 +36,15 @@ CATEGORIES = (
"conventions",
"test-coverage",
)
ISSUE_TYPES = (
"bug",
"security",
"performance",
"maintainability",
"test",
"style",
"documentation",
)
SEVERITIES = ("HIGH", "MEDIUM", "LOW")
FINDING_VERDICTS = ("CONFIRMED", "PLAUSIBLE", "UNVERIFIED")
VOTE_VERDICTS = ("CONFIRMED", "PLAUSIBLE", "REFUTED")
@ -58,6 +68,7 @@ COMPILED_RULE_ID_RE = re.compile(
)
MAX_TEXT = 8000
MAX_RULE_IDS_PER_FINDING = 50
MAX_LOCATION_LINES = 50
class RenderError(RuntimeError):
@ -211,8 +222,8 @@ def validate_code(value: object, field: str) -> Dict[str, Any]:
line["highlight"] = True
highlighted += 1
normalized.append(line)
if normalized and highlighted != 1:
die(f"{field} must highlight exactly one line")
if normalized and highlighted < 1:
die(f"{field} must highlight at least one line")
return {
"language": code["language"],
"label": code["label"],
@ -230,12 +241,49 @@ def validate_finding(value: object, index: int) -> Dict[str, Any]:
line = positive_int(finding.get("line"), f"{field}.line")
if finding.get("category") not in CATEGORIES:
die(f"{field}.category is not in the closed list")
if finding.get("issue_type") not in ISSUE_TYPES:
die(f"{field}.issue_type is not in the closed list")
if finding.get("severity") not in SEVERITIES:
die(f"{field}.severity is invalid")
if finding.get("confidence") not in SEVERITIES:
die(f"{field}.confidence is invalid")
if finding.get("verdict") not in FINDING_VERDICTS:
die(f"{field}.verdict is invalid")
location = as_map(finding.get("location"))
start_line = positive_int(
location.get("start_line"), f"{field}.location.start_line"
)
end_line = positive_int(
location.get("end_line"), f"{field}.location.end_line"
)
if start_line > end_line:
die(f"{field}.location starts after it ends")
if end_line - start_line + 1 > MAX_LOCATION_LINES:
die(f"{field}.location spans more than {MAX_LOCATION_LINES} lines")
if end_line != line:
die(f"{field}.line must equal location.end_line")
normalized_location = {
"start_line": start_line,
"end_line": end_line,
"existing_code": safe_text(
location.get("existing_code"),
f"{field}.location.existing_code",
),
}
suggestion = finding.get("suggestion")
normalized_suggestion: Optional[Dict[str, str]] = None
if suggestion is not None:
suggestion_record = as_map(suggestion)
replacement = safe_text(
suggestion_record.get("replacement_code"),
f"{field}.suggestion.replacement_code",
allow_empty=False,
)
if not normalized_location["existing_code"]:
die(f"{field}.suggestion has no exact existing code anchor")
if replacement == normalized_location["existing_code"]:
die(f"{field}.suggestion does not change the anchored code")
normalized_suggestion = {"replacement_code": replacement}
reporters = finding.get("reporters")
if not isinstance(reporters, list) or not all(
isinstance(item, str) for item in reporters
@ -270,10 +318,21 @@ def validate_finding(value: object, index: int) -> Dict[str, Any]:
"category": record["category"],
}
)
return {
normalized_code = validate_code(finding.get("code"), f"{field}.code")
if normalized_code["lines"]:
highlighted = {
entry["number"]
for entry in normalized_code["lines"]
if entry.get("highlight")
}
expected = set(range(start_line, end_line + 1))
if highlighted != expected:
die(f"{field}.code highlights do not match its location")
normalized = {
"id": display_id,
"file": path,
"line": line,
"location": normalized_location,
"summary": safe_text(
finding.get("summary"), f"{field}.summary", allow_empty=False
),
@ -288,6 +347,7 @@ def validate_finding(value: object, index: int) -> Dict[str, Any]:
allow_empty=False,
),
"category": finding["category"],
"issue_type": finding["issue_type"],
"severity": finding["severity"],
"confidence": finding["confidence"],
"reports": positive_int(finding.get("reports"), f"{field}.reports"),
@ -299,8 +359,11 @@ def validate_finding(value: object, index: int) -> Dict[str, Any]:
"verdict_reasoning": safe_text(
finding.get("verdict_reasoning"), f"{field}.verdict_reasoning"
),
"code": validate_code(finding.get("code"), f"{field}.code"),
"code": normalized_code,
}
if normalized_suggestion is not None:
normalized["suggestion"] = normalized_suggestion
return normalized
def validate_findings(value: object) -> List[Dict[str, Any]]:
@ -333,6 +396,8 @@ def validate_ledger(records: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]
positive_int(record.get("line"), f"{field}.line")
if record.get("category") not in CATEGORIES:
die(f"{field}.category is not in the closed list")
if record.get("issue_type") not in ISSUE_TYPES:
die(f"{field}.issue_type is not in the closed list")
if record.get("disposition") not in DISPOSITIONS:
die(f"{field}.disposition is invalid")
validated.append(dict(record))
@ -351,6 +416,10 @@ def validate_votes(records: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]]
if record.get("verdict") not in VOTE_VERDICTS:
die(f"{field}.verdict is invalid")
safe_text(record.get("reasoning"), f"{field}.reasoning")
if "suggestion_valid" in record and not isinstance(
record.get("suggestion_valid"), bool
):
die(f"{field}.suggestion_valid must be a boolean")
validated.append(dict(record))
return validated
@ -582,6 +651,12 @@ def code_block(code: Mapping[str, Any]) -> List[str]:
return body
def fenced_text(value: str, language: str = "text") -> List[str]:
longest = max((len(run) for run in re.findall(r"`+", value)), default=0)
fence = "`" * max(4, longest + 1)
return [fence + language, value, fence]
def describe_target(manifest: Mapping[str, Any]) -> str:
mode = str(manifest.get("mode"))
if mode == "files":
@ -601,11 +676,19 @@ def revision_summary(manifest: Mapping[str, Any]) -> str:
def finding_markdown(finding: Mapping[str, Any]) -> List[str]:
location = f"{finding['file']}:{finding['line']}"
location_data = finding["location"]
start_line = location_data["start_line"]
end_line = location_data["end_line"]
location = (
f"{finding['file']}:{start_line}"
if start_line == end_line
else f"{finding['file']}:{start_line}-{end_line}"
)
rule_ids = finding.get("rule_ids") or []
lines = [
f"### {finding['id']} · {finding['severity']} "
f"{finding['category']}{escape_markdown(finding['short_summary'])}",
f"{finding['issue_type']} / {finding['category']}"
f"{escape_markdown(finding['short_summary'])}",
"",
f"{code_span(location)} · verdict {finding['verdict']} · "
f"confidence {finding['confidence']} · reported by "
@ -636,6 +719,22 @@ def finding_markdown(finding: Mapping[str, Any]) -> List[str]:
reasoning = str(finding.get("verdict_reasoning") or "").strip()
if reasoning:
lines.extend(["", f"**Verifier.** {escape_markdown(reasoning)}"])
suggestion = finding.get("suggestion")
if isinstance(suggestion, dict):
lines.extend(
[
"",
"<details><summary>Suggested change</summary>",
"",
"**Before:**",
*fenced_text(location_data["existing_code"]),
"",
"**After:**",
*fenced_text(suggestion["replacement_code"]),
"",
"</details>",
]
)
excerpt = code_block(finding["code"])
if excerpt:
lines.extend(["", *excerpt])
@ -841,7 +940,9 @@ def jsonl_line(finding: Mapping[str, Any]) -> str:
"id",
"file",
"line",
"location",
"category",
"issue_type",
"severity",
"confidence",
"verdict",
@ -854,6 +955,8 @@ def jsonl_line(finding: Mapping[str, Any]) -> str:
"source",
)
}
if finding.get("suggestion") is not None:
record["suggestion"] = finding["suggestion"]
return json.dumps(record, ensure_ascii=False, separators=(",", ":"))
@ -889,14 +992,19 @@ def sarif_uri(path: str) -> str:
return quote(path, safe="/")
def sarif_location(path: str, line: int) -> Dict[str, Any]:
def sarif_location(
path: str, start_line: int, end_line: Optional[int] = None
) -> Dict[str, Any]:
region: Dict[str, Any] = {"startLine": start_line}
if end_line is not None and end_line != start_line:
region["endLine"] = end_line
return {
"physicalLocation": {
"artifactLocation": {
"uri": sarif_uri(path),
"uriBaseId": "%SRCROOT%",
},
"region": {"startLine": line},
"region": region,
}
}
@ -970,22 +1078,36 @@ def sarif_result(
)
if finding["verdict"] == "UNVERIFIED":
message += "\n\n" + SARIF_UNVERIFIED_NOTE
location = finding["location"]
start_line = location["start_line"]
end_line = location["end_line"]
fingerprint_anchor = (
location["existing_code"]
or f"{start_line}:{end_line}"
)
fingerprint = hashlib.sha256(
(
f"{finding['file']}\0{finding['issue_type']}\0"
f"{fingerprint_anchor}"
).encode("utf-8")
).hexdigest()
result: Dict[str, Any] = {
"ruleId": primary,
"ruleIndex": rule_indices[primary],
"level": SARIF_LEVELS[finding["severity"]],
"message": {"text": message},
"locations": [sarif_location(finding["file"], finding["line"])],
"locations": [
sarif_location(finding["file"], start_line, end_line)
],
"partialFingerprints": {
"codeReviewIdentity/v1": (
f"{finding['file']}:{finding['line']}:{finding['category']}"
)
"codeReviewIdentity/v2": fingerprint
},
"properties": {
key: finding[key]
for key in (
"id",
"category",
"issue_type",
"severity",
"confidence",
"verdict",
@ -997,6 +1119,32 @@ def sarif_result(
)
},
}
suggestion = finding.get("suggestion")
if isinstance(suggestion, dict):
result["fixes"] = [
{
"description": {"text": "Apply the verified suggested change"},
"artifactChanges": [
{
"artifactLocation": {
"uri": sarif_uri(finding["file"]),
"uriBaseId": "%SRCROOT%",
},
"replacements": [
{
"deletedRegion": {
"startLine": start_line,
"endLine": end_line,
},
"insertedContent": {
"text": suggestion["replacement_code"]
},
}
],
}
],
}
]
anchors = finding.get("anchors") or []
if anchors:
result["relatedLocations"] = [

View file

@ -7,7 +7,7 @@ report.
## Canonical files
The canonical bundle is schema version 3.
The canonical bundle is schema version 4.
- `review-manifest.json` identifies the review, target, revision, request,
completion status, counts, and canonical file set. At the rule-mapped
@ -22,8 +22,10 @@ The canonical bundle is schema version 3.
carries the candidate's applicable `rule_ids` (empty outside the
rule-mapped tiers).
- `findings.json` contains only the reportable subset. It is the authoritative
finding list. Each reported finding also carries a `code` excerpt, which the
ledger's candidate records do not, and its `rule_ids`.
finding list. Each reported finding carries its orthogonal `issue_type`, an
engine-derived `location` with the exact original code and start/end lines,
a highlighted `code` excerpt, and its `rule_ids`. A verified replacement is
stored as optional `suggestion.replacement_code`.
- `coverage.json` records what the review dispatched, what returned, what was
rejected for failing the finding contract, and what a cap dropped. At the
rule-mapped tiers it also records the authoritative target-file list, the
@ -39,9 +41,10 @@ The canonical bundle is schema version 3.
and cap drops -- also emitted into the workflow context so calibration
across many runs can read it from the event log.
- `votes.jsonl` contains one record for each dispatched verification, with the
exact claim shown to the verifier (including its claimed `rule_ids` and the
file's effective checks at the rule-mapped tiers), plus its verdict and
reasoning when it completed.
exact claim shown to the verifier (including its location, proposed
replacement, claimed `rule_ids`, and the file's effective checks at the
rule-mapped tiers), plus its verdict and reasoning when it completed. A vote
over a proposed replacement also carries `suggestion_valid`.
## Derived files
@ -117,6 +120,12 @@ verifier's instructions, never the arithmetic. At `low`, verification is
skipped by design: findings carry `verdict: "UNVERIFIED"` and the reports say
so.
A proposed replacement is independent of the keep verdict. The verifier must
return `suggestion_valid: true`, the engine must be able to read the exact
original range from the unchanged reviewed tree, and the replacement must
differ from it. Low-effort findings never carry suggestions because they have
no independent verification.
## Deduplication and ranking
A candidate's identity is its normalized file, line, and category. Two angles
@ -146,12 +155,16 @@ categories (`reuse`, `simplification`, `efficiency`, `altitude`,
report count, then confidence, then file and line. The report cap cuts from
the bottom, and everything cut is in the ledger as `deferred-by-cap`.
## Source excerpts
## Locations, source excerpts, and suggestions
A finding's `code` excerpt is not agent-quoted: `final-tally` reads the lines
around the finding from the reviewed tree, so the line numbers are the tree's
own and no agent transcribes them. The excerpt is omitted when the file is
unreadable, binary, oversized, or the line is out of range.
A finder supplies a bounded `start_line`/`end_line` range. `final-tally` reads
that range from the reviewed tree and records its exact text as
`location.existing_code`; the agent never supplies the canonical original
text. The adjacent `code` excerpt is read the same way and highlights the
complete range. Exact text and the excerpt are omitted when the file is
unreadable, binary, oversized, or the range is invalid. A proposed
`suggestion_code` becomes canonical only after the verifier approves it and
the exact original text is available.
## HTML rendering
@ -174,11 +187,14 @@ validated bundle:
covers every check ID any result cites.
- Severity maps to level: `HIGH` is `error`, `MEDIUM` is `warning`, `LOW` is
`note`.
- Each result's location is the finding's file and line relative to
- Each result's location is the finding's file and line range relative to
`%SRCROOT%`; anchors become related locations. The finding's identity,
category, severity, confidence, verdict, reports, reporters, rule IDs,
anchors, and source are result properties, and the file, line, and
category form a stable partial fingerprint.
category, issue type, severity, confidence, verdict, reports, reporters,
rule IDs, anchors, and source are result properties. File, issue type, and
exact original code form a stable hashed partial fingerprint, falling back
to the line range when source text is unavailable.
- A verified suggestion becomes a SARIF `fix` that replaces the complete
location range.
- An `UNVERIFIED` finding (the `low` tier) says so in its result message and
carries the verdict in its properties.
- The run's automation ID is `code-review/<mode>`, and the run properties

View file

@ -203,6 +203,19 @@ function renderCode(code) {
return details;
}
function renderSuggestedChange(finding) {
if (!finding.suggestion || !finding.location ||
!finding.location.existing_code) return null;
const details = el("details");
details.appendChild(el("summary", null, "Suggested change"));
details.appendChild(el("p", "label", "Before"));
details.appendChild(el("pre", "code", finding.location.existing_code));
details.appendChild(el("p", "label", "After"));
details.appendChild(el("pre", "code",
finding.suggestion.replacement_code || ""));
return details;
}
function renderFinding(finding) {
const card = el("article", "finding");
const head = el("div", "finding-head");
@ -213,6 +226,7 @@ function renderFinding(finding) {
chips.appendChild(el("span", "chip sev-" + finding.severity,
finding.severity + " severity"));
chips.appendChild(el("span", "chip", finding.category));
chips.appendChild(el("span", "chip", finding.issue_type));
if ((finding.rule_ids || []).length) {
chips.appendChild(el("span", "chip",
"rule " + finding.rule_ids.join(", ")));
@ -222,7 +236,11 @@ function renderFinding(finding) {
chips.appendChild(el("span", "chip",
finding.reports + " report(s): " + (finding.reporters || []).join(", ")));
card.appendChild(chips);
card.appendChild(el("p", "location", finding.file + ":" + finding.line));
const loc = finding.location || {};
const span = loc.start_line && loc.end_line && loc.start_line !== loc.end_line
? loc.start_line + "-" + loc.end_line
: (loc.end_line || finding.line);
card.appendChild(el("p", "location", finding.file + ":" + span));
if ((finding.anchors || []).length) {
card.appendChild(el("p", "location", "Also reported at " +
finding.anchors.map(a => a.file + ":" + a.line + " (" + a.category +
@ -242,6 +260,8 @@ function renderFinding(finding) {
verifier.appendChild(el("p", null, finding.verdict_reasoning));
card.appendChild(verifier);
}
const suggestion = renderSuggestedChange(finding);
if (suggestion) card.appendChild(suggestion);
const code = renderCode(finding.code);
if (code) card.appendChild(code);
return card;