Buscar/Manter um SDK de protocolo bidirecional em um monorepo TypeScript sem quebrar sua API pública

Manter um SDK de protocolo bidirecional em um monorepo TypeScript sem quebrar sua API pública

Use ao alterar um SDK tipado, publicado em vários pacotes, em que cliente e servidor enviam requisições. Escolhe com cuidado o que é exportado, adiciona operações nos dois lados com verificação de capacidades, registra mudanças incompatíveis e roda os exemplos como testes.

Para usar estas regras de IA
## Ver em qual camada a mudança entra e se ela toca a API pública Este guia rege o trabalho dentro de um workspace TypeScript com múltiplos pacotes que publica um SDK de protocolo: um núcleo interno compartilhado, um pacote cliente, um pacote servidor, um pacote de schemas independente e pacotes adaptadores finos para frameworks ou runtimes. Assuma verificação estrita de tipos, módulos ES e scripts abrangendo todo o workspace, executados por um único gerenciador de pacotes. Duas propriedades dominam todas as decisões. Primeira: a superfície publicada é um contrato — tudo o que é alcançável a partir do ponto de entrada de um pacote é API pública e não pode ser alterado sem critério. Segunda: o protocolo é bidirecional — ambos os pares podem originar requisições —, então uma mudança de um lado normalmente traz uma obrigação espelhada do outro. Antes de editar, identifique a qual camada a mudança pertence e se ela toca a superfície publicada. Se tocar, trate-a como uma mudança de API e aplique as regras de superfície e de migração descritas abaixo, e não apenas as regras de estilo. ## Rodar um só teste ao iterar e todas as verificações antes de concluir O workspace expõe scripts agregados que cobrem todo o repositório: instalar dependências, compilar todos os pacotes, executar as checagens de lint e formatação, corrigir automaticamente lint e formatação, verificar os tipos de todos os pacotes, rodar a suíte completa de testes e um script de checagem combinada que junta verificação de tipos com lint. Reduza o escopo do ciclo enquanto itera. Use a flag de filtro do gerenciador de pacotes para rodar o script de teste de um único pacote, passe um caminho de arquivo para rodar um único arquivo de teste e passe um filtro de nome para rodar um único caso de teste. Existe um script em modo watch para iterações rápidas. Reserve as execuções sobre o workspace inteiro para o momento anterior a declarar o trabalho concluído, e rode então tanto a checagem combinada quanto a suíte de testes. A verificação de tipos sozinha não comprova o comportamento em tempo de execução — nunca afirme que uma mudança funciona sem executar os testes que a exercitam. ## Seguir as regras de nomes e formatação do repositório - Verificação estrita de tipos em todo o código; tipos de retorno explícitos em funções exportadas. - PascalCase para classes e tipos; camelCase para funções e variáveis. - Nomes de arquivo em minúsculas e separados por hífen; arquivos de teste levam um sufixo de teste na extensão. - Imports de módulos ES. Siga a configuração de resolução de módulos do projeto quanto às extensões — com resolução no estilo bundler, imports relativos não levam extensão de arquivo. Agrupe os imports de forma lógica. - Indentação de dois espaços, ponto e vírgula obrigatório, aspas simples preferidas. - Os testes ficam no diretório de testes dedicado de cada pacote, para que o padrão de inclusão do executor os encontre; use nomes descritivos para os testes. - Comentários de documentação nas APIs públicas; comentários inline reservados a lógicas genuinamente não óbvias. Quando uma regra for ambígua, siga o padrão do arquivo ao redor. Não reformate nem refatore código não relacionado dentro de uma mudança funcional. ## Trazer os exemplos da documentação de arquivos que são compilados Trechos escritos à mão dentro de comentários de documentação apodrecem silenciosamente, porque nada os compila. Em vez disso, extraia o código de exemplo de módulos de exemplo complementares, que são compilados e têm seus tipos verificados junto com o restante do código-fonte. Um arquivo complementar fica ao lado do módulo correspondente, sob um nome equivalente. Regiões nomeadas dentro do arquivo complementar são referenciadas a partir do bloco de código da documentação por meio de um atributo de origem que aponta para o arquivo e a região. Os nomes das regiões seguem um padrão previsível: o nome do símbolo exportado mais uma variante, ou o nome da classe mais o nome do método mais uma variante. Omitir a referência à região inclui o arquivo inteiro, o que funciona para qualquer tipo de arquivo. Execute o script de sincronização de trechos após editar os exemplos, para que o conteúdo atual seja copiado para os comentários de documentação e para as páginas markdown, e faça o commit da saída sincronizada junto com a mudança. ## Exportar o que é público por nome e manter a raiz do pacote neutra ao ambiente Separe um barrel interno — que exporta tudo, é marcado como privado e é consumido apenas por pacotes irmãos dentro do workspace — de um módulo de exportação pública curado, que expõe tipos, classes de erro, constantes, guards e classes base compartilhadas. Os pontos de entrada dos pacotes reexportam o módulo curado mais suas próprias exportações explicitamente nomeadas. Regras que não são negociáveis: - Use exportações nomeadas explícitas nos pontos de entrada e no módulo curado. Nunca use reexportações com curinga nesses lugares. - Adicionar um símbolo ao ponto de entrada de um pacote o torna API pública. Faça isso apenas de forma deliberada. - Helpers internos permanecem no barrel interno e nunca são adicionados ao módulo curado nem a um ponto de entrada. - Mantenha o ponto de entrada raiz de cada pacote neutro em relação ao runtime, para que bundlers de navegador e de edge runtime consigam consumi-lo. Qualquer exportação cujo grafo de módulos alcance transitivamente recursos nativos específicos de plataforma, como criação de processos ou sockets brutos, deve ficar atrás de uma exportação por subcaminho nomeado e ser coberta por um teste que garanta que o barrel raiz permaneça livre deles. - Mantenha tipos de bibliotecas de validação fora das superfícies publicadas que prometem permanecer livres deles; em vez disso, publique schemas brutos de runtime a partir do pacote de schemas dedicado e deixe que os pacotes irmãos resolvam esse pacote como uma dependência externa real, em vez de carregarem cópias privadas dos schemas. ## Construir sobre a camada de esquemas e manter transportes substituíveis Organize o SDK em três camadas. 1. Camada de tipos e schemas — tipos de mensagens do protocolo, schemas de validação e constantes alinhados à especificação. Esta é a única fonte de verdade; todas as demais camadas derivam dela. 2. Camada de protocolo — uma classe base abstrata que cuida do roteamento de mensagens, da correlação entre requisição e resposta, da negociação de capacidades e do ciclo de vida do transporte. As implementações de ambos os pares a estendem, e é por isso que o comportamento bidirecional é simétrico por construção. 3. APIs de alto nível — as classes cliente e servidor que expõem métodos tipados, mais um wrapper de servidor de nível mais alto que oferece registro simplificado de ferramentas, recursos e prompts. Os transportes formam uma camada plugável separada por trás de uma única interface: um transporte HTTP com streaming, recomendado para pares remotos; um transporte legado baseado em fluxo de eventos, mantido por compatibilidade retroativa; e um transporte de entrada e saída padrão para integrações com processos iniciados localmente. A validação é plugável no mesmo espírito, com um validador padrão mais alternativas compatíveis com runtimes restritos. Os pacotes adaptadores de framework e de runtime são camadas finas de integração. Eles nunca devem acrescentar funcionalidade de protocolo; tudo o que altera o comportamento do protocolo pertence às camadas centrais. ## Acompanhar o fluxo de requisições de saída e de entrada nos dois lados Fluxo de saída. Um método de alto nível delega ao método de requisição da classe base, que atribui um identificador único à mensagem, verifica se o par remoto declarou a capacidade exigida pelo método, registra um handler de resposta pendente, entrega a requisição serializada ao transporte e aguarda a resolução quando a resposta correspondente chega. Fluxo de entrada. O transporte expõe uma mensagem recebida e o roteamento da classe base a despacha como requisição, resposta ou notificação. O tratamento de requisições busca um handler indexado pelo nome do método, constrói um objeto de contexto, permite que a subclasse enriqueça esse contexto com detalhes específicos do transporte, invoca o handler e devolve o resultado do handler como resposta pelo transporte. Formato do contexto. Um identificador de sessão; um grupo de requisição contendo o identificador da mensagem, o nome do método, os metadados da requisição, um sinal de abort para cancelamento e helpers para enviar uma requisição ou notificação relacionada de volta ao par; e um grupo opcional de transporte que carrega detalhes de autorização validados quando o transporte os fornece. O contexto do lado servidor acrescenta helpers de logging, de solicitação de entrada do usuário e de amostragem de modelo. A verificação de capacidades ocorre nas duas direções e também no momento do registro de handlers, de modo que um par não consiga registrar um handler para algo que nunca declarou. ## Adicionar uma operação de protocolo com verificação de capacidades nos dois lados 1. Defina o schema dos parâmetros da requisição e o schema do resultado na camada de tipos. 2. Adicione o flag de capacidade correspondente ao tipo de capacidades do cliente ou do servidor. 3. Implemente o método de envio no par que inicia a operação. 4. Adicione a verificação de capacidade dentro da checagem de capacidades desse par. 5. Registre o handler receptor no par oposto. 6. Adicione um wrapper de alto nível na API simplificada do servidor quando a operação justificar. 7. Adicione testes dos dois lados e um exemplo executável quando a operação for voltada ao usuário. Os passos dois e quatro são os mais frequentemente esquecidos e os mais caros de esquecer: uma operação sem negociação de capacidades falha em tempo de execução diante de pares que não a implementam, em vez de falhar de forma limpa na inicialização. ## Registrar cada mudança incompatível com o que mudou, por quê e como migrar Registre toda mudança incompatível no guia de migração, dentro da seção do subsistema que ela afeta. Procure seções relacionadas e amplie-as, em vez de acrescentar novas seções isoladas — um guia formado por entradas paralelas e avulsas se torna ilegível exatamente quando alguém precisa dele. Mudanças restritas a um único arquivo de notas de versão vão para lá. Toda entrada diz três coisas: o que mudou, por que mudou e como migrar. Renomeações mecânicas pertencem aos arquivos de mapeamento da ferramenta automatizada de migração. Aponte um link para o mapeamento em vez de reproduzir tabelas de renomeação em prosa; tabelas duplicadas ficam dessincronizadas da ferramenta que de fato executa a renomeação. Critério: prefira mudanças aditivas. Quando a quebra for inevitável, mantenha o caminho antigo funcionando sob depreciação por um ciclo, sempre que viável, e desenhe a quebra de modo que ela apareça como um erro claro em tempo de compilação, e não como uma mudança silenciosa de comportamento. ## Rodar no CI os exemplos de cliente e servidor como testes Trate os exemplos como testes, não como enfeite. Cada exemplo é um pacote próprio do workspace, contendo um servidor e um cliente pareados; o cliente conecta, verifica os resultados que espera e encerra com status de falha em caso de divergência. Um executor roda todos os exemplos em todas as combinações configuradas de transporte e de era do protocolo, e esse job faz parte do gate de integração contínua aplicado a cada mudança. A infraestrutura compartilhada dos exemplos vive em um único pacote compartilhado, cuja exportação raiz permanece mínima — parsing de argumentos, helpers de asserção, resolução de caminhos de pacotes irmãos. Dependências opcionais mais pesadas, como um provedor de autorização de demonstração ou um event store em memória, ficam atrás de uma exportação por subcaminho, para que exemplos não relacionados não as carreguem antecipadamente. Os exemplos importam apenas essa infraestrutura e configuram os transportes inline, de modo que cada exemplo se leia como uso real, e não como um invólucro em torno de uma configuração oculta. As páginas de guia da documentação recebem companheiros de trechos por página, sincronizados pelo mesmo passo de sincronização de trechos, e os que são executáveis também rodam na integração contínua.