Como extraímos o “Why” de um PR para o CHANGELOG com Jev? 🤔
Hoi hoi! 👋
Eu sou @nyaomaru, engenheiro frontend, atualmente desenvolvendo um gerador de CHANGELOG assistido por IA e experimentando o Jev. 😸
Recentemente, fiquei pensando em um problema.
Como podemos extrair isto:
## Why
The previous implementation could leave stale release data after an update.
de um pull request e colocá-lo no CHANGELOG?
### Fixed
- Fix cache invalidation
Why: The previous implementation could leave stale release data after an update.
Obter o WHAT é relativamente fácil.
Obter o WHY é muito mais difícil.
E eu não queria que a IA simplesmente inventasse uma justificativa que parecesse boa.
Vamos ver como o changelog-bot lida com isso atualmente! 👀
https://github.com/nyaomaru/changelog-bot
📝 WHAT é fácil. WHY é difícil.
Suponha que um PR contenha:
## Description
Extract shared runtime configuration into ProviderBase.
Update OpenAI, Anthropic, and Gemini adapters to use the shared implementation.
This reduces duplicated setup logic and makes adding new providers easier.
As duas primeiras frases descrevem principalmente WHAT e HOW.
Mas esta é diferente:
This reduces duplicated setup logic and makes adding new providers easier.
Ela está muito mais próxima do WHY.
Poderíamos simplesmente enviar o PR inteiro para um LLM e perguntar:
Why was this change made?
Mas então o modelo pode resumir, parafrasear ou inferir uma justificativa que nunca foi escrita explicitamente.
Para um CHANGELOG, eu queria algo mais rigoroso.
Talvez extrair o WHY não seja um problema de geração.
Talvez seja um problema de seleção.
✂️ Primeiro, encontre possíveis candidatos a WHY
Antes de chamar um modelo, o changelog-bot pré-processa a descrição do PR.
Ele procura sinais como:
const STRONG_CANONICAL_SECTION_NAMES = new Set([
"why",
"reason",
"because",
"motivation",
"context",
"background",
"problem",
"rationale",
]);
const RATIONALE_MARKER_RE =
/\b(why|because|so that|in order to|reason|rationale|motivation|to avoid|to prevent|context|problem)\b|,\s*so\b/i;
Ele também remove ruído de templates, como checkboxes, placeholders e campos irrelevantes.
Depois, cria pequenos snippets candidatos:
const candidates = buildWhyCandidateSnippets(
extracted.sections,
body,
options.maxCharsPerPr,
!extracted.hasTargetSection,
);
Então, em vez de enviar um corpo enorme do PR, podemos acabar com algo como:
[
"Extract shared runtime configuration into ProviderBase.",
"This reduces duplicated setup logic and makes adding new providers easier.",
];
Agora o problema é muito menor.
Qual candidato é realmente o WHY?
🧠 Fazendo duas perguntas ao Jev
Foi aqui que comecei a experimentar o Jev.
Eu não peço ao Jev para escrever uma frase de WHY.
Em vez disso, cada candidato é avaliado com duas perguntas.
Primeiro:
question: "Does the target candidate explicitly state why this changelog change was made?";
Basicamente:
Isto é realmente uma justificativa?
Mas isso, sozinho, não basta.
Imagine:
We changed the cache invalidation logic.
The README was updated because the previous explanation was confusing.
A segunda frase claramente contém uma justificativa.
Mas ela explica a mudança no README, e não a mudança na invalidação de cache.
Então também perguntamos:
question: "Does the target candidate state a reason that applies to the identified changelog change?";
Agora cada candidato possui dois sinais:
{
probability: explicitWhyProbability,
relevanceProbability: changeRelevanceProbability,
}
Então a pergunta real passa a ser:
Isto é uma justificativa?
E
É a justificativa desta mudança?
🎯 Selecionando o candidato
O changelog-bot compara as probabilidades dos candidatos:
const bestCandidate = candidateProbabilities.reduce((best, candidate) =>
combinedWhyCandidateProbability(candidate) >
combinedWhyCandidateProbability(best)
? candidate
: best,
);
Depois, ambos os thresholds mínimos precisam ser atingidos:
const accepted =
selectedCandidate !== undefined &&
bestCandidate.probability >= JEV_MIN_EXPLICIT_WHY_PROBABILITY &&
bestCandidate.relevanceProbability >= JEV_MIN_CHANGE_RELEVANCE_PROBABILITY;
Se o candidato não passar:
if (!accepted) continue;
Nenhum WHY é adicionado.
E eu acho isso importante.
Às vezes, a resposta correta é:
There is no reliable WHY in this PR.
Isso é melhor do que inventar um.
🔒 Mantendo o texto original do PR
Esta provavelmente é minha parte favorita.
Quando um candidato é aceito:
items.push({
prNumber: item.prNumber,
why: selectedCandidate,
confidence: mappedConfidence,
});
Repare:
why: selectedCandidate;
O WHY final é o texto original do PR.
Não é uma nova frase gerada pela IA.
Então o pipeline fica assim:
PR description
↓
candidate extraction
↓
Jev evaluates candidates
↓
select candidate
↓
original source text
↓
CHANGELOG
O Jev decide.
Mas a frase continua vindo do autor do PR. 😸
🧱 Onde o WHY entra no pipeline v1?
Enquanto trabalhava na extração de WHY, também comecei a redesenhar o pipeline do changelog-bot para a v1.
A ideia principal é deterministic-first.
Um CHANGELOG completo deve existir mesmo quando a IA está desativada ou falha.
Portanto, a IA deve enriquecer o resultado, e não ser dona do resultado.
O pipeline alvo da v1 se parece, aproximadamente, com isto:
source changes
↓
ReleaseChange[]
↓
deterministic classification
↓
complete ReleaseDraft
↓
optional editorial enrichment
+
optional WHY enrichment
↓
ReleaseResult
↓
deterministic renderer
↓
CHANGELOG
Assim, a extração de WHY é uma etapa opcional e independente de enrichment.
Conceitualmente:
ReleaseDraft / ReleaseChange[]
↓
find eligible WHY targets
↓
collect PR rationale candidates
↓
LLM or Jev
↓
trust / confidence validation
↓
attach structured WHY notes
↓
ReleaseResult
Uma mudança importante aqui é que o engine de WHY não precisa controlar o Markdown final.
Hoje, algumas partes do pipeline ainda encontram targets a partir do Markdown já gerado e inserem o texto de WHY depois.
Na v1, quero remover essa fronteira.
Em vez disso:
{
prNumber,
why: selectedCandidate,
confidence,
}
primeiro se transforma em dados estruturados do release.
Depois, um único renderer determinístico decide como isso será exibido:
- Fix cache invalidation
- Why: The previous implementation could leave stale release data after an update.
Assim, a responsabilidade fica mais clara:
WHY engine
→ encontrar a evidência
Renderer
→ formatar a evidência
Isso também faz parte de uma regra maior que estou usando na migração para a v1:
Código determinístico é responsável pela estrutura do CHANGELOG. A IA apenas a enriquece.
As fases 1–4 dessa migração já foram implementadas, e a fronteira entre structured rendering e enrichment faz parte da próxima fase.
Você pode ver o design completo da v1 aqui 👇
https://github.com/nyaomaru/changelog-bot/blob/main/v1-design.md
🧪 Mas qual threshold devemos usar?
Claro que, quando temos probabilidades, surge outra pergunta:
0.50?
0.60?
0.80?
Eu não queria escolher um valor só porque parecia razoável.
Então criei um evaluation harness com um corpus inicial de 14 casos.
Por exemplo:
{
candidates: [
"Separate changelog execution into input-resolution and output-finalization phases.",
"This simplifies the main orchestration flow and runs independent GitHub lookups concurrently.",
],
expectedSelectedCandidateIndex: 1,
}
Também precisamos de exemplos negativos:
{
candidates: [
"Extract shared provider runtime configuration into ProviderBase.",
"Refactor OpenAI, Anthropic, and Gemini adapters to inherit it.",
],
expectedSelectedCandidateIndex: null,
}
Há bastante informação sobre implementação.
Mas nenhum WHY explícito forte.
Então null é o resultado correto.
🤝 Então John entrou no projeto
Enquanto eu trabalhava nesse experimento, @johnnylemonny entrou no projeto como contributor. 🎉
John propôs expandir o pequeno benchmark para cerca de 50 casos cobrindo:
Explicit Rationale
Implementation-Only
Template & Noise
Multilingual PRs
Gostei da ideia, mas queria manter uma regra bem clara:
O ground truth deve ser independente do threshold atual do Jev.
Então o benchmark primeiro define se um candidato é realmente válido.
Depois avaliamos diferentes thresholds.
John implementou o corpus expandido no PR #212. 🙌
O benchmark cresceu de 14 para 50 casos, incluindo casos negativos complicados.
Ele também adicionou sweeps de threshold mais detalhados:
[0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9];
Agora podemos comparar precision, recall e F1 em vez de dizer:
0.8 parece seguro! 👍
Melhor medir. 😸
🧩 O pipeline atual
Hoje, a extração de WHY funciona aproximadamente assim:
Pull Request
↓
Normalize PR body
↓
Extract WHY candidates
↓
Remove noise
↓
Local trust check
↓
Jev:
explicit rationale?
relevant to this change?
↓
Apply threshold
↓
Candidate or none
↓
Original PR text
↓
CHANGELOG
São muitas etapas para uma pequena linha Why:. 😂
Mas foi exatamente isso que tornou esse problema interessante.
🎯 O que aprendi
Quando comecei essa feature, achei que a pergunta principal era:
How can AI write a good reason?
Agora acho que a pergunta mais interessante é:
How can we find evidence that the reason already exists?
Então minha abordagem atual é simples:
Use deterministic code to find evidence.
Use Jev to evaluate the evidence.
Keep the original evidence as the output.
E, graças às contribuições do John, agora temos uma forma muito melhor de testar se essa abordagem realmente funciona.
Open source é divertido quando alguém olha para seu experimento e diz:
E se a gente testar isso direito?
E então constrói isso junto com você. 🙌
O changelog-bot continua evoluindo, e o suporte ao Jev ainda é experimental.
Estou curioso para ver até onde essa abordagem pode chegar!
https://github.com/nyaomaru/changelog-bot
Obrigado por ler!
Até o próximo artigo! 👋😸