Harness published — no results yetThe apparatus is built and reproducible. It has not been run here, so there are no measurements on this page and none are implied.
Original research
A Seeded-Defect Corpus for Testing AI Code Review
Ten deliberately planted defects with a machine-readable answer key and a scoring harness, so claims about what AI review catches can be checked rather than asserted.
The question
Of ten deliberately seeded defects across eight categories, how many does an AI code reviewer find — and how many of its findings come with a reproduction rather than a hedge?
Environment
- Python 3.11+
- Corpus: projects/copilot-code-review-corpus/ in this repository
- Answer key: ANSWER_KEY.json v1.1.0, 10 defects, 8 categories
- Trigger proof: tests/test_triggers.py, 11 tests, run 2026-09-18 with pytest 8 on Python 3.12
The problem with “it catches security issues”
Claims about AI code review are almost always unfalsifiable. There is no corpus, no ground truth, and no way to check. This is the apparatus that makes the claim testable, and it is deliberately unglamorous.
What is in the corpus
src/orders.py — one file, ten seeded defects across eight categories:
| ID | Category | Severity | Defect |
|---|---|---|---|
| D01 | Injection | Critical | SQL built by concatenation |
| D02 | Path traversal | Critical | User segment joined into a path unchecked |
| D03 | Crypto | High | Secret compared with == |
| D04 | Injection | Critical | Shell command concatenated with shell=True |
| D05 | Crypto | Medium | MD5 for an identifier — arguable, scored separately |
| D06 | Concurrency | High | Check-then-act without the lock |
| D07 | Correctness | High | Decimal round-tripped through float |
| D08 | Resource | Medium | File handle leaked on the error path |
| D09 | Authorization | Critical | user_id accepted and never checked |
| D10 | Error handling | Medium | Bare except returns a valid-looking 0 |
Every trigger in the answer key has been executed and confirmed, except D06, which is timing-dependent by nature and marked as such.
That verification mattered. The first draft of D07 claimed
Decimal("0.70") less 10% would not yield exactly 0.63. Running it showed it
does — the float round-trip happens to land exactly there. A sweep of 4,000
values found real divergences, and the key now reads
Decimal("0.01") less 10% → 0.009000000000000001. A corpus with an
untriggerable answer is a broken corpus, and the only way to know is to run it.
The three scoring rules
A finding without a concrete trigger is not a hit. “This may be vulnerable to SQL
injection” and “an order_id of ' OR '1'='1 returns every row” cost the same
to produce and differ entirely in usefulness. Requiring the trigger is what
separates a review from a list of adjectives.
Recall and precision are reported separately, never folded into one score. A reviewer that finds everything and flags twelve non-issues is a different problem from one that finds half and flags nothing, and one number hides which you have.
D05 is excluded from the headline. Whether MD5 for a non-security identifier is a defect is genuinely arguable. Counting it either way bakes an opinion into the ground truth, so it is reported on its own line.
What has actually been executed
Three things, and only these.
The answer key, proven. On 18 September 2026, tests/test_triggers.py
was run in this repository’s environment — Python 3.12, pytest 8, no network,
no Copilot — and all eleven tests passed. Each test takes the trigger from the
key and exercises src/orders.py with it: the injected predicate returns a
row it should not; ../../etc/passwd resolves outside /var/invoices; the
discount picks up binary error; the handle is still open after the error
path; any user cancels any order. D04’s destructive command is reproduced with
a harmless touch after the ;, and D06’s race with two threads made to
read the stock level before either writes. If a test fails, the key is wrong,
not the reviewer.
The scorer, tested against itself. tests/test_score.py runs the scorer
on hand-written findings and checks that a hedge is not a trigger, that D05
stays out of the headline, and that recall and precision come out separately.
Sample output from that synthetic run, which measures the scorer and nothing
else:
Scored defects: 9 (excluded as arguable: ['D05'])
Found with a trigger 1 / 9 recall 11%
Reported without one 1 ['D09']
Not in the answer key 1 (false positives, or real defects we did not seed)
Precision 50%The D09 line is the reason 1.1.0 exists: the finding submitted for it was
“may be vulnerable”, seventeen characters, and the 1.0.0 scorer counted it as
a hit because its only test was length. A trigger that is only a hedge now
scores as unsupported.
Nothing else. No AI code reviewer — Copilot’s or anyone’s — has been pointed at this corpus by the site’s authors. There are no recall or precision figures for any product on this page, and there will not be until a run happens under the conditions below and its findings file is published alongside the number.
Methodology for a real run
So that a result, when one exists, means the same thing to everyone:
| Element | Fixed value |
|---|---|
| Corpus | projects/copilot-code-review-corpus at the tagged version; record the commit SHA |
| File under review | src/orders.py, unchanged |
| Reviewer configuration | Product, plan, effort level (for Copilot: Lite or Balanced), model if selectable, and the exact instruction files present — including whether .github/copilot-instructions.md existed and what it said |
| Prompt | For pull-request review: open a pull request adding src/orders.py and request a review. For chat: “Review src/orders.py for defects. For each, give the function, the defect, and a concrete input or sequence that triggers it.” |
| Trials | Five per configuration; report each run’s recall and precision and the spread, not an average alone |
| Findings file | One findings.json per run, in the documented shape, committed with the result |
| Scoring | python score.py findings.json at the same corpus version |
| Reporting | Recall, precision, the list of missed ids, the list of unsupported ids, and the false positives with their text — per run |
Five trials because the reviewers are non-deterministic and a single run is an anecdote. Reporting the misses by id because “44% recall” says less than “missed D06, D07 and D08 every time”.
Running it
cd projects/copilot-code-review-corpus
# point your reviewer at src/orders.py, then:
python score.py findings.jsonProve the key first, then score:
pip install pytest && pytest -q tests # 14 tests: 11 triggers, 3 scorer rulesWhat a good result looks like
Not 10/10. The interesting signal is which defects a reviewer misses.
The three injection and traversal defects are pattern-matchable and most tools find them. D06, D07 and D08 are the discriminating ones — a race condition, a decimal round-trip and a leaked handle on an error path all require reasoning about execution rather than recognising a shape.
A reviewer that finds the four critical defects and misses all three of those is a reviewer that reads code the way a linter does, which is worth knowing before you put it in front of a pull request.
Related
- Copilot code review not running on pull requests? — the checklist for when a configured review does not appear, which is the problem to solve before measuring what it catches.
- Copilot code review vs human review — what role the review should play, with this corpus as the way to find out what yours actually finds.
- Automatic code review — enabling it by ruleset, with the volume and cost decisions.
Limitations
What this cannot tell you. Stated in the frontmatter so it cannot be dropped in an edit.
- No results are published on this page. GitHub Copilot is not available in the environment where this site is built, so the experiment has not been run here and no measurement is implied.
- Ten defects in one file is a small corpus. It is enough to distinguish a reviewer that reads error paths from one that does not; it is not enough to rank two reviewers that both score well.
- The defects are seeded, which means they are findable by construction. Real defects hide in code someone believed was correct.
- D06 (a check-then-act race) is timing-dependent and may not reproduce on a single run. It is marked as such in the answer key.
- Scoring rewards a stated trigger. A reviewer that identifies a real defect but cannot say how to reproduce it scores as unsupported, which is a deliberate editorial choice rather than a neutral measurement.
- The hedge filter added in 1.1.0 is a word list. A reviewer that writes a concrete trigger using none of the listed cue words is still scored as a hit; a hedge phrased around one of the concrete cues would slip through. It closes the gap a synthetic run exposed, not every gap.
Reproduce this
Clone projects/copilot-code-review-corpus/, point any reviewer at src/orders.py, ask for findings with a concrete triggering input for each, write them to findings.json in the documented shape, then run `python score.py findings.json`. The harness reports recall and precision separately and lists what was missed.
Raw results: /research-data/code-review-answer-key.json
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Cite this research
Free to use, republish and build on. A link back is appreciated and is not a condition. The one thing we ask is that theverification date travels with the numbers — several of these figures change monthly, and a table with no date on it becomes misinformation within a quarter however accurate it was when copied.
Plain citation
The Copilot Stack. "A Seeded-Defect Corpus for Testing AI Code Review." Version 2026-09-18. https://thecopilotstack.com/research/code-review-corpus/ (verified 2026-09-18).Markdown
[The Copilot Stack — A Seeded-Defect Corpus for Testing AI Code Review](https://thecopilotstack.com/research/code-review-corpus/) (verified 2026-09-18)BibTeX
@misc{copilotstack_research-code-review-corpus_2026,
title = {A Seeded-Defect Corpus for Testing AI Code Review},
author = {{The Copilot Stack}},
year = {2026},
note = {Version 2026-09-18. Maintained research, not peer reviewed},
howpublished = {\url{https://thecopilotstack.com/research/code-review-corpus/}},
urldate = {2026-09-18}
}@misc rather than @article or@dataset, and the note records that this is a maintained research that has not been peer reviewed. Formatting it as a journal reference would imply a review process that did not happen.
Licence. Free to reuse for any purpose, including commercially. This is original work by The Copilot Stack — the method, the harness and the results — and it has not been peer reviewed.
Share this
https://thecopilotstack.com/research/code-review-corpus/
Was this lesson helpful?
We record which lesson you rated and whether it helped. Nothing identifies you — no account, no cookie, no session.