Pensando sobre o que venho experimentando no dia a dia, trabalho num produto em produção cujo código é gigante e está mal arquitetado. Não temos uma especificação técnica de qualidade que cubra o que já existe, e muito menos as novas funcionalidades. Não temos um code style, aquela coisa que normalmente serve para organizar como os humanos vão ler o código uns dos outros. Não temos uma suíte de testes integrados que cubra todos os casos, inclusive os que falham e geram bug funcional em produção. É desse chão que eu parto, porque acho que aqui a experiência concreta importa mais do que a teoria.

Usar IA para implementar uma spec completa me levou a entender uma coisa que eu não esperava: manter um humano no loop torna o trabalho puramente lento, principalmente por causa do review. E o motivo fica claro quando você olha de perto. Para aprovar o que o modelo gerou, o humano precisa entender o código, entender o style, e se sentir confortável com as decisões de arquitetura e de implementação que o modelo tomou. Sendo assim, ele acaba perdendo tempo criticando o trabalho do modelo de acordo com o próprio critério, e esse critério quase sempre é subjetivo e nunca foi especificado, porque dá para ver que falta especificação até sobre como a arquitetura deveria funcionar. Ou seja, a gente trava medindo a saída do modelo contra uma régua que mora só na cabeça de uma pessoa. Não é a geração que é cara, é essa avaliação contra um padrão fantasma.

A recomendação

A recomendação, então, seria abordar as mudanças com testes bem especificados, parar de fazer code review com o olhar de que um humano vai manter o código depois, e produzir documentação de qualidade sobre o estado atual da arquitetura, sobre os requisitos funcionais e não funcionais, além do estilo de código. Acho que investir tempo exclusivamente nesse aspecto pode acelerar muito a adoção e o desenvolvimento com modelo.

Mas preciso ser honesto comigo mesmo, senão isso vira entusiasmo vazio. A frase “não revisar como se um humano fosse manter o código” é a parte mais arriscada do que eu estou dizendo, e eu não quero que ela seja lida como “ninguém mais precisa entender nada”. A forma sóbria disso é outra: a revisão não desaparece, ela sobe de camada. Em vez de revisar cada linha gerada, você passa a revisar a spec e os testes, que são a verdadeira fonte da verdade, e o código vira artefato de build. É o mesmo movimento que a gente já fez uma vez, quando parou de ler assembly e passou a confiar no compilador. A confiança migrou do resultado para a ferramenta e para o fonte de nível mais alto. O que eu estou propondo é repetir esse salto mais uma camada acima. Alguém ainda vai depurar produção de madrugada, alguém ainda responde pelo sistema, só que o esforço humano para de ser diluído linha a linha e se concentra onde o retorno é maior.

A objeção que o time vai levantar

Sei qual vai ser a resistência mais visceral do time, porque ela é legítima como sensação: não dá para revisar todo o código que o modelo gera. E a minha resposta para isso é uma pergunta. Você já faz isso hoje com as suas dependências?

Eu gosto de usar o exemplo do Rust. Um projeto que usa a Tokio carrega dezenas de milhares de linhas de um runtime assíncrono rodando no coração do sistema, linhas que ninguém no time leu e provavelmente nunca vai ler, e mesmo assim a gente confia, sobe para produção e dorme à noite. Sobe um nível, para a infraestrutura. Você revisou o código do Kafka antes de publicar o primeiro evento nele? Leu o fonte do RabbitMQ, do Postgres, do TimescaleDB antes de confiar os seus dados a eles? Claro que não. Você confia porque aquilo funciona com base nas especificações funcionais e não funcionais que você tem para cada um, ou seja, comportamento documentado, garantias, um contrato, mais um histórico longo e os seus próprios testes na fronteira.

Então o critério nunca foi “eu li cada linha”, sempre foi “eu tenho um contrato em que confio e uma forma de verificar o comportamento na borda”. Ler linha por linha era só um substituto que a gente usava para o próprio código, por não ter nada melhor à mão. O código que o modelo gera é apenas mais código na mesma categoria, código que você não escreveu. Logo, a reação honesta não é exigir o impossível, é dar a esse código o mesmo que você dá ao Postgres, um contrato claro, que é a spec, e verificação na fronteira, que são os testes. Vale uma ressalva para eu não ser preguiçoso com a analogia: Tokio e Postgres apanharam de milhões de usuários durante anos, e o módulo que o modelo acabou de gerar tem histórico zero. Então a confiança não pode vir da reputação ainda, ela tem que vir inteira do contrato e da verificação, o que levanta a régua dos testes e da spec em vez de abaixar. Que é, convenientemente, exatamente o que eu venho defendendo.

A linguagem vira detalhe

Tem uma consequência disso tudo que eu acho a parte mais interessante. Se a especificação é a fonte da verdade e o código é artefato de build, então a linguagem de programação deixa de ser uma decisão gravada em pedra e vira um alvo de renderização. Com uma spec completa e precisa o suficiente, você poderia ter o código em qualquer linguagem. É isso que eu chamo de transpile with steroids. A ideia não é pegar um código em Rust e torcer para o modelo adivinhar o C equivalente, é ter uma descrição de comportamento tão precisa que o código, em qualquer linguagem, seja derivado dela. Da mesma forma, você pode gerar código a partir de uma linguagem formal como F*.

Só que aqui eu preciso separar duas coisas que são fáceis de confundir, porque elas dão garantias bem diferentes. A garantia que você recebe depende de onde a sua spec está num espectro. Numa ponta tem prosa que o modelo interpreta, que é rápido, flexível, garantia zero, e a saída é plausível e precisa de testes para ser confiável. Na outra ponta tem uma linguagem formal como F* ou TLA*, ou um sistema de tipos rico o bastante, onde a renderização pode vir com prova junto. A maioria de nós vive no meio, e saber onde você está nesse espectro é saber o quanto pode confiar na saída sem reler cada linha. O sonho de gerar o sistema em qualquer linguagem é real, a letra miúda é que a fidelidade é função da formalidade.

Alimentar o modelo é o mesmo que fazer onboarding

Acho que a melhor forma de enxergar o modelo é como um colaborador novo. Competente, rápido, incansável, e sem absolutamente nenhum contexto implícito sobre o nosso domínio. E aí fica óbvio: o que você entrega para uma pessoa nova no primeiro dia é exatamente o que falta para o modelo, ou seja, arquitetura documentada, requisitos funcionais e não funcionais escritos, guia de estilo, suíte de testes servindo de contrato. Quando você não tem isso, cada interação com o modelo repete o custo do onboarding, de novo e de novo. Por isso eu digo que, sem especificação, a gente continua queimando tempo em vez de queimar tokens.

E tem uma versão mais grave dessa mesma doença, que é quando a empresa escreve sistemas como extensão de pessoas. Mesmo tendo o sistema, ela continua dependendo de indivíduos específicos, com conhecimento específico, para que aquilo siga funcionando. Isso para mim é um erro grotesco, porque a razão de ter um sistema é justamente a oposta, é automatizar e liberar o humano para fazer todas as outras coisas. Um sistema que acorrenta à própria operação a pessoa que o entende fez o contrário do que prometeu, é um processo manual disfarçado de software. E repare que o conhecimento que mantém esse sistema vivo mora em cabeças, não em texto, que é o mesmo conhecimento que falta na spec, que o novo contratado não tem e que o modelo não consegue ler. É o mesmo furo visto de três ângulos, e escrever esse conhecimento resolve os três de uma vez.

Arquitetura é uma escolha, e hoje ela ficou barata

Outra coisa que eu acho que vale repensar agora é arquitetura. Ela sempre foi uma escolha, só que uma escolha cara de fazer bem, e por ser cara acabava sendo adiada. Pega um sistema financeiro, que é o exemplo que eu mais gosto. Modelar com orientação a eventos não é modismo, é fidelidade ao mundo real, porque o mundo acontece em eventos ao longo de uma linha do tempo, e dinheiro é literalmente uma sequência de lançamentos, não um número. Se você guarda só o último estado, tipo o saldo final da conta corrente, você jogou fora a história. Se você guarda o fluxo de eventos, o estado atual vira uma projeção que você recalcula quando quiser, e de brinde você ganha um sistema capaz de viajar no tempo e na informação, ou seja, reconstruir qualquer estado passado, auditar, reprocessar, responder perguntas que você nem tinha feito quando escreveu o código. É um sistema rico em vez de um sistema amnésico.

E esse tipo de decisão, escolher entre event sourcing, CQRS, orientação a eventos, modelo de atores ou um bom e velho monólito, é exatamente o que vale fazer de forma deliberada, no começo, usando o modelo. Inclusive porque ele tem um arcabouço gigantesco de padrões para escolher e coloca as alternativas e os tradeoffs na mesa em minutos. Isso ataca uma das mortes mais comuns de software, que é o sistema que nasce com boa intenção e termina como um grande monólito que queria ser alguns microserviços. Isso acontece por vários motivos, e um deles é confundir o que era para ser um MVP ou uma prova de conceito com um produto pronto, aí o andaime que servia só para testar uma ideia vira estrutural e ninguém volta para trocar. Quando a gente usa modelo, a fase de MVP e prova de conceito fica muito mais robusta, dá para renderizar um protótipo descartável porém bem estruturado, ou até explorar duas arquiteturas de verdade antes de se comprometer. A desculpa clássica, “é só um MVP, depois a gente faz direito”, perde a força quando fazer direito custa tokens em vez de meses. O único cuidado, e eu preciso dizer isso, é que mais opção também é mais corda para se enforcar, porque o modelo vai felizmente construir uma catedral de event sourcing para um CRUD que não precisava disso. O arcabouço só vale quando vem com julgamento sobre o que o domínio realmente pede.

SOLID, Clean Code e hexagonal continuam valendo, por um motivo novo

Vale também falar de SOLID, Clean Code e arquitetura hexagonal, porque eu acho que essas coisas são boas e a gente às vezes esquece por quê. Elas existem porque tornavam a comunicação de código dentro do time muito mais dinâmica e assertiva, traziam robustez à tecnologia e evitavam o débito técnico e o desgaste natural dos sistemas. No fundo, eram em boa parte uma disciplina para a cognição humana, porque a gente só consegue segurar um tanto de coisa na cabeça, então desacopla, nomeia bem e isola responsabilidades para conseguir raciocinar sobre um pedaço sem carregar o sistema inteiro junto.

O modelo segue esses princípios com naturalidade e a baixo custo, porque são alguns dos padrões mais bem representados no que ele aprendeu, e porque dá para expressá-los como restrições que ele honra quando você pede. E aqui está a parte que eu acho não óbvia: o motivo de esses princípios serem bons muda, e eles ficam mais valiosos, não menos. O baixo acoplamento e as fronteiras claras eram a forma de o humano raciocinar localmente, e agora são também as costuras por onde você reparte o trabalho para o modelo e verifica cada pedaço isoladamente. A arquitetura hexagonal, com suas portas e adaptadores, te entrega justamente as unidades delimitadas e testáveis que a geração automática adora. Ou seja, o enquadramento sai de legibilidade, que era para o humano ler, e vai para verificabilidade, que é para você gerar e conferir em pedaços. Mesmos princípios, motivo atualizado.

Ainda preciso ser honesto em dois pontos

Antes de fechar, dois pontos que eu não quero varrer para baixo do tapete. O primeiro é que existe uma circularidade incômoda: escrever uma spec boa de um sistema grande e bagunçado exige o mesmo entendimento profundo que o review me dava, então o gargalo não some, ele se desloca para frente. A diferença é que você paga uma vez e colhe sempre, em vez de pagar a cada pull request, por isso para mim vale a pena, mas é investimento, não atalho. O segundo é que teste só cobre o que você imaginou testar, e o review humano pegava o que ninguém especificou, principalmente nos requisitos não funcionais como desempenho, segurança e comportamento sob falha parcial, que são muito mais difíceis de capturar em teste do que os funcionais. Juntar os dois no mesmo balde seria me enganar.

O teste do Joel aos 26 anos

Por fim, acho que vale uma revisitada ao Teste do Joel, que agora já é um adulto de 26 anos. Em 2000, o Joel Spolsky publicou doze perguntas binárias para medir em três minutos se um time de software era sério, e ele continua quase todo válido, só que foi escrito para um mundo em que o leitor do código era sempre humano. Dá para manter a mesma mecânica e medir outra coisa, que é se a sua organização está pronta para desenvolver com modelo:

  1. A arquitetura atual está escrita em texto que um modelo consegue consumir por inteiro?
  2. Existe uma suíte de testes executável que cobre os caminhos de falha, e não só os de sucesso?
  3. Os requisitos funcionais e os não funcionais estão escritos em algum lugar?
  4. O estilo de código é imposto por ferramenta, tipo formatter, linter e tipos, em vez de gosto pessoal?
  5. Um agente consegue rodar build e testes com um único comando, de forma isolada?
  6. Existe uma forma de entregar ao modelo a fatia certa do código sem afogá-lo em contexto?
  7. Você consegue avaliar a saída de forma objetiva, testes passam, tipos fecham, propriedades provadas, antes de olhar com critério subjetivo?
  8. A especificação é tratada como fonte da verdade e o código como artefato?
  9. Existe registro das decisões de arquitetura, com o porquê e não só o quê?
  10. A revisão humana acontece sobre a spec e os testes, e não sobre cada linha gerada?
  11. Um colaborador novo, humano ou modelo, chega ao primeiro commit útil só com o que está escrito?
  12. Você mede sucesso por tokens gastos até o verde, e não por horas de review?

Assim como no teste original, eu aposto que a maioria dos times vai pontuar dois ou três, e para mim esse é exatamente o ponto. A diferença bonita é que no teste do Joel as perguntas eram sobre disciplina para humanos colaborarem entre si, e aqui elas são sobre tornar o nosso domínio legível para uma inteligência que não compartilha o nosso contexto implícito.

O que fica

Acho que estamos num momento de profunda reflexão, e vale mergulhar no paradigma de como a gente aborda o modelo e como traz ele para dentro do domínio. Ele deveria ser alimentado com o máximo de especificação possível para estar alinhado com a forma que a gente espera de um sistema, e isso é a mesma abordagem de como deveríamos fazer o onboarding de um colaborador novo. É o mesmo problema de sempre, especificação, só que agora com uma consequência econômica direta e medível. Antes, a falta de spec era uma dívida silenciosa que aparecia meses depois num bug. Agora ela aparece na hora, em cada ciclo com o modelo, como tempo humano gasto compensando um documento que nunca foi escrito. A boa notícia é que esse esforço não é gambiarra para agradar a máquina, é a mesma coisa que torna o sistema compreensível para qualquer um que chegue depois. A diferença é que agora eu tenho um motivo barato e imediato para finalmente fazer o que já devia estar feito. Sem isso, a gente continua queimando tempo em vez de tokens.

Referência

Joel Spolsky, “The Joel Test: 12 Steps to Better Code”, 2000. Disponível em https://www.joelonsoftware.com/2000/08/09/the-joel-test-12-steps-to-better-code/.