Skip to content

fix(review): disclose a ceiling-skipped summary in the posted review - #524

Merged
devops-thiago merged 1 commit into
release/v0.6.0from
fix/518-ceiling-summary-disclosure
Aug 11, 2026
Merged

fix(review): disclose a ceiling-skipped summary in the posted review#524
devops-thiago merged 1 commit into
release/v0.6.0from
fix/518-ceiling-summary-disclosure

Conversation

@devops-thiago

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

Copy link
Copy Markdown
Owner

What type of PR is this?

  • 🐛 Bug fix

Description

When the token spend ceiling trips after every batch succeeded — before the summary call, or refusing the summary call itself — the review degrades to the counts-only summary with a log-only warning: TruncationDetail stayed empty, so the posted review carried a bare counts-only summary with no stated reason (and could even APPROVE). #513 fixed exactly this asymmetry for the truncation flavor of the same summary lane; this PR gives the ceiling flavor the same seams:

  • BudgetPlan gains a summarySkippedAtCeiling flag (recorder + snapshot accessor, mirroring summaryResponseCut), recorded in FindingPipeline.countsOnlySummary — covering both of its call sites (pre-summary gate skip, and the mid-call TokenSpendCeilingExceededException refusal).
  • VerdictBuilder.build threads it into a matching TruncationDetail.summarySkippedAtCeiling component.
  • Rendering: a dedicated SUMMARY_SKIPPED_NOTICE banner ("Summary skipped. The review's token spend ceiling (REVIEW_MAX_TOKENS_PER_REVIEW) was reached … the findings themselves are complete."), a coverageGapClause clause and coverageGapBrief part naming the ceiling, and a check-run suffix — each folding into the partial-review clause instead of stacking when real file gaps exist.
  • No approval hold: the findings are complete; truncated() is untouched by the flag.
  • Follow-up delta comment renders a contradictory disclosure on a summary-only cut #516's empty-guard treats the new flag exactly like summaryResponseCut: hasFileGaps() ignores it, so the delta comment / on-demand disclosure never wrap it in partial-coverage framing.
  • Fixes the two over-claiming comments ("the sibling spend-ceiling degradation of the same lane already discloses itself" — previously true only when batches were also skipped): FindingPipeline.salvagedOrCountsOnlySummary javadoc and the ReviewResult.coverageGapClause comment now describe both flavors accurately.

Related Issues

Fixes #518

How Has This Been Tested?

  • Unit tests

Red/green proven via two integration tests through the real pipeline→plan→verdict seam (see Logs): FindingPipelineTest.aCeilingSkippedSummaryIsDisclosedInThePostedReviewNotJustTheLog and …aSummaryCallRefusedAtTheCeilingIsDisclosedInThePostedReviewNotJustTheLog — one per countsOnlySummary call site. Plus mirrors of #513's unit tests for the new flag: plan recorder/snapshot (DiffBudgetPlannerTest), verdict banner/no-approval-hold and fold-into-clause (VerdictBuilderTest), clause/brief/isEmpty/hasFileGaps and the #516 guard (ReviewResultTest), delta-comment guard (FollowUpDeltaSummaryTest), and disjointness assertions on the pipeline paths. Full suite: 2581 tests, 0 failures.

Gates: spotless:applyclean compile spotbugs:check spotless:check (BugInstance size 0) → clean test green. JaCoCo ∩ git diff -U0 against the stack parent: zero uncovered lines/branches in changed main code.

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

Red run of both integration tests on unfixed code (stack parent, before this commit):

[ERROR] FindingPipelineTest.aCeilingSkippedSummaryIsDisclosedInThePostedReviewNotJustTheLog:1254
the posted review must name the ceiling that degraded the summary, got:  ==> expected: <true> but was: <false>
[ERROR] FindingPipelineTest.aSummaryCallRefusedAtTheCeilingIsDisclosedInThePostedReviewNotJustTheLog:1281
the posted review must name the ceiling that refused the summary call, got:  ==> expected: <true> but was: <false>

Both green with the fix applied.

Additional Notes

Third PR of the #515#516#518 stack: based on fix/516-delta-disclosure; re-target to release/v0.6.0 after #523 (#516) merges — the diff scoped to this issue is the last commit. Design per audit finding AUDIT3-A F2, following #513's implementation as the pattern.

@thrillhousebot

Copy link
Copy Markdown

🤖 ThrillhouseBot PR Summary

What this PR does

When the token spend ceiling trips after all batches succeed — either at the pre-summary gate or when the summary call is refused mid-call — FindingPipeline.countsOnlySummary now records a summarySkippedAtCeiling flag on the BudgetPlan. VerdictBuilder threads it into TruncationDetail, and the rendered review gains a dedicated SUMMARY_SKIPPED_NOTICE banner (when no file gaps exist), a coverageGapClause-clause and coverageGapBrief-part naming REVIEW_MAX_TOKENS_PER_REVIEW, and a check-run suffix — without holding approval because the findings are complete. The change also corrects the two over-claiming comments that previously asserted the spend-ceiling degradation already disclosed itself.

Control-Flow Diagram

🔀 Show diagram
flowchart TD
    A["Batch calls succeed"] --> B{"Ceiling reached at summary gate?"}
    B -- "yes" --> C["countsOnlySummary records summarySkippedAtCeiling"]
    B -- "no" --> D["summarize() call"]
    D -- "TokenSpendCeilingExceededException" --> C
    D -- "ok" --> E["Normal summary path"]
    C --> F["VerdictBuilder threads flag into TruncationDetail"]
    F --> G{"File-coverage gaps present?"}
    G -- "no" --> H["SUMMARY_SKIPPED_NOTICE banner + check suffix; no approval hold"]
    G -- "yes" --> I["Skip folds into coverageGapClause; partial-review banner"]
Loading

Changes Overview

  • Files changed: 9
  • Lines added: +419
  • Lines removed: -26

Changed Files

File Change Summary
src/main/java/dev/thiagogonzaga/thrillhousebot/review/DiffBudgetPlanner.java Modified Adds the summarySkippedAtCeiling AtomicBoolean to BudgetPlan with recorder, snapshot accessor, null-coalescing compact constructor, and a back-compat 8-arg constructor.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipeline.java Modified Records summarySkippedAtCeiling on the plan in countsOnlySummary at both ceiling call sites (gate skip and refused summary) and fixes the over-claiming javadoc.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ReviewResult.java Modified Adds summarySkippedAtCeiling to TruncationDetail (canonical + back-compat constructors, isEmpty), the SUMMARY_SKIPPED_NOTICE banner, and clause/brief entries naming the ceiling.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/VerdictBuilder.java Modified Threads plan.summaryWasSkippedAtCeiling() into TruncationDetail, adds the ceiling check-run suffix and the summary-skipped banner branch (only when no file gaps).
src/test/java/dev/thiagogonzaga/thrillhousebot/review/DiffBudgetPlannerTest.java Modified Tests record/snapshot semantics of the new flag, live-backing behavior, and that the flag does not affect plan.truncated().
src/test/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipelineTest.java Modified Integration tests for both ceiling call sites asserting the posted review names the ceiling; disjointness assertions against the response-cut flag.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/FollowUpDeltaSummaryTest.java Modified Tests that a ceiling-skip-only summary does not trigger partial-coverage framing in the delta comment (#516 guard).
src/test/java/dev/thiagogonzaga/thrillhousebot/review/ReviewResultTest.java Modified Tests that the ceiling flag defeats isEmpty, is ignored by hasFileGaps and truncationDisclosure guards, and appears in coverageGapClause/Brief.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/VerdictBuilderTest.java Modified Tests the summary-skipped banner without an approval hold and the fold-into-coverage-clause shape when file gaps coexist.

Risk Assessment

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

Everything's coming up Thrillhouse! 🎉

No issues found in this PR.


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 11, 2026
Base automatically changed from fix/516-delta-disclosure to release/v0.6.0 August 11, 2026 01:03
@devops-thiago
devops-thiago force-pushed the fix/518-ceiling-summary-disclosure branch from 414715c to 5d56cf4 Compare August 11, 2026 04:55
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

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

Scanned Files

None

@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.

Everything's coming up Thrillhouse! 🎉

No issues found in this PR.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@sonarqubecloud

Copy link
Copy Markdown

@devops-thiago
devops-thiago merged commit e2f5682 into release/v0.6.0 Aug 11, 2026
14 checks passed
@devops-thiago
devops-thiago deleted the fix/518-ceiling-summary-disclosure branch August 11, 2026 12:06
devops-thiago added a commit that referenced this pull request Aug 11, 2026
…#530)

## What type of PR is this?

- [x] 🐛 Bug fix

## Description

On the degenerate budgeted lane (every reviewable file over budget, zero
batches), `FindingPipeline.summarizeWithoutReview` makes the review's
only AI call — the summary. `AiReviewService.runWithRetries` runs the
token-spend gate before every attempt and deliberately lets the typed
`TokenSpendCeilingExceededException` propagate; the multi-call lane
catches exactly that at its summarize seam and degrades to
`countsOnlySummary`, but `summarizeWithoutReview` caught only
`AiResponseTruncatedException`. A mid-retry ceiling refusal (attempt 1
bills usage that reaches `REVIEW_MAX_TOKENS_PER_REVIEW` and then fails
transiently; attempt 2's gate throws) therefore escaped `run`, landed in
`ReviewOrchestrator.handleReviewFailure`, and the whole review was lost
instead of posting counts-only with its omission disclosures — defeating
this lane's own stated contract. `plan.summarySkippedAtCeiling` was
never recorded either, so no disclosure would have been rendered even if
the review had survived.

Fix: catch `TokenSpendCeilingExceededException` at the same seam and
degrade to `countsOnlySummary(...)` exactly like the multi-call lane —
the review still posts with its omission disclosures, and the ceiling
skip is recorded on the plan so the posted review names
`REVIEW_MAX_TOKENS_PER_REVIEW`.

## Related Issues

Fixes #526

## How Has This Been Tested?

- [x] Unit tests

New test
`FindingPipelineTest.summarizeWithoutReviewDegradesToCountsOnlyWhenTheCeilingRefusesTheSummary`:
degenerate all-omitted plan, summarize stub throwing the typed refusal;
asserts the review no longer throws, returns the counts-only shape
(empty findings, null summary), persists the response, records
`summaryWasSkippedAtCeiling()` on the plan, and leaves `summaryWasCut()`
false (disjoint classes).

Red on unfixed code (the refusal escapes `pipeline.run` and the skip is
never recorded):

```
[ERROR] dev.thiagogonzaga.thrillhousebot.review.FindingPipelineTest.summarizeWithoutReviewDegradesToCountsOnlyWhenTheCeilingRefusesTheSummary -- Time elapsed: 2.596 s <<< ERROR!
dev.thiagogonzaga.thrillhousebot.review.ai.TokenSpendCeilingExceededException: AI call skipped: this review has consumed 120000 tokens, at or above its 100000-token spend ceiling (REVIEW_MAX_TOKENS_PER_REVIEW / thrillhousebot.review.max-tokens-per-review)
	at dev.thiagogonzaga.thrillhousebot.review.ai.AiReviewService.summarize(AiReviewService.java:103)
	at dev.thiagogonzaga.thrillhousebot.review.FindingPipeline.summarizeWithoutReview(FindingPipeline.java:625)
	at dev.thiagogonzaga.thrillhousebot.review.FindingPipeline.runWithLedger(FindingPipeline.java:144)
	at dev.thiagogonzaga.thrillhousebot.review.FindingPipeline.run(FindingPipeline.java:128)
```

Green with the fix. Gates: `spotless:apply` clean; `clean compile
spotbugs:check spotless:check` — BugInstance size is 0; full `clean
test` — 2595 tests, 0 failures. Jacoco on the changed lines: 0 missed
instructions, 0 missed branches.

## Checklist

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

## Screenshots / Logs

N/A

## Additional Notes

The catch mirrors the multi-call lane's `"summary call refused"`
degradation verbatim; no rendering changes — the disclosure plumbing
shipped in #524 renders the recorded skip.
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