Skip to content

Commit b3ac704

Browse files
EricCogenCopilot
andauthored
feat(cli): rich PR review summary body with Why/Action/Evidence (v2.0.3) (#144)
When a finding's evidence line falls outside the PR diff, GitHub's inline-comment API rejects the anchor, so GauntletCI falls back to a top-level review body. Previously that fallback was a single bullet line per group, missing the rationale that makes a finding actionable. BuildReviewBody now wraps each summary group in a collapsible <details> block whose body is the same Why/Action/Evidence/Confidence/Severity layout used by inline comments and the run-log report. Result: parity between what reviewers see in the PR conversation and what they see in the workflow run output. Adds 4 new unit tests covering empty/inline-only/single-group/multi-group review bodies. Co-authored-by: Eric Cogen <ericcogen@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 76f2a9f commit b3ac704

3 files changed

Lines changed: 97 additions & 4 deletions

File tree

src/GauntletCI.Cli/GauntletCI.Cli.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
<PackAsTool>true</PackAsTool>
4040
<ToolCommandName>gauntletci</ToolCommandName>
4141
<PackageId>GauntletCI</PackageId>
42-
<Version>2.0.2</Version>
42+
<Version>2.0.3</Version>
4343
<Authors>Eric Cogen</Authors>
4444
<Description>GauntletCI is a .NET pre-commit and PR integration gate that evaluates diffs against 42 behavioral-change rules — catching unvalidated changes, missing tests, async deadlock risk, security risks, and resource lifecycle violations before code is committed or merged. Optional local LLM enrichment via Ollama. Built-in MCP server for AI assistant integration. Fully offline — no data leaves your machine. Open source CodeRabbit alternative with deterministic, auditable findings.</Description>
4545
<PackageTags>pre-commit;code-review;llm;static-analysis;behavioral-analysis;diff-analysis;git-hooks;ci;dotnet;csharp;mcp;pr-review;risk-detection;behavioral-testing;security-risk;async-safety;ollama;ai-code-review</PackageTags>
46-
<PackageReleaseNotes>v2.0.2: Findings sharing (RuleId, FilePath) are grouped into one console block / annotation / PR comment / check annotation. GitHub Checks output now populates a rich markdown summary; annotations include multi-line Why/Action; PR review comments use the run-log layout. Diagnostic logging on writer success. v2.0.1: New CLI flags (--github-pr-comments, --github-checks, --notify-slack, --notify-teams, --with-coverage, --with-ticket-context, --pr-comment-suggest, --no-baseline, --show-context, --severity, --verbose), config blocks (ci, notifications, output, ticketProvider), network license validation, Phi-4 Mini migration. v2.0.0: 42 built-in rules, local LLM expert distillery, MCP server, Ollama embedding engine, corpus pipeline, GitHub Actions annotations, full audit log.</PackageReleaseNotes>
46+
<PackageReleaseNotes>v2.0.3: PR review summary body (the fallback used when finding evidence references lines outside the PR diff) now embeds the full Why/Action/Evidence/Confidence/Severity body in collapsible &lt;details&gt; sections, matching the inline-comment and run-log layouts. v2.0.2: Findings sharing (RuleId, FilePath) are grouped into one console block / annotation / PR comment / check annotation. GitHub Checks output now populates a rich markdown summary; annotations include multi-line Why/Action; PR review comments use the run-log layout. Diagnostic logging on writer success. v2.0.1: New CLI flags (--github-pr-comments, --github-checks, --notify-slack, --notify-teams, --with-coverage, --with-ticket-context, --pr-comment-suggest, --no-baseline, --show-context, --severity, --verbose), config blocks (ci, notifications, output, ticketProvider), network license validation, Phi-4 Mini migration. v2.0.0: 42 built-in rules, local LLM expert distillery, MCP server, Ollama embedding engine, corpus pipeline, GitHub Actions annotations, full audit log.</PackageReleaseNotes>
4747
<Copyright>Copyright © 2024 Eric Cogen</Copyright>
4848
<PackageProjectUrl>https://gauntletci.com</PackageProjectUrl>
4949
<PackageLicenseFile>LICENSE</PackageLicenseFile>

src/GauntletCI.Cli/Output/GitHubPrReviewWriter.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ public static string BuildReviewBody(List<GroupedFinding> summaryGroups, bool ha
354354
var sb = new StringBuilder();
355355
sb.AppendLine("**GauntletCI** found the following issues:");
356356
sb.AppendLine();
357+
sb.AppendLine("_These findings reference lines outside the PR diff, so they appear here instead of inline. Expand each entry for full evidence, rationale, and suggested action._");
358+
sb.AppendLine();
357359

358360
foreach (var g in summaryGroups)
359361
{
@@ -364,12 +366,18 @@ public static string BuildReviewBody(List<GroupedFinding> summaryGroups, bool ha
364366
? $" (`{g.FilePath}:{g.PrimaryLine}`)"
365367
: $" (`{g.FilePath}`)")
366368
: string.Empty;
367-
sb.AppendLine($"- **{g.RuleId}{g.RuleName}**{location}: {g.Summary}");
369+
370+
sb.AppendLine("<details>");
371+
sb.AppendLine($"<summary><strong>{g.RuleId}{g.RuleName}</strong>{location}: {g.Summary}</summary>");
372+
sb.AppendLine();
373+
sb.AppendLine(BuildCommentBody(g));
374+
sb.AppendLine();
375+
sb.AppendLine("</details>");
376+
sb.AppendLine();
368377
}
369378

370379
if (hasInlineComments)
371380
{
372-
sb.AppendLine();
373381
sb.Append("Additional findings are posted as inline comments on the diff.");
374382
}
375383

src/GauntletCI.Tests/GitHubPrReviewWriterTests.cs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,91 @@ public void BuildCommentBody_WithCoverageNote_IncludesCoverageSection()
183183
Assert.Contains("No test coverage", body);
184184
}
185185

186+
// --- BuildReviewBody ---
187+
188+
private static GroupedFinding MakeGroup(
189+
string ruleId = "GCI0016",
190+
string ruleName = "Concurrency Rule",
191+
string summary = "Static mutable field detected",
192+
string filePath = "src/Foo.cs",
193+
int primaryLine = 12,
194+
string whyItMatters = "Mutable static fields are shared across threads.",
195+
string suggestedAction = "Use Interlocked or readonly.",
196+
string evidence = "Line 12: private static long _count;") => new()
197+
{
198+
RuleId = ruleId,
199+
RuleName = ruleName,
200+
Summary = summary,
201+
FilePath = filePath,
202+
PrimaryLine = primaryLine,
203+
Lines = new[] { primaryLine },
204+
Evidence = new[] { evidence },
205+
WhyItMatters = whyItMatters,
206+
SuggestedAction = suggestedAction,
207+
Confidence = Confidence.Medium,
208+
Severity = RuleSeverity.Warn,
209+
Count = 1,
210+
};
211+
212+
[Fact]
213+
public void BuildReviewBody_NoSummaryGroups_NoInline_ReturnsEmpty()
214+
{
215+
var body = GitHubPrReviewWriter.BuildReviewBody(new(), hasInlineComments: false);
216+
Assert.Equal(string.Empty, body);
217+
}
218+
219+
[Fact]
220+
public void BuildReviewBody_NoSummaryGroups_HasInline_ReturnsPointerNote()
221+
{
222+
var body = GitHubPrReviewWriter.BuildReviewBody(new(), hasInlineComments: true);
223+
Assert.Contains("inline comments", body);
224+
}
225+
226+
[Fact]
227+
public void BuildReviewBody_SummaryGroup_EmbedsRichDetailsBlock()
228+
{
229+
var groups = new List<GroupedFinding> { MakeGroup() };
230+
var body = GitHubPrReviewWriter.BuildReviewBody(groups, hasInlineComments: false);
231+
232+
// Top-level header preserved
233+
Assert.Contains("**GauntletCI** found the following issues:", body);
234+
// Details/summary scaffolding present
235+
Assert.Contains("<details>", body);
236+
Assert.Contains("</details>", body);
237+
Assert.Contains("<summary>", body);
238+
// Rich body: rule id, evidence, why, action, confidence/severity all present (matches inline format)
239+
Assert.Contains("GCI0016", body);
240+
Assert.Contains("**Evidence:**", body);
241+
Assert.Contains("Why it matters", body);
242+
Assert.Contains("Suggested action", body);
243+
Assert.Contains("Confidence:", body);
244+
Assert.Contains("Severity:", body);
245+
}
246+
247+
[Fact]
248+
public void BuildReviewBody_MultipleSummaryGroups_EmitsOneDetailsPerGroup()
249+
{
250+
var groups = new List<GroupedFinding>
251+
{
252+
MakeGroup(ruleId: "GCI0010", ruleName: "Hardcoding", summary: "Hardcoded conn string"),
253+
MakeGroup(ruleId: "GCI0042", ruleName: "TODO Detection", summary: "TODO in payment flow"),
254+
};
255+
var body = GitHubPrReviewWriter.BuildReviewBody(groups, hasInlineComments: false);
256+
257+
var detailsCount = System.Text.RegularExpressions.Regex.Matches(body, "<details>").Count;
258+
Assert.Equal(2, detailsCount);
259+
Assert.Contains("GCI0010", body);
260+
Assert.Contains("GCI0042", body);
261+
}
262+
263+
[Fact]
264+
public void BuildReviewBody_HasInlineAndSummary_AppendsInlinePointer()
265+
{
266+
var groups = new List<GroupedFinding> { MakeGroup() };
267+
var body = GitHubPrReviewWriter.BuildReviewBody(groups, hasInlineComments: true);
268+
Assert.Contains("inline comments on the diff", body);
269+
}
270+
186271
// --- ResolvePrNumber ---
187272

188273
[Fact]

0 commit comments

Comments
 (0)