Coding With AI
Get real work out of an AI coding assistant: small steps, the right files and goal, a careful read of every diff, and tests that define what done means.
On this page
You ask an AI assistant to "add CSV export to the reports page". A minute later it hands back 300 changed lines across six files. You skim it, it looks reasonable, and the page works when you click the button. You merge it.
A week later a colleague finds that the change also rewrote a date helper in another file, and invoices now show the wrong month. The assistant did not do anything strange. It made a big change, and nobody read all of it. The model types faster than you can check, so the limit on safe progress is how much you can review, not how much it can write.
Work in steps you can check
The fix is to size each step to what you can read and test in a few minutes. Instead of "add CSV export", you ask for one piece at a time: first a function that turns rows into CSV text, with tests. Then the button that calls it. Then the file download.
Each step follows the same loop:
- Say the goal and give the files. The model can only use what is in its context. Paste or attach the code it will touch and the code it must fit with.
- Get one small change. One function, one bug fix, one file if you can.
- Read the diff, the list of lines it added and removed. Every line, before you run anything.
- Run the tests. If something fails, paste the real error back.
- Commit it to version control. Now you have a safe point to return to if the next step goes wrong.
Give it the files and the goal
Here is a request that leaves the model to guess almost everything.
That reply looks fine at a glance. It has three problems: it picks the wrong middle for an even number of values, it sorts the caller's array in place, and it returns undefined for an empty list. The model could only have known about the first. The other two are problems because of how your code uses the function. Compare a request that carries the situation.
The second request names the goal, the files, the rules that are specific to your code, and how the result will be checked. "Do not change the input array" is the kind of fact only you know. The model cannot read it from the world.
Tests are the contract
Tests turn "it looks right" into "it does what I said". Write them first, or have the model write them first and then read them carefully, because a wrong test makes wrong code look correct. Each test is one promise the code must keep.
Step through what happens when the five tests from the brief run against three versions of the function.
- tests
expected v1 v2 v3 [3, 1, 2] 2 [4, 1, 3, 2] 2.5 [7] 7 input unchanged yes [] throws yes - version
- none yet
1The contract: five tests that pin down what the brief asked for. Nothing has run yet.
- tests
expected v1 v2 v3 [3, 1, 2] 2 2 [4, 1, 3, 2] 2.5 3 [7] 7 7 input unchanged yes no [] throws yes no - version
- v1
- passed
- 2 / 5
2v1 is the quick answer from the first prompt. It sorts and takes the middle. Odd lengths pass. For four values it returns 3, the upper middle, not 2.5.
- tests
expected v1 v2 v3 [3, 1, 2] 2 2 [4, 1, 3, 2] 2.5 3 [7] 7 7 input unchanged yes no [] throws yes no - version
- v1
- next
- paste errors back
3It also fails the other two.
arr.sortsorts the caller's array in place, and an empty list returnsundefinedinstead of throwing. You paste the three failure messages back, word for word. - tests
expected v1 v2 v3 [3, 1, 2] 2 2 2 [4, 1, 3, 2] 2.5 3 2.5 [7] 7 7 7 input unchanged yes no no [] throws yes no yes - version
- v2
- passed
- 4 / 5
4v2 adds the empty check and averages the two middle values. Four pass. It still calls
sorton the input, so the chart would get a sorted array. - tests
expected v1 v2 v3 [3, 1, 2] 2 2 2 [4, 1, 3, 2] 2.5 3 2.5 [7] 7 7 7 input unchanged yes no no [] throws yes no yes - version
- v2
- next
- sort a copy
5This is the bug a quick read misses: the return values are all correct. Only the test that checks the input afterwards catches it. That test exists because the brief said the caller reuses the array.
- tests
expected v1 v2 v3 [3, 1, 2] 2 2 2 2 [4, 1, 3, 2] 2.5 3 2.5 2.5 [7] 7 7 7 7 input unchanged yes no no yes [] throws yes no yes yes - version
- v3
- passed
- 5 / 5
6v3 sorts a copy. All five pass. You read the final diff once more, see that only
stats.jsand its test file changed, and commit.
Here is v3 with its tests. Every requirement in the brief has a line that checks it.
const assert = require("node:assert");
function median(nums) {
if (nums.length === 0) throw new Error("median of empty list");
const sorted = [...nums].sort((a, b) => a - b); // copy: callers reuse nums
const mid = Math.floor(sorted.length / 2);
if (sorted.length % 2 === 1) return sorted[mid];
return (sorted[mid - 1] + sorted[mid]) / 2;
}
assert.strictEqual(median([3, 1, 2]), 2);
assert.strictEqual(median([4, 1, 3, 2]), 2.5);
assert.strictEqual(median([7]), 7);
const times = [120, 80, 95];
median(times);
assert.deepStrictEqual(times, [120, 80, 95]); // input not changed
assert.throws(() => median([]));def median(nums):
if not nums:
raise ValueError("median of empty list")
ordered = sorted(nums) # copy: callers reuse nums
mid = len(ordered) // 2
if len(ordered) % 2 == 1:
return ordered[mid]
return (ordered[mid - 1] + ordered[mid]) / 2
assert median([3, 1, 2]) == 2
assert median([4, 1, 3, 2]) == 2.5
assert median([7]) == 7
times = [120, 80, 95]
median(times)
assert times == [120, 80, 95] # input not changed
try:
median([])
raise AssertionError("expected an error")
except ValueError:
passIn Python the in-place bug looks different (nums.sort() versus sorted(nums)), but it is the same mistake, and the same test catches it.
Read every diff
Reading a diff is faster than writing the code, as long as you read with questions in mind:
- Did it change anything I did not ask for? Renamed variables, reformatted files and "small improvements" elsewhere are where surprises hide.
- Did it touch the tests? A loosened assertion or a deleted test makes the run go green without fixing anything.
- Did it add a dependency? Check that the package really exists and is the one you think. Models sometimes invent plausible names, and Privacy and Security explains why attackers care.
- Does it hide errors? An empty
catchblock or a default value that swallows bad input turns a loud bug into a quiet one. - Could I explain every line to a colleague? If not, ask the model to explain it, then check the explanation against the code.
Try it yourself
This request produced a function that worked on the example but broke the app in two places. Rewrite it so the model knows the goal, the files, the rules and how the result will be checked.
Show a sample answer
One stronger version:
Why it works: it says where the function is used, so the model knows the format matters. The examples double as tests and settle the edge cases (small amounts, zero, refunds) that a quick answer would miss. Asking for the tests first lets you check the contract before any code exists.
Common mistakes
Check yourself
An assistant returns a 400-line change across eight files for a feature you asked for in one sentence. What is the best next move?
In the tracer, why did v2 pass four tests but still have a bug?
After you report a failing test, the model's next diff changes assert.strictEqual(median([4, 1, 3, 2]), 2.5) to expect 3. What does that tell you?
What to practise next
Next time you use an assistant on real code, write the tests (or read its tests) before you look at the implementation. Then continue with Verifying AI Output, which covers checks for facts, numbers and sources as well as code.