The Drift Log · Audit, then actually repair
Automating Dead Branch Pruning After Legacy System Audits
5 September 2026 · 4 min read · 792 words · established

Static analyzers miss dynamic dispatch. Safely prune legacy dead code by coupling runtime execution logging with syntax-aware AST excision.
A static analysis run finishes, finds 3,800 lines of unreachable functions, and outputs a 40-page PDF. Three months later, all 3,800 lines are still in main.
This is the standard outcome of a repository audit. Static analysers flag dead branches with high confidence, but developers refuse to delete them. The hesitation is rational: static analysis maps declared syntax, not runtime reality. If a legacy codebase uses dynamic dispatch, reflection, metaprogramming, or conditionally loaded configuration, "unreachable" code might be load-bearing at 02:00 on the first of the month.
When the audit ends in a report, the repair becomes a manual refactoring project. Manual refactoring carries risk without immediate feature value, so it gets deprioritised. The dead code sits in place, confusing new engineers, bloating build artifacts, and compounding your technical debt.
To clear dead code, you must replace human caution with a verified excision pipeline.
The Static Detection Trap
Static analysis tools build an Abstract Syntax Tree (AST) and traverse call graphs from known entry points. When a function has no inbound edges, the tool marks it dead.
In a modern greenfield project, that is often enough. In a decade-old system, it is an invitation to an outage.
Consider a payment gateway module:
def handle_v1_settlement(payload):
# Static analysis shows 0 callers in the repository
return legacy_settle(payload)
def process_webhook(event_type, payload):
handler_name = f"handle_{event_type}"
handler = globals().get(handler_name, default_handler)
return handler(payload)
To the static analyzer, handle_v1_settlement is dead code. In production, an external webhook sends event_type="v1_settlement" once every quarter. The moment an engineer deletes that function based on a static report, the pipeline breaks silently.
Because engineers have been burned by this exact failure mode, they ignore static audit reports. The report cannot prove safety, so no one pulls the trigger.
Instrument, Verify, Excise
Automating dead branch removal requires moving past static graphs into dynamic verification. The loop must verify execution paths before any AST modification is committed to a branch. As we detail in our doctrine on closing the loop from repository audit to merged repair, an audit that cannot generate a verified, merged patch is just overhead.
A safe automated pruning pipeline executes in three stages:
- Runtime Execution Logging: Instrument candidate functions identified by the initial audit with low-overhead entry probes. In production or staging, log invocations over a known business cycle (e.g., end-of-month processing). If an edge fires, it is stripped of its "dead" status immediately.
- Boundary Test Synthesis: For functions that register zero invocations, run existing integration and contract tests. If the suite passes without the function being loaded in the runtime memory map, generate a deterministic boundary test that asserts the system's behavior remains unchanged when that specific symbol is unexported or absent.
- AST Excision: An automated agent or script removes the target node from the AST—not via regex or line deletions, but via syntax-aware transformation. This removes unused imports, dangling parameters, and orphaned interfaces alongside the target symbol.
[Repository Audit]
│ (flagged dead symbols)
▼
[Runtime Probe Window] ──(invoked)──► [Mark Keep / Add Edge]
│ (zero invocations)
▼
[AST Node Excision]
│
▼
[Harness Execution Gate] ──(regression)──► [Rollback / Flag Inconclusive]
│ (clean pass)
▼
[Atomic Pull Request]
By the time a pull request is opened, the branch is not asking an engineer, "Is this safe to delete?" It presents a diff where the symbol has been removed, the dependencies cleaned, and the entire test gauntlet executed against the reduced surface.
Preventing Zombie Resurrections
The final failure mode of dead code pruning is regression: an engineer or an automated tool re-introduces the deleted pathway because stale dependencies remained in the manifest.
When an automated pipeline prunes a dead branch, it must seal the boundary. That means:
- Removing unused transitive packages from lockfiles.
- Enforcing strict compiler or linter boundaries so that newly introduced callers cannot bind to un-pruned private interfaces.
- Running a verification harness—such as our isolated test harness—to confirm that compilation outputs, archive sizes, and runtime memory profiles strictly decrease without altering input/output contracts.
If an audit identifies fifty dead functions, the pipeline should not produce one massive PR that touches fifty files. It should emit fifty atomic, verified PRs. If one excision causes an edge-case regression in a downstream consumer, that single PR is rejected or rolled back without stalling the removal of the other forty-nine.
What to Do on Monday
Do not schedule another architecture review to discuss the results of your last static scan.
Instead, pick one low-risk module with high reported dead code:
- Parse the AST to extract all top-level functions with zero internal references.
- Add a simple runtime counter or logging probe to those entry points and deploy it to your staging or production environment.
- Schedule an automated job to inspect those counters after one full release cycle.
- For every symbol with zero hits, run an AST transformation script to remove the node, run your test gauntlet, and open an atomic pull request containing both the deletion and the test execution receipt.
Once developers see that deletion is backed by runtime verification rather than static guesswork, the fear evaporates—and the codebase actually shrinks.
This post supports the longer argument in Closing the Loop from Repository Audit to Merged Repair.
Keep reading
Next in the log
- Closing the Loop from Repository Audit to Merged Repair
Static audits fail because they catalog symptoms instead of isolating boundaries. Here is how to convert legacy audit findings into verifiable, merged patches.
- From Audit Report to Pull Request: Automating the Repair Loop
Transform static analysis audits into verified pull requests by pairing machine-readable diagnostics with a strict certification harness.
- Extracting Pure Primitives from Tangled Monoliths
Shrink refactoring blast radius by carving pure, deterministic business logic out of I/O-heavy monolithic controllers before attempting major rewrites.
The Strategic Master Library · written and reviewed under the house's own epistemic rules: nothing claimed that we cannot show.