Repository navigation
Description
Contexto
A honestidade das métricas (WPM, precisão, erros) é a verdade sagrada do domínio — foi a origem do feedback do Iniciante que virou #20 ("Precisão 100%" coexistindo com "Erros 1"). Mas hoje toda a lógica de cálculo mora inline dentro do componente src/components/Race.tsx, o que a torna impossível de testar por unidade:
correctChars— loop char-a-char (Race.tsx:49-53)wpm—(correctChars / 5) / elapsedMinarredondado (Race.tsx:55)accuracy—(1 - errors/totalKeystrokes) * 100com clamp e arredondamento (Race.tsx:56-57)progress—typed.length / code.length(Race.tsx:58)- contagem de erros/keystrokes em
handleInput, incluindo a regra "backspace não conta keystroke" (Race.tsx:81-92)
Existe o script .claude/skills/cr-typing-engine/scripts/validate-metrics.mjs, mas ele valida uma cópia da lógica, não o código que roda em produção — divergência silenciosa é possível. Esta issue extrai as funções puras para src/lib/ e as cobre com testes reais, tornando a engine testável sem alterar comportamento.
Depende do runner instalado pela issue de infra de testes (Vitest) — se ela ainda não tiver mergeado, esta pode incluir a config mínima ou aguardar.
Escopo
- Criar
src/lib/metrics.tscom funções puras e determinísticas, extraídas 1:1 deRace.tsx(sem mudar fórmula nem arredondamento):countCorrectChars(typed: string, code: string): numbercomputeWpm(correctChars: number, elapsedMin: number): numbercomputeAccuracy(errors: number, totalKeystrokes: number): numbercomputeProgress(typedLen: number, codeLen: number): number
Race.tsxpassa a importar e usar essas funções (nenhuma mudança de comportamento observável — mesmos números na tela).- Suite
src/lib/metrics.test.tscobrindo as invariantes de honestidade.
Acceptance criteria (verificáveis)
-
Race.tsxnão contém mais o cálculo inline — usametrics.ts; a UI mostra exatamente os mesmos valores de antes (sem regressão visual/numérica). -
computeWpm:elapsedMin <= 0.001→0(evita divisão por ~0); conta sócorrectChars(chars errados não contam WPM). -
computeAccuracy:totalKeystrokes === 0→100; nunca retorna 100 quandoerrors > 0(a honestidade de Engine/Honestidade: "Precisão 100%" coexiste com "Erros 1" — arredondamento mascara erro e rótulo confunde #20 — o arredondamento de0.995não pode mascarar erro); clampada em[0,100]. -
countCorrectChars: só conta posições ondetyped[i] === code[i];typedmais longo quecodenão estoura. -
computeProgress:codeLen === 0→0; clampada em[0,1]. -
node scripts/validate-metrics.mjs(via skill) continua verde;pnpm testverde.
Dica de abordagem
- Extração pura, zero mudança de fórmula — copie exatamente
Race.tsx:49-58. O ganho é testabilidade e uma única fonte de verdade (a duplicata emvalidate-metrics.mjspode passar a importar demetrics.tsnuma fatia futura). - Área sagrada (HANDBOOK §2):
Race.tsxestá no caminho de input da corrida (60fps). A extração deve ser puro refactor — as funções continuam chamadas dentro deuseMemo/render como hoje, sem trabalho extra por keystroke. Não introduza alocação/loop novo no hot path. - Gate:
pnpm typecheck && pnpm build && pnpm test && node scripts/validate-metrics.mjs. PR normal (§7.3 — refactor + testes de engine com validate-metrics verde).
Activity
caioross commentedon Jul 17, 2026
🏛️ Parecer do Conselho · 2026-07-17
Veredito: APROVADA PARA EXECUÇÃO — mas a issue tem uma contradição interna que o Resolvedor precisa resolver conscientemente (ver Engenharia). Não é decisão do dono (é fix de bug já reconhecido em #20), porém muda um número visível — o PR deve declarar.
countCorrectChars/computeWpm/computeProgressé 1:1 e segura (Race.tsx:49-58): tracei os três e o comportamento atual já satisfaz seus AC (typed>code não estoura poiscode[i]===undefined;elapsedMin>0.001?…:0;codeLen===0?0, clamp[0,1]).computeAccuracy: o escopo diz "copie exatamente, zero mudança de fórmula", mas o AC diz "nunca retorna 100 quandoerrors>0". A fórmula atual (Race.tsx:56-57)Math.round((1 - errors/totalKeystrokes)*100)retorna 100 com erro:errors=1, totalKeystrokes≥200→99.5…→Math.round→100. É exatamente o bug Engine/Honestidade: "Precisão 100%" coexiste com "Erros 1" — arredondamento mascara erro e rótulo confunde #20 ("Precisão 100%" + "Erros 1"). Cópia 1:1 → o AC de honestidade falha; satisfazer o AC → viola o "zero mudança". Não há como ter os dois.computeAccuracycom guarda explícita:const raw = totalKeystrokes===0 ? 100 : (1 - errors/totalKeystrokes)*100; const r = Math.round(raw); return errors>0 ? Math.min(r, 99) : r;(clamp[0,100]). AlternativaMath.floortambém mata o mascaramento mas mexe em mais valores; omin(…,99)é o mínimo que satisfaz o AC preservando o resto. Só a accuracy muda; WPM/progress/correctChars ficam 1:1.Closes #20(ou linkar).Race.tsxe a cópia emvalidate-metrics.mjs(o próprio issue aponta). Numa fatia futura,validate-metrics.mjspode importar demetrics.ts; nesta, manter o script verde.correctCharsfica nouseMemoatual (Race.tsx:49-53) — manter. Funções chamadas em render/useMemocomo hoje; não introduzir loop/alocação novos por keystroke.handleInput(l.74-96) conta erros inline — pode ficar como está (fora do escopo) ou extraircountNewErrors(prev,next,code)puro para testar a regra "backspace não pontua" (l.90-92); opcional, alta densidade de teste.Plano sugerido:
src/lib/metrics.ts:countCorrectChars/computeWpm/computeProgress(1:1 deRace.tsx:49-58) +computeAccuracycom o guard de honestidade acima. Puras, sem DOM.Race.tsx: importar e usar;correctCharssegue emuseMemo; UI idêntica exceto accuracy honesta.src/lib/metrics.test.ts(Vitest de Testes: instalar Vitest + primeiro suite na fronteira anti-cheat (sanitizeResults) e rodar no CI #31): pinar invariantes — WPM só conta corretos eelapsedMin≤0.001→0; accuracy nunca 100 com erro (errors=1,ks=201),ks=0→100, clamp[0,100]; correctChars não estoura; progresscodeLen=0→0, clamp[0,1].typecheck && build && test && node scripts/validate-metrics.mjsverde.Dependência: precisa do Vitest de #31 mergeado para o
.test.tsrodar no gate; se ainda não, incluir config mínima do Vitest OU pinar as invariantes novalidate-metrics.mjsprovisoriamente (sem duplicar harness em definitivo).caioross commentedon Jul 19, 2026
Resolvida na PR #48 (
refactor(engine), Closes #32).Entregue:
src/lib/metrics.ts(fonte única de verdade —countCorrectChars/computeWpm/computeAccuracy/computeProgress, extração 1:1) +src/lib/metrics.test.ts(13 testes de honestidade);Race.tsximporta e usa, comcorrectCharsseguindo nouseMemo(hot path intacto).Divergência do Parecer (declarada): a contradição "1:1 × nunca 100 com erro" não se materializou — a correção de honestidade da #20 já estava inline na
main(Math.floor+ teto de 99%). Portanto é refactor puro, zero mudança de número na tela; os testes só pinam o comportamento vigente. SemCloses #20.Gate: typecheck ✅ · build ✅ · test 34/34 ✅ · validate-metrics 37/0 ✅ · validate-persistence 33/0 ✅ · lint N/A.
refactor(engine): extrai métricas puras de Race.tsx para src/lib/metr…
fix(rooms): reset não depende de kicked_ids e kick falha alto sem a m…
refactor(practice): TypingCore e /api/snippet reusam as libs já na main