refactoring
Why dependency-aware sequencing beats parallel edits
Dependency-aware sequencing applies code changes in topological order so each intermediate state compiles, avoiding the broken states that parallel edits create.

When an AI agent proposes twenty file changes to rename a function used across your codebase, the naive approach fires off edits in parallel and hopes the compiler is happy at the end. It usually isn't. The call site in api/handlers.go references the old name because the definition in core/auth.go hasn't been written yet. The test in auth_test.go imports a package that still exports the stale symbol. You end up with a broken intermediate state, retry loops that thrash between conflicting fixes, and a diff so large you can't tell which change caused the failure.
Dependency-aware refactoring sequences edits in topological order—definitions before call sites, leaf modules before their importers—so every intermediate commit compiles. This isn't just cleaner housekeeping. It's the difference between a machine-checkable chain of small reversible steps and a big-bang rewrite that forces you to trust the AI got everything right in one shot.
The parallel edit trap
Most code generation tools treat files as independent units. They build a plan, open N files simultaneously, apply transforms, and write everything back. This works fine for isolated changes—adding a new endpoint, scaffolding a component—but breaks down the moment you touch shared interfaces or widely-used utilities.
Consider renaming ValidateUser to AuthenticateUser in a Go service with fifty call sites across ten packages. A parallel strategy opens all fifty files, runs find-and-replace, and writes them out. The problem surfaces when the compiler runs:
- Half the call sites still reference
ValidateUserbecause the agent's context window missed them or the edit order was non-deterministic - Import statements in test files point to the old export before the source file has been updated
- Generated mocks or interface implementations lag behind the actual definition
You get a cascade of errors. The agent retries, often re-editing the same files in a different order, and you end up in a loop. The human has to step in, manually sequence the changes, or accept a non-compiling intermediate state and fix it by hand.
How topological ordering prevents breakage
Dependency-aware sequencing walks the module graph and schedules edits so that:
1. Definitions change before usages. If you're renaming a function, the file that declares it gets edited first. Only then do call sites update.
2. Leaf nodes change before their importers. In a layered architecture, you touch pkg/models before internal/service before cmd/server.
3. Tests follow implementation. Unit tests that import the module under change get updated after the source, so they always reference the current API.
This isn't a heuristic or best-effort sorting. It's a strict topological sort derived from your project's actual import graph. When Goatfied plans a multi-file refactor, it parses go.mod, package.json, requirements.txt, or your language's equivalent, builds the dependency DAG, and produces an edit sequence that respects it.
The result: every edit lands on a codebase that compiled before the change. When you hit a failure, you know the current diff caused it, not some lingering inconsistency three steps back.
Small diffs as checkpoint boundaries
Sequencing alone isn't enough if each edit touches a hundred lines. You still can't isolate which logical change introduced a bug. Dependency-aware refactoring combines topological ordering with small, single-concern diffs.
Instead of one commit that renames ValidateUser in twenty files, you get twenty commits, each changing one call site or one definition. Each commit:
- Compiles (or at least, should—your CI gates catch it immediately if not)
- Passes existing tests
- Represents a single logical step you can review, revert, or bisect
If a test fails after commit 14, you know commits 1–13 were fine. You revert 14, inspect the diff (which is ten lines, not a thousand), fix the issue, and replay. This is reproducible forward progress, not a dice roll.
In Goatfied's agent loop—plan, constrain, edit, validate, retry—the validate step runs after every edit. That means go build, cargo check, mypy, eslint, whatever your stack's compile-time checks are, execute immediately. If the validation fails, the agent sees the exact error message, knows which file it just touched, and generates a fix scoped to that file. It doesn't have to re-reason about the entire refactor.
Handling cycles and shared interfaces
Real-world dependency graphs aren't always clean trees. You might have:
- Circular imports (common in Python, sometimes smuggled into Go via interfaces)
- Shared interfaces defined in a
typespackage, implemented in multiple modules that import each other indirectly - Generated code that depends on source files that depend on the generated output
A naive topological sort fails or produces an arbitrary order that breaks one side of the cycle. Dependency-aware sequencing handles this by:
- Detecting strongly connected components (SCCs) in the graph and treating them as a single logical unit
- Editing all files in an SCC together, since you can't linearize them without breaking compilation
- Minimizing SCC size by refactoring import structure when possible—splitting a god package, extracting interfaces, or using dependency inversion
Goatfied's constraint phase identifies SCCs during planning. If your refactor touches a cycle, the agent flags it in the plan and either groups those files into one atomic edit or suggests a preparatory refactor to break the cycle first. You see this as a plan step like "extract UserValidator interface to pkg/interfaces to break auth ↔ session cycle," followed by the actual rename once the graph is acyclic.
When parallel edits make sense
Dependency-aware sequencing is overkill for changes that don't cross module boundaries. If you're reformatting code, adding comments, or updating configuration files that nothing imports, parallelism is faster and simpler.
The heuristic: sequence when edits change public APIs, import paths, or type signatures; parallelize when they don't.
Goatfied's planner uses static analysis to classify changes:
- Interface-affecting: renames, signature changes, module moves → sequence
- Implementation-only: refactoring within a function, adding private helpers, reformatting → parallelize
- Configuration/assets: JSON, YAML, Dockerfiles, docs → parallelize
You can override this in the plan if you know better. The tool respects your expertise but defaults to the safe path.
Audit trails and rollback
Dependency-aware sequencing gives you a clean audit trail for free. Each step in the sequence is a standalone commit (or pending change, if you're reviewing before merge). Your version control history shows:
refactor: rename ValidateUser to AuthenticateUser in core/auth.go
refactor: update ValidateUser call site in api/handlers.go
refactor: update ValidateUser call site in api/middleware.go
...
If a customer reports a bug two weeks later and you need to bisect, git bisect lands on the exact edit that changed behavior. You're not staring at a 3,000-line diff titled "refactor: update user validation."
This also matters for compliance and code review. Teams in regulated industries—fintech, healthcare, defense—need to show an auditor exactly what changed and when. A sequenced refactor with per-file commits and CI green-checks at every step is evidence of process. A squashed parallel edit is a black box.
Goatfied's self-hosted and managed deployments both log every edit, validation result, and retry in structured JSON. You can pipe that into your compliance tooling or replay the sequence against a historical commit to verify the agent's reasoning.
Concrete example: renaming across packages
Let's say you're renaming internal/auth.ValidateToken to internal/auth.VerifyToken. The function is called in:
api/middleware/auth.goapi/handlers/user.gointernal/session/manager.gointernal/session/manager_test.go
A dependency-aware sequence:
1. Edit internal/auth/token.go: rename ValidateToken → VerifyToken
2. Run go build ./internal/auth → passes
3. Edit internal/session/manager.go: update call site
4. Run go build ./internal/session → passes
5. Edit internal/session/manager_test.go: update test call
6. Run go test ./internal/session → passes
7. Edit api/middleware/auth.go: update call site
8. Run go build ./api/middleware → passes
9. Edit api/handlers/user.go: update call site
10. Run go build ./api/handlers → passes
If step 7 fails because middleware.go indirectly imports a mock that still uses the old name, the agent knows the failure is in api/middleware/auth.go or its direct dependencies. It doesn't re-edit internal/auth or thrash through the call sites again.