Skip to content

fix(review): count only the verdicts the audit acted on as verification coverage - #719

Merged
devops-thiago merged 1 commit into
mainfrom
fix/710-verification-coverage
Aug 15, 2026
Merged

fix(review): count only the verdicts the audit acted on as verification coverage#719
devops-thiago merged 1 commit into
mainfrom
fix/710-verification-coverage

Conversation

@devops-thiago

Copy link
Copy Markdown
Owner

What type of PR is this?

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

Description

FindingVerificationService reports how much of the candidate set the second-pass audit
covered, and #623 turns a non-FULL coverage into a banner, a coverage clause and a check-run
brief. The verified half of that record asked only whether some salvaged verdict carried
the candidate's id.

That is not the same question as "was this finding screened". apply() acts on three decisions
confirmed, downgraded, rejected — and everything else (no verdict field, a blank one,
or a label this service does not recognize) lands in its fail-open default, where the finding
posts exactly as the reviewer raised it, indistinguishable from a candidate with no verdict at
all. The coverage count called those screened anyway, so a cut whose salvage recovered such an
element reported FULL coverage and every surface stayed silent about a published set that
mixed verified and unverified findings and read as fully screened.

The fix routes both apply()'s switch and the coverage count through one decision normalizer,
so what the review does with a verdict and what it claims it verified cannot drift.
Undercounting is the safe direction: it costs an over-cautious clause on a finding the audit
ruled on in words we cannot read, while the silence it replaces is exactly the harm #623 exists
to prevent.

On the two cut edges the issue asks about — both were measured against the real code, not
assumed, and are now pinned in the javadoc and in tests:

  • Zero salvaged. Already (N, 0)NONE on both cut lanes; unchanged. The new sibling
    case — elements salvaged but none of them decidable — now reports (N, 0) too, since the
    findings are in the state the empty-body path leaves them in.
  • All salvaged. Stays genuinely FULL, deliberately. A body cut after the verdicts array
    closed raises Jackson's Unexpected end-of-input: expected close marker for Object against
    the root object, and every verdict inside it is complete and applied; what the cut
    destroyed is the body's tail. Disclosing a gap there would tell the reader a fully screened
    set was not screened. The cut is still logged for the operator either way.

Measurement note for the reported symptom: a partial salvage was already reported as PARTIAL
on both cut lanes before this change ((3, 2), (2, 1) — pinned by existing tests and
re-confirmed by probe). The only way the cut lanes could read FULL was a salvage covering every
candidate — either genuinely complete verdicts (correct, and kept), or the undecidable-verdict
drift this PR closes. The log line quoted in the issue is the root-object shape: Jackson's message
is multi-line, so salvaged N verdict(s) covering M of K candidate finding(s) sits on a later
line of the same record, and the absence of Finding verification: N kept, M downgraded, K rejected only means nothing was rejected or downgraded — apply() logs that line only when it
changed something.

Fail-open is untouched: coverage changes what the review says, never which findings it keeps.

Related Issues

Closes #710

How Has This Been Tested?

  • Unit tests
  • Integration tests
  • Manual testing

Red/green, with the new tests run against the unfixed service first:

[ERROR] Tests run: 5, Failures: 4, Errors: 0, Skipped: 0
[ERROR]   FindingVerificationServiceTest.doesNotCountAVerdictWhoseDecisionTheAuditCannotRead:774
  expected: <[VerificationCoverage[candidates=2, verified=1]]> but was: <[VerificationCoverage[candidates=2, verified=2]]>
[ERROR]   FindingVerificationServiceTest.reportsPartialCoverageWhenACutSalvagesAVerdictCarryingNoDecision:677
  expected: <[VerificationCoverage[candidates=2, verified=1]]> but was: <[VerificationCoverage[candidates=2, verified=2]]>
[ERROR]   FindingVerificationServiceTest.reportsPartialCoverageWhenTheLengthCapSalvagesAVerdictCarryingNoDecision:702
  expected: <[VerificationCoverage[candidates=2, verified=1]]> but was: <[VerificationCoverage[candidates=2, verified=2]]>
[ERROR]   FindingVerificationServiceTest.reportsZeroCoverageWhenACutSalvagesOnlyVerdictsCarryingNoDecision:725
  expected: <[VerificationCoverage[candidates=2, verified=0]]> but was: <[VerificationCoverage[candidates=2, verified=2]]>

The fifth (reportsFullCoverageWhenACutSalvagedEveryCandidatesVerdict) is characterization, not
red/green: it pins the all-salvaged edge so it cannot drift into a false alarm.

Two of the new tests drive a real cut end to end rather than the record type — one through the
parse-failure lane (STOP, body cut after the array closed) and one through the reported
finish_reason=length lane, which unwinds via AiResponseTruncatedException into
salvageTruncatedVerdicts.

Downstream disclosure was verified rather than assumed: VerdictBuilderTest
(partialVerificationCoverageDisclosesTheHonestXOfY) already proves a recorded (4, 3) renders
only covered 3 of the 4 finding(s) in the summary and
Verification covered 3 of 4 finding(s); the rest posted unverified. in the check-run brief.

Gates, all green:

./mvnw -B clean compile spotbugs:check spotless:check
[INFO] --- spotbugs:4.10.3.0:check (default-cli) @ thrillhousebot ---
[INFO] BugInstance size is 0
[INFO] --- spotless:3.9.0:check (default-cli) @ thrillhousebot ---
[INFO] BUILD SUCCESS

./mvnw -B clean test
[INFO] Tests run: 3253, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

Patch coverage from target/site/jacoco/jacoco.xml intersected with git diff -U0 origin/main --:
7 instrumented changed main lines, 0 uncovered lines and 0 uncovered branches (the
decisionOf ternary reports 4/4 branches, the reworked apply() switch 3/3).

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

None.

Additional Notes

No behaviour change to the fail-open contract, to #617's salvaging of the verdicts that closed
before a cut, or to the verdict application itself — only to what the coverage record counts as
a screened candidate.

…on coverage

#623's disclosure reads coverage off the candidate count and the number of candidates
that received a verdict, and every degradation lane records it — including the two that
salvage a body cut mid-JSON (#546/#617). The count asked only whether some salvaged
verdict carried the candidate's id, so a verdict the audit could not act on — no
decision label, a blank one, or a word this service does not recognize — counted as
screened while apply() left its finding posting exactly as the reviewer raised it. A cut
whose salvage recovered such an element therefore reported FULL coverage, and every
surface stayed silent about a published set that mixed verified and unverified findings
and read as fully screened — the harm #623 exists to prevent, on the lane most likely to
hit it.

Coverage now follows what the audit acted on: apply()'s switch and the count read the
decision through one normalizer, so what the review DOES with a verdict and what it
CLAIMS it verified cannot drift. Undercounting is the safe direction — it costs an
over-cautious clause on a finding ruled on in words this service cannot read, while the
silence it replaces tells the reader nothing is outstanding.

Both edges a cut can land on were measured and are pinned in the javadoc and in tests.
Nothing decidable salvaged reports (N, 0) — NONE, the same disclosure the empty-body
path renders, since the findings are in the same state. A cut whose salvage covers every
candidate stays genuinely FULL: a body cut after the verdicts array closed (Jackson's
"expected close marker for Object" against the root) carries a complete, applied verdict
for every finding, so disclosing a gap there would tell the reader a fully screened set
was not screened. The cut itself is logged either way.
@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

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

Scanned Files

None

@codecov

codecov Bot commented Aug 15, 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

@thrillhousebot

Copy link
Copy Markdown
Contributor

🤖 ThrillhouseBot PR Summary

What this PR does

Verification coverage now records only candidates whose salvaged verdict carries a decision the audit applies (confirmed, downgraded, or rejected), routing both the coverage count and apply() through a shared decisionOf() normalizer. Added tests pin the parse-failure and length-cap cut lanes as well as the zero- and full-salvage edges, while fail-open review application is unchanged.

Description vs. Implementation

No mismatch found between the PR description and the change.

Control-Flow Diagram

🔀 Show diagram
flowchart TD
    A["Verifier response body"] --> B{"Body cut or length-capped?"}
    B -- yes --> C["salvageTruncatedVerdicts()"]
    B -- no --> D["apply()"]
    C --> E["candidatesCovered()"]
    D --> F["decisionOf(verdict)"]
    E --> F
    F --> G{"Label in ACTED_ON_DECISIONS?"}
    G -- yes --> H["Audit acts and id counts toward verified"]
    G -- no --> I["Fail-open keeps original; id not counted"]
    H --> J["Post findings and VerificationCoverage outcome"]
    I --> J
Loading

Changes Overview

  • Files changed: 2
  • Lines added: +181
  • Lines removed: -5

Changed Files

File Change Summary
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ai/FindingVerificationService.java Modified Adds ACTED_ON_DECISIONS and decisionOf(); coverage count and apply() share decision normalization.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/ai/FindingVerificationServiceTest.java Modified Adds five coverage tests for decisionless/unknown verdicts, including cut, length-cap, zero, and full edges.

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 required CI is confirmed green.

⚠️ Required CI Checks Status

Some required checks are still pending or have failed:

Check Type Status Detail
test check-run ⏳ Pending -
trivy check-run ⏳ Pending -
frontend check-run ⏳ Pending -
format 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 labels Aug 15, 2026
@devops-thiago
devops-thiago merged commit cbcd7b0 into main Aug 15, 2026
17 checks passed
@devops-thiago
devops-thiago deleted the fix/710-verification-coverage branch August 15, 2026 02:45
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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Verification cut mid-JSON is reported as full coverage, so #623's disclosure never fires

1 participant