CommunityEscrita e Ediçãogithub.com

Máquinas de estado sans-IO legíveis

Use esta skill para reestruturar um núcleo de protocolo Rust sans-IO, preservando o comportamento. Ela mapeia estados, entradas, saídas e temporizadores, refatora em pequenos passos verificados contra o original e termina com um resumo.

O que é Máquinas de estado sans-IO legíveis?

Esta skill é projetada para refatorar o núcleo de protocolos Rust que seguem o padrão sans-IO, onde a lógica de E/S é abstraída e o código age como uma máquina de estados. Ela é útil quando um desenvolvedor deseja tornar a máquina de estados mais legível, desembaraçar flags em enums de estado, simplificar manipuladores, ou reorganizar um arquivo grande em módulos, mesmo para tarefas simples como "mover código". A skill aceita comandos curtos, como "este resolver.rs é difícil de seguir". O processo envolve mapear estados, entradas, saídas e temporizadores, refatorar em pequenos passos que são verificados contra o original, manter a API pública idêntica e terminar com um resumo das alterações. Ela não deve ser usada para corrigir bugs, adicionar funcionalidades, ajustar desempenho, revisar PRs, escrever documentação para o usuário ou converter código assíncrono ou baseado em sockets para sans-IO. A skill suporta qualquer crate Rust sans-IO, como quinn-proto ou str0m.

Funciona com✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/FalkWoldmann/skills/tree/HEAD/sansio-readability

Installed? Explore more Escrita e Edição skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que Máquinas de estado sans-IO legíveis faz?

Uma biblioteca sans-IO é uma máquina de estado com a E/S removida. O chamador a alimenta com entradas (handle_*, cada uma com now), drena suas saídas (poll_transmit, poll_event, poll_timeout) e possui os soquetes e temporizadores. Código como este só vale a pena ter se as pessoas conseguirem raciocinar sobre ele: o próximo mantenedor, o autor de um driver, um revisor verificando-o contra um RFC. Cada um deles deve ser capaz de responder rapidamente a quatro perguntas:

  1. Estados: em quais estados cada entidade de longa duração (conexão, consulta, fluxo, sessão) pode estar, e quais dados existem em cada um?
  2. Transições: quais entradas movem de um estado para outro, e onde no código cada movimento acontece?
  3. Saídas: o que cada movimento enfileira (transmissões, eventos, escritas), e em qual ordem?
  4. Tempo: quando precisa ser despertado e qual regra define cada prazo?

Seu trabalho é tornar essas respostas visíveis nos tipos e no código, enquanto a máquina continua fazendo exatamente o que fazia. Você edita a árvore de trabalho e termina com um resumo.

<skill> abaixo significa o diretório base desta skill, a pasta que contém este SKILL.md.

Regras fundamentais

  • O comportamento permanece idêntico. Em um crate sans-IO, o comportamento abrange coisas que parecem cosméticas:

    • a ordem na qual as saídas são enfileiradas
    • a ordem de iteração dos mapas (um BTreeMap ou um Vec ordenado por inserção decide quem vai primeiro)
    • qual instante poll_timeout retorna
    • se uma escrita vazia ou um evento sem operação é emitido
    • com qual erro uma entidade falha ou encerra
    • a saída de um tipo #[derive(Debug)] cujos campos você altera, se um snapshot, log de asserção ou mensagem de erro a imprimir

    Testes que passam pela API pública, e todos os snapshots, devem passar inalterados. Se um precisar de edição, essa etapa mudou o comportamento: desfaça-a. Testes de unidade dentro do crate que nomeiam itens privados podem precisar da mesma edição mecânica que o código (uma flag que virou uma variante, um helper renomeado). Faça apenas essa edição, mantenha o que cada asserção verifica e liste o teste no resumo.

  • Deixe a API pública em paz. Mantenha os nomes, assinaturas, variantes e campos de itens pub como estão, pois drivers e crates downstream dependem deles (e do semver). pub(crate) e itens privados são permitidos. Coloque melhorias na API no resumo como sugestões. Como verificar isso está na etapa 5.

  • Não gaste desempenho em legibilidade. Sem novos clones, alocações ou passagens extras sobre os mapas em caminhos que executam por entrada (por pacote, mensagem ou consulta). Um mem::replace ou mem::take é gratuito; clonar um Vec para driblar o verificador de empréstimos não é.

  • Trabalhe em pequenos passos. Cada passo compila e passa nas verificações, então uma verificação com falha aponta para uma alteração e o diff pode ser revisado peça por peça.

  • Siga o projeto. Antes de editar, leia seus documentos de agente e contribuidor (AGENTS.md, CLAUDE.md, CONTRIBUTING), a configuração de lint e o workflow de CI. Combine a densidade de comentários e o idioma ao redor. Onde o projeto mantém documentos em sincronia com o código (uma tabela de ciclo de vida, um comentário mapeando linhas para funções, uma tabela de cobertura de especificação), atualize-os no mesmo passo.

Dimensionar o trabalho para a mudança

A rede de segurança existe para capturar mudanças de comportamento que os testes existentes não pegariam, então dimensione-a para a mudança que você planeja:

  • Pequeno. Um manipulador ou alguns helpers, sem mudança em quais campos mantêm estado. Execute as verificações do projeto uma vez antes e uma vez depois, e pule tudo marcado como grande abaixo. No resumo, mapeie apenas a entidade que você tocou.
  • Médio. Várias transições ou helpers de saída em um módulo. Execute as verificações antes de cada passo e no final. Construa um harness diferencial apenas se nenhum teste existente fixar as saídas exatas dos caminhos que você altera, e apenas sobre esses caminhos. Sem fuzzing, sem casos extras de teste de propriedade, sem benchmarks: as próprias verificações do projeto em suas configurações normais são o peso certo aqui.
  • Grande. Flags substituídas por um enum de fase, muitos escritores de um campo de estado mesclados em transições, a maioria dos manipuladores remodelados. Use o workflow completo.

Decida o tamanho após o mapa (etapa 2). Se a mudança crescer no meio do caminho, suba de tamanho e diga isso no resumo.

Workflow

1. Configure uma rede de segurança

Faça isso antes da primeira edição, para que cada falha posterior seja sua e não pré-existente.

  • Anote a base. Execute git status e anote o commit base: toda comparação posterior é contra ele. Se arquivos que você planeja tocar já têm alterações não commitadas, avise o usuário antes de misturar suas edições nas deles. Se você prosseguir sobre elas, use git stash create como base: ele as registra como um commit sem tocar na árvore.
  • Corrija um comando de verificação. Use o próprio do projeto se ele tiver um (um workflow de CI, um justfile, um xtask, os comandos em seus documentos de agente). Caso contrário, use a porta fornecida: <skill>/scripts/check.sh <repo> default all. Liste os conjuntos de recursos que CI usa, por exemplo default all none tls. A porta verifica a formatação sem alterá-la, depois executa clippy e os testes para cada conjunto de recursos, e sai com código não zero se algo falhar. Escopo para o pacote que você altera e os membros que dependem dele com CHECK_PACKAGES="-p <crate> -p <dependent>", e execute todo o workspace uma vez no final. Se CI também executar algo mais (uma verificação de MSRV, flags de lint extras), execute isso junto.
  • Se a linha de base não estiver verde, não a corrija. Registre quais verificações falham e como, avise o usuário e, a partir de então, espere as mesmas falhas e nenhuma nova. Formate apenas os arquivos que você edita. Uma falha que vem do ambiente, e não do código (uma configuração global do cargo com flags apenas noturnas, uma toolchain ou ferramenta ausente) vai para Verificação como "não pôde executar", com o motivo. Se o repositório não tem CI, pegue o piso da toolchain de rust-version, a edition e os recursos de linguagem em uso, e diga o que você pôde e não pôde executar.
  • Mudanças grandes: construa um harness diferencial quando os testes existentes não fixam as saídas exatas dos caminhos que você alterará. Fixar significa testes de rastreamento ou snapshot que afirmam a sequência de saída e os prazos textualmente; asserções sobre o estado final sozinhas não contam. O harness executa a base e a árvore de trabalho lado a lado nas mesmas chamadas aleatórias e compara cada saída. <skill>/scripts/difftest_scaffold.py cria o par de crates e um modelo fora do repositório. Preencher o modelo é o trabalho real, então planeje para isso: leia references/differential.md primeiro. Verifique se ele pega um mutante plantado de uma linha antes de confiar nele; um é suficiente.
  • Mantenha o harness mais barato que a refatoração. Dirija os caminhos que você reestrutura, não o crate inteiro. Se cobrir um caminho custaria mais do que a mudança nele, relate a lacuna sob Verificação e confie nos testes existentes daquele caminho. Exemplos: um caminho atrás de TLS ou criptografia, não determinismo que você teria que normalizar, um par que você teria que simular.
  • Apenas mudanças grandes: execute as verificações lentas uma vez com mais casos. Execute os testes de propriedade, dimensionados para terminar em alguns minutos (dez vezes o padrão, ou PROPTEST_CASES=10000 quando os casos são baratos), e novamente no final. Execute um alvo de fuzzing apenas se ele atingir código que você alterará, com tempo limitado a 30–60 s. Execute benchmarks apenas se uma mudança adicionar trabalho em um caminho por entrada; reestruturar o fluxo de controle não precisa deles.
  • Preencha lacunas de cobertura que devem sobreviver à mudança. Se um caminho que você reestruturará não tem cobertura e você não está construindo um harness, adicione testes que fixam suas saídas atuais primeiro. Se você está construindo um, ele cobre a lacuna para a refatoração; adicione um teste apenas para um comportamento que ele revela e que vale a pena manter (uma peculiaridade que você manteve de propósito). Coloque novos testes em um novo arquivo ou módulo, como seu próprio passo, para que os testes existentes permaneçam inalterados. A exceção é a linha mod que um layout de teste de crate único como tests/main.rs precisa.

2. Mapeie a máquina

Mapeie as entidades sobre as quais o pedido é, e qualquer entidade que uma mudança planejada tocaria. Em um crate grande onde o pedido não diz qual parte importa, pegue a entidade que o usuário nomeou e diga o que você deixou de fora. Escreva o mapa antes de mudar qualquer coisa (um arquivo temporário é suficiente). Ele se torna o mapa de estado do resumo e, frequentemente, a documentação do módulo.

  • Entidades. Para cada entidade no escopo, liste seus campos e agrupe-os pela fase na qual eles significam algo. Combinações de bools e Options que codificam uma fase entre eles são o alvo principal.
  • Entradas. Toda pub fn que recebe dados externos ou now, e o que cada uma pode acionar.
  • Escritores. Para cada campo de estado, todo lugar que o atribui (procure por .field =, mem::replace, .take(), += 1). Um campo com muitos escritores é uma transição espalhada pelo arquivo.
  • Saídas. Toda inserção em uma fila de saída, e todo prazo que poll_timeout lê.
  • Invariantes. O que deve sempre valer: um ID em exatamente um mapa, um prazo definido sempre que algo espera, uma conexão referenciada que existe.
  • Peculiaridades. Comportamento que parece acidental: análise leniente, precedência estranha entre dois erros, uma resposta enviada em resposta a lixo, entrada ainda aceita após um prazo. Você mantém (regra 1), mas liste para o usuário.

Então escreva o mapa como uma tabela: estado, entrada, próximo estado, saídas. Onde você não pode preenchê-lo a partir do código sem rastrear várias funções, o código ainda não é legível, e é aí que o trabalho vai.

3. Escolha as mudanças

references/patterns.md é o catálogo, com exemplos antes/depois e os casos onde cada padrão não compensa. Leia seu índice, depois as seções para o que seu mapa revelou, incluindo seus parágrafos "Não vale a pena". Leia o arquivo inteiro para uma mudança grande. Classifique as mudanças candidatas por quanto cada uma encurta a resposta a uma das quatro perguntas, contra o tamanho do diff. Os movimentos que geralmente compensam mais:

  • um enum cujas variantes carregam os dados de cada fase, no lugar de flags e options que codificam a fase (padrão 1)
  • uma função nomeada por transição, para que cada campo de estado tenha poucos escritores (padrão 2)
  • manipuladores que casam a entrada, decidem, aplicam a transição e enfileiram as saídas, ao longo de um eixo de despacho consistente (padrão 3)
  • um helper por tipo de saída, nomeado após sua intenção (padrão 4)
  • cada regra de prazo escrita uma vez, com poll_timeout derivado do estado (padrão 5)
  • o ciclo de vida escrito onde um leitor chega, e invariantes nos campos (padrão 9)

Se o usuário nomeou uma função ou uma parte, mude apenas aquilo e o que ele precisa. Um helper que você extrai pode ser reutilizado por seus irmãos naturais, mas não retrabalhe código fora do pedido só porque o helper cabe ali também. Liste essas melhorias sob Sugestões em vez disso: um revisor que perguntou sobre uma função não deve ter que verificar uma segunda.

Deixe de fora mudanças que apenas movem código, renomeações de variáveis locais para agradar a um gosto, e qualquer coisa que torne o código mais longo sem tornar uma pergunta mais fácil de responder. Quando o usuário pedir pelo movimento em si (dividir um arquivo longo em módulos), veja o padrão 11. Código que já lê bem pode ficar como está: diga isso no resumo, em vez de inventar trabalho.

4. Aplique em passos

Ordene os passos para que cada um seja revisável e nenhum adicione código que um passo posterior delete:

  1. Adições puras e extrações: helpers de saída, predicados nomeados, helpers de prazo, passos protegidos por features movidos para helpers, codificação e análise de fio movidos para funções (padrões 4, 5, 6, 10).
  2. A mudança central: um enum de fase, junto com as transições e formas de manipulador que ele força (padrões 1–3, 7, 12). Isso frequentemente tem que chegar como um passo. Se sim, diga por que.
  3. Nomes (padrão 8).
  4. Documentação (padrão 9).
  5. Reordenar métodos, por último e por si só, se for o caso (padrão 11).

Entre edições, use um loop rápido: clippy e os testes do pacote que você está alterando, no conjunto de recursos padrão, mais o harness se você construiu um. No final de cada passo, execute o comando de verificação da etapa 1. Então faça do passo um commit se o usuário quiser commits (ele pediu, ou o workflow do projeto os espera); caso contrário, deixe o trabalho não commitado e liste os passos no resumo. Não silencie um lint com scaffold que o próximo passo remove (uma porta cfg ou um #[allow] adicionado apenas para deixar um passo verde). Reordene os passos em vez disso.

Abandone um padrão em qualquer lugar onde o verificador de empréstimos o force a contorções, já que código contorcido lê pior do que o que você começou. Os sinais usuais:

  • um método que recebe um campo irmão de self (uma fila de saída como &mut VecDeque<Transmit>) ou sua própria chave de mapa como argumento, apenas para passar no empréstimo
  • um método &self que recebe uma subestrutura apenas para poder ler a configuração
  • chaves clonadas ou uma busca extra

As correções usuais:

  • retorne a saída e deixe o chamador enfileirá-la
  • coloque o helper na entidade e passe o valor de configuração Copy que ele precisa
  • mantenha o código inline

5. Verifique e resuma

Execute o comando de verificação uma última vez, sobre todo o workspace, mais o que mais você executou no início: as verificações lentas e o harness.

Então verifique a API pública, se o diff toca um item pub, um derive, uma impl de trait, um tipo de campo (traits automáticos como Send seguem campos privados) ou um cfg em um item:

  • Use cargo semver-checks --baseline-rev <base> uma vez por conjunto de recursos, com flags explícitas: --default-features, --all-features, --only-explicit-features (nenhuma), e --only-explicit-features --features x.
  • Ele pega apenas mudanças que quebram, então também confirme que nada novo se tornou público: use cargo public-api diff se instalado, ou leia git diff <base> | grep -E '^\+.*\bpub\b'.
  • Se nenhuma ferramenta está instalada, pergunte antes de instalar uma, e caso contrário reporte a verificação de API como não executada.
  • Se o diff toca apenas corpos de função e itens privados, diga isso.

Habilidades Relacionadas