GUIDE · LEGACY CODE REFACTORING

Codebase-Aware AI Refactoring Strategies

A 400,000-line monolith cannot fit in any context window. The question is not how to give the AI the whole codebase — it is how to give it exactly the right slice of the codebase for each refactoring operation, without paying for the parts it doesn't need.

Wakalix Engineering June 2026 11 min read Pillar 2 · Legacy Code Refactoring

Every AI refactoring tool eventually runs into the same constraint: the code that needs to change is not the only code that matters. Changing a function signature in module A affects callers in modules B, C, and D — and if the AI only sees module A, it cannot tell you whether the change is safe, let alone help you migrate the callers.

The naive solution — give the AI the whole codebase — fails at scale. A 300,000-line codebase at ~4 characters per token is roughly 75 million characters, or about 18 million tokens. At current API rates, sending the full codebase on every refactoring operation would cost thousands of dollars per run, take minutes per API call, and produce worse results than a focused query because attention degrades in very long contexts. You cannot brute-force repo-scale context with a large enough window.

The engineering solution is to build a structured representation of the codebase — an index — that lets you retrieve exactly the relevant context for each operation, at a fraction of the token cost of naive full-repo inclusion.

Why single-file refactors break monoliths

A monolith is a codebase where the call graph is dense — functions call each other across package boundaries, packages import each other in ways that create hidden coupling, and a change in one place has non-obvious effects in many others. This is not a design flaw in the original architecture; it is the natural consequence of a codebase that grew organically over years without strict module isolation.

The structural property that matters for refactoring: in a dense call graph, the blast radius of a change is proportional to the depth of the callee in the call graph, not to the size of the change. A one-line change to a function called by 40 other functions has a blast radius of 40, regardless of how simple the change appears. A one-line change to a leaf function called by nothing has a blast radius of 1.

Single-file AI refactoring tools are implicitly optimised for low-blast-radius changes — they can suggest improvements to the code they see without knowing whether that code is a root or a leaf. On a monolith where most interesting refactoring targets are internal nodes of the call graph, single-file tools produce suggestions that are locally coherent and globally breaking.

The blast radius calculation requires the call graph. The call graph requires an AST parse of the full codebase. This is the analysis step that most AI tooling shortcuts and that most refactoring failures trace back to.

Indexing and vector search architecture

A codebase index for AI-assisted refactoring is not a full-text search index. Full-text search finds files containing a specific string; it does not understand the semantic structure of code or the relationships between functions. What you need is a combination of structural analysis (call graphs, import graphs, type hierarchies) and semantic similarity search (vector embeddings that let you find code that does the same thing even if it uses different names).

Chunking strategy: The first architectural decision is how to split the codebase into indexable units. Naive approaches — fixed token windows, file-level chunks — lose the semantic coherence that makes retrieval useful. A file-level chunk mixes function definitions, their callers, their tests, and their documentation into a single blob; similarity search on this blob is noisy. The right unit of chunking for code is the declaration: each function, class, or module is its own indexed unit, with its imports and docstring as metadata.

Most language-aware indexing libraries produce AST-level chunks. For Python, tree-sitter parses the AST and lets you extract function-level chunks with their full signatures and docstrings. For Go, the standard go/ast package provides the same structure. The chunking step produces a set of structured records: function name, file path, signature, docstring, body, and the list of functions it calls and the list of functions that call it.

Embedding model choice: The embedding model converts each chunk into a vector for similarity search. For code, a code-specific embedding model outperforms general-purpose text embeddings. Models fine-tuned on code — several are available from major providers and as open weights — embed functions such that functions with similar semantics are close in vector space, even if they use different variable names or implementation styles. The model choice trades embedding quality against index-update cost: a better model is slower to run on incremental updates, which matters if your codebase changes frequently.

Index update cadence: The index must reflect the current state of the codebase to be useful. Three update patterns are in common use: (1) on-commit hook that reindexes changed files immediately; (2) periodic background reindex on a schedule (nightly is the minimum acceptable cadence for an active codebase); (3) lazy-reindex on query, where the retrieval step checks the file modification time against the index timestamp and reindexes stale chunks before returning results. The on-commit hook gives the freshest index but adds latency to every commit; the lazy approach is simplest to implement and acceptable for most use cases.

Dependency-graph-aware refactors

The dependency graph is the structural complement to the semantic index. Where the vector index answers "what other code is semantically similar to this?", the dependency graph answers "what other code directly or transitively uses this?"

Building a dependency graph at the function level requires parsing the AST to extract call relationships. For a 300,000-line codebase in a language with a well-tooled AST library, this takes minutes, not hours. The graph is stored as an adjacency list or similar structure that supports efficient traversal: given a function node, return all callers (incoming edges) and all callees (outgoing edges) up to N hops.

For a refactoring operation, the dependency graph query runs first, before the language model is involved:

# Example: find all code that needs to change when auth.validate() signature changes
def get_refactor_context(changed_fn: str, graph: CallGraph, depth: int = 2) -> list[str]:
    """Return file paths of all code affected by changing changed_fn."""
    callers = graph.callers(changed_fn, depth=depth)
    tests   = graph.test_coverage(changed_fn)
    return list(set(callers + tests))

context_files = get_refactor_context("auth.validate", call_graph, depth=2)
# Returns: ["api/handlers.go", "middleware/jwt.go", "auth/session.go",
#           "tests/auth_test.go", "tests/integration/session_test.go"]
# Five files instead of the 2,400 in the full codebase

The language model then receives only those five files, plus the proposed change to auth.validate, and is asked to assess impact and generate the migration. At five files instead of 2,400, the context is focused, the token cost is controllable, and the retrieval is structurally grounded rather than semantically approximate.

The depth parameter controls the blast radius search depth. Depth 1 returns direct callers only. Depth 2 returns callers of callers. Depth 3 and beyond quickly expands to cover most of a dense monolith; use depth 2 for most changes and depth 3 only for changes to very foundational interfaces with evidence (from the graph) that the deep callers are relevant.

The token-cost trade-off

Building and maintaining a codebase index has upfront costs: compute time for the initial index build, storage for the vectors and graph, and model API cost for generating the embeddings. For a 300,000-line codebase indexed at function level with an average of 30 lines per function, you are indexing roughly 10,000 functions. At a typical code chunk length of 200 tokens, with an embedding model that processes 1,000 tokens per API call, the initial index build is 2,000 API calls — a one-time cost of a few dollars at current embedding API rates.

On-commit incremental updates add the changed functions only: typically 1–10 functions per commit, which is 1–10 embedding calls per commit. The incremental maintenance cost is negligible.

The per-refactor-operation savings are significant. Without an index, giving the AI full-repo context for a function that has 5 callers in a 2,400-file codebase costs roughly the tokens for all 2,400 files (if you can fit them) or produces poor results (if you cannot). With the dependency-graph query above, you send 5 files: a 480× reduction in context tokens for that operation, at the cost of a dependency-graph traversal that takes milliseconds.

Across a sustained legacy refactoring project — hundreds of refactoring operations over several weeks — the indexing infrastructure pays for itself many times over in reduced API costs and in improved model output quality from focused context.

Picking the implementation details for your codebase

The approach above describes the general architecture. The specific implementation details — which embedding model, which graph traversal library, how to handle multi-language monorepos, how to deal with dynamic dispatch that the static call graph cannot trace — are engineering decisions that depend on the codebase's language, structure, and team conventions.

A conventions layer sits above the indexing infrastructure: it specifies which parts of the codebase are stable interfaces that the AI must not propose changes to without explicit approval, which parts are active development where autonomous suggestions are acceptable, and which parts are off-limits entirely. This layer is what makes the index actionable for a real team rather than a theoretical one.

The result of combining structural indexing, dependency-graph context selection, and explicit convention boundaries is a refactoring system that operates at repo scale without requiring repo-scale token spend on every operation. The index is the upfront investment; every subsequent refactoring operation benefits from it.

See how repo-scale AI refactoring works in practice

The indexing architecture above describes the technical substrate. Applying it to a real legacy codebase — building the index, establishing the call-graph queries, wiring the conventions layer — is the engagement that makes the theory operational.

Legacy Code Refactoring at Wakalix →