Skip to content
Forensic Code Cleanup: deleting code with discipline

Forensic Code Cleanup: deleting code with discipline

How I deleted 12 zombie files in Shuri with forensic cleanup, ripgrep, tests, and adversarial review as the final guardrail.

MI

Mario Inostroza

The issue looked like a simple migration

Issue #461 sounded reasonable: migrate 7 legacy Shuri handlers from .cjs to TypeScript.

It looked mechanical. Open the old files, convert imports, add types, remove obsolete environment variables, close the PR.

But when I opened apps/agents/src/agents/shuri/handlers/, something felt wrong.

There were 7 .cjs handlers, a bridge.cjs, references to a JSON registry/, and 14 test files with empty mocks. Not partial mocks. Not behavioral doubles. Empty mocks.

// Repeated across multiple tests
vi.mock('../handlers/quote_medical_order.cjs', () => ({}));
vi.mock('../handlers/buy_medical_order.cjs', () => ({}));
vi.mock('../handlers/apply_coupon.cjs', () => ({}));

That is not coverage. It is a warning sign.

An empty mock can mean two things: the module no longer matters, or the test is lying about a critical dependency. Before migrating anything, I had to investigate.

Forensics before refactoring

The natural temptation was to convert the files to TypeScript and move on. But a migration without forensics can preserve dead code with nicer syntax.

The first question was simple: does anything actually consume these handlers?

rg "from.*handlers/(quote_medical_order|buy_medical_order)" \
  --type ts apps/agents/src/ \
  | grep -v ".cjs" \
  | grep -v "test"

Result: zero real imports.

The legacy handlers were not in the production path. The only files naming them were tests with empty mocks.

The second question was more dangerous: is there another system with similar names?

Yes. tools/shuri/ contained a simulation harness with similar file names. If I deleted without checking, I could break debugging tools.

rg "apps/agents" tools/shuri/

Result: zero references.

tools/shuri/ and apps/agents/src/agents/shuri/ shared naming, but not dependencies. The simulation harness was a separate scope. The zombie handlers could die.

What had actually happened

The answer was in a previous PR.

PR #423 had already unified the WhatsApp quote and purchase flow into PurchaseHandler.handleQuote, written in TypeScript. The functional migration was done. What remained was technical waste: unused files, circular imports, defensive mocks, and one environment variable nobody needed anymore.

The right change was not migrating 7 handlers. It was deleting them.

The final PR removed 12 files, 108 lines of empty mocks, and 1 obsolete environment variable. After that, 1058 tests still passed.

That number matters. Deleting without tests is optimism. Deleting with 1058 green tests and reference searches is engineering.

The forensic method

The core of the work was not adversarial review. It was the forensic method before it.

Before touching files, I built a simple matrix:

QuestionEvidence searchedResult
Who imports these handlers?rg in production0 real consumers
Do tests validate behavior?mocks and assertions108 lines of empty mocks
Are there similarly named tools?rg between tools/ and apps/separate scopes
What replaced the old flow?previous PR and current handlerPurchaseHandler.handleQuote

That table changed the goal. It was no longer “migrate legacy to TypeScript”. It was prove the legacy was dead.

Only then did the PR make sense: delete files, run tests, and check that no orphan references remained.

// compliance-verify.ts: manual verification runner
const testCases = [
  { name: "No exposed secrets", check: () => scanForSecrets() },
  { name: "No orphan imports", check: () => verifyImportGraph() },
  { name: "No zombie env vars", check: () => validateEnvUsage() },
];

Adversarial review as a guardrail

After the forensic cleanup, I used Judgment Day only as a final guardrail: an adversarial review to try to break the diff before merge.

The scope matters. In older notes I described it as “my Judgment Day protocol,” but today I use it inside a work harness based on Gentle AI, the open-source project by Alan Buscaglia: github.com/Gentleman-Programming/gentle-ai.

The focus of this post is not Judgment Day. The focus is deleting dead code with evidence. Gentle AI comes at the end to review blockers: secrets, orphan imports, .bak files, zombie env vars, and documentation that could mislead the next engineer.

In this case, that review found a .bak file with an exposed API key and one additional file that escaped the first pass. Without forensic cleanup, the problem would have been hard to scope. Without adversarial review, the diff could have shipped with hidden risk.

The cleanup mental model

The process became less “refactor” and more forensic investigation:

  1. Map dead-code candidates.
  2. Verify real consumers with rg.
  3. Separate production from auxiliary tools.
  4. Delete only what has no consumers.
  5. Run tests.
  6. Use adversarial review as the final guardrail.
  7. Repeat only when real blockers appear.

That order matters. If you start by deleting, you guess. If you start by mapping, you reduce the error surface.

What I learned

1. An empty mock is not innocent

An empty mock can hide dead code for months. The test keeps passing because it never exercises the real dependency. That creates an illusion of safety.

When I see vi.mock(..., () => ({})) now, I treat it as an open question: does this module still exist for a reason, or are we protecting historical trash?

2. Migrating dead code is technical debt with makeup

Converting .cjs to TypeScript would have left the system just as confusing, only typed. That does not improve architecture. It only makes it more expensive to discover the file was useless.

The best migration was not migrating.

3. Similar names are a trap

tools/shuri/ and apps/agents/src/agents/shuri/ looked like part of the same flow. They were not. Verifying boundaries between harnesses, scripts, and production avoided deleting useful tools.

4. Adversarial review is the closing step, not the center

Gentle AI provides a useful framework for disciplined review. But in this case, the main value came earlier: mapping consumers, checking boundaries, and proving the code was dead.

Attribution still matters: I use Alan Buscaglia’s Gentle AI as the review-discipline foundation. But the technical lesson from the PR is simpler: do not migrate dead code; delete it with evidence.

What comes next

The next candidate is service-token.cjs, the last live .cjs file in Shuri. The forensics there are narrower: core/shuri-bridge.ts appears as a real consumer, so deleting it blindly would be wrong.

The rule is now written: grep first, refactor second.

Dead code is not harmless. It consumes mental space, slows reviews, confuses new contributors, and can hide old secrets. Deleting it with discipline is not cosmetic cleanup. It is architecture.

📱 WhatsApp: +56962170366
🐦 X.com: @mariohealthbits
🌐 mariohealthbits.dev

Related reading