Skip al contenido
Forensic Code Cleanup: borrar código con método

Forensic Code Cleanup: borrar código con método

Cómo borré 12 archivos zombie en Shuri con forensic cleanup, ripgrep, tests y revisión adversarial como guardrail final.

MI

Mario Inostroza

El issue parecía una migración simple

Issue #461 decía algo razonable: migrar 7 handlers legacy de Shuri desde .cjs a TypeScript.

Sonaba como una tarea mecánica. Abrir archivos viejos, convertir imports, tipar un poco, sacar variables obsoletas y cerrar el PR.

Pero cuando entré a apps/agents/src/agents/shuri/handlers/, algo no cuadraba.

Había 7 handlers .cjs, un bridge.cjs, referencias a un registry/ de JSONs y 14 archivos de test con mocks vacíos. No mocks parciales. No doubles con comportamiento. Vacíos.

// Repetido en varios tests
vi.mock('../handlers/quote_medical_order.cjs', () => ({}));
vi.mock('../handlers/buy_medical_order.cjs', () => ({}));
vi.mock('../handlers/apply_coupon.cjs', () => ({}));

Eso no es cobertura. Es una señal de alarma.

Un mock que retorna {} puede significar dos cosas: que el módulo ya no importa, o que el test está mintiendo sobre una dependencia crítica. Antes de migrar, había que investigar.

Forensic antes de refactor

La tentación natural era convertir los archivos a TypeScript y seguir. Pero una migración sin forensic puede terminar preservando código muerto con mejor sintaxis.

La primera pregunta fue simple: ¿alguien consume realmente estos handlers?

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

Resultado: cero imports reales.

Los handlers legacy no participaban en producción. Los únicos archivos que los nombraban eran tests con mocks vacíos.

La segunda pregunta era más peligrosa: ¿hay otro sistema con nombres parecidos?

Sí. En tools/shuri/ había un harness de simulación con archivos similares. Si borraba sin verificar, podía romper herramientas de debugging.

rg "apps/agents" tools/shuri/

Resultado: cero referencias.

tools/shuri/ y apps/agents/src/agents/shuri/ compartían nombres, pero no dependencias. El harness de simulación era otro ámbito. Los handlers zombie sí podían morir.

Lo que realmente había pasado

La respuesta estaba en un PR anterior.

PR #423 había unificado el flujo de cotización y compra de WhatsApp en PurchaseHandler.handleQuote, ya en TypeScript. La migración funcional estaba hecha. Lo que quedó atrás fue basura técnica: archivos no usados, imports circulares, mocks defensivos y una variable de entorno que ya nadie necesitaba.

El cambio correcto no era migrar 7 handlers. Era borrarlos.

El PR final terminó eliminando 12 archivos, 108 líneas de mocks vacíos y 1 env var obsoleta. Después de eso, 1058 tests seguían pasando.

Ese número importa. Borrar sin tests es optimismo. Borrar con 1058 tests verdes y búsqueda de referencias es ingeniería.

El método forense

El corazón del trabajo no fue la revisión adversarial. Fue el método forense previo.

Antes de tocar archivos, armé una matriz simple:

PreguntaEvidencia buscadaResultado
¿Quién importa estos handlers?rg en producción0 consumidores reales
¿Los tests validan comportamiento?mocks y assertions108 líneas de mocks vacíos
¿Hay herramientas con nombres parecidos?rg entre tools/ y apps/ámbitos separados
¿Qué reemplazó el flujo viejo?PR anterior y handler actualPurchaseHandler.handleQuote

Esa tabla cambió el objetivo. Ya no era “migrar legacy a TypeScript”. Era demostrar que el legacy estaba muerto.

Recién ahí el PR tuvo sentido: eliminar archivos, correr tests y revisar que no quedaran referencias huérfanas.

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

La revisión adversarial como guardrail

Después del forensic cleanup, usé Judgment Day solo como guardrail final: una revisión adversarial para intentar romper el diff antes del merge.

Aquí conviene corregir el ámbito. En notas antiguas lo describí como “mi protocolo Judgment Day”, pero hoy lo uso dentro del harness de trabajo basado en Gentle AI, el proyecto open source de Alan Buscaglia: github.com/Gentleman-Programming/gentle-ai.

El foco del post no es Judgment Day. El foco es borrar código muerto con evidencia. Gentle AI entra al final para revisar blockers: secretos, imports huérfanos, archivos .bak, env vars zombie y documentación que podía inducir a error.

En este caso, esa revisión encontró un archivo .bak con una API key expuesta y un archivo adicional que se había escapado del primer pase. Sin el forensic cleanup, habría sido difícil acotar el problema. Sin la revisión adversarial, el diff podía haber salido con riesgos escondidos.

El diagrama mental del cleanup

El proceso terminó siendo menos “refactor” y más investigación forense:

  1. Mapear candidatos a código muerto.
  2. Verificar consumidores reales con rg.
  3. Separar producción de herramientas auxiliares.
  4. Eliminar solo lo que no tiene consumidores.
  5. Correr tests.
  6. Usar revisión adversarial como guardrail final.
  7. Repetir solo si aparecen blockers reales.

Ese orden importa. Si partes borrando, adivinas. Si partes mapeando, reduces superficie de error.

Lo que aprendí

1. Un mock vacío no es inocente

Un mock vacío puede esconder código muerto durante meses. El test sigue pasando porque nunca ejercita la dependencia real. Eso crea una ilusión de seguridad.

Cuando veo vi.mock(..., () => ({})), ahora lo trato como una pregunta pendiente: ¿este módulo todavía existe por una razón o solo estamos protegiendo basura histórica?

2. Migrar código muerto es deuda técnica con maquillaje

Convertir .cjs a TypeScript habría dejado el sistema igual de confuso, pero con tipos. Eso no mejora la arquitectura. Solo hace más caro detectar que el archivo no servía.

La mejor migración fue no migrar.

3. Los nombres parecidos son una trampa

tools/shuri/ y apps/agents/src/agents/shuri/ parecían parte del mismo flujo. No lo eran. Verificar límites entre harness, scripts y producción evitó borrar herramientas útiles.

4. La revisión adversarial es el cierre, no el centro

Gentle AI aporta un marco útil para revisar con disciplina. Pero en este caso el valor principal estuvo antes: mapear consumidores, probar límites y demostrar que el código estaba muerto.

El crédito sigue siendo importante: uso Gentle AI de Alan Buscaglia como base de disciplina de revisión. Pero el aprendizaje técnico del PR es más simple: no migres código muerto; elimínalo con evidencia.

Lo que viene

El próximo candidato es service-token.cjs, el último archivo .cjs vivo en Shuri. Ahí el forensic ya está más acotado: core/shuri-bridge.ts aparece como consumer real, así que no corresponde borrarlo a ciegas.

La regla queda escrita: grep primero, refactor después.

El código muerto no es inofensivo. Ocupa espacio mental, ralentiza revisiones, confunde a nuevos contribuidores y puede esconder secretos viejos. Borrarlo con método no es limpieza cosmética. Es arquitectura.

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

Lecturas relacionadas