The database analogy is usually wrong when people use it to explain AI.
A language model is not a database full of facts, documents, or answers. When you ask it a question, it is not looking up a record and returning the matching row. There is no table called “JavaScript bugs” or “how to design an API” hidden inside the model.
But the analogy is not useless. It is just usually applied at the wrong level.
The prompt is not a key that retrieves a stored answer. It is more like a fuzzy query into a compressed space of learned patterns. The model does not fetch the answer. It reconstructs a plausible continuation from the patterns that the prompt makes relevant.
This matters because people often mentally separate the prompt from the data.
“My instruction is the prompt. The codebase is the material the model works on.”
But the model has no such separation.
In a codebase task, the codebase is the prompt.
The surrounding code conditions the output just as much as the typed request does. The naming, the architecture, the hidden assumptions, the tests, the missing tests, the file layout, the comments, the abstractions, and the inconsistencies all become part of the query.
This became obvious to me after using Fable 5.
I was blown away. I blasted through two Claude Code subscriptions, one personal and one from work, and upgraded to the Max plan almost entirely because of Fable. I had personal projects lying dormant for years, not because I did not care about them, but because I could not find the time and energy to get back into the complexity they required. I had tried earlier models every now and then, asking them to add features or move these projects forward, and every time they failed hard.
With Fable 5, that changed.
It solved multiple problems that I had personally sat with for hours without success. More importantly, it solved them in a way I could understand. It did not just dump code on me. It helped me see the shape of the solution. For a while, it felt almost godlike. I was in heaven. The only thing keeping me from going completely crazy from lack of sleep were the token limits.
So I expanded the experiment. I used it on more personal projects, including multi-year-old projects I had not touched because of time constraints, life, and having small kids. Again, the same thing happened. Old projects started moving. Problems that had felt too expensive to re-enter suddenly became solvable.
But at work, I could not get the same fable boost.
At first, the obvious explanation was process. Maybe I was not committing to the tool in the same way. Maybe strict acceptance criteria, guardrails, review requirements, and company constraints prevented the same flow.
Then came the ban.
That pause gave me time to review what had actually happened. I looked back at code Fable had written for me. I also reviewed code from other developers that had been generated with Fable. The picture became more complicated.
Sometimes Fable had produced excellent code. Sometimes it had solved genuinely difficult problems. But sometimes it had produced bad code with obvious issues.
Eventually, something clicked.
The quality of the output was closely linked to the quality of the codebase it was working with.
That sounds obvious. But the solution is not obvious.
The simple reflex is to say: if the model performs worse in a bad codebase, give it more good code. Give it best practices. Give it clean examples. Give it architecture guidelines. Add counterweights to the prompt so the messy codebase does not dominate the answer.
But this does not help as much as one would hope.
The reason is that good code is local.
In a good codebase, encapsulation and clear interfaces let you understand a function from its signature and a small neighborhood around it. You do not need to read the entire system to safely change one piece. The relevant information is close by. The code compresses well because the same patterns repeat. The model can see the local shape and infer the missing parts.
Bad code is non-local.
A function depends on hidden global state. Behavior changes because of something in a distant file. A method only works if someone remembered to call init() first. A harmless-looking change breaks an unrelated feature because the real dependency is implicit. The important information is not near the code being changed.
This is where LLMs struggle.
The information the model needs to be correct may simply not be in its context window. Worse, neither the user nor the model necessarily knows which distant code is the relevant context to supply. So the model reasons from what it can see, fills the gaps with priors, and produces something confident and wrong.
This is why “just add best practices” often fails.
Best practices are not neutral. They also become part of the query. If you give the model a messy codebase and then add a clean example, you may think you are saying: “Fix this bad code using this good principle.”
But the prompt is saying: “Continue with this pattern.”
Those are different tasks.
The model can get pulled away from the actual problem. It may produce code that looks better in isolation but does not fit the hidden constraints of the system. It may follow the best practice while missing the reason the code became ugly in the first place.
A bad codebase is therefore not merely a hard query against a good prompt.
It is a bad prompt.
You are handing the model a worse input and asking for a better output than the input supports.
This closes the loop back to compression. The very property that makes code bad for humans — irregularity, inconsistency, special cases, high entropy — is the same property that makes it hard for a predictive model to work with. Good code compresses well because it is regular. A language model is, at bottom, a compressor of regularities. So regular code is its home turf, and irregular code is where it is weakest.
The thing that frustrates the human and the thing that defeats the model are one thing.
That is also why in-context learning is often misunderstood. The model is not learning your codebase in the way a developer does. It is not building a durable mental model. It is being temporarily conditioned by whatever fits into the prompt. If the relevant invariants are missing, implicit, or spread across the system, the model cannot reliably infer them just because you gave it more text.
And this also explains context rot.
Context rot is not merely what happens when the context window gets long. It is what happens when the query decays. Old assumptions, abandoned solutions, stale files, half-correct explanations, and irrelevant examples remain in the conversation. The model keeps trying to satisfy all of them because, from its perspective, they are still part of the prompt.
The practical conclusion is not that LLMs are useless on bad codebases.
It is that the fix is usually not a cleverer prompt.
The fix is to supply the non-local context the model cannot see, and to shrink the task until locality holds again. Instead of asking the model to fix the system, ask it to inspect one boundary. Instead of asking it to refactor a tangled module, first ask it to identify the hidden invariants. Instead of giving it generic best practices, give it the specific constraints that make this codebase weird.
The people who get real value from LLMs on bad codebases are often the people who already understand the code well enough to compress the chaos for the model.
The model rewards whoever can make the query sharp.
Do not just prompt the model to solve the problem. First use the model, and your own system knowledge, to find the smallest slice of reality where the problem becomes locally understandable.
Top comments (9)
The strongest move here is treating "compresses well" and "the model handles it" as the same fact seen twice. The regularity that lets a human skim a file is the same regularity that lowers the model's perplexity on it. That is not analogy, it is one gradient.
Where it leaves a door open: the fix bottoms out at "have a knowledgeable developer compress the chaos for the model." But if that developer is reading the same codebase as their only source, the compression is self-certifying. The code cannot tell you what it was supposed to do, only what it does. So the question under the question: when the human compresses, where does the constraint they encode come from, if not from the same prompt the model already failed to read?
That is the part I think is exogenous to the codebase, and it is the only part that is not also the prompt.
I think this is exactly the interesting crack in the argument.
The codebase can tell you what it currently does. It cannot reliably tell you what it was supposed to do, why it became that way, which constraints are accidental, which are sacred, and which “temporary workaround” from 2019 is now load-bearing infrastructure with a mortgage and two children.
That missing information is exogenous to the codebase. And I think that is the part where the human still matters a lot.
If the human developer has no more context than the model — no product history, no company intuition, no memory of past incidents, no sense of which customers scream the loudest, no knowledge of what the team values, no understanding of which other systems depend on this one — then yes, the human may just be compressing the same chaos into a more confident-looking shape. At that point the loop is not very magical. It is just two entities hallucinating in sequence, one of them with coffee.
But in practice, humans often do bring more than the code.
They have seen how the company works. They know which tradeoffs are usually accepted and which ones get rejected in review. They know that “clean architecture” is nice, but shipping the billing fix before Friday is nicer. They have seen neighboring projects. They know which patterns survived contact with production and which ones only looked good in the architecture diagram. They have intuition about where risk hides in this specific organization.
That context is not in the repository. It is not in the README. It is often not written down anywhere, because apparently we as an industry decided that the most important knowledge should live in Slack threads, incident memories, and the head of someone currently on vacation.
So I agree with the challenge: “have a knowledgeable developer compress the chaos for the model” only helps if the developer is injecting constraints that the model could not infer from the code alone.
But I think that is exactly the sweet spot.
The human is not valuable merely because they can read the same files more slowly. The human is valuable because they can add the missing outside context: intent, priority, history, risk, taste, politics, customer pain, operational scars, and all the other deeply unglamorous information that determines whether a change is actually good.
The hard part is that making this context available to the model is a huge undertaking. Most companies do not know how to do it yet. Many do not even know how to make it available to their own developers. The model exposes the same organizational problem, just with better autocomplete.
So maybe the practical version is:
LLMs are very good at working inside a compressed frame. Humans are still needed to choose and correct the frame.
The mistake is expecting the model to derive company intent from code alone. The opportunity is using humans to supply the exogenous constraints, then letting the model operate within them.
Not “AI replaces the developer.”
Not “the developer babysits the AI.”
More like: the developer turns organizational fog into a sharp enough query that the model can actually be useful.
Which is less glamorous than the marketing pitch, but probably closer to the real leverage.
You moved the crack to exactly the right place and then stopped one step short of it. Exogenous-at-input is not the same as exogenous-at-check. A human who turns the org fog into a sharp query has supplied a frame, but that frame is now consumed by the same loop that has to grade the result, and the only test left is whether the output is consistent with the frame the human authored. That is your two-entities-hallucinating-in-sequence problem again, just relocated: the human compresses the company into a constraint, the model executes inside it, and both agree because nothing outside the loop ever weighed in. The intent in the prompt has no counterparty until production hands it one. What makes the operational scars actually exogenous is not that they live in someone's head, it is that they have a payer: the incident that fires, the customer who screams, the neighboring team that vetoes in review. Those can contradict the output after the fact. A constraint supplied at write-time and never able to overrule the plausible result is indistinguishable from a confident guess in nicer words. So the human's load-bearing role is not supplying the frame, it is keeping the standing to reject what the frame produced against context the loop never saw. The frame is the cheap half. The veto is the one with a mortgage and two children.
Yes, that is a very fair push.
I do not think it replaces the previous point, though. It completes it. Apparently the human does not have one job in this loop. Of course not. Software would never be that kind.
There are really two human jobs here.
First, the human helps shape the frame: product history, company values, operational scars, weird customer constraints, team preferences, and all the context that is not sitting politely in the repository waiting to be embedded.
But second, the human keeps the right to veto.
Because a good frame is still only an input. It can make the model more useful, but it does not make the output true. The model can satisfy the prompt beautifully and still be wrong in the world, which is basically software engineering wearing a small hat.
So yes: exogenous-at-input is not enough. The loop also needs something exogenous-at-check.
And interestingly, the check side may be easier to partially automate than the frame side.
We already have tools for letting reality vote: tests, type systems, linters, code review, A/B tests, canary releases, feature flags, metrics, alerts, incident reports, customer feedback, rollback buttons, and the ancient enterprise ritual of “wait, why is conversion down?”
Those mechanisms are imperfect, but they have one huge advantage: they can contradict us.
A prompt cannot scream. Production can.
That makes checks special. They are not just more context inside the same hallucination soup. They are contact with the outside world. They can say: “Nice theory. Unfortunately, latency tripled and the billing team is now holding a tiny pitchfork.”
The frame tells the model what kind of solution to attempt.
The check tells us whether that solution survives contact with reality.
Both matter, but they are not equally easy to systematize. Framing requires translating messy organizational knowledge into useful constraints. That is hard because most companies barely manage to transmit this knowledge to humans, let alone to a model. It lives in Slack archaeology, incident folklore, customer scars, and the head of someone currently on vacation in Portugal.
Checks, by contrast, already have infrastructure. We know how to automate parts of them. We know how to run experiments, compare metrics, gate rollouts, detect regressions, and roll back bad changes. We do not need the model to perfectly understand the company if we have systems that can safely tell us when its output is nonsense.
So maybe the sharper version is:
The human’s value is not only compressing chaos into a prompt. It is also deciding which parts of reality get a veto.
And that is where experience becomes more valuable, not less. Experienced developers are better at framing the problem, but also better at knowing what must be checked before trusting the answer.
The model can work inside the frame.
The checks make sure the frame did not accidentally become a very confident bedtime story.
You completed it cleanly, and there is one more turn left in the check half. Your list of checks is only exogenous where the oracle has a payer the loop did not appoint. A test the same agent wrote is the frame again in assertion form. It passes because it encodes what the author already believed, and it goes green on exactly the case nobody thought to doubt. Type systems, production metrics, incident reports earn their veto from a source outside the change: the type checker was authored by someone who never saw your feature, production by users who never read your prompt. That is what lets them contradict you. The trap is the check that looks exogenous and is not. The eval the model wrote for itself. The assertion lifted from the same spec the code was built against. It runs forever at zero tokens and confirms the author to the author. So the check side is easier to automate, but only the part that was already external. The unautomatable half is deciding which checks have standing to overrule the result, and which are just the frame in a lab coat.
This article is not the full answer. It is the first constraint.
If the codebase is the prompt, then bad code gives the model a bad query. That is the point I want to land here.
The check question is the sequel.
And even there, tests are not “just checks.” They are also documentation, intent, and trace. Even generated tests have value because they show what the model thought mattered. They expose the shape of its interpretation. That makes them useful not only for validation, but also for review: “Ah, so this is the part of the problem the tiny silicon intern was staring at.”
But that belongs more to the quality argument: why checks, traces, tests, and feedback loops are not process overhead, but the infrastructure that lets you move faster without turning your codebase into a haunted escape room.
And then there is the third article: agents as actual outside pressure. Not just “the model wrote code and then nodded approvingly at its own homework,” but autonomous systems that inspect, run, compare, challenge, and validate from another angle. That is where my agent harness comes in — and I am already getting real value out of it!
The three-article split sounds right, and the second one is the half that is harder to write because the temptation is to keep extending the first. "The codebase is the prompt" is a strong-enough first constraint that you can spend a whole post just on its implications, and the check piece always feels like a smaller sequel even though it is actually doing more work.
One flag for the third article: tests-as-evidence has a quiet failure mode when the test author and the implementation author are the same model in the same window. The tests document the model's interpretation, which is what you said, but that is not the same as documenting whether the interpretation was right. The trace shows you what the silicon intern thought mattered. It does not show you whether they thought the right thing. That is where the outside-pressure harness earns its keep, because the gap between "what I checked" and "what I should have checked" is the part the same window cannot see.
Looking forward to the harness piece. The fact that you are already getting real value out of it means the third article will have receipts the first two had to gesture at.
Calling a bad codebase a bad prompt instead of a hard query is what makes this click for me. It quietly changes what the work even is. The job stops being how you word the request and becomes where you cut the task so locality still holds, and only someone who already knows the system can cut in the right place. The honest catch is the one you raised yourself, that nobody always knows which far-off file holds the real context, so half the work is finding that boundary before you can even shrink down to it.
Sometimes half the work is archaeology before engineering even begins. In legacy codebases this seems to matter even more now.
So maybe the practical rule is:
Do not just prompt the model to solve the problem. First use the model, and your own system knowledge, to find the smallest slice of reality where the problem becomes locally understandable.