Skip to content

fix(review): ground the PR summary in the whole change set (#335) - #451

Merged
devops-thiago merged 2 commits into
release/v0.6.0from
fix/335-summary-scope
Aug 8, 2026
Merged

fix(review): ground the PR summary in the whole change set (#335)#451
devops-thiago merged 2 commits into
release/v0.6.0from
fix/335-summary-scope

Conversation

@devops-thiago

@devops-thiago devops-thiago commented Aug 8, 2026

Copy link
Copy Markdown
Owner

What type of PR is this?

  • 🐛 Bug fix
  • ✨ Feature
  • 📝 Documentation
  • 🔧 Refactor
  • 🚀 Performance
  • ✅ Test
  • 🔒 Security
  • 📦 Dependency update
  • 🏗️ CI/CD

Description

Dogfooding turned up a summary that described a full multi-collaborator decompose (~1.6k LOC, 7 new collaborators) as "extracts CI-status evaluation" — the prose named one class while the diff was the whole refactor.

Diagnosis. On a large PR the summary comes from a separate model call (AiReviewService.summarizePrReviewer.summarizeStream, driven by PrReviewPrompts.SUMMARY_SYSTEM / SUMMARY_USER), and that call never sees the diff. The PR title and description were already reaching it through the prContext slot, so the gap was not the metadata — it was that:

  1. SUMMARY_SYSTEM told the model to derive pr_purpose "from the changed files and findings" and never mentioned the author's stated intent or the size of the change; and
  2. nothing in the prompt stated how big the change actually is. The only breadth signal was a changed-file list that clampOverview may truncate on exactly the large PRs where this matters.

Fix.

  • FindingPipeline now renders a deterministic PR-scope block at the head of the changed-files section: PR scope (whole pull request): N files changed, +A -D, then a per-directory breakdown (- src/main/java/...: 3 files (+460 -900)), capped at 10 directories with a rollup for the rest. The totals are GitHub's authoritative PR totals (ctx.prTotals(), the same numbers the rendered Changes Overview reports since fix(review): PR summary "Changes Overview" counts diverge from GitHub's file/line totals #298), falling back to the diff-derived counts when the fetch failed. It sits ahead of the per-file rows so budget clamping can only drop the tail, the same reason the pure-rename rollup leads.
  • SUMMARY_SYSTEM now requires pr_purpose to describe the WHOLE change set, grounded in the PR title/description and the scope totals, and explicitly forbids presenting one extracted class, one file, or the one component carrying findings as if it were the whole PR. It also states that a summary narrower than the stated PR scope is wrong and that few/no findings never means the change was small.
  • description_gaps now covers the qualitative case this issue is about: a description whose scope is narrower than the change itself.
  • SUMMARY_USER renames the file-list section to ## PR scope and changed files (computed from the diff — authoritative) and states the purpose must account for all of it.

Everything new is data derived from the diff and file list; the untrusted prose (title/body) keeps going through the existing escaped prContext slot.

Deliberately not implemented (both optional in the issue):

  • The "mentions fewer files than changedFiles.size()" guard. pr_purpose is prose that the prompt explicitly asks to "describe behavior, not a file listing", so a well-written summary mentions no file paths at all. A mention-count threshold would append "(partial — see walkthrough)" to correct summaries, which erodes trust in the other direction. The deterministic effort went into making the scope authoritative and unclampable instead.
  • A "Description vs PR metadata" pass in FindingVerifierPrompts.SYSTEM. That verifier audits individual candidate findings and runs per batch, before the summary exists — it has no summary to check. The equivalent signal is instead encoded where the summary is produced (description_gaps, already rendered by PrSummaryGenerator.appendDescriptionGaps as "⚠️ Description vs. Implementation"), with no extra AI call.

No new config keys, so no README/.env.example changes.

Related Issues

Fixes #335

How Has This Been Tested?

  • Unit tests
  • Integration tests
  • Manual testing

Red/green validated per test: with the production change stashed (git stash push -- src/main/java) and the tests kept, all seven new assertions fail; with it restored they pass.

Red phase (./mvnw -B test -Dtest='FindingPipelineTest,PrReviewPromptsContentTest'Tests run: 47, Failures: 7):

FindingPipelineTest.summaryOverviewStatesTheWholePrScopeForAMultiFileRefactor
  src/main/java/app/review/Orchestrator.java (modified, +40 -900)
  ... ==> expected: <true> but was: <false>
FindingPipelineTest.summaryOverviewScopeUsesGitHubsAuthoritativeTotalsWhenAvailable
  a.java (modified, +3 -0) ... ==> expected: <true> but was: <false>
FindingPipelineTest.summaryOverviewScopeStaysSingularForASingleFilePr
  src/main/java/app/Tiny.java (modified, +3 -1) ==> expected: <true> but was: <false>
FindingPipelineTest.summaryOverviewRollsUpDirectoriesBeyondTheCap
  pkg0/File.java (modified, +1 -0) ... ==> expected: <true> but was: <false>
PrReviewPromptsContentTest.summaryPromptGroundsThePurposeInTheWholeChangeSet
  pr_purpose must be scoped to the whole change set, not one file (#335)
  — missing marker: "what the WHOLE change set does" ==> expected: <true> but was: <false>
PrReviewPromptsContentTest.summaryPromptRejectsASummaryNarrowerThanThePrScope
  a summary whose scope is a subset of the diff's must be called out as wrong (#335)
  — missing marker: "scope is narrower than the stated PR scope is wrong"
PrReviewPromptsContentTest.summaryUserPromptFramesTheFileListAsAuthoritativeScope
  the summary user prompt must present the scope block as authoritative
  — missing marker: "## PR scope and changed files (computed from the diff — authoritative)"

(The assertion message on the pipeline tests is the captured SummaryInputs.changedFiles() value, i.e. the file list with no scope header.)

Green phase: same command, Tests run: 47, Failures: 0.

Coverage of the acceptance criteria:

  • Summary reflects multi-file refactorssummaryOverviewStatesTheWholePrScopeForAMultiFileRefactor drives the multi-file fixture (5 files over 3 directories, title/body announcing the full scope) through the map-reduce path and asserts the summary call receives the totals, the per-directory breakdown, and that they precede the per-file rows. The prompt-side half is pinned by the PrReviewPromptsContentTest cases.
  • Regression test with a multi-file fixture — the same test, plus summaryOverviewScopeUsesGitHubsAuthoritativeTotalsWhenAvailable (GitHub totals win over diff-derived counts) and summaryOverviewRollsUpDirectoriesBeyondTheCap (the breakdown stays bounded).
  • No regression on small single-purpose PRssummaryOverviewScopeStaysSingularForASingleFilePr asserts a one-file PR renders singular scope text with no multi-file or multi-directory language, and the single-call review path (which is what small PRs use) is untouched.

Also run: ./mvnw -B spotless:apply, ./mvnw -B clean compile spotbugs:check spotless:check (BugInstance size is 0, BUILD SUCCESS), ./mvnw -B clean test (full suite: Tests run: 1884, Failures: 0, Errors: 0, Skipped: 0).

Checklist

  • My code follows the project's coding standards
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the documentation accordingly
  • My changes generate no new warnings or errors

Screenshots / Logs

The block the summary call now receives ahead of the file list, for a decompose-shaped PR:

PR scope (whole pull request): 5 files changed, +664 -902
Directories touched: 3
- src/main/java/app/review: 3 files (+460 -900)
- src/test/java/app/review: 1 file (+200 -0)
- (repository root): 1 file (+4 -2)
src/main/java/app/review/Orchestrator.java (modified, +40 -900)
...

Additional Notes

summaryFindingsJsonIsClampedToThePerCallBudget calibrates a per-call budget against the exact fixed prompt sections, so it needed its expected overview updated for the new scope block and slightly longer finding descriptions to keep the findings share dominant; its assertions are unchanged.

The summary call of a large multi-call review never sees the diff: it gets
the computed findings, the PR title/description, and a changed-file list the
input budget may clamp. Nothing in SUMMARY_SYSTEM told the model to describe
the whole change set, and nothing told it how big that set is — so pr_purpose
could latch onto a single file and a full multi-collaborator decompose was
summarized as "extracts CI-status evaluation".

Prepend a deterministic PR-scope block to the changed-files section: the
authoritative file/line totals (GitHub's, the same numbers the rendered
Changes Overview reports) plus a per-directory breakdown, rendered ahead of
the per-file rows so clamping can only drop the tail. SUMMARY_SYSTEM now
grounds pr_purpose in the PR title/description together with those totals,
forbids presenting one extracted class as the whole pull request, and treats
a description narrower than the change as a description gap.

The single-call path is untouched: that call sees the entire diff, so small
single-purpose PRs keep their existing summary behavior.

Refs #335
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@thrillhousebot

thrillhousebot Bot commented Aug 8, 2026

Copy link
Copy Markdown

🤖 ThrillhouseBot PR Summary

What this PR does

This change adds a deterministic PR scope block (file counts, line totals, per-directory breakdown) to the summary input, so the AI model can ground the 'What this PR does' description in the whole change set rather than just one file. It also updates the summary system/user prompts to instruct the model to cover the entire PR and flag when the description is narrower than the actual changes.

Control-Flow Diagram

🔀 Show diagram
flowchart TD
  A["changedFilesOverview()"] --> B{"Pure renames present?"}
  B -->|yes| C["Append rename rollup"]
  C --> D["changeScopeSummary(ctx)"]
  B -->|no| D
  D --> E{"ctx.prTotals() != null?"}
  E -->|yes| F["Use GitHub totals for header"]
  E -->|no| G["Sum additions/deletions from diff"]
  F --> H["Build header: files changed, +A -D"]
  G --> H
  H --> I["appendDirectoryBreakdown"]
  I --> J["Group by directory, sort"]
  J --> K["List top 10, rollup rest"]
  K --> L["Return scope block text"]
  L --> M["Append 'Directories touched:' line"]
  M --> N["Append per-file rows"]
Loading

Changes Overview

  • Files changed: 4
  • Lines added: +280
  • Lines removed: -18

Changed Files

File Change Summary
src/main/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipeline.java Modified Adds changeScopeSummary and helper methods to compute and render authoritative PR-scope totals and directory breakdown ahead of the per-file list.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ai/PrReviewPrompts.java Modified Updates SUMMARY_SYSTEM and SUMMARY_USER to instruct the model to base pr_purpose on the whole change set and to treat narrower summaries as wrong.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipelineTest.java Modified Adds tests for the new scope block rendering: multi-file, single-file, authoritative totals, directory cap, and budget recalibration.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/ai/PrReviewPromptsContentTest.java Modified Adds prompt-content assertions to ensure the new summary instructions are present.

Risk Assessment

Risk Count
🔴 Critical 0
🟠 High 0
🟡 Medium 0
🔵 Low 0

No new issues found in this PR, but the review cannot be approved until CI is confirmed green.

⚠️ CI Checks Status

Some checks are still pending or have failed:

Check Type Status Detail
test check-run ⏳ Pending -
format check-run ⏳ Pending -
actionlint check-run ⏳ Pending -
changes check-run ⏳ Pending -
frontend check-run ⏳ Pending -
trivy check-run ⏳ Pending -
dependency-review check-run ⏳ Pending -

Automated review by ThrillhouseBot. Reply with /review to re-run.

@thrillhousebot thrillhousebot Bot added bug Something isn't working java Pull requests that update java code testing Test coverage and test quality labels Aug 8, 2026
@codecov

codecov Bot commented Aug 8, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

codecov/patch flagged six lines of the new scope block with no covering
test: the empty-change-set guards and the path-with-no-directory branch.

Cover them, and fix what the gap exposed. Totals reporting zero files are
not usable — treat them like a failed fetch and fall back to the
diff-derived counts, instead of announcing a zero-file PR above a
non-empty file list. Drop the null check in directoryOf: the per-file loop
above already probes the same name against an immutable set, so a null
name throws there first and the guard could never run.

Refs #335

@thrillhousebot thrillhousebot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ThrillhouseBot found no issues in this PR, but some checks are still pending or failed:

  • Check dependency-review is pending

@sonarqubecloud

sonarqubecloud Bot commented Aug 8, 2026

Copy link
Copy Markdown

@devops-thiago
devops-thiago merged commit 3c195b7 into release/v0.6.0 Aug 8, 2026
14 checks passed
@devops-thiago
devops-thiago deleted the fix/335-summary-scope branch August 8, 2026 11:12
devops-thiago added a commit that referenced this pull request Aug 8, 2026
Absorbs #449 (per-repo ignore patterns), #451 (whole-change-set PR summary),
#453 (decline re-check) and four dependency bumps.

Two textual conflicts, both from independent additions at the same insertion
point rather than any disagreement:

- ReviewContextLoader: #449's resolveIgnoreGlobs and this branch's
  resolveConfigKeyContext are separate private helpers that git could not
  place. Kept both.
- FindingPipelineTest: #451 parameterized the reviewContext helper with an
  explicit reviewable-file list while this branch added the configKeyContext
  record component. Kept both — the helper's parameter, with "" in the new
  component's position.

One silent breakage git merged cleanly: #453's new declinedRaceContext helper
constructs a ReviewContext without configKeyContext. Filled in.

The interaction between the two features is the one worth noting. #449 made
load() compute reviewableFiles from the global globs unioned with the repo's
own, and config-key resolution already read that post-filter list, so a key
documented only in an ignored file is now correctly never resolved — and it
inherits per-repo ignore rules for free. Pinned with a test that fails if the
raw file list is ever passed instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working java Pull requests that update java code testing Test coverage and test quality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant