Skip to content

Fix critic false-negative on findings the diff claims are intentional - #62

Merged
kadireren7 merged 1 commit into
mainfrom
fix/critic-claimed-intent-false-negative
Sep 21, 2026
Merged

kadireren7 merged 1 commit into
mainfrom
fix/critic-claimed-intent-false-negative

Conversation

@kadireren7

Copy link
Copy Markdown
Owner

Summary

Investigates PR #57's reviewer/critic behavior (production evidence: candidate_count=1, accepted_count=0, rejected_count=1, status=succeeded -- reviewer found the swapped-counts bug, critic rejected it, nothing published).

Reproduced the exact production diff (format_summary_body's swapped "Published inline"/"Summary-only" counts) against a real reviewer+critic run and confirmed the root cause by inspecting the persisted proposal/verdict directly: the reviewer's finding was accurate (evidence, reasoning, impact all correct), and the critic rejected it because the comment directly above the bug ("intentionally swapped... do not fix") convinced it the swap was correct. That is a false negative: the comment's claim of intent says why the code was written that way, not whether the output is correct. A bug introduced on purpose is still a bug -- and the critic's own system prompt already says the diff is untrusted data, it just never named this specific pattern (a claimed-intent assertion, distinct from the impersonated-system-instruction pattern the existing secq-o-prompt-injection-claims-safe / py-prompt-injection-hidden-bug cases already cover).

  • Root cause: critic system prompt lacked an explicit rule against treating an in-diff claim of "intentional/do not fix" as evidence of correctness.
  • Fix: one targeted paragraph added to the critic system prompt (patchfrog/review/prompt.py) forbidding exactly that inference. No reject criteria were removed or loosened; no threshold was lowered.
  • Diagnostics: the critic now returns a machine-classified rejection_category (mirrors the existing ValidationOutcome pattern -- never inferred from free text after the fact), persisted on critic_verdicts (additive migration 0031), plus one structured finding_candidate_diagnostics log line per proposal (finding id, category, reviewer confidence, evidence-entry count, critic decision, rejection category -- never a prompt, provider response, secret, or source blob).
  • Regression fixture: tests/fixtures/evaluation/cases/py-claimed-intentional-swapped-counts reproduces the pattern for patchfrog.evaluation, phrased as ordinary developer prose (not an impersonated system instruction) to keep it a distinct case from the existing prompt-injection corpus.

Test plan

  • pytest -- every unit test in the touched modules passes (test_review_critic.py, test_review_prompt_injection.py, test_evaluation_fixtures_validation.py); full tests/unit/ (1680/1682, 2 pre-existing/unrelated failures) and the bulk of tests/integration/ (unrelated to this change) pass. A handful of Postgres-only concurrency tests and two unrelated ruff-subprocess-based static-analysis/fix-verification test files could not be run cleanly in this sandbox (local Postgres container credential mismatch; subprocess flakiness under heavy memory pressure) -- neither touches any file in this diff.
  • ruff check . -- clean
  • mypy patchfrog -- clean, 351 files
  • Empirically reproduced the real PR E2E test: intentional swapped-counts bug (do not merge) #57 diff against a real reviewer+critic run (candidate=1, rejected=1, matching production) and confirmed the fix's guardrail directly targets the observed rejection reasoning

PR #57 (test/webhook-e2e-review-check) exercised a real, deliberately
introduced bug in format_summary_body(): the "Published inline" and
"Summary-only" counts are swapped. The reviewer correctly proposed the
finding (evidence, reasoning, and impact were all accurate), but the
critic rejected it, reasoning that the comment directly above the bug
("intentionally swapped... do not fix") proved the behavior was not a
defect. Reproduced against the real production diff (merge-base
bd25117 vs head b82d279) with a real reviewer+critic run: candidate=1,
accepted=0, rejected=1, matching the reported production shape exactly.
The rejection is a false negative -- the critic's own system prompt
already says everything in the diff is untrusted data, but nothing in
it named this specific pattern (a claim of intent, as opposed to an
impersonated system instruction), so the critic treated "the author
says this is deliberate" as proof the output is correct. Deliberateness
and correctness are different questions; a bug introduced on purpose is
still a bug.

- patchfrog/review/prompt.py: add an explicit critic-prompt rule that a
  comment/commit message claiming code is intentional, a test fixture,
  or "do not fix" is untrusted data, not evidence of correctness -- the
  critic must judge the finding against what the code does, not against
  the diff's own claim about why it exists. Existing reject criteria are
  unchanged; no threshold was lowered.
- patchfrog/review/domain.py, schemas.py, critic.py: add
  CriticRejectionCategory, a machine-classified reject reason the critic
  now returns in its own structured response (mirroring the existing
  ValidationOutcome pattern -- never inferred from reasoning_summary
  prose after the fact).
- patchfrog/persistence/models/review.py,
  repositories/critic_verdict.py, migrations/versions/0031_*: persist
  rejection_category on critic_verdicts (nullable, additive).
- patchfrog/review/service.py: one structured "finding_candidate_diagnostics"
  log line per proposal (finding id, category, reviewer confidence,
  evidence-entry count, critic decision, rejection category) -- never
  the prompt, a provider response, a secret, or source content.
- tests/fixtures/evaluation/cases/py-claimed-intentional-swapped-counts:
  regression fixture reproducing the pattern for patchfrog.evaluation.
- tests/unit/test_review_critic.py, test_review_prompt_injection.py:
  cover rejection_category parsing/schema and the new prompt guardrail.
@kadireren7
kadireren7 merged commit dcb5fe2 into main Sep 21, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant