>_devkit
maintainability
skills/maintainability/

references/ai-era.md

Maintainability when most code is generated

Two separate jobs: reviewing code a model wrote, and structuring a repo so a model writes good code in it. The second one is the leverage.

The verification gap

The defining problem of this era, stated plainly:

Generation got roughly a hundred times faster. Review did not get faster at all.

Every practice below follows from that. The constraint on how much code you can safely ship is now your ability to verify, and if you accept more than you verify, the gap becomes a category of defect that nobody knows exists, in code nobody has read, in a system everybody assumes is understood.

The failure is not that models write bad code. They mostly write reasonable code. The failure is unreviewed code accumulating faster than understanding, and it does not announce itself. A codebase in that state looks fine, has passing tests, and cannot be changed by anyone.

The one rule for review

Never merge code you cannot explain.

Not "code that looks right", not "code that passes". Code you could stand up and describe: what it does, why it is there, what happens if it is wrong. If you cannot, you have introduced a dependency on something nobody in the organisation understands, and no test suite converts that back into knowledge.

This applies to you as the author. Generating a change and merging it because CI is green is the same act as merging a stranger's patch unread.

What generated code gets wrong

Not random. The failure modes are consistent enough to make a checklist.

Volume and defensiveness. Null checks for values that cannot be null, try/except around code that cannot raise, comments restating the line below, error handling for conditions the type system already excludes. It is all plausible in isolation and it is all liability. Cut it, and remember that a model asked to "handle errors" will produce handling for errors that do not exist rather than admit there are none.

Plausible-but-unused structure. Helper functions with one caller. Options objects where nothing sets the options. An interface with a single implementation. A model will build the general version of a problem you do not have, because generality reads as quality. Apply the rule of three from deletion.md.

Duplication. The most common and most damaging one. A model that cannot see your existing formatCurrency writes a second one, slightly different, in the feature it was working on. Nothing fails. Six months later there are four, they disagree on rounding, and the bug is in whichever one the invoice screen uses.

This is the failure to actively hunt, because it is invisible in a diff. The diff shows a new function that is correct. The problem is not in the diff.

# before accepting a new helper, look for what it duplicates
rg -n "def format_currency|const formatCurrency|function formatCurrency"
rg -n "toFixed\(2\)|Intl\.NumberFormat"

Convention drift. Given four patterns in a codebase, a model picks one roughly at random per file. Given one, it follows it. This is why consistency became a control input rather than a courtesy.

Confidently wrong on the local specifics. General knowledge is strong. Knowledge of your auth model, your tenancy rule, your rounding policy is inferred from context, and inference fails silently. The riskiest generated code is the code that touches a rule that only exists in your system, which is exactly where mongodb-production's tenant filter and zitadel-production's audience validation live.

Tests that assert the implementation. A test generated alongside the code it tests often encodes what the code does rather than what it should do. It will pass forever and catch nothing. The signal: a test that would need editing for any correct refactor.

Reviewing at speed

Since you cannot read everything at generation rates, spend attention where it pays.

Triage by blast radius, not by line count. Twenty lines touching the auth middleware deserve more attention than four hundred lines of a new isolated screen. Line count is the wrong sort order and it is the one every review tool gives you by default.

Read these first, always:

  • Anything touching authorization, tenancy, money, or personal data
  • Anything that changes a shared module rather than adding a leaf
  • Migrations, and anything that changes persisted shape
  • Configuration and CI, where a mistake is invisible until it is an incident
  • Deletions, which are easy to skim and easy to get wrong

Read the diff, not the summary. A model's description of its own change is a plausible account, not a verified one. It is usually accurate, and the cases where it is not are precisely the cases where the summary reads most reassuringly, because the same misunderstanding produced both the code and the description.

Ask for the reasoning separately. "Why this approach, what did you reject" surfaces more real problems than reading the code line by line, and it is fast. An answer that cannot name a rejected alternative usually means there was no choice made.

Make it prove itself. For anything non-trivial, the test should come from a different pass than the implementation, ideally written against the requirement rather than the code. A test and an implementation generated together share their assumptions, including the wrong ones.

Structuring a repo an agent works well in

This is where the leverage is, because it improves every future change instead of one.

One way to do everything. The highest-value property by a distance. Two competing patterns do not average out, they alternate, and the codebase becomes less consistent with every change. If there are two, pick one and record the choice.

Small, single-purpose files with names that say what is in them. A model retrieves by relevance. Everything in this catalogue that helps a human find code helps retrieval find it too: feature folders, one exported component per file, no utils.ts landfill. react-native-expo's "no barrel files" rule has a second payoff here, because an index.ts that re-exports forty things makes every import look identical and tells a retriever nothing.

A conventions file that is real. CLAUDE.md in the repo root, naming the stack, the layout rule, the commands to run tests and lint, and the three things people most often get wrong here. Keep it short enough that it is read in full and true enough that it is trusted. A stale conventions file is worse than none, because it produces confidently wrong code rather than uncertain code.

Rules at level 4 and up. From enforcement.md: a model cannot read your wiki, but it sees your eslint.config.js, your types, and the error output when a check fails. Every rule you move from a document into a mechanism becomes a rule the model actually follows, and gets corrected on automatically when it does not.

Tests that describe behaviour. They are the specification a model reads to learn what the system is for. Test names that read as sentences about the domain teach more than any comment.

Types at the boundaries. Parse untrusted input into typed values once, at the edge. Then a generated function deep in the system cannot mishandle a shape, because it cannot receive the wrong one.

Things worth doing that were not worth doing before

The economics changed, so a few old trade-offs flipped.

  • Writing the conventions file properly. It used to pay back once per new

hire. It now pays back on every prompt, in every session, forever.

  • Fixing inconsistency for its own sake. Converging two patterns into one

used to be low-value tidying. It is now a direct multiplier on the quality of everything generated afterwards.

  • Deleting dead code aggressively. It was mildly good hygiene. It now also

removes wrong examples that a model will faithfully imitate. Dead code is not inert any more, it is training data for your own repo.

  • Tests for boring code. Writing them was the expensive part, and it is not

any more. The bar for "not worth testing" moved a long way down.

And one that flipped the other way:

  • Clever, dense, hard-to-read code. It was always a bad trade. It is now

worse, because it degrades both the human and the machine reading it, and the saving it used to buy (less typing) is worth nothing at all.

The honest risk

The systems that will be hardest to maintain in five years are not the ones built without AI. They are the ones built with it, quickly, by teams who never established the structure, the enforcement, or the review discipline, and who therefore have a large, plausible, internally inconsistent codebase that nobody has read.

That failure is entirely preventable, and everything preventing it is in this skill. None of it is new. What changed is that the consequences arrive much sooner.