Skip to content
matthewcm.dev

    ↑ ↓ move · ↵ open · tab scopeopen graph →

    Make the System Explain Itself

    AI's best use on a codebase isn't only shipping features faster. It's structure: modules, less duplication, reusable functions and, above all, explicit interfaces so nobody has to work things out at runtime. Pin shape, behaviour and users' mental models with tests so you can change safely, then change the mental model on purpose, with a learning curve that's guided and obvious.

    Seedtended planted 5 min read

    🌱 A rough idea, planted recently. Expect rough edges — it will grow.

    On this page

    [!abstract] In one sentence Use AI to make the code explain itself through clear modules and explicit interfaces, pin what users have learned with tests, and when the system needs to get better, change the mental model on purpose with a guided, obvious learning curve.

    Most of the conversation about AI coding tools is about features: how fast you can go from ticket to pull request. That’s real, but it’s the smaller win. The bigger one is structural. An agent is very good at the patient, wide-reaching work most teams never get round to: splitting a tangled module along its real responsibilities, spotting the same logic written four slightly different ways across a codebase, and pulling it into one named, tested function.

    My favourite of these is the least glamorous: writing down how the parts of the system talk to each other.

    Stop working it out at runtime

    Runtime guessing vs an explicit interface. Without one, everyone rediscovers the shape of an object by logging, reading call sites, asking the author or guessing. With typed inputs and outputs, schemas at boundaries and one named contract, the code answers the question and the compiler keeps the answer true.

    In a lot of codebases the honest answer to “what does this function receive?” is “run it and see”. You add a log line, click through the flow, and read the object in the console. Then the next person does the same, and the one after that. The cost isn’t paid once; it’s paid by every reader, every time.

    “I didn’t know status could be null until it crashed in production.”

    “There are three functions called formatPrice. Which one is the real one?”

    “I spent the morning reading call sites just to find out which fields are optional.”

    (Illustrative, but you’ve probably said one of these.)

    An explicit interface turns that runtime discovery into something you can read:

    // Before: the shape lives in people's heads
    function submitOrder(order) { /* … */ }
    
    // After: the shape lives in the code, and the compiler keeps it honest
    interface OrderRequest {
      customerId: string;
      lines: OrderLine[];
      deliveryDate?: string; // ISO date; omitted = next available slot
    }
    function submitOrder(order: OrderRequest): Promise<OrderResult> { /* … */ }

    That one change pays off three times:

    1. Maintenance. The interface is documentation that can’t drift: if it stops being true, the build fails.
    2. Onboarding. A new joiner reads the contract, not twelve call sites, to understand what a module does.
    3. AI agents. An agent’s context is whatever it can read. Types and contracts put that context in the code, so it spends fewer tokens exploring and guesses less. It’s the same argument as Contract-Driven Development at the API layer, and TypeScript as a Team Contract inside the codebase.

    Put interfaces at real boundaries (module edges, service seams, anywhere data crosses from one owner to another), not around every function. And don’t abstract on the second copy of something: wait for the third. A wrong abstraction is more expensive than a bit of duplication.

    Pin three things before you change anything

    Three things to pin before you change anything: interface and contract tests fix the shape, characterisation tests fix the behaviour, and mental-model tests fix the order, grouping and flow users have learned.

    Interfaces fix the shape. Tests fix the behaviour. Together they’re what lets you restructure a system, or let an agent restructure it, without breaking anything functional. Characterisation tests are the starting point: they pin what the system actually does today, including the odd bits people rely on (Brownfield Agentic Engineering).

    But there’s a third thing that can break without any test going red: the mental model the application has taught its users.

    “I always fill in the dates first. Now it asks for the customer first and I keep losing my place.”

    Nothing failed. Every assertion still passes. Yet reordering two steps of a form changed how people think about the task, and every user paid a small cost in confusion and trust. Even changing the order of a few operations can shift a mental model.

    So I’d add a novel kind of test: a mental-model test. It pins the things users have learned rather than the things the code computes:

    • the order of steps in a flow
    • which operations are grouped together (and which are kept apart)
    • what’s always visible versus tucked away
    • the labels people navigate by

    These aren’t hard to write: they’re usually assertions about the order and grouping of elements in an end-to-end or component test. What matters is their purpose. When one fails, it doesn’t mean “bug”. It means “you’re changing what users know, so decide that on purpose.”

    But don’t let the old model hold you hostage

    Here’s the tension. If every mental-model test is sacred, the system can never get better. Staying with the legacy way of doing things just because that’s what the current ecosystem provides caps how good it can ever be. Real improvement will ask users to learn something. There’s going to be a learning curve.

    The job isn’t to avoid that curve. It’s to make it short, guided and obvious.

    Change the mental model on purpose: pin today's model, break it deliberately when there's a better way, make the new way obvious with colour-coded legends, numbered steps and grouped operations, then pin the new model.

    1. Pin it. Write mental-model tests for the flows people rely on.
    2. Break it deliberately. When a better design means changing the model, the failing test is your cue to design the transition, not to delete the test and move on.
    3. Make the new way obvious. A system that walks the user through it, or is obvious at a glance:
      • Colour-coded legends in charts, so nobody has to hover over every line to find out which is which.
      • Numbered steps in multi-stage forms, so people always know where they are and how far is left.
      • Clear separation of like operations: group the things that belong together, and keep destructive actions apart from everyday ones.
    4. Pin the new model. Update the test. The better way is now the protected way.

    The user voice tells you when it’s worked:

    BeforeAfter
    “Which line is which? I hover over each one to find out.”“Blue is mine, grey is the target. Got it.”
    “Am I on step two or step four?”“Step 3 of 4, nearly done.”
    “Where did Export go? It used to be next to Save.”“All the sharing stuff is together now. Makes sense.”
    “I deleted it by accident; the button was right next to Edit.”“Delete’s off on its own, so I don’t hit it by mistake.”

    (Illustrative quotes.) The “before” column is people working things out at runtime, in the UI rather than the code. It’s the same problem, and the same fix: make the system say what it is. Grow the Interface with the User covers the onboarding side of this: show each stage only what it needs.

    Where AI fits

    An agent can help at every step here, as long as the order is right:

    • Find it: duplication, tangled modules, implicit shapes that should be interfaces.
    • Pin it first: characterisation and mental-model tests before any change, written in a different session from the one that makes the change, so the tests encode reality and not the agent’s assumptions.
    • Change it in small pieces: structural refactors in their own small PRs, kept apart from features (Working with AI Coding Agents).
    • Leave it more readable than it was: every interface it adds is context the next agent, and the next person, gets for free.

    The best refactor AI can do isn’t the cleverest one. It’s the one that makes the system explain itself, to the people who maintain it and to the people who use it.

    • #ai-agents
    • #refactoring
    • #interfaces
    • #contracts
    • #typescript
    • #characterization-tests
    • #mental-models
    • #ux
    • #onboarding
    • #maintainability

    Nothing links here yet — it’s a fresh shoot.

    ↪ Links to (5)