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.
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:
| Pregunta | Evidencia buscada | Resultado |
|---|---|---|
| ¿Quién importa estos handlers? | rg en producción | 0 consumidores reales |
| ¿Los tests validan comportamiento? | mocks y assertions | 108 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 actual | PurchaseHandler.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:
- Mapear candidatos a código muerto.
- Verificar consumidores reales con
rg. - Separar producción de herramientas auxiliares.
- Eliminar solo lo que no tiene consumidores.
- Correr tests.
- Usar revisión adversarial como guardrail final.
- 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
Recomendado
Mitigación de Alucinaciones en Sub-agentes: Protocolo de 4 Comandos para Producción
Cómo detectar y mitigar alucinaciones en sub-agentes con un protocolo práctico basado en verificación adversarial y redundancia.
Recomendado
Testing estratégico en agentes de IA: de unitarios a adversariales
Cómo construí un sistema de testing multi-capa para agentes médicos, desde unitarios con TDD hasta protocolos adversariales en producción.
En esta serie
MCP / Tool Use: el futuro de la integración de herramientas reales
Cómo los Modelos de Control de Proceso están revolucionando la manera en que los agentes IA interactúan con herramientas externas para ejecutar tareas complejas.