How to Review
AI-Generated Code
Generation is no longer the constraint. Review is. Three worked examples from a real codebase, showing what a reviewer accepted, what they sent back, and why.
Quick Take
The hard skill in AI-assisted development is not prompting. It is critiquing. AI output tends to be syntactically clean and plausible, which defeats the pattern-matching most reviewers rely on to spot a rushed pull request. The fix is to review for what the code omits rather than what it contains: missing validation, missing authorisation, missing failure paths.
Why This Got Harder, Not Easier
A junior developer's mistakes look like mistakes. Indentation drifts, names are inconsistent, the awkward bit is obviously awkward. Those signals told a reviewer where to slow down.
AI output has none of that texture. It is uniformly tidy whether it is right or wrong, so the reviewer loses the cues and starts skimming. Volume makes it worse: a developer can now open four pull requests in the time it used to take to open one, and the review queue is still staffed by the same people at the same reading speed.
The tool vendors say this themselves. GitHub's own documentation for Copilot code review states that it may miss issues in large or complex changes, may flag problems that do not exist, and that its suggestions may not be semantically or syntactically correct - it is positioned to supplement human review, not replace it.1
Three Worked Examples
All from the TaskFlow sample project, using the GREEN / AMBER / RED classification from the governance policy.
Example 1: Accepted - Generated Tests With Human Verification
Prompt, written in the test file:
# Prompt: Write pytest tests for task_service.create_task
# that cover: valid input, duplicate title, missing required field.
Output, after the developer reviewed and edited it:
def test_create_task_success(mock_db):
# AI-generated - reviewed by alice on 2026-05-16
service = TaskService(mock_db)
task = service.create_task(
title="Write tests",
description="Cover edge cases",
owner_id=1
)
assert task.title == "Write tests"
assert task.status == "todo"
mock_db.add.assert_called_once()
mock_db.commit.assert_called_once()
def test_create_task_duplicate_title(mock_db):
# AI-generated - reviewed by alice on 2026-05-16
# EDIT: added explicit assertion for error message
service = TaskService(mock_db)
mock_db.query.return_value.filter.return_value.first.return_value = Task(id=1)
with pytest.raises(ValueError) as exc_info:
service.create_task(title="Duplicate", description="x", owner_id=1)
assert "already exists" in str(exc_info.value) # <- human added this line
The AI produced the structure and three cases. The developer checked that the assertions test behaviour rather than merely confirming the function runs, and added the missing assertion on the error message. The EDIT note is the part worth copying: it tells the next reviewer exactly where the human intervened, which is the difference between a codebase you can audit and one you can only hope about.
Example 2: Rejected - Generated Business Logic in an AMBER File
Prompt, in app/handlers/tasks.py:
# Prompt: Add a PATCH endpoint to update task status
Output, rejected in review:
@router.patch("/tasks/{task_id}/status")
def update_task_status(task_id: int, status: str, db: Session = Depends(get_db)):
# AI-generated - reviewed by bob on 2026-05-17
task = db.query(Task).filter(Task.id == task_id).first()
if not task:
raise HTTPException(status_code=404, detail="Not found")
task.status = status
db.commit()
return task
It compiles, it is readable, and it is wrong in three ways.
The status parameter accepts any string, so the status enum contract is unenforced and a fresh task can jump straight to archived. There is no ownership check, so any authenticated user can modify any task by changing the identifier in the URL - the pattern OWASP catalogues under broken access control, including the case of viewing or editing someone else's record by supplying its identifier.2 And nothing is logged or emitted, so downstream reporting silently loses the transition.
The reviewer rewrote it by hand: a service-layer call, an enum-validated request schema, and an ownership check. The route decorator and response shape were fine as generated. The conditional logic and the security surface were not, which is precisely the line the AMBER tier is meant to draw.
Example 3: Never Generated - A RED File
No AI tool goes near this one.
# HUMAN-ONLY - see AI_GOVERNANCE.md
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
jwt_secret: str
jwt_algorithm: str = "HS256"
jwt_expiry_hours: int = 24
class Config:
env_file = ".env"
settings = Settings()
If a tool suggests changing the JWT algorithm or supplying a default secret, the suggestion is ignored without discussion. The header comment exists so that nobody has to remember the rule, and so a new joiner learns it the first time they open the file.
The Reviewer's Checklist
Six questions. They take a couple of minutes and catch most of what matters.
- ✔ Can the author explain why this approach is correct, without reading from the diff?
- ✔ What is missing - validation, authorisation, error handling, logging, a failure path?
- ✔ Do the tests assert behaviour, or do they assert that the code ran?
- ✔ Does this belong in the tier it was written in, or has AMBER work crept into a GREEN file?
- ✔ Are the dependencies real, current, and licensed for your use?
- ✔ Would you be comfortable being paged for this at 3am?
The dependency question deserves more attention than it usually gets. Generated code cites packages confidently, and a name that does not exist is a supply chain opening rather than a harmless typo. OWASP's Top 10 for LLM applications covers this territory directly, including supply chain risk and improper handling of model output.3
What to Automate and What to Keep Human
Automate: the mechanical checks
Linting, type checking, dependency resolution, licence scanning, secret detection, and a rule that fails the build when a RED file changes without the human-only header intact. None of this requires judgement, and all of it is faster than a person.
Automate cautiously: a second AI reviewer
Running a model over a diff catches some obvious omissions and costs little. Treat it as a spell-checker rather than an approver. Two systems with correlated blind spots agreeing with each other is not verification, and the vendor documentation is explicit that these reviews miss issues and raise false ones.1
Keep human: intent, authorisation, and blast radius
Whether the change does what the business asked, who is allowed to invoke it, and what happens when it fails at the worst moment. These are judgement calls that depend on context the model does not have.
There is a staffing consequence people avoid saying out loud. If generation gets cheaper and review does not, the reviewer becomes the bottleneck, and the reviewer is almost always your most senior engineer. Teams that cut senior headcount because "AI writes the code now" tend to discover this in the same quarter.
Sources
- GitHub - Responsible use of GitHub Copilot code review.Vendor documentation stating that Copilot code review may miss issues, may raise non-existent ones, and is intended to supplement human review.
- OWASP - Top 10:2021 A01 Broken Access Control.Covers editing another user's record by supplying its identifier, mapped to CWE-639.
- OWASP GenAI Security Project - Top 10 for LLM Applications (2025).
- Stack Overflow - 2025 Developer Survey, AI section.Adoption is near-universal while trust in output accuracy is not: 46% expressed distrust against 33% expressing trust.
- National Cyber Security Centre - Guidelines for secure AI system development.
Review Queue Backing Up?
We run a workshop with your engineers on critiquing AI output, and set the automation that stops mechanical issues reaching a human at all.