


Fazer a IA escrever código de sistemas com cuidado, até os tipos e o tratamento de erros
Para usar IA em código de sistemas exigente. Ela revisa o código pelo menos o triplo do tempo que levou para escrevê-lo, cobre todos os erros, leva as regras aos tipos, mantém formatos antigos e novos compatíveis e faz commits pequenos que compilam.
Para usar estas regras de IA
## Revisar o código escrito pela IA pelo menos o triplo do tempo que levou para escrevê-lo
Trate o assistente como um multiplicador de qualidade, não como um multiplicador de velocidade. Gaste o tempo que ele economiza em um rigor além do que o trabalho sem auxílio alcançaria.
- Busque a excelência: cubra mais casos de borda, refatore em favor da compreensibilidade, resolva os TODOs pendentes, faça as coisas tediosas e mire em zero bugs.
- Revise por muito mais tempo do que você gera. Gaste pelo menos três vezes mais tempo revisando o código gerado do que levou para produzi-lo, ponderando cada linha e cada decisão de design, e procurando ativamente maneiras de quebrá-lo.
- O autor é dono do resultado. Não entregue um primeiro rascunho como trabalho finalizado, a menos que você esteja explicitamente apenas pedindo opinião sobre uma direção.
- Trabalho que mostra sinais de ter sido produzido sem cuidado, como casos ausentes que uma revisão humana atenta teria pego, pode ser recusado de imediato em vez de revisado.
- Declare o padrão no início de qualquer conversa que envolva mudanças de código, e novamente antes de abrir um pull request, para que autor e revisor compartilhem o mesmo nível de exigência.
## Levar a correção para os tipos e cobrir todos os casos-limite
Prefira a implementação correta à conveniente, e empurre o máximo de correção possível para dentro do sistema de tipos, de modo que os erros falhem na compilação em vez de falharem em produção.
- Modele todo o espaço de erros. Sem atalhos e sem tratamento de erros simplificado.
- Trate todos os casos de borda, incluindo condições de corrida, temporização de sinais e diferenças de comportamento entre sistemas operacionais.
- Codifique restrições de correção em tipos: tipos invólucro para valores de domínio, padrões builder para construções complexas, estados de tipo em genéricos quando as transições de estado importam, e empréstimo em vez de clonagem.
- Prefira garantias em tempo de compilação a verificações em tempo de execução sempre que a escolha existir.
- Mantenha a visibilidade a mais estreita possível. Amplie um item apenas quando algo fora de seu módulo genuinamente precisar dele.
- Marque os tipos públicos de pacotes estáveis e consumidos externamente como extensíveis, para compatibilidade futura. Pacotes internos que não fazem promessa de estabilidade não precisam disso, embora tipos de erro ainda possam querer.
- Use passagem de mensagens ou o modelo de atores em vez de estado mutável compartilhado, para que corridas de dados não possam surgir.
- Acertar os detalhes importa mais do que terminar rápido.
## Dar mensagens de erro úteis e o mesmo comportamento em cada sistema operacional
O comportamento voltado ao usuário é uma entrada de design, não um retoque final.
- Produza diagnósticos estruturados e úteis em vez de simples strings de erro, e use uma biblioteca de diagnósticos rica sempre que a mensagem chegar a uma pessoa.
- Faça o relato de progresso responsivo e informativo.
- Mantenha o comportamento consistente entre plataformas mesmo onde as capacidades subjacentes diferem, e implemente cada plataforma com seus próprios mecanismos nativos em vez de emular uma plataforma sobre outra.
- Escreva as mensagens voltadas ao usuário em presente claro: "a ferramenta agora oferece suporte", não "a ferramenta agora ofereceu suporte".
- Centralize a pluralização em um módulo auxiliar compartilhado cujas funções retornam a forma singular ou plural para uma contagem. Adicione novos auxiliares ali em vez de embutir a lógica de pluralização nos pontos de chamada.
## Evoluir o design passo a passo e registrar o porquê das escolhas
Mantenha o design específico e componível em vez de abstrato e excessivamente genérico.
- Evolua o design de forma incremental em vez de tentar uma arquitetura perfeita logo de início.
- Registre as decisões de design e seus compromissos em documentos de design mantidos junto ao projeto, para que leitores posteriores possam ver por que um formato foi escolhido.
- Quando houver incerteza, explore e itere em vez de congelar um palpite dentro de uma abstração.
## Separar erros do usuário de falhas internas e anexar contexto detalhado
Os erros fazem parte da superfície que o usuário vê e merecem a mesma atenção de design que o caminho bem-sucedido.
- Use tipos de erro baseados em derivação em vez de erros tipados como strings, e agrupe erros relacionados sob uma enumeração de espécie assim que o conjunto crescer.
- Carregue contexto rico e estruturado nos erros em vez de achatar o detalhe em uma única mensagem.
- Use um modelo de dois níveis. Erros causados pelo usuário e erros externos formam um tipo distinto que mapeia para códigos de saída semânticos; erros internos de programação são separados e podem gerar pânico.
- Escreva o texto de exibição do erro como um fragmento de frase em minúsculas que se leia corretamente quando embutido em uma frase envolvente como "falha ao ...".
## Avisar sobre chaves desconhecidas na configuração e dar erros de configuração claros
As decisões de desserialização determinam a qualidade de todo erro de configuração que um usuário verá.
- Rastreie e reporte caminhos ignorados ou desconhecidos na configuração, para que um erro de digitação apareça como aviso em vez de silenciosamente não fazer nada.
- Nunca achate estruturas aninhadas durante a desserialização. O buffering interno que isso exige degrada os avisos de campo desconhecido; copie os campos para a struct explicitamente em vez disso.
- Nunca dependa da desserialização de variantes sem etiqueta, que produz mensagens de erro ruins. Escreva um visitante personalizado com uma string de expectativa precisa em vez disso.
## Antes de mudar um formato salvo, verificar se versões antigas e novas leem os dados uma da outra
Qualquer mudança em uma estrutura escrita em disco ou enviada pela rede deve ser rastreada por toda a matriz de compatibilidade antes de entrar.
- Leitor antigo com dados novos: ele ainda consegue desserializar, e perde informação?
- Leitor novo com dados antigos: os valores padrão declarados produzem valores corretos, e não apenas válidos?
- Escritor antigo com dados novos: ele consegue completar o ciclo de leitura e escrita sem descartar campos? Este é o caso mais frequentemente esquecido.
- Incremente a versão do formato de forma proativa. Se um novo campo for semanticamente importante, eleve a versão quando o campo for adicionado, e não quando um valor diferente do padrão for escrito pela primeira vez; caso contrário, builds mais antigos corrompem os dados silenciosamente ao reescrevê-los.
- Trate os valores padrão como necessários, mas não suficientes. Eles permitem que um leitor antigo analise dados novos, mas um escritor antigo ainda descarta os campos que desconhece.
## Usar async só onde for preciso e não disparar tarefas sem limite
A assincronia é uma ferramenta para entrada e saída e para concorrência, não um estilo padrão.
- Use um runtime assíncrono multithread, mas confine o código assíncrono àquilo que realmente precisa dele e mantenha todo o resto síncrono.
- Use concorrência estruturada para que o trabalho concorrente não precise receber um tempo de vida artificialmente longo.
- Agende o trabalho enfileirado com consciência de contrapressão em vez de criar tarefas sem limite.
- Forneça primitivas de temporização pausáveis onde o controle de tarefas possa suspender e retomar o processo, para que o tempo suspenso não seja contabilizado contra os tempos limite.
## Não pôr lógica em módulos agregadores e escrever as importações no topo
Os limites de módulo são impostos por estrutura e visibilidade, não apenas por convenção.
- Use arquivos de módulo agregadores somente para reexportar itens públicos. Lógica não trivial pertence a um submódulo de implementação ou a um mais específico.
- Mantenha o código específico de plataforma em arquivos separados por plataforma, atrás de compilação condicional, com uma interface compartilhada entre eles.
- Mantenha os auxiliares de teste em módulos ou arquivos dedicados.
- Importe tipos e funções bem no topo do módulo. Nunca importe dentro do corpo de uma função; a única exceção é uma função compilada condicionalmente que precisa de uma importação condicionada.
- Prefira importar o próprio tipo, e importe um módulo apenas onde essa for a grafia convencional. Evite caminhos totalmente qualificados nas assinaturas: importe o módulo pai e qualifique apenas com ele.
- Importar variantes de enumeração para correspondência de padrões é aceitável.
## Emprestar ou compartilhar dados em vez de copiá-los
Escolha a representação que evita trabalho, em vez de otimizar depois do fato.
- Compartilhe dados imutáveis por contagem de referências ou empréstimo, em vez de copiá-los.
- Use um tipo otimizado para strings pequenas onde o código lida com muitas strings curtas.
- Preste atenção à clonagem e à referenciação. Evite clonar onde os dados têm uma estrutura natural de árvore ou de posse que o empréstimo já expressa.
- Faça os dados fluírem por iteradores sempre que possível, em vez de armazená-los inteiramente em memória.
## Formatar antes de cada commit e gerenciar as versões das dependências em um só lugar
A consistência mecânica é barata de impor e cara de aplicar retroativamente.
- Comece todo arquivo-fonte com a linha de copyright do projeto e o identificador de licença.
- Fixe a edição da linguagem e execute o comando de formatação do projeto antes de cada commit. A formatação é imposta na integração contínua, então um commit não formatado falha lá.
- Suprima um lint com a forma de expectativa em vez da forma de permissão, para que a própria supressão avise quando o lint deixar de disparar e supressões mortas não se acumulem.
- Onde um lint proibir anexar texto formatado a uma string, use a macro de escrita do projeto em vez de acrescentar uma string formatada.
- Gerencie todas as versões de dependências de forma centralizada na raiz do workspace, e fixe os pacotes internos em versões exatas.
- Comente escolhas não óbvias de dependências e de features, especialmente onde uma feature é desativada por motivo de segurança ou de tamanho.
## Escrever comentários que expliquem o porquê, não só o quê
- Comentários explicam o porquê, não meramente o quê.
- Não narre dentro do corpo das funções. Adicione um comentário apenas onde algo é não óbvio ou especial, ou onde a intenção precisa de uma explicação mais profunda.
- A documentação em nível de módulo declara o propósito e as responsabilidades do módulo.
- Sempre termine os comentários de código com um ponto final.
- Nunca use caixa de título em cabeçalhos e títulos; use sempre caixa de frase.
- Use sempre a vírgula de Oxford.
- Não omita artigos. Escreva "o arquivo tem uma versão mais recente", não "arquivo tem versão mais recente".
## Rodar os testes com o runner do projeto e reaproveitar os fixtures existentes
Teste do jeito que o projeto executa testes, e reutilize o modelo de testes que já existe.
- Use o executor de testes designado pelo projeto para testes unitários e de integração. Não recorra ao executor padrão da linguagem quando a suíte depender do modelo de execução do executor designado.
- Use uma cópia da ferramenta construída localmente apenas ao verificar mudanças no comportamento da própria ferramenta, como uma nova flag ou um novo formato de saída. Execute a suíte em si com o executor padrão, já que os testes de integração criam seus próprios processos internos a partir dos artefatos de build.
- Execute os testes de documentação com o comando que os suporta quando o executor principal não os suportar.
- Mantenha os testes unitários ao lado do código que testam, os testes de integração em um pacote dedicado, as fixtures compartilhadas em um pacote de fixtures e os utilitários de teste em um pacote de suporte.
- Prefira o modelo de resultados esperados do pacote de fixtures a verificações pontuais escritas à mão.
- Recorra ao ferramental que já existe: casos parametrizados, testes baseados em propriedades, asserções de snapshot, harnesses personalizados e saída de asserção melhorada. Verifique o que está disponível antes de adicionar um novo mecanismo.
## Começar o assunto do commit pelo escopo e manter cada commit compilável
O histórico é um artefato revisável, então trate cada commit como uma unidade de revisão.
- Prefixe o assunto de cada commit com o escopo que ele toca, entre colchetes, seguido de uma breve descrição. Use um escopo meta para trabalhos transversais, como mínimos da cadeia de ferramentas, releases e mudanças de integração contínua.
- Commits de incremento de versão e de preparação de release seguem seus próprios assuntos fixos e normalmente são produzidos pela ferramenta de release.
- Escreva os corpos em pretérito simples e presente: o que costumava acontecer, o que acontece agora e quais testes foram adicionados.
- Mensagens de commit são Markdown. Não use crases na linha de assunto, mas use-as no corpo.
- Faça commits atômicos. Cada commit é uma unidade lógica de mudança.
- Mantenha o histórico bissecável. Todo commit deve compilar e passar em todas as verificações.
- Separe as preocupações. Correções de formatação e refatorações pertencem a commits próprios, à parte das mudanças de comportamento.
## Dividir um runner concorrente em despachante e executor que se falam por mensagens
Um executor concorrente é mais fácil de raciocinar quando um componente é dono do mundo externo e outro é dono da execução.
- Um despachante interage com o mundo externo, lineariza os eventos de todas as fontes, multiplexa essas fontes com uma construção select e mantém o estado autoritativo do que está em execução no momento. Pontos de sincronização mantêm o relato livre de corridas.
- Um executor agenda e roda unidades de trabalho como máquinas de estado assíncronas, tratando retentativas, tempos limite, criação de processos e captura de saída. Cada unidade é a fonte da verdade do próprio estado e tem um canal dedicado ao despachante em vez de uma difusão.
- Não compartilhe nenhum estado diretamente; tudo cruza entre os componentes como mensagens.
- Modele todo o espaço de falhas, e use temporizadores pausáveis para que os sinais de suspensão e retomada não distorçam a temporização.
- Use a contenção de processos e as facilidades de sinal nativas de cada plataforma, proteja-as com compilação condicional e módulos por plataforma atrás de uma interface compartilhada, e documente as diferenças e os compromissos entre plataformas em comentários.
