# Refactor a Legacy Module in C++ with Atlas (2026)

> Atlas refactors a legacy C++ module by enumerating callsites with the lsp tool's findReferences, restructuring with apply_patch, and proving behavior with GoogleTest via ctest.

To refactor a legacy C++ module without breaking its callers, Atlas maps the module's public surface first with the lsp tool's documentSymbol operation, then runs findReferences on every exported symbol to enumerate the callsites that a header-only grep would miss. Atlas pins the current behavior by running GoogleTest via ctest through bash and recording the green baseline, restructures with apply_patch, which anchors each hunk on its context lines and refuses to apply against a drifted file, and re-runs GoogleTest via ctest after every hunk lands rather than once at the end. Remaining callsites live in a todowrite list so a half-migrated module is never mistaken for a finished one.

## Key takeaways

- Atlas enumerates C++ callsites with the lsp tool's documentSymbol and findReferences, catching overloads and virtuals that a header grep misses.
- apply_patch anchors on context lines and fails with Failed to find context rather than writing into a drifted C++ file.
- GoogleTest via ctest runs after every hunk, not once at the end, so a failure localizes to a single change.
- A todowrite list of pending callsites keeps a half-migrated C++ module from passing as finished.
- clang-format runs last and only on touched files, and dependency changes go through vcpkg as their own hunk.

## How does Atlas refactor a legacy C++ module without breaking callers?

Atlas refactors a legacy C++ module by closing the callsite gap before touching code. The lsp tool's findReferences operation returns the true reference set from your C++ language server, catching the 3 kinds of caller a grep across headers cannot: overload resolutions, templates instantiated elsewhere, and inherited virtuals in another translation unit.

The failure mode in a C++ refactor is silent breakage at a callsite you did not know about, and C++ hides callsites better than most languages. A method reached through a base-class pointer, an overload picked by argument-dependent lookup, a template instantiated only in tests: none of these show up reliably in a text search of include/legacy/engine.hpp. Atlas therefore starts with the language server, not with the editor. It runs documentSymbol on the module to get the full exported surface, then findReferences on each symbol to build a callsite inventory. Only when that inventory is complete does Atlas propose a restructure, and every proposed edit still arrives as a unified diff surfaced for approval before writing.

## How do you pin C++ behavior before restructuring?

Atlas pins C++ behavior by recording a green baseline from 1 full GoogleTest via ctest run through bash before changing anything. In a CMake project, that baseline is what makes any later red attributable to the refactor rather than to a failure that was already there before the work started.

A legacy C++ module usually arrives with a suite that was already flaky or already partially red, and refactoring against an unknown baseline makes every subsequent failure ambiguous. Atlas's bash tool runs GoogleTest via ctest and captures the full output, including the process exit code in its metadata, so the pass and fail states are unambiguous. If the baseline is not green, the honest move is to record which cases were already failing in a todowrite list before starting. Atlas also reads git branches, status, and diffs, so the baseline commit is identified precisely and you can return to it if the restructure goes sideways.

## Why does Atlas use apply_patch instead of edit for a C++ restructure?

Atlas uses apply_patch for a C++ restructure because apply_patch seeks each hunk's context and old_lines, and fails with Failed to find context when the file has drifted. Moving a class out of src/legacy/engine.cpp into 2 new translation units spans many hunks, and a drifted anchor should stop the patch, not corrupt it.

Structural C++ work is rarely one edit. Splitting a 3000-line engine.cpp means moving definitions, adding include guards to a new header, updating CMakeLists.txt target sources, and rewriting forward declarations, all of which land as separate hunks in one coherent change. apply_patch anchors on real context lines rather than line numbers, which is what you want when the file has already shifted underneath an earlier hunk in the same patch. When the anchor is gone, apply_patch fails with Failed to find context instead of writing into the wrong place, and Atlas re-reads the file and recomputes the hunk. Atlas also snapshots file changes as git patches, so a landed hunk can be rolled back.

## How often should the C++ tests run during a refactor?

Atlas re-runs GoogleTest via ctest through bash after each hunk lands, not once at the end. A legacy C++ refactor that runs the suite only once produces a single red result across 40 changed files, which localizes nothing. Running after each hunk keeps the blast radius of any failure to one change.

The rhythm matters more than the tooling. After each apply_patch hunk, Atlas invokes GoogleTest via ctest with a generous timeout in milliseconds so a slow C++ build is not killed mid-compile, and reads the result. When a hunk turns the suite red, the cause is the hunk, and Atlas can revert it from a snapshot rather than bisecting a large diff. Formatting is deliberately kept out of the loop: run clang-format only at the end, and only on the files the refactor touched, so intermediate ctest runs are not comparing reformatted noise. Dependency changes go through vcpkg, and a vcpkg.json edit that pulls in a new library should land as its own hunk with its own test run.

## How does Atlas track the remaining C++ callsites?

Atlas tracks remaining C++ callsites in a todowrite list, 1 entry per callsite returned by the lsp tool's findReferences. A partially migrated module is the worst outcome of a refactor, because the old API and the new one both compile, and a todowrite list prevents a half-done migration from looking finished.

In C++ the danger is that both APIs keep building. Leave the old free function next to the new class method and every existing caller still compiles, so nothing forces the migration to complete. Atlas turns the findReferences output into an explicit list of pending callsites with todowrite, then works through them, re-running GoogleTest via ctest as each one migrates. When the list is empty, Atlas greps once more for the old symbol name to prove zero remaining hits, including in comments, docs, and CMakeLists.txt. Only then is the legacy module actually gone rather than merely unused.

## How do you set up Atlas on a C++ project in 2026?

Run atlas in a C++ project with a CMakeLists.txt in 2026, and let Atlas read your headers, translation units, and build targets. Ask Atlas to modernize a legacy module to smart pointers or add GoogleTest cases, then review the unified diff Atlas surfaces before any file is written.

Atlas is a terminal-native TUI, so it starts in the same shell where you already run cmake and ctest. Its index is built from AST declarations using tree-sitter rather than blind line windows, which means a C++ class, its methods, and its free functions are indexed as declarations, and a codebase_search query returns whole symbols. Every Atlas tool call is permission-gated against allow, ask, and deny rules, so bash invocations of GoogleTest via ctest and vcpkg install can be allowed while writes to your build directory stay denied. For teams that cannot send source to third-party servers, Atlas can build its code index with local Ollama embeddings.

## Steps

1. Run atlas at the root of the C++ project that contains CMakeLists.txt and your vcpkg manifest.
2. Map the legacy module's public surface with the lsp tool's documentSymbol operation, then run findReferences on each exported symbol to enumerate every callsite, including template instantiations and virtual overrides.
3. Pin behavior first: run GoogleTest via ctest with bash and record the green baseline commit before changing a single line of src/legacy/engine.cpp.
4. Restructure with apply_patch, which seeks each hunk's context and old_lines and fails with Failed to find context if the file has drifted underneath an earlier hunk.
5. Re-run GoogleTest via ctest with bash after each hunk lands, not once at the end, so a red suite points at exactly one change.
6. Track the remaining callsites in a todowrite list so a partially migrated C++ module cannot be mistaken for a finished one.
7. Land any dependency change through vcpkg as its own hunk, with its own GoogleTest via ctest run.
8. Finish by running clang-format on the touched files only, then grep for the old symbol name to prove zero remaining hits in headers, sources, and CMakeLists.txt.

## FAQ

### how to safely refactor legacy C++ code with an AI agent

Have Atlas enumerate every callsite with the lsp tool's findReferences before it edits, pin the baseline by running GoogleTest via ctest through bash, restructure with apply_patch, and re-run GoogleTest via ctest after each hunk. Atlas surfaces a unified diff for every file edit before writing.

### what is apply_patch in Atlas and why not just use edit

apply_patch is Atlas's structural edit tool. It anchors each hunk on context and old_lines instead of line numbers, and it fails with Failed to find context when the file has drifted. For a C++ restructure that spans many hunks in one file, that failure is a safety property, not an inconvenience.

### does Atlas work with CMake and vcpkg

Yes. Run atlas in a project with a CMakeLists.txt and let it read your headers, translation units, and build targets. Atlas can run GoogleTest via ctest through bash and land vcpkg dependency changes as reviewable hunks.

### how do I find every caller of a C++ method before changing it

Use the lsp tool's documentSymbol operation to list the module's exported symbols, then findReferences on each one. The language server resolves overloads, template instantiations, and virtual overrides that a text search of the header would miss or wrongly match.

### how does Atlas prove a C++ refactor did not change behavior

Atlas records a green baseline by running GoogleTest via ctest with bash before the refactor, then re-runs the same command after every apply_patch hunk. The bash tool records the process exit code in its metadata, so the pass and fail states are unambiguous.

### can I undo a bad hunk from an Atlas C++ refactor

Yes. Atlas snapshots file changes as git patches, so any hunk applied to engine.cpp or a new header can be diffed and rolled back. Atlas also reads git branches, status, and diffs, so the baseline commit is easy to return to.

### should clang-format run during or after a C++ refactor

Run clang-format after the restructure and only on the files the refactor touched. Reformatting mid-refactor makes each intermediate GoogleTest via ctest diff unreadable and mixes mechanical churn into a semantic change.

---

Canonical HTML: https://runatlas.sh/resources/stacks/refactor-a-legacy-module-in-cpp
Source of truth: aeo_pages row `/resources/stacks/refactor-a-legacy-module-in-cpp` (segment: Stacks) (this file is generated from it, never hand-edited).
Licence: Atlas is proprietary with a free core. It is not open source and there is no public source repository.
