I have been experimenting with AI-assisted code review for a while.
The first attempts were not particularly useful.
They were not wrong, exactly. The reviews found things: a questionable name here, a function that might be extracted there, an opportunity for useMemo, perhaps an any that could be made more precise.
There was plenty of activity.
There was rather less reviewing.
What bothered me was the lack of proportion. A naming suggestion and a possible race condition could appear next to each other as though they deserved roughly the same amount of attention.
They do not.
That observation led to a small experiment.
Instead of asking an AI to find problems in code, I tried to describe how I wanted a review to proceed.
I ended up writing a code-review skill.
This is a story about what went into it, what changed along the way, and what the exercise taught me about code review itself.
The first version was a checklist
This seemed sensible.
Frontend code has plenty of things worth checking:
- TypeScript types
- React effects
- accessibility
- error handling
- performance
- tests
- state management
- API contracts
- naming
- duplication
So I wrote them down.
The result was comprehensive.
It was also not very good.
Given enough checklist items, an AI can nearly always find something to say. The difficulty is that software engineering is rarely about finding the largest number of technically defensible observations.
It is about deciding what matters.
Consider a small change:
useEffect(() => {
fetchUser(userId).then(setUser)
}, [userId])
There are several things one might say about these four lines.
But the interesting question, for me, is what happens when userId changes before the first request completes.
Request A starts for user 10.
The component changes.
Request B starts for user 11.
B completes first.
Then A completes.
The code is still short. The types may still be correct. The linter may be entirely content.
The UI may now be showing the wrong user.
That was the first useful shift in the skill:
Review the behaviour around the code, not merely the code in the diff.
A review needs an idea of what matters
Once I had noticed this, the checklist began to look like the wrong abstraction.
The skill needed priorities.
I eventually wrote this near the beginning:
Review code as a production risk audit, not a style pass.
That sentence became surprisingly useful.
It gives the review a direction.
A resource leak matters before a naming preference.
A broken permission boundary matters before duplicated code.
Losing a user's changes matters before whether a helper should be extracted.
An out-of-order response matters before an unnecessary allocation.
This does not make maintainability, performance or style irrelevant.
It simply gives them somewhere to stand.
The review starts with things that can break user trust or cause the running system to diverge from its intended behaviour: lifecycle leaks, asynchronous races, state integrity, permissions, persistence and other high-impact paths.
That ordering made the reviews noticeably quieter.
I considered that progress.
Contract before implementation
The next problem was more subtle.
How can a reviewer decide whether code is correct without first deciding what the code is supposed to do?
This sounds obvious when written down.
In practice, I had been skipping it.
So the first review gate became Contract.
The contract is not necessarily written in one place. It may be distributed across the pull-request description, types, tests, public exports, documentation, neighbouring code and the vocabulary of the project.
Even the word refactor contains a contract.
If I say:
This is only a refactor.
I am making a fairly strong claim:
Observable behaviour has not changed.
Similarly:
This improves performance.
contains another claim:
There is a measurable improvement, and the relevant behaviour has been preserved.
Once I started treating those statements as claims rather than descriptions, the review changed.
Instead of asking:
Is this implementation good?
the more useful question became:
What has this change promised, and what would convince me that the promise holds?
I like this framing because it removes quite a lot of personal taste from the conversation.
The diff is smaller than the change
Another thing I kept running into was the boundary of the pull request.
A prop changes in one component, but the behavioural change belongs to every consumer.
A query key changes in one hook, but the consequence belongs to the cache.
An API field changes in one type, but the actual surface may run through validation, mapping, persistence and several pieces of UI.
A new option is particularly interesting.
Adding it is easy.
Working out what every existing consumer does when it encounters that option is often where the real review begins.
So I added another gate: Impacted surface.
The instruction is essentially to follow changed values and behaviour through the system: props, state, API payloads, events, permissions, loading states, errors, cache keys, exports and side effects.
This is expensive compared with commenting on the changed lines.
It is also much closer to what I find myself doing when reviewing an important change manually.
The diff tells me where the edit happened.
It does not necessarily tell me where the change happened.
Time deserves its own review
Frontend software has another awkward property: much of its behaviour exists across time.
A component renders, mounts, updates and unmounts.
A request starts, gets cancelled, retries, resolves or becomes stale.
A form is initialised, edited, validated, submitted, rejected, reset and perhaps abandoned halfway through.
Looking at one moment in these processes hides a surprising number of bugs.
So the skill began to describe lifecycles explicitly:
Component
render → hydrate → mount → update → suspend → unmount
Async work
start → cancel → retry → resolve/reject → stale result → cleanup
Form
initialise → edit → dirty → validate → submit → error/success → reset
Data
fetch → normalise → validate → cache → derive → render → mutate → invalidate
These are not meant as universal state machines.
They are prompts for attention.
When an effect changes, I want the review to follow it through unmount.
When a form changes, I want to know what happens after the server rejects the submission.
When caching changes, I want to know how stale data eventually becomes fresh again.
This turned out to be one of the more useful additions.
Evidence changed the tone of the review
There was still a problem.
AI is rather good at producing plausible concerns.
This could cause unnecessary renders.
There may be a race condition here.
Consider memoising this value.
Each sentence sounds reasonable.
None of them is necessarily true.
I did not want to solve this by making the model more confident.
Quite the opposite.
I wanted uncertainty to become part of the output.
That led to the Evidence gate.
A correctness change should have a regression test, or a reason why one is impractical.
A behaviour-preserving refactor should have evidence that the relevant old behaviour still exists.
A performance improvement should have a measurement.
And when the available context cannot establish something, the review should say so.
For example:
I cannot establish whether this request is cancelled when the component unmounts. The caller lifecycle or a focused cancellation test would close that gap.
I find that much more useful than:
Potential memory leak.
The former tells me what is known, what is not known and how to reduce the uncertainty.
The latter mostly tells me that a memory leak is imaginable.
Perhaps unsurprisingly, this also made the reviews less argumentative.
Evidence is easier to discuss than confidence.
Refactoring needed a different rule
At some point I tried using the same skill for refactoring.
That exposed another distinction.
When I ask for a review, I want the system to challenge behaviour.
When I ask for a refactor, I usually want the opposite: improve the implementation while being extremely conservative about behaviour.
So refactoring acquired a rather strict constraint:
Zero behaviour change unless explicitly approved.
Public APIs, execution order, side effects, error semantics and observable identity all matter here.
Even a cleaner implementation is not an improvement if it quietly changes one of them.
This also changed how I think about abstraction.
Two pieces of code looking similar is not sufficient reason to combine them.
I now prefer to ask whether they share a contract and the same edge cases.
If they do, perhaps there is an abstraction.
If they merely happen to contain similar lines today, duplication may be cheaper.
What the skill looks like now
After several iterations, the structure became smaller than the original checklist.
Conceptually, it is roughly:
Contract
↓
Impacted surface
↓
Failure & divergence
↓
Evidence
↓
Lower-priority quality
There are specialised expansions underneath it for effects, forms, caches, permissions, dates, APIs, generated files, dependencies and other areas.
But those are secondary.
The sequence is the important part.
It tells the reviewer where to spend attention before telling it what details it might notice.
That distinction took me longer to see than I expected.
I thought I was writing instructions for an AI
The slightly unexpected part of this experiment is that I am no longer sure the skill is primarily about AI.
Writing it forced me to make some of my own review habits explicit.
Why do I sometimes open five files before commenting on one changed line?
Because I am tracing the impacted surface.
Why does an innocent-looking useEffect make me uncomfortable?
Usually because I have not yet accounted for its lifecycle.
Why am I reluctant to approve a refactor even though the new code is obviously cleaner?
Because cleaner is visible in the diff.
Behaviour preserving needs evidence.
Why do some reviews with fifteen comments feel less useful than reviews with one?
Because the number of observations is not the same thing as the amount of uncertainty removed.
I started with a fairly mechanical question:
How should an AI review frontend code?
I ended up with a different one:
What do I actually do when I believe I am reviewing code well?
The skill is my current answer.
I expect it to change.
That may be the most useful property it has.

Top comments (0)