Skip to content

Conteúdo/Treino Livre: "treinar de novo" repete o snippet recém-digitado — o fix da #115 não alcançou /api/snippet (snippet/route.ts:26) #121

Description

@caioross

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 #11520% 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/snippet aceita um parâmetro opcional de exclusão (ex.: exclude=<título>) e o
    repassa como 3º argumento de pickSnippet. Ausente → comportamento de hoje, inalterado.
  • PracticeView envia o título do snippet anterior somente quando language e difficulty
    nã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.
  • Primeiro carregamento da página (sem snippet anterior) não manda exclude.
  • Clicar "treinar de novo" no mesmo bucket nunca devolve o título recém-digitado — salvo a
    degradação já documentada em pickSnippet (bucket de 1, ou títulos repetidos dentro do
    bucket), em que repetir é o comportamento correto em vez de falhar.
  • Fronteira defensiva na rota, no mesmo espírito de resolveLang/resolveDifficulty: teto de
    tamanho 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.
  • Cobertura em src/lib/snippets.test.ts para a regra nova, com o teste falhando sem a
    correçã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 test verde.
  • Docstring de pickSnippet (snippets.ts:3699-3701) atualizado — a frase sobre o Treino
    Livre 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 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Prioridade normalarea:contentSnippets de código, linguagens, dificuldadesarea:engineMotor de digitação, WPM, precisão, anti-cheat

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions