Como montar um projeto que decide se um pull request precisa de revisão humana

fluxo de revisão automática de pull request classificando PRs para aprovação ou revisão humana
Resposta rápida

Revisão automática de pull request aqui não é um bot que escreve comentário bonito: é um classificador. Você monta um estado enxuto com metadados do PR (via gh pr view --json), lista de arquivos tocados (via gh pr diff --name-only) e um resumo do diff, manda pro Jev, o modelo de decisão tipada da TypeSafe AI, e recebe de volta valor estruturado com probabilidade e confiança. A partir daí você roteia: aprovar automaticamente, pedir revisão humana ou bloquear. O post monta o projeto passo a passo e mostra onde essa triagem quebra.

Fala aí, beleza? Se a fila de pull request do teu time parece infinita, o problema quase nunca é a quantidade: é que a maior parte dela é bump de dependência, ajuste de texto e teste novo, e o revisor humano gasta a atenção dele ali em vez de gastar no PR que mexe no fluxo de pagamento

A ideia deste projeto é simples: colocar um triador na frente da fila

E o candidato natural pra isso não é um LLM que escreve parecer

É o Jev, o primeiro modelo System One da TypeSafe AI, anunciado em setembro de 2026. Ele não gera texto: recebe um estado e perguntas tipadas, e devolve valores estruturados com probabilidade e confiança

Ou seja, ele não te conta uma história sobre o teu PR, ele te devolve uma decisão que teu código consegue ler num if

Bora montar? O objetivo é classificar cada PR em três rotas: aprovar automaticamente, pedir revisão ou bloquear

O que você precisa antes de começar

A lista é curta, e isso é parte da graça

  • Python 3.10 ou superior, que é o mínimo exigido pelo SDK
  • SDK instalado: pip install typesafe-sdk (ou uv add typesafe-sdk se tu já vive no uv)
  • Chave na variável de ambiente TYPESAFE_API_KEY
  • GitHub CLI autenticado, porque todo o estado do PR vai sair de gh pr view e gh pr diff

Se tu preferir não usar o SDK, dá pra falar HTTP direto com a API System One:

curl -X POST https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "state": { "titulo": "chore: bump eslint" },
    "model": "jev-latest",
    "questions": { }
  }'

Toda requisição tem os mesmos três campos de topo: state (o conteúdo a avaliar), model e questions (o mapa de perguntas tipadas)

O Jev também está disponível via OpenRouter, com as entradas typesafe/jev-1.13 e jev-latest, caso tua stack já roteie tudo por lá

E tem um atalho pra quem vive dentro de agente de código: a TypeSafe publica uma skill oficial, e a doc dela lista os comandos. No Claude Code é claude plugin marketplace add typesafe-ai/skills e depois claude plugin install typesafe@typesafe-ai, e o plugin é invocado por /typesafe:typesafe-ai. Em outros agentes, npx skills add typesafe-ai/skills --skill typesafe-ai

A instalação é project-local por padrão, e tem -g pra global, tudo conforme a mesma doc da skill. Se tu já brincou de montar agentes no Claude Code, o fluxo é exatamente o mesmo de sempre

Formação Claude Code
Formação Recomendada

Formação Claude Code

Domine Claude Code do absoluto zero até o avançado

  • 120 aulas
  • 4 projetos
  • 9h 45min

Passo a passo: do pull request à decisão tipada

A regra de ouro do projeto inteiro: o modelo só é tão bom quanto o estado que tu monta

A doc é bem direta nisso, a acurácia cai conforme o estado se enche de conteúdo irrelevante, então a filtragem é feita no teu código, antes da chamada

  1. Extrair os metadados do PR
gh pr view 482 --json title,body,labels,author,baseRefName,changedFiles,additions,deletions,files

Esse JSON já te dá quase tudo que interessa pra uma triagem: o que o PR diz que faz, quem abriu, pra qual branch vai e o tamanho da mudança

O erro comum deste passo: mandar o body inteiro cru pro estado. Descrição de PR costuma vir com template gigante, checklist e link de ticket, e isso é peso morto pro modelo

  1. Puxar a lista de arquivos tocados, já filtrada
gh pr diff 482 --name-only \
  --exclude '*.generated.*' \
  --exclude '*.lock'

A flag --exclude aceita glob e é repetível, então tu corta arquivo gerado e lockfile antes de qualquer coisa chegar na API

O erro comum deste passo: deixar lockfile passar. Um package-lock.json sozinho consegue inflar o diff e empurrar a decisão pro lado errado, porque o modelo vê um volume de mudança que não representa risco nenhum

  1. Montar o state como objeto estruturado

O state aceita string, objeto, array ou null, e a doc recomenda objeto estruturado pra qualquer requisição não trivial

state = {
    "pr": {
        "titulo": pr["title"],
        "branch_destino": pr["baseRefName"],
        "labels": [l["name"] for l in pr["labels"]],
        "linhas_adicionadas": pr["additions"],
        "linhas_removidas": pr["deletions"],
        "arquivos_alterados": pr["changedFiles"],
    },
    "arquivos": arquivos_filtrados,          # saída do gh pr diff --name-only
    "resumo_diff": resumo_por_arquivo,       # hunks encurtados, sem o diff inteiro
    "contexto_do_repo": {
        "areas_criticas": ["src/auth/", "src/billing/", "db/migrations/"],
    },
}

Repara no contexto_do_repo: é ali que entra o histórico que TU já tem, tipo o mapa de áreas que já deram incidente. O modelo não adivinha a convenção do teu monorepo, tu precisa contar

O erro comum deste passo: mandar número ou booleano puro como state. Isso retorna 422, o tipo simplesmente não é aceito. O estado é string, objeto, array ou null, e ponto

E tem o orçamento de contexto: no jev-1.13 são 64k tokens pra estado mais todas as perguntas juntas, e 32k pra estado mais a pergunta mais longa. Passou disso, a requisição falha

  1. Escrever as perguntas tipadas, todas na mesma requisição

Esse é o pulo do gato: o modelo ingere o estado UMA vez e avalia todas as perguntas em paralelo

from typesafe_sdk import Choice, Noul, Score, TypeSafeClient

questions = {
    "rota": Choice(
        instructions=(
            "Classifique este pull request quanto à necessidade de revisão humana, "
            "considerando os arquivos tocados em `arquivos`, o resumo em `resumo_diff` "
            "e as áreas listadas em `contexto_do_repo.areas_criticas`"
        ),
        criteria={
            "aprovar_auto": "mudança de baixo impacto: documentação, testes, bump de dependência sem breaking change",
            "pedir_revisao": "mudança de lógica de produto que não toca área crítica",
            "bloquear": "toca área crítica, remove teste, altera migração ou mexe em permissão",
        },
    ),
    "toca_area_sensivel": Noul(
        instructions="Algum arquivo em `arquivos` pertence a autenticação, migração de banco ou pagamento?"
    ),
    "risco": Score(
        instructions="Avalie o risco de este PR quebrar produção se for para main sem revisão humana"
    ),
}

Três detalhes que mudam o desenho:

  • Choice devolve a opção de maior probabilidade mais todas as opções mapeadas em probabilidades que somam 1, com limite de 255 opções
  • Noul é o sim/não: o valor vai de 0 a 1 e representa a probabilidade de a resposta ser sim, e ele não tem campo de confiança separado (Choice e Score têm)
  • Score existe, mas a própria doc de jaggedness registra que os níveis de Score do jev-1.13 são fracos em calibração numérica. Trate como sinal de apoio, nunca como o número que decide sozinho

O erro comum deste passo: escrever instructions pedindo uma coisa e criteria pedindo outra. Instrução e critério brigando é uma limitação documentada do modelo, e o resultado é decisão instável entre requisições parecidas

  1. Chamar a API
client = TypeSafeClient()   # usa TYPESAFE_API_KEY do ambiente

resposta = client.system_one(state=state, questions=questions)

Tem AsyncTypeSafeClient pra versão assíncrona, se o teu triador roda dentro de um serviço que já é async

  1. Ler a resposta e aplicar o roteamento por confiança

As respostas voltam sob as MESMAS chaves que tu escolheu (rota, toca_area_sensivel, risco), e Choice e Score trazem um confidence entre 0 e 1 derivado da distribuição de probabilidade

Aqui entra o padrão que a doc chama de confidence-gated routing, em três faixas:

Faixa de confiança O que a doc indica fazer Como isso vira ação no PR
Alta agir automaticamente aplica a rota que o modelo escolheu
Média seguir com cautela aplica, mas só a rota conservadora (pedir revisão)
Baixa não agir, mandar pra uma pessoa ou outro sistema cai em revisão humana, sem label automático

E o ponto que mais importa: o threshold é diferente por ação, conforme a consequência do erro

Errar pro lado de "pedir revisão" custa alguns minutos de alguém. Errar pro lado de "aprovar automaticamente" custa um deploy quebrado. Então o piso de confiança pra aprovar sozinho tem que ser o mais duro do projeto inteiro

O erro comum deste passo: usar um threshold único pras três rotas. Funciona nos testes, morre no primeiro PR ambíguo

  1. Devolver o resultado pro PR

A saída final é comentário ou label. Label é melhor: dá pra filtrar a fila por ela, dá pra medir depois, e não polui a timeline do PR

O erro comum deste passo: ligar merge automático no dia um. Não faça isso ainda, eu explico no fim do post

Onde esse triador rende mais (e onde não vale a pena)

O cálculo é bem favorável quando o volume existe

O Jev 1.13 no OpenRouter custa US$ 0,042 por 1 milhão de tokens de entrada e US$ 0,00 por 1 milhão de tokens de saída, e a TypeSafe estima custo médio em torno de US$ 0,0004 por decisão nos benchmarks publicados dela

A latência fica entre 70 e 500 ms ponta a ponta, o que significa que o triador cabe dentro de um step de CI sem ninguém reclamar do tempo de build 🙂

Rende MUITO em:

  • Repositório com enxurrada de PR de dependência e documentação, onde a maior parte da fila é ruído
  • Monorepo com áreas críticas mapeadas por caminho, porque aí tu passa areas_criticas no estado e o modelo tem em que se apoiar
  • Priorização de fila, ordenando o que o humano olha primeiro em vez de bloquear qualquer coisa
  • Gate em branch de release, onde a rota "bloquear" vira um sinal explícito de que aquilo precisa de olho humano antes do corte

Se tu quer aplicar isso em escala de organização e não em um repo só, vale ver como ativar checagens só nos repos certos antes de sair ligando em tudo

Rende pouco em:

  • Time pequeno com poucos PRs por semana, onde o revisor já lê tudo em dez minutos e o triador só adiciona peça pra manter
  • Repositório sem convenção de caminho, onde src/ tem de tudo e não existe área crítica identificável. Sem convenção, o estado fica pobre e a decisão fica no chute

Problemas comuns nessa integração e como resolver

Sintoma: a requisição falha em PRs grandes

Causa: tu mandou o diff inteiro. O orçamento é 64k tokens pra estado mais todas as perguntas, e 32k pra estado mais a pergunta mais longa

Solução: resumir o diff em código antes da chamada, guardando por arquivo só o que a pergunta precisa (caminho, tamanho da mudança, trechos curtos das áreas sensíveis)

Como prevenir: medir o tamanho do estado antes de enviar e cortar por prioridade, e não por ordem alfabética de arquivo. Lembrando que isso não é só evitar erro: a acurácia cai conforme o estado se enche de conteúdo irrelevante

Sintoma: 422 na resposta

Causa: state em tipo inválido. Número ou booleano puro não passa

Solução: embrulhar em objeto, sempre. Mesmo que tu esteja avaliando uma coisa só

Como prevenir: padronizar um builder de estado no projeto, com a serialização acontecendo em um lugar só

Sintoma: a mesma classe de PR cai ora em pedir_revisao, ora em bloquear

Causa: instructions e criteria pedindo coisas diferentes, que é uma confusão documentada do modelo

Solução: deixar a instrução como enquadramento (o que avaliar, olhando quais campos) e os critérios como a régua de cada opção, sem repetir regra nos dois

Como prevenir: revisar a pergunta como se fosse código: instrução e critério são responsabilidades separadas

Sintoma: um PR nitidamente arriscado volta como aprovar_auto

Causa: conteúdo do próprio diff ou da descrição empurrando a resposta. O estado não é tratado como hostil por padrão, e conteúdo adversarial consegue mover o resultado

Solução: nunca colocar body do PR e comentário de código no estado sem sanitização, e manter a decisão de "aprovar automaticamente" amarrada a sinais que vêm de metadado (caminho de arquivo, label, branch), não de texto livre escrito por quem abriu o PR

Como prevenir: pensar o estado como input de usuário. Porque é exatamente isso que ele é 😀

Sintoma: a pergunta "não toca autenticação?" devolve o oposto do esperado

Causa: o jev-1.13 lê a pergunta de forma bastante literal, e escopo, negações e condições implícitas vão ao pé da letra

Solução: escrever pergunta afirmativa e direta, sem negação e sem "exceto quando". Se precisa de duas condições, faça duas perguntas (elas rodam em paralelo de qualquer jeito)

Até onde dá para confiar: os limites de julgar um PR sem ler o código inteiro

Agora o veredito honesto, porque prometi isso no começo

No benchmark interno da TypeSafe, feito sobre 4 fluxos de produção, o Jev marcou 67,8% de acurácia. Vale dizer com todas as letras: é benchmark do próprio fabricante, e não existe métrica pública de acurácia do Jev especificamente em code review ou análise de diff

Ou seja, tu não sabe qual é o número NA TUA tarefa. Tu só sabe que não é 100%

Soma a isso as limitações que a própria TypeSafe registra na página de jagged edges do jev-1.13: dificuldade com tarefas que exigem níveis extras de indireção, leitura bastante literal da pergunta e dificuldade com precisão numérica

Indireção é justamente o coração do code review

"Essa função mudou de assinatura, quem chama ela quebra?" é uma pergunta de dois saltos, e você não colocou os chamadores no estado, porque não caberia. Então o modelo está julgando a SUPERFÍCIE do PR: onde mexeu, quanto mexeu, o que o repo diz sobre aquela área

É triagem e priorização

Não é revisor

A postura que sai disso é bem concreta: aprovar_auto fica com o threshold mais duro do projeto, e baixa confiança cai em revisão humana por padrão, sempre. O custo de errar pra mais é uma pessoa olhando um PR bobo; o custo de errar pra menos é produção

Um triador que acerta a maioria e manda o resto pro humano já devolve a tarde do teu time. Um triador que aprova sozinho pra parecer esperto devolve incidente…

Próximo passo

Recapitulando o desenho, que é o que realmente importa aqui:

estado enxuto (filtrado em código, nunca o diff cru) + perguntas tipadas na mesma requisição (Choice pra rota, Noul pro sim/não, Score só como apoio) + roteamento por confiança com threshold diferente por ação

O resto é encanamento de gh e de label

E o próximo passo prático, antes de ligar QUALQUER automação: roda em modo sombra por algumas semanas. O triador decide, grava a decisão num log, e não faz nada no PR. Aí tu compara com o reviewDecision real, que também sai do gh pr view --json

Depois de algumas dezenas de PRs tu vai ter a tua própria taxa de acerto, na tua base, com a tua convenção de pastas. Essa é a única acurácia que vale pra decidir se o "aprovar automaticamente" pode existir no teu repo

Pra afinar o uso do modelo dentro de agente de código, as skills oficiais estão em typesafe-ai/skills

Até o próximo post! =)

Perguntas frequentes

Quanto custa rodar revisão automática de pull request com o Jev em volume alto?

A TypeSafe estima um custo médio bem baixo por decisão nos benchmarks publicados dela, e os preços por token do jev-1.13 no OpenRouter estão na seção de custo aqui do post. Pra um fluxo de triagem de PR, que manda um estado enxuto por chamada, isso fica bem baixo comparado a rodar um LLM generativo pra escrever parecer.

Dá pra avaliar vários critérios do mesmo pull request numa chamada só?

Dá sim. A API ingere o state uma única vez e avalia todas as questions em paralelo, então rota de aprovação, se toca área sensível e outros checks podem ir juntos na mesma requisição. É exatamente esse desenho que sustenta o passo 4 do projeto, com Choice e Noul na mesma chamada.

Posso confiar na decisão do Jev sem nenhuma revisão humana no meio?

Não é essa a proposta. A doc descreve um padrão de confidence-gated routing em três faixas (alta, média, baixa), com threshold por ação conforme a consequência do erro. E, como mostro na seção sobre os limites, o único número de acurácia que existe vem do benchmark do próprio fornecedor, o que reforça manter a faixa baixa sempre indo pra um humano.

Qual a diferença entre usar Choice e Noul pra decidir a rota de um PR?

Choice devolve a opção de maior probabilidade entre até 255 alternativas, com a distribuição completa somando 1 e um confidence de 0 a 1. Noul é a pergunta binária: um valor de 0 a 1 representando a probabilidade de sim, sem campo de confidence separado, diferente de Choice e Score.

O que acontece se o diff do pull request for grande demais pro modelo avaliar?

O jev-1.13 tem orçamento de 64k tokens pra estado mais todas as perguntas juntas, e 32k pra estado mais a pergunta mais longa. Passar desse limite faz a requisição falhar, por isso o passo de filtrar arquivos com gh pr diff –name-only e –exclude antes de montar o state é tão importante.

Existe algum tipo de PR que costuma confundir o Jev na hora de classificar?

Sim, a documentação lista jagged edges do jev-1.13: leitura bastante literal de negações e condições implícitas, dificuldade com níveis extras de indireção, precisão numérica fraca (inclusive na calibração do Score) e o estado não é tratado como hostil por padrão. Vale considerar isso ao escrever as instructions de cada Choice.



Escrito por | Matheus Battisti

Matheus Battisti
Fundador da Hora de Codar

Programador apaixonado pelo mundo das tecnologias, sempre buscando em aprender e se aprofundar em linguagens, frameworks e o que mais for necessário para executar um bom trabalho. Agora tem uma nova missão que é de passar seu conhecimento adiante para formar novos programadores e especializar mais os que já são.

Subscribe
Notify of
guest

0 Comentários
Oldest
Newest Most Voted

Formações

Formação Vibe Coding

Formação Vibe Coding

Do Prompt ao Produto: Crie Software Real com IA

  • 474 aulas
  • 20 projetos
  • 39h 27min

Blog | Mais populares