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) / elapsedMin arredondado (Race.tsx:55)
accuracy — (1 - errors/totalKeystrokes) * 100 com 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.ts com funções puras e determinísticas, extraídas 1:1 de Race.tsx (sem mudar fórmula nem arredondamento):
countCorrectChars(typed: string, code: string): number
computeWpm(correctChars: number, elapsedMin: number): number
computeAccuracy(errors: number, totalKeystrokes: number): number
computeProgress(typedLen: number, codeLen: number): number
Race.tsx passa a importar e usar essas funções (nenhuma mudança de comportamento observável — mesmos números na tela).
- Suite
src/lib/metrics.test.ts cobrindo as invariantes de honestidade.
Acceptance criteria (verificáveis)
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 em validate-metrics.mjs pode passar a importar de metrics.ts numa fatia futura).
- Área sagrada (HANDBOOK §2):
Race.tsx está no caminho de input da corrida (60fps). A extração deve ser puro refactor — as funções continuam chamadas dentro de useMemo/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).
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)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 parasrc/lib/e as cobre com testes reais, tornando a engine testável sem alterar comportamento.Escopo
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): numberRace.tsxpassa a importar e usar essas funções (nenhuma mudança de comportamento observável — mesmos números na tela).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
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).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.pnpm typecheck && pnpm build && pnpm test && node scripts/validate-metrics.mjs. PR normal (§7.3 — refactor + testes de engine com validate-metrics verde).