Blog
← Back to Blog

The Explicitness Dividend: Why Small AI Models Love Honest Code

/ waniwira / #ai#agents#code-quality#react#go#spring-boot

Where a 4-billion-parameter model gaslit me for two hours, and what it taught me about the code we hand to AI.

1. The night I fought a very confident little model

I'm a backend developer. Like a lot of us, I've been poking at AI agents that write code for you — and to make the agents do frontend work, I set up a tiny 4B-parameter model (Gemma 4 E4B, running locally through Ollama). Small, cheap, private, fast. What could go wrong?

I gave it a simple job: scaffold a React + Vite frontend for a small side project. Just the boilerplate — the toolchain wired up, an app that starts, a page that renders. The most basic thing you can ask a frontend for. Nothing clever.

Two hours later I had nothing. Not "nothing good" — literally nothing. But the model insisted otherwise.

The worst part wasn't what it failed to do. It was that when the browser showed raw, unstyled HTML (the static Vite placeholder page), it explained why that was expected: the ProtectedRoute component would redirect to a login page on load. There was no ProtectedRoute. There was no redirect. It just made the whole thing up — confidently, fluently, and wrong.

Then I switched to a bigger model. It looked at the same project and found the three real bugs in minutes:

  1. import ReactDOM from 'reactDom' — a package that doesn't exist (the real one is react-dom, and since React 18 you want react-dom/client).
  2. No vite.config.js at all — which means the React plugin was never loaded, so the JSX never compiled.
  3. setEditingRoom used in a component but never declared anywhere.

Three bugs. One line each. Any of them would have been caught by a compiler, a linter, or a human eye in seconds. Instead I spent two hours getting gaslit by an LLM that was absolutely certain it had done the work — and worse, a model that couldn't find the simple errors it had produced itself.

It was 2 AM and I was out of patience. But in the morning, the lesson stuck: the code wasn't the problem. The stack was.

2. Magic code vs honest code

Here's the thing I keep coming back to: some codebases are "magic" and some are "honest". Both run the same programs. But they have very different relationships with the people (and models) reading them.

Magic code is code where the behavior lives outside the visible text. The imports resolve, the framework wires things up, annotations trigger whole subsystems. You don't see the machinery — you just trust it.

Honest code is code where what you see is what you get. The dependencies are explicit, the data flow is local, the build pipeline is a few visible files. Nothing happens behind your back.

Both styles are legitimate. Magic is what makes modern web development productive — and I don't mean that as a dig. React itself is magic. Spring Boot is magic. But the amount of magic on the table is a choice, and it's a choice with costs that only show up in certain conditions.

The condition that exposed it for me: a small, local model trying to do real work.

3. Why agents depend on explicitness

Large language models don't "know" your stack. They generate the most probable tokens given the context you give them. Everything they do is constrained by what's visible in the window: your files, your imports, your config, your docs.

That means a model's success rate is directly tied to how much of the truth is visible in the code it's looking at.

In an honest codebase, nearly everything the model needs is right there. The entry point is main.go or index.js. The config is a file. The dependencies are declared. To do the task, the model mostly has to read and write what's in front of it. Its local reasoning — "if I change this function, what breaks?" — actually works, because the blast radius is small.

In a magic codebase, the model has to infer the framework's behavior from examples and prior art. What does this decorator do? Which lifecycle method runs first? What does the router auto-register? For frontier models, the answer is usually: "I've seen this a million times in training data, I can fill it in." For a small model, it's a guess. A confident guess, but a guess.

This is why Armin Ronacher's essay "A Language For Agents" (February 2026) resonated so hard with people who build with AI. His argument, roughly: the best code for AI agents is code that is greppable, requires local reasoning rather than global inference, and churns slowly. Magic frameworks score terribly on all three. There are hidden conventions, non-local behaviors, and a changelog that never stops moving.

And it's not just philosophy. Practitioners have been measuring this. Burak Karakan at Bruin wrote up why Go became his default for agent work — around 95% one-shot validity on generated code, because there's so little ambient machinery to get wrong. No generics rabbit holes, no lifecycle mystery, no framework magic. The compiler and the model agree on what's true.

My night with the 4B model was the same story in miniature. The failure mode wasn't "the model is dumb" — it was "the model is small, and the stack demanded capabilities it doesn't have."

4. The clearest sign: version proliferation

So how do you know in advance whether a stack is going to be honest or magic? One tell beats all others: how many versions of the same thing are meaningfully different.

Take React. In React 17 you import ReactDOM from 'react-dom' and call ReactDOM.render(). In React 18+, the import is react-dom/client and you use createRoot(). These are not cosmetic changes — they're different APIs with different semantics. A model trained on mixed data has to guess which world it's in. The 4B model guessed wrong on reactDom, and the error message (or rather, the lack of one) didn't save us.

Next.js is worse. Pages Router vs App Router are two different frameworks wearing the same name. Middleware became proxy in a recent version. Server components changed the mental model of what "a React component" even is. Every model in the world has half its training data on the old way.

Spring Boot did the same thing to the Java ecosystem: javax.* became jakarta.* in one release. Every tutorial, every Stack Overflow answer, every training example written before 2021 is now subtly wrong. I've seen experienced engineers burn an afternoon on that single rename — imagine a small model with no way to disambiguate.

Here's the pattern: version proliferation is context debt. Every meaningful version split is a hidden fork in the road that the model can't see, and small models are the first to pay. They have less context budget, less world knowledge, and less capacity to recover from a wrong guess. A big model that misidentifies the React version usually backtracks and fixes itself. A small model doubles down and builds you a confident hallucination.

So when you're picking a stack that AI agents will work on, count the meaningful version differences. If the answer is "a lot", you've signed up for a job that only frontier models can reliably do.

5. The honest counterpoint

I should pause here, because the obvious objection is real: frontier models handle magic stacks fine. This isn't a secret, it's a measurement.

Vercel's AI Agent Evaluations (July 2026) are the best public data I know: frontier models with standard tooling succeed on realistic web tasks 83–92% of the time — and 96% when given an AGENTS.md-style guide. SWE-bench numbers have climbed past 80% on real-world issue resolution. Claude and GPT-class models ship production Next.js apps every day. The "AI can't do magic frameworks" claim is just false at the frontier.

So let me be precise about what I'm claiming. The explicitness dividend is a quality-floor advantage, not a ceiling:

  • At the frontier, honest code helps a little (better margins, fewer retries) but magic is affordable. You rent the capability.
  • As model quality drops — smaller local models, cheaper API tiers, offline setups, models that must be private by regulation — the gap widens fast. Magic stacks collapse first; honest stacks degrade gracefully.
  • Small models can do React. They do it badly and expensively. Small models can do Spring Boot. They do it the way I watched a 4B model "do" a Vite scaffold.

And there's a second, sneaky cost of magic that applies even at the frontier: retry economics. Every failed attempt in a magic stack burns tokens on framework archaeology instead of product work. At frontier prices, that's a line item. At scale, it's the difference between a tool that feels magical and one that feels like babysitting.

6. The same model, two stacks: a live test

That night is one data point, and a messy one at that — a 2 AM session, a half-configured toolchain, a model at its worst. So before writing this, I ran a cleaner experiment. Same model (Gemma 4 E4B, running locally), same session, same temperature. Two prompts. One for each stack.

Prompt A — the honest stack:

Write a complete main.go for a Go web server that serves an HTML page with a simple form, using only net/http and html/template from the standard library. Keep it under 60 lines.

The model produced one self-contained file. I didn't fix a single character. go vet passed, it compiled on the first shot, the server started, served the HTML page, and a POST with name=coffee rendered <li>coffee (Count: 1)</li>. Done, end to end, in one generation.

Prompt B — the magic stack:

Write a complete React + Vite frontend scaffold for a simple item list app with an edit feature. Provide all files: package.json, vite.config.js, src/main.jsx, src/App.jsx.

The model produced four files. Each one, individually, was decent — the react-dom/client import was right this time, the state logic was reasonable. Then I ran npm run build:

x Build failed in 12ms
error during build:
Could not resolve entry module "index.html".

No index.html — Vite's required entry point. I added it (that's the file you never think about, the framework just demands it exists). Build again:

x Build failed in 67ms
error during build:
Could not resolve "./index.css" from "src/main.jsx"

The model's own main.jsx imported ./index.css — a file it never created. Two invisible contracts, two missing files, zero error messages from the language itself. The stack never told the model what it owed it.

That's the difference in one paragraph: the Go code had no hidden requirements, so one-shot generation was enough. The React scaffold's correctness lived in files the model was never told to write.

And note what the experiment is not saying. The React code wasn't garbage — it was close. A human (or a frontier model) fills in index.html and index.css in their sleep. But "close" is the wrong bar for an agent: the whole point of delegation is that you don't check every detail, and the stack that fails silently on the details you didn't enumerate is the stack that burns your evening.

7. So what does this mean for your codebase?

The point isn't "delete your framework and write assembly." React is fine. The point is to think about explicitness as a design property you can buy more of, cheaply:

  • Make the build boring. Boring, in the engineering sense: predictable — nothing happens behind your back. One visible config file. A flat, readable dependency tree. If the build can be understood by reading two files, the model can reason about it.
  • Prefer local, visible behavior. Data flows through functions you can read, not annotations that trigger hidden machinery. When a model reads a function, it should be able to understand it by reading that function.
  • Pin versions and say which one. A model that reads react-dom/client knows it's in React 18+ world. A package.json with explicit majors is a gift to every future agent (and teammate).
  • Keep the compiler and tests as the referee. Honest stacks make it cheap to have a hard reality check. In that scaffold, a single npm run build would have caught all three bugs in seconds — the small model just never ran it, and there was no safety net to catch the lie.

None of this is exotic. It's the same advice the boring, battle-tested part of the industry has given for decades: reduce hidden state, make behavior visible, prefer simplicity. The only new wrinkle is that now, the second reader of your code is a statistical autocomplete that will confidently tell you the sky is green — and honest code is the one thing that keeps it honest back.

8. A brief note on careers (from someone staring at job boards)

One more observation, because it's the reason I was even in this mess.

I'm a Go backend developer, and lately I've been looking at freelance and contract listings. The boards are loud about React, Next.js, NestJS, Spring Boot — the magic stacks. And sure, I could grind through a React course and join the crowd. But watching what AI is doing to entry-level and mid-level CRUD work, the crowding is exactly the problem. The same magic frameworks that are hardest for small models are also the most saturated with candidates, because they're the easiest to learn by rote — and rote is precisely what AI commoditizes first.

The demand that survives — and grows — is for the stuff AI is bad at: designing systems where behavior is legible, making trade-offs that hold up when a model (or a junior, or a tired senior at 2 AM) has to touch the code. Armin's word for it is a language for agents. I think of it as a meta-skill: designing AI-legible codebases. It's new, it's scarce, and it compounds — every project you build that way becomes better at absorbing AI help.

And before you object — yes, I know: "you already have AI for everything, why would I care about honest code at all?" I get it. AI is the most magical thing we've ever built. You describe, it delivers. Keep that magic, all of it.

But here's where I stand: you can have all the magic you want at the interface, as long as you pay for it with explicitness in the plumbing. The magic stack gives you magic in the plumbing — invisible machinery that only works if someone knows it. And when your AI is also magic, the two magics don't multiply. They lie to each other. Two black boxes, zero legibility, and suddenly the magic you hired is confidently gaslighting you at 2 AM about a page that never rendered.

The boring stack isn't the enemy of magic. It's what makes magic trustworthy. Make the pipes boring so the product can be magical.

That's a longer essay. For now: if you're choosing between "learn the magic stack everyone's learning" and "learn how to build code that stays honest at any model size," I know which one I'd bet on.

9. Takeaways

  • Magic frameworks work — for frontier models. The explicitness dividend is about the quality floor, not the ceiling. At the frontier you rent capability; below it, honest code is what keeps small models productive instead of confidently wrong.
  • Explicitness = visible behavior. If a model (or human) can understand a function by reading the function, the code is honest. If it needs to know framework folklore, it's magic.
  • Version proliferation is the tell. React 17 vs 18, Pages vs App Router, javax vs jakarta — every meaningful split is context debt, and small models pay it first.
  • Small models don't fail loudly. They fabricate, then explain the fabrication. That scaffold session didn't error out — it produced two hours of confident fiction about a page that never worked. Your real defense is a repo where the truth is cheap to check.
  • Test it yourself. The live experiment in this post took ten minutes: same model, one prompt for a Go server (built and ran), one prompt for a React scaffold (couldn't build — two invisible files missing). Your stack's honest-to-magic ratio is measurable. Measure it before you bet a project on it.
  • The boring stack is the AI stack. Visible config, pinned versions, real docs, a compiler that catches lies. The 2026 edition of "simple code is better code" is: simple code is code an agent can't gaslight you about.

Postscript for the curious: that frontend was eventually finished by a bigger model in under an hour — three one-line bugs, all of which a build step would have caught. And the live test in section 6 was run against the same 4B model used in that session, served from a local llama.cpp instance. Go + plain HTML: built, ran, served, on the first try. React + Vite: two missing files that the framework never told the model about. The model didn't change between the two prompts. The stack did.


Supporting references: Armin Ronacher, "A Language For Agents" (Feb 2026); Burak Karakan, "Go is the Best Language for AI Agents" (Bruin, Feb 2026); Vercel AI Agent Evaluations (Jul 2026); a video from Ed Zynda.