← All opinions

June 12, 2026 · 13 min read

Writing Code for Readers Who Don't Remember

A style guide for AI-assisted development, and why each rule earns its place

AIengineeringcode style

We are all writing code with a new kind of collaborator now, and most of the advice floating around treats this as a tooling change. It is not. It is a change in who reads our code, and how.

So start with the reader. Picture a competent newcomer who has no memory of this project, navigates by search rather than recall, completes patterns confidently, and is sometimes wrong. That is a junior developer on their first day. It is also an AI model in a fresh session. The interesting thing is that these two readers fail in the same way and need the same help, which means you do not have to choose between writing for humans and writing for AI. You write for the memoryless reader, and you serve both.

This reframes the whole “AI-friendly code” conversation. We are not optimizing for the machine at the expense of people. We are giving up a crutch we leaned on for years: the accumulated mental context that let us get away with implicit code. The names that only made sense because we were there when they were chosen. The control flow that was obvious because we wrote it last Tuesday. AI did not create this debt. It just stopped paying the interest on our behalf, and now the implicit has to become explicit. That is the entire shift.

One principle sits above everything that follows. When “easier for the AI right now” genuinely conflicts with “maintainable for a human for years,” the human wins. Code is read far more times over its life than any single AI session lasts, and the day a subtle bug needs a person, you do not want a codebase only the model can navigate.


The analogy worth taking seriously, and where it breaks

Here is the argument I keep hearing, and it is a good one. We used to write assembly. Then we got high level languages that compile down to machine code, and we stopped caring what the assembly looked like. So will we reach a point where we stop caring what our Python or TypeScript or C# looks like, as long as we have tools that tell us what it does?

I think the answer is a qualified yes, and the qualification is the most important thing in this guide.

The reason we stopped reading assembly was not that we got good tools for reading assembly. It was that a higher level artifact became the source of truth, and the lower one became disposable output we never need to inspect. The contract moved up. And it worked for one specific reason: compilation is deterministic and verified. The same source produces the same machine code every time, and that mapping is trustworthy enough that you bet your production system on it without looking.

AI generation is not that. Not yet. It is neither deterministic nor formally verified, and natural language intent is ambiguous in a way that C source is not. So today, the code is still the contract, because it remains the only artifact that says unambiguously what will actually happen. You cannot yet point at a prompt or a spec and say “this is ground truth, the code is just output,” because the code can drift from your intent and nothing reliably catches it.

That gives us a clean test for every rule below. Ask: if generation were a trustworthy, deterministic compiler, and we treated the implementation as disposable, would this rule still matter? Where the answer is yes, the rule is durable, and it tends to point at the future. Where the answer is no, the rule is transitional, a bridge we need only while a human reading the source is still the verification mechanism of last resort.

The punchline, which surprised me: the durable rules and the transitional rules sort almost perfectly along one line. The durable ones are about things a machine can check. The transitional ones are about things only a human reads. A good AI-era style guide is mostly a project to move trust from the second category into the first.


Types and contracts: the cheapest specification you have

Start here, because this is the future layer hiding in plain sight.

Use types at every boundary. Make illegal states unrepresentable where the language allows it, so an enum the reader cannot misuse beats a string they can. Validate untrusted input once at the edge, then pass typed values inward and trust the interior.

Why this matters more than anything else on the list: a type is a specification that the compiler enforces for free. When the signature guarantees something, you do not need to read the body to believe it. That is exactly the property that let us stop reading assembly, and it is the property we are trying to recreate one layer up. As we lean more on generated code, types are how you make the implementation genuinely disposable. “Validate at the edges, trust the interior” is the same move written as architecture: parse once, and the entire interior becomes a place you can reason about without re-checking. If any section of this guide survives the transition fully intact, it is this one, because it gets more load-bearing as you read implementations less.

Tests: executable specifications, not an afterthought

Treat tests as primary documentation. Name them so a failure reads like a sentence: rejects_sync_when_cursor_is_stale. And cover the gotchas, the weird edge cases, the “it has to happen in this order” surprises.

Why it matters: tests are how a human verifies AI output today, and they are how the next reader learns what the code is supposed to do. In the short term, as generation stays imperfect, tests are the net that catches a wrong implementation. In the long term, as generation improves, tests become the thing you author and trust instead of the body. Either way the trend points up, not down. A test name that reads like a specification is a specification. This is the same future layer as types, expressed dynamically instead of statically.

Comments: the WHY, never the WHAT

Comment why, never what. The code already says what it does, and a comment that restates it drifts out of sync and quietly becomes a lie. // loop over users is noise. // vendor API rejects batches over 50, see TICKET-411 is gold. Document the things no reader can deduce from the code itself: invariants, ordering dependencies, the “do not touch this because” landmines. Delete stale comments on sight, because a wrong comment is worse than none.

Why it matters, and why this is more durable than it looks: the WHY is runtime and business context that cannot be inferred from the source by any reader, human or machine, and cannot be recovered by a compiler or a type checker. It is genuinely missing information, not redundant information. That is why it survives. One caution though, and it is the through-line of this whole guide: the strongest version of a WHY is one you promote into something enforced. An invariant in a comment is a hope. The same invariant in a type or an assertion or a test is a guarantee. Comment what you cannot yet enforce, and enforce what you can.

A specific anti-pattern to kill: comments that narrate intent for the model’s benefit, prose that restates what you are about to do so the AI feels oriented. It helps for exactly one session and then rots forever. If the code needs that narration to be understood, the code is unclear. Fix the code.

Error handling: fail loudly, fail early

Throw, log, crash in development. No silent catches, because an empty catch {} hides the one signal that would have corrected a bad assumption. And write error messages that name the thing and the expectation: “expected non-empty sync_id, got null in reconcileBatch” tells the reader precisely where their model of the world was wrong.

Why it matters: our memoryless reader is confidently wrong sometimes, and so is the model generating code. Loud failure is the runtime enforcement of your contracts, the dynamic partner to the static guarantees from types. It is, concretely, how a wrong generated implementation gets caught at all instead of silently corrupting state. Code that swallows errors lets bad assumptions survive, which is the most expensive thing that can happen when neither the author nor the reader was present at the relevant moment.

Indirection: do not hide the path

Minimize runtime indirection that cannot be traced statically. Deep inheritance, dynamic dispatch, reflection, metaprogramming, and event or signal spaghetti all hide the call path, and a reader following the code hits a dead end at the dispatch point. Use signals and events deliberately, document the wiring, and prefer a direct call when the decoupling buys you nothing. Aim for one obvious path through the common case, because if there are six ways control can reach a function, no one can reason about it.

Why it matters, and why it is more than a readability nicety: untraceable runtime indirection does not just defeat human reading, it defeats static analysis itself. The same dynamic dispatch that loses a person also makes the code path un-verifiable by tools. So this rule does not expire when we stop reading source by hand. It stays, because the future spec-and-verification layer needs to pin down what actually runs just as badly as you do. The reason simply upgrades from “humans get lost” to “nothing, human or machine, can determine the path.”

Naming and structure: the bridge layer

Give names that state intent rather than mechanics: retryableHttpClient over client2, unsyncedRecords over tmp. Avoid abbreviations that are not universal. Make names unique enough to grep, and avoid data, handler, process, and value as standalone names, because a grep that returns two hundred hits is navigation that no longer works. Pick one verb for one operation, fetch or get or load, and stay consistent. Keep structure predictable over clever: one concept per file, the file named after the concept, a layout a stranger could guess. Co-locate things that change together, and never let utils.js or misc/ become a junk drawer of unrelated functions.

Why it matters, and an honest caveat: this is the most transitional section in the guide, and that is fine. These rules exist almost entirely to help a reader navigate by search, which is exactly what a memoryless reader does today. A fully verified spec layer would not care what an identifier is called. But we are not there, search is how both your new hire and your model find their way, and a good name is the cheapest informal specification you can write. So invest here for now, with clear eyes that this is the bridge, not the destination.

Functions and the discipline of not over-abstracting

Write small, single-purpose functions with honest signatures, where the signature lets you predict behavior without reading the body and there are no hidden side effects. Prefer obvious code over clever code: if a one-liner needs a comment to explain how it works, write the three readable lines instead. And abstract on the third repetition, not the first.

That last one cuts against a real AI-era temptation, so it is worth dwelling on. There is an instinct to DRY everything immediately, to collapse every near-duplicate into a shared abstraction, partly because the model will happily do it. Resist it. Premature abstraction hides logic behind indirection the reader then has to chase, and duplication is far cheaper to read than the wrong abstraction is to untangle later. “Honest signatures, no hidden side effects” is the durable core here, because it is what lets the interface be trusted without the body. The rest is judgment that protects the humans who will maintain this.

A related trap: do not split a module just to fit a context window. That is optimizing for this quarter’s model limits, which are loosening quickly, and you will be left with conceptually fractured code long after the constraint is gone. Split on conceptual boundaries only.

The context layer: highest leverage, lowest cost

The cheapest and most reversible way to give a memoryless reader context is outside the code, not by reshaping the code. Keep a project-root context file, a CLAUDE.md or a strong README, that states your conventions, the architecture in three honest paragraphs, and the landmines. Add per-module notes for anything non-obvious: what the module owns, what it explicitly does not, how it wires into the rest. And when you have a choice, fix the context here rather than contorting the code, because scaffolding around the code is cheap to change while architecture is expensive to reverse. Reach for the cheap lever first.

Why it matters, with a caveat that keeps you honest: this is enormously high leverage right now, because it hands the model the mental model it lacks at almost no cost. But these files are prose, and prose is not enforced, so it carries the same rot risk as comments. It will always be a useful aid and it will never become the trusted ground truth that types and tests can become. Use it heavily, and do not mistake it for a contract.


What not to do in the name of “AI-friendliness”

A short list of moves that help the model for one session and tax humans permanently. Each is a transitional hack dressed up as a best practice.

Do not choose repetition over abstraction “because the AI handles explicit code better.” Past the third copy that is just duplication debt with a fashionable excuse. Do not write comments that restate intent for the model. Do not split files to fit context windows. And above all, do not let the AI produce code that only the AI is comfortable maintaining.

That last one is the one I would tattoo on the wall. Letting a tool generate code you cannot follow builds a dependency, and the dependency has a bill that comes due on the worst possible day: when a subtle bug needs a human, or the model fumbles the very file it wrote, and you find yourself stranded in your own codebase. This is exactly where the assembly analogy bites. We do not care that we cannot read assembly, because the compiler never fumbles and we can always regenerate from trustworthy source. AI is not yet that trustworthy or that reversible. Until it is, keep the code legible to you, not just to the tool. The rule expires the day generation becomes as reliable and deterministic as compilation, and not one day before.


The one test that survives everything

Before you merge, ask one question. Could a sharp newcomer with no project context understand this by reading and searching, without asking anyone? If yes, the AI can too, and so can you in six months.

That question is durable because it is really a proxy for something deeper: is the intent legible somewhere a reader can check? The only thing the transition changes is where “reading” points. Today it points at the implementation. As types, tests, and specs mature into a layer we trust, it will point there instead, and the body will finally become the assembly we no longer bother to read.

So here is the same test, rewritten for the world we are heading toward, which doubles as the goal of this entire guide: could a sharp newcomer understand what this code does, and what it does not do, from the contracts and the tests alone, without reading the body? On the day you can honestly answer yes, the abstraction will have moved up one more level, the way it did when we left assembly behind, and the analogy will finally be complete.

We are not there. But every rule worth keeping is a step in that direction, and you can tell the durable rules from the temporary ones by a single question: is there a machine that can check it? Build toward the ones where the answer is yes.


Want the rules without the argument? The condensed version is here: An AI-Era Code Style Checklist.