Buscar/Alterar um pipeline de dados com plugins que roda em dois runtimes sem quebrá-lo

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.