Feeding the Model a Real Codebase: Files, Types, and Constraints That Actually Fit in Context
Good code from an AI depends less on the question and more on which files, types, and rules you put in front of it.
The failure mode nobody warns you about isn't the model being dumb. It's the model being confidently uninformed. Ask it to add a field to your API and it writes clean, idiomatic code against a data layer that doesn't exist, an error type it invented, and a validation library you removed six months ago. The output is competent. It's just answering a question about a different codebase, because that's the only codebase it could see.
Context selection is the actual skill in 2026. Tools have gotten good at retrieval, but retrieval is a default, not a guarantee, and knowing what to force into the window is still on you. Here's how to think about it.
The window is a budget, and relevance beats volume
Even with large context windows, more files is not more better. Two things degrade as you dump context: the model's attention spreads thin across irrelevant code, and you pay latency and money for tokens that don't help. A focused 2,000-token context of exactly the right files beats a 50,000-token dump of your whole module. The dump also raises the odds the model anchors on some unrelated pattern it happened to notice.
The mental model that works: include a file if the model would make a wrong decision without it. Not "might be nice." Wrong-without-it. That filter is stricter than most people apply, and applying it is most of the job.
The four buckets worth including
For a typical change, the context that earns its place falls into four groups:
- The types and interfaces the code must satisfy. The struct, the interface, the schema, the DTO. This is the highest-value context per token because it constrains the output shape directly.
- One or two real examples of the pattern you want. An existing handler, an existing repository method, an existing test. The model imitates far better than it invents to spec.
- The exact call sites that will use the new code, so the signature it produces actually fits its callers.
- Explicit constraints that live nowhere in the code: which dependencies are allowed, which runtime, which patterns are banned, what's deprecated-but-still-present.
What usually doesn't earn its place: entire files when you need one function, generated code, lockfiles, vendored dependencies, and anything the model can safely assume (it knows what a for-loop is).
Give it types, not prose descriptions of types
Developers routinely describe their data model in English when the actual type definition is shorter and unambiguous. Don't say "we have a user object with an id, an email, an optional display name, and a role that's either admin or member." Paste this:
type Role = 'admin' | 'member';
interface User {
id: UserId; // branded string, not a raw string
email: string;
displayName?: string;
role: Role;
createdAt: Date;
}The type carries the branded-id detail, the exact role union, and the optionality precisely. Prose loses all three. This extends to the whole dependency graph of a type: if the function returns a Result<T, E>, include the Result definition and the error enum, or the model will invent its own and it won't match.
Let the tool retrieve, then check what it actually pulled
Cursor, Claude Code, Copilot's workspace features, and their peers all do some automatic retrieval over your repo. That's genuinely useful and you should use it. But treat it as a first pass you supervise, not an oracle. Retrieval keys on similarity, and similarity misses things: the interface you need to satisfy may not be textually similar to your request, so it never gets pulled.
Two habits make this reliable. First, reference files explicitly when you know them. In Cursor and Claude Code you can name a path directly; do it rather than hoping semantic search finds it. Second, ask the model to state its assumptions about types and dependencies before it writes anything:
Before you write code, tell me:
- what type you expect getSession() to return
- which error type you'll return on failure
- what module you'll import the db client from
If you're guessing at any of these, say so and I'll paste the real one.The "say so" line surfaces exactly the gaps retrieval left. When the model guesses getSession() returns a plain object and it actually returns a Session | null, you catch it in one line instead of one review cycle.
Encode the durable rules once, in a file the tool reads automatically
Constraints that hold across the whole project shouldn't be retyped every prompt. Most 2026 assistants read a project instruction file: CLAUDE.md for Claude Code, .cursorrules or Cursor's rules directory, Copilot's instruction files. Put the non-obvious, project-wide rules there:
# Project conventions
- Error handling: return Result<T, AppError>, never throw in domain code.
- DB access only through repositories in src/db/repos. No raw SQL in handlers.
- We are on Node 20. No dependencies without discussion.
- Deprecated: the old `logger` module. Use `ctx.log`.
- Tests: vitest, colocated as *.test.ts, no global mocks.This is the difference between correcting the same mistake every session and correcting it once. Keep it short and specific; a 400-line rules file gets partially ignored and dilutes the important lines. The deprecation entries are especially valuable because they encode knowledge that's invisible in the current code: the old module is still there, still importable, still wrong to use.
For large repos, give a map before the territory
When a change spans several modules, the model needs to know the shape of the system before it can pick files intelligently. A short architecture note, or the output of a directory listing with one-line annotations, orients it enough to ask for the right pieces:
src/
api/ HTTP handlers, thin, no business logic
domain/ business logic, pure, returns Results
db/repos/ the only place that talks to Postgres
jobs/ background workers, triggered by the queue
The change touches domain/ and db/repos/. api/ stays as-is.That last line does real work: it tells the model where not to go, which prevents the scattershot edits that agentic tools produce when they don't know the boundaries.
Know the honest limits
Two things context can't fix. Retrieval and even large windows still struggle with truly global reasoning: a change whose correctness depends on invariants spread across twenty files won't be reliably reasoned through just because those files are all present. You still have to identify the invariant and state it. And stale context is worse than no context: if you paste a type definition that's since changed, the model trusts your paste over its retrieval and produces confidently wrong code. Paste from the current file, not from memory or an old chat.
The through-line is that these tools are excellent at working within constraints and terrible at inferring constraints that live only in your head or scattered across files they didn't see. Context engineering is the practice of making those constraints visible and current. Do that well and the model's competence finally points at your codebase instead of a statistical average of everyone else's.
A note on shelf life. AI products change fast. This guide deliberately focuses on the parts that stay true — how to judge a tool, what the trade-offs are — rather than ranking products that will have changed by the time you read it. Prices and feature claims should always be checked against the provider before you rely on them.