Depurando Gargalos Legados com IA: Um Workflow Sistemático para Codebases Complexas
Todo desenvolvedor que já colocou a mão em um sistema legado sabe como é aquela sensação: você abre o repositório, olha para uma função de 400 linhas sem testes, e percebe que qualquer mudança pode derrubar algo em produção que ninguém sequer sabe que existe. Foi exatamente esse contexto que enfrentei recentemente ao trabalhar na estabilização de uma plataforma administrativa consolidada — um sistema de monitoramento financeiro com lógica de negócios crítica, anos de acúmulo técnico e zero margem para erro.
O que mudou minha abordagem dessa vez foi integrar o Claude Code ao meu workflow de debugging. Não como uma bala de prata, mas como um parceiro de raciocínio sistemático. Neste artigo, vou compartilhar o processo que desenvolvi para navegar, refatorar e depurar restrições legadas com segurança — usando TypeScript como linguagem principal e IA como copiloto.
O Problema: Alarmes Fantasmas em um Sistema de Monitoramento
O sistema em questão era uma plataforma administrativa com um módulo de cash-polling — basicamente um mecanismo que monitora operações financeiras em intervalos regulares e dispara alarmes quando detecta anomalias. O bug era sutil e antigo: erros transientes estavam sendo persistidos incorretamente, de modo que uma operação que falhava momentaneamente e depois se recuperava com sucesso ainda aparecia como "em alarme" no dashboard.
O problema parecia simples na superfície, mas a lógica de estado do alarme estava espalhada por vários módulos interdependentes. Mudar qualquer coisa sem entender o fluxo completo era como desarmar uma bomba sem o manual.
Minha primeira tentativa foi a abordagem clássica: ler o código linearmente, traçar o fluxo à mão, tentar reproduzir o bug localmente. Depois de algumas horas, eu tinha um mapa mental fragmentado e muita incerteza. Foi aí que mudei de estratégia.
Usando IA para Mapear Território Desconhecido
A primeira coisa que aprendi ao usar o Claude Code em codebases legadas é que a ferramenta é mais útil quando você a trata como um interlocutor para raciocínio estruturado, não como um gerador de código automático.
Meu workflow ficou assim:
1. Contexto antes de código
Antes de pedir qualquer sugestão de fix, eu alimentava o modelo com contexto rico: o comportamento esperado do sistema, o comportamento observado, e os trechos de código relevantes. Algo como:
"Este módulo de polling verifica o status de operações financeiras a cada N segundos. Quando uma operação falha, um alarme é ativado. Quando a operação seguinte tem sucesso, o alarme deveria ser desativado. O bug é que o alarme persiste mesmo após o sucesso. Aqui está a função de atualização de estado..."
Essa abordagem me economizou tempo porque as sugestões vinham calibradas para o comportamento real do sistema, não para um caso genérico.
2. Hipóteses antes de soluções
Em vez de pedir "como corrigir isso?", eu perguntava: "Quais são as possíveis causas para esse comportamento, dado este código?" Isso gerava uma lista de hipóteses que eu podia validar sistematicamente — muito mais confiável do que seguir o primeiro instinto.
O modelo identificou algo que eu tinha passado por cima: a função de reset do alarme só era chamada quando a operação completava sem nenhum erro anterior na sessão, ignorando o caso em que erros transientes já tinham sido registrados. Era uma condição de guarda mal posicionada.
3. Validação incremental com TypeScript
Quando chegou a hora de implementar, optei por refatorar a lógica de estado do alarme em uma função pura e testável. Aqui está uma versão simplificada e sanitizada do padrão que usei:
// Antes: lógica de alarme acoplada ao handler de polling
async function pollOperation(operationId: string): Promise<void> {
try {
const result = await fetchOperationStatus(operationId);
if (result.status === 'ERROR') {
await triggerAlarm(operationId);
}
// BUG: sem lógica de reset quando status é 'OK' após erro anterior
} catch (err) {
await triggerAlarm(operationId);
logger.error('Polling failed', { operationId, err });
}
}
// Depois: lógica de estado extraída e explícita
type AlarmState = 'ACTIVE' | 'RESOLVED' | 'IDLE';
interface OperationAlarmContext {
operationId: string;
currentAlarmState: AlarmState;
lastSuccessAt: Date | null;
}
function resolveAlarmTransition(
context: OperationAlarmContext,
operationStatus: 'OK' | 'ERROR' | 'TRANSIENT_ERROR'
): AlarmState {
const { currentAlarmState } = context;
if (operationStatus === 'OK') {
// Sucesso sempre resolve o alarme, independente do histórico
return currentAlarmState === 'ACTIVE' ? 'RESOLVED' : 'IDLE';
}
if (operationStatus === 'TRANSIENT_ERROR') {
// Erros transientes ativam alarme, mas não bloqueiam resolução futura
return 'ACTIVE';
}
// Erro persistente mantém ou ativa alarme
return 'ACTIVE';
}
async function pollOperation(
context: OperationAlarmContext
): Promise<OperationAlarmContext> {
let operationStatus: 'OK' | 'ERROR' | 'TRANSIENT_ERROR';
try {
const result = await fetchOperationStatus(context.operationId);
operationStatus = result.status === 'ERROR' ? 'ERROR' : 'OK';
} catch (err) {
// Falha na rede ou timeout = erro transiente
operationStatus = 'TRANSIENT_ERROR';
logger.warn('Transient polling failure', { operationId: context.operationId, err });
}
const nextAlarmState = resolveAlarmTransition(context, operationStatus);
if (nextAlarmState === 'RESOLVED') {
await resolveAlarm(context.operationId);
} else if (nextAlarmState === 'ACTIVE' && context.currentAlarmState !== 'ACTIVE') {
await triggerAlarm(context.operationId);
}
return {
...context,
currentAlarmState: nextAlarmState,
lastSuccessAt: operationStatus === 'OK' ? new Date() : context.lastSuccessAt,
};
}
A diferença crítica: a nova versão trata a transição de estado de forma explícita. Um sucesso após qualquer tipo de erro — transiente ou não — agora corretamente move o alarme para RESOLVED. Isso eliminou o bug.
O benefício adicional de extrair resolveAlarmTransition como função pura foi imediato: eu consegui testar todas as transições de estado sem precisar mockar chamadas de rede.
// Testes das transições de estado — sem mocks complexos
describe('resolveAlarmTransition', () => {
const baseContext: OperationAlarmContext = {
operationId: 'op-001',
currentAlarmState: 'IDLE',
lastSuccessAt: null,
};
it('deve resolver alarme ativo quando operação tem sucesso', () => {
const context = { ...baseContext, currentAlarmState: 'ACTIVE' as AlarmState };
const result = resolveAlarmTransition(context, 'OK');
expect(result).toBe('RESOLVED');
});
it('deve manter IDLE quando operação tem sucesso sem alarme ativo', () => {
const result = resolveAlarmTransition(baseContext, 'OK');
expect(result).toBe('IDLE');
});
it('deve ativar alarme em erro transiente', () => {
const result = resolveAlarmTransition(baseContext, 'TRANSIENT_ERROR');
expect(result).toBe('ACTIVE');
});
it('deve resolver alarme mesmo após histórico de erros transientes', () => {
const context = { ...baseContext, currentAlarmState: 'ACTIVE' as AlarmState };
const result = resolveAlarmTransition(context, 'OK');
expect(result).toBe('RESOLVED');
});
});
Navegando Dependências Conflitantes: O Outro Front
Enquanto resolvía o bug de alarme, outro desafio consumia energia em paralelo: uma atualização de SDK que quebrou o processo de build por conta de conflitos entre pacotes. Esse tipo de problema — dependências que se contradizem, versões incompatíveis que somem em builds específicos — é onde a IA também se mostrou valiosa, mas de uma forma diferente.
Aqui o padrão que funcionou foi diagnóstico assistido por busca exaustiva. Em vez de tentar resolver o conflito intuitivamente, eu usava o modelo para gerar uma árvore de hipóteses sobre quais combinações de versões poderiam ser compatíveis, e então validava sistematicamente.
Por exemplo, ao lidar com versões conflitantes de uma biblioteca de área segura entre plataformas diferentes, o raciocínio ficou assim:
# Inspecionar o grafo de dependências para identificar versões requeridas
npx npm-why react-native-safe-area-context
# Ver todas as versões instaladas na árvore
npm list react-native-safe-area-context --all
# Forçar resolução para uma versão específica compatível com ambas as plataformas
# (em package.json, usando resolutions)
{
"resolutions": {
"react-native-safe-area-context": "4.x.x"
},
"overrides": {
"react-native-safe-area-context": "4.x.x"
}
}
O que aprendi nesse processo: a IA é excelente para raciocinar sobre grafos de dependência quando você fornece o output dos comandos de diagnóstico como contexto. Colar o resultado de npm list e perguntar "onde está o conflito?" gera análises muito mais rápidas do que ler a árvore manualmente.
Boas Práticas que Consolidei
Depois desse dia intenso, algumas práticas cristalizaram como parte permanente do meu workflow para codebases legadas com IA:
1. Sempre extraia lógica de negócio crítica em funções puras antes de refatorar. Funções puras são testáveis isoladamente, e isso é seu maior aliado quando o contexto de integração é opaco. Se eu não consigo escrever um teste unitário simples para a lógica, é sinal de que ela está acoplada demais.
2. Use IA para gerar hipóteses, não soluções diretas. Em sistemas legados, o problema raramente é o que parece na superfície. Pedir ao modelo para listar possíveis causas antes de sugerir fixes aumenta a qualidade do diagnóstico.
3. Documente o raciocínio junto com o código. Em refatorações de lógica crítica, adicionei comentários explicando por que cada transição de estado existe — não apenas o que ela faz. Isso foi especialmente importante porque eu sabia que outra pessoa (ou eu mesmo, em seis meses) precisaria entender as decisões sem ter o histórico do bug.
4. Valide em fatias pequenas. Cada mudança em código legado foi acompanhada de um teste antes de avançar. O custo de escrever o teste é sempre menor do que o custo de depurar uma regressão em produção.
5. Trate erros transientes explicitamente. Esse foi o aprendizado central do bug de alarme. Sistemas de polling e monitoramento precisam distinguir entre falhas permanentes e falhas que se recuperam. Ignorar essa distinção cria estados inconsistentes que são difíceis de reproduzir e ainda mais difíceis de depurar.
Conclusão
Trabalhar em código legado com IA não é sobre deixar a ferramenta escrever o código por você — é sobre ter um interlocutor que não se cansa de raciocinar junto. O que mudou na minha abordagem foi a sistematização: contexto rico antes de qualquer pergunta, hipóteses antes de soluções, validação incremental em cada passo.
O bug de alarme que resolvei existia há bastante tempo porque a lógica de transição de estado estava implícita e distribuída. Torná-la explícita — como uma função pura com casos claramente nomeados — foi tanto a solução quanto a prevenção para bugs similares no futuro.
Se você está encarando um sistema legado parecido, meu conselho principal é: resista à tentação de corrigir o sintoma sem entender o estado. Desenhe as transições, nomeie os casos, escreva os testes. A IA pode acelerar essa exploração, mas o raciocínio sistemático ainda é seu.
Top comments (1)
AI is useful on legacy bottlenecks when it helps build a map before suggesting fixes. The dangerous version jumps straight to edits; the better workflow identifies hotspots, ownership, tests, and constraints first.