Migração de código com IA: Como usar LLMs pra modernizar codebases legados sem perder a sanidade
Seu CTO acabou de anunciar que vocês vão migrar 200 mil linhas de AngularJS pra React. O prazo é seis meses. Metade do time nunca mexeu em AngularJS. Alguém na reunião solta um "não dá pra usar IA pra isso?" e todo mundo vira pra te olhar.
Isso tá acontecendo em milhares de empresas agora. Codebases legados — AngularJS, jQuery, Java 8, Python 2, COBOL, PHP antigo — precisam ser modernizados. A abordagem tradicional (reescrita manual) leva anos e acaba com a moral do time. A abordagem nova (jogar pro LLM e rezar) parece ótima até você perceber que a IA criou três bugs novos pra cada um que corrigiu.
A verdade tá no meio. LLMs podem acelerar dramaticamente a migração de código, mas só se você montar o pipeline certo em volta. Esse guia é sobre montar esse pipeline: o que funciona, o que não funciona, e como evitar os erros que transformam um projeto de migração com IA num desastre pior que o código legado.
Por que migração de código é ideal pra LLMs (e por que é mais difícil do que parece)
Migração de código tem propriedades que a tornam especialmente boa pra LLMs:
- É cheio de padrão: A maioria das migrações repete a mesma transformação em centenas de arquivos. Controllers AngularJS seguem um template. POJOs Java seguem um template. LLMs são bons em reconhecer e aplicar padrões.
- Input/output definido: Você tem um "antes" (framework velho) e um "depois" (framework novo) claros. As regras de transformação são conhecíveis.
- Verificável: Diferente de escrita criativa ou resumos, migração de código tem verificação dura — compila? Testes passam?
Mas tem desafios fundamentais que a maioria dos approaches "manda no ChatGPT" perdem:
O problema do context window
Um controller AngularJS real não existe sozinho. Importa services que importam outros services. Referencia templates que referenciam directives. Depende de cadeias de herança de scope que cruzam múltiplos arquivos. Um LLM que só vê o arquivo do controller vai produzir código React sintaticamente correto mas semanticamente errado.
┌─────────────────────────────────────────────────────────────┐
│ O Iceberg da Migração │
│ │
│ ┌──────────────┐ │
│ │ Controller │ ← LLM vê isso │
│ │ (1 arquivo) │ │
│ ─────────┴──────────────┴───────── │
│ / \ │
│ / ┌──────────┐ ┌──────────┐ \ │
│ / │ Services │ │Templates │ \ │
│ / │ (12 dep) │ │ (3 HTML) │ \ │
│ / └──────────┘ └──────────┘ \ │
│ / ┌──────────┐ ┌──────────┐ ┌──────────┐ \ │
│ / │ Scope │ │ Route │ │ Estado │ \ │
│ / │ Chain │ │ Config │ │ Global │ \ │
│ / └──────────┘ └──────────┘ └──────────┘ \ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ← LLM precisa de TUDO isso pra migrar certo │
└─────────────────────────────────────────────────────────────┘
Quando a sintaxe engana
Migrar código não é trocar sintaxe. É traduzir paradigma. AngularJS usa two-way data binding com $scope. React usa fluxo unidirecional com hooks. Não tem mapeamento 1:1. Um LLM que converte mecanicamente $scope.name pra useState('name') vai gerar código que compila mas se comporta diferente nos edge cases: race conditions em formulários, watchers atrasados, timing do digest cycle. É aí que o bicho pega.
A regra 80/20 da migração com IA
Na prática, LLMs lidam bem com ~80% do trabalho — as transformações chatas e repetitivas. Os 20% restantes — lógica de negócio complexa, edge cases específicos do framework, concerns transversais — precisam de julgamento humano. Seu pipeline precisa ser projetado em volta dessa realidade.
A arquitetura: pipeline de migração com AST
A abordagem ingênua — colar arquivo no ChatGPT, receber output — não escala. Isso é o que realmente funciona pra migrações de produção:
┌─────────────────────────────────────────────────────────────┐
│ Pipeline de Migração com IA │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 1. Parse │───▶│ 2. Chunk │───▶│ 3. Enri- │ │
│ │ (AST) │ │ (Split) │ │ quecer │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 6. Teste │◀───│ 5. Pós- │◀───│ 4. LLM │ │
│ │(Verificar)│ │Processar │ │Transform │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 7. Review│ Confiança < 90%? → Flag pra review humano │
│ │ Humano │ │
│ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
Bora por cada etapa.
Step 1: Parsear pra AST
Não jogue código fonte cru pro LLM. Parseia pra um Abstract Syntax Tree primeiro. Isso dá awareness estrutural que texto plano não tem.
import { Project, SyntaxKind } from 'ts-morph'; interface MigrationUnit { filePath: string; type: 'component' | 'service' | 'directive' | 'filter' | 'config'; className: string; dependencies: string[]; templatePath?: string; sourceCode: string; ast: any; complexity: number; } function parseAngularModule(filePath: string): MigrationUnit[] { const project = new Project(); const sourceFile = project.addSourceFileAtPath(filePath); const units: MigrationUnit[] = []; sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression).forEach(call => { const expr = call.getExpression().getText(); if (expr.includes('.component') || expr.includes('.controller')) { const args = call.getArguments(); const name = args[0]?.getText().replace(/['"]/g, ''); const deps = extractDependencies(call); const complexity = calculateComplexity(call); units.push({ filePath, type: expr.includes('.component') ? 'component' : 'service', className: name, dependencies: deps, sourceCode: call.getFullText(), ast: call.getStructure(), complexity, }); } }); return units; } function calculateComplexity(node: any): number { let complexity = 1; node.getDescendantsOfKind(SyntaxKind.IfStatement).forEach(() => complexity++); node.getDescendantsOfKind(SyntaxKind.SwitchStatement).forEach(() => complexity++); node.getDescendantsOfKind(SyntaxKind.ForStatement).forEach(() => complexity++); node.getDescendantsOfKind(SyntaxKind.WhileStatement).forEach(() => complexity++); node.getDescendantsOfKind(SyntaxKind.ConditionalExpression).forEach(() => complexity++); return complexity; }
Por que AST primeiro?
- Priorizar: Migrar arquivos de baixa complexidade primeiro (taxa de sucesso maior do LLM)
- Dividir com inteligência: Cortar em limites de função/classe, não em linhas arbitrárias
- Rastrear dependências: Saber quais arquivos precisam ser migrados juntos
- Validar output: Comparar estrutura AST de entrada e saída pra pegar regressões
Step 2: Chunking por unidade de migração
Não migre arquivos inteiros. Migre unidades lógicas — um componente, um service, uma função utilitária por vez. Cada chamada ao LLM fica focada e dentro do context window.
interface MigrationBatch { primary: MigrationUnit; dependencies: MigrationUnit[]; templates: string[]; totalTokens: number; } function createBatches( units: MigrationUnit[], maxTokens: number = 12000 ): MigrationBatch[] { const sorted = units.sort((a, b) => a.complexity - b.complexity); return sorted.map(unit => { const deps = unit.dependencies .map(dep => units.find(u => u.className === dep)) .filter(Boolean) as MigrationUnit[]; const depContext = deps.map(d => extractTypeSignature(d)); const totalTokens = estimateTokens( unit.sourceCode + depContext.join('\n') ); return { primary: unit, dependencies: deps, templates: unit.templatePath ? [readFileSync(unit.templatePath, 'utf-8')] : [], totalTokens, }; }); }
Step 3: Enriquecer com contexto
Aqui é onde a maioria das tentativas de migração com IA falha. O LLM precisa de contexto além do arquivo que tá sendo migrado:
interface MigrationContext { batch: MigrationBatch; targetFramework: { version: string; stateManagement: string; styling: string; routing: string; }; conventions: { fileNaming: string; exportStyle: string; hookPrefix: string; testFramework: string; }; // Migrações já completadas (few-shot learning) examples: { before: string; after: string; explanation: string; }[]; edgeCases: string[]; }
O campo examples é chave. Depois de migrar manualmente 3-5 componentes representativos, inclua como exemplos few-shot em cada chamada ao LLM. A qualidade do output melhora demais porque o modelo aprende as convenções específicas do seu projeto.
Step 4: Transformação LLM
O prompt importa muito. As decisões chave:
- Rule 8 é a mais importante: "Preservar TODA a lógica de negócio exatamente." LLMs querem "melhorar" o código durante a migração. Você não quer isso. Migração e refactoring são PRs separados.
- Few-shot examples: Inclua 2-3 migrações reais do seu projeto. Isso vale mais que qualquer quantidade de texto de instrução.
- Contexto de dependências: Inclua type signatures das dependências pro LLM saber o que tá disponível sem gastar tokens com detalhes de implementação.
Step 5: Pós-processamento
Não confie no output cru do LLM. Pós-processe:
async function postProcess( llmOutput: string, context: MigrationContext ): Promise<{ code: string; confidence: number; issues: string[]; }> { const issues: string[] = []; let confidence = 100; try { const project = new Project({ useInMemoryFileSystem: true }); const sourceFile = project.createSourceFile('output.tsx', llmOutput); const diagnostics = sourceFile.getPreEmitDiagnostics(); if (diagnostics.length > 0) { confidence -= diagnostics.length * 10; issues.push( ...diagnostics.map(d => `TS Error: ${d.getMessageText()}`) ); } const imports = sourceFile.getImportDeclarations(); for (const imp of imports) { const moduleSpecifier = imp.getModuleSpecifierValue(); if (!isValidImport(moduleSpecifier, context)) { confidence -= 15; issues.push(`Import não resolvido: ${moduleSpecifier}`); } } const text = sourceFile.getFullText(); if (text.includes('// TODO') || text.includes('// FIXME')) { confidence -= 5; issues.push('LLM adicionou comentários TODO/FIXME'); } const hookViolations = checkHookRules(sourceFile); if (hookViolations.length > 0) { confidence -= hookViolations.length * 20; issues.push(...hookViolations); } const originalFunctions = extractFunctionNames( context.batch.primary.sourceCode ); const migratedFunctions = extractFunctionNames(llmOutput); const missing = originalFunctions.filter( f => !migratedFunctions.some(m => isSimilarName(f, m)) ); if (missing.length > 0) { confidence -= missing.length * 15; issues.push(`Funções faltando: ${missing.join(', ')}`); } return { code: llmOutput, confidence, issues }; } catch (e) { return { code: llmOutput, confidence: 0, issues: [`Erro de parse: ${e.message}`], }; } }
Step 6: Testes automatizados
O passo mais crítico. Nenhuma migração deveria ser commitada sem verificação automatizada:
async function verifyMigration( originalPath: string, migratedPath: string, testSuite: string ): Promise<MigrationVerification> { const results: MigrationVerification = { compiles: false, testsPass: false, renderMatches: false, accessibilityPass: false, performanceRegression: false, }; const compileResult = await exec(`npx tsc --noEmit ${migratedPath}`); results.compiles = compileResult.exitCode === 0; if (testSuite) { const testResult = await exec(`npx vitest run ${testSuite}`); results.testsPass = testResult.exitCode === 0; } results.renderMatches = await compareScreenshots( originalPath, migratedPath ); return results; }
Step 7: Review humano com scoring de confiança
Nem todo arquivo migrado precisa do mesmo nível de atenção humana:
function triageMigration( result: PostProcessResult, verification: MigrationVerification ): 'auto-merge' | 'quick-review' | 'deep-review' | 'manual-rewrite' { if (result.confidence >= 95 && verification.testsPass && verification.compiles) { return 'auto-merge'; } if (result.confidence >= 80 && verification.compiles) { return 'quick-review'; } if (result.confidence >= 50) { return 'deep-review'; } return 'manual-rewrite'; }
Na prática, pra uma migração AngularJS→React bem preparada:
- ~60% arquivos: Auto-merge ou review rápido
- ~25% arquivos: Review profundo (principalmente state management complexo)
- ~15% arquivos: Reescrita manual (herança de $scope complexa, hacks do digest cycle)
Padrões de migração no mundo real
Padrão 1: AngularJS → React
A migração enterprise mais comum hoje.
// ❌ ANTES: Controller AngularJS angular.module('app').controller('UserListCtrl', ['$scope', '$http', 'UserService', 'NotificationService', function($scope, $http, UserService, NotificationService) { $scope.users = []; $scope.loading = true; $scope.searchTerm = ''; $scope.selectedRole = 'all'; $scope.loadUsers = function() { $scope.loading = true; UserService.getAll({ role: $scope.selectedRole }) .then(function(users) { $scope.users = users; $scope.loading = false; }) .catch(function(err) { NotificationService.error('Failed to load users'); $scope.loading = false; }); }; $scope.filteredUsers = function() { if (!$scope.searchTerm) return $scope.users; return $scope.users.filter(function(user) { return user.name.toLowerCase() .includes($scope.searchTerm.toLowerCase()); }); }; $scope.$watch('selectedRole', function(newVal, oldVal) { if (newVal !== oldVal) $scope.loadUsers(); }); $scope.loadUsers(); } ]);
// ✅ DEPOIS: React + TypeScript import { useState, useEffect, useMemo, useCallback } from 'react'; import { useUserService } from '@/hooks/useUserService'; import { useNotification } from '@/hooks/useNotification'; interface User { id: string; name: string; email: string; role: string; } type RoleFilter = 'all' | 'admin' | 'user' | 'moderator'; export function UserList() { const [users, setUsers] = useState<User[]>([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); const [selectedRole, setSelectedRole] = useState<RoleFilter>('all'); const userService = useUserService(); const { showError } = useNotification(); const loadUsers = useCallback(async () => { setLoading(true); try { const data = await userService.getAll({ role: selectedRole }); setUsers(data); } catch { showError('Failed to load users'); } finally { setLoading(false); } }, [selectedRole, userService, showError]); useEffect(() => { loadUsers(); }, [loadUsers]); const filteredUsers = useMemo(() => { if (!searchTerm) return users; return users.filter(user => user.name.toLowerCase().includes(searchTerm.toLowerCase()) ); }, [users, searchTerm]); if (loading) return <LoadingSpinner />; return ( <div> <SearchInput value={searchTerm} onChange={setSearchTerm} /> <RoleFilter value={selectedRole} onChange={setSelectedRole} /> <UserTable users={filteredUsers} /> </div> ); }
O que o LLM acerta: Mapeamento de estado, conversão básica de hooks, dependências de effects.
O que o LLM erra: Arrays de dependência de useCallback (deps faltando), limites de otimização de useMemo, padrões de error handling específicos do seu sistema de notificações.
Padrão 2: Java 8 → Kotlin
// ❌ ANTES: Java 8 public class OrderProcessor { private final OrderRepository orderRepo; private final PaymentService paymentService; private final NotificationService notificationService; public OrderProcessor(OrderRepository orderRepo, PaymentService paymentService, NotificationService notificationService) { this.orderRepo = orderRepo; this.paymentService = paymentService; this.notificationService = notificationService; } public OrderResult processOrder(OrderRequest request) { if (request == null || request.getItems() == null || request.getItems().isEmpty()) { throw new IllegalArgumentException("Invalid order"); } BigDecimal total = request.getItems().stream() .map(item -> item.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); if (total.compareTo(BigDecimal.valueOf(10000)) > 0) { request.setDiscount(total.multiply( BigDecimal.valueOf(0.1))); } PaymentResult payment = paymentService.charge( request.getCustomerId(), total); if (!payment.isSuccessful()) { return OrderResult.failed(payment.getErrorMessage()); } Order order = orderRepo.save( Order.from(request, payment.getTransactionId())); notificationService.sendConfirmation( request.getCustomerId(), order); return OrderResult.success(order); } }
// ✅ DEPOIS: Kotlin class OrderProcessor( private val orderRepo: OrderRepository, private val paymentService: PaymentService, private val notificationService: NotificationService, ) { fun processOrder(request: OrderRequest): OrderResult { require(request.items.isNotEmpty()) { "Invalid order" } val total = request.items.sumOf { item -> item.price * item.quantity.toBigDecimal() } if (total > 10_000.toBigDecimal()) { request.discount = total * 0.1.toBigDecimal() } val payment = paymentService.charge(request.customerId, total) if (!payment.isSuccessful) { return OrderResult.failed(payment.errorMessage) } val order = orderRepo.save( Order.from(request, payment.transactionId) ) notificationService.sendConfirmation(request.customerId, order) return OrderResult.success(order) } }
O que o LLM acerta: Null safety, require em vez de checks explícitos de null, sintaxe de acesso a propriedades, trailing commas, simplificação de expressões.
O que o LLM erra: Operator overloading custom pra BigDecimal, extensões de coleção específicas de Kotlin (sumOf), tratamento de erros idiomático com Result ou sealed classes.
Padrão 3: Python 2 → Python 3
# ❌ ANTES: Python 2 class DataProcessor(object): def __init__(self, config): self.config = config self.logger = logging.getLogger(__name__) def process_batch(self, items): results = [] for item in items: try: processed = self._transform(item) results.append(processed) except Exception, e: self.logger.error( u"Failed to process item %s: %s" % (item.get('id', 'unknown'), unicode(e)) ) return results def _transform(self, item): if isinstance(item, basestring): item = {'value': item} keys = item.keys() keys.sort() output = {} for key in keys: value = item[key] if isinstance(value, unicode): output[key] = value.encode('utf-8') elif isinstance(value, (int, long)): output[key] = float(value) else: output[key] = value return output
# ✅ DEPOIS: Python 3 class DataProcessor: def __init__(self, config): self.config = config self.logger = logging.getLogger(__name__) def process_batch(self, items): results = [] for item in items: try: processed = self._transform(item) results.append(processed) except Exception as e: self.logger.error( f"Failed to process item {item.get('id', 'unknown')}: {e}" ) return results def _transform(self, item): if isinstance(item, str): item = {'value': item} output = {} for key in sorted(item.keys()): value = item[key] if isinstance(value, str): output[key] = value elif isinstance(value, int): output[key] = float(value) else: output[key] = value return output
O que o LLM acerta: Sintaxe except Exception as e, f-strings, remover unicode/basestring/long, remover herança de (object).
O que o LLM erra: Mudanças sutis de comportamento — dict.keys() do Python 2 retorna lista (mutável), Python 3 retorna view. O LLM envolve corretamente com sorted(), mas perde casos onde o código muta a lista de keys durante iteração.
O playbook de prompt engineering pra migrações
Depois de rodar milhares de transformações, esses padrões consistentemente produzem os melhores resultados:
1. System message: a persona de migração
You are a staff-level software engineer performing a code migration.
Your output will be committed directly to a production repository.
CRITICAL RULES:
- Preserve ALL business logic exactly as-is. Do NOT refactor, optimize,
or "improve" the code during migration.
- If you are unsure about a conversion, mark it with
__MIGRATION_REVIEW__ in a comment.
- Do NOT add explanatory comments about the migration itself.
- Do NOT change variable names unless required by target language
conventions.
- Output ONLY the migrated code. No markdown fences, no explanations.
2. Exemplos few-shot ganham de instruções longas
Em vez de escrever 50 regras sobre como converter AngularJS pra React, inclua 3 exemplos reais. O modelo aprende padrões melhor de exemplos concretos do que de regras abstratas.
3. O princípio "Preservar, não melhorar"
A regra mais importante. LLMs adoram fazer isso:
- Adicionar error handling que não existia
- Otimizar algoritmos
- Renomear variáveis pra ficarem "mais claras"
- Adicionar tipos TypeScript rigorosos ou relaxados demais
Cada uma dessas mudanças introduz risco. Migração e melhoria devem ser PRs separados.
4. Marcadores de confiança
Diga pro LLM marcar incerteza:
If any conversion is ambiguous (e.g., unclear scope inheritance,
non-obvious side effects), add this exact comment:
// __MIGRATION_REVIEW__: [reason for uncertainty]
This will be caught by our post-processing pipeline and flagged
for human review.
Torcer pro LLM acertar é fria. Muito melhor ele te dizer "não tenho certeza aqui".
Guardrails de produção: o que pode dar errado
1. O import alucinado
O LLM inventa imports pra pacotes que não existem. Ele viu import { useQueryClient } from '@tanstack/react-query' nos dados de treino, então usa — mesmo que seu projeto use SWR.
Fix: Pós-processe todos os imports contra seu package.json real e a estrutura de arquivos do projeto.
2. A mudança silenciosa de comportamento
O bug mais perigoso. O código compila, testes passam (porque os testes são rasos), mas o comportamento mudou sutilmente. Exemplos comuns:
- Timing de event handlers (digest cycle do Angular vs state batching do React)
- Diferenças no tratamento de null/undefined
- Ordem de operações async
Fix: Invista em testes de integração antes de começar a migração. Escreva contra o codebase velho, depois verifique que passam contra o código novo.
3. O componente over-engineered
O LLM converte um controller AngularJS simples num componente React com 4 custom hooks, 3 context providers e um reducer. Tecnicamente correto mas impossível de manter.
Fix: Adicione ao prompt: "Preferir simplicidade. Usar useState a menos que a lógica de estado seja complexa o suficiente pra precisar de useReducer. Não criar custom hooks pra lógica usada em apenas um componente."
4. A explosão de copy-paste
Ao migrar componentes similares, o LLM produz código quase idêntico sem extrair lógica compartilhada. Você acaba com 20 componentes, cada um com sua cópia do mesmo padrão de data fetching.
Fix: Depois do primeiro passe de migração, rode um segundo passe focado em extrair padrões compartilhados em hooks e utilities. Isso é melhor como passo separado porque o LLM tem acesso a todos os arquivos migrados de uma vez.
Medindo o sucesso da migração
| Métrica | Meta | Como medir |
|---|---|---|
| Taxa de auto-merge | > 50% | Arquivos que passam todas as verificações |
| Sucesso de compilação | > 90% | Primeira compilação TypeScript |
| Taxa de testes | > 85% | Test suite existente vs código migrado |
| Preservação de lógica | 100% | Suite de testes de integração |
| Linhas migradas / dia | 2,000+ | Depois de calibrar o pipeline |
| Tempo de review / arquivo | < 15 min | Média do tier "quick-review" |
O dashboard de migração
Monte um dashboard simples que rastreie progresso por módulo:
interface MigrationStatus { module: string; totalFiles: number; migrated: number; autoMerged: number; inReview: number; manualRewrite: number; avgConfidence: number; blockers: string[]; }
Essa visibilidade é crucial pra comunicação com stakeholders. "Migramos 147 de 200 arquivos com 92% de taxa de auto-merge" é muito mais convincente que "tá indo bem".
Qual LLM usar
Em abril de 2026, baseado em testes extensivos de migração:
| Modelo | Melhor pra | Limitações |
|---|---|---|
| Claude 4 Sonnet | Preservação de lógica complexa, precisão TypeScript, retenção fiel de lógica de negócio | Context window de 200K pode limitar em batches multi-arquivo grandes |
| GPT-5 | Suporte amplo de linguagens, formatação consistente, forte em seguir instruções | Tendência a refatorar demais; custo por token mais alto |
| Gemini 2.5 Pro | Contexto longo (1M tokens), compreensão multi-arquivo, custo-benefício em escala | Às vezes inventa APIs inexistentes |
| DeepSeek V3 | Custo-benefício pra transformações simples, forte em padrões Python/Java | Menor precisão em lógica de negócio complexa e dependências cross-file |
Recomendação: Use Claude 4 Sonnet ou GPT-5 pra migração inicial (preservação de lógica complexa), depois Gemini 2.5 Pro ou DeepSeek pra passes de limpeza em arquivos simples. A diferença de custo é 10-50x, e arquivos simples não precisam do modelo caro.
A real: o que a IA não consegue migrar
- Decisões de arquitetura: A app monolítica AngularJS deveria virar micro-frontend? A IA não vai te dizer.
- Design de state management: Zustand, Redux ou Context? O LLM usa o que você mandar, mas não toma a decisão arquitetural.
- Otimização de performance: O LLM não conhece seus padrões de tráfego nem seus bottlenecks.
- Validação de regras de negócio: Se o código original tem um bug que funciona "como esperado" (compensado em outro lugar), o LLM vai reproduzir fielmente o bug.
- Concerns transversais: Logging, monitoramento, tracking de erros, feature flags — precisam de design humano na nova arquitetura.
Timeline: um plano de migração realista
Pra uma migração de 200K LOC de AngularJS pra React:
| Fase | Duração | O que rola |
|---|---|---|
| 1. Setup | 2 semanas | Montar pipeline, configurar LLM, criar 5-10 migrações manuais de exemplo |
| 2. Piloto | 2 semanas | Migrar 1 módulo (20-30 arquivos) end-to-end, calibrar prompts |
| 3. Escalar | 8-10 semanas | Pipeline processa os módulos restantes em ordem de dependência |
| 4. Polir | 4 semanas | Corrigir edge cases, extrair padrões compartilhados, calibrar performance |
| 5. Validar | 2 semanas | Teste de regressão completo, aprovação dos stakeholders |
Total: ~4-5 meses vs os 12-18 meses tradicionais pra migração manual.
O ponto chave: fases 1 e 2 são as mais importantes. Se seu pipeline consegue migrar o módulo piloto com >80% de taxa de auto-merge, o resto é execução. Se o piloto falhar, repensar a abordagem antes de escalar.
Checklist de migração
Preparação
- Test suite existente com >70% de cobertura nos paths críticos
- Convenções do framework alvo documentadas
- 5-10 migrações de referência feitas na mão
- Parser AST configurado pra linguagem fonte
- Pipeline CI incluindo verificação de compilação e testes
Pipeline
- Template de prompt testado em 20+ arquivos representativos
- Pós-processamento pega imports inválidos
- Scoring de confiança calibrado (threshold de auto-merge definido)
- Exemplos few-shot incluídos pra cada tipo de arquivo
- Ordem de resolução de dependências calculada
Execução
- Migrando em ordem de dependências (nós folha primeiro)
- Cada batch verificado antes de ir pro próximo
- Dashboard rastreando progresso e scores de confiança
- Reviewers humanos definidos por módulo
- Estratégia de rollback definida por módulo
Validação
- Testes de integração passam contra código migrado
- Testes de regressão visual completados
- Benchmarks de performance iguais ou melhores
- Review de segurança nos paths de auth/dados
- Aprovação do time de produto
Migração de código com IA não é mágica. É engenharia — montar um pipeline que aproveita os LLMs no que eles são bons (transformação de padrão) enquanto coloca guardrails pro que eles são ruins (garantias de corretude). Monte bem o pipeline e você transforma uma migração de um ano num trimestre. Monte errado e você vai passar esse trimestre debugando por que a IA decidiu reescrever sua lógica de auth. Bora construir.
Explore ferramentas relacionadas
Experimente estas ferramentas gratuitas do Pockit