Contexto
A #115 tirou da urna o snippet recém-digitado — mas só no caminho da sala. O Treino Livre
(/practice) continua sorteando com o pool inteiro, então "treinar de novo" pode devolver
exatamente o trecho que o jogador acabou de terminar.
Conferido em origin/main (7197db1), os três elos:
| Caminho |
Chamada |
Exclui? |
| sala / revanche |
pickSnippet(room.language, room.difficulty, room.snippet?.title) (src/app/api/rooms/[code]/route.ts:152) |
✅ |
| Treino Livre |
pickSnippet(lang.value, difficulty.value) (src/app/api/snippet/route.ts:26) |
❌ |
E o cliente nem tem como pedir: PracticeView.tsx:43 monta a query só com language e
difficulty, e o botão "treinar de novo" (PracticeView.tsx:177) rechama o mesmo
loadSnippet(language, difficulty) — não há canal para o título anterior.
O terceiro argumento já existe e já é opcional (snippets.ts:3702-3705), com a exclusão e a
degradação de bucket pequeno já resolvidas e cobertas por snippets.test.ts. O docstring de
pickSnippet (snippets.ts:3699-3701) registra explicitamente que o Treino Livre "segue chamando
com dois argumentos" — esta issue completa aquela decisão, não a contradiz (o docstring deve
ser atualizado junto).
Por que dói mais aqui que na sala
Probabilidade idêntica à da #115 — 20% nos buckets de 5 (JS/Júnior), 33% nos de 3 — mas o
Treino Livre é justamente onde se clica "de novo" em sequência para caçar PB. Na segunda passada o
jogador não está digitando, está recitando de memória: o WPM sobe sem a habilidade subir, e é
esse número inflado que ele leva para a sala depois. Repetir no modo de treino corrompe justamente
a métrica que o modo existe para medir.
Reproduzido pela persona O Competitivo na produção real em 01/08 (JS/Júnior, snippet "Inverter
string" repetido em 1 clique) — Discussion #120,
onde é o pedido nº 1 da semana. ICE = 4 × 5 × 5 = 100.
Acceptance criteria
Dica de abordagem
A parte com regra de negócio é o "só exclui no mesmo bucket", e ela está hoje num componente
de cliente, onde teste não alcança. Extraia-a para uma função pura em src/lib/snippets.ts
(ex.: exclusionFor(prev: {lang, diff, title} | null, lang, diff): string | null) e deixe o
PracticeView só guardar o último {lang, diff, title} num useRef e chamar o helper — assim a
regra inteira cai dentro de snippets.test.ts sem teste de componente.
Cuidado com o fetchSeq de PracticeView.tsx:33-34: o "último título" precisa ser gravado no
mesmo ponto em que a resposta é aceita (PracticeView.tsx:52-53), nunca na resposta descartada
por estar fora de ordem.
Escopo / fluxo
Não toca src/app/api/rooms/**, nem persistência, nem banco, nem anti-cheat → HANDBOOK §7.3
(fluxo normal, PR non-draft sem quórum). Rota só-leitura, sem efeito no leaderboard.
Contexto
A #115 tirou da urna o snippet recém-digitado — mas só no caminho da sala. O Treino Livre
(
/practice) continua sorteando com o pool inteiro, então "treinar de novo" pode devolverexatamente o trecho que o jogador acabou de terminar.
Conferido em
origin/main(7197db1), os três elos:pickSnippet(room.language, room.difficulty, room.snippet?.title)(src/app/api/rooms/[code]/route.ts:152)pickSnippet(lang.value, difficulty.value)(src/app/api/snippet/route.ts:26)E o cliente nem tem como pedir:
PracticeView.tsx:43monta a query só comlanguageedifficulty, e o botão "treinar de novo" (PracticeView.tsx:177) rechama o mesmoloadSnippet(language, difficulty)— não há canal para o título anterior.O terceiro argumento já existe e já é opcional (
snippets.ts:3702-3705), com a exclusão e adegradação de bucket pequeno já resolvidas e cobertas por
snippets.test.ts. O docstring depickSnippet(snippets.ts:3699-3701) registra explicitamente que o Treino Livre "segue chamandocom dois argumentos" — esta issue completa aquela decisão, não a contradiz (o docstring deve
ser atualizado junto).
Por que dói mais aqui que na sala
Probabilidade idêntica à da #115 — 20% nos buckets de 5 (JS/Júnior), 33% nos de 3 — mas o
Treino Livre é justamente onde se clica "de novo" em sequência para caçar PB. Na segunda passada o
jogador não está digitando, está recitando de memória: o WPM sobe sem a habilidade subir, e é
esse número inflado que ele leva para a sala depois. Repetir no modo de treino corrompe justamente
a métrica que o modo existe para medir.
Reproduzido pela persona O Competitivo na produção real em 01/08 (JS/Júnior, snippet "Inverter
string" repetido em 1 clique) — Discussion #120,
onde é o pedido nº 1 da semana. ICE = 4 × 5 × 5 = 100.
Acceptance criteria
GET /api/snippetaceita um parâmetro opcional de exclusão (ex.:exclude=<título>) e orepassa como 3º argumento de
pickSnippet. Ausente → comportamento de hoje, inalterado.PracticeViewenvia o título do snippet anterior somente quandolanguageedifficultynão mudaram. Trocar de bucket não pode carregar a exclusão: títulos se repetem entre
linguagens (ex.: "Inverter string" existe em vários pools), e excluir entre buckets tiraria
da urna um snippet que o jogador nunca viu.
exclude.degradação já documentada em
pickSnippet(bucket de 1, ou títulos repetidos dentro dobucket), em que repetir é o comportamento correto em vez de falhar.
resolveLang/resolveDifficulty: teto detamanho para o valor de
exclude(ex.: 120 chars) e nenhum eco do parâmetro na resposta.Valor desconhecido não é erro — o filtro simplesmente não casa e a urna fica cheia.
src/lib/snippets.test.tspara a regra nova, com o teste falhando sem acorreção (como foi feito na Conteúdo/Revanche: "Jogar de novo" pode sortear o MESMO snippet — 20% (33% em 10 linguagens) de repetir o código recém-digitado, com o WPM memorizado indo ao ranking (snippets.ts:3668-3680) #115).
pnpm testverde.pickSnippet(snippets.ts:3699-3701) atualizado — a frase sobre o TreinoLivre chamar com dois argumentos deixa de valer.
Dica de abordagem
A parte com regra de negócio é o "só exclui no mesmo bucket", e ela está hoje num componente
de cliente, onde teste não alcança. Extraia-a para uma função pura em
src/lib/snippets.ts(ex.:
exclusionFor(prev: {lang, diff, title} | null, lang, diff): string | null) e deixe oPracticeViewsó guardar o último{lang, diff, title}numuseRefe chamar o helper — assim aregra inteira cai dentro de
snippets.test.tssem teste de componente.Cuidado com o
fetchSeqdePracticeView.tsx:33-34: o "último título" precisa ser gravado nomesmo ponto em que a resposta é aceita (
PracticeView.tsx:52-53), nunca na resposta descartadapor estar fora de ordem.
Escopo / fluxo
Não toca
src/app/api/rooms/**, nem persistência, nem banco, nem anti-cheat → HANDBOOK §7.3(fluxo normal, PR non-draft sem quórum). Rota só-leitura, sem efeito no leaderboard.