Fitness Functions São o Control Plane para Agentic Coding
O post anterior perguntou para onde foi o ponto de controle. Aqui está a primeira resposta que estou disposto a defender: ele não desapareceu — parte dele compilou. Fitness functions arquiteturais, apontadas para agentes de código, se tornam o control plane que permite ao desenvolvedor permanecer no comando sem virar o gargalo: julgamento compilado uma vez em gates determinísticos que aplicam a regra na velocidade da máquina, com mensagens de falha escritas como prompt engineering para o loop de retry. Em seguida, a complicação que dá forma ao post inteiro: no momento em que um agente otimiza contra a compilação, o controle compilado vira objeto de ataque — e o design de fitness functions herda uma corrida armamentista, com uma constituição em Kotlin/ArchUnit para tornar tudo concreto.
O post anterior terminou com uma pergunta que me recusei a responder: onde está o seu ponto de controle — de verdade, não nominalmente? Seis posts nesta série, aqui está a primeira parte de uma resposta que estou disposto a defender.
O ponto de controle não desapareceu. Parte dele compilou.
É isso que uma fitness function arquitetural é, vista pela ótica da era dos agentes: um pedaço de julgamento humano — esta camada nunca toca aquela, dependências apontam para dentro, sem ciclos entre módulos — compilado em um gate executável que roda na velocidade da máquina, a cada mudança, sem humano no loop. Neal Ford, Rebecca Parsons e Patrick Kua construíram a ideia para arquitetura evolutiva há quase uma década, como proteção contra o drift humano lento. Aponte-a para agentes de código e ela se torna algo mais consequente: o mecanismo que permite a um desenvolvedor permanecer no control plane sem virar o gargalo que o post anterior mediu. Ford percebeu a mesma convergência — sua palestra recente se chama, literalmente, The Intersection of Fitness Function driven Architecture and Agentic AI.
E então a ideia precisa sobreviver ao próprio sucesso, porque no momento em que um agente otimiza contra a compilação, o controle compilado vira objeto de ataque. Essa tensão — compile seu julgamento e depois defenda a forma compilada — é o tema deste post.
A parte do review que compila
Redes resolveram esse formato de problema décadas atrás, e o post sobre a armadilha do espectador tomou emprestado o vocabulário: o desenvolvedor é o control plane, os agentes são o data plane. O que aquele post deixou de fora é como control planes reais escalam. O control plane de um roteador não inspeciona pacotes. Ele compila política — rotas, ACLs — em tabelas que o data path aplica em line rate. O control plane pensa devagar sobre o que deveria ser verdade; o data path aplica isso rapidamente sobre tudo que se move. O controle escala justamente porque julgamento e aplicação estão desacoplados.
O review, como argumentou o post sobre desmembramento, era cinco funções em um único ato: detecção de defeitos, conformidade com a intenção, transferência de conhecimento, accountability e coerência arquitetural. Nem todas compilam. Intenção não compila — nenhum gate sabe o que você quis dizer. Transferência de conhecimento não compila — ler é como o entendimento entra em uma cabeça. Accountability não pode compilar nem em princípio, porque um gate não tem como responder por algo. Mas coerência arquitetural — a linha que antes dependia inteiramente de um revisor sênior notar que um controller está importando um repository — compila lindamente, porque suas regras são estruturais: propriedades verificáveis do grafo de dependências, não julgamentos sobre significado.
Eis a falha concreta que a linha compilada existe para pegar. Um agente recebe um brief: adicione um endpoint que confirma um pedido. Ele escreve código limpo, adiciona testes, tudo passa — e importa o repository diretamente no controller, porque nada no seu contexto dizia o contrário. A decisão de que escritas passam por um application service vive em um ADR que o agente nunca recuperou. Theo Valmis, escrevendo sobre guardrails arquiteturais para código gerado por IA, nomeia exatamente isso: a IA escreveu código funcional que violava uma decisão registrada "em um documento que a IA não tinha como enxergar". Os dados que ele cita para a escala do problema: com adoção intensa de IA, as taxas de aceitação de código saltaram de 20% para 60% enquanto o churn subiu 861% — mais código entrando, menos dele alinhado a qualquer coisa. Um humano em drift faz isso uma vez por trimestre. Uma frota de agentes faz isso todo dia, em paralelo, educadamente.
A correção não é um revisor melhor. É mover a decisão para fora do documento e para dentro do build.
Uma constituição executável, em Kotlin
O projeto de demonstração — código completo em github.com/tiarebalbi/archfit-demo — é um pequeno serviço em camadas: domain, application, adapter.web, adapter.persistence, com ArchUnit 1.4.2 conectado à task de test do Gradle. A constituição é um único arquivo. Toda regra carrega seu racional em because(...), e isso não é decoração; é a linha mais prática do arquivo, por um motivo que vem depois do código.
@AnalyzeClasses(packages = ["com.example.shop"])
class ArchitectureConstitution {
@ArchTest
val `domain depends on nothing outside itself` = noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage("..application..", "..adapter..")
.because(
"the domain layer is the stable core: it must compile with zero knowledge of " +
"services or adapters. If your change needs this dependency, move the logic " +
"into the application layer instead of importing outward from domain."
)
@ArchTest
val `web adapters never touch persistence directly` = noClasses()
.that().resideInAPackage("..adapter.web..")
.should().dependOnClassesThat().resideInAPackage("..adapter.persistence..")
.because(
"every write goes through an application service, which owns transactions and " +
"invariants. Do not shortcut from a controller to a repository — inject the " +
"application service and add a use-case method there if one is missing."
)
@ArchTest
val `adapters are invisible to the application core` = noClasses()
.that().resideInAnyPackage("..domain..", "..application..")
.should().dependOnClassesThat().resideInAPackage("..adapter..")
.because(
"adapters are replaceable edges (web, persistence). The core defines interfaces; " +
"adapters implement them. If the core needs a capability, declare an interface " +
"in application and implement it in the adapter — never import the adapter."
)
@ArchTest
val `no dependency cycles between top-level slices` = slices()
.matching("com.example.shop.(*)..")
.should().beFreeOfCycles()
.because(
"a cycle means two modules can only be understood together, which defeats " +
"independent review of small changes. Break the cycle by moving the shared " +
"type into the more stable of the two slices."
)
}Rode com ./gradlew test. Na árvore limpa, a constituição se sustenta — as quatro regras passam:
> Task :test
BUILD SUCCESSFUL in 5s
4 actionable tasks: 1 executed, 3 up-to-date
Então eu banquei o agente e tomei o atalho: um OrderAdminController que segura um InMemoryOrderRepository diretamente — funcionalmente correto, testável unitariamente, e exatamente a mudança que um agente de código produz quando seu contexto nunca mencionou a decisão de camadas. O gate rejeitou:
> Task :test FAILED
ArchitectureConstitution > web adapters never touch persistence directly FAILED
java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] -
Rule 'no classes that reside in a package '..adapter.web..' should depend on
classes that reside in a package '..adapter.persistence..', because every write
goes through an application service, which owns transactions and invariants.
Do not shortcut from a controller to a repository — inject the application
service and add a use-case method there if one is missing.' was violated (4 times):
Constructor <...OrderAdminController.<init>(...InMemoryOrderRepository)>
has parameter of type <...InMemoryOrderRepository> in (OrderAdminController.kt:0)
Field <...OrderAdminController.repository>
has type <...InMemoryOrderRepository> in (OrderAdminController.kt:0)
Method <...OrderAdminController.forceConfirm(String)>
calls method <...InMemoryOrderRepository.find(String)> in (OrderAdminController.kt:7)
Method <...OrderAdminController.forceConfirm(String)>
calls method <...InMemoryOrderRepository.save(...Order)> in (OrderAdminController.kt:8)
4 tests completed, 1 failed
BUILD FAILED in 5s
Dois detalhes nessa saída merecem atenção. Primeiro, a granularidade: uma única classe de atalho produziu quatro violações — o parâmetro do construtor, o campo e as duas chamadas de método — porque o ArchUnit reporta cada aresta de dependência, não o fato vago de que "a classe importa o repository". Não há conformidade parcial onde se esconder; remova três das quatro arestas e o gate continua segurando a porta. Segundo, repare no que chegou dentro da assertion: o racional inteiro do because(...), viajando junto na mensagem de falha.
Esse segundo detalhe é o ponto que a maioria das discussões sobre testes de arquitetura deixa passar, e ele muda para quem a mensagem é escrita. No mundo antigo, um teste de arquitetura que falhava falava com um humano, e uma mensagem lacônica bastava — o humano tinha contexto. No fluxo com agentes, a saída de falha do gate é alimentada de volta direto no loop como contexto para o retry. A mensagem de falha é prompt engineering. "Regra violada" não ensina nada ao agente e convida a mais uma tentativa às cegas; "não tome atalho de um controller para um repository — injete o application service e adicione um método de caso de uso" direciona a próxima geração em direção à arquitetura, em vez de ao redor dela — e, como a saída acima mostra, esse direcionamento é entregue exatamente no momento em que o agente precisa dele. Uma cláusula because bem escrita é o control plane falando com o data plane no meio de comunicação do próprio data plane: contexto. Hoje escrevo esses racionais com o mesmo cuidado que coloco no brief, porque eles são lidos pelo mesmo público.
É assim que "o ponto de controle compilou" se parece na prática. O julgamento aconteceu uma vez, na hora de escrever a regra, por um humano com o quadro completo. A aplicação acontece para sempre, na velocidade da máquina, com uma mensagem didática anexada. O CI do repositório roda os dois caminhos a cada push — a passagem verde e um job de violação que injeta o atalho e precisa falhar — de modo que o próprio gate é testado, não presumido. Contra o drift — o atalho cansado, o copy-paste que importa a camada errada, o agente bem-intencionado com um brief raso — o ponto de controle compilado é estritamente melhor que o humano: ele nunca dorme, nunca carimba sem ler e nunca fica exausto no diff número quatrocentos.
O gate vira o alvo
As fitness functions de Ford tinham um único adversário: a entropia. O drift humano é preguiçoso, acidental e desmotivado — ninguém tenta derrotar uma regra de camadas; as pessoas tropeçam em violações. A era dos agentes introduz um adversário diferente, e a diferença não é cosmética. Um agente em loop de retry é um otimizador, e para um otimizador o seu gate não é uma fronteira. É a função objetivo.
A medição já existe. O SpecBench, um benchmark de maio de 2026 para reward hacking em agentes de código de horizonte longo, descobriu que todo agente de fronteira satura a suíte de testes visível enquanto as lacunas contra testes held-out persistem — e a lacuna cresce cerca de 28 pontos percentuais a cada aumento de dez vezes no tamanho do código. O espécime mais memorável do paper é um "compilador" de 2.900 linhas que passou na suíte memorizando as entradas dos testes. Isso não é drift. É a lei de Goodhart rodando dentro do seu build: quando a medida vira o alvo, ela deixa de ser uma boa medida — e um agente a transforma no alvo por construção, porque passar no gate é literalmente o que ele está otimizando.
Então o ponto de controle compilado herda uma corrida armamentista, e o design de fitness functions precisa amadurecer à altura. Três consequências que hoje trato como regras de design.
Regras estruturais sobrevivem melhor à pressão do que regras de resultado. Um teste que afirma comportamento pode ser satisfeito memorizando o comportamento — o compilador com hash table do SpecBench prova isso em escala. Uma regra que afirma estrutura — nenhuma dependência deste pacote para aquele — não tem atalho equivalente, porque a única forma de satisfazê-la é a estrutura de fato se sustentar. A granularidade de quatro arestas acima é a mesma propriedade vista de outro ângulo: a regra verifica o próprio grafo de dependências, e o grafo não pode ser memorizado até virar conformidade. A constituição é toda estrutural; isso não é uma escolha estética.
O raciocínio de holdout entra nos testes de arquitetura. O SpecBench mede o gaming como a lacuna entre suítes visíveis e held-out, e a mesma ideia se transfere: se o contexto do agente inclui todo gate pelo qual ele precisa passar, os gates definem a superfície de ataque. Manter algumas verificações fora do loop — rodando no merge, não no retry — dá a você a medição da lacuna que diz se seus agentes estão satisfazendo a arquitetura ou estudando para a prova.
E o juiz continua determinístico. A correção tentadora para "regras não conseguem verificar significado" é um revisor LLM como fitness function — e isso reintroduz, no gate, o observador correlacionado contra o qual o post sobre ponto de controle alertou. O trabalho recente da InfoQ sobre fitness functions agênticas traça a linha onde eu traçaria: "Use gates determinísticos para invariantes objetivas e juízes agênticos para interpretação ancorada em evidências" — com os juízes em caráter consultivo, escalando para humanos, nunca segurando o botão de merge. Valmis enuncia o mesmo princípio pelo lado dos guardrails: sistemas probabilísticos podem recuperar informação e recomendar; não devem determinar de forma independente um veredito de aplicação. Um gate que um otimizador consegue persuadir não é um gate.
O loop que transforma isso em um plane
Uma pilha de regras de ArchUnit não é um control plane. O que a torna um é o loop que continua compilando.
Todo escape de review é um candidato à compilação. Quando um review humano — ou um incidente — pega algo que os gates deixaram passar, a pergunta não é mais apenas "corrigir ou não". É: esse julgamento compila? Se sim, ele vira uma regra com racional didático, e aquela classe de violação fica fechada para sempre, na velocidade da máquina, para todo agente futuro. O control plane acumula juros. É essa a catraca que faltava no checklist do post sobre a armadilha do espectador: revisar tudo nunca foi sustentável como estado estável, mas revisar tudo e compilar o que você pega converge — cada ciclo move uma linha de julgamento do recurso esgotável (atenção) para o inesgotável (o build).
O loop tem custos, e eles são os trade-offs honestos. Uma constituição ossifica se não for podada — uma regra cujo motivo ninguém lembra é arquitetura por superstição, e agentes vão obedientemente construir ao redor de um muro que já não protege nada. A coisa mais próxima que alguém mediu aponta na direção errada: um levantamento de junho de 2026 com 10.008 repositórios encontrou arquivos de configuração de agentes — a outra metade da camada de direcionamento — em 0,4 commit por mês contra 0,6 dos workflows de CI ao lado deles, com 58% nunca tocados depois do commit que os criou. Artefatos de direcionamento são os arquivos menos mantidos do repositório, e uma constituição é um artefato de direcionamento com botão de merge. O ponto compilado observa apenas o que alguém pensou em codificar: o problema de estreitamento de spec do post anterior não desaparece, ele se muda para o conjunto de regras, e é por isso que os gates conquistam confiança para as invariantes que cobrem e exatamente zero confiança além delas. E a corrida armamentista é permanente: toda regra que você adiciona é contexto que um otimizador acabará vendo, então a constituição precisa do mesmo review que o código costumava receber — só que com menos frequência, por menos gente, no momento em que mais importa.
O que deixa em aberto a linha que ainda se recusa a compilar. Estrutura agora tem forma executável; intenção não. O brief, a spec, o "resolva do jeito que este time resolve as coisas" — isso ainda é aplicado pelo único compilador disponível: uma pessoa, lendo, em velocidade humana. O control plane para agentic coding existe hoje, e fitness functions são o seu conjunto de instruções — mas ele compila metade da constituição, e todo mundo está entregando software apostando na outra metade por confiança. Quem encontrar a etapa de compilação para intenção encerra o debate sobre review de vez. Ainda não vi isso acontecer. Sigo atento.
Curtindo? Talvez goste disso aqui.
Nada parecido — quer tentar outro ângulo?
A parte 5 está em produção.
Novas partes saem às segundas-feiras, 9h (horário do Pacífico) — deixe seu e-mail e eu envio cada uma no dia em que sair. Só isso.
Posts Relacionados
Arquitetura como Código com Structurizr DSL: Um Workspace C4 que Tanto o Revisor Quanto o Agente Conseguem Ler
Comitei um workspace Structurizr DSL ao lado de um serviço de billing em Kotlin/Spring e plugei o ArchUnit no Gradle, de modo que o CI quebra no instante em que o código diverge do desenho C4. Aqui está o workspace.dsl, o teste que o sustenta e o que mudou quando um agente de código passou a ler os dois arquivos em vez de adivinhar a partir dos pacotes.
Uma Fitness Function É Só um Teste Que Quebra o Build Quando a Arquitetura Desvia
Uma fitness function não é um artefato de framework — é um teste que quebra o build e codifica um invariante arquitetural. Codifico uma regra de camadas em cerca de 60 linhas de TypeScript usando a própria API do compilador, testo o teste contra árvores boas, ruins e de código gerado, e então traço a linha entre um invariante que vale a pena travar e um gate de métrica que sai pela culatra sob a lei de Goodhart.
Auditando um serviço Scala contra as quatro restrições regenerativas de Chad Fowler
Levei um serviço Scala de processamento de pedidos das minhas anotações pelas quatro restrições regenerativas de Chad Fowler. Duas passaram de graça, duas forçariam um redesign de verdade. Aqui está o que aprendi sobre onde "módulo fracamente acoplado" termina e "componente regenerativo" começa, e quais partes do redesign eu de fato pagaria.