Repository navigation
Description
O editor da corrida bloqueia paste (onPaste → preventDefault, TypingCore.tsx:noPaste) e a barra de status promete isso em voz alta: "paste off · backspace não pontua" (CodeEditor.tsx:341). Mas a textarea não trata drop — e o snippet-alvo está renderizado, selecionável, ao lado, no mesmo viewport.
O vetor (1 gesto de mouse, sem devtools)
CodeDisplaydesenha o alvo como texto normal em<span>s (CodeDisplay.tsx:74-92) — semuser-select: none, e não há regra global de seleção (globals.cssnão temuser-select/drag).- O jogador seleciona o código-alvo com o mouse e arrasta para dentro da
textarea(CodeEditor.tsx:305-327). - O browser insere o texto solto e dispara
input/change—onPastenunca roda, porquedropé outro evento. handleInput(TypingCore.tsx:104-127) vênext.length > typed.length, compara char-a-char contracode, não acha nenhuma divergência: soma N acertos, 0 erros, ecomputeProgressvai a 100% de uma vez. Resultado: precisão 100%, WPM proporcional ao tempo decorrido — e no multiplayer o progresso vai porbroadcastProgresscomo se tivesse sido digitado.
Não é o mesmo buraco da #34 (coerência temporal do finish no servidor) nem da #28 (teto de plausibilidade no leaderboard): essas são a rede de segurança server-side/leaderboard, e ambas ainda deixam passar o caso "arrastou e depois esperou" — dentro do plausível temporal, com 100% de precisão. Aqui o furo é na fronteira de entrada da engine, a mesma que o onPaste já defende. Fairness na sala com amigos (o coração do produto) não tem outra defesa.
Acceptance criteria
-
dropnatextareada corrida não alteravalue:onDropeonDragOvercompreventDefault(), em paridade com oonPastejá existente (CodeEditor.tsx:313). - Repro manual documentada no PR (antes/depois): selecionar o alvo no
CodeDisplay, arrastar para o editor, soltar → texto não entra, progresso/precisão/WPM não se mexem. - Defesa em profundidade na fonte: arrastar a partir do
CodeDisplaynão inicia um drag útil (ex.:onDragStart→preventDefaultno container). Não remover a seleção do texto (user-select: noneprejudica acessibilidade e cópia legítima fora da corrida). - A promessa da barra de status continua verdadeira — se a copy mudar, que descreva o que de fato é bloqueado.
- Área sagrada intacta: nenhum trabalho novo por keystroke; os handlers adicionados só rodam em
drop/dragover.node scripts/validate-metrics.mjsverde. - Nenhuma fórmula de
src/lib/metrics.tsalterada (a engine não muda; só a fronteira de entrada).
Dica de abordagem
Menor raio possível: a textarea já recebe onPaste vindo por prop de TypingCore (noPaste). Dá para tratar drop/dragover no mesmo lugar (bloqueio local no CodeEditor, já que não há caso de uso legítimo de soltar texto no editor da corrida) ou estender a prop para um onBlockedInsert único e reusar noPaste. Preferir o que deixar um ponto de verdade sobre "o que é entrada ilegítima".
Vale um comentário curto no código explicando por que drop precisa de tratamento próprio — foi exatamente o que passou despercebido quando o onPaste foi escrito.
Activity
caioross commentedon Jul 24, 2026
🏛️ Parecer do Conselho · 2026-07-24
Veredito: APROVADA PARA EXECUÇÃO — com um corte de escopo explícito (ver Engenharia).
textarea(CodeEditor.tsx:305-327):onDrop={e => e.preventDefault()}+onDragOver={e => e.preventDefault()}. O par é necessário —sem
preventDefaultnodragoverodropnão é garantidamente entregue como evento cancelávelem todos os motores. Tem de ser na
textarea, não no card: o espelho<pre>épointer-events-none(:293) e o card temoverflow-hidden, então o alvo real do drop é só ela.Como
CodeEditoré compartilhado, a correção cobre de graça o/practice(a Practice — rota /practice com Treino Livre (sem sala), reusando Race #25/PR feat(practice): rota /practice com Treino Livre solo, sem sala (#25) #40 usa omesmo
TypingCore— ver o comentário deTypingCore.tsx:15-18).Corte de escopo (importante): a "dica de abordagem" sugere um ponto único de verdade sobre
entrada ilegítima. Não implemente isso como regra de tamanho em
handleInput(
TypingCore.tsx:100-111,added = next.length - typed.length). OTabda Engine/Input: Tab-para-indentar existe mas não insere a indentação na corrida (CodeEditor.tsx:95–115) — fricção nº1 do Iniciante #41 chamaonChange(text)com N espaços de uma vez (CodeEditor.tsx:146-150), e a Engine/Mobile: Enter não auto-indenta a linha seguinte — indentar no teclado virtual é espaço a espaço (CodeEditor.tsx:139) #62 aberta hojefará o mesmo no
Enter. Uma guarda deadded > 1quebraria a Engine/Input: Tab-para-indentar existe mas não insere a indentação na corrida (CodeEditor.tsx:95–115) — fricção nº1 do Iniciante #41 e nasceria já quebrando a Engine/Mobile: Enter não auto-indenta a linha seguinte — indentar no teclado virtual é espaço a espaço (CodeEditor.tsx:139) #62.O fix de classe existe, mas exige um canal de origem confiável (ex.:
onChange(text, {source:"indent"}))— é outra issue, maior, e não é esta. Aqui: fronteira de evento, diff de ~4 linhas.
onDragStartno container doCodeDisplay,CodeDisplay.tsx:57-92) é cosmético e não deve consumir orçamento: o snippet também estáacessível fora do
CodeDisplay(outra aba com a mesma sala,/api/snippet, view-source), e ojogador pode arrastar texto de qualquer origem — inclusive de fora do browser. Só o bloqueio
no destino (
dropnatextarea) fecha a classe inteira de origens. Faça oonDragStartsequiser, mas como cortesia, não como defesa — e sem
user-select: none, como a própria AC pede.Vale registrar a fronteira: isto é fairness client-side. Anti-cheat: finish sem coerência temporal — leaderboard forjável em 1 request e corrida alheia encerrável #34 (coerência temporal do
finish)e Anti-cheat/Leaderboard: recorde impossível (3596 WPM) no Ranking Global — sem teto de plausibilidade #28 (teto de plausibilidade) seguem sendo a rede server-side, e nenhuma das duas pega o caso
"arrastou e esperou". Não anuncie o fix como anti-cheat resolvido.
(
CodeEditor.tsx:338,"paste off · backspace não pontua"). Mantida a copy, nada muda para quemdigita: os handlers só existem em
drop/dragover. Não adicione toast nem flash de erro no dropbloqueado — silêncio é o comportamento certo aqui (o
onPastedeTypingCore.tsx:121-123tambémé silencioso; divergir criaria dois vocabulários para a mesma recusa). Teclado-primeiro intacto.
useCallback/estado novo, nenhuma dependência nova. Se optar por definir os handlers inline noJSX, note que eles já convivem com os inline existentes (
:308) — não é regressão.único contexto em que o CodeRacer é jogado hoje. Custo/benefício é o melhor da fila: ~4 linhas
contra "corrida 100% sem digitar". A prioridade P2 está subestimada — o rótulo honesto é P1
(mesma família de Anti-cheat: finish sem coerência temporal — leaderboard forjável em 1 request e corrida alheia encerrável #34/Anti-cheat/Multiplayer: kick é broadcast sem autoridade — qualquer jogador expulsa qualquer um (inclusive o líder) #39, ambas P1/P2 de fairness, e mais barata que as duas). Sugiro ao Curador
promover para P1; não estou reetiquetando por conta própria porque a ordem do Top-3 já a coloca
em 1º e a promoção não mudaria o consumo desta semana.
Plano sugerido:
auto/issue-61-*a partir deorigin/main. Fluxo §7.3 normal — não é área de quórum(
src/components/**, sem tocarapi/rooms/**nem persistência).CodeEditor.tsx:305-327— adicionar natextarea, junto deonPaste:onDragOver={e => e.preventDefault()}eonDrop={e => e.preventDefault()}. Comentário curto de2 linhas dizendo por que
dropprecisa de tratamento próprio (não épaste) — foiexatamente o que passou despercebido; o comentário é o que impede a terceira ocorrência.
onDragStart={e => e.preventDefault()}no container deCodeDisplay.tsx:57.Se custar mais que 2 linhas, deixe de fora e diga no PR — não é a defesa.
TypingCore.tsx:93-118, em@/lib/metricsnem em@/lib/indent.jsdom/RTL. O runner éenvironment: "node"cominclude: ["src/**/*.test.ts"](vitest.config.ts) — não há como testar handler de DOM hoje, ea dependência nova cairia em §7.2 por um fix de 4 linhas. A evidência é a repro manual
antes/depois no corpo do PR, como a AC já exige.
pnpm typecheck && pnpm build,pnpm test,node scripts/validate-metrics.mjsenode scripts/validate-persistence.mjs. Todos devem ficar idênticos — se algum número mudar,o diff saiu do escopo.
caioross commentedon Jul 26, 2026
Resolvida em #94 (non-draft, quórum solicitado).
Fix no destino:
onDragOver+onDropcompreventDefault()natextarea(
CodeEditor.tsx, ao lado doonPaste) — fecha a classe inteira de origens, inclusive drag defora do browser, e cobre
/practicede graça. Na fonte,onDragStartno container doCodeDisplaycomo cortesia, semuser-select: none. +9 linhas em 2 arquivos.Evidência: em produção hoje,
dragover/dropnatextareasaem comdefaultPrevented: false(vetor confirmado); no bundle deste branch os dois handlers cancelam. Drag nativo humano não é
reproduzível em headless — declarado no PR, verificação final no preview da Vercel.
Gate verde: typecheck · build · 54 testes vitest · validate-metrics 37/37 ·
validate-persistence 72/72 (lint N/A, sem config ESLint). Segui o plano do Conselho, divergindo
só na classificação: pedi quórum porque §7.2 lista "anti-cheat" sem qualificar caminho.
fix(anti-cheat): bloqueia drop na textarea da corrida — arrastar o al…