Skip to content

fix(metrics): count only hand-typed attempts; SPM over the text's characters - #43

Draft
prodelt wants to merge 1 commit into
mainfrom
fix/anti-cheat-spm
Draft

prodelt wants to merge 1 commit into
mainfrom
fix/anti-cheat-spm

Conversation

@prodelt

@prodelt prodelt commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Why

A jury-style audit (COMPETITION_RULES §4.4.4, §6.1, rubric B1, E1) found:

  1. Burst input counted as a test. One beforeinput (insertText) with the whole 59-character line gave «+10 XP, Вправу опановано» at 9 949 SPM; an automation tool's type_text (73 chars in 1.0 s, 4 264 SPM) counted too and became the personal best.
  2. SPM counted wrong keystrokes. With k = 7 errors on 59 characters SPM read about 12 % above 60·N/T, outside the jury's ±10 %.

What

  • SPM = 60 000 × min(correctCharKeystrokes, textLength) ÷ elapsedMs, i.e. 60·N/T on a finished attempt. A wrong key costs accuracy and time, never adds speed. Accuracy is unchanged (correct ÷ all, corrections included).
  • Seam: an insertText carrying more than one character is ignored (reason burst), like a paste. IME compositions are still judged character by character.
  • Plausibility verdict (packages/metrics/src/plausibility.ts, pure, unit-tested): metrics.implausible = 'burst' when the median interval between character keystrokes is < 25 ms, 'tooFast' when SPM > 1 500. Intervals run char to char, so a held Shift or Backspace repeat does not shorten them.
  • Counts toward nothing: curriculum's typedByHand is the one predicate. The progress fold (history, Confidence, Mastery Rule, which it neither advances nor resets), Academy progress, XP/level, streak and daily minutes, personal best (result comparison, profile), speed trend and session sizing skip such attempts. The result screen says «Не зараховано: набір не схожий на ручний» with the reason. finish-race maps the same verdict to too_fast / implausible_interval (takes effect on the next functions deploy). DERIVED_VERSION is 2.
  • Docs: Formulas page (uk + en: new SPM formula, new block «Набір руками»), ADR 0003 amendment dated 2026-10-06, CONTEXT.md (SPM, «Typed by Hand»), docs/security.md.
  • Tests: the e2e harness types at 45 ms a key (about 1 300 SPM), and the CDP harness waits the same by default. No test-only escape exists in the product.

Thresholds and why

  • 1 500 SPM ceiling: Klavogonki treats > 1 500 зн/хв as certain cheating and its top rank starts at 800 (docs/research/02-typing-trainers.md); Barbara Blackburn's record peak is about 1 060; finish-race already used 1 500.
  • 25 ms median interval: a sustained 2 400 SPM. A record typist's median sits near 50 ms, while bursts, compositions and scripted event series land at 0–15 ms (the audit's tool: 14 ms). A median, so a human's occasional near-zero rollover or an IME word committed whole does not tip it.

Checks

  • pnpm check: typecheck and lint are green; unit tests 835 passed. The one failure is the existing tools/architecture.test.ts (CoachMarks.tsx), which another PR fixes.
  • pnpm build: green, initial JS within the 150 KB budget (index 123.4 KB gzip).
  • e2e chromium, CI=1: formulas, jury, security -g "paste|counts for nothing" (new: an instant per-character series and a whole-line composition complete but are not counted): 12/12 passed. Harness consumers path-progress, stage2, academy, exercise, result, stage1-moves, review, session, groups, csp, settings, shell, plus real-input on cdp: all pass except failures that are already on main. Those are result.spec scenarios 4/5/6 and stage1-moves "weakest Transition" (both expect older coach copy) and the settings Profile axe audit (contrast of unearned badges).

Not here

  • submit-attempt keeps its own older, stricter checks (a quarter of intervals < 40 ms or > 1 000 SPM rejects the upload). It stores the new verdict with the recomputed metrics.
  • features/daily, features/sprint, features/home and features/owntext are owned by other PRs and were not touched. The sprint's own best and the daily challenge's "cleared" days do not read the verdict yet.

🤖 Generated with Claude Code

… characters

A jury-style audit found that one beforeinput carrying the whole line was
graded as a passing test (9 949 SPM, "exercise mastered"), and that an
automation tool's 4 264 SPM became the personal best. It also found that
SPM counted wrong keystrokes, so 7 errors on a 59-character line read 12%
above 60·N/T, outside the jury's ±10%.

- SPM = 60 000 × min(correct character keystrokes, text length) / elapsed
  ms, which is 60·N/T on a finished attempt. Accuracy is unchanged.
- The input seam takes an insertText of several characters as ignored
  (reason "burst"), like a paste. Compositions are still judged per char.
- packages/metrics gives each attempt a verdict, metrics.implausible:
  "burst" when the median interval between character keystrokes is under
  25 ms, "tooFast" above 1 500 SPM. Such an attempt is kept and shown as
  "Не зараховано: набір не схожий на ручний", but counts toward nothing:
  the progress fold, Academy progress, XP and level, streak and daily
  minutes, personal best, speed trend and session sizing all skip it
  through curriculum's typedByHand. finish-race reads the same verdict.
  DERIVED_VERSION is 2.
- The Formulas page (uk, en), ADR 0003 (amendment), CONTEXT.md and
  docs/security.md state both rules.
- The e2e harness types at 45 ms a key (about 1 300 SPM) and the CDP
  harness waits the same by default; the product has no test escape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
typing-race Ready Ready Preview Oct 6, 2026 5:33am UTC

prodelt added a commit that referenced this pull request Oct 6, 2026
… best

The plausibility verdict from #43 now reaches the two free-practice modes
that keep their own records: a daily attempt flagged implausible clears no
day, and a sprint score carrying a verdict sets no personal best.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — c18ad5fa Deployed Oct 6, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant