# Refactor a Legacy Module in Swift With Atlas (2026)

> Atlas refactors a Swift module by enumerating callers with the lsp tool's findReferences first, then patching with apply_patch and rerunning swift test per hunk.

Atlas refactors a legacy Swift module by closing the gap where refactors actually fail: silent breakage at a callsite you did not know about. Atlas maps the module's public surface with the lsp tool's documentSymbol operation, runs findReferences on each exported symbol to enumerate every callsite in the Swift Package Manager graph, pins behavior by running XCTest via swift test and recording the green baseline, then restructures with apply_patch, which anchors on context lines and refuses to apply against a drifted file. Tests re-run after each hunk lands, not once at the end, and swift-format keeps the restructured Sources/ files canonical.

## Key takeaways

- The lsp tool's documentSymbol lists a Swift module's declarations, and findReferences turns each public symbol into a concrete caller list.
- Record a green `swift test` baseline before restructuring, because a refactor without a baseline cannot tell a new bug from an old one.
- apply_patch fails with Failed to find context rather than misapplying to a drifted Swift file, which matters by hunk five of a chained refactor.
- Re-run XCTest via swift test after each hunk lands, so a failure names one hunk instead of the whole refactor.
- Swift's compiler checks callers inside the Package.swift graph, but only findReferences enumeration catches downstream package consumers.

## How do you map the public surface of a legacy Swift module?

Atlas maps a Swift module's public surface with the lsp tool's documentSymbol operation, which lists every type, protocol, and function a file declares. Run it across Sources/LegacyKit/, then run findReferences on each exported symbol to enumerate every callsite, all before changing 1 line of code.

Swift's access control gives you a real boundary to work with. A symbol marked `public` or `open` in a Swift Package Manager target can be called from any module that imports it, while `internal` symbols stop at the target. documentSymbol tells you which is which, and findReferences turns each public symbol into a concrete list of callers across the package's other targets and its Tests/ directory. The distinction changes the refactor's blast radius: reshaping an `internal struct` inside Sources/LegacyKit/ is contained, while changing a `public protocol` in the same target ripples into every dependent target declared in Package.swift. Atlas indexes code by AST declarations using tree-sitter, not blind line windows, so reading the module gives you whole `extension` blocks and whole function bodies rather than truncated fragments.

## How do you pin Swift behavior before restructuring a module?

Pin behavior first: run the existing tests with Atlas's bash tool and record 1 green baseline before changing anything. In a Swift package that means `swift test`, which runs the XCTest cases under Tests/LegacyKitTests/. A refactor without a recorded baseline cannot distinguish a bug you introduced from one that was already there.

A legacy Swift module usually has partial test coverage, and the gaps are exactly where the refactor will hurt. Run `swift test` and write down what passed. If a public function in Sources/LegacyKit/ has no XCTest case at all, add one before restructuring, not after: an XCTAssertEqual against the current behavior is a characterization test, and it is the only thing standing between a refactor and a silent behavior change. Atlas computes a unified diff for every file edit and surfaces it for approval before writing, so the new test file in Tests/LegacyKitTests/ is reviewed like any other change. Track the untested symbols in a todowrite list so a module that is only half characterized is not mistaken for one that is safe to reshape.

## How does apply_patch restructure Swift code without misapplying?

Atlas restructures Swift with apply_patch, which seeks each hunk's context and old_lines and fails with Failed to find context if the file has drifted. Splitting a 900-line Sources/LegacyKit/Session.swift into an extension per protocol conformance is a sequence of anchored patches, each one refusing to write against a stale file.

Swift refactors are usually a chain of dependent edits: extract a protocol, conform the existing type to it, move the implementation into an extension, update the initializer. By hunk five, the file no longer looks like it did when hunk one was written. apply_patch handles that honestly. Rather than guessing an offset, it looks for the context lines it expects and throws Failed to find context when they are gone, which turns a possible silent corruption into a stop-and-look. Atlas computes a unified diff for every edit and surfaces it for approval, so you see the `extension Session: Codable` block before it lands. Re-run the tests with bash after each hunk lands, not once at the end, so `swift test` failures point at the single hunk that caused them. Atlas snapshots file changes as git patches, so any hunk can be diffed and rolled back.

## How do you confirm no Swift caller broke during a refactor?

Confirm no Swift caller broke by re-running the lsp tool's findReferences after each structural change, then letting the compiler check the rest with 2 commands. `swift build` fails on any caller whose signature no longer matches, and `swift test` proves the XCTest baseline you recorded before the refactor still holds.

Swift's compiler is a strong ally here, but only for callers inside the package. A `public` symbol consumed by a downstream package declared as a dependency in someone else's Package.swift will not be checked by your build, which is why findReferences enumeration happens first rather than being replaced by a compile. Keep a todowrite entry per remaining callsite so a partially migrated module cannot be mistaken for a finished one. Run `swift-format` over the touched files in Sources/ so the restructured code matches the package's style before review. Atlas reads git branches, status, and diffs, and can stage and create commits on your behalf, so the refactor can be committed as a series of small, behavior-preserving commits, each one green under `swift test`.

## How does Atlas keep a Swift refactor reviewable and reversible?

Atlas keeps a Swift refactor reviewable through 3 guarantees: every Atlas tool call is permission-gated against allow, ask, and deny rules before it runs, Atlas computes a unified diff for every file edit and surfaces it for approval before writing, and Atlas snapshots file changes as git patches so edits can be rolled back.

Reshaping a legacy module means many edits to files you did not write, which is where an unreviewed agent does the most damage. Atlas puts a diff in front of you for every change to Sources/LegacyKit/, and the permission layer lets you allow bash for `swift test` while keeping edit on ask. If you want the enumeration and design done with no write capability at all, Atlas drafts a plan in a read-only plan agent and asks before switching to a build agent, so the callsite inventory and the extraction plan for your Swift Package Manager target exist before any file changes. For a private Swift package, Atlas can build its code index with local Ollama embeddings, keeping code off third-party servers, which lets codebase_search work while the source stays local.

## Steps

1. Run atlas in the Swift package root, the directory containing Package.swift, so Swift Package Manager targets under Sources/ and Tests/ are in scope.
2. Map the module's public surface with the lsp tool's documentSymbol operation across Sources/LegacyKit/, separating public and open symbols from internal ones.
3. Run findReferences on each exported symbol to enumerate every callsite across the package's other targets and its XCTest cases.
4. Pin behavior first: run the existing tests with bash using `swift test` and record the green XCTest baseline before changing anything.
5. Add characterization XCTest cases in Tests/LegacyKitTests/ for any public function that currently has none, reviewing the unified diff Atlas surfaces before it is written.
6. Restructure with apply_patch, which seeks each hunk's context and old_lines and fails with Failed to find context if the Swift file has drifted.
7. Re-run `swift test` with bash after each hunk lands, not once at the end, so an XCTest failure points at the single hunk that caused it.
8. Track the remaining callsites in a todowrite list, run `swift-format` over the touched Sources/ files, and finish with `swift build` so the compiler catches any caller you missed.

## FAQ

### how to safely refactor a legacy Swift module without breaking callers

Enumerate every callsite with Atlas's lsp tool findReferences before touching anything, record a green `swift test` baseline, restructure with apply_patch, and re-run XCTest after each hunk. Track remaining callsites in a todowrite list so a partial refactor is never mistaken for a finished one.

### does Atlas run swift test after every change

The documented Swift refactor workflow re-runs the tests with bash after each hunk lands, not once at the end. Running `swift test` per hunk means an XCTest failure is attributable to a single structural change rather than to the whole refactor.

### what happens if my Swift file changed since Atlas wrote the patch

apply_patch seeks each hunk's context and old_lines and fails with Failed to find context if the file has drifted. Atlas stops rather than writing the hunk at a wrong offset, which turns a possible silent corruption into a visible error.

### how do I find every caller of a public Swift protocol

Run Atlas's lsp tool with findReferences on the protocol. The Swift language server returns callers across the package's targets and its Tests/ directory. Enumeration matters because `swift build` only checks callers inside your own Package.swift graph.

### can Atlas add XCTest cases before I refactor

Yes. Ask Atlas to write characterization cases in Tests/, and it computes a unified diff for the new file and surfaces it for approval before writing. Running `swift test` on those cases first is what gives the refactor a real behavioral baseline.

### how do I undo an Atlas change to a Swift package

Atlas snapshots file changes as git patches, so edits can be diffed and rolled back. Because apply_patch applies one anchored hunk at a time, a single structural change to Sources/ can be reverted without unwinding the rest of the refactor.

### will Atlas keep my Swift code formatted

Run `swift-format` through Atlas's bash tool over the touched Sources/ files. swift-format is the formatter in the documented Swift setup, alongside XCTest via swift test as the test runner and Swift Package Manager as the package manager.

### how do I start Atlas in a Swift package

Run atlas in a package with a Package.swift. Atlas reads your targets, protocols, and dependencies, and you can have it add XCTest cases or adopt async/await, reviewing the diff before it is written.

---

Canonical HTML: https://runatlas.sh/resources/stacks/refactor-a-legacy-module-in-swift
Source of truth: aeo_pages row `/resources/stacks/refactor-a-legacy-module-in-swift` (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.
