Skip to content

Million+ LOC Strategies

Million-line codebases exceed every context window, so AI-assisted work in them depends on structure rather than capacity: a layered context pyramid instead of loading the repo, semantic search instead of grep, a blast-radius report before any edit, and incremental changes verified module by module. Cursor, Claude Code, and Codex each need different indexing and context setup.

You inherited a 1.8-million-line monolith. The original architects left two years ago, the docs describe a system that no longer exists, and your first ticket renames a field on User. The agent confidently edits three files in packages/web, declares victory, and two hours after you ship the billing-worker service throws in production, because it read the same field from a shared schema the agent never opened. The model was not wrong because it is dumb. It was wrong because it never saw the file that mattered.

  • A semantic-search MCP setup (Zilliz Claude Context) wired into Cursor, Claude Code, and Codex so the AI finds code by intent, not string match
  • A four-layer context pyramid and per-tool rules files that keep the architecture layer loaded without loading the repo
  • A reusable architecture reconnaissance prompt, plus a prompt that turns the result into a living architecture document
  • Two blast-radius prompts — one for a cross-package rename, one for a single class — that run read-only before any edit
  • A decomposition-then-execute loop that turns “refactor the auth system” into a reviewable checklist
  • Copy-paste prompts for risk-tiered mass migrations, strangler-fig wrapping, legacy modernization against a reference module, and backward-compatible signature changes
  • Concrete recovery steps for stale indexes, hallucinated paths and counts, context overflow, and colliding parallel refactors

Why a bigger context window is not the fix

Section titled “Why a bigger context window is not the fix”

Current flagships support 1M-token context windows (Claude Fable 5, Opus 5, Sonnet 5, Gemini 3.1 Pro) and a multi-million-line codebase still will not fit. The answer is not more capacity, it is better selection — and that is exactly where AI earns its place at this scale.

Semantic, not textual

With a vector index, “find all authentication flows” surfaces OAuth, JWT, and session code even when none of them share a keyword.

Dependency tracing

The AI follows imports and call sites across module boundaries far faster than you can click through “find usages.”

Characterization tests

For undocumented legacy code, the AI drafts tests that pin current behavior so you can refactor without fear.

You stay the architect

The AI does the mechanical scanning and boilerplate. You make the domain and architecture calls it cannot.

Think of context as four layers, loaded in decreasing permanence:

  1. Architecture layer (always present): high-level documentation, dependency maps, module boundaries
  2. Domain layer (task-specific): the subsystem you are working in, its interfaces, and its contracts
  3. Implementation layer (file-specific): the actual files you are modifying
  4. Reference layer (on-demand): examples of similar patterns elsewhere in the codebase

The architecture layer is the one you encode once and stop paying for. Each tool has a file for it, and the shape of that file is what separates an agent that reasons about the system from one that guesses from three open tabs.

Cursor’s codebase indexing supplies part of the architecture layer automatically. Pin the rest in a Project Rule so it applies without being re-typed:

# .cursor/rules/architecture.mdc (alwaysApply: true)
Large-scale payment processing platform, 1.8M LOC.
Key modules:
- /src/payments/ - Payment processing (Stripe, PayPal, internal ledger)
- /src/accounts/ - User account management and KYC
- /src/notifications/ - Event-driven notification system
- /src/shared/ - Shared types, utilities, and base classes
When modifying any module, always check:
1. The module's public API in its index.ts barrel export
2. Integration tests in /tests/integration/{module-name}/
3. The event contracts in /src/shared/events/

Scope the domain layer with a glob-bound rule next to the code it governs:

# .cursor/rules/payment.mdc (glob: services/payment/**)
When working with payment code:
- All monetary amounts are integer cents — never floats
- Mutations require an idempotency key
- Never log full card numbers (PCI)
- Add audit logging for every state transition

Pull specific context in with @file and @folder. For cross-cutting changes, reference the shared contract first: @src/shared/types/payment.ts before touching any payment module. For straightforward projects, a root AGENTS.md works as a simpler alternative to structured rules.

Semantic code search with Zilliz Claude Context

Section titled “Semantic code search with Zilliz Claude Context”

Text search fails at scale because related code rarely shares vocabulary. A semantic index built on vector embeddings fixes that: it answers “where is payment processing handled?” without the file being open. The maintained server is Zilliz Claude Context (@zilliz/claude-context-mcp — previously published as code-context). MCP setup is nearly identical across all three tools; only the registration command differs.

Add to ~/.cursor/mcp.json:

{
"mcpServers": {
"claude-context": {
"command": "npx",
"args": ["-y", "@zilliz/claude-context-mcp@latest"],
"env": {
"EMBEDDING_PROVIDER": "OpenAI",
"OPENAI_API_KEY": "your-api-key",
"MILVUS_TOKEN": "your-zilliz-key"
}
}
}
}

Once indexed, you ask for concepts and the server returns the relevant files regardless of naming. For sensitive codebases that can’t reach a cloud embedding API, LuotoCompany/cursor-local-indexing runs an on-premise ChromaDB index and exposes it over a local SSE endpoint:

Add to ~/.cursor/mcp.json:

{
"mcpServers": {
"workspace-code-search": {
"url": "http://localhost:8978/sse"
}
}
}

This keeps source code on your own infrastructure — the right call for financial services, healthcare, or defense work where code can’t leave the network.

An external vector index is one half; the other half is what the tool itself sees natively, and that differs sharply between the three.

Cursor indexes the workspace automatically and computes embeddings for semantic search. Keep the index lean with ignore files:

  • .cursorignore blocks files from indexing and from agent access (use for secrets, node_modules/, build output).
  • .cursorindexingignore excludes files from the index only — they stay reachable via explicit @-mention. Use it for large generated files (lockfiles, dist/, snapshots) that pollute search results.
.cursorindexingignore
dist/
**/*.snap
pnpm-lock.yaml

Then scope context with symbols, not whole files: @accountType (a symbol) gives a tighter, less noisy context than @user-service.ts (a 2,000-line file).

Architecture reconnaissance in an unfamiliar monolith

Section titled “Architecture reconnaissance in an unfamiliar monolith”

Where do you start? Top-down. Get the AI to build a mental model before you touch anything, then drill into the area your ticket actually concerns.

Cursor’s Agent self-gathers context from the indexed codebase — just describe what you want. Use @Folders to scope a question to one area and @Code to point at a specific snippet:

@Folders services/auth
Explain the authentication and authorization architecture: where tokens
are issued, how refresh works, and which services validate them.

For a precise reference, select a function in the editor and add it with @Code before asking the agent to trace its call sites.

Turning the recon into a living architecture document

Section titled “Turning the recon into a living architecture document”

Recon you throw away is recon you pay for again next week. Write it to a file the tool re-reads on every future session — that file is your architecture layer, and keeping it current is cheaper than re-deriving it.

Where it lands differs per tool, and the destination is what makes it automatic rather than something you remember to @-mention:

Save it to .cursor/architecture.md and reference it with @.cursor/architecture.md, or point an always-on Project Rule at it so every chat starts with it loaded.

Zooming instead of loading: the context hierarchy

Section titled “Zooming instead of loading: the context hierarchy”

The biggest mistake with large codebases is loading everything at once. Your assistant doesn’t need all 1.8 million lines — it needs the right slice at the right moment. Think of it as zooming on a map: continent, country, city, street.

  1. Domain level (10,000 ft)

    What are the main bounded contexts in this system, and how do the payment,
    user, and inventory domains interact?
  2. Service level (1,000 ft)

    Within the payment domain, explain the service architecture and the main
    APIs each service exposes.
  3. Component level (100 ft)

    Show me how PaymentProcessor handles credit-card transactions and what its
    retry strategy is for failed charges.
  4. Implementation level (ground)

    In PaymentProcessor.processCard(), why is there a 30-second timeout, and is
    the synchronized block safe to remove?

Mapping the blast radius before you touch code

Section titled “Mapping the blast radius before you touch code”

The single highest-leverage move in a large repo is to make the agent find and report the affected files before it edits anything. It catches the cross-package dependency the agent would otherwise miss, and it is cheap: a read-only pass costs a fraction of a botched refactor.

Two shapes of that pass, for two different changes. The first is for a change that spreads by name across packages — a rename, a field, a config key — and its output is a ranked table you review against:

If the report misses a service you know exists, your index is incomplete or its ignore rules are too aggressive — fix that before proceeding. Keep the report open as your review artifact and check files off as the change lands.

The second is for a change centred on one class or module. It asks for a graph rather than a list, and it is the version that catches the consumers a name search cannot see: event listeners and message-queue subscribers that never import the symbol at all.

Decomposing a change into reviewable steps

Section titled “Decomposing a change into reviewable steps”

Never hand a large repo an open-ended task like “refactor the entire authentication system.” The model fans out, loses the thread halfway, and you get a 40-file diff you can’t review. Make it emit a checklist first, then execute one item per turn.

  1. List every file that must change

    Get the list before a single line is written, and review it against your own understanding of the system.

  2. Modify shared interfaces first

    Start with type definitions, interfaces, and contracts. Those changes propagate compile errors that reveal the hidden dependencies the report missed.

  3. Update implementations one module at a time

    Modify each consuming module independently, and run that module’s tests before moving to the next.

  4. Run integration tests after each module

    Do not wait until all modules are updated. Catch integration failures while the diff is still small enough to read.

  5. Verify across the cut

    Run the full suite, type-check the whole codebase, and review the complete diff before committing.

Once the plan is approved, drive it one item at a time and name the exact symbol, so the agent stays on target instead of re-deriving scope:

When the phases are obvious from the shape of the change — an interface first, then its consumers — you can skip the separate planning turn and bake them into a single prompt with explicit stops. This is the compact form of the same discipline:

For the full planning discipline behind this loop, see PRD to Plan to Todo.

Incremental refactoring across a million lines

Section titled “Incremental refactoring across a million lines”

Refactoring a million-line codebase is like renovating a hospital while surgery continues — you can’t shut everything down. The pattern that works: discover, template, migrate in small batches, verify.

Take a Node.js codebase still riddled with error-first callbacks. Manual migration to async/await would take months. Instead, have the AI categorize the work by risk, then generate one reusable transformation per category:

// Before — error-first callback
function loadUser(id, callback) {
db.query('SELECT * FROM users WHERE id = ?', [id], (err, rows) => {
if (err) return callback(err);
callback(null, rows[0]);
});
}
// After — async, with a backward-compatible callback shim
async function loadUser(id, callback) {
try {
const rows = await db.query('SELECT * FROM users WHERE id = ?', [id]);
if (callback) return callback(null, rows[0]);
return rows[0];
} catch (err) {
if (callback) return callback(err);
throw err;
}
}

The shim lets callers migrate on their own schedule. Apply the transformation directory by directory, run the existing tests after each batch, and track progress — never transform the whole tree in one pass.

Coordinating parallel refactors across a team

Section titled “Coordinating parallel refactors across a team”

For a large effort split across a team, have the AI partition the work to minimize cross-team conflicts, then keep the branches honest:

  1. Partition by dependency boundaries

    Analyze module dependencies and propose how to split this refactor across
    four developers so their territories barely overlap. Flag any shared files
    that two teams would both need to edit.
  2. Branch per territory

    Terminal window
    git checkout -b refactor/user-services
    git checkout -b refactor/payment-services
    git checkout -b refactor/shared-utils
  3. Detect collisions early

    Review the diffs across all refactor/* branches and identify conflicting
    or breaking changes between teams before we attempt to merge.

Every large codebase has archaeological layers — code from different eras and philosophies, some of it predating the team. The classic horror: a 15,000-line stored procedure no one understands that still processes real money daily.

The strangler-fig pattern lets you modernize without a rewrite: wrap the legacy code behind a clean interface, then extract pieces one at a time while running old and new in parallel until you trust the new path.

Not every legacy change deserves a facade. When you only need to add something to old code, give the AI a Rosetta Stone instead: the most recent, well-written module that follows current conventions, as the reference pattern to imitate. This keeps the diff small where the strangler-fig prompt deliberately builds a new surface.

When documentation doesn’t exist, tests become the documentation. Ask the AI to write characterization tests that pin current behavior — including the weird parts — so any future change that alters output fails loudly:

describe('Legacy OrderProcessor — current behavior', () => {
it('returns status code 1 on a standard single-item order', async () => {
const result = await processOrder({
customerId: 123,
items: [{ sku: 'WIDGET-1', quantity: 1 }],
});
expect(result.status).toBe(1); // 1 = success (undocumented magic number)
expect(result.orderId).toMatch(/^ORD-\d{8}$/);
});
it('returns -99 when inventory is unavailable', async () => {
const result = await processOrder({
customerId: 123,
items: [{ sku: 'OUT-OF-STOCK', quantity: 1 }],
});
expect(result.status).toBe(-99); // -99 = inventory error
});
});

Cross-team coordination on breaking changes

Section titled “Cross-team coordination on breaking changes”

In a million-line codebase, different teams own different territory. The hard part is making a change that crosses a boundary without breaking someone else. The blast-radius prompts above stop at a report; this one goes further and asks for the migration path, because a breaking change you cannot stage is a breaking change you cannot ship.

Pair this with auto-generated contracts. Ask the AI to produce an OpenAPI spec and event schemas for a service another team consumes — that turns “go read our code” into a stable boundary they can integrate against without spelunking through your internals.

In a long session the conversation history itself becomes stale context — the agent keeps “remembering” the bug you fixed an hour ago. Reset deliberately when you switch tasks. The mechanics differ per tool:

Start a new chat for each distinct task (new feature, new bug). Cursor keeps each chat’s context separate, so a fresh chat means the agent reasons only about the job in front of it. Use checkpoints to roll back a chat if an exploratory edit goes sideways.

Large-codebase AI workflows fail in specific, recognizable ways. Know the recovery for each.