Most "AI doesn't write good code" complaints come from prompts of the form "write a function that parses dates from strings." That prompt has no specification, so the model invents one — and invents one differently each time. The fix isn't a better model; it's a tiny spec.
What a spec for a function looks like
Five lines, written before the prompt:
Function: parseDate(input: string): Date | null
Inputs: ISO 8601, RFC 2822, "YYYY-MM-DD", "MM/DD/YYYY"
Output: Date in UTC, or null if unparseable
Errors: never throws — return null
Edge: trim whitespace; "00/00/0000" → nullThat's it. The model now has something to satisfy, and you have something to verify against.
Ship the spec into the file
Paste the spec as a JSDoc / docstring above the function the model produces. It documents the contract, and the next time anyone (human or AI) touches the function, they have the original intent in front of them.
Generate the test file at the same time
Same spec; produce the test file in the same conversation. Now you have a function and tests that share the same source of truth. If the tests pass and the spec is right, you're done.
Tip
Use the spec as the assertion list directly: "test should return null for '00/00/0000'" maps line-for-line to a test case. Skip the "now write tests for this code" round-trip.
When the spec is the wrong size
If the spec is two paragraphs, the function is too big. Decompose first: "What are the smaller functions I'd compose to build this?" then write specs for each.
If the spec is 30 lines, it's not a function — it's a system. Move up to architectural design and write specs for the interfaces between pieces.
The cultural shift
Teams that write specs first end up with more readable code, more useful tests, and fewer arguments in code review. The spec is the artifact reviewers actually want to debate, anyway. Putting it on paper before the code surfaces the design decisions early — which is the entire point.