Documentation is beating your AI stack
Companies are spending heavily to give AI architectural awareness: vector embeddings, semantic search, retrieval pipelines. Good engineering already solved most of it. Put the context next to the code, and let your rules point upstream, and the model honors your architecture with no retrieval system at all. A consolidation of the inline-documentation and defined-contract pieces.
Everyone is solving AI consistency with bigger systems. More retrieval. More indexing. More context injection. Vector embeddings of every file, semantic search over design docs, custom agents that walk dependency graphs before generating a line of code. And yet the results still drift.
The problem is not intelligence. It is what the model can see, and what it is forced to guess about everything it cannot.
There is an expensive way to address that and a cheap one. The expensive way is to feed the model more of the system on every request, which is most of what the current wave of AI tooling is selling. The cheap way, the one good engineering arrived at decades ago, is to put the context where the model is already looking and to write down what it would otherwise infer. Two disciplines do almost all of the work: keep context next to the code, and place shared rules where they belong with pointers that run upstream.
This piece consolidates two articles in a series. The throughline across both is the same: reduce what the AI has to infer. Inference is where models fail, not because they cannot reason, but because every inference is a coin flip weighted by whatever appeared most often in training data, which is mostly code written by developers who had not yet had the breakthroughs that produce good architecture. Every rule a senior engineer would infer correctly is a rule the AI will infer incorrectly often enough to matter. The fix is not to hope the model gets it right. The fix is to write down what would otherwise be inferred, in the place the AI is already looking.
The real constraint: the context window
AI does not "know" your system. It only knows what it can currently see.
That view is limited by the context window, a fixed amount of code, instructions, and documentation that fits into a single request. Once that space fills up, something gets pushed out. Not flagged. Not degraded. Just gone.
You think the AI is following your architecture, but the rule that defined it might have fallen out of context three steps ago. What you get back still looks valid. It compiles. It even feels reasonable. But it is subtly wrong in ways that are hard to trace. This is context overflow, and it is the root of most inconsistency people blame on AI.
The natural reaction is to add more information. Teams dump documentation into prompts, wire up retrieval systems, or try to build a "perfect" context layer that feeds the AI everything it needs. That sounds right until you see the trade being made. The more you add, the faster you hit the limit. The more you rely on external context, the more fragile the system becomes. You are constantly playing a game of what fits and what gets dropped. Instead of solving the problem, you are accelerating it.
Movement one: move the context to the code
Stop trying to bring context to the AI. Start putting context where the AI is already looking, the code itself.
Inline documentation, the kind people used to avoid, becomes incredibly effective here. Not because it explains syntax, but because it carries intent alongside implementation. When the AI opens a file or edits a function, it does not need to search for meaning. It is already there. External documentation disappears first when context overflows. Inline documentation stays attached to the code being worked on.
XML documentation in C# and JSDoc in JavaScript were originally built for humans and tooling like IntelliSense. Now they double as structured signals for AI. They are predictable, consistent, and easy to parse. A well-written summary block is no longer just a description, it is instruction.
/// <summary>
/// Processes a payment and updates ledger state.
/// </summary>
/// <remarks>
/// Business Rules:
/// - Must be idempotent
/// - Fails if account balance is stale
///
/// Dependencies:
/// - Services/Payments/PaymentGateway.cs
/// - Data/LedgerRepository.cs
///
/// Do Not:
/// - Bypass validation layer
/// </remarks>
public async Task<Result> ProcessPaymentAsync(PaymentRequest request)
/**
* Processes a payment and updates ledger state.
*
* @remarks
* Business Rules:
* - Must be idempotent
* - Fails if account balance is stale
*
* Dependencies:
* - /services/payments/paymentGateway.js
* - /data/ledgerRepository.js
*
* Do Not:
* - Skip validation step
*/
async function processPayment(request) {}
Nothing here is excessive. But everything that matters is present. Each block should quickly answer four things: what this does and why it exists, only if that is not obvious; the rules that must be followed; dependencies, using real relative paths; and what must not be changed. That is enough. Once you go beyond that, you are not adding clarity, you are diluting it. Most functions only need five to twelve lines. File-level headers are where broader context belongs. Functions should stay tight.
Listing relative paths to related files gives the AI a lightweight map of the system without a full index or retrieval step. It also forces discipline. If you cannot name the related files, you probably do not fully understand the dependency chain yourself.
The same discipline applies to your AI config files. A bloated CLAUDE.md contradicts the principle you are trying to enforce. Each file should stay thin by design and model the exact behavior you want from your code.
# Project context
This is a financial services API. Payments are the core domain.
TypeScript + Node.js. Postgres via Prisma.
# Documentation philosophy
Context lives next to code, not in external docs or this file.
Each module owns its own rules via JSDoc. This file sets defaults only.
# Global defaults (override locally in JSDoc @remarks)
- All public functions require JSDoc with @remarks
- @remarks must include: Business Rules, Dependencies, Do Not
- Relative paths only in Dependencies
- 5-12 lines per block. More than that belongs at a higher level.
# What NOT to document here
Do not repeat module-specific rules here. If a rule only applies
to /services/payments/, it belongs in that file's header comment.
This file should stay under 60 lines.
# Agent behaviour
Extends CLAUDE.md, read that first.
# Documentation tasks
When asked to add or update documentation:
- Read the file header first. It defines local rules.
- Match existing patterns, do not invent new ones.
- Never shorten a "Do Not" section. Only append.
# Before editing any file
1. Read its header comment
2. List the dependencies named there
3. Confirm those files exist before proceeding
If a referenced file is missing, stop and report.
Do not assume the path is wrong.
# When to stop and ask
- A JSDoc rule conflicts with CLAUDE.md
- A dependency path doesn't resolve
- You're unsure which layer owns a piece of logic
# Rules are local, not global.
# The real rules live in each file's JSDoc.
# Read the file before editing.
[*.ts]
rule = Preserve existing @remarks blocks in full
rule = New public exports require JSDoc with Business Rules,
Dependencies, Do Not
[services/**/*.ts]
rule = Read the file header, it defines what this service owns
rule = No direct DB calls. Use the repository pattern.
rule = No HTTP concerns (no req, res, status codes)
[data/**/*.ts]
rule = No business logic. Validation belongs in /services/.
[routes/**/*.ts]
rule = Handlers delegate immediately to a service. No logic here.
# Cursor rules should NOT contain:
# - Business rules (those live in JSDoc per-function)
# - Dependency lists (those live in each file's @remarks)
This approach fails the same way everything else does when it is overdone. If comments turn into long explanations of obvious code, people stop reading. If every function carries a wall of text, the signal gets buried. And crucially, you start wasting the very context space you were trying to protect. There is a practical limit, and ignoring it defeats the purpose entirely.
Movement two: shared rules, where they belong
Inline documentation handles one file at a time. But some rules apply to more than one file by definition. Contracts. Anything shared. That is the layer the first movement does not address, and it is where the most expensive AI tooling is aimed.
If your interfaces define their own contracts and your implementations point back to them, the AI does not need a retrieval system to find architectural intent. It is already in the file the AI is editing. The same documentation that helps a human reader understand a contract helps the model honor it. No infrastructure required.
Why AI defaults to bad architectural patterns
A junior developer looks at an interface with a single implementation and sees ceremony. Why the abstraction? Why the indirection? Why split the contract from the mechanics when one file would do? They are not wrong to ask. They are missing the answer, which they cannot have yet, because the answer is on the other side of an experience they have not had.
The experience is a migration. Or a rewrite. Or an incident at 3am where a tightly coupled module took down three services that should have been independent. The recognition that follows, the moment a developer understands why the patterns exist because they have lived through their absence, is what I called anagnorisis in an earlier piece on AI development as a communication problem. It cannot be taught. It has to be experienced. And it is the dividing line between developers who reach for interfaces by default and developers who see interfaces as bloat.
AI sits on the wrong side of that line. The training corpus for these models is overwhelmingly code written by developers who have not yet had the breakthrough, simply because that is the statistical majority of public code. Models default to direct instantiation, recreated abstractions, business rules duplicated across implementations, and tightly coupled modules. Not because they cannot reason about architecture, modern frontier models can, when given the context. They default that way because it is the center of mass in what they learned from. The burden of enforcing the contract layer falls on the human, and the cheapest place to enforce it is in the code itself, before the AI ever generates anything.
The failure modes this addresses
Three patterns show up consistently when AI is left to infer architectural intent on its own.
Recreated abstractions. The model writes a helper, a validator, a mapper, a result type that already exists somewhere in the codebase. It cannot see what it cannot see, and the existing abstraction was three files away from the current context. So it makes a new one. Multiplied across a codebase, this is how you end up with four implementations of the same idea, none of which know about each other.
Ignored interface contracts. The interface says implementations must be idempotent. The new implementation is not. The interface said so in a design doc that lives in Confluence, or in a code review comment from eighteen months ago, or in the head of the engineer who wrote the original implementation. None of those are visible to the AI when it generates the new one.
Inconsistent implementations of the same pattern. Three implementations of the same interface, three different approaches to error handling, three different logging conventions, three different ways of expressing the same business rule. Each looks reasonable in isolation. Together they are unmaintainable.
All three share a root cause: the rules that should constrain implementations are not written down where the AI can see them at the moment it needs them.
The hierarchy
Rules belong at the layer they apply to. Globals in config files, contracts on interfaces, mechanics in implementations. This is not new computer science. It is separation of concerns applied to documentation. The contribution is being explicit about placement, because placement is exactly the kind of decision the AI will infer if you let it, and inference is where this all goes wrong.
- Global rules live in CLAUDE.md, AGENTS.md, and .cursor/rules. The rules that apply everywhere: documentation requirements, naming conventions, project-wide constraints. These are the ones established in the first movement above.
- Contract rules live on the interface or base class. The rules every implementation must honor: idempotency requirements, ordering guarantees, side effects, what callers are entitled to assume.
- Implementation rules live on the implementing type. The rules specific to this code: which SDK version, which retry policy, which configuration knobs, which extensions are permitted and which are not.
The interface defines the contract once:
/// <summary>
/// Contract for processing payments against the ledger.
/// </summary>
/// <remarks>
/// Global Rules: CLAUDE.md, AGENTS.md, .cursor/rules
///
/// Contract Rules:
/// - Implementations must be idempotent
/// - Must reject stale account balances
/// - Must emit a LedgerEvent on success
///
/// Implementations:
/// - Services/Payments/StripePaymentProcessor.cs
/// - Services/Payments/AchPaymentProcessor.cs
///
/// Do Not:
/// - Add methods that bypass validation
/// - Allow implementations to swallow exceptions
/// </remarks>
public interface IPaymentProcessor
{
Task<Result> ProcessPaymentAsync(PaymentRequest request);
}
The implementation points back to the contract and documents only what is specific to it:
/// <summary>
/// Stripe-backed implementation of IPaymentProcessor.
/// </summary>
/// <remarks>
/// Contract: Services/Payments/IPaymentProcessor.cs
///
/// Implementation Notes:
/// - Idempotency enforced via Stripe-Idempotency-Key header
/// - Retries handled by Polly policy registered in DI
/// - Accepts optional metadata dict via params overload for Stripe-specific tags
///
/// Dependencies:
/// - Services/Payments/IPaymentProcessor.cs
/// - Infrastructure/Stripe/StripeClient.cs
/// - Data/LedgerRepository.cs
///
/// Do Not:
/// - Move retry logic into this class
/// - Use the metadata overload for ledger-relevant data
/// </remarks>
public class StripePaymentProcessor : IPaymentProcessor
When the AI opens the implementation to make a change, it sees the Contract: pointer immediately. If the change touches the contract's rules, it has the path. If the change is purely about Stripe mechanics, the implementation file already tells it what it needs to know. The metadata overload is a Stripe-specific extension that does not belong on the interface, every other implementation would have to ignore it. It belongs here, with a Do Not clarifying that it should not leak into ledger logic. The pattern is the same in TypeScript, Python, or any language with structured documentation. The format is whatever the language and tooling already support. The discipline is what matters.
The rule that ties both movements together: point upstream
Here is the single discipline underneath everything above, and the one worth stating plainly, because it is what makes documentation discoverable to a model without any retrieval system at all.
Every layer points to the layer above it. An implementation points to its contract. A contract points to the global rules file. Nothing is repeated. If a rule changes, it changes in one place, and every reader, human or model, sees the new rule the next time it follows the pointer.
That chain of pointers is the index the retrieval systems are trying to buy. The difference is that this index lives inside the file the AI is already editing, so it never falls out of context, never needs to be rebuilt, and never goes stale silently, because the moment a pointer does not resolve, the agent is instructed to stop and report. Discoverability stops being an infrastructure problem and becomes a writing habit: put the rule at the layer it governs, and leave a pointer up to the layer above. The model walks the chain the same way a new engineer would.
That makes the global rules file's job small. The first movement established it; the contract layer adds one section to it.
# Documentation hierarchy
Rules live at the layer they apply to. Never duplicate across layers.
- Global rules: this file, AGENTS.md, .cursor/rules
- Contract rules: on the interface or base class
- Implementation rules: on the implementing type
When documenting an interface or base class:
- Reference global rules with: "Global Rules: CLAUDE.md, AGENTS.md, .cursor/rules"
- Define Contract Rules that every implementation must honor
- List known Implementations by relative path
- Use Do Not for what implementations are forbidden from doing
When documenting an implementation:
- Reference the contract with: "Contract: <relative path>"
- Document only what is specific to this implementation
- Do not restate Contract Rules
- Use Do Not for what this specific code must not do
Before editing an implementation, read its Contract first.
Before editing a contract, list its Implementations and check them.
# Contract changes
When modifying an interface or base class:
1. Read the Implementations list in its @remarks
2. Confirm each listed file exists
3. If a Contract Rule changes, every implementation must be reviewed
If the Implementations list is missing or stale, stop and report.
That is the entire enforcement layer. No retrieval system, no vector store, no custom agent. The rules are in the files the AI is already reading, and the pointers tell it where to go next.
The diagnostic
Three signs the structure has collapsed:
If your implementations all repeat the same rules in their headers, your interface is not doing its job. The contract layer exists specifically so those rules live in one place.
If your interface has no Implementations: list, the AI cannot follow the chain when a contract rule changes. Neither can you. Maintain the list.
If global rules appear on contracts, or contract rules appear on implementations, the hierarchy has flattened. Push them back up to where they belong.
These are not exotic failure modes. They are the same failures any documentation discipline produces when it is not maintained. The difference now is that the AI is also a reader, and unlike a human, it will not push back when the structure is wrong. It will silently produce code that conforms to whatever flattened structure it finds, and it will do so at a rapid pace.
The trade you are actually making
You are not choosing comments over clean code, and you are not choosing flat duplication over layered context. You are choosing embedded context over external dependency. You give up some centralization, the convenience of seeing every rule in every file. In exchange you get consistency, a documentation structure that mirrors your code structure, and a codebase that is easier to understand with or without AI.
There is no dashboard. No vector store. No procurement cycle. Just a discipline applied consistently in the files you are already writing. That is not a Boolean choice against tooling, either. This overhead has a real cost and has to be managed, and at sufficient scale a retrieval layer may still earn its keep. This complements any tool acting in the same domain rather than replacing it.
Companies are investing in tooling that addresses the symptoms of undisciplined codebases. The tooling is not useless. But the cheapest, most durable fix is almost always upstream, in the code itself, applied early enough that the symptoms never arise. Context next to code. Vocabulary built through experience. Contracts defined where they belong, pointing up. This is the elegance of artisans: build systems that naturally produce better outcomes. We may not be writing the code by hand anymore, but these AI tools are sheet-metal presses, and they can be molded to follow the same elegant solutions the craft spent decades discovering.