AIREITER

329 comandos, 128 ainda ausentes: reconcilie uma migração com matemática de conjuntos, não com um modelo

Última Atualização: 2026-07-31 07:49:12

Depois de traduzir duzentas funções com testes passando, é fácil concluir que a migração acabou. Mas código traduzido não é necessariamente código migrado. Em uma migração real de Go para Python, havia 329 comandos no registro original e 128 deles simplesmente não existiam no lado novo. A diferença só apareceu quando o problema foi tratado como o que ele é: uma operação de conjuntos.

Validar se uma função foi bem traduzida é justamente uma das tarefas em que modelos se saem bem. Já responder se o sistema inteiro foi migrado exige outra abordagem. É preciso comparar conjuntos — e essa é uma tarefa que um modelo não deveria executar, porque é também uma das formas mais fáceis de ele passar uma falsa sensação de cobertura.

Veja os números dessa migração de Go para Python e por que essa conferência precisa ser resolvida por um script. Ao modelo, cabe explicar o resultado.

Três números que definem o estado da migração

O registro antigo em Go tinha 23 plataformas e 329 comandos. O conjunto de comandos que o novo lado em Python deriva das declarações de argparse tem interseção exata de 201 com o registro antigo. Sobram, portanto, 128 comandos que existem apenas no lado Go: não foram migrados para Python nem ficaram como stubs.

329 = 201 + 128. Essa conta não tem complexidade técnica, mas é a única em toda a migração que responde se ela terminou. E ela desaparece quando o trabalho é feito função por função. Um item ausente é um erro por ausência: não lança erro, não gera exceção, não quebra teste. É apenas um nome que deveria existir e não existe. Ele nunca entrou na conversa com o modelo, e você pode olhar para duzentos checks verdes sem percebê-lo.

Por que evitar stubs e proxies de compatibilidade

No meio da migração, a tentação é deixar um placeholder para os comandos ainda pendentes: um stub com raise NotImplementedError ou um proxy de compatibilidade que encaminha para o binário antigo, só para que “o catálogo de endpoints pareça completo”. Não faça isso. Uma casca vazia custa mais do que uma lacuna, por três motivos.

Primeiro, o stub quebra a reconciliação. O nome do comando passa a integrar o conjunto novo, o diff cai para 0 e parece que o trabalho terminou. Uma lacuna é um alerta vermelho honesto; um stub é uma mentira verde que transforma “faltam 128” em “todos estão presentes”.

Já o proxy de compatibilidade eterniza uma dependência que deveria ter sido removida. Se ele encaminha para o binário Go antigo, o runtime velho nunca poderá ser eliminado. O objetivo da migração é abandonar a stack anterior, mas um proxy de encaminhamento deixa essa stack entrar com o rótulo de “compatibilidade temporária” e permanecer para sempre.

Um endpoint pela metade também engana quem chama. Um Agent ou uma pessoa consulta o catálogo, pressupõe que funciona, faz a chamada e recebe runtime_unavailable — ou, pior, um falso sucesso que devolve silenciosamente um resultado vazio.

A lacuna explícita é, na prática, a opção mais barata: o diff a sinaliza de imediato, e todos conseguem ver o que ainda falta. É o mesmo princípio do limiar de evidência na engenharia reversa de apps: marcar algo como “ainda não utilizável” sempre custa menos do que publicar uma implementação incompleta.

Estrutura básica do script de reconciliação

O núcleo da reconciliação cabe em uma regra: os conjuntos de comandos dos dois lados devem ser derivados das declarações, sem transcrição manual. Ao manter manualmente uma lista de itens “migrados”, você cria uma terceira fonte de verdade que inevitavelmente se distancia do código. Em duas semanas, ela será a primeira coisa a ficar errada.

No lado novo, em Python, a fonte única de verdade são as declarações de argparse no cli.py de cada plataforma. Um módulo catalog percorre os subcomandos, exporta um conjunto {platform/command} e o emite com python -m reverse describe --format json. A explicação de por que uma declaração pode ser a fonte única de verdade — e de como o catálogo é derivado de forma totalmente automática — está no texto sobre interface como código. No lado antigo, em Go, já existe um mapa platform -> command, uma allowlist imutável compilada no binário, então exportar um JSON com o mesmo formato é trivial.

Com os dois arquivos JSON em mãos, o restante são operações de conjuntos:

# Both sides' command sets derive from declarations, not transcription.
# Transcribe by hand and you've added a third source of truth that will drift.
import json
from collections import Counter

def ids(path):
    doc = json.load(open(path))
    return {f"{p['name']}/{c['name']}"
            for p in doc["platforms"] for c in p["commands"]}

old = ids("go-registry.dump.json")      # old registry: immutable platform->command allowlist
new = ids("python-catalog.dump.json")   # python -m reverse describe --format json

missing = old - new     # old side only: each one needs a keep-or-drop verdict
added   = new - old     # new side only: new capability, logged separately
kept    = old & new     # intersection: migrated, but still check for semantic drift

assert missing | kept == old            # every old-side item classified, none dropped

by_platform = Counter(pc.split("/")[0] for pc in missing)  # goes straight into the README table

Isso executa em poucos milissegundos, sem custo, de forma determinística e com 100% de precisão. missing são aqueles 128 comandos; agrupados por plataforma, eles formam a tabela abaixo:

Plataforma

Comandos não migrados

xiaohongshu

33

tiktok

30

hotspot

21

douyin

19

reddit

8

weibo

7

bilibili

5

zhihu

3

linkedin

1

netease_music

1

Total

128

Não há espaço para um modelo nesta etapa.

O custo e os erros de pedir uma comparação linha a linha ao modelo

Pule o script, cole as duas listas no chat e pergunte “quais dos 329 não aparecem nestes 201”. Três problemas surgem sem falta.

O modelo omite itens: quando a lista fica longa, ele não calcula uma diferença de conjuntos elemento por elemento. Ele faz uma amostragem baseada em algo que “parece certo”, os itens do fim se diluem e a resposta vem aparentemente completa, mas faltando uma dúzia. Ele também inventa: aponta como ausentes itens presentes nos dois lados ou conta itens realmente ausentes como migrados, porque está imitando o formato de um relatório de reconciliação, não calculando a diferença. E o resultado não é reproduzível: use a mesma entrada duas vezes e a lista de ausentes muda. Uma “reconciliação” que entrega um resultado diferente a cada execução não é reconciliação.

Também não faz sentido pelo custo. O script leva poucos milissegundos; pedir ao modelo a comparação consome algumas centenas de milhares de tokens, além de múltiplas rodadas de autocorreção. É caro, lento e pouco confiável. Entregar a operação de conjuntos à ferramenta que faz operações de conjuntos é a afirmação menos controversa deste texto.

O papel certo do modelo: explicar a diferença, não decidir se houve migração

O script entrega 128 fatos do tipo “não migrou”, mas um fato não é uma conclusão. Cada caso exige uma decisão de manter ou descartar, e toda decisão precisa de uma justificativa. Esse é o território do modelo.

Ele deve explicar, item por item, por que algo não migrou. Era código morto? O endpoint upstream foi descontinuado? Ficou adiado? Ou, no caso mais difícil, não foi removido: foi incorporado a outro comando. O nome desapareceu, mas a capacidade continua lá. Essa correspondência oculta de “fundido, não removido” não aparece apenas olhando a lista de ausentes; é preciso ler os dois registros simultaneamente para encontrá-la.

Os 201 itens da interseção também não estão automaticamente seguros. Ter migrado não significa que a semântica foi preservada: um comando com o mesmo nome pode ter um padrão alterado discretamente, uma semântica de paginação invertida ou dois códigos de erro condensados em um. Isso é drift semântico, mais traiçoeiro que uma lacuna, porque o diff fica verde e o caso nem entra em missing. Detectar drift exige que o modelo leia as duas implementações e avalie se o comportamento é equivalente; a confirmação final vem com testes diferenciais, pela comparação de fixtures na terceira etapa do fluxo de trabalho em quatro estágios. Essa capacidade de olhar para uma tradução aparentemente bem-sucedida e ainda dizer “o comportamento mudou aqui” é exatamente o tema da seção sobre contraevidência no texto de fingerprinting. Um modelo fraco apenas repete que a migração foi concluída com sucesso.

A divisão, então, é clara: decidir se algo existe é função do script; decidir se deve permanecer e se mudou exige raciocínio. Desta vez, 4 comandos sinalizados em vermelho pelo diff se mostraram necessários na revisão e foram restaurados como novos comandos de primeira classe. O script decide, o modelo explica, a pessoa decide: três camadas, cada uma no seu lugar.

Qual modelo usar em cada etapa

Todos os quatro níveis abaixo atuam na camada de explicação. A camada de decisão, o diff, não usa modelo algum. Essa é a diferença central entre esta abordagem e outros textos sobre “migração com IA”.

Etapa

Capacidade necessária

Escolha

model id

Fornecer os dois registros de uma vez e identificar correspondências de “não removido, mas fundido em outro lugar”

Contexto longo para ler as declarações completas dos dois lados ao mesmo tempo

Kimi K3

kimi-k3

Primeira análise de manter ou descartar os 128 itens ausentes, em rascunho estruturado

Baixo custo e centenas de chamadas com alta concorrência

Claude Sonnet 5

claude-sonnet-5

Avaliar drift semântico: foi migrado, mas o comportamento mudou? Lê as duas implementações

Raciocínio forte e disposição para afirmar “isso mudou”

Claude Opus 5

claude-opus-5

Foi migrado, mas a fixture não confere: explicar a diferença pelos parâmetros ou pelo formato da resposta

Atribuição com raciocínio intermediário

GPT-5.6 Sol

gpt-5.6-sol

O nível que mais vale testar é o terceiro. A avaliação de drift semântico verifica exatamente se o modelo vai contestar uma tradução já considerada bem-sucedida — e é nesse ponto que trocar de modelo mais altera o resultado. O protocolo é o seguinte:

  1. Use uma migração real entre duas linguagens do seu projeto e execute o script para gerar o conjunto missing. Esta etapa não usa modelo algum.

  2. Rotule manualmente de 10 a 15 casos com a verdade de referência — descartar, manter, fundido em outro lugar ou adiado — para formar um controle.

  3. Envie o mesmo prompt de “explique se cada item deve ser mantido ou descartado” a claude-opus-5 e a um nível barato. Observe duas coisas: a justificativa aponta para um fato concreto no código ou entrega algo vago como “possivelmente descontinuado”? E quantas correspondências de itens fundidos em outro lugar cada um encontra?

  4. O número de correspondências ocultas encontradas será sua base para decidir se vale confiar a ele a primeira análise.

O problema não é escolher o modelo, mas trocar entre eles

São quatro modelos de três fornecedores, com três SDKs, três esquemas de autenticação e três formatos de erro. Reescrever o cliente três vezes para alternar entre níveis não compensa. Por isso, muita gente usa um único modelo em todo o processo e, justamente na revisão de drift semântico — onde mais precisa de raciocínio —, recorre a um nível barato que só produz respostas vagas e deixa todo o drift verde passar.

AIReiter simplifica essa camada: uma chave, uma interface compatível com OpenAI, os quatro níveis por trás dela e a troca feita apenas alterando o campo model no corpo da requisição.

# Semantic-drift review / per-item keep-or-drop: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<both implementations + this command migration status, ask if behavior is equivalent>"}]
  }'

# First pass on 128 missing items in bulk: change the model field, leave the rest
#   "model": "claude-sonnet-5"
# Diff attribution when a fixture won't match:
#   "model": "gpt-5.6-sol"

Se você já usa o SDK da OpenAI, aponte base_url para https://aireiter.com/api/v1 e não altere mais nada. No SDK da Anthropic, use POST /api/v1/messages com a mesma chave.

O preço faz sentido nesse fluxo: a primeira análise processa centenas de itens de uma vez e é refeita a cada rodada de migração, cenário em que o claude-sonnet-5, com alta concorrência, é o mais barato. A revisão de drift semântico consiste em uma dúzia de casos difíceis, submetidos repetidamente ao claude-opus-5, o mais caro por item. Ambos são níveis Claude, e o desconto de 30% incide justamente nas partes mais densas e mais caras. O gpt-5.6-sol cuida da atribuição das diferenças, com GPT pela metade do preço.

Conclusão

“Traduzido” é uma ilusão criada ao olhar uma função isolada. “Migrado” é algo resolvido pelo diff. Operações de conjuntos vão para o script, explicações vão para o modelo e decisões vão para humanos. Essa ordem não pode ser invertida — muito menos deixando o modelo decidir.

Há ainda uma etapa que é fácil ignorar: a lista de ausentes precisa entrar no README e continuar visível ao longo do tempo. O 128 permanece lá até virar 0, ou até que cada item tenha uma justificativa escrita: “não será migrado, porque X”. Uma reconciliação que só existe em uma discussão de PR não é reconciliação, porque a próxima pessoa a assumir o trabalho não conseguirá vê-la e repetirá o erro nos mesmos 128 itens. Esse é o ponto em comum entre este texto, o texto sobre interface como código e o texto sobre por que não construir um Model de resposta unificado: deixe a fonte única de verdade falar por si e não espalhe conclusões pela memória das pessoas.