From 9e3f30391cbc37e5593e861fcbb557c722c45bdc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:13:45 +0000 Subject: [PATCH 1/2] Initial plan From d0d5c16cb4bdc5a423243024796512037827875e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:24:42 +0000 Subject: [PATCH 2/2] feat: separate issue and PR summaries into distinct AI-generated fields - Replace single `Summary` field with `IssueSummary` and `PRSummary` in types.Review - Update AI prompt to request separate fields with explicit content constraints: issueSummary derived ONLY from linked issues prSummary derived ONLY from PR diff/changed files - Remove splitIssueAndPRSummaries/splitParagraphs helpers from render.go - Update renderMarkdown to use the new fields directly - Update whatIfReview to set the new fields - Update all tests to use the new field names and JSON keys Co-authored-by: richardthombs <1511599+richardthombs@users.noreply.github.com> --- cmd/prr/render.go | 50 ++-------------------------- cmd/prr/review_render_test.go | 36 +++++++++++++------- internal/engine/engine.go | 24 +++++++------- internal/engine/engine_test.go | 21 +++++++----- internal/types/review.go | 20 ++++++++---- internal/types/review_test.go | 60 +++++++++++++++++++++++++--------- 6 files changed, 109 insertions(+), 102 deletions(-) diff --git a/cmd/prr/render.go b/cmd/prr/render.go index 11b4b6b..e527831 100644 --- a/cmd/prr/render.go +++ b/cmd/prr/render.go @@ -13,8 +13,6 @@ import ( func renderMarkdown(review types.Review, prID int, prURL string, issues []types.RelatedIssue) string { var builder strings.Builder - issueSummary, prSummary := splitIssueAndPRSummaries(review.Summary) - builder.WriteString("## Summary\n") builder.WriteString(renderPRLine(prID, prURL)) builder.WriteString("\n") @@ -22,11 +20,11 @@ func renderMarkdown(review types.Review, prID int, prURL string, issues []types. builder.WriteString("\n\n") builder.WriteString("### Issue summary\n") - builder.WriteString(issueSummary) + builder.WriteString(review.IssueSummary) builder.WriteString("\n\n") builder.WriteString("### PR summary\n") - builder.WriteString(prSummary) + builder.WriteString(review.PRSummary) builder.WriteString("\n\n") builder.WriteString("## Review\n") @@ -152,47 +150,3 @@ func buildPRURL(repoURL string, prID int) string { parsed.Path = strings.TrimSuffix(parsed.Path, "/") + "/pull/" + strconv.Itoa(prID) return parsed.String() } - -func splitIssueAndPRSummaries(summary string) (string, string) { - paragraphs := splitParagraphs(summary) - if len(paragraphs) == 0 { - return "No issue summary provided.", "No PR summary provided." - } - - issueParas := paragraphs - if len(issueParas) > 2 { - issueParas = issueParas[:2] - } - - prStart := len(issueParas) - prParas := []string{} - if prStart < len(paragraphs) { - prParas = paragraphs[prStart:] - } - if len(prParas) > 2 { - prParas = prParas[:2] - } - if len(prParas) == 0 { - prParas = issueParas - } - - return strings.Join(issueParas, "\n\n"), strings.Join(prParas, "\n\n") -} - -func splitParagraphs(text string) []string { - trimmed := strings.TrimSpace(text) - if trimmed == "" { - return nil - } - - raw := strings.Split(trimmed, "\n\n") - paragraphs := make([]string, 0, len(raw)) - for _, para := range raw { - cleaned := strings.TrimSpace(para) - if cleaned == "" { - continue - } - paragraphs = append(paragraphs, cleaned) - } - return paragraphs -} diff --git a/cmd/prr/review_render_test.go b/cmd/prr/review_render_test.go index bde7a05..84c86e0 100644 --- a/cmd/prr/review_render_test.go +++ b/cmd/prr/review_render_test.go @@ -88,8 +88,11 @@ func TestReviewCommandEmitsStructuredJSONAndKeepsDiagnosticsOffStdout(t *testing t.Fatalf("expected valid review JSON, got %v", err) } - if payload["summary"] == "" { - t.Fatalf("expected summary field in review JSON") + if payload["issueSummary"] == "" { + t.Fatalf("expected issueSummary field in review JSON") + } + if payload["prSummary"] == "" { + t.Fatalf("expected prSummary field in review JSON") } if _, ok := payload["risk"].(map[string]any); !ok { t.Fatalf("expected risk object in review JSON") @@ -209,8 +212,11 @@ func TestReviewCommandAcceptsPRURLArgument(t *testing.T) { if err := json.Unmarshal(bytes.TrimSpace(stdout.Bytes()), &payload); err != nil { t.Fatalf("expected valid review JSON, got %v", err) } - if payload["summary"] == "" { - t.Fatalf("expected summary in review JSON") + if payload["issueSummary"] == "" { + t.Fatalf("expected issueSummary in review JSON") + } + if payload["prSummary"] == "" { + t.Fatalf("expected prSummary in review JSON") } } @@ -264,8 +270,11 @@ func TestReviewCommandAcceptsPipedCheckoutJSONWithoutArgs(t *testing.T) { if err := json.Unmarshal(bytes.TrimSpace(stdout.Bytes()), &payload); err != nil { t.Fatalf("expected valid review JSON, got %v", err) } - if payload["summary"] == "" { - t.Fatalf("expected summary in review JSON") + if payload["issueSummary"] == "" { + t.Fatalf("expected issueSummary in review JSON") + } + if payload["prSummary"] == "" { + t.Fatalf("expected prSummary in review JSON") } } @@ -325,8 +334,11 @@ func TestReviewCommandBypassesSetupWithAuthoritativeCheckoutJSON(t *testing.T) { if err := json.Unmarshal(bytes.TrimSpace(stdout.Bytes()), &payload); err != nil { t.Fatalf("expected valid review JSON, got %v", err) } - if payload["summary"] == "" { - t.Fatalf("expected summary in review JSON") + if payload["issueSummary"] == "" { + t.Fatalf("expected issueSummary in review JSON") + } + if payload["prSummary"] == "" { + t.Fatalf("expected prSummary in review JSON") } } @@ -455,7 +467,8 @@ func TestReviewCommandEmitsDeterministicJSONShape(t *testing.T) { reviewEngineFactory = func() engine.ReviewEngine { return reviewEngineFunc(func(_ context.Context, _ engine.ReviewInput) (types.Review, error) { return types.Review{ - Summary: "Deterministic review", + IssueSummary: "Deterministic issue summary", + PRSummary: "Deterministic PR summary", Risk: types.Risk{ Score: 0.25, Reasons: []string{"Stable fixture"}, @@ -484,7 +497,7 @@ func TestReviewCommandEmitsDeterministicJSONShape(t *testing.T) { t.Fatalf("expected review command to succeed, got %v", err) } - const expected = "{\"summary\":\"Deterministic review\",\"risk\":{\"score\":0.25,\"reasons\":[\"Stable fixture\"]},\"findings\":[{\"id\":\"F001\",\"file\":\"a.go\",\"line\":7,\"severity\":\"important\",\"category\":\"tests\",\"message\":\"Add coverage\",\"suggestion\":\"Add assertions\"}],\"checklist\":[\"Run CI\"]}\n" + const expected = "{\"issueSummary\":\"Deterministic issue summary\",\"prSummary\":\"Deterministic PR summary\",\"risk\":{\"score\":0.25,\"reasons\":[\"Stable fixture\"]},\"findings\":[{\"id\":\"F001\",\"file\":\"a.go\",\"line\":7,\"severity\":\"important\",\"category\":\"tests\",\"message\":\"Add coverage\",\"suggestion\":\"Add assertions\"}],\"checklist\":[\"Run CI\"]}\n" if stdout.String() != expected { t.Fatalf("expected deterministic JSON output.\nwant: %q\n got: %q", expected, stdout.String()) } @@ -577,7 +590,8 @@ func (f reviewEngineFunc) Review(ctx context.Context, input engine.ReviewInput) func deterministicReview() types.Review { return types.Review{ - Summary: "Deterministic review", + IssueSummary: "Deterministic issue summary", + PRSummary: "Deterministic PR summary", Risk: types.Risk{ Score: 0.25, Reasons: []string{"Stable fixture"}, diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 20b5371..328028b 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -207,7 +207,8 @@ func marshalBundlePayload(bundle types.BundleV1, reviewInstructions string) (str 2) Treat that JSON object as the complete review input. 3) Return ONLY valid JSON using this exact schema (no markdown fences or extra prose): { - "summary": string, + "issueSummary": string, + "prSummary": string, "risk": {"score": number, "reasons": string[]}, "findings": [ { @@ -222,14 +223,12 @@ func marshalBundlePayload(bundle types.BundleV1, reviewInstructions string) (str ], "checklist": string[] } -4) summary MUST be concise and deterministic with: - - Issue summary in at most 2 paragraphs total across all linked issues. - - PR summary in at most 2 paragraphs focused on what changed (not critique). - - If needed, separate issue and PR summary blocks using exactly one blank line. -5) findings must support final markdown grouping under headings: +4) issueSummary MUST be derived ONLY from the linked issues in the bundle. It MUST NOT contain information from the PR diff or changed files. Summarise what problem(s) the issues describe in at most 2 paragraphs. If there are no linked issues, set issueSummary to "No linked issues." +5) prSummary MUST be derived ONLY from the PR diff and changed files. It MUST NOT contain information from the linked issues. Summarise what changes have been made in at most 2 paragraphs. +6) findings must support final markdown grouping under headings: Blocker, Important, Suggestion, Nitpick (map Nitpick to severity "nit"). -6) risk.score MUST be a decimal number between 0 and 1 inclusive. -7) Be deterministic and concise.`) +7) risk.score MUST be a decimal number between 0 and 1 inclusive. +8) Be deterministic and concise.`) stdinEnvelope := strings.Join([]string{ ri, @@ -305,13 +304,16 @@ func extractJSONObject(raw string) (string, error) { } func whatIfReview(bundle types.BundleV1) types.Review { - summary := "what-if: review agent was not executed" + issueSummary := "what-if: review agent was not executed" + prSummary := "what-if: review agent was not executed" if bundle.PRID > 0 { - summary = fmt.Sprintf("what-if: review agent was not executed for PR #%d", bundle.PRID) + issueSummary = fmt.Sprintf("what-if: review agent was not executed for PR #%d", bundle.PRID) + prSummary = fmt.Sprintf("what-if: review agent was not executed for PR #%d", bundle.PRID) } return types.Review{ - Summary: summary, + IssueSummary: issueSummary, + PRSummary: prSummary, Risk: types.Risk{ Score: 0, Reasons: []string{"What-if mode skips external agent execution."}, diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index d664f62..c45ad09 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -37,7 +37,7 @@ func TestCLIAdapterBuildsCommandWithStdinEnvelopeModelAndWorkDir(t *testing.T) { capturedArgs = append([]string{}, args...) capturedCwd = cwd capturedStdin = stdinPayload - return commandResult{Stdout: `{"summary":"ok","risk":{"score":0.1,"reasons":["low"]},"findings":[],"checklist":["run tests"]}`}, nil + return commandResult{Stdout: `{"issueSummary":"issue ok","prSummary":"pr ok","risk":{"score":0.1,"reasons":["low"]},"findings":[],"checklist":["run tests"]}`}, nil }}, } @@ -106,7 +106,7 @@ func TestCLIAdapterWhatIfSkipsExecutionAndReturnsStructuredReview(t *testing.T) if called { t.Fatalf("expected no external execution in what-if mode") } - if review.Summary == "" || review.Risk.Reasons == nil || review.Checklist == nil { + if review.IssueSummary == "" || review.PRSummary == "" || review.Risk.Reasons == nil || review.Checklist == nil { t.Fatalf("expected structured review response in what-if mode") } if len(logs) == 0 { @@ -122,7 +122,7 @@ func TestCLIAdapterParsesMixedTextAndJSONOutput(t *testing.T) { adapter := &CLIAgentAdapter{ config: DefaultAgentConfig(), runner: fakeRunner{run: func(_ context.Context, _ string, _ []string, _ string, _ string) (commandResult, error) { - return commandResult{Stdout: "info line\n{\"summary\":\"ok\",\"risk\":{\"score\":0.1,\"reasons\":[\"low\"]},\"findings\":[],\"checklist\":[\"run tests\"]}\ntrailing"}, nil + return commandResult{Stdout: "info line\n{\"issueSummary\":\"issue ok\",\"prSummary\":\"pr ok\",\"risk\":{\"score\":0.1,\"reasons\":[\"low\"]},\"findings\":[],\"checklist\":[\"run tests\"]}\ntrailing"}, nil }}, } @@ -133,8 +133,11 @@ func TestCLIAdapterParsesMixedTextAndJSONOutput(t *testing.T) { if err != nil { t.Fatalf("expected parser to extract JSON object, got %v", err) } - if review.Summary != "ok" { - t.Fatalf("expected parsed summary 'ok', got %q", review.Summary) + if review.IssueSummary != "issue ok" { + t.Fatalf("expected parsed issueSummary 'issue ok', got %q", review.IssueSummary) + } + if review.PRSummary != "pr ok" { + t.Fatalf("expected parsed prSummary 'pr ok', got %q", review.PRSummary) } } @@ -142,7 +145,7 @@ func TestCLIAdapterRejectsOutOfRangeRiskScore(t *testing.T) { adapter := &CLIAgentAdapter{ config: DefaultAgentConfig(), runner: fakeRunner{run: func(_ context.Context, _ string, _ []string, _ string, _ string) (commandResult, error) { - return commandResult{Stdout: `{"summary":"ok","risk":{"score":4,"reasons":["high churn"]},"findings":[{"id":"F001","file":"a.go","line":7,"severity":"important","category":"tests","message":"m","suggestion":"s"}],"checklist":["run tests"]}`}, nil + return commandResult{Stdout: `{"issueSummary":"issue ok","prSummary":"pr ok","risk":{"score":4,"reasons":["high churn"]},"findings":[{"id":"F001","file":"a.go","line":7,"severity":"important","category":"tests","message":"m","suggestion":"s"}],"checklist":["run tests"]}`}, nil }}, } @@ -193,7 +196,7 @@ func TestCLIAdapterVerboseLogsCopilotOutput(t *testing.T) { config: DefaultAgentConfig(), runner: fakeRunner{run: func(_ context.Context, _ string, _ []string, _ string, _ string) (commandResult, error) { return commandResult{ - Stdout: "{\"summary\":\"ok\",\"risk\":{\"score\":0.1,\"reasons\":[\"low\"]},\"findings\":[],\"checklist\":[\"run tests\"]}", + Stdout: "{\"issueSummary\":\"issue ok\",\"prSummary\":\"pr ok\",\"risk\":{\"score\":0.1,\"reasons\":[\"low\"]},\"findings\":[],\"checklist\":[\"run tests\"]}", Stderr: "copilot diagnostic", }, nil }}, @@ -347,7 +350,7 @@ func TestCLIAdapterUsesCustomReviewInstructions(t *testing.T) { config: DefaultAgentConfig(), runner: fakeRunner{run: func(_ context.Context, _ string, _ []string, _ string, stdinPayload string) (commandResult, error) { capturedStdin = stdinPayload - return commandResult{Stdout: `{"summary":"ok","risk":{"score":0.1,"reasons":["low"]},"findings":[],"checklist":["run tests"]}`}, nil + return commandResult{Stdout: `{"issueSummary":"issue ok","prSummary":"pr ok","risk":{"score":0.1,"reasons":["low"]},"findings":[],"checklist":["run tests"]}`}, nil }}, } @@ -377,7 +380,7 @@ func TestCLIAdapterUsesDefaultReviewInstructionsWhenEmpty(t *testing.T) { config: DefaultAgentConfig(), runner: fakeRunner{run: func(_ context.Context, _ string, _ []string, _ string, stdinPayload string) (commandResult, error) { capturedStdin = stdinPayload - return commandResult{Stdout: `{"summary":"ok","risk":{"score":0.1,"reasons":["low"]},"findings":[],"checklist":["run tests"]}`}, nil + return commandResult{Stdout: `{"issueSummary":"issue ok","prSummary":"pr ok","risk":{"score":0.1,"reasons":["low"]},"findings":[],"checklist":["run tests"]}`}, nil }}, } diff --git a/internal/types/review.go b/internal/types/review.go index 4b19585..97c7e99 100644 --- a/internal/types/review.go +++ b/internal/types/review.go @@ -29,10 +29,11 @@ type Finding struct { } type Review struct { - Summary string `json:"summary"` - Risk Risk `json:"risk"` - Findings []Finding `json:"findings"` - Checklist []string `json:"checklist"` + IssueSummary string `json:"issueSummary"` + PRSummary string `json:"prSummary"` + Risk Risk `json:"risk"` + Findings []Finding `json:"findings"` + Checklist []string `json:"checklist"` } func NormalizeAndValidateReviewOutput(input Review) (Review, error) { @@ -50,9 +51,14 @@ func NormalizeAndValidateReview(input Review) (Review, error) { func validateReview(input Review, wrap wrapFunc, allowMissingID bool, validateRiskRange bool, label string) (Review, error) { review := input - review.Summary = strings.TrimSpace(review.Summary) - if review.Summary == "" { - return Review{}, wrap(label+" is missing summary", nil) + review.IssueSummary = strings.TrimSpace(review.IssueSummary) + if review.IssueSummary == "" { + return Review{}, wrap(label+" is missing issueSummary", nil) + } + + review.PRSummary = strings.TrimSpace(review.PRSummary) + if review.PRSummary == "" { + return Review{}, wrap(label+" is missing prSummary", nil) } if validateRiskRange { diff --git a/internal/types/review_test.go b/internal/types/review_test.go index 0a12c29..1858b35 100644 --- a/internal/types/review_test.go +++ b/internal/types/review_test.go @@ -7,7 +7,8 @@ import ( func TestNormalizeAndValidateReviewOutputRejectsRiskScoreOutsideZeroToOne(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 4, Reasons: []string{"r"}}, Findings: []Finding{{ File: "a.go", @@ -26,7 +27,8 @@ func TestNormalizeAndValidateReviewOutputRejectsRiskScoreOutsideZeroToOne(t *tes func TestNormalizeAndValidateReviewOutputStillValidatesStructure(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 4, Reasons: []string{"r"}}, Findings: []Finding{{ File: "a.go", @@ -45,7 +47,8 @@ func TestNormalizeAndValidateReviewOutputStillValidatesStructure(t *testing.T) { func TestValidateReviewInputKeepsStrictRiskScale(t *testing.T) { _, err := ValidateReviewInput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 4, Reasons: []string{"r"}}, Findings: []Finding{{ ID: "F001", @@ -63,23 +66,40 @@ func TestValidateReviewInputKeepsStrictRiskScale(t *testing.T) { } } -func TestValidateReviewRejectsMissingSummary(t *testing.T) { +func TestValidateReviewRejectsMissingIssueSummary(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ + PRSummary: "ok", Risk: Risk{Score: 0.5, Reasons: []string{"r"}}, Findings: []Finding{}, Checklist: []string{}, }) if err == nil { - t.Fatalf("expected error for missing summary") + t.Fatalf("expected error for missing issueSummary") } - if !strings.Contains(err.Error(), "missing summary") { - t.Fatalf("expected missing summary diagnostic, got %v", err) + if !strings.Contains(err.Error(), "missing issueSummary") { + t.Fatalf("expected missing issueSummary diagnostic, got %v", err) + } +} + +func TestValidateReviewRejectsMissingPRSummary(t *testing.T) { + _, err := NormalizeAndValidateReviewOutput(Review{ + IssueSummary: "ok", + Risk: Risk{Score: 0.5, Reasons: []string{"r"}}, + Findings: []Finding{}, + Checklist: []string{}, + }) + if err == nil { + t.Fatalf("expected error for missing prSummary") + } + if !strings.Contains(err.Error(), "missing prSummary") { + t.Fatalf("expected missing prSummary diagnostic, got %v", err) } } func TestValidateReviewRejectsNilRiskReasons(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.5, Reasons: nil}, Findings: []Finding{}, Checklist: []string{}, @@ -94,7 +114,8 @@ func TestValidateReviewRejectsNilRiskReasons(t *testing.T) { func TestValidateReviewRejectsNilFindings(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.5, Reasons: []string{"r"}}, Findings: nil, Checklist: []string{}, @@ -109,7 +130,8 @@ func TestValidateReviewRejectsNilFindings(t *testing.T) { func TestValidateReviewRejectsMissingFindingFile(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.1, Reasons: []string{"low"}}, Findings: []Finding{{ Severity: "nit", @@ -129,7 +151,8 @@ func TestValidateReviewRejectsMissingFindingFile(t *testing.T) { func TestValidateReviewRejectsInvalidFindingCategory(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.1, Reasons: []string{"low"}}, Findings: []Finding{{ File: "a.go", @@ -151,7 +174,8 @@ func TestValidateReviewRejectsInvalidFindingCategory(t *testing.T) { func TestValidateReviewRejectsMissingFindingMessage(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.1, Reasons: []string{"low"}}, Findings: []Finding{{ File: "a.go", @@ -173,7 +197,8 @@ func TestValidateReviewRejectsMissingFindingMessage(t *testing.T) { func TestValidateReviewRejectsMissingFindingSuggestion(t *testing.T) { _, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.1, Reasons: []string{"low"}}, Findings: []Finding{{ File: "a.go", @@ -195,7 +220,8 @@ func TestValidateReviewRejectsMissingFindingSuggestion(t *testing.T) { func TestNormalizeAndValidateReviewOutputAutoAssignsIDAndLine(t *testing.T) { result, err := NormalizeAndValidateReviewOutput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.1, Reasons: []string{"low"}}, Findings: []Finding{{ File: "a.go", @@ -220,7 +246,8 @@ func TestNormalizeAndValidateReviewOutputAutoAssignsIDAndLine(t *testing.T) { func TestValidateReviewInputRejectsMissingFindingID(t *testing.T) { _, err := ValidateReviewInput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.2, Reasons: []string{"low"}}, Findings: []Finding{{ File: "a.go", @@ -243,7 +270,8 @@ func TestValidateReviewInputRejectsMissingFindingID(t *testing.T) { func TestValidateReviewInputRejectsZeroLine(t *testing.T) { _, err := ValidateReviewInput(Review{ - Summary: "ok", + IssueSummary: "ok", + PRSummary: "ok", Risk: Risk{Score: 0.2, Reasons: []string{"low"}}, Findings: []Finding{{ ID: "F001",