fix(code-review): surface discarded findings; stop treating absence as disproof (ISS-10711) - #203
cchenault4 wants to merge 3 commits into
Conversation
…S-10711) An APPROVED carrying `reason: ""` is indistinguishable from a review that found nothing. In ISS-10711 seven of eight verifiers rejected real findings -- two P1s, one writing permanently-wrong rows into an append-only audit ledger -- and the rollup emitted APPROVED with an empty reason. Nothing in the output said anything had been discarded, so the run looked spotless. `_compute_canonical_verdict` now takes the rejected bucket and composes a reason naming the count and the dominant rejection classes. Gating is deliberately unchanged: a rejected finding is by definition not gating, and the verdict returned for every input is what it was before. The reason is composed to fit `_VERDICT_REASON_MAX` rather than truncated to it -- a reason chopped mid-class reads as a different class -- and the count survives when classes have to be dropped. Scope note: the underlying wrong-checkout cause ISS-10711 was filed for is already fixed on main. `review_root` is now always populated (worktree_path or `_git_toplevel()`), `_require_review_root` refuses a run it cannot prove, and both agent prompts tell the fleet to STOP rather than fall back to the working directory. This commit closes the remaining auditability half. Tests: 8 added, including an end-to-end assertion through `cmd_finalize_result` -- the unit tests alone cannot catch a call site that forgets to pass `rejected`, and that wiring is the fix. Counterfactuals run: reverting the reason kills 4 unit tests; dropping the call-site argument kills the end-to-end test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hHB3pezXXMQdJZUdTjJ95
… existence check (ISS-10711) The EXISTENCE CHECK collapsed every form of absence into REJECTED / evidence_not_found. Absence splits three ways and only one of them disproves a finding. - cited file missing under review_root -> TENTATIVE. A missing file is equally well explained by a rename, a bad path, or a tree that is not the one this diff describes. - snippet present in the patch hunks as an ADDED line but absent from review_root -> TENTATIVE, reported as a scope or checkout fault in the run. This is the ISS-10711 fingerprint: a tree holding none of the branch's new code makes every finding about it look fabricated, which is how eight verifiers rejected seven real findings at 0.95 confidence. - file exists, read, and neither the snippet nor any snippet_hash matches, and it is not an added line in the patch -> REJECTED. This is now the only branch carrying positive disconfirmation. Narrow on purpose. A blanket downgrade would let every hallucinated finding survive as TENTATIVE, and TENTATIVE at MEDIUM promotes to NEEDS_ATTENTION -- that would trade a false green for a false red across every review. No code-side detector added: `_require_review_root` already refuses a root that is missing, or has drifted from, any file the diff changes, so a mechanical check of the same condition would be redundant. Editing this prompt changes verifier_prompt_hash, which correctly invalidates cached verdicts produced under the old ladder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hHB3pezXXMQdJZUdTjJ95
Required by the Plugin Version Bump CI job: any change under plugins/<name>/ must bump that plugin's manifest. Minor rather than patch, following the repo's own convention -- a fix that changes observable behaviour takes a minor (3.9.0 "resolve review paths against the reviewed worktree", 3.6.0), while a test-only or narrow fix takes a patch (3.8.1, 3.7.1). This PR changes verifier verdict semantics and the content of the verdict reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017hHB3pezXXMQdJZUdTjJ95
mikeangstadt
left a comment
There was a problem hiding this comment.
Good fix, and the reason-composition-not-truncation detail is the right call. Ran the 8 new tests, they pass. One real question on the first prompt bullet and two contract-hygiene things inline, none blocking.
| "I could not locate it" and "it is not there" are different claims. | ||
|
|
||
| - **The cited file does not exist under `review_root`** → TENTATIVE. | ||
| A missing file is equally well explained by a rename, a bad path, or a |
There was a problem hiding this comment.
This is the bullet I'd push on. Bullets 2 and 3 are tightly scoped, but "cited file does not exist under review_root" is also the exact signature of the most common reviewer hallucination: an agent inventing a plausible path it never read. TENTATIVE lands the finding in verified[] via the bucketing at code_review_helpers.py:3850, and Rule 3.5 turns any TENTATIVE into NEEDS_ATTENTION for the whole run. So a single fabricated path now reds the review instead of being dismissed. The PR body says a blanket downgrade would let hallucinated findings survive as TENTATIVE, but that is what this bullet does for the hallucinated-path class specifically. _require_review_root does not help here either, since it only proves the root holds the files the diff changed, and a hallucinated path is by definition not one of those. False-red is the safer direction so I'm not holding the PR, but if you see NEEDS_ATTENTION noise from this, the split you want is probably "missing path is also not in the patch at all" to REJECTED, keeping TENTATIVE for the rename/drift case.
| ) | ||
|
|
||
| return "APPROVED", "" | ||
| return "APPROVED", _approved_verdict_reason(rejected) |
There was a problem hiding this comment.
SCHEMA.md:278 still says "verdict_reason cites the specific finding(s) that produced the verdict." After this, on APPROVED it cites the findings that specifically did not. That doc line is the contract a downstream reader parses against, so it needs a sentence for the APPROVED case in this PR.
| "name": "code-review", | ||
| "description": "Code review plugin", | ||
| "version": "3.10.0", | ||
| "version": "3.11.0", |
There was a problem hiding this comment.
No CHANGELOG.md in the branch. CI only enforces the version bump, so this passes green, but .githooks/pre-push blocks exactly this and CLAUDE.md says run /update-documentation before pushing. Looks like core.hooksPath isn't set locally. Worth a run before merge so the release notes aren't reconstructed later.
Closes the two ISS-10711 remedies that the cause fix did not cover.
Context: the cause is already fixed on
mainISS-10711 was filed against a machine pinned to plugin 3.1.0, where
review_rootwas populated only forscope_kind == "pr"and both agent prompts told the fleet to fall back to the current working directory when it was empty. A pre-PRbranch-scope review therefore ran its whole verifier fleet against the invoking checkout: eight verifiers, seven rejecting real findings at 0.95 confidence, and anAPPROVEDon a change carrying two P1 defects.None of that is true of
main.review_rootis now always populated (worktree_pathor_git_toplevel()),_require_review_rootrefuses a root it cannot prove holds the diff, and the cwd fallback is gone from both prompts.This PR is only the part that survived that fix. Two remedies from the ticket were verifier-stage and rollup-stage, independent of where source is read, and both were still reachable on
main.1. An APPROVED must name what verification discarded
_compute_canonical_verdictended inreturn "APPROVED", "". An APPROVED carrying an empty reason is indistinguishable from a review that found nothing, so a run whose every finding was discarded reads as clean — which is precisely how ISS-10711 went unnoticed until a lane distrusted it and re-read the findings by hand.The verdict now carries e.g.
7 finding(s) rejected by verification (evidence_not_found x7).Gating is deliberately unchanged. A rejected finding is by definition not gating, and every input returns the verdict it returned before; only the reason string differs. The reason is composed to fit
_VERDICT_REASON_MAXrather than truncated to it, because a reason chopped mid-class reads as a different class — the count is the load-bearing half and survives when classes have to be dropped.2. "Could not locate it" and "is not there" are different claims
The EXISTENCE CHECK collapsed every form of absence into
REJECTED/evidence_not_found. It now splits three ways:review_root→TENTATIVE. Equally well explained by a rename, a bad path, or a tree that is not the one this diff describes.review_root→TENTATIVE, reported as a scope or checkout fault in the run rather than evidence against the finding. This is ISS-10711's fingerprint, and it would have caught that run even with the oldreview_rootbug fully intact.snippet_hashmatches, and it is not an added line in the patch →REJECTED. This is now the only branch carrying positive disconfirmation.Narrow on purpose. A blanket downgrade would let every hallucinated finding survive as
TENTATIVE, andTENTATIVEat MEDIUM promotes toNEEDS_ATTENTION— trading a false green for a false red across every review.No code-side detector was added for the same condition:
_require_review_rootalready refuses a root missing or drifted on any file the diff changes, so a mechanical check would be redundant.Test plan
pytest plugins/code-review/→ 1405 passed, 3 skipped, 0 failed.ruff checkon both touched Python files → 210 findings before, 210 after; none introduced.cmd_finalize_result. The unit tests alone cannot catch a call site that forgets to passrejected, and that wiring is the fix.""kills 4 unit tests; dropping therejected=rejectedargument at the call site kills the end-to-end test. The 3 unit tests that survive the first mutation are the ones asserting unchanged behaviour, which is correct.validation_errors == [], so it cannot hide a real envelope regression in its own schema noise.Editing
verifier_prompt.txtchangesverifier_prompt_hash, which correctly invalidates verdicts cached under the old ladder.test_verifier_prompt_changes_hashalready pins that property.Not covered
resolve-scopecomparing against a local ref rather thanorigin/<base>, which silently widened one run's scope by 29 files of other people's work. Distinct trigger from thereview_rootcause; not verified against 3.7.0 and should not be assumed fixed alongside it.resolve-scopeprependingorigin/to a SHA base) remains its own ticket.🤖 Generated with Claude Code
https://claude.ai/code/session_017hHB3pezXXMQdJZUdTjJ95