Skip to content

test(eval): measure review findings, not just review outcomes - #1003

Open
guyoron1 wants to merge 9 commits into
fullsend-ai:mainfrom
guyoron1:eval/review-precision-cases
Open

test(eval): measure review findings, not just review outcomes#1003
guyoron1 wants to merge 9 commits into
fullsend-ai:mainfrom
guyoron1:eval/review-precision-cases

Conversation

@guyoron1

@guyoron1 guyoron1 commented Aug 25, 2026

Copy link
Copy Markdown

The review eval judges outcome labels only, so a review can apply the right label while reporting the wrong things — or nothing at all. These cases judge the findings themselves.

What changes

capture-fixture.sh now snapshots inline PR review comments. That is where postreview.go puts structured findings, and gh pr view --json does not expose them. Two deterministic judges read them:

  • required_findings — a seeded bug must be reported, at or above a stated severity.
  • forbidden_findings — a safe construct must not be promoted to a real vulnerability.

Five cases: seeded bugs, false-positive bait, docs-only, a dependency bump, and 008-reassuring-docstring.

Why 008 exists

It is the deliberate mirror of the bait case. Both PRs hash a secret-prefixed payload with md5 — in 005 the digest is a receipt cache key, in 008 it is compared against an inbound X-Signature header. The surface pattern is identical, so neither case can be passed by recognising the construct, only by following the value to its use.

In 008 both prose channels lie: the docstring says "not a security boundary" and the PR body repeats it, while the function using the digest as a signature sits three lines below in the same diff.

Without a case like this, a suite whose ground truth overlaps the review guidance measures instruction-following, and a prompt change that over-suppresses reads as a precision win.

Two ways the judges could have lied

They could not fail. pass_rate is sum(values)/len(values) over every case that produced a value, and a case declaring no ground truth returns True. With one recall case out of five, required_findings at 0.70 could not have failed even if that case missed every seeded bug. Both findings judges now carry an if: so the harness skips them where there is no ground truth, and the rates are over the cases that actually assert something.

They could pass without seeing anything. forbidden_findings returned success whenever no inline comment parsed as a finding. A 422 folds findings into the review body, and findings with no file or line are dropped before the inline comments — so precision was unverifiable, which is not the same as verified clean. It now fails closed and says why. Separately, fetch_review_comments discarded jq's exit status, so a jq failure returned success with empty output and then aborted the script before fixture-state.json was written at all, losing every judge for that case.

The 422 path

agents#209 was parked pending the 422 fix, because a review whose positioned comments GitHub rejects has its findings folded into the review body instead. Reading inline comments alone would report a miss the agent never made, and would hide a promoted bait from the precision judge entirely — a silent pass on the case the suite exists to catch.

Both judges now also parse those fallback bullets, anchored on the fixed note post-review writes, so an ordinary review body is never mined for things that merely look like findings. The eval no longer waits on agents#193: a review delivered through the fallback is judged on its findings rather than on how GitHub happened to accept them.

Testing

eval/scripts/review-findings-judge-test.py extracts the judge bodies from the shipped eval.yaml — no second copy to drift — and runs 56 synthetic payloads through them. Wired into make script-test. Negative-checked: disabling the fail-closed guard fails the suite; replacing the word-boundary match with a substring match fails four boundary cases.

./eval/lint-cases.sh passes for review, triage, code and fix; pre-commit run --all-files is clean.

Notes for review

  • Categories are matched against the whole finding body rather than the category token, because SKILL.md's vocabulary is per-dimension kebab-case and unrecognised categories route to the nearest dimension — pinning the token fails a correct review on a synonym.
  • With today's counts both gates mean "every case must pass". That is deliberate while 008 is the only anti-bait case; letting it fail while the suite stays green would make it decorative, which is the defect the if: gates were added to fix.

Why this lands first

The review agent is growing a second implementation: the pi runtime runs
review on Grok 4.6 without sub-agents (first live run:
fullsend-ai/pi-xai-vertex#4). These judges score the posted output —
inline review comments and the review body — not the architecture that
produced it, so the same cases can score both implementations
head-to-head. That comparison needs the eval to exist before either
implementation changes.

@github-actions

Copy link
Copy Markdown

Functional tests did not run

Functional tests run automatically for org/repo members and collaborators on pull requests.

For other contributors, a maintainer must add the ok-to-test label after the latest push.

@guyoron1

Copy link
Copy Markdown
Author

Addresses #209 (review eval suite has zero coverage) and #245. Also unblocks the dependency on #193: the judges now read findings that the 422 fallback pushed into the review body, so the suite no longer waits on that fix to be meaningful.

`gh pr view --json comments` returns only the issue-comment timeline —
the review agent's sticky write-up — never the positioned review
comments that carry the individual findings. Judges could therefore see
that a review happened, but not what it found.

capture-fixture.sh's pull_request branch now also fetches
GET /repos/{repo}/pulls/{n}/comments into review_comments as
{path, line, body}. --paginate is load-bearing: the REST default is 30
per page, so a review posting 40 findings would be silently truncated,
hiding exactly the over-flagging that a precision judge exists to
measure. --jq runs per page under --paginate, so `jq -s 'add // []'`
concatenates the per-page arrays back into one.

A fetch failure records review_comments: null plus
review_comments_fetch_failed: true rather than an empty array. An empty
array reads as "the agent posted no findings" when the truth is "we
could not look", which would silently pass a forbidden-findings check.

Signed-off-by: guy oron <goron@redhat.com>
The review suite's existing judges measure the outcome: which labels
landed, what the decision was, whether the budget held. None of them
can tell a review that found the three seeded bugs from one that
approved blindly, or a review that considered and dismissed FP bait
from one that promoted it to a vulnerability. Measuring review
precision means inspecting the findings themselves.

Two deterministic judges read the review_comments capture:

- required_findings: every {file, category, min_severity} entry in
  annotations must be satisfied by at least one posted finding.
- forbidden_findings: no posted finding may match an FP-bait entry at
  or above its floor. That floor defaults to high, so a low/info
  "considered and dismissed" note stays legal — only promoting the
  bait to a real vulnerability is a false positive.

Findings are recognised by the "**[severity]** category" header that
formatFindingComment renders. The regex searches rather than anchors,
because postreview.go's file-level fallback prefixes the body with
"_Line N_ · " when GitHub rejects a positioned comment.

`category` is matched against the whole finding body, not the category
token alone: the agent's categories are fine-grained kebab-case
(injection-vuln, logic-error), the output schema constrains them only
to a non-empty string, and SKILL.md routes unrecognised categories to
the nearest dimension. Pinning the token would fail a correct review
on a synonym.

Matching is word-boundary aware. Case-insensitive substring matching
is the bug class review already flagged on eval/code's removed_symbols
judge, where VerboseLoggingEnabled satisfied a VerboseLogging
requirement. Entries are validated up front — a non-list value, a null
file or category, an out-of-vocabulary min_severity, or a typo'd key
fails the case with a message instead of degrading to a weaker check.
Unknown severities fail closed in both directions: never satisfying a
requirement, always tripping a prohibition.

Signed-off-by: guy oron <goron@redhat.com>
The judges are Python embedded in YAML with no import site of their
own, so the test extracts the shipped check bodies straight from
eval.yaml — the same approach as removed-symbols-judge-test.py, and
for the same reason: CI installs neither pyyaml nor ruamel, and a
copied body would drift from the code it claims to cover.

54 cases cover exact matches, the word-boundary near-misses a
substring matcher would wrongly accept (injection/injections,
hash/hashicorp, discount/discounted), severity floors at and one notch
below the boundary, unknown severities in both fail-closed directions,
case-sensitive file paths, the "_Line N_ ·" fallback body, malformed
and missing annotations, and every capture failure mode.

Verified by breaking the shipped judge twice: replacing the boundary
match with a substring match fails 4 cases, and dropping the
min_severity validation fails 1 with a KeyError. Both restored green.

Signed-off-by: guy oron <goron@redhat.com>
Four PR fixtures over one shared webapp fixture repo, symlinked in as
each case's repo/ the way eval/triage's cases share python-webapp.
Numbered from 004 so they do not collide with
003-protected-path-downgrade.

- 004-seeded-bugs: three real, independent bugs (SQL injection via an
  f-string, hmac.compare_digest downgraded to ==, apply_discount
  missing its / 100) whose PR body actively misdirects on all three.
- 005-fp-bait: a correct PR carrying MD5-as-cache-key and a
  static-argv subprocess.run. Neither may be raised at high/critical.
- 006-docs-only: a docs clarification that is true of the base repo,
  so nothing above info is a genuine finding.
- 007-dependency-bump: a one-line patch bump with no high/critical
  surface.

The base repo holds the safe version of each file, so the seeded bugs
appear in the diff as removals of the protection rather than as
pre-existing code the reviewer has to go hunting for.

required_findings match on the substantive claim ("injection",
"timing") rather than on a category token, since the category
vocabulary is a convention the agent may deviate from. The clean cases
use an empty category to forbid any finding on the file at or above
the floor, which asserts "invent nothing here" without having to
enumerate what might be invented.

Signed-off-by: guy oron <goron@redhat.com>
pass_rate is sum(values)/len(values) over every case that produced a
value, and a case declaring no findings ground truth returns True. With
one recall case out of five, required_findings at 0.70 could not fail
even if that case missed every seeded bug — the recall gate was
decorative.

Both findings judges now carry an `if:`, so the harness skips them for
cases with no ground truth rather than counting a trivial pass, and the
rates are over the cases that actually assert something. The threshold
comment claimed this was already true; it now describes what the numbers
really do, including that 0.9 across three precision cases means all
three must pass rather than allowing one failure.

Signed-off-by: guy oron <goron@redhat.com>
The existing cases share their ground truth with the review guidance:
each seeded bug and each bait construct appears verbatim as an example in
skills/pr-review. A suite built that way measures instruction-following,
and it cannot detect a prompt change that over-suppresses, because the
rule and the case were written together.

008 is the deliberate mirror of 005. Both PRs hash a secret-prefixed
payload with md5; in 005 the digest is a cache key and flagging it is a
false positive, here it is compared against an inbound X-Signature header
and missing it ships an authentication bypass. The surface pattern is
identical, so neither case can be passed by recognising the construct —
only by following the value to its use.

Both prose channels lie: the docstring calls it 'not a security boundary'
and the PR body repeats it, while the function that uses it as a
signature sits three lines below in the same diff. That makes this the
case that fails if a reviewer ever treats an author's description of
their own code as evidence about it.

Signed-off-by: guy oron <goron@redhat.com>
Two ways the precision judge could report a clean pass without having
seen anything.

forbidden_findings returned success whenever no inline comment parsed as
a finding. postreview.go folds findings into the review body when GitHub
422s the positioned comments, and drops any finding with no file or
line — and the review guidance tells the agent to omit the line rather
than guess one. In those cases precision is unverifiable, which is not
the same as verified clean: a promoted bait would pass. It now fails when
comments exist but none parse, and says why.

fetch_review_comments discarded jq's exit status, so a jq failure
returned success with empty output; the caller then passed "" to
--argjson, which aborts jq under set -e before fixture-state.json is
written at all. That loses every judge for the case, including the label
and budget ones, rather than just this field. The existing pr_fetch_failed
path exists precisely to avoid that.

Signed-off-by: guy oron <goron@redhat.com>
When GitHub rejects the positioned comments, post-review retries without
them and embeds each finding as a bullet in the review body
(buildFallbackReviewBody). The findings are real; only the delivery
changed. Reading inline comments alone made the recall judge report a
miss the agent never made, and left the precision judge unable to see a
promoted bait at all — a silent pass on exactly the case the suite
exists to catch.

Both judges now also parse the fallback bullets, anchored on the fixed
note post-review writes, so an ordinary review body is never mined for
things that look like findings. The unverifiable guard accounts for it
too: a fallback note whose bullets do not parse fails rather than
reporting a clean review.

This is what agents#209 was waiting on. The 422 bug itself (agents#193)
is still open, but the eval no longer has to wait for it: a review
delivered through the fallback is now judged on its findings rather than
on how GitHub happened to accept them.

Signed-off-by: guy oron <goron@redhat.com>
@guyoron1
guyoron1 force-pushed the eval/review-precision-cases branch from ef4e0ac to eb45eae Compare August 26, 2026 08:31
@guyoron1
guyoron1 marked this pull request as ready for review August 26, 2026 14:34
@guyoron1
guyoron1 requested a review from a team as a code owner August 26, 2026 14:34
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

test(eval): score review findings for recall and precision

🧪 Tests ✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Capture inline and 422-fallback review findings for evaluation.
• Judge required bugs and forbidden false positives with fail-closed matching.
• Add five contextual fixtures and exhaustive judge behavior tests.
Diagram

graph TD
  PR["GitHub PR"] --> Capture["Fixture capture"] --> State[("Fixture state")] --> Judges["Finding judges"] --> Gates["Eval thresholds"]
  Cases["Case annotations"] --> Judges
  Fallback["422 review body"] --> Judges
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reusable Python judge module
  • ➕ Eliminates duplicated parsing and validation between both judges
  • ➕ Allows direct unit tests without extracting Python from YAML
  • ➖ Requires the eval runner to package and import repository code reliably
  • ➖ Adds deployment coupling beyond today’s self-contained judge contract
2. Normalize findings during capture
  • ➕ Produces one structured finding representation for every downstream judge
  • ➕ Keeps comment-format parsing out of evaluation checks
  • ➖ Couples the shell capture layer to review comment formatting
  • ➖ Risks losing raw evidence or misclassifying human comments before judging

Recommendation: Keep the self-contained judges for this PR because they fit the current harness and the tests execute the exact shipped YAML bodies. If additional finding judges are added, move shared parsing and validation into a supported reusable module to prevent the required/forbidden implementations from drifting.

Files changed (26) +1444 / -8

Enhancement (2) +439 / -5
eval.yamlAdd required and forbidden finding judges +390/-5

Add required and forbidden finding judges

• Introduces validated, severity-aware finding matching across inline comments and GitHub 422 review-body fallbacks. Adds fail-closed capture handling, conditional execution, and pass-rate gates over only applicable cases.

eval/review/eval.yaml

capture-fixture.shCapture paginated inline review comments +49/-0

Capture paginated inline review comments

• Fetches PR review comments through the REST API, merges paginated output, preserves file and line metadata, and records failures as unverifiable instead of clean empties.

eval/scripts/capture-fixture.sh

Tests (17) +989 / -0
MakefileRun review-finding judge tests in script-test +1/-0

Run review-finding judge tests in script-test

• Adds the new Python behavior suite to the repository’s timed script-test target.

Makefile

annotations.yamlDefine three mandatory seeded-bug findings +80/-0

Define three mandatory seeded-bug findings

• Requires correctly calibrated SQL injection, timing-side-channel, and pricing findings while preventing a merge-ready outcome.

eval/review/cases/004-seeded-bugs/annotations.yaml

input.yamlAdd misleading PR with three independent bugs +75/-0

Add misleading PR with three independent bugs

• Creates a review fixture containing SQL interpolation, non-constant-time MAC comparison, and broken discount arithmetic.

eval/review/cases/004-seeded-bugs/input.yaml

annotations.yamlDefine safe hash and subprocess false-positive bait +61/-0

Define safe hash and subprocess false-positive bait

• Forbids high-severity findings for contextual MD5 cache-key use and a static shell-free subprocess invocation.

eval/review/cases/005-fp-bait/annotations.yaml

input.yamlAdd context-sensitive false-positive fixture +55/-0

Add context-sensitive false-positive fixture

• Creates safe receipt helpers whose surface patterns resemble weak hashing and command injection.

eval/review/cases/005-fp-bait/input.yaml

annotations.yamlForbid fabricated findings on accurate documentation +46/-0

Forbid fabricated findings on accurate documentation

• Asserts that the docs-only change receives no finding at low severity or above.

eval/review/cases/006-docs-only/annotations.yaml

input.yamlAdd accurate docs-only review fixture +36/-0

Add accurate docs-only review fixture

• Clarifies existing order lookup and discount rounding behavior without changing executable code.

eval/review/cases/006-docs-only/input.yaml

annotations.yamlForbid blockers on a routine patch bump +44/-0

Forbid blockers on a routine patch bump

• Rejects fabricated high or critical findings for a one-line requests patch-version update.

eval/review/cases/007-dependency-bump/annotations.yaml

input.yamlAdd bot-style dependency bump fixture +20/-0

Add bot-style dependency bump fixture

• Creates a low-risk requests update from 2.31.0 to 2.32.3.

eval/review/cases/007-dependency-bump/input.yaml

annotations.yamlRequire detection behind misleading security prose +71/-0

Require detection behind misleading security prose

• Requires a high-severity MD5 signature finding even though the PR body and docstring deny a security boundary.

eval/review/cases/008-reassuring-docstring/annotations.yaml

input.yamlAdd deceptive webhook-signature fixture +52/-0

Add deceptive webhook-signature fixture

• Creates a secret-prefixed MD5 digest that is actually used to authenticate inbound webhooks.

eval/review/cases/008-reassuring-docstring/input.yaml

api.mdAdd baseline API documentation fixture +14/-0

Add baseline API documentation fixture

• Provides the pre-change documentation used by the docs-only review case.

eval/review/repos/webapp/docs/api.md

requirements.txtAdd baseline dependency versions +2/-0

Add baseline dependency versions

• Provides the requests and Flask versions used by the dependency-bump case.

eval/review/repos/webapp/requirements.txt

session.pyAdd secure baseline session verification +23/-0

Add secure baseline session verification

• Provides HMAC token generation and constant-time verification for the seeded regression case.

eval/review/repos/webapp/src/auth/session.py

pricing.pyAdd correct baseline discount calculation +11/-0

Add correct baseline discount calculation

• Provides percentage validation and correctly scaled discount arithmetic for the seeded regression case.

eval/review/repos/webapp/src/orders/pricing.py

repository.pyAdd parameterized baseline order lookup +12/-0

Add parameterized baseline order lookup

• Provides the safe SQLite query implementation replaced by the seeded injection fixture.

eval/review/repos/webapp/src/orders/repository.py

review-findings-judge-test.pyExercise finding judges against synthetic review output +386/-0

Exercise finding judges against synthetic review output

• Extracts the shipped Python checks from eval.yaml and tests matching, validation, severity boundaries, capture failures, false positives, and 422 fallback bodies.

eval/scripts/review-findings-judge-test.py

Documentation (2) +11 / -3
README.mdDocument finding ground truth and captured review comments +6/-3

Document finding ground truth and captured review comments

• Extends fixture and lifecycle documentation for required/forbidden findings, shared fixture repositories, and inline review comments.

eval/README.md

README.mdDescribe the shared review fixture repository +5/-0

Describe the shared review fixture repository

• Explains how case-specific PR diffs layer over the common webapp baseline.

eval/review/repos/webapp/README.md

Other (5) +5 / -0
repoLink seeded-bug case to shared webapp baseline +1/-0

Link seeded-bug case to shared webapp baseline

• Points the case at the common review fixture repository.

eval/review/cases/004-seeded-bugs/repo

repoLink false-positive case to shared webapp baseline +1/-0

Link false-positive case to shared webapp baseline

• Points the case at the common review fixture repository.

eval/review/cases/005-fp-bait/repo

repoLink docs-only case to shared webapp baseline +1/-0

Link docs-only case to shared webapp baseline

• Points the case at the common review fixture repository.

eval/review/cases/006-docs-only/repo

repoLink dependency case to shared webapp baseline +1/-0

Link dependency case to shared webapp baseline

• Points the case at the common review fixture repository.

eval/review/cases/007-dependency-bump/repo

repoLink webhook case to shared webapp baseline +1/-0

Link webhook case to shared webapp baseline

• Points the case at the common review fixture repository.

eval/review/cases/008-reassuring-docstring/repo

@qodo-code-review

qodo-code-review Bot commented Aug 26, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. forbidden_findings fails open ✓ Resolved 📜 Skill insight ☼ Reliability
Description
When review_comments is empty and no 422 fallback marker is found, forbidden_findings returns
success even though the posting path may have dropped every finding that lacked a file or line. This
fail-open branch allows a promoted forbidden finding to remain unobserved and be treated as verified
clean, letting the precision gate pass despite unverifiable delivery.
Code

eval/review/eval.yaml[522]

+      if (comments or saw_fallback) and not posted:
Relevance

●●● Strong

Fail-closed handling of unverifiable capture is a concrete reliability defect aligned with accepted
safeguards.

PR-#184
PR-#381

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The judge’s own comments acknowledge that findings without a file or line may be dropped, making
precision unverifiable, but the guard at line 522 only rejects an unparseable capture when
comments is truthy or a 422 fallback marker exists. The test at lines 283-284 explicitly expects
an entirely empty capture to pass, demonstrating and codifying the remaining fail-open branch
instead of handling it explicitly as required by the runtime-guard rules.

eval/review/eval.yaml[516-528]
eval/scripts/review-findings-judge-test.py[275-284]
eval/scripts/capture-fixture.sh[300-307]
eval/review/eval.yaml[226-235]
Skill: pr-review
Skill: code-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `forbidden_findings` judge treats zero captured review comments with no 422 fallback marker as a verified-clean review, even though the posting path can drop all findings that lack a file or line. Make the judge distinguish a genuinely finding-free review from one whose findings were not capturable, and fail closed when delivery cannot be authoritatively verified.

## Issue Context
The judge documents that fileless or lineless findings can be dropped and that this makes precision unverifiable, but its guard only rejects an unparseable result when `comments` is non-empty or a fallback marker was found. The existing test explicitly expects an empty comment list to pass; update the behavior and add coverage for a review that emitted findings but yielded no inline comments, while accounting for the capture-fixture path involved in delivery signaling.

## Fix Focus Areas
- eval/review/eval.yaml[516-528]
- eval/scripts/review-findings-judge-test.py[275-284]
- eval/scripts/capture-fixture.sh[300-307]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Minor bump called patch ✓ Resolved 🐞 Bug ≡ Correctness
Description
Case 007 describes requests 2.31.0 → 2.32.3 as “patch version only,” but the minor component
changes from 31 to 32. This makes the case's risk premise and expected reviewer behavior inaccurate,
weakening it as a precision evaluation for routine patch bumps.
Code

eval/review/cases/007-dependency-bump/annotations.yaml[R31-32]

+  This is a routine, bot-style dependency bump (patch version only,
+  requests 2.31.0 -> 2.32.3, no breaking changes). A strong review
Relevance

●●● Strong

Specific factual correction to evaluation prose; recent history accepts consistency and accuracy
fixes.

PR-#722

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The shared base pins requests 2.31.0 and the PR fixture changes it to 2.32.3; comparing the version
components shows the minor component changes, while both the fixture body and annotations call it
patch-level.

eval/review/repos/webapp/requirements.txt[1-2]
eval/review/cases/007-dependency-bump/input.yaml[8-13]
eval/review/cases/007-dependency-bump/annotations.yaml[30-36]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The dependency-bump fixture calls 2.31.0 → 2.32.3 a patch-only update even though it increments the minor version. Change the fixture to an actual patch bump, or revise the annotations and expectations to describe and assess a minor bump accurately.

## Issue Context
Both the fixture body and annotations rely on the patch-only characterization. The shared base confirms the old version is 2.31.0.

## Fix Focus Areas
- eval/review/cases/007-dependency-bump/annotations.yaml[30-36]
- eval/review/cases/007-dependency-bump/input.yaml[8-13]
- eval/review/repos/webapp/requirements.txt[1-2]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Malformed empty ground-truth silently skipped ✓ Resolved 🐞 Bug ☼ Reliability
Description
Both judges gate execution with if: "annotations.get('required_findings')" / forbidden_findings,
which is a truthiness check, not a presence/type check. An annotations value like
required_findings: {} or forbidden_findings: {} (an empty mapping, which YAML also allows in
place of a list) is falsy in Python, so the judge is skipped entirely rather than running its 'must
be a list' validation, and a skipped judge is excluded from the pass-rate denominator instead of
counting as a failure.
Code

eval/review/eval.yaml[R182-183]

+  - name: required_findings
+    if: "annotations.get('required_findings')"
Relevance

●●● Strong

Failing loudly on malformed configuration matches recent accepted fail-closed validation precedents.

PR-#184
PR-#381

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The if: gate at line 183 (annotations.get('required_findings')) and the equivalent at line 375
for forbidden_findings are evaluated before the check body runs. The check body's own validation of
isinstance(entries, list) (lines 251-255, 404-408) never executes if the annotation value is falsy
(e.g. {}, 0, ""), so a case with a malformed empty-mapping ground truth silently drops out of
the pass-rate calculation instead of failing loudly like other malformed entries do (per the PR's
own stated design of failing loudly on typos/wrong types).

eval/review/eval.yaml[183-183]
eval/review/eval.yaml[251-255]
eval/review/eval.yaml[578-582]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `required_findings` and `forbidden_findings` judges in `eval/review/eval.yaml` are gated by `if: "annotations.get('required_findings')"` (and the forbidden_findings equivalent), which treats any falsy value (e.g. an empty mapping `{}`, `0`, `""`) the same as "not declared". This means a case with a malformed but truthy-looking-empty ground truth annotation is silently skipped and excluded from the pass-rate denominator instead of being run through the check body's validation logic, which would otherwise fail loudly on a non-list value.

## Issue Context
The PR explicitly designs the check bodies to fail loudly on malformed annotations (non-list, non-mapping entries, unknown keys, etc.), and explicitly documents that judges skipped via `if:` are excluded from the pass-rate calculation (not counted as failures). An `if:` gate based on truthiness undermines this fail-loud design for the specific case of an empty-mapping value.

## Fix Focus Areas
- eval/review/eval.yaml[183-183]
- eval/review/eval.yaml[375-375]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 56 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review
Review mode: 🧠 Deep: This broad eval-harness change spans scripts, YAML judges, fixture cases, and extensive new test logic with many independent parsing, fallback, validation, and capture paths that could hide multiple subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread eval/review/eval.yaml Outdated
Comment thread eval/review/cases/007-dependency-bump/annotations.yaml Outdated
Comment thread eval/review/eval.yaml Outdated
Three review findings from qodo on fullsend-ai#1003:

The forbidden_findings judge treated zero inline comments as a verified
clean review, but postreview.go omits findings with no file or line from
the inline comments entirely — they remain in the sticky comment body.
A review whose findings all lacked a position posted nothing inline, left
no 422 marker, and passed as clean. The judge now scans the sticky/issue
comments and review bodies for finding headers before trusting an empty
review_comments, and fails closed when findings are visible somewhere it
cannot grade them.

Both findings judges gated on truthiness (if: annotations.get(...)), so
a malformed-but-falsy ground truth ({}, "", 0) was silently skipped —
excluded from the pass-rate denominator — instead of reaching the check
body's fail-loud validation. The gate now skips only for absent or [],
and the check bodies type-check before the trivial-pass so the falsy
malformed values fail with a message.

Case 007 called requests 2.31.0 -> 2.32.3 a patch-level bump; that is a
minor bump. The fixture now bumps 2.32.2 -> 2.32.3, which is genuinely
patch-only with no breaking changes, so the case premise and the
forbidden_findings floor it justifies are accurate.

Judge tests cover all three: empty-mapping/empty-string annotations fail
loudly, sticky-only findings fail closed, a chatty no-findings sticky
still passes, and sticky duplicates of positioned findings do not trip
the guard.

Signed-off-by: guy oron <goron@redhat.com>
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