-
Notifications
You must be signed in to change notification settings - Fork 0
fix(#2054): synthesize review body when findings contradict summary #78
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
dad1177
2ae83d8
178656f
c70a3de
4729cf0
09a478a
d38e97c
64aeee5
2235687
97c0747
4a1c03f
1cf4907
cca4481
9ff7aa0
b67e244
c70a5e7
925de84
3483e84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -87,6 +87,14 @@ has moved, a stale-head failure is posted instead.`, | |
| return fmt.Errorf("parsing review result: %w", err) | ||
| } | ||
|
|
||
| // Ensure the summary body is consistent with the | ||
| // verdict and findings. A stale or multi-run scenario | ||
| // can produce a body that says "No findings" while the | ||
| // action is request-changes with critical findings. | ||
| if patched := ensureBodyFindingsConsistency(&parsed); patched { | ||
| printer.StepWarn("Review body was inconsistent with findings — synthesized body from structured findings") | ||
| } | ||
|
|
||
| // CLI flag takes precedence over JSON field. | ||
| if headSHA != "" { | ||
| parsed.HeadSHA = headSHA | ||
|
|
@@ -506,6 +514,101 @@ func minimizeStaleReviews(ctx context.Context, client forge.Client, user string, | |
| printer.StepDone("Stale reviews minimized") | ||
| } | ||
|
|
||
| // ensureBodyFindingsConsistency detects when the review body omits | ||
| // significant findings despite the action mapping to REQUEST_CHANGES. | ||
| // Instead of regex-patching individual phrases (which is fragile — | ||
| // see #2055), it checks whether the body references any critical/high | ||
| // finding categories. If none are referenced, the body is replaced | ||
| // entirely with one synthesized from the structured findings array. | ||
| // Returns true if the body was replaced. | ||
| func ensureBodyFindingsConsistency(result *ReviewResult) bool { | ||
| if result == nil || len(result.Findings) == 0 { | ||
| return false | ||
| } | ||
|
|
||
| event, ok := reviewActionToEvent(result.Action) | ||
| if !ok || event != "REQUEST_CHANGES" { | ||
| return false | ||
| } | ||
|
|
||
| // Collect critical/high findings — these must be reflected in the body. | ||
| var significant []ReviewFinding | ||
| for _, f := range result.Findings { | ||
| switch strings.ToLower(f.Severity) { | ||
| case "critical", "high": | ||
| significant = append(significant, f) | ||
| } | ||
| } | ||
| if len(significant) == 0 { | ||
| return false | ||
| } | ||
|
|
||
| // Check whether the body already references any significant finding. | ||
| // A body is considered consistent if it mentions at least one | ||
| // critical/high finding's category. Categories are hyphenated tokens | ||
| // like "logic-error", "auth-bypass", "missing-test" — specific enough | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [info] false-negative Single-word categories can naturally appear in well-written review prose, causing the consistency check to consider the body consistent even when it does not actually describe the specific finding. Practical risk is negligible. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] edge-case The body-consistency check uses strings.Contains for category substring matching. While the code comment explains that hyphenated tokens are specific enough to avoid false matches, a theoretical false negative could occur with short category names. The failure mode is safe (unnecessary body replacement). |
||
| // to avoid false positives against natural prose. | ||
| bodyLower := strings.ToLower(result.Body) | ||
| for _, f := range significant { | ||
| if f.Category != "" && strings.Contains(bodyLower, strings.ToLower(f.Category)) { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| // Body does not reference any significant findings — synthesize a | ||
| // complete replacement from the structured findings array. | ||
| result.Body = synthesizeReviewBody(result.Findings) | ||
| return true | ||
| } | ||
|
|
||
| // synthesizeReviewBody builds a review comment body from the structured | ||
| // findings array, following the format defined in the pr-review skill | ||
| // (step 7). Findings are grouped by severity level with only populated | ||
| // severity sections included. | ||
| func synthesizeReviewBody(findings []ReviewFinding) string { | ||
| // Group findings by severity. | ||
| order := []string{"critical", "high", "medium", "low", "info"} | ||
| groups := make(map[string][]ReviewFinding) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] logic-error In synthesizeReviewBody, when a finding has an empty Category field, the output produces malformed Markdown: - [] — description. The ensureBodyFindingsConsistency function correctly skips empty-category findings during the consistency check, but synthesizeReviewBody includes all findings regardless of category presence, creating an inconsistency between the two functions. Suggested fix: Handle the empty-category case by omitting the [...] wrapper or using a fallback label like [uncategorized]. |
||
| for _, f := range findings { | ||
| sev := strings.ToLower(f.Severity) | ||
| groups[sev] = append(groups[sev], f) | ||
| } | ||
|
|
||
| var b strings.Builder | ||
| b.WriteString("## Review\n\n### Findings\n") | ||
|
|
||
| for _, sev := range order { | ||
| fs, ok := groups[sev] | ||
| if !ok { | ||
| continue | ||
| } | ||
| // Title-case the severity for the section heading. | ||
| heading := strings.ToUpper(sev[:1]) + sev[1:] | ||
| fmt.Fprintf(&b, "\n#### %s\n\n", heading) | ||
| for _, f := range fs { | ||
| b.WriteString("- **[") | ||
| b.WriteString(f.Category) | ||
| b.WriteString("]**") | ||
| if f.File != "" { | ||
| fmt.Fprintf(&b, " `%s", f.File) | ||
| if f.Line > 0 { | ||
| fmt.Fprintf(&b, ":%d", f.Line) | ||
| } | ||
| b.WriteString("`") | ||
| } | ||
| b.WriteString(" — ") | ||
| b.WriteString(strings.TrimSpace(f.Description)) | ||
| if f.Remediation != "" { | ||
| b.WriteString("\n Remediation: ") | ||
| b.WriteString(strings.TrimSpace(f.Remediation)) | ||
| } | ||
| b.WriteString("\n") | ||
| } | ||
| } | ||
|
|
||
| return b.String() | ||
| } | ||
|
|
||
| // parseReviewResult attempts to parse the body as a JSON ReviewResult. | ||
| // If parsing fails, treats the entire input as a plain-text body. | ||
| // Returns an error if the JSON is valid but the body field is empty | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[low] edge-case
When all critical/high findings have an empty Category field, the consistency check loop never matches (because of the f.Category != empty-string guard), and the body is unconditionally replaced. The synthesized body renders empty category brackets via synthesizeReviewBody.
Suggested fix: Either treat empty-category findings as inherently consistent (skip them in the significant slice), or handle the empty-category case in synthesizeReviewBody by omitting the brackets or using a fallback label like uncategorized.