Something has changed in the last two years, and most teams have not adjusted their architecture to account for it. Assistants now write, refactor and review a growing share of production code. The tooling improved dramatically. The codebases did not.

The common assumption is that a coding assistant is a productivity multiplier — that whatever your team was doing, it now happens faster. That assumption is half right, and the missing half is the expensive one.

An assistant is a multiplier on the structure it is working inside. Including when that structure is bad.

Point a capable model at a codebase with clear conventions and it produces code that looks like the rest of the system. Point the same model at a codebase where the conventions live in three people's heads and it produces something plausible. Not wrong exactly. Plausible. It compiles, it passes review because it looks like everything else, and it quietly adds another way of doing a thing that already had four ways of being done.

Give it a vague codebase and it produces vague code, faster.

Vague is not the same as bad

This is worth being precise about, because "bad code" is a distraction. The codebases that struggle most with assistance are frequently not bad. They are often written by strong teams, well tested, and running profitably in production.

They are vague in a specific sense: the system's rules are real but unstated.

  • The architecture diagram was accurate in month three. It is now decoration.
  • There is a right way to add a feature. It is not written down; it is learned by reading three existing features and averaging them.
  • Two services disagree about what a customer is, and both are right within their own boundary, and nothing anywhere says so.
  • A database row holds the current state and has forgotten every decision that produced it.

Humans navigate this well. A developer joining your team spends six weeks absorbing the unwritten rules, then produces work that fits. They ask someone at lunch. They notice a raised eyebrow in review. They build a model of the system that lives outside the system.

An assistant has no lunch and no eyebrow. It has the file you gave it, the files it can retrieve, and a very strong prior about what code like this usually looks like. When your conventions are unstated, that prior wins. It produces the industry-average version of your feature — a layered service class, a hand-written API client, a DTO mirrored on both sides — because that is what the training distribution says a feature looks like.

It will not tell you it guessed. That is the part that costs you.

Three places vagueness compounds

1. Between the layers

Most drift lives at boundaries. A C# type and its TypeScript counterpart, maintained by hand. A DTO that mirrors an entity until someone adds a field to one of them. Validation implemented twice, in two languages, with two subtly different notions of what a valid email is.

Every one of those is a place where the system can be internally inconsistent without anything failing. No test breaks. Nothing goes red. The inconsistency surfaces in production, as a customer's problem.

An assistant is exceptionally good at producing this class of bug, because from where it sits both sides look correct. It is writing the TypeScript. The C# is elsewhere. It has no mechanism that makes the mismatch visible — unless you built one.

2. In the state

A row that says status: 'cancelled' has thrown away everything that matters. Who cancelled it. When. Whether it was cancelled once or cancelled, reinstated, and cancelled again. Whether a human did it or a nightly job did it at 3am.

Ask a developer "how did this order get here?" and they will start reading logs. Ask an assistant and it will read the same logs, with less context, and reach a confident conclusion faster.

The problem is not that the answer is unavailable. It is that the system was never designed to be asked. It stores conclusions, not evidence.

3. In the shape of a feature

Ask five developers on the same team to add a feature and you will get five structures. Not because any of them is wrong, but because the correct structure was never written down anywhere a compiler could check it.

Then an assistant produces a sixth. It is internally coherent, it works, and it makes the codebase slightly more expensive to change forever.

This is the compounding one. Every inconsistency makes the next generation slightly worse, because the assistant is now averaging over a wider spread of patterns — including the ones it added.

The fix is not a better prompt

The reflexive response is prompt engineering: a longer system prompt, a more detailed instruction file, a carefully worded description of how we do things here. That helps, and it is not sufficient, for one reason:

A convention you can break silently isn't a convention. It's a suggestion.

A rule that lives in a markdown file is advisory. It is followed when the model attends to it and ignored when the context gets long. It has no teeth. You find out it was ignored in review, if the reviewer is paying attention, on a Friday.

The rules that actually hold are the ones that fail the build.

This is not a new idea. It is the same reason we moved from style guides to formatters, from "please write tests" to coverage gates, from "be careful with types" to type systems. We already know that the way you make a rule real is to make violating it stop the pipeline. Assistance did not change that principle. It raised the stakes, because code is now produced faster than it can be reviewed by the people who hold the unwritten rules.

What explicit structure actually looks like

Concretely — and this is what we build Cratis around — there are four things worth making explicit, in roughly this order of payoff.

One contract across the boundary. If your frontend types are generated from your backend types, drift stops being a class of bug and becomes a compile error. Rename a property in C#, rebuild, and the TypeScript stops compiling until you fix it. Nobody has to remember. Nobody has to review for it.

C# — one slice
[Command]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
    public AuthorRegistered Handle() => new(Name);
}

The TypeScript proxy for that command is generated. There is no second definition to keep in step, because there is no second definition.

Facts instead of conclusions. If the system records what happened rather than overwriting what is true now, "how did we get here?" is a query rather than a forensic exercise. This is what event sourcing buys, and the benefit that is usually undersold is not audit — it is that the system becomes answerable. To a regulator, to a support engineer, and increasingly to an agent trying to work out why the code does what it does.

One shape for a feature. If everything for one behaviour — the command, the events, the projection, the screen, the specs — lives in one place, then "add a feature" has a single correct answer, and both a new developer and an assistant can find it by looking at any existing feature.

Rules with teeth. Analyzers that fail the build on convention drift. Not a document describing the convention. A check that stops the merge.

None of these are AI features. Every one of them was a good idea in 2015. What changed is the cost of not having them: a vague codebase used to degrade at the speed of human typing. Now it degrades at the speed of generation.

The part people skip

There is a second half to this, and it gets far less attention than the code-writing half.

Your assistant can write a feature. Can it tell you what your system did last Tuesday?

For most systems the answer is no, and the reason is the same vagueness: the running system is not legible. There is no interface to it except logs, dashboards someone built two years ago, and a database you would rather nobody queried directly.

If your event log is inspectable through a defined interface, an assistant can answer questions about production without anyone handing it credentials to your database. It can browse what happened, replay a projection, and identify a stuck consumer — and it can do that as a read-only participant.

We think this direction is worth being deliberate about, so we made it one-way on purpose: operate, not mutate. Inspecting is open. Changing state still goes through commands and events, like everything else. History stays honest, whether a human or an agent is asking.

What this is actually about

It would be easy to read all of this as an argument about AI tooling. It isn't, quite.

Every problem described here existed before assistants. Unstated conventions, contract drift, state that has forgotten its own history, five structures for the same feature — these have been making systems expensive for decades. Teams absorbed the cost because it accrued slowly, at human speed, and because the people who held the unwritten rules were usually still around.

What changed is the rate. And a cost that was survivable at one speed becomes structural at another.

So the useful question is not "how do we get better output from the assistant?" It is the older question, made urgent:

How much of what your system knows is written down somewhere a machine can check?

If the answer is "most of it," assistance is genuinely a multiplier and you will pull ahead. If the answer is "it's mostly in people's heads," you are about to generate a great deal of plausible code on top of a model nobody has stated out loud.

The model is either in the system, or it is in your team's heads. Only one of those is something an assistant — or a new hire, or you in eighteen months — can read.