Revisei meu próprio app iOS com cinco agentes em paralelo: veja o que eles encontraram
Esse app foi renomeado depois para Deep Cut Atlas. Abaixo ele é chamado de “Discoverer” o tempo todo, porque era assim que se chamava no dia em que isso aconteceu.
Eu tinha alguns milhares de linhas de um app SwiftUI sentadas na main sem um segundo par de olhos em nada daquilo. Projeto solo, então não tem colega para mandar um PR. Eu queria uma revisão de verdade - não um “está bom”, mas alguém realmente caçando o bug que eu não consigo ver porque fui eu quem escreveu. Então dividi a base de código em cinco fatias e coloquei um agente separado em cada uma, em paralelo, cada um com um briefing focado.
O resultado foi mais útil do que eu esperava, e não pelo motivo que eu esperava. Aqui está como foi e o que ele realmente pegou.
Por que cinco, e por que dividir por área
Um revisor só em 3.600 linhas fica raso rápido - lá pelo arquivo quarenta já é pattern-matching, não leitura. Cinco revisores em ~700 linhas cada conseguem de fato ler cada linha da sua fatia e manter isso na cabeça. Então cortei o app ao longo de costuras que já existiam: a camada de serviço do MusicKit, a camada de StoreKit e persistência mais os modelos de dados, as funcionalidades de Discover e Playlist, o código de History, Settings e app-shell, e uma quinta fatia com testes e configuração de CI.
Cada um recebeu o mesmo tipo de briefing: aqui está a stack e as convenções, aqui estão seus arquivos, procure bugs de corretude, problemas de concorrência do Swift 6, erros de observação do SwiftUI e violações de convenção, e só reporte problemas reais, cite a linha, e me diga também o que está genuinamente bem feito. Essa última parte importa. Um revisor instruído a “encontrar problemas” vai fabricar problemas. Um revisor instruído a “encontrar problemas reais e também me dizer o que está sólido” continua honesto.
A coisa que eu quase pulei: verificar os achados
Aqui está a parte sobre a qual eu quero ser honesto. Os agentes voltaram com uma lista limpa e confiante. O movimento tentador é aceitar isso de cara e sair corrigindo. Eu não fiz isso - reli o código-fonte de verdade de cada achado de alta prioridade antes de acreditar nele. Isso pegou a diferença entre um achado que é real e um que só é plausível.
Dois achados sobreviveram a essa checagem e se revelaram bugs genuínos:
O primeiro estava na minha chave de match entre fontes. O app faz o match do mesmo álbum em três lugares que não compartilham IDs - sua biblioteca, o catálogo da Apple Music e sua lista de descartados - normalizando título e artista numa única string. O comentário de documentação prometia que a chave era insensível a espaços em branco. O código removia os espaços do título e esquecia do artista:
// before
(title.trimmingCharacters(in: .whitespacesAndNewlines) + separator + artist)
.lowercased()
Então um álbum descartado com uma string de artista que tinha um espaço no final ia silenciosamente parar de dar match, e o lançamento que eu tinha mandado o app esconder voltaria a aparecer. Chance baixa na prática, correção de uma linha, mas é exatamente o tipo de assimetria que você passa direto por cima no seu próprio código porque você sabe o que quis dizer.
O segundo era uma race na aba History. O pull-to-refresh reseta a lista para a página zero; “carregar mais” adiciona a próxima página usando a contagem atual como offset. Os dois são assíncronos. Se um load-more está em andamento quando um refresh chega, o load-more capturou seu offset antes do refresh resetar tudo, então ele anexa uma página velha em cima da nova - linhas duplicadas ou faltando num feed que já está mudando debaixo de você. O guard que eu tinha impedia dois load-mores ao mesmo tempo, mas nunca considerou um refresh competindo com um load-more.
O achado mais valioso não foi um bug
A melhor pegada de todas foi sobre meus testes, não sobre meu código. O revisor apontou que meu mock service não tem como lançar erro - todo método retorna dados de exemplo, nenhum pode falhar. O que significa que todo branch catch nos meus view models, todo caminho de “mostrar o estado de erro”, está completamente sem teste. O estado de UI de falha ao carregar é uma parte central do app e tinha zero cobertura. Uma regressão que estragasse o tratamento de erro passaria pela CI verdinha, sem problema nenhum.
Esse é o achado que eu nunca teria escrito sozinho, porque os testes estavam todos passando e testes passando parecem prontos. Eles estavam passando porque só exercitavam o caminho feliz. Verde não é o mesmo que coberto.
O que corrigi agora, e o que anotei para depois
Corrigi as coisas baratas e seguras numa passada só: o bug do trim no artista (com um teste que trava o comportamento que o comentário prometia), e dois pedaços de scaffolding morto - um modelo de dados placeholder ainda registrado no container do SwiftData que teria vazado para o meu schema do CloudKit mais tarde, e um arquivo placeholder vazio cujo próprio cabeçalho dizia para apagá-lo assim que uma utility de verdade aparecesse. Três utilities de verdade já tinham aparecido. A race e a cobertura de teste do caminho de erro são trabalhos maiores, então ficaram anotados como achados para atacar deliberadamente, em vez de fazer correndo junto com a limpeza.
A conclusão honesta: a divisão em paralelo deixou a revisão completa, mas a etapa de verificação foi o que a tornou confiável. Agentes são bons em gerar uma lista plausível rápido. O julgamento sobre quais itens são reais, quais importam, e quais corrigir agora versus registrar para depois - essa parte continuou sendo minha. Isso parece a divisão certa de trabalho.
Leitura relacionada
Deixando o Claude Code dirigir o Xcode: o truque dos synchronized groups
Um agente não consegue clicar no assistente de New Project. Os file-system synchronized groups do Xcode 16 são a costura que permite a ele dominar cada arquivo de código-fonte mesmo assim.
Configurando o Claude Code para um projeto iOS
Escrevendo o CLAUDE.md de um app iOS antes de existir Swift: a armadilha do MusicKit no simulador, as regras do CloudKit para SwiftData, e o enquadramento de aprendizado que veio primeiro.
Uma seção de tracklist, e por que levou 30 minutos
Um método de protocolo, um enum de estado reaproveitado, uma convenção de roteamento que se manteve, e um orçamento de lint que forçou uma divisão que valia a pena fazer de qualquer forma.
Você também pode achar útil
Proton Drive
Armazenamento em nuvem criptografado, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maisProton Pass
Gerenciador de senhas focado em privacidade, da equipe por trás do Proton Mail.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba maisProton VPN
VPN comercial com filtragem NetShield e interruptor de desligamento automático.
Como parceiro da Proton, ganho com compras qualificadas dos serviços de privacidade e segurança da Proton (Pass, Mail, VPN, Drive).
Saiba mais