


Alterar um pipeline de dados com plugins que roda em dois runtimes sem quebrá-lo
Use quando um agente de código trabalha em um backend com plugins que roda em dois runtimes. Mantém as versões em um só arquivo, aplica os filtros de teste à tarefa certa, segue as regras de plugins, concorrência e camadas de serviço, e confirma que cada teste novo falha sem a sua mudança.
Para usar estas regras de IA
## Manter as versões em um só arquivo e fazer o build sempre a partir da mesma raiz
Mantenha as versões e as coordenadas do toolchain canônicas, e sempre compile a partir do mesmo lugar.
- Armazene cada número de versão e cada coordenada do toolchain em um único arquivo canônico; todos os consumidores (scripts de build, helpers, testes, descritores de pacote) leem a partir dele, e você atualiza apenas esse arquivo.
- Quando um valor é propositalmente fixado (hardcoded) ou duplicado em vários lugares, como um segmento de caminho de compatibilidade de runtime, não trate isso como um bug nem o calcule dinamicamente; encontre o valor atual no lugar canônico e reproduza-o.
- Execute todos os comandos de build e test a partir de uma única raiz consistente, para que as referências relativas sejam resolvidas sempre da mesma forma.
- Prefira um toolchain vendorizado e fixado, provisionado por uma etapa de bootstrap, em vez de uma linguagem ou gerenciador de versões instalado globalmente, para que todo ambiente compile da mesma maneira.
## Aplicar os filtros de teste à tarefa que realmente contém os testes
Entenda como as tarefas de teste estão conectadas antes de executar um subconjunto filtrado.
- Uma tarefa de teste guarda-chuva frequentemente exclui todas as classes em si e apenas delega para subtarefas, de modo que um filtro por nome aplicado a ela silenciosamente não encontra nada e parece ter passado. Aplique filtros à tarefa concreta que de fato contém os testes.
- As suítes são divididas de propósito: alguns testes são movidos para fora da suíte óbvia, por exemplo testes que inicializam um segundo runtime rodam na suíte que fornece esse runtime. Se um filtro não encontra nada, o alvo provavelmente está na outra suíte.
- Saiba onde ficam os testes unitários, de integração e de aceitação, para invocar o ponto de entrada correto.
## Reestruturar o código para que cada caso novo pareça planejado desde o início
Ao modificar código, não se limite a acrescentar algo ao que já existe. Avalie se o código ao redor ainda faz sentido com sua mudança. Se uma abstração foi projetada para um caso e você está adicionando um segundo, reestruture para que o resultado pareça ter sido pensado para ambos os casos desde o início. Dê a cada conceito distinto sua própria unidade com nome claro, e componha essas unidades em código de nível mais alto para que a intenção fique óbvia sem precisar ler cada implementação.
## Criar plugins sobre as classes base e o registro do framework
Construa extensões sobre os próprios pontos de extensão do framework, não ao redor deles.
- Herde da classe base específica do tipo e sobrescreva seus hooks de ciclo de vida: um hook de setup ou registro, além do ponto de entrada do tipo.
- Declare a configuração por meio da DSL declarativa de opções do framework, em vez de ler parâmetros brutos, para que validação, coerção e geração de documentação sejam feitas automaticamente.
- Defina a concorrência no nível da classe: shared quando várias threads de worker podem chamar a instância ao mesmo tempo, single quando as chamadas precisam ser serializadas para que apenas uma thread execute por vez.
- Encaminhe toda busca de plugins pelo registry central, registre novos plugins nativos ali, e deixe que plugins externos sejam descobertos como unidades empacotadas.
## Colocar métricas e lógica comum em um delegador que envolve cada plugin
Mantenha as preocupações transversais em uma camada de wrapping e obtenha colaboradores a partir do framework.
- Execute cada plugin por trás de um delegator que adiciona preocupações transversais como métricas e thread-safety; ao mudar o comportamento, decida se ele pertence ao delegator ou ao próprio plugin.
- Deixe que as métricas sigam uma hierarquia de namespace, criando filhos por meio de uma chamada de namespace.
- Forneça uma métrica null-object que implementa a mesma interface, mas descarta os dados, para quando as métricas estiverem desativadas ou em teste.
- Nunca instancie objetos de métrica diretamente; receba-os a partir do framework. Mixins padrão, como logging e configuração, são incluídos automaticamente, enquanto capacidades mais pesadas permanecem opt-in.
## Manter o loop dos workers rápido e iniciar e parar na ordem certa
Proteja o hot path e respeite a ordem de início e parada.
- Trate o loop do worker como o hot path: ele puxa batches, os executa através do pipeline compilado e sinaliza a conclusão. Não adicione chamadas bloqueantes nem alocações pesadas dentro dele.
- Siga a ordem estrita do ciclo de vida: outputs e filters se registram primeiro, depois os workers iniciam, depois os inputs iniciam por último; no desligamento, os inputs param primeiro, os workers drenam, e então filters e outputs fecham. Violar essa ordem causa perda de dados ou deadlock.
- Use primitivas atômicas para estado compartilhado entre threads, não campos booleanos simples, porque a visibilidade de memória entre threads não é garantida de outra forma.
## Compilar a configuração do pipeline em código e exportá-lo para depuração
Alguns engines compilam configuração declarativa em classes executáveis em tempo de execução por meio de um pipeline em estágios: faça o parse do texto de configuração com uma gramática para obter uma árvore sintática; reduza essa árvore para uma representação intermediária, um grafo acíclico dirigido de nós e condicionais; gere o código-fonte de cada nó e o compile em classes em tempo de execução dentro de um namespace dedicado; então conecte as unidades compiladas ao loop de execução. Condicionais são compilados em funções predicado conectadas a nós de ramificação. Forneça uma flag de debug que despeja o código-fonte gerado em um diretório para que você possa inspecionar o que de fato está rodando.
## Tratar erros, nulos e logging do mesmo jeito em todo o código
Aplique convenções consistentes de estrutura, erros, nulos e logging em toda a base de código.
- Organize os módulos por domínio funcional, não por camada arquitetural, e coloque cada novo tipo no domínio ao qual ele pertence.
- Mantenha a nomenclatura consistente: sufixos e prefixos reservados para tipos de extensão, delegator e abstratos; marque classes como final a menos que a criação de subclasses seja intencional.
- Prefira exceções unchecked em tempo de execução; use exceções checked apenas quando inevitável, sempre com uma mensagem descritiva e a cadeia de causas preservada.
- Para nulos, valide argumentos de construtor com uma verificação explícita de não nulo, anote contratos intencionalmente anuláveis (nullable), e use um tipo optional para buscas que podem falhar, e não como substituto geral de null.
- Declare um logger static e final por classe e escolha os níveis deliberadamente: diagnósticos finos em nível baixo, problemas recuperáveis como warnings, falhas não tratadas como errors.
- Priorize campos final imutáveis, use tipos de map concorrentes para caches compartilhados, e documente a thread-safety onde um tipo é compartilhado.
## Enviar as respostas da API por um único helper e colocar a lógica em classes de comando
Estruture o serviço em camadas de modo que roteamento, respostas e lógica tenham, cada um, um único lugar.
- Faça os módulos de API herdarem de uma base compartilhada que estende a classe base do framework web e recebe dependências comuns por meio do construtor.
- Monte os endpoints em prefixos de caminho declarados em um único mapa central de namespaces; adicione novos namespaces ali, e não por meio de roteamento ad-hoc.
- Envie cada resposta por meio de um único response helper que envolve os payloads com metadados padrão; não escreva respostas brutas manualmente.
- Represente erros com um pequeno conjunto de subclasses de erro tipadas, como not-found, bad-request e timeout; lance-as a partir dos handlers e deixe que a base formate um envelope consistente.
- Mantenha a lógica de negócio em classes de command dedicadas, não nos handlers de rota; um handler constrói um command e passa seu resultado ao response helper.
## Conectar dois runtimes e testar o caminho de ida e volta pelos dois
Quando o código faz a ponte entre dois runtimes, como uma máquina virtual host controlando uma segunda linguagem, registre as classes que fazem essa ponte no ponto de registro designado pelo framework e siga os padrões já existentes ali. Obtenha objetos de contexto por thread no ponto de uso, nunca os armazenando no momento da construção, porque um contexto armazenado falha quando reutilizado a partir de outra thread. Teste o percurso completo (round trip) pelas duas camadas em vez de cada lado isoladamente, para capturar bugs de coerção de tipo e de registro que testes unitários de uma única camada não detectam. Testes que carregam o segundo runtime devem apontar seus caminhos de busca de módulo para o local vendorizado; esse caminho é intencional, então não o altere ao editar esses testes.
## Testar com os helpers do pipeline real e limpar o estado depois
Exercite o comportamento real com helpers construídos para esse fim, e não deixe nenhum estado para trás.
- Prefira os helpers de pipeline fornecidos, que sobem um pipeline real e capturam resultados, em vez de construir objetos de pipeline manualmente.
- Para isolamento, defina classes de plugin descartáveis dentro do arquivo de spec e registre-as no registry real.
- Use shared examples para contratos de interface e shared context para setup comum; conduza os testes de integração com um padrão de fixture-and-service que faz o bootstrap a partir de arquivos de fixture.
- Confirme que os blocos rescue ou catch existentes realmente cobrem os novos caminhos de código adicionados.
- Limpe o estado global ou da máquina virtual no teardown: toda propriedade que você define precisa de uma limpeza correspondente, para que o estado não vaze para outros testes.
- Prefira doubles concretos, construídos para esse fim, que herdam classes base reais, em vez de mocks genéricos; reserve mocks para fronteiras de infraestrutura, e deixe o método real rodar quando um stub for apenas para setup.
## Confirmar que cada teste novo falha sem a sua mudança
Garanta que cada teste realmente dependa da mudança que ele protege.
- Verifique se cada novo teste falha sem a mudança correspondente; um teste que passa com ou sem a correção não prova nada.
- Faça a asserção sobre o comportamento específico que a mudança introduz, não sobre condições preexistentes: prenda a asserção a uma diferença observável, como um novo campo, um valor alterado ou uma linha de log que antes não existia.
- Para uma correção de bug, reproduza o bug no teste primeiro; observe-o falhar pelo mesmo motivo que o bug se manifesta, depois aplique a correção e observe-o passar.
- Cuidado com mocks tautológicos: fazer stub de um método para retornar o valor que você depois vai afirmar (assert) é circular, e se apagar a implementação mantendo só o mock ainda passa, o teste só está exercitando o mock.
## Introduzir mudanças incompatíveis em etapas e mascarar segredos nos logs
Mude o comportamento em uma escada de estágios, proteja valores sensíveis e mantenha a documentação sincronizada.
- Faça mudanças que quebram compatibilidade em estágios: introduza o novo comportamento como opt-in e adicione avisos de depreciação em releases menores (minor), depois mude o padrão mantendo uma opção de opt-out e, por fim, remova o comportamento antigo em releases maiores (major).
- Para configurações que carregam valores sensíveis, aplique o tipo dedicado de secret do framework ou um wrapper, e sobrescreva a representação em string para que o valor seja mascarado; nunca escreva esses valores em logs, em nenhum nível, por padrão.
- Trate a documentação como parte da mudança: atualize a documentação de referência e a de contribuidores relevante sempre que o comportamento mudar, siga as convenções de cada formato, e exija documentação antes de fazer merge de funcionalidades voltadas ao usuário.
