feat(review): walkthrough/risk_verdict/test_coverage schema

This commit is contained in:
claude
2026-08-22 00:36:50 +00:00
parent 72b77a96e0
commit 2e982846d9
10 changed files with 170 additions and 23 deletions
+40 -14
View File
@@ -109,7 +109,10 @@ Output STRICT JSON only — no prose, no markdown fences. Shape:
"fix": "one line: how to fix it",
"suggestion": "<exact replacement lines for that location, or empty string if you cannot produce safe replacement code>"
}
]
],
"walkthrough": ["2-6 short bullets, file- or change-grouped, plain prose"],
"risk_verdict": "Low|Medium|High|Critical risk: <one-line concrete reason>",
"test_coverage": "Tests added" | "Tests changed" | "No tests for behavioral change" | "No test files in repo"
}
Rules:
@@ -121,6 +124,15 @@ Rules:
Keep it minimal — just the changed lines, indented as they would appear in the
file. Leave it empty ("") if a safe textual replacement is not possible (e.g.
a missing test, an architectural note).
- `walkthrough`: 2-6 short bullets, file- or change-grouped, plain prose.
Default to `[]` when the diff is trivial. Backward compatible: parsers
default to `[]` if absent.
- `risk_verdict`: exactly one line. Lead with "Low|Medium|High|Critical risk:"
followed by a concrete reason. Default to `""` when not applicable.
Backward compatible: parsers default to `""` if absent.
- `test_coverage`: short string. One of "Tests added" / "Tests changed" /
"No tests for behavioral change" / "No test files in repo". Default to `""`
when not applicable. Backward compatible: parsers default to `""` if absent.
- Skip nitpicks, pure formatting, and praise. At most ~15 findings, highest
severity first.
- If the diff is clean, output: {"findings": []}
@@ -728,45 +740,59 @@ def salvage_summary(text: str, max_chars: int = SALVAGE_MAX_CHARS) -> str:
)
def parse_review_output(text: str) -> tuple[str, list[dict], list[str], list[str]]:
"""Parse the opengine's stdout into (summary, findings, summary_changes, risks).
def parse_review_output(
text: str,
) -> tuple[str, list[dict], list[str], list[str], list[str], str, str]:
"""Parse the opengine's stdout into a 7-tuple:
(summary, findings, summary_changes, risks,
walkthrough, risk_verdict, test_coverage)
Accepts `{"summary": "...", "summary_changes": [...], "risks": [...],
"findings": [...]}` (the opencode pragent agent), `{"findings": [...]}`,
or a bare `[...]` of finding dicts. `summary_changes` and `risks` default
to empty lists; older outputs without them still parse fine. Uses the
LAST fenced block (the pragent agent emits JSON as the final block), with
a tolerant fallback that scans for the last balanced object/array in the
prose tail. Never raises.
"walkthrough": [...], "risk_verdict": "...", "test_coverage": "...",
"findings": [...]}` (the opencode pragent agent), the legacy 4-field
shape, or a bare `[...]` of finding dicts. The three new fields
(`walkthrough`, `risk_verdict`, `test_coverage`) default to empty
list / empty strings when absent — older outputs and the bare-array
shape stay backward compatible.
Uses the LAST fenced block (the pragent agent emits JSON as the final
block), with a tolerant fallback that scans for the last balanced
object/array in the prose tail. Never raises.
"""
blob = _last_json_block(text)
if blob is None:
return "", [], [], []
return "", [], [], [], [], "", ""
try:
data = json.loads(blob)
except json.JSONDecodeError:
return "", [], [], []
return "", [], [], [], [], "", ""
summary = ""
summary_changes: list[str] = []
risks: list[str] = []
walkthrough: list[str] = []
risk_verdict = ""
test_coverage = ""
findings_raw = None
if isinstance(data, dict):
summary = str(data.get("summary", "") or "").strip()
summary_changes = _string_list(data.get("summary_changes"))
risks = _string_list(data.get("risks"))
walkthrough = _string_list(data.get("walkthrough"))
risk_verdict = str(data.get("risk_verdict", "") or "").strip()
test_coverage = str(data.get("test_coverage", "") or "").strip()
findings_raw = data.get("findings")
elif isinstance(data, list):
# Bare array: each item is a finding; no summary/sections.
findings_raw = data
else:
return "", [], [], []
return "", [], [], [], [], "", ""
out = []
if isinstance(findings_raw, list):
for f in findings_raw:
n = _normalize_finding(f)
if n is not None:
out.append(n)
return summary, out, summary_changes, risks
return summary, out, summary_changes, risks, walkthrough, risk_verdict, test_coverage
def _string_list(value) -> list[str]:
@@ -2028,7 +2054,7 @@ def review_pr(
compression_note=compression_note,
additional_context=additional_context,
)
review_summary, findings, summary_changes, risks = parse_review_output(stdout)
review_summary, findings, summary_changes, risks, _walkthrough, _risk_verdict, _test_coverage = parse_review_output(stdout)
if not findings and not review_summary:
# The findings JSON was missing or malformed. Don't discard the
# run: salvage the prose, keep the usage report (the label asked